@lightningjs/renderer 3.0.1 → 3.0.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (100) hide show
  1. package/README.md +56 -196
  2. package/dist/src/core/CoreNode.js +25 -4
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/CoreTextNode.d.ts +9 -2
  5. package/dist/src/core/CoreTextNode.js +32 -11
  6. package/dist/src/core/CoreTextNode.js.map +1 -1
  7. package/dist/src/core/CoreTextureManager.d.ts +8 -0
  8. package/dist/src/core/CoreTextureManager.js +13 -1
  9. package/dist/src/core/CoreTextureManager.js.map +1 -1
  10. package/dist/src/core/Stage.d.ts +8 -0
  11. package/dist/src/core/Stage.js +23 -0
  12. package/dist/src/core/Stage.js.map +1 -1
  13. package/dist/src/core/TextureMemoryManager.d.ts +8 -13
  14. package/dist/src/core/TextureMemoryManager.js +22 -27
  15. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  16. package/dist/src/core/lib/ImageWorker.d.ts +2 -2
  17. package/dist/src/core/lib/ImageWorker.js +31 -12
  18. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  19. package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
  20. package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
  21. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  22. package/dist/src/core/lib/textureCompression.js +19 -10
  23. package/dist/src/core/lib/textureCompression.js.map +1 -1
  24. package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
  25. package/dist/src/core/lib/validateImageBitmap.js +4 -4
  26. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  27. package/dist/src/core/platform.js +2 -2
  28. package/dist/src/core/platform.js.map +1 -1
  29. package/dist/src/core/platforms/Platform.d.ts +4 -0
  30. package/dist/src/core/platforms/Platform.js.map +1 -1
  31. package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
  32. package/dist/src/core/platforms/web/WebPlatform.js +13 -0
  33. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  34. package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
  35. package/dist/src/core/renderers/CoreRenderer.js +8 -0
  36. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  37. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
  38. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
  39. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  40. package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
  41. package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
  42. package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
  43. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +2 -0
  44. package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
  45. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  46. package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
  47. package/dist/src/core/text-rendering/CanvasFont.js +120 -0
  48. package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
  49. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +1 -2
  50. package/dist/src/core/text-rendering/CanvasTextRenderer.js +11 -19
  51. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  52. package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
  53. package/dist/src/core/text-rendering/CoreFont.js +48 -0
  54. package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
  55. package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
  56. package/dist/src/core/text-rendering/FontManager.js +41 -0
  57. package/dist/src/core/text-rendering/FontManager.js.map +1 -0
  58. package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
  59. package/dist/src/core/text-rendering/SdfFont.js +142 -0
  60. package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
  61. package/dist/src/core/text-rendering/SdfFontHandler.js +7 -5
  62. package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
  63. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +2 -2
  64. package/dist/src/core/text-rendering/SdfTextRenderer.js +141 -132
  65. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  66. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  67. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  68. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  69. package/dist/src/core/text-rendering/TextRenderer.d.ts +26 -20
  70. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  71. package/dist/src/core/text-rendering/Utils.js +3 -0
  72. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  73. package/dist/src/main-api/Renderer.d.ts +14 -0
  74. package/dist/src/main-api/Renderer.js +29 -3
  75. package/dist/src/main-api/Renderer.js.map +1 -1
  76. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  77. package/package.json +2 -1
  78. package/src/core/CoreNode.ts +29 -4
  79. package/src/core/CoreTextNode.test.ts +237 -0
  80. package/src/core/CoreTextNode.ts +53 -33
  81. package/src/core/CoreTextureManager.ts +14 -2
  82. package/src/core/Stage.ts +29 -0
  83. package/src/core/TextureMemoryManager.test.ts +134 -0
  84. package/src/core/TextureMemoryManager.ts +23 -30
  85. package/src/core/platforms/Platform.ts +5 -0
  86. package/src/core/platforms/web/WebPlatform.ts +13 -0
  87. package/src/core/renderers/CoreRenderer.ts +10 -0
  88. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  89. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  90. package/src/core/renderers/webgl/WebGlRenderer.ts +38 -28
  91. package/src/core/text-rendering/CanvasTextRenderer.ts +13 -41
  92. package/src/core/text-rendering/SdfFontHandler.ts +8 -5
  93. package/src/core/text-rendering/SdfTextRenderer.ts +166 -163
  94. package/src/core/text-rendering/TextRenderer.ts +23 -21
  95. package/src/core/text-rendering/Utils.ts +5 -1
  96. package/src/main-api/Renderer.test.ts +153 -0
  97. package/src/main-api/Renderer.ts +33 -3
  98. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  99. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  100. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
@@ -65,7 +65,9 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
65
65
 
66
66
  // SDF layout caching for performance
67
67
  private _cachedLayout: TextLayout | null = null;
68
- private _lastVertexBuffer: Float32Array | null = null;
68
+ // Mutable ref box shared with SdfTextRenderer so the renderer can write the
69
+ // created WebGLBuffer back into it, allowing reuse across frames.
70
+ private _sdfBufferRef: { current: WebGLBuffer | null } = { current: null };
69
71
 
70
72
  // Text renderer properties - stored directly on the node
71
73
  private textProps: CoreTextNodeProps;
@@ -113,6 +115,18 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
113
115
  this.setUpdateType(UpdateType.IsRenderable);
114
116
  };
115
117
 
118
+ /**
119
+ * Delete the cached WebGLBuffer held by the SDF renderer ref and reset the
120
+ * ref so the next renderQuads call allocates a fresh one.
121
+ * Safe to call from destroy() or on text change.
122
+ */
123
+ private releaseSdfBuffer(): void {
124
+ const buf = this._sdfBufferRef.current;
125
+ if (buf === null) return;
126
+ this.stage.renderer.deleteBuffer(buf);
127
+ this._sdfBufferRef.current = null;
128
+ }
129
+
116
130
  allowTextGeneration() {
117
131
  const p = this.props.parent;
118
132
  if (p === null) {
@@ -196,7 +210,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
196
210
  * Override CoreNode's update method to handle text-specific updates
197
211
  */
198
212
  override update(delta: number, parentClippingRect: RectWithValid): void {
199
- const hasValidText = this.textProps.text && this.textProps.text.length > 0;
213
+ const hasValidText =
214
+ typeof this.textProps.text === 'string' && this.textProps.text.length > 0;
200
215
 
201
216
  if (
202
217
  hasValidText === true &&
@@ -207,7 +222,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
207
222
  if (this.fontHandler.isFontLoaded(this.textProps.fontFamily) === true) {
208
223
  this._waitingForFont = false;
209
224
  this._cachedLayout = null; // Invalidate cached layout
210
- this._lastVertexBuffer = null; // Invalidate last vertex buffer
225
+ this.releaseSdfBuffer(); // Free the cached WebGLBuffer
211
226
  const resp = this.textRenderer.renderText(this.textProps);
212
227
  this.handleRenderResult(resp);
213
228
  this._layoutGenerated = true;
@@ -216,11 +231,13 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
216
231
  this._waitingForFont = true;
217
232
  }
218
233
  } else if (hasValidText === false) {
234
+ this.props.w = 0;
235
+ this.props.h = 0;
219
236
  // If text is invalid, ensure node is not renderable
220
237
  this.setRenderable(false);
221
238
  this._layoutGenerated = false;
222
239
  this._cachedLayout = null;
223
- this._lastVertexBuffer = null;
240
+ this.releaseSdfBuffer(); // Free the cached WebGLBuffer
224
241
  }
225
242
 
226
243
  // First run the standard CoreNode update
@@ -232,7 +249,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
232
249
  */
233
250
  override updateIsRenderable(): void {
234
251
  // Guard: Text nodes are never renderable without valid text
235
- const hasValidText = this.textProps.text && this.textProps.text.length > 0;
252
+ const hasValidText =
253
+ typeof this.textProps.text === 'string' && this.textProps.text.length > 0;
236
254
  if (hasValidText === false) {
237
255
  this.setRenderable(false);
238
256
  return;
@@ -335,37 +353,39 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
335
353
  }
336
354
 
337
355
  // Early return if no cached data
338
- if (!this._cachedLayout) {
356
+ if (this._cachedLayout === null) {
339
357
  return;
340
358
  }
341
359
 
342
- if (this._lastVertexBuffer === null) {
343
- this._lastVertexBuffer = this.textRenderer.addQuads(this._cachedLayout);
344
- }
345
-
346
360
  const props = this.textProps;
347
- this.textRenderer.renderQuads(
348
- renderer,
349
- this._cachedLayout as TextLayout,
350
- this._lastVertexBuffer!,
351
- {
352
- fontFamily: this.textProps.fontFamily,
353
- fontSize: props.fontSize,
354
- color: this.props.color || 0xffffffff,
355
- offsetY: props.offsetY,
356
- worldAlpha: this.worldAlpha,
357
- globalTransform: this.globalTransform!.getFloatArr(),
358
- clippingRect: this.clippingRect,
359
- width: this.props.w,
360
- height: this.props.h,
361
- parentHasRenderTexture: this.parentHasRenderTexture,
362
- framebufferDimensions:
363
- this.parentHasRenderTexture === true
364
- ? this.parentFramebufferDimensions
365
- : null,
366
- stage: this.stage,
367
- },
368
- );
361
+ this.textRenderer.renderQuads(renderer, this._cachedLayout as TextLayout, {
362
+ fontFamily: this.textProps.fontFamily,
363
+ fontSize: props.fontSize,
364
+ color: this.props.color || 0xffffffff,
365
+ offsetY: props.offsetY,
366
+ worldAlpha: this.worldAlpha,
367
+ globalTransform: this.globalTransform!.getFloatArr(),
368
+ clippingRect: this.clippingRect,
369
+ width: this.props.w,
370
+ height: this.props.h,
371
+ parentHasRenderTexture: this.parentHasRenderTexture,
372
+ framebufferDimensions:
373
+ this.parentHasRenderTexture === true
374
+ ? this.parentFramebufferDimensions
375
+ : null,
376
+ stage: this.stage,
377
+ glBufferRef: this._sdfBufferRef,
378
+ });
379
+ }
380
+
381
+ override updateRenderState(renderState: CoreNodeRenderState): void {
382
+ super.updateRenderState(renderState);
383
+ if (
384
+ this._type === 'sdf' &&
385
+ renderState === CoreNodeRenderState.OutOfBounds
386
+ ) {
387
+ this.releaseSdfBuffer();
388
+ }
369
389
  }
370
390
 
371
391
  override destroy(): void {
@@ -375,7 +395,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
375
395
 
376
396
  // Clear cached layout and vertex buffer
377
397
  this._cachedLayout = null;
378
- this._lastVertexBuffer = null;
398
+ this.releaseSdfBuffer(); // Delete the cached WebGLBuffer before losing stage ref
379
399
 
380
400
  this.fontHandler = null!; // Clear reference to avoid memory leaks
381
401
  this.textRenderer = null!; // Clear reference to avoid memory leaks
@@ -289,8 +289,6 @@ export class CoreTextureManager extends EventEmitter {
289
289
  * @param immediate - Whether to prioritize the texture for immediate loading
290
290
  */
291
291
  async loadTexture(texture: Texture, priority?: boolean): Promise<void> {
292
- this.stage.txMemManager.removeFromOrphanedTextures(texture);
293
-
294
292
  if (texture.type === TextureType.subTexture) {
295
293
  // ignore subtextures - they get loaded through their parent
296
294
  return;
@@ -464,6 +462,20 @@ export class CoreTextureManager extends EventEmitter {
464
462
  }
465
463
  }
466
464
 
465
+ /**
466
+ * Destroy the CoreTextureManager and release all internal references.
467
+ *
468
+ * @remarks
469
+ * Clears the upload queue and key/inverse-key caches so that queued
470
+ * textures can be garbage-collected after a renderer teardown.
471
+ */
472
+ destroy(): void {
473
+ this.uploadTextureQueue = [];
474
+ this.keyCache.clear();
475
+ // inverseKeyCache is a WeakMap – entries will be GC'd automatically once
476
+ // the texture objects themselves are no longer referenced.
477
+ }
478
+
467
479
  /**
468
480
  * Resolve a parent texture from the cache or fallback to the provided texture.
469
481
  *
package/src/core/Stage.ts CHANGED
@@ -882,6 +882,35 @@ export class Stage {
882
882
  this.txMemManager.cleanup(full);
883
883
  }
884
884
 
885
+ /**
886
+ * Destroy the stage and release all resources.
887
+ *
888
+ * @remarks
889
+ * This method stops the render loop, destroys all nodes, releases all
890
+ * textures and GPU resources, and terminates any background workers.
891
+ */
892
+ destroy(): void {
893
+ // Stop the render loop and terminate workers
894
+ this.platform.stopLoop();
895
+
896
+ // Recursively destroy all nodes (includes CoreTextNode font/text cleanup)
897
+ this.root.destroy();
898
+
899
+ // Free all GPU-side textures and clear internal tracking arrays/caches
900
+ this.txMemManager.destroy();
901
+
902
+ // Clear the texture upload queue and key caches
903
+ this.txManager.destroy();
904
+
905
+ // Release the GPU context (WebGL) or canvas resources
906
+ this.renderer.destroy();
907
+
908
+ // Clear text renderer caches
909
+ for (const key in this.textRenderers) {
910
+ this.textRenderers[key]!.clearCache();
911
+ }
912
+ }
913
+
885
914
  set clearColor(value: number) {
886
915
  this.renderer.updateClearColor(value);
887
916
  this.renderRequested = true;
@@ -0,0 +1,134 @@
1
+ /*
2
+ * If not stated otherwise in this file or this component's LICENSE file the
3
+ * following copyright and licenses apply:
4
+ *
5
+ * Copyright 2024 Comcast Cable Communications Management, LLC.
6
+ *
7
+ * Licensed under the Apache License, Version 2.0 (the License);
8
+ * you may not use this file except in compliance with the License.
9
+ * You may obtain a copy of the License at
10
+ *
11
+ * http://www.apache.org/licenses/LICENSE-2.0
12
+ *
13
+ * Unless required by applicable law or agreed to in writing, software
14
+ * distributed under the License is distributed on an "AS IS" BASIS,
15
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16
+ * See the License for the specific language governing permissions and
17
+ * limitations under the License.
18
+ */
19
+
20
+ import { describe, it, expect } from 'vitest';
21
+ import { mock } from 'vitest-mock-extended';
22
+ import {
23
+ TextureMemoryManager,
24
+ type TextureMemoryManagerSettings,
25
+ } from './TextureMemoryManager.js';
26
+ import type { Stage } from './Stage.js';
27
+ import type { Texture } from './textures/Texture.js';
28
+ import { TextureType } from './textures/Texture.js';
29
+
30
+ function makeSettings(
31
+ overrides?: Partial<TextureMemoryManagerSettings>,
32
+ ): TextureMemoryManagerSettings {
33
+ return {
34
+ criticalThreshold: 124e6,
35
+ targetThresholdLevel: 0.5,
36
+ cleanupInterval: 5000,
37
+ debugLogging: false,
38
+ baselineMemoryAllocation: 25e6,
39
+ doNotExceedCriticalThreshold: false,
40
+ ...overrides,
41
+ };
42
+ }
43
+
44
+ function makeTexture(memUsed = 1024): Texture {
45
+ const texture = mock<Texture>();
46
+ texture.memUsed = memUsed;
47
+ texture.type = TextureType.image;
48
+ texture.state = 'loaded';
49
+ (texture as { renderable: boolean }).renderable = false;
50
+ texture.preventCleanup = false;
51
+ texture.canBeCleanedUp.mockReturnValue(true);
52
+ return texture;
53
+ }
54
+
55
+ function makeStage() {
56
+ const stage = mock<Stage>();
57
+ const txManager = mock<Stage['txManager']>();
58
+ (stage as { txManager: Stage['txManager'] }).txManager = txManager;
59
+ return stage;
60
+ }
61
+
62
+ describe('TextureMemoryManager', () => {
63
+ describe('destroy()', () => {
64
+ it('calls destroyTexture for every loaded texture', () => {
65
+ const stage = makeStage();
66
+ const manager = new TextureMemoryManager(stage, makeSettings());
67
+
68
+ const textures = [makeTexture(512), makeTexture(1024), makeTexture(2048)];
69
+
70
+ for (const texture of textures) {
71
+ manager.setTextureMemUse(texture, texture.memUsed);
72
+ }
73
+
74
+ manager.destroy();
75
+
76
+ for (const texture of textures) {
77
+ expect(texture.destroy).toHaveBeenCalledOnce();
78
+ }
79
+ });
80
+
81
+ it('clears internal tracking so memUsed is 0 after destroy', () => {
82
+ const stage = makeStage();
83
+ const manager = new TextureMemoryManager(stage, makeSettings());
84
+
85
+ const texture = makeTexture(4096);
86
+ manager.setTextureMemUse(texture, texture.memUsed);
87
+
88
+ manager.destroy();
89
+
90
+ expect(manager.getMemoryInfo().memUsed).toBe(0);
91
+ });
92
+
93
+ it('reports no loaded textures after destroy', () => {
94
+ const stage = makeStage();
95
+ const manager = new TextureMemoryManager(stage, makeSettings());
96
+
97
+ manager.setTextureMemUse(makeTexture(512), 512);
98
+ manager.setTextureMemUse(makeTexture(1024), 1024);
99
+
100
+ manager.destroy();
101
+
102
+ expect(manager.getMemoryInfo().loadedTextures).toBe(0);
103
+ });
104
+
105
+ it('removes each texture from the cache via txManager', () => {
106
+ const stage = makeStage();
107
+ const manager = new TextureMemoryManager(stage, makeSettings());
108
+
109
+ const textures = [makeTexture(256), makeTexture(512)];
110
+ for (const texture of textures) {
111
+ manager.setTextureMemUse(texture, texture.memUsed);
112
+ }
113
+
114
+ manager.destroy();
115
+
116
+ expect(stage.txManager.removeTextureFromCache).toHaveBeenCalledTimes(
117
+ textures.length,
118
+ );
119
+
120
+ for (const texture of textures) {
121
+ expect(stage.txManager.removeTextureFromCache).toHaveBeenCalledWith(
122
+ texture,
123
+ );
124
+ }
125
+ });
126
+
127
+ it('handles an empty loadedTextures list without throwing', () => {
128
+ const stage = makeStage();
129
+ const manager = new TextureMemoryManager(stage, makeSettings());
130
+
131
+ expect(() => manager.destroy()).not.toThrow();
132
+ });
133
+ });
134
+ });
@@ -116,7 +116,6 @@ export interface MemoryInfo {
116
116
  export class TextureMemoryManager {
117
117
  private memUsed = 0;
118
118
  private loadedTextures: (Texture | null)[] = [];
119
- private orphanedTextures: Texture[] = [];
120
119
  private criticalThreshold: number = 124e6;
121
120
  private targetThreshold: number = 0.5;
122
121
  private cleanupInterval: number = 5000;
@@ -145,35 +144,6 @@ export class TextureMemoryManager {
145
144
  this.updateSettings(settings);
146
145
  }
147
146
 
148
- /**
149
- * Add a texture to the orphaned textures list
150
- *
151
- * @param texture - The texture to add to the orphaned textures list
152
- */
153
- addToOrphanedTextures(texture: Texture) {
154
- // if the texture is already in the orphaned textures list add it at the end
155
- if (this.orphanedTextures.includes(texture)) {
156
- this.removeFromOrphanedTextures(texture);
157
- }
158
-
159
- // If the texture can be cleaned up, add it to the orphaned textures list
160
- if (texture.preventCleanup === false) {
161
- this.orphanedTextures.push(texture);
162
- }
163
- }
164
-
165
- /**
166
- * Remove a texture from the orphaned textures list
167
- *
168
- * @param texture - The texture to remove from the orphaned textures list
169
- */
170
- removeFromOrphanedTextures(texture: Texture) {
171
- const index = this.orphanedTextures.indexOf(texture);
172
- if (index !== -1) {
173
- this.orphanedTextures.splice(index, 1);
174
- }
175
- }
176
-
177
147
  /**
178
148
  * Set the memory usage of a texture
179
149
  *
@@ -415,4 +385,27 @@ export class TextureMemoryManager {
415
385
  this.setTextureMemUse = () => {};
416
386
  }
417
387
  }
388
+
389
+ /**
390
+ * Destroy the TextureMemoryManager and release all internal references.
391
+ *
392
+ * @remarks
393
+ * Clears the debug-logging interval (if active) and empties all internal
394
+ * texture tracking arrays so that held textures can be garbage-collected.
395
+ */
396
+ destroy(): void {
397
+ if (this.loggingID) {
398
+ clearInterval(this.loggingID);
399
+ this.loggingID = 0 as unknown as ReturnType<typeof setInterval>;
400
+ }
401
+ // Free GPU resources for every loaded texture before clearing the array
402
+ for (let i = 0; i < this.loadedTextures.length; i++) {
403
+ const texture = this.loadedTextures[i];
404
+ if (texture !== null && texture !== undefined) {
405
+ this.destroyTexture(texture);
406
+ }
407
+ }
408
+ this.loadedTextures = [];
409
+ this.memUsed = 0;
410
+ }
418
411
  }
@@ -89,6 +89,11 @@ export abstract class Platform {
89
89
  */
90
90
  abstract startLoop(stage: Stage): void;
91
91
 
92
+ /**
93
+ * Stops the main rendering loop and releases platform resources.
94
+ */
95
+ abstract stopLoop(): void;
96
+
92
97
  /**
93
98
  * Fetches a resource from the network.
94
99
  * @param url - The URL of the resource to fetch.
@@ -48,6 +48,7 @@ export class WebPlatform extends Platform {
48
48
  private useImageWorker: boolean;
49
49
  private imageWorkerManager: ImageWorkerManager | null = null;
50
50
  private hasWorker = !!self.Worker;
51
+ private stopped = false;
51
52
 
52
53
  constructor(settings: PlatformSettings = {}) {
53
54
  super(settings);
@@ -100,10 +101,12 @@ export class WebPlatform extends Platform {
100
101
  ////////////////////////
101
102
 
102
103
  override startLoop(stage: Stage): void {
104
+ this.stopped = false;
103
105
  let isIdle = false;
104
106
  let lastFrameTime = 0;
105
107
 
106
108
  const runLoop = (currentTime: number = 0) => {
109
+ if (this.stopped) return;
107
110
  const targetFrameTime = stage.targetFrameTime;
108
111
 
109
112
  // Check if we should throttle this frame
@@ -173,6 +176,16 @@ export class WebPlatform extends Platform {
173
176
  requestAnimationFrame(runLoop);
174
177
  }
175
178
 
179
+ override stopLoop(): void {
180
+ this.stopped = true;
181
+ if (this.imageWorkerManager !== null) {
182
+ for (const worker of this.imageWorkerManager.workers) {
183
+ worker.terminate();
184
+ }
185
+ this.imageWorkerManager = null;
186
+ }
187
+ }
188
+
176
189
  ////////////////////////
177
190
  // Image handling
178
191
  ////////////////////////
@@ -67,5 +67,15 @@ export abstract class CoreRenderer {
67
67
  abstract getQuadCount(): number | null;
68
68
  abstract updateViewport(): void;
69
69
  abstract updateClearColor(color: number): void;
70
+ abstract destroy(): void;
70
71
  getTextureCoords?(node: CoreNode): TextureCoords | undefined;
72
+
73
+ /**
74
+ * Delete a GPU buffer previously allocated by this renderer.
75
+ * No-op for renderers that do not use WebGL buffers (e.g. Canvas).
76
+ */
77
+ // eslint-disable-next-line @typescript-eslint/no-unused-vars
78
+ deleteBuffer(_buffer: WebGLBuffer): void {
79
+ // no-op default — overridden by WebGlRenderer
80
+ }
71
81
  }
@@ -255,4 +255,10 @@ export class CanvasRenderer extends CoreRenderer {
255
255
  getDefaultShaderNode() {
256
256
  return null;
257
257
  }
258
+
259
+ override destroy(): void {
260
+ // Release canvas 2D context by resizing canvas to 0
261
+ this.canvas.width = 0;
262
+ this.canvas.height = 0;
263
+ }
258
264
  }