Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: 499 better memory management #606

Merged
merged 12 commits into from
Apr 24, 2024
Merged
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
Loading