Skip to content

Commit

Permalink
feat: 499 better memory management (#606)
Browse files Browse the repository at this point in the history
* chore: memory management playground

* feat: recursively free cpu and gpu memory allocation on remove

* chore: clumsy attempt to dispose on unmount

* chore: lint fix

* feat: remove scene root on disposal

* chore: fix lint

* docs: added disposal guide on `performance` docs
  • Loading branch information
alvarosabu authored Apr 24, 2024
1 parent 5f20467 commit e98ca6d
Show file tree
Hide file tree
Showing 14 changed files with 215 additions and 49 deletions.
3 changes: 2 additions & 1 deletion docs/.vitepress/config/en.ts
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,8 @@ export const enConfig: LocaleSpecificConfig<DefaultTheme.Config> = {

items: [
{ text: 'Extending', link: '/advanced/extending' },
{ text: 'Primitive', link: '/advanced/primitive' },
{ text: 'Primitives', link: '/advanced/primitive' },
{ text: 'Scaling Performance 🚀', link: '/advanced/performance' },
{
text: 'Caveats',
link: '/advanced/caveats',
Expand Down
31 changes: 31 additions & 0 deletions docs/advanced/performance.md
Original file line number Diff line number Diff line change
Expand Up @@ -124,3 +124,34 @@ const { advance } = useTres()
advance()
</script>
```

## Dispose resources `dispose()` <Badge type="tip" text="^4.0.0" />

When you are done with a resource, like a texture, geometry, or material, you should dispose of it to free up memory. This is especially important when you are creating and destroying resources frequently, like in a game.

TresJS will automatically dispose of resources recursively when the component is unmounted, but you can also perform this manually by calling the `dispose()` directly from the package:

::: warning
To avoid errors and unwanted sideeffects, resources created programatically with the use of `primitives` need to be manually disposed.
:::

```html {2,12}
<script setup lang="ts">
import { dispose } from '@tresjs/core'
import { useGLTF } from '@tresjs/cientos'
const { nodes } = await useGLTF(
'https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb',
{ draco: true },
)
const model = nodes.Cube
onUnmounted(() => {
dispose(model)
})
</script>

<template>
<primitive :object="model" />
</template>
```
26 changes: 4 additions & 22 deletions playground/components.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ export {}
declare module 'vue' {
export interface GlobalComponents {
AkuAku: typeof import('./src/components/AkuAku.vue')['default']
AnimatedModel: typeof import('./src/components/AnimatedModel.vue')['default']
BlenderCube: typeof import('./src/components/BlenderCube.vue')['default']
<<<<<<< HEAD
DynamicModel: typeof import('./src/components/DynamicModel.vue')['default']
=======
Box: typeof import('./src/components/Box.vue')['default']
CameraOperator: typeof import('./src/components/CameraOperator.vue')['default']
Cameras: typeof import('./src/components/Cameras.vue')['default']
Expand All @@ -21,36 +23,16 @@ declare module 'vue' {
EventsPropogation: typeof import('./src/components/EventsPropogation.vue')['default']
FBXModels: typeof import('./src/components/FBXModels.vue')['default']
Gltf: typeof import('./src/components/gltf/index.vue')['default']
>>>>>>> v4
GraphPane: typeof import('./src/components/GraphPane.vue')['default']
LocalOrbitControls: typeof import('./src/components/LocalOrbitControls.vue')['default']
MeshWobbleMaterial: typeof import('./src/components/meshWobbleMaterial/index.vue')['default']
MultipleCanvas: typeof import('./src/components/MultipleCanvas.vue')['default']
PortalJourney: typeof import('./src/components/portal-journey/index.vue')['default']
RenderingLogger: typeof import('./src/components/RenderingLogger.vue')['default']
Responsiveness: typeof import('./src/components/Responsiveness.vue')['default']
RouterLink: typeof import('vue-router')['RouterLink']
RouterView: typeof import('vue-router')['RouterView']
ShadersExperiment: typeof import('./src/components/shaders-experiment/index.vue')['default']
TestSphere: typeof import('./src/components/TestSphere.vue')['default']
Text3D: typeof import('./src/components/Text3D.vue')['default']
TheBasic: typeof import('./src/components/TheBasic.vue')['default']
TheCameraOperator: typeof import('./src/components/TheCameraOperator.vue')['default']
TheConditional: typeof import('./src/components/TheConditional.vue')['default']
TheEnvironment: typeof import('./src/components/TheEnvironment.vue')['default']
TheEvents: typeof import('./src/components/TheEvents.vue')['default']
TheExperience: typeof import('./src/components/TheExperience.vue')['default']
TheFireFlies: typeof import('./src/components/portal-journey/TheFireFlies.vue')['default']
TheFirstScene: typeof import('./src/components/TheFirstScene.vue')['default']
TheGizmos: typeof import('./src/components/TheGizmos.vue')['default']
TheGroups: typeof import('./src/components/TheGroups.vue')['default']
TheModel: typeof import('./src/components/gltf/TheModel.vue')['default']
TheParticles: typeof import('./src/components/TheParticles.vue')['default']
ThePortal: typeof import('./src/components/portal-journey/ThePortal.vue')['default']
TheSmallExperience: typeof import('./src/components/TheSmallExperience.vue')['default']
TheSphere: typeof import('./src/components/TheSphere.vue')['default']
TheUSDZModel: typeof import('./src/components/udsz/TheUSDZModel.vue')['default']
TresLechesTest: typeof import('./src/components/TresLechesTest.vue')['default']
Udsz: typeof import('./src/components/udsz/index.vue')['default']
VectorSetProps: typeof import('./src/components/VectorSetProps.vue')['default']
}
}
2 changes: 1 addition & 1 deletion playground/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vue-tsc && vite build",
"build": "vite build",
"preview": "vite preview"
},
"dependencies": {
Expand Down
18 changes: 15 additions & 3 deletions playground/src/components/BlenderCube.vue
Original file line number Diff line number Diff line change
@@ -1,15 +1,27 @@
<script setup lang="ts">
import { useTresContext } from '@tresjs/core'
import { dispose } from '@tresjs/core'
import { useGLTF } from '@tresjs/cientos'
import { useControls } from '@tresjs/leches'
const { nodes } = await useGLTF('https://raw.githubusercontent.com/Tresjs/assets/main/models/gltf/blender-cube.glb', { draco: true })
const model = nodes.Cube
model.position.set(0, 1, 0)
const state = useTresContext()
useControls({
disposeBtn: {
label: 'Dispose',
type: 'button',
onClick: () => {
dispose(model)
},
size: 'sm',
},
})
state.invalidate()
onUnmounted(() => {
dispose(model)
})
</script>

<template>
Expand Down
1 change: 0 additions & 1 deletion playground/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,5 +7,4 @@ import 'uno.css'
const app = createApp(App)

app.use(router)

app.mount('#app')
4 changes: 2 additions & 2 deletions playground/src/pages/basic/index.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ const sphereExists = ref(true)
@pointer-out="onPointerOut"
>
<TresSphereGeometry :args="[2, 32, 32]" />
<TresMeshToonMaterial color="teal" />
<TresMeshBasicMaterial color="teal" />
</TresMesh>
</TresGroup>

Expand All @@ -84,7 +84,7 @@ const sphereExists = ref(true)
receive-shadow
>
<TresPlaneGeometry :args="[10, 10, 10, 10]" />
<TresMeshToonMaterial />
<TresMeshBasicMaterial />
</TresMesh>

<TresDirectionalLight
Expand Down
2 changes: 1 addition & 1 deletion playground/src/pages/empty.vue
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<script setup>
<script setup lang="ts">
</script>

Expand Down
65 changes: 65 additions & 0 deletions playground/src/pages/perf/Memory.vue
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import { BasicShadowMap, NoToneMapping, SRGBColorSpace } from 'three'
import { TresLeches, useControls } from '@tresjs/leches'
import '@tresjs/leches/styles'
const gl = {
clearColor: '#fff',
shadows: true,
alpha: false,
shadowMapType: BasicShadowMap,
outputColorSpace: SRGBColorSpace,
toneMapping: NoToneMapping,
}
const { isVisible } = useControls({
isVisible: true,
})
/* const mesh = new Mesh(
new BoxGeometry(),
new MeshToonMaterial({ color: 0x00ff00 }),
)
*/
onUnmounted(() => {
// dispose(mesh)
})
</script>

<template>
<RouterLink to="/basic">
Go to another page
</RouterLink>
<TresLeches />
<TresCanvas v-bind="gl">
<TresPerspectiveCamera
:position="[3, 3, 3]"
:look-at="[0, 0, 0]"
/>
<TresGroup v-if="isVisible">
<TresMesh :position="[0, 0, 0]">
<TresBoxGeometry />
<TresMeshToonMaterial :color="0x00FF00" />
</TresMesh>
</TresGroup>
<!-- <Suspense> -->
<!-- <BlenderC -->ube v-if="isVisible" />
<!-- </Suspense> -->
<!-- <TresMesh :position="[0,0,0]" v-if="isVisible">
<TresBoxGeometry />
<TresMeshToonMaterial :color="0x00ff00" />
</TresMesh> -->
<!-- <TresGridHelper /> -->
<!-- <TresGroup v-if="isVisible">
<TresMesh :position="[0,0,0]" >
<TresBoxGeometry />
<TresMeshToonMaterial :color="0x00ff00" />
</TresMesh>
</TresGroup> -->

<TresAmbientLight :intensity="1" />
</TresCanvas>
</template>
5 changes: 5 additions & 0 deletions playground/src/router/routes/performance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,4 +4,9 @@ export const perfRoutes = [
name: 'On Demand',
component: () => import('../../pages/perf/OnDemand.vue'),
},
{
path: '/perf/memory',
name: 'Memory',
component: () => import('../../pages/perf/Memory.vue'),
},
]
31 changes: 29 additions & 2 deletions src/components/TresCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ import {
getCurrentInstance,
h,
onMounted,
onUnmounted,
provide,
ref,
shallowRef,
Expand All @@ -33,10 +34,11 @@ import {
} from '../composables'
import { extend } from '../core/catalogue'
import { nodeOps } from '../core/nodeOps'
import { registerTresDevtools } from '../devtools'
import { disposeObject3D } from '../utils/'
import type { RendererPresetsType } from '../composables/useRenderer/const'
import type { TresCamera, TresObject } from '../types/'
import { registerTresDevtools } from '../devtools'
export interface TresCanvasProps
extends Omit<WebGLRendererParameters, 'canvas'> {
Expand Down Expand Up @@ -134,14 +136,34 @@ const mountCustomRenderer = (context: TresContext) => {
}
const dispose = (context: TresContext, force = false) => {
scene.value.children = []
disposeObject3D(context.scene.value)
if (force) {
context.renderer.value.dispose()
context.renderer.value.renderLists.dispose()
context.renderer.value.forceContextLoss()
}
scene.value.__tres = {
root: context,
}
mountCustomRenderer(context)
resume()
/* disposeObject3D(scene.value) */
/* scene.value.children.forEach((child) => {
child.removeFromParent()
disposeObject3D(child)
})
context.scene.value.children.forEach((child) => {
child.removeFromParent()
disposeObject3D(child)
}) */
/* console.log('disposing', scene.value.children)
if (force) {
context.renderer.value.dispose()
context.renderer.value.renderLists.dispose()
context.renderer.value.forceContextLoss()
}
mountCustomRenderer(context)
resume() */
}
const disableRender = computed(() => props.disableRender)
Expand Down Expand Up @@ -210,8 +232,13 @@ onMounted(() => {
addDefaultCamera()
}
// HMR support
if (import.meta.hot && context.value) { import.meta.hot.on('vite:afterUpdate', () => dispose(context.value as TresContext)) }
})
onUnmounted(() => {
dispose(context.value as TresContext)
})
</script>

<template>
Expand Down
27 changes: 13 additions & 14 deletions src/core/nodeOps.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@ import { isFunction } from '@alvarosabu/utils'
import type { Camera, Object3D } from 'three'
import type { TresContext } from '../composables'
import { useLogger } from '../composables'
import { deepArrayEqual, isHTMLTag, kebabToCamel } from '../utils'
import { deepArrayEqual, disposeObject3D, isHTMLTag, kebabToCamel } from '../utils'
import type { TresObject, TresObject3D, TresScene } from '../types'
import { catalogue } from './catalogue'

Expand Down Expand Up @@ -145,24 +145,14 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()
}
}
}

function remove(node) {
if (!node) { return }
const ctx = node.__tres
// remove is only called on the node being removed and not on child nodes.
node.parent = node.parent || scene

if (node.isObject3D) {
const disposeMaterialsAndGeometries = (object3D: TresObject) => {
const tresObject3D = object3D as TresObject3D
// TODO: to be improved on https://github.com/Tresjs/tres/pull/466/files
if (ctx.disposable) {
tresObject3D.material?.dispose()
tresObject3D.material = undefined
tresObject3D.geometry?.dispose()
tresObject3D.geometry = undefined
}
}

const deregisterCameraIfRequired = (object: Object3D) => {
const deregisterCamera = node.__tres.root.deregisterCamera

Expand All @@ -171,19 +161,28 @@ export const nodeOps: () => RendererOptions<TresObject, TresObject | null> = ()

node.removeFromParent?.()

// Remove nested child objects. Primitives should not have objects and children that are
// attached to them declaratively ...

node.traverse((child: Object3D) => {
disposeMaterialsAndGeometries(child as TresObject)
deregisterCameraIfRequired(child)
// deregisterAtPointerEventHandlerIfRequired?.(child as TresObject)
if (child.onPointerMissed) {
ctx.root.eventManager.deregisterPointerMissedObject(child)
}
})

disposeMaterialsAndGeometries(node)
deregisterCameraIfRequired(node as Object3D)
/* deregisterAtPointerEventHandlerIfRequired?.(node as TresObject) */
invalidateInstance(node as TresObject)

// Dispose the object if it's disposable, primitives needs to be manually disposed by
// calling dispose from `@tresjs/core` package like this `dispose(model)`
const isPrimitive = node.__tres.primitive

if (!isPrimitive && node.__tres.disposable) {
disposeObject3D(node as TresObject3D)
}
node.dispose?.()
}
}
Expand Down
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import type { App } from 'vue'
import TresCanvas from './components/TresCanvas.vue'
import { normalizeColor, normalizeVectorFlexibleParam } from './utils/normalize'
import templateCompilerOptions from './utils/template-compiler-options'
import { disposeObject3D as dispose } from './utils'

export * from './composables'
export * from './core/catalogue'
Expand Down Expand Up @@ -30,4 +31,5 @@ export {
normalizeColor,
normalizeVectorFlexibleParam,
templateCompilerOptions,
dispose,
}
Loading

0 comments on commit e98ca6d

Please sign in to comment.