Explorar el Código

refactor: improve memory test and object disposal

alvarosabu hace 3 meses
padre
commit
009b4baae7

+ 1 - 0
.gitignore

@@ -28,3 +28,4 @@ docs/.vitepress/cache/
 
 .env
 .env.local
+reports/

+ 31 - 28
playground/vue/src/pages/advanced/MemoryTresObjects.vue

@@ -1,63 +1,66 @@
 <script setup lang="ts">
 import { TresCanvas } from '@tresjs/core'
+import { onUnmounted, ref } from 'vue'
 
-const toggleMax = 1000
+const toggleMax = 400
 const numObjectsMax = 2000
-const startTimeMS = Date.now()
+const startTimeMS = ref(0)
 
 const toggleCount = ref(0)
 const show = ref(false)
-const msg = ref('Test is running.')
+const msg = ref('Click Start Test to begin.')
 const r = ref(null)
-const isPaused = ref(true)
+const isStarted = ref(false)
 
-let intervalId: ReturnType<typeof setInterval>
+let rafId: number
 
-const startInterval = () => {
-  intervalId = setInterval(() => {
+const startTest = () => {
+  isStarted.value = true
+  startTimeMS.value = Date.now()
+  msg.value = 'Test is running...'
+  show.value = true // Start by showing the canvas
+
+  const tick = () => {
     if (toggleCount.value < toggleMax) {
-      // NOTE: Make sure that objects are mounted by
-      // checking `!!r.value`.
-      if (r.value) {
+      if (r.value && show.value) {
         show.value = false
         toggleCount.value++
       }
-      else {
+      else if (!show.value) {
         show.value = true
       }
+      rafId = requestAnimationFrame(tick)
     }
     else {
-      clearInterval(intervalId)
-      const elapsedSec = (Date.now() - startTimeMS) / 1000
+      const elapsedSec = (Date.now() - startTimeMS.value) / 1000
       msg.value = `Test completed in ${elapsedSec} seconds.`
     }
-  }, 1000 / 120)
-}
-
-const togglePause = () => {
-  isPaused.value = !isPaused.value
-  if (!isPaused.value) {
-    startInterval()
-  }
-  else {
-    clearInterval(intervalId)
   }
+
+  rafId = requestAnimationFrame(tick)
 }
 
-onUnmounted(() => clearInterval(intervalId))
+onUnmounted(() => {
+  if (rafId) {
+    cancelAnimationFrame(rafId)
+  }
+})
 </script>
 
 <template>
   <OverlayInfo>
     <h1>Memory test: Tres Objects</h1>
     <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 }} TresCanvas instances with {{ numObjectsMax }} objects each.</p>
     <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' }}
+    <button
+      v-if="!isStarted"
+      style="padding: 8px 16px; margin-top: 10px;"
+      @click="startTest"
+    >
+      Start Test
     </button>
   </OverlayInfo>
   <div v-if="show" style="width: 90%; height: 90%; border: 1px solid #F00">

+ 7 - 4
src/components/TresCanvas.vue

@@ -164,6 +164,13 @@ const dispose = (context: TresContext, force = false) => {
     context.renderer.value.dispose()
     context.renderer.value.renderLists.dispose()
     context.renderer.value.forceContextLoss()
+
+    // Clear WebGL context
+    const gl = context.renderer.value.getContext()
+    if (gl) {
+      const loseContext = gl.getExtension('WEBGL_lose_context')
+      loseContext?.loseContext()
+    }
   }
   (scene.value as TresScene).__tres = {
     root: context,
@@ -234,10 +241,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()
   }
 

+ 2 - 0
src/composables/useRenderer/index.ts

@@ -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()
 
@@ -170,6 +171,7 @@ export function useRenderer(
       outputColorSpace: plainRenderer.outputColorSpace,
     }
     plainRenderer.dispose()
+    plainRenderer.forceContextLoss()
 
     return defaults
   }

+ 58 - 21
src/utils/index.ts

@@ -1,8 +1,8 @@
 import type { nodeOps } from 'src/core/nodeOps'
 import type { AttachType, LocalState, TresInstance, TresObject, TresPrimitive } from 'src/types'
-import type { Material, Mesh, Object3D, Texture } from 'three'
+import type { Material, Object3D, Texture } from 'three'
+import { BufferAttribute, BufferGeometry, DoubleSide, InterleavedBufferAttribute, Light, Line, MathUtils, Mesh, MeshBasicMaterial, Points, Scene, Vector3 } from 'three'
 import type { TresContext } from '../composables/useTresContextProvider'
-import { DoubleSide, MathUtils, MeshBasicMaterial, Scene, Vector3 } from 'three'
 import { HightlightMesh } from '../devtools/highlight'
 import * as is from './is'
 
@@ -279,42 +279,79 @@ function hasMap(material: Material): material is Material & { map: Texture | nul
 export function disposeMaterial(material: Material): void {
   if (hasMap(material) && material.map) {
     material.map.dispose()
+    // material.map = null
   }
 
   material.dispose()
 }
 
 export function disposeObject3D(object: TresObject): void {
-  if (object.parent) {
-    object.removeFromParent?.()
-  }
-  delete object.__tres
   // Clone the children array to safely iterate
   const children = [...object.children]
   children.forEach(child => disposeObject3D(child))
 
-  if (object instanceof Scene) {
-    // Optionally handle Scene-specific cleanup
+  // Remove the object from its parent
+  if (object.parent) {
+    object.removeFromParent?.()
   }
-  else {
-    const mesh = object as unknown as Partial<Mesh>
-    if (object) {
-      object.dispose?.()
+
+  if (object instanceof Mesh || object instanceof Points || object instanceof Line) {
+    if (object.geometry) {
+      object.geometry.dispose()
+      // object.geometry = null
     }
-    if (mesh.geometry) {
-      mesh.geometry.dispose()
-      delete mesh.geometry
+    if (object.material) {
+      disposeMaterial(object.material)
+      // object.material = null
     }
+  }
 
-    if (Array.isArray(mesh.material)) {
-      mesh.material.forEach(material => disposeMaterial(material))
-      delete mesh.material
+  // Handle lights
+  if (object instanceof Light) {
+    if (object.shadow?.map) {
+      object.shadow.map.dispose()
     }
-    else if (mesh.material) {
-      disposeMaterial(mesh.material)
-      delete mesh.material
+  }
+
+  // Clean up any custom properties
+  if (object.userData) {
+    // Clear any custom user data that might hold references
+    object.userData = {}
+  }
+
+  // Clear any animations
+  if (object.animations) {
+    object.animations = []
+  }
+
+  // Clear buffers if present
+  if (object instanceof BufferGeometry) {
+    Object.values(object.attributes).forEach((attribute: BufferAttribute | InterleavedBufferAttribute) => {
+      if (attribute instanceof BufferAttribute && attribute.array) {
+        attribute.array = new Float32Array(0)
+      }
+      else if (attribute instanceof InterleavedBufferAttribute && attribute.data?.array) {
+        attribute.data.array = new Float32Array(0)
+      }
+    })
+    object.attributes = {}
+  }
+
+  if (object instanceof Scene) {
+    object.background = null
+    object.environment = null
+
+    if (object.fog) {
+      object.fog = null
     }
   }
+
+  // Remove tres state
+  delete object.__tres
+
+  if (object.clear) {
+    object.clear()
+  }
 }
 
 /**