@lightningjs/renderer 3.0.2 → 3.0.4

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 (119) hide show
  1. package/README.md +56 -196
  2. package/dist/src/core/CoreNode.d.ts +2 -1
  3. package/dist/src/core/CoreNode.js +31 -7
  4. package/dist/src/core/CoreNode.js.map +1 -1
  5. package/dist/src/core/CoreTextNode.d.ts +26 -6
  6. package/dist/src/core/CoreTextNode.js +163 -60
  7. package/dist/src/core/CoreTextNode.js.map +1 -1
  8. package/dist/src/core/CoreTextureManager.d.ts +8 -0
  9. package/dist/src/core/CoreTextureManager.js +13 -1
  10. package/dist/src/core/CoreTextureManager.js.map +1 -1
  11. package/dist/src/core/Stage.d.ts +8 -0
  12. package/dist/src/core/Stage.js +23 -0
  13. package/dist/src/core/Stage.js.map +1 -1
  14. package/dist/src/core/TextureMemoryManager.d.ts +8 -13
  15. package/dist/src/core/TextureMemoryManager.js +22 -27
  16. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  17. package/dist/src/core/lib/ImageWorker.d.ts +2 -2
  18. package/dist/src/core/lib/ImageWorker.js +31 -12
  19. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  20. package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
  21. package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
  22. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  23. package/dist/src/core/lib/fps.d.ts +15 -0
  24. package/dist/src/core/lib/fps.js +62 -0
  25. package/dist/src/core/lib/fps.js.map +1 -0
  26. package/dist/src/core/lib/textureCompression.js +19 -10
  27. package/dist/src/core/lib/textureCompression.js.map +1 -1
  28. package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
  29. package/dist/src/core/lib/validateImageBitmap.js +4 -4
  30. package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
  31. package/dist/src/core/platform.js +2 -2
  32. package/dist/src/core/platform.js.map +1 -1
  33. package/dist/src/core/platforms/Platform.d.ts +4 -0
  34. package/dist/src/core/platforms/Platform.js.map +1 -1
  35. package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
  36. package/dist/src/core/platforms/web/WebPlatform.js +13 -0
  37. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  38. package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
  39. package/dist/src/core/renderers/CoreRenderer.js +8 -0
  40. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  41. package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
  42. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
  43. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  44. package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
  45. package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
  46. package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
  47. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +4 -2
  48. package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
  49. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  50. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +2 -3
  51. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  52. package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
  53. package/dist/src/core/text-rendering/CanvasFont.js +120 -0
  54. package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
  55. package/dist/src/core/text-rendering/CanvasFontHandler.d.ts +1 -1
  56. package/dist/src/core/text-rendering/CanvasFontHandler.js +1 -1
  57. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  58. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +3 -5
  59. package/dist/src/core/text-rendering/CanvasTextRenderer.js +16 -22
  60. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  61. package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
  62. package/dist/src/core/text-rendering/CoreFont.js +48 -0
  63. package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
  64. package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
  65. package/dist/src/core/text-rendering/FontManager.js +41 -0
  66. package/dist/src/core/text-rendering/FontManager.js.map +1 -0
  67. package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
  68. package/dist/src/core/text-rendering/SdfFont.js +142 -0
  69. package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
  70. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +4 -6
  71. package/dist/src/core/text-rendering/SdfTextRenderer.js +87 -168
  72. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  73. package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
  74. package/dist/src/core/text-rendering/TextGenerator.js +36 -0
  75. package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
  76. package/dist/src/core/text-rendering/TextLayoutEngine.js +43 -12
  77. package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
  78. package/dist/src/core/text-rendering/TextRenderer.d.ts +41 -27
  79. package/dist/src/core/text-rendering/Utils.d.ts +2 -0
  80. package/dist/src/core/text-rendering/Utils.js +3 -0
  81. package/dist/src/core/text-rendering/Utils.js.map +1 -1
  82. package/dist/src/main-api/Inspector.d.ts +1 -1
  83. package/dist/src/main-api/Inspector.js +25 -20
  84. package/dist/src/main-api/Inspector.js.map +1 -1
  85. package/dist/src/main-api/Renderer.d.ts +14 -0
  86. package/dist/src/main-api/Renderer.js +29 -3
  87. package/dist/src/main-api/Renderer.js.map +1 -1
  88. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  89. package/package.json +2 -1
  90. package/src/core/CoreNode.test.ts +1 -1
  91. package/src/core/CoreNode.ts +37 -8
  92. package/src/core/CoreTextNode.test.ts +350 -0
  93. package/src/core/CoreTextNode.ts +201 -74
  94. package/src/core/CoreTextureManager.ts +14 -2
  95. package/src/core/Stage.ts +29 -0
  96. package/src/core/TextureMemoryManager.test.ts +134 -0
  97. package/src/core/TextureMemoryManager.ts +23 -30
  98. package/src/core/platforms/Platform.ts +5 -0
  99. package/src/core/platforms/web/WebPlatform.ts +13 -0
  100. package/src/core/renderers/CoreRenderer.ts +10 -0
  101. package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
  102. package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
  103. package/src/core/renderers/webgl/WebGlRenderer.ts +40 -31
  104. package/src/core/renderers/webgl/WebGlShaderProgram.test.ts +274 -0
  105. package/src/core/renderers/webgl/WebGlShaderProgram.ts +7 -7
  106. package/src/core/text-rendering/CanvasFontHandler.ts +2 -2
  107. package/src/core/text-rendering/CanvasTextRenderer.ts +24 -45
  108. package/src/core/text-rendering/SdfTextRenderer.ts +106 -215
  109. package/src/core/text-rendering/TextLayoutEngine.ts +61 -28
  110. package/src/core/text-rendering/TextRenderer.ts +42 -33
  111. package/src/core/text-rendering/Utils.ts +5 -1
  112. package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +20 -0
  113. package/src/main-api/Inspector.ts +25 -25
  114. package/src/main-api/Renderer.test.ts +153 -0
  115. package/src/main-api/Renderer.ts +33 -3
  116. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
  117. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
  118. package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
  119. package/src/core/renderers/webgl/SdfRenderOp.ts +0 -106
@@ -21,7 +21,6 @@ import type {
21
21
  FontHandler,
22
22
  TextRenderer,
23
23
  TrProps,
24
- TextLayout,
25
24
  TextRenderInfo,
26
25
  } from './text-rendering/TextRenderer.js';
27
26
  import {
@@ -40,6 +39,11 @@ import type { RectWithValid } from './lib/utils.js';
40
39
  import type { CoreRenderer } from './renderers/CoreRenderer.js';
41
40
  import type { TextureLoadedEventHandler } from './textures/Texture.js';
42
41
  import { Matrix3d } from './lib/Matrix3d.js';
42
+ import { BufferCollection } from './renderers/webgl/internal/BufferCollection.js';
43
+ import type { SdfShaderProps } from './shaders/webgl/SdfShader.js';
44
+ import type { WebGlRenderer } from './renderers/webgl/WebGlRenderer.js';
45
+ import type { WebGlCtxTexture } from './renderers/webgl/WebGlCtxTexture.js';
46
+ import { mergeColorAlpha } from '../utils.js';
43
47
  export interface CoreTextNodeProps extends CoreNodeProps, TrProps {
44
48
  /**
45
49
  * Force Text Node to use a specific Text Renderer
@@ -63,19 +67,14 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
63
67
  private _waitingForFont = false;
64
68
  private _containType: TextConstraint = TextConstraint.none;
65
69
 
66
- // SDF layout caching for performance
67
- private _cachedLayout: TextLayout | null = null;
68
- private _lastVertexBuffer: Float32Array | null = null;
70
+ private _sdfBuffer: WebGLBuffer | null = null;
71
+ private _sdfQuadCollection: BufferCollection | null = null;
72
+ private _sdfShaderProps: Partial<SdfShaderProps> | null = null;
69
73
 
70
74
  // Text renderer properties - stored directly on the node
71
- private textProps: CoreTextNodeProps;
75
+ textProps: CoreTextNodeProps;
72
76
 
73
- private _renderInfo: TextRenderInfo = {
74
- width: 0,
75
- height: 0,
76
- };
77
-
78
- private _type: 'sdf' | 'canvas' = 'sdf'; // Default to SDF renderer
77
+ private _renderInfo: TextRenderInfo | null = null;
79
78
 
80
79
  constructor(
81
80
  stage: Stage,
@@ -85,7 +84,6 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
85
84
  super(stage, props);
86
85
  this.textRenderer = textRenderer;
87
86
  this.fontHandler = textRenderer.font;
88
- this._type = textRenderer.type;
89
87
 
90
88
  // Initialize text properties from props
91
89
  // Props are guaranteed to have all defaults resolved by Stage.createTextNode
@@ -113,6 +111,19 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
113
111
  this.setUpdateType(UpdateType.IsRenderable);
114
112
  };
115
113
 
114
+ /**
115
+ * Delete the cached WebGLBuffer held by the SDF renderer ref and reset the
116
+ * ref so the next renderQuads call allocates a fresh one.
117
+ * Safe to call from destroy() or on text change.
118
+ */
119
+ private releaseSdfBuffer(): void {
120
+ const buf = this._sdfBuffer;
121
+ if (buf === null) return;
122
+ this.stage.renderer.deleteBuffer(buf);
123
+ this._sdfBuffer = null;
124
+ this._sdfQuadCollection = null;
125
+ }
126
+
116
127
  allowTextGeneration() {
117
128
  const p = this.props.parent;
118
129
  if (p === null) {
@@ -196,7 +207,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
196
207
  * Override CoreNode's update method to handle text-specific updates
197
208
  */
198
209
  override update(delta: number, parentClippingRect: RectWithValid): void {
199
- const hasValidText = this.textProps.text && this.textProps.text.length > 0;
210
+ const hasValidText =
211
+ typeof this.textProps.text === 'string' && this.textProps.text.length > 0;
200
212
 
201
213
  if (
202
214
  hasValidText === true &&
@@ -206,8 +218,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
206
218
  ) {
207
219
  if (this.fontHandler.isFontLoaded(this.textProps.fontFamily) === true) {
208
220
  this._waitingForFont = false;
209
- this._cachedLayout = null; // Invalidate cached layout
210
- this._lastVertexBuffer = null; // Invalidate last vertex buffer
221
+ this._renderInfo = null; // Clear any previous render info before generating new layout
222
+ this.releaseSdfBuffer(); // Free the cached WebGLBuffer
211
223
  const resp = this.textRenderer.renderText(this.textProps);
212
224
  this.handleRenderResult(resp);
213
225
  this._layoutGenerated = true;
@@ -216,11 +228,13 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
216
228
  this._waitingForFont = true;
217
229
  }
218
230
  } else if (hasValidText === false) {
231
+ this.props.w = 0;
232
+ this.props.h = 0;
219
233
  // If text is invalid, ensure node is not renderable
220
234
  this.setRenderable(false);
221
235
  this._layoutGenerated = false;
222
- this._cachedLayout = null;
223
- this._lastVertexBuffer = null;
236
+ this._renderInfo = null;
237
+ this.releaseSdfBuffer(); // Free the cached WebGLBuffer
224
238
  }
225
239
 
226
240
  // First run the standard CoreNode update
@@ -232,20 +246,21 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
232
246
  */
233
247
  override updateIsRenderable(): void {
234
248
  // Guard: Text nodes are never renderable without valid text
235
- const hasValidText = this.textProps.text && this.textProps.text.length > 0;
236
- if (hasValidText === false) {
249
+ const hasValidText =
250
+ typeof this.textProps.text === 'string' && this.textProps.text.length > 0;
251
+
252
+ const renderInfo = this._renderInfo;
253
+ if (hasValidText === false || renderInfo === null) {
237
254
  this.setRenderable(false);
238
255
  return;
239
256
  }
240
257
 
241
258
  // SDF text nodes are always renderable if they have a valid layout
242
- if (this._type === 'canvas') {
259
+ if (renderInfo.type === 'canvas') {
243
260
  super.updateIsRenderable();
244
261
  return;
245
262
  }
246
-
247
- // For SDF, check if we have a cached layout
248
- this.setRenderable(this._cachedLayout !== null);
263
+ this.setRenderable(true);
249
264
  }
250
265
 
251
266
  /**
@@ -253,10 +268,19 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
253
268
  */
254
269
  private handleRenderResult(result: TextRenderInfo): void {
255
270
  // Host paths on top
256
- const textRendererType = this._type;
271
+ const textRendererType = result.type;
257
272
  let width = result.width;
258
273
  let height = result.height;
259
274
 
275
+ // Handle zero-dimension case (can happen with certain text inputs or font issues)
276
+ if (width === 0 || height === 0) {
277
+ this.emit('failed', {
278
+ type: 'text',
279
+ error: new Error('Text rendering failed, width or height zero'),
280
+ } satisfies NodeTextFailedPayload);
281
+ return;
282
+ }
283
+
260
284
  // Handle Canvas renderer (uses ImageData)
261
285
  if (textRendererType === 'canvas') {
262
286
  if (result.imageData === undefined) {
@@ -273,6 +297,9 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
273
297
  premultiplyAlpha: true,
274
298
  src: result.imageData as ImageData,
275
299
  });
300
+
301
+ this.props.w = width;
302
+ this.props.h = height;
276
303
  // It isn't renderable until the texture is loaded we have to set it to false here to avoid it
277
304
  // being detected as a renderable default color node in the next frame
278
305
  // it will be corrected once the texture is loaded
@@ -282,25 +309,31 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
282
309
  // We do want the texture to load immediately
283
310
  this.texture.setRenderableOwner(this._id, true);
284
311
  }
285
- }
312
+ } else {
313
+ const layout = result.layout;
314
+ // For SDF, we rely on the presence of a valid layout to determine renderability
315
+ if (layout === undefined) {
316
+ this.emit('failed', {
317
+ type: 'text',
318
+ error: new Error(
319
+ 'SDF text rendering failed, no layout data returned',
320
+ ),
321
+ } satisfies NodeTextFailedPayload);
322
+ return;
323
+ }
286
324
 
287
- // Handle zero-dimension case (can happen with certain text inputs or font issues)
288
- if (width === 0 || height === 0) {
289
- this.emit('failed', {
290
- type: 'text',
291
- error: new Error('Text rendering failed, width or height zero'),
292
- } satisfies NodeTextFailedPayload);
293
- return;
294
- }
325
+ this.props.w = width;
326
+ this.props.h = height;
327
+ this.setUpdateType(UpdateType.Local);
328
+ this.setRenderable(true);
329
+ this.numQuads = layout.glyphCount;
295
330
 
296
- this._cachedLayout = result.layout || null;
297
- this.props.w = width;
298
- this.props.h = height;
331
+ this._sdfShaderProps = {
332
+ size: layout.fontScale,
333
+ distanceRange: layout.distanceRange,
334
+ };
299
335
 
300
- // Handle SDF renderer (uses layout caching)
301
- if (textRendererType === 'sdf') {
302
- this.setRenderable(true);
303
- this.setUpdateType(UpdateType.Local);
336
+ this.renderOpTextures = [result.atlasTexture as WebGlCtxTexture];
304
337
  }
305
338
 
306
339
  this._renderInfo = result;
@@ -309,6 +342,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
309
342
 
310
343
  // Reusable bound method for emitting loaded event
311
344
  private emitTextLoadedEvent = () => {
345
+ if (this._renderInfo === null) return; // Guard against unexpected null
346
+
312
347
  this.emit('loaded', {
313
348
  type: 'text',
314
349
  dimensions: {
@@ -328,44 +363,72 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
328
363
  return;
329
364
  }
330
365
 
331
- // Canvas renderer: use standard texture rendering via CoreNode
332
- if (this._type === 'canvas') {
333
- super.renderQuads(renderer);
366
+ // Early return if no renderInfo
367
+ if (this._renderInfo === null) {
334
368
  return;
335
369
  }
336
370
 
337
- // Early return if no cached data
338
- if (!this._cachedLayout) {
371
+ // Canvas renderer: use standard texture rendering via CoreNode
372
+ if (this._renderInfo.type === 'canvas') {
373
+ super.renderQuads(renderer);
339
374
  return;
340
375
  }
341
376
 
342
- if (this._lastVertexBuffer === null) {
343
- this._lastVertexBuffer = this.textRenderer.addQuads(this._cachedLayout);
344
- }
345
-
346
- 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
- },
377
+ if (this._sdfBuffer === null) {
378
+ const glw = (this.stage.renderer as WebGlRenderer).glw;
379
+ this._sdfBuffer = glw.createBuffer();
380
+ if (this._sdfBuffer === null) {
381
+ console.error('Failed to create WebGL buffer for SDF text rendering');
382
+ return;
383
+ }
384
+ glw.arrayBufferData(
385
+ this._sdfBuffer,
386
+ this._renderInfo.layout.vertexBuffer,
387
+ glw.STATIC_DRAW,
388
+ );
389
+
390
+ this._sdfQuadCollection = new BufferCollection([
391
+ {
392
+ buffer: this._sdfBuffer,
393
+ attributes: {
394
+ a_position: {
395
+ name: 'a_position',
396
+ size: 2,
397
+ type: glw.FLOAT as number,
398
+ normalized: false,
399
+ stride: 4 * Float32Array.BYTES_PER_ELEMENT,
400
+ offset: 0,
401
+ },
402
+ a_textureCoords: {
403
+ name: 'a_textureCoords',
404
+ size: 2,
405
+ type: glw.FLOAT as number,
406
+ normalized: false,
407
+ stride: 4 * Float32Array.BYTES_PER_ELEMENT,
408
+ offset: 2 * Float32Array.BYTES_PER_ELEMENT,
409
+ },
410
+ },
411
+ },
412
+ ]);
413
+ }
414
+
415
+ this.sdfShaderProps!.transform = this.globalTransform!.getFloatArr();
416
+ this.sdfShaderProps!.color = mergeColorAlpha(
417
+ this.props.color,
418
+ this.worldAlpha,
368
419
  );
420
+
421
+ this.textRenderer.renderQuads(this);
422
+ }
423
+
424
+ override updateRenderState(renderState: CoreNodeRenderState): void {
425
+ super.updateRenderState(renderState);
426
+ if (
427
+ this._renderInfo !== null &&
428
+ renderState === CoreNodeRenderState.OutOfBounds
429
+ ) {
430
+ this.releaseSdfBuffer();
431
+ }
369
432
  }
370
433
 
371
434
  override destroy(): void {
@@ -374,8 +437,8 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
374
437
  }
375
438
 
376
439
  // Clear cached layout and vertex buffer
377
- this._cachedLayout = null;
378
- this._lastVertexBuffer = null;
440
+ this._renderInfo = null;
441
+ this.releaseSdfBuffer(); // Delete the cached WebGLBuffer before losing stage ref
379
442
 
380
443
  this.fontHandler = null!; // Clear reference to avoid memory leaks
381
444
  this.textRenderer = null!; // Clear reference to avoid memory leaks
@@ -383,6 +446,70 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
383
446
  super.destroy();
384
447
  }
385
448
 
449
+ /**
450
+ * used in webgl SDF shader to get the quad buffer collection for rendering text quads
451
+ */
452
+ override get quadBufferCollection(): BufferCollection {
453
+ return this._sdfQuadCollection || super.quadBufferCollection;
454
+ }
455
+
456
+ /**
457
+ * used in webgl SDF shader to get the SDF shader props for rendering text quads
458
+ */
459
+ get sdfShaderProps(): SdfShaderProps {
460
+ return this._sdfShaderProps as SdfShaderProps;
461
+ }
462
+
463
+ override get isSdfRenderOp(): boolean {
464
+ return this.textRenderer.type === 'sdf';
465
+ }
466
+
467
+ override draw(renderer: WebGlRenderer) {
468
+ if (this.textRenderer.type === 'canvas') {
469
+ super.draw(renderer);
470
+ return;
471
+ }
472
+
473
+ const { glw, stage } = renderer;
474
+ const canvas = stage.platform!.canvas!;
475
+ const shader = this.props.shader as any;
476
+
477
+ stage.shManager.useShader(shader.program);
478
+ shader.program.bindRenderOp(this);
479
+
480
+ const clippingRect = this.clippingRect;
481
+
482
+ // Clipping
483
+ if (clippingRect.valid === true) {
484
+ const pixelRatio = this.parentHasRenderTexture ? 1 : stage.pixelRatio;
485
+
486
+ const clipX = Math.round(clippingRect.x * pixelRatio);
487
+ const clipWidth = Math.round(clippingRect.w * pixelRatio);
488
+ const clipHeight = Math.round(clippingRect.h * pixelRatio);
489
+ let clipY = Math.round(
490
+ canvas.height - clipHeight - clippingRect.y * pixelRatio,
491
+ );
492
+ // if parent has render texture, we need to adjust the scissor rect
493
+ // to be relative to the parent's framebuffer
494
+ if (this.parentHasRenderTexture) {
495
+ const parentFramebufferDimensions = this.parentFramebufferDimensions;
496
+ clipY =
497
+ parentFramebufferDimensions !== null
498
+ ? parentFramebufferDimensions.h - this.props.h
499
+ : 0;
500
+ }
501
+
502
+ glw.setScissorTest(true);
503
+ glw.scissor(clipX, clipY, clipWidth, clipHeight);
504
+ } else {
505
+ glw.setScissorTest(false);
506
+ }
507
+
508
+ // SDF rendering uses drawArrays with explicit triangle vertices (6 vertices per quad)
509
+ // Note: buffers should be bound by bindRenderOp -> bindBufferCollection
510
+ glw.drawArrays(glw.TRIANGLES, 0, 6 * this.numQuads);
511
+ }
512
+
386
513
  override set w(value: number) {
387
514
  this.maxWidth = value;
388
515
  }
@@ -601,7 +728,7 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
601
728
  }
602
729
  }
603
730
 
604
- get renderInfo(): TextRenderInfo {
731
+ get renderInfo(): TextRenderInfo | null {
605
732
  return this._renderInfo;
606
733
  }
607
734
  }
@@ -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
  }