@lightningjs/renderer 3.0.2 → 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.
- package/README.md +56 -196
- package/dist/src/core/CoreNode.js +25 -4
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.d.ts +9 -2
- package/dist/src/core/CoreTextNode.js +32 -11
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +8 -0
- package/dist/src/core/CoreTextureManager.js +13 -1
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.d.ts +8 -0
- package/dist/src/core/Stage.js +23 -0
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.d.ts +8 -13
- package/dist/src/core/TextureMemoryManager.js +22 -27
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.d.ts +2 -2
- package/dist/src/core/lib/ImageWorker.js +31 -12
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +105 -56
- package/dist/src/core/lib/WebGlContextWrapper.js +164 -158
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/textureCompression.js +19 -10
- package/dist/src/core/lib/textureCompression.js.map +1 -1
- package/dist/src/core/lib/validateImageBitmap.d.ts +2 -1
- package/dist/src/core/lib/validateImageBitmap.js +4 -4
- package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
- package/dist/src/core/platform.js +2 -2
- package/dist/src/core/platform.js.map +1 -1
- package/dist/src/core/platforms/Platform.d.ts +4 -0
- package/dist/src/core/platforms/Platform.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.d.ts +2 -0
- package/dist/src/core/platforms/web/WebPlatform.js +13 -0
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/CoreRenderer.d.ts +6 -0
- package/dist/src/core/renderers/CoreRenderer.js +8 -0
- package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +1 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderOp.d.ts +45 -0
- package/dist/src/core/renderers/webgl/WebGlRenderOp.js +127 -0
- package/dist/src/core/renderers/webgl/WebGlRenderOp.js.map +1 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +2 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +30 -22
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasFont.d.ts +14 -0
- package/dist/src/core/text-rendering/CanvasFont.js +120 -0
- package/dist/src/core/text-rendering/CanvasFont.js.map +1 -0
- package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +1 -2
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +11 -19
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/CoreFont.d.ts +33 -0
- package/dist/src/core/text-rendering/CoreFont.js +48 -0
- package/dist/src/core/text-rendering/CoreFont.js.map +1 -0
- package/dist/src/core/text-rendering/FontManager.d.ts +11 -0
- package/dist/src/core/text-rendering/FontManager.js +41 -0
- package/dist/src/core/text-rendering/FontManager.js.map +1 -0
- package/dist/src/core/text-rendering/SdfFont.d.ts +29 -0
- package/dist/src/core/text-rendering/SdfFont.js +142 -0
- package/dist/src/core/text-rendering/SdfFont.js.map +1 -0
- package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +2 -2
- package/dist/src/core/text-rendering/SdfTextRenderer.js +141 -132
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextGenerator.d.ts +10 -0
- package/dist/src/core/text-rendering/TextGenerator.js +36 -0
- package/dist/src/core/text-rendering/TextGenerator.js.map +1 -0
- package/dist/src/core/text-rendering/TextRenderer.d.ts +26 -20
- package/dist/src/core/text-rendering/Utils.d.ts +2 -0
- package/dist/src/core/text-rendering/Utils.js +3 -0
- package/dist/src/core/text-rendering/Utils.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +14 -0
- package/dist/src/main-api/Renderer.js +29 -3
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +2 -1
- package/src/core/CoreNode.ts +29 -4
- package/src/core/CoreTextNode.test.ts +237 -0
- package/src/core/CoreTextNode.ts +53 -33
- package/src/core/CoreTextureManager.ts +14 -2
- package/src/core/Stage.ts +29 -0
- package/src/core/TextureMemoryManager.test.ts +134 -0
- package/src/core/TextureMemoryManager.ts +23 -30
- package/src/core/platforms/Platform.ts +5 -0
- package/src/core/platforms/web/WebPlatform.ts +13 -0
- package/src/core/renderers/CoreRenderer.ts +10 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +6 -0
- package/src/core/renderers/webgl/WebGlRenderer.rtt.test.ts +551 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +38 -28
- package/src/core/text-rendering/CanvasTextRenderer.ts +13 -41
- package/src/core/text-rendering/SdfTextRenderer.ts +166 -163
- package/src/core/text-rendering/TextRenderer.ts +23 -21
- package/src/core/text-rendering/Utils.ts +5 -1
- package/src/main-api/Renderer.test.ts +153 -0
- package/src/main-api/Renderer.ts +33 -3
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.d.ts +0 -1
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js +0 -2
- package/dist/src/core/renderers/webgl/WebGlCoreShader.destroy.js.map +0 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lightningjs/renderer",
|
|
3
|
-
"version": "3.0.
|
|
3
|
+
"version": "3.0.3",
|
|
4
4
|
"description": "Lightning 3 Renderer",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/exports/index.js",
|
|
@@ -40,6 +40,7 @@
|
|
|
40
40
|
"concurrently": "^8.2.2",
|
|
41
41
|
"eslint": "^9.36.0",
|
|
42
42
|
"eslint-config-prettier": "^8.10.2",
|
|
43
|
+
"happy-dom": "17.6.3",
|
|
43
44
|
"husky": "^8.0.3",
|
|
44
45
|
"lint-staged": "^13.3.0",
|
|
45
46
|
"prettier": "^2.8.8",
|
package/src/core/CoreNode.ts
CHANGED
|
@@ -202,6 +202,26 @@ export enum UpdateType {
|
|
|
202
202
|
All = 16383,
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
+
/**
|
|
206
|
+
* Bitmask of UpdateType flags that represent a visually significant change
|
|
207
|
+
* within a node. Used to gate notifyParentRTTOfUpdate() so that RTT surfaces
|
|
208
|
+
* are only marked dirty when something actually visible changed, rather than
|
|
209
|
+
* on every update() cycle that merely propagates child traversal.
|
|
210
|
+
*
|
|
211
|
+
* Excluded flags (non-visual cascade/bookkeeping):
|
|
212
|
+
* Children, RenderBounds, RenderState, ParentRenderTexture, Autosize
|
|
213
|
+
*/
|
|
214
|
+
const RTT_NOTIFY_MASK =
|
|
215
|
+
UpdateType.Local |
|
|
216
|
+
UpdateType.Global |
|
|
217
|
+
UpdateType.Clipping |
|
|
218
|
+
UpdateType.SortZIndexChildren |
|
|
219
|
+
UpdateType.PremultipliedColors |
|
|
220
|
+
UpdateType.WorldAlpha |
|
|
221
|
+
UpdateType.IsRenderable |
|
|
222
|
+
UpdateType.RenderTexture |
|
|
223
|
+
UpdateType.RecalcUniforms;
|
|
224
|
+
|
|
205
225
|
/**
|
|
206
226
|
* A custom data map which can be stored on an CoreNode
|
|
207
227
|
*
|
|
@@ -1345,10 +1365,15 @@ export class CoreNode extends EventEmitter {
|
|
|
1345
1365
|
}
|
|
1346
1366
|
}
|
|
1347
1367
|
|
|
1348
|
-
// If the node has an RTT parent and
|
|
1349
|
-
//
|
|
1350
|
-
//
|
|
1351
|
-
|
|
1368
|
+
// If the node has an RTT parent and a visually relevant change occurred (or a
|
|
1369
|
+
// nested RTT child already flagged this node via hasRTTupdates), notify the
|
|
1370
|
+
// nearest RTT ancestor so it re-renders its surface.
|
|
1371
|
+
// Guarded by RTT_NOTIFY_MASK to avoid redundant notifications on frames where
|
|
1372
|
+
// only child-traversal bookkeeping flags (Children, RenderBounds, etc.) are set.
|
|
1373
|
+
if (
|
|
1374
|
+
parentHasRenderTexture === true &&
|
|
1375
|
+
(this.hasRTTupdates === true || (updateType & RTT_NOTIFY_MASK) !== 0)
|
|
1376
|
+
) {
|
|
1352
1377
|
this.notifyParentRTTOfUpdate();
|
|
1353
1378
|
}
|
|
1354
1379
|
|
|
@@ -19,6 +19,7 @@
|
|
|
19
19
|
|
|
20
20
|
import { describe, expect, it, vi, beforeEach } from 'vitest';
|
|
21
21
|
import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js';
|
|
22
|
+
import { CoreNodeRenderState } from './CoreNode.js';
|
|
22
23
|
import { Stage } from './Stage.js';
|
|
23
24
|
import { CoreRenderer } from './renderers/CoreRenderer.js';
|
|
24
25
|
import { mock } from 'vitest-mock-extended';
|
|
@@ -308,4 +309,240 @@ describe('CoreTextNode', () => {
|
|
|
308
309
|
expect(node.isRenderable).toBe(false);
|
|
309
310
|
});
|
|
310
311
|
});
|
|
312
|
+
|
|
313
|
+
function makeStageWithDeleteBuffer(deleteBuffer: ReturnType<typeof vi.fn>) {
|
|
314
|
+
return mock<Stage>({
|
|
315
|
+
strictBound: createBound(0, 0, 1920, 1080),
|
|
316
|
+
preloadBound: createBound(0, 0, 1920, 1080),
|
|
317
|
+
defaultTexture: { state: 'loaded' },
|
|
318
|
+
renderer: { deleteBuffer } as unknown as CoreRenderer,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
describe('updateRenderState – SDF buffer release on OutOfBounds', () => {
|
|
323
|
+
it('should call renderer.deleteBuffer and clear _sdfBufferRef when transitioning to OutOfBounds', () => {
|
|
324
|
+
const deleteBuffer = vi.fn();
|
|
325
|
+
const node = new CoreTextNode(
|
|
326
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
327
|
+
defaultTextProps,
|
|
328
|
+
mockTextRenderer,
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
// Simulate a live WebGLBuffer sitting in the ref
|
|
332
|
+
const fakeBuffer = {};
|
|
333
|
+
(node as any)._sdfBufferRef.current = fakeBuffer;
|
|
334
|
+
|
|
335
|
+
node.updateRenderState(CoreNodeRenderState.OutOfBounds);
|
|
336
|
+
|
|
337
|
+
expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
|
|
338
|
+
expect((node as any)._sdfBufferRef.current).toBeNull();
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
it('should not call renderer.deleteBuffer when _sdfBufferRef is already null', () => {
|
|
342
|
+
const deleteBuffer = vi.fn();
|
|
343
|
+
const node = new CoreTextNode(
|
|
344
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
345
|
+
defaultTextProps,
|
|
346
|
+
mockTextRenderer,
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
// _sdfBufferRef.current is null by default
|
|
350
|
+
node.updateRenderState(CoreNodeRenderState.OutOfBounds);
|
|
351
|
+
|
|
352
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('should not release the buffer when transitioning to InBounds', () => {
|
|
356
|
+
const deleteBuffer = vi.fn();
|
|
357
|
+
const node = new CoreTextNode(
|
|
358
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
359
|
+
defaultTextProps,
|
|
360
|
+
mockTextRenderer,
|
|
361
|
+
);
|
|
362
|
+
|
|
363
|
+
const fakeBuffer = {};
|
|
364
|
+
(node as any)._sdfBufferRef.current = fakeBuffer;
|
|
365
|
+
|
|
366
|
+
node.updateRenderState(CoreNodeRenderState.InBounds);
|
|
367
|
+
|
|
368
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
369
|
+
expect((node as any)._sdfBufferRef.current).toBe(fakeBuffer);
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
it('should not release the buffer for a canvas-type text node', () => {
|
|
373
|
+
const deleteBuffer = vi.fn();
|
|
374
|
+
const canvasTextRenderer = {
|
|
375
|
+
...mockTextRenderer,
|
|
376
|
+
type: 'canvas' as const,
|
|
377
|
+
} as any;
|
|
378
|
+
|
|
379
|
+
const node = new CoreTextNode(
|
|
380
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
381
|
+
defaultTextProps,
|
|
382
|
+
canvasTextRenderer,
|
|
383
|
+
);
|
|
384
|
+
|
|
385
|
+
(node as any)._sdfBufferRef.current = {};
|
|
386
|
+
|
|
387
|
+
node.updateRenderState(CoreNodeRenderState.OutOfBounds);
|
|
388
|
+
|
|
389
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
describe('SDF buffer release on layout regeneration', () => {
|
|
394
|
+
it('should call renderer.deleteBuffer before regenerating layout when font is already loaded', () => {
|
|
395
|
+
const deleteBuffer = vi.fn();
|
|
396
|
+
const props = { ...defaultTextProps, forceLoad: true };
|
|
397
|
+
const node = new CoreTextNode(
|
|
398
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
399
|
+
props,
|
|
400
|
+
mockTextRenderer,
|
|
401
|
+
);
|
|
402
|
+
|
|
403
|
+
const fakeBuffer = {} as WebGLBuffer;
|
|
404
|
+
(node as any)._sdfBufferRef.current = fakeBuffer;
|
|
405
|
+
|
|
406
|
+
node.update(16, clippingRect);
|
|
407
|
+
|
|
408
|
+
expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
|
|
409
|
+
expect((node as any)._sdfBufferRef.current).toBeNull();
|
|
410
|
+
});
|
|
411
|
+
|
|
412
|
+
it('should call renderer.deleteBuffer again on each subsequent layout regeneration', () => {
|
|
413
|
+
const deleteBuffer = vi.fn();
|
|
414
|
+
const props = { ...defaultTextProps, forceLoad: true };
|
|
415
|
+
const node = new CoreTextNode(
|
|
416
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
417
|
+
props,
|
|
418
|
+
mockTextRenderer,
|
|
419
|
+
);
|
|
420
|
+
|
|
421
|
+
node.update(16, clippingRect); // first layout – no buffer yet, no delete call
|
|
422
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
423
|
+
|
|
424
|
+
// Trigger a second layout pass by invalidating the layout
|
|
425
|
+
node.fontSize = 24;
|
|
426
|
+
const secondBuffer = {} as WebGLBuffer;
|
|
427
|
+
(node as any)._sdfBufferRef.current = secondBuffer;
|
|
428
|
+
|
|
429
|
+
node.update(16, clippingRect);
|
|
430
|
+
|
|
431
|
+
expect(deleteBuffer).toHaveBeenCalledWith(secondBuffer);
|
|
432
|
+
expect((node as any)._sdfBufferRef.current).toBeNull();
|
|
433
|
+
});
|
|
434
|
+
|
|
435
|
+
it('should not call renderer.deleteBuffer when buffer is already null at regeneration time', () => {
|
|
436
|
+
const deleteBuffer = vi.fn();
|
|
437
|
+
const props = { ...defaultTextProps, forceLoad: true };
|
|
438
|
+
const node = new CoreTextNode(
|
|
439
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
440
|
+
props,
|
|
441
|
+
mockTextRenderer,
|
|
442
|
+
);
|
|
443
|
+
|
|
444
|
+
// _sdfBufferRef.current is null by default
|
|
445
|
+
node.update(16, clippingRect);
|
|
446
|
+
|
|
447
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
448
|
+
});
|
|
449
|
+
});
|
|
450
|
+
|
|
451
|
+
describe('SDF buffer release when text becomes invalid', () => {
|
|
452
|
+
it('should call renderer.deleteBuffer when text is cleared during update', () => {
|
|
453
|
+
const deleteBuffer = vi.fn();
|
|
454
|
+
const props = { ...defaultTextProps, text: 'Hello', forceLoad: true };
|
|
455
|
+
const node = new CoreTextNode(
|
|
456
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
457
|
+
props,
|
|
458
|
+
mockTextRenderer,
|
|
459
|
+
);
|
|
460
|
+
|
|
461
|
+
// Prime the node with a cached buffer
|
|
462
|
+
const fakeBuffer = {} as WebGLBuffer;
|
|
463
|
+
(node as any)._sdfBufferRef.current = fakeBuffer;
|
|
464
|
+
(node as any)._layoutGenerated = true;
|
|
465
|
+
|
|
466
|
+
node.text = '';
|
|
467
|
+
node.update(16, clippingRect);
|
|
468
|
+
|
|
469
|
+
expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
|
|
470
|
+
expect((node as any)._sdfBufferRef.current).toBeNull();
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
it('should not call renderer.deleteBuffer when text is invalid and buffer is already null', () => {
|
|
474
|
+
const deleteBuffer = vi.fn();
|
|
475
|
+
const props = { ...defaultTextProps, text: '', forceLoad: true };
|
|
476
|
+
const node = new CoreTextNode(
|
|
477
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
478
|
+
props,
|
|
479
|
+
mockTextRenderer,
|
|
480
|
+
);
|
|
481
|
+
|
|
482
|
+
node.update(16, clippingRect);
|
|
483
|
+
|
|
484
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
485
|
+
});
|
|
486
|
+
|
|
487
|
+
it('should also clear _cachedLayout when text becomes invalid', () => {
|
|
488
|
+
const deleteBuffer = vi.fn();
|
|
489
|
+
const props = { ...defaultTextProps, text: 'Hello', forceLoad: true };
|
|
490
|
+
const node = new CoreTextNode(
|
|
491
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
492
|
+
props,
|
|
493
|
+
mockTextRenderer,
|
|
494
|
+
);
|
|
495
|
+
|
|
496
|
+
(node as any)._cachedLayout = {};
|
|
497
|
+
(node as any)._layoutGenerated = true;
|
|
498
|
+
|
|
499
|
+
node.text = '';
|
|
500
|
+
node.update(16, clippingRect);
|
|
501
|
+
|
|
502
|
+
expect((node as any)._cachedLayout).toBeNull();
|
|
503
|
+
});
|
|
504
|
+
});
|
|
505
|
+
|
|
506
|
+
describe('SDF buffer release on destroy', () => {
|
|
507
|
+
it('should call renderer.deleteBuffer on destroy when a buffer is held', () => {
|
|
508
|
+
const deleteBuffer = vi.fn();
|
|
509
|
+
const node = new CoreTextNode(
|
|
510
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
511
|
+
defaultTextProps,
|
|
512
|
+
mockTextRenderer,
|
|
513
|
+
);
|
|
514
|
+
|
|
515
|
+
const fakeBuffer = {} as WebGLBuffer;
|
|
516
|
+
(node as any)._sdfBufferRef.current = fakeBuffer;
|
|
517
|
+
|
|
518
|
+
node.destroy();
|
|
519
|
+
|
|
520
|
+
expect(deleteBuffer).toHaveBeenCalledWith(fakeBuffer);
|
|
521
|
+
expect((node as any)._sdfBufferRef.current).toBeNull();
|
|
522
|
+
});
|
|
523
|
+
|
|
524
|
+
it('should not call renderer.deleteBuffer on destroy when buffer is already null', () => {
|
|
525
|
+
const deleteBuffer = vi.fn();
|
|
526
|
+
const node = new CoreTextNode(
|
|
527
|
+
makeStageWithDeleteBuffer(deleteBuffer),
|
|
528
|
+
defaultTextProps,
|
|
529
|
+
mockTextRenderer,
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
// _sdfBufferRef.current is null by default
|
|
533
|
+
node.destroy();
|
|
534
|
+
|
|
535
|
+
expect(deleteBuffer).not.toHaveBeenCalled();
|
|
536
|
+
});
|
|
537
|
+
|
|
538
|
+
it('should clear _cachedLayout on destroy', () => {
|
|
539
|
+
const node = new CoreTextNode(stage, defaultTextProps, mockTextRenderer);
|
|
540
|
+
|
|
541
|
+
(node as any)._cachedLayout = {};
|
|
542
|
+
|
|
543
|
+
node.destroy();
|
|
544
|
+
|
|
545
|
+
expect((node as any)._cachedLayout).toBeNull();
|
|
546
|
+
});
|
|
547
|
+
});
|
|
311
548
|
});
|
package/src/core/CoreTextNode.ts
CHANGED
|
@@ -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
|
-
|
|
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 =
|
|
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.
|
|
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.
|
|
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 =
|
|
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 (
|
|
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
|
-
|
|
349
|
-
|
|
350
|
-
this.
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
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.
|
|
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
|
+
});
|