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

fix: 912-helpers-disposal-issue #913

Closed
wants to merge 8 commits into from
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ docs/.vitepress/cache/

.env
.env.local
reports/
175 changes: 133 additions & 42 deletions playground/vue/src/pages/advanced/MemoryTresObjects.vue
Original file line number Diff line number Diff line change
@@ -1,72 +1,163 @@
<script setup lang="ts">
import { TresCanvas } from '@tresjs/core'
import type { Group, Material } from 'three'
import { BoxGeometry, Color, Mesh, MeshBasicMaterial, PerspectiveCamera, Scene, Vector3, WebGLRenderer } from 'three'
import { onUnmounted, ref } from 'vue'

const toggleMax = 1000
const toggleMax = 400
const numObjectsMax = 2000
const startTimeMS = Date.now()

const startTimeMS = ref(0)
const width = 900
const height = 600
const toggleCount = ref(0)
const show = ref(false)
const msg = ref('Test is running.')
const showTres = ref(false)
const showVueThree = ref(false)
const msg = ref('Click Start Test to begin.')
const r = ref(null)
const isPaused = ref(true)

let intervalId: ReturnType<typeof setInterval>

const startInterval = () => {
intervalId = setInterval(() => {
const testVueThree = (() => {
let renderer: WebGLRenderer | null = null
let scene: Scene | null = null
let camera: PerspectiveCamera | null = null
let frameCount = 0
function testVueThree() {
if (toggleCount.value < toggleMax) {
// NOTE: Make sure that objects are mounted by
// checking `!!r.value`.
if (r.value) {
show.value = false
toggleCount.value++
if (renderer) {
if (frameCount < 2) {
frameCount++
renderer.render(scene!, camera!)
}
else {
camera?.removeFromParent()
scene!.children.forEach((m) => { ((m as Mesh).material as Material).dispose(); (m as Mesh).geometry.dispose() })
renderer.dispose()
frameCount = 0
camera = null
scene = null
renderer = null
showVueThree.value = false
toggleCount.value++
}
}
else {
renderer = new WebGLRenderer({ canvas: r.value })
renderer.setSize(width, height)
renderer.setClearColor(new Color('#EEE'))
scene = new Scene()
camera = new PerspectiveCamera()
camera.position.x = 10
camera.position.y = 10
camera.lookAt(new Vector3(0, 0, 0))
for (let i = 0; i < numObjectsMax; i++) {
scene.add(new Mesh(new BoxGeometry(), new MeshBasicMaterial()))
}
scene.add(camera)
renderer.render(scene, camera)
}
}
else {
show.value = true
else if (!showVueThree.value) {
showVueThree.value = true
}
}
else {
const elapsedSec = (Date.now() - startTimeMS.value) / 1000
msg.value = `Plain Vue/THREE test completed in ${elapsedSec} seconds.`
clearInterval(intervalId)
const elapsedSec = (Date.now() - startTimeMS) / 1000
msg.value = `Test completed in ${elapsedSec} seconds.`
}
}, 1000 / 120)
}

const togglePause = () => {
isPaused.value = !isPaused.value
if (!isPaused.value) {
startInterval()
}
else {
clearInterval(intervalId)
return testVueThree
})()
const testTres = (() => {
let frameCount = 0
return () => {
if (toggleCount.value < toggleMax) {
if (r.value && frameCount < 2) {
// NOTE: Wait until Tres has actually rendered before
// removing the canvas.
((r.value as Group).children[0] as Mesh).onAfterRender = () => { frameCount++ }
}
else {
if (frameCount < 1) {
showTres.value = true
}
else {
toggleCount.value++
showTres.value = false
frameCount = 0
}
}
}
else {
const elapsedSec = (Date.now() - startTimeMS.value) / 1000
msg.value = `Tres test completed in ${elapsedSec} seconds.`
clearInterval(intervalId)
}
}
})()
const isStarted = ref(false)
const startTestTres = () => {
isStarted.value = true
startTimeMS.value = Date.now()
// NOTE: Using `setInterval`; it typically will keep
// running in situations were `requestAnimationFrame` will pause.
intervalId = setInterval(testTres, 1000 / 60)
msg.value = 'Test is running...'
}

onUnmounted(() => clearInterval(intervalId))
const startTestVueThree = () => {
isStarted.value = true
startTimeMS.value = Date.now()
// NOTE: Using `setInterval`; it typically will keep
// running in situations were `requestAnimationFrame` will pause.
intervalId = setInterval(testVueThree, 1000 / 60)
msg.value = 'Test is running...'
}
onUnmounted(() => {
clearInterval(intervalId)
})
</script>

<template>
<OverlayInfo>
<h1>Memory test: Tres Objects</h1>
<h1>Memory test: Canvases with objects – Tres vs Plain Vue/THREE</h1>
<p><span style="color: red">IMPORTANT</span> Epileptic warning: the tests run on this page cause the screen to flash rapidly.</p>
<h2>Setup</h2>
<p>This page will successively create and remove a TresCanvas containing a number of objects.</p>
<p>This test will create and remove {{ toggleMax }} canvas instances with {{ numObjectsMax }} objects/materials/geometries each.</p>
<h2>Note</h2>
<ul>
<li>These tests are intended to help spot memory leaks.</li>
<li>Faster/slower test duration does not indicate a problem.</li>
</ul>
<h2>Status</h2>
<p>{{ msg }}</p>
<p>Number of TresCanvases created: {{ toggleCount }} / {{ toggleMax }}</p>
<p>Number of Objects per TresCanvas: {{ numObjectsMax }}</p>
<button style="padding: 8px 16px; margin-top: 10px;" @click="togglePause">
{{ isPaused ? 'Start Test' : 'Pause Test' }}
<p>Number of canvases created: {{ toggleCount }} / {{ toggleMax }}</p>
<button
v-if="!isStarted"
style="padding: 8px 16px; margin-top: 10px;"
@click="startTestTres"
>
Start Tres test
</button>

<button
v-if="!isStarted"
style="padding: 8px 16px; margin-top: 10px;"
@click="startTestVueThree"
>
Start plain Vue/THREE test
</button>
</OverlayInfo>
<div v-if="show" style="width: 90%; height: 90%; border: 1px solid #F00">
<TresCanvas clear-color="black">
<TresGroup ref="r" />
<TresMesh v-for="_, i of Array.from({ length: numObjectsMax })" :key="i">
<TresMeshBasicMaterial />
<TresBoxGeometry />
</TresMesh>
<div v-if="showTres" :style="{ width: `${width}px`, height: `${height}px` }">
<TresCanvas clear-color="#EEE">
<TresGroup ref="r">
<TresMesh v-for="_, i of Array.from({ length: numObjectsMax })" :key="i">
<TresMeshBasicMaterial />
<TresBoxGeometry />
</TresMesh>
</TresGroup>
</TresCanvas>
</div>
<div v-if="showVueThree">
<canvas ref="r" clear-color="black"></canvas>
</div>
</template>
21 changes: 11 additions & 10 deletions src/components/TresCanvas.vue
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,6 @@ import {
import pkg from '../../package.json'
import {
type TresContext,
useLogger,
useTresContextProvider,
} from '../composables'
import { extend } from '../core/catalogue'
Expand Down Expand Up @@ -97,8 +96,6 @@ const slots = defineSlots<{
default: () => any
}>()

const { logWarning } = useLogger()

const canvas = ref<HTMLCanvasElement>()

/*
Expand Down Expand Up @@ -161,9 +158,17 @@ const mountCustomRenderer = (context: TresContext, empty = false) => {
const dispose = (context: TresContext, force = false) => {
disposeObject3D(context.scene.value as unknown as TresObject)
if (force) {
context.renderer.value.dispose()
context.renderer.value.renderLists.dispose()
context.renderer.value.forceContextLoss()
// Clear WebGL context first
const gl = context.renderer.value?.getContext()
if (gl) {
const loseContext = gl.getExtension('WEBGL_lose_context')
loseContext?.loseContext()
}

// Then dispose renderer
context.renderer.value?.dispose()
context.renderer.value?.renderLists.dispose()
context.renderer.value?.forceContextLoss()
}
(scene.value as TresScene).__tres = {
root: context,
Expand Down Expand Up @@ -234,10 +239,6 @@ onMounted(() => {
)

if (!camera.value) {
logWarning(
'No camera found. Creating a default perspective camera. '
+ 'To have full control over a camera, please add one to the scene.',
)
addDefaultCamera()
}

Expand Down
2 changes: 2 additions & 0 deletions src/composables/useRenderer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -157,6 +157,7 @@ export function useRenderer(

const { logError } = useLogger()

// TODO: This is a hack to get the defaults of the renderer. We should find a better way to do this.
const getThreeRendererDefaults = () => {
const plainRenderer = new WebGLRenderer()

Expand All @@ -170,6 +171,7 @@ export function useRenderer(
outputColorSpace: plainRenderer.outputColorSpace,
}
plainRenderer.dispose()
plainRenderer.forceContextLoss()

return defaults
}
Expand Down
4 changes: 3 additions & 1 deletion src/directives/vDistanceTo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export const vDistanceTo = {
},
unmounted: (el: TresObject) => {
arrowHelper?.dispose()
el.parent.remove(arrowHelper)
if (el.parent) {
el.parent.remove(arrowHelper)
}
},
}
6 changes: 3 additions & 3 deletions src/directives/vLightHelper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,11 +51,11 @@ export const vLightHelper = {
logWarning(`${el.type} is not a light`)
return
}
currentInstance = el.parent.children.find((child: TresObject) => child instanceof CurrentHelper)

if (currentInstance && currentInstance.dispose) {
currentInstance.dispose()
}
el.parent.remove(currentInstance)
if (el.parent) {
el.parent.remove(currentInstance)
}
},
}
Loading