@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
@@ -0,0 +1,551 @@
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 2023 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
+ /**
21
+ * RTT (Render To Texture) pipeline unit tests for WebGlRenderer.
22
+ *
23
+ * These tests validate rttNodes ordering, dirty-flag lifecycle, and skip
24
+ * conditions without requiring a real WebGL context. The WebGlRenderer is
25
+ * tested at the level of its internal bookkeeping methods only.
26
+ */
27
+ import { describe, it, expect, beforeEach, vi } from 'vitest';
28
+ import { mock } from 'vitest-mock-extended';
29
+ import {
30
+ CoreNode,
31
+ CoreNodeRenderState,
32
+ UpdateType,
33
+ type CoreNodeProps,
34
+ } from '../../CoreNode.js';
35
+ import type { Stage } from '../../Stage.js';
36
+ import type { CoreRenderer } from '../CoreRenderer.js';
37
+ import { createBound } from '../../lib/utils.js';
38
+ import type { TextureOptions } from '../../CoreTextureManager.js';
39
+ import { Texture } from '../../textures/Texture.js';
40
+ import { WebGlRenderer } from './WebGlRenderer.js';
41
+
42
+ // ---------------------------------------------------------------------------
43
+ // Helpers
44
+ // ---------------------------------------------------------------------------
45
+
46
+ const makeDefaultProps = (): CoreNodeProps => ({
47
+ alpha: 1,
48
+ autosize: false,
49
+ boundsMargin: null,
50
+ clipping: false,
51
+ color: 0xffffffff,
52
+ colorBl: 0xffffffff,
53
+ colorBottom: 0xffffffff,
54
+ colorBr: 0xffffffff,
55
+ colorLeft: 0xffffffff,
56
+ colorRight: 0xffffffff,
57
+ colorTl: 0xffffffff,
58
+ colorTop: 0xffffffff,
59
+ colorTr: 0xffffffff,
60
+ h: 100,
61
+ mount: 0,
62
+ mountX: 0,
63
+ mountY: 0,
64
+ parent: null,
65
+ pivot: 0.5,
66
+ pivotX: 0.5,
67
+ pivotY: 0.5,
68
+ rotation: 0,
69
+ rtt: false,
70
+ scale: 1,
71
+ scaleX: 1,
72
+ scaleY: 1,
73
+ shader: null,
74
+ src: '',
75
+ texture: null,
76
+ textureOptions: {} as TextureOptions,
77
+ w: 100,
78
+ x: 0,
79
+ y: 0,
80
+ zIndex: 0,
81
+ });
82
+
83
+ /**
84
+ * Minimal mock stage that satisfies CoreNode's constructor requirements.
85
+ */
86
+ const makeStage = (): Stage =>
87
+ mock<Stage>({
88
+ strictBound: createBound(0, 0, 1920, 1080),
89
+ preloadBound: createBound(0, 0, 1920, 1080),
90
+ defaultTexture: { state: 'loaded' } as unknown as Texture,
91
+ renderer: mock<CoreRenderer>() as CoreRenderer,
92
+ txManager: {
93
+ createTexture: vi.fn().mockReturnValue({
94
+ state: 'loaded',
95
+ ctxTexture: { framebuffer: {} },
96
+ }),
97
+ } as unknown as Stage['txManager'],
98
+ });
99
+
100
+ // ---------------------------------------------------------------------------
101
+ // RTT ordering tests — exercised via renderToTexture() on a real
102
+ // WebGlRenderer instance created with Object.create so no GL context is needed.
103
+ // ---------------------------------------------------------------------------
104
+
105
+ /**
106
+ * Creates a minimal WebGlRenderer instance with only `rttNodes` initialised.
107
+ * Object.create skips the constructor so no GL context is required.
108
+ * insertRTTNodeInOrder / findMaxChildRTTIndex / renderToTexture only access
109
+ * `this.rttNodes`, so this stub is sufficient to exercise the real production
110
+ * code paths.
111
+ */
112
+ function makeOrderer(): WebGlRenderer {
113
+ const r = Object.create(WebGlRenderer.prototype) as WebGlRenderer;
114
+ r.rttNodes = [];
115
+ return r;
116
+ }
117
+
118
+ // ---------------------------------------------------------------------------
119
+ // Tests
120
+ // ---------------------------------------------------------------------------
121
+
122
+ describe('RTT — rttNodes insertion ordering', () => {
123
+ let stage: Stage;
124
+ let orderer: WebGlRenderer;
125
+
126
+ beforeEach(() => {
127
+ stage = makeStage();
128
+ orderer = makeOrderer();
129
+ });
130
+
131
+ it('adds a single RTT node to an empty list', () => {
132
+ const node = new CoreNode(stage, makeDefaultProps());
133
+ orderer.renderToTexture(node);
134
+ expect(orderer.rttNodes.length).toBe(1);
135
+ expect(orderer.rttNodes[0]).toBe(node);
136
+ });
137
+
138
+ it('does not add the same node twice', () => {
139
+ const node = new CoreNode(stage, makeDefaultProps());
140
+ orderer.renderToTexture(node);
141
+ orderer.renderToTexture(node);
142
+ expect(orderer.rttNodes.length).toBe(1);
143
+ });
144
+
145
+ it('places a child RTT node BEFORE its RTT parent', () => {
146
+ const parent = new CoreNode(stage, makeDefaultProps());
147
+ const child = new CoreNode(stage, makeDefaultProps());
148
+ child.parent = parent;
149
+
150
+ // Parent added first, then child — child must end up before parent
151
+ orderer.renderToTexture(parent);
152
+ orderer.renderToTexture(child);
153
+
154
+ const parentIdx = orderer.rttNodes.indexOf(parent);
155
+ const childIdx = orderer.rttNodes.indexOf(child);
156
+ expect(childIdx).toBeLessThan(parentIdx);
157
+ });
158
+
159
+ it('places a parent RTT node AFTER an already-registered child', () => {
160
+ const parent = new CoreNode(stage, makeDefaultProps());
161
+ const child = new CoreNode(stage, makeDefaultProps());
162
+ child.parent = parent;
163
+
164
+ // Child added first, then parent — parent must end up after child
165
+ orderer.renderToTexture(child);
166
+ orderer.renderToTexture(parent);
167
+
168
+ const parentIdx = orderer.rttNodes.indexOf(parent);
169
+ const childIdx = orderer.rttNodes.indexOf(child);
170
+ expect(childIdx).toBeLessThan(parentIdx);
171
+ });
172
+
173
+ it('handles 3-level nested RTT ordering: grandchild < child < parent', () => {
174
+ const grandparent = new CoreNode(stage, makeDefaultProps());
175
+ const parent = new CoreNode(stage, makeDefaultProps());
176
+ const grandchild = new CoreNode(stage, makeDefaultProps());
177
+ parent.parent = grandparent;
178
+ grandchild.parent = parent;
179
+
180
+ // Insert in arbitrary order
181
+ orderer.renderToTexture(grandparent);
182
+ orderer.renderToTexture(grandchild);
183
+ orderer.renderToTexture(parent);
184
+
185
+ const gpIdx = orderer.rttNodes.indexOf(grandparent);
186
+ const pIdx = orderer.rttNodes.indexOf(parent);
187
+ const gcIdx = orderer.rttNodes.indexOf(grandchild);
188
+
189
+ expect(gcIdx).toBeLessThan(pIdx);
190
+ expect(pIdx).toBeLessThan(gpIdx);
191
+ });
192
+
193
+ it('inserts sibling RTT nodes in insertion order (a before b)', () => {
194
+ const root = new CoreNode(stage, makeDefaultProps());
195
+ const a = new CoreNode(stage, makeDefaultProps());
196
+ const b = new CoreNode(stage, makeDefaultProps());
197
+ a.parent = root;
198
+ b.parent = root;
199
+
200
+ orderer.renderToTexture(a);
201
+ orderer.renderToTexture(b);
202
+
203
+ expect(orderer.rttNodes.length).toBe(2);
204
+ // Siblings have no ordering constraint relative to each other, so the
205
+ // expected behaviour is that insertion order is preserved.
206
+ expect(orderer.rttNodes.indexOf(a)).toBeLessThan(
207
+ orderer.rttNodes.indexOf(b),
208
+ );
209
+ });
210
+ });
211
+
212
+ // ---------------------------------------------------------------------------
213
+ // RTT dirty-flag lifecycle — exercised directly on CoreNode fields
214
+ // (no GL context needed)
215
+ // ---------------------------------------------------------------------------
216
+
217
+ describe('RTT — hasRTTupdates dirty-flag lifecycle', () => {
218
+ let stage: Stage;
219
+
220
+ beforeEach(() => {
221
+ stage = makeStage();
222
+ });
223
+
224
+ it('hasRTTupdates starts as false on a new node', () => {
225
+ const node = new CoreNode(stage, makeDefaultProps());
226
+ expect(node.hasRTTupdates).toBe(false);
227
+ });
228
+
229
+ it('notifyParentRTTOfUpdate sets hasRTTupdates on the RTT ancestor', () => {
230
+ const rttParent = new CoreNode(stage, {
231
+ ...makeDefaultProps(),
232
+ rtt: false,
233
+ });
234
+ const child = new CoreNode(stage, makeDefaultProps());
235
+ child.parent = rttParent;
236
+
237
+ // Manually wire rttParent flag used by notifyParentRTTOfUpdate path
238
+ // (normally set by markChildrenWithRTT when rtt is enabled on rttParent)
239
+ // Simulate the state that exists after rtt=true is set on rttParent
240
+ rttParent['props'].rtt = true;
241
+ child.parentHasRenderTexture = true;
242
+ child.rttParent = rttParent;
243
+
244
+ rttParent.hasRTTupdates = false;
245
+
246
+ // Call the protected method via cast
247
+ (
248
+ child as unknown as { notifyParentRTTOfUpdate(): void }
249
+ ).notifyParentRTTOfUpdate();
250
+
251
+ expect(rttParent.hasRTTupdates).toBe(true);
252
+ });
253
+
254
+ it('notifyParentRTTOfUpdate is a no-op when node has no RTT ancestor', () => {
255
+ const node = new CoreNode(stage, makeDefaultProps());
256
+ // No parent — should not throw
257
+ expect(() => {
258
+ (
259
+ node as unknown as { notifyParentRTTOfUpdate(): void }
260
+ ).notifyParentRTTOfUpdate();
261
+ }).not.toThrow();
262
+ expect(node.hasRTTupdates).toBe(false);
263
+ });
264
+
265
+ it('hasRTTupdates can be reset to false', () => {
266
+ const node = new CoreNode(stage, makeDefaultProps());
267
+ node.hasRTTupdates = true;
268
+ node.hasRTTupdates = false;
269
+ expect(node.hasRTTupdates).toBe(false);
270
+ });
271
+ });
272
+
273
+ // ---------------------------------------------------------------------------
274
+ // RTT parentHasRenderTexture propagation
275
+ // ---------------------------------------------------------------------------
276
+
277
+ describe('RTT — parentHasRenderTexture propagation', () => {
278
+ let stage: Stage;
279
+
280
+ beforeEach(() => {
281
+ stage = makeStage();
282
+ });
283
+
284
+ it('parentHasRenderTexture is false by default', () => {
285
+ const node = new CoreNode(stage, makeDefaultProps());
286
+ expect(node.parentHasRenderTexture).toBe(false);
287
+ });
288
+
289
+ it('markChildrenWithRTT sets parentHasRenderTexture=true on direct children', () => {
290
+ const parent = new CoreNode(stage, makeDefaultProps());
291
+ const child = new CoreNode(stage, makeDefaultProps());
292
+ child.parent = parent;
293
+
294
+ // Call private method directly
295
+ (
296
+ parent as unknown as { markChildrenWithRTT(): void }
297
+ ).markChildrenWithRTT();
298
+
299
+ expect(child.parentHasRenderTexture).toBe(true);
300
+ });
301
+
302
+ it('markChildrenWithRTT propagates to grandchildren', () => {
303
+ const parent = new CoreNode(stage, makeDefaultProps());
304
+ const child = new CoreNode(stage, makeDefaultProps());
305
+ const grandchild = new CoreNode(stage, makeDefaultProps());
306
+ child.parent = parent;
307
+ grandchild.parent = child;
308
+
309
+ (
310
+ parent as unknown as { markChildrenWithRTT(): void }
311
+ ).markChildrenWithRTT();
312
+
313
+ expect(child.parentHasRenderTexture).toBe(true);
314
+ expect(grandchild.parentHasRenderTexture).toBe(true);
315
+ });
316
+ });
317
+
318
+ // ---------------------------------------------------------------------------
319
+ // RTT parentRenderTexture getter — uncached parent-chain walk
320
+ // (this is Bottleneck 1 in the perf analysis)
321
+ // ---------------------------------------------------------------------------
322
+
323
+ describe('RTT — parentRenderTexture getter', () => {
324
+ let stage: Stage;
325
+
326
+ beforeEach(() => {
327
+ stage = makeStage();
328
+ });
329
+
330
+ it('returns null when there is no RTT ancestor', () => {
331
+ const node = new CoreNode(stage, makeDefaultProps());
332
+ expect(node.parentRenderTexture).toBe(null);
333
+ });
334
+
335
+ it('returns the nearest RTT ancestor', () => {
336
+ const rttAncestor = new CoreNode(stage, makeDefaultProps());
337
+ rttAncestor['props'].rtt = true;
338
+ const child = new CoreNode(stage, makeDefaultProps());
339
+ child.parent = rttAncestor;
340
+
341
+ expect(child.parentRenderTexture).toBe(rttAncestor);
342
+ });
343
+
344
+ it('returns the NEAREST RTT ancestor in a nested chain', () => {
345
+ const outer = new CoreNode(stage, makeDefaultProps());
346
+ const inner = new CoreNode(stage, makeDefaultProps());
347
+ const leaf = new CoreNode(stage, makeDefaultProps());
348
+ outer['props'].rtt = true;
349
+ inner['props'].rtt = true;
350
+ inner.parent = outer;
351
+ leaf.parent = inner;
352
+
353
+ // leaf's nearest RTT ancestor is inner, not outer
354
+ expect(leaf.parentRenderTexture).toBe(inner);
355
+ });
356
+ });
357
+
358
+ // ---------------------------------------------------------------------------
359
+ // RTT — renderRTTNodes skip conditions
360
+ // Validated via a lightweight simulation of the skip logic
361
+ // ---------------------------------------------------------------------------
362
+
363
+ describe('RTT — renderRTTNodes skip conditions', () => {
364
+ let stage: Stage;
365
+
366
+ beforeEach(() => {
367
+ stage = makeStage();
368
+ });
369
+
370
+ /**
371
+ * Simulate the exact skip gates in renderRTTNodes() for a single node.
372
+ * Returns true if the node would be rendered, false if skipped.
373
+ */
374
+ const wouldRender = (
375
+ node: CoreNode,
376
+ texture: { state: string } | null,
377
+ ): boolean => {
378
+ if (node.hasRTTupdates === false) return false;
379
+ if (node.worldAlpha === 0) return false;
380
+ if (node.renderState === CoreNodeRenderState.OutOfBounds) return false;
381
+ if (texture === null || texture.state !== 'loaded') return false;
382
+ return true;
383
+ };
384
+
385
+ it('skips a node when hasRTTupdates is false', () => {
386
+ const node = new CoreNode(stage, makeDefaultProps());
387
+ node.hasRTTupdates = false;
388
+ node.worldAlpha = 1;
389
+ node.renderState = CoreNodeRenderState.InBounds;
390
+ expect(wouldRender(node, { state: 'loaded' })).toBe(false);
391
+ });
392
+
393
+ it('renders a node when hasRTTupdates is true and all other conditions pass', () => {
394
+ const node = new CoreNode(stage, makeDefaultProps());
395
+ node.hasRTTupdates = true;
396
+ node.worldAlpha = 1;
397
+ node.renderState = CoreNodeRenderState.InBounds;
398
+ expect(wouldRender(node, { state: 'loaded' })).toBe(true);
399
+ });
400
+
401
+ it('skips a node when worldAlpha is 0', () => {
402
+ const node = new CoreNode(stage, makeDefaultProps());
403
+ node.hasRTTupdates = true;
404
+ node.worldAlpha = 0;
405
+ node.renderState = CoreNodeRenderState.InBounds;
406
+ expect(wouldRender(node, { state: 'loaded' })).toBe(false);
407
+ });
408
+
409
+ it('skips a node when renderState is OutOfBounds', () => {
410
+ const node = new CoreNode(stage, makeDefaultProps());
411
+ node.hasRTTupdates = true;
412
+ node.worldAlpha = 1;
413
+ node.renderState = CoreNodeRenderState.OutOfBounds;
414
+ expect(wouldRender(node, { state: 'loaded' })).toBe(false);
415
+ });
416
+
417
+ it('skips a node when texture is null', () => {
418
+ const node = new CoreNode(stage, makeDefaultProps());
419
+ node.hasRTTupdates = true;
420
+ node.worldAlpha = 1;
421
+ node.renderState = CoreNodeRenderState.InBounds;
422
+ expect(wouldRender(node, null)).toBe(false);
423
+ });
424
+
425
+ it('skips a node when texture is not loaded', () => {
426
+ const node = new CoreNode(stage, makeDefaultProps());
427
+ node.hasRTTupdates = true;
428
+ node.worldAlpha = 1;
429
+ node.renderState = CoreNodeRenderState.InBounds;
430
+ expect(wouldRender(node, { state: 'loading' })).toBe(false);
431
+ });
432
+ });
433
+
434
+ // ---------------------------------------------------------------------------
435
+ // RTT_NOTIFY_MASK — gate on notifyParentRTTOfUpdate() inside update()
436
+ //
437
+ // The mask ensures RTT surfaces are re-rendered only when a visually relevant
438
+ // UpdateType flag is present. Non-visual cascade flags (Children, RenderBounds,
439
+ // RenderState, ParentRenderTexture, Autosize) must NOT trigger re-renders.
440
+ // ---------------------------------------------------------------------------
441
+
442
+ describe('RTT — RTT_NOTIFY_MASK gate in update()', () => {
443
+ // Minimal clipping rect required by CoreNode.update()
444
+ const clippingRect = { x: 0, y: 0, w: 1920, h: 1080, valid: false };
445
+
446
+ let stage: Stage;
447
+
448
+ // Builds a child node wired into an RTT parent so the
449
+ // `parentHasRenderTexture === true` branch in update() is reachable.
450
+ function makeRttChild() {
451
+ const rttParent = new CoreNode(stage, makeDefaultProps());
452
+ rttParent['props'].rtt = true;
453
+
454
+ const child = new CoreNode(stage, makeDefaultProps());
455
+ child.parent = rttParent;
456
+ child.parentHasRenderTexture = true;
457
+ child.rttParent = rttParent;
458
+ child.hasRTTupdates = false;
459
+
460
+ return { rttParent, child };
461
+ }
462
+
463
+ beforeEach(() => {
464
+ stage = makeStage();
465
+ });
466
+
467
+ it('fires notifyParentRTTOfUpdate for a visual flag (PremultipliedColors)', () => {
468
+ const { rttParent, child } = makeRttChild();
469
+ const spy = vi.spyOn(
470
+ child as unknown as { notifyParentRTTOfUpdate(): void },
471
+ 'notifyParentRTTOfUpdate',
472
+ );
473
+ rttParent.hasRTTupdates = false;
474
+
475
+ child.updateType = UpdateType.PremultipliedColors;
476
+ child.update(0, clippingRect);
477
+
478
+ expect(spy).toHaveBeenCalledOnce();
479
+ });
480
+
481
+ it('fires notifyParentRTTOfUpdate for a visual flag (WorldAlpha)', () => {
482
+ const { child } = makeRttChild();
483
+ const spy = vi.spyOn(
484
+ child as unknown as { notifyParentRTTOfUpdate(): void },
485
+ 'notifyParentRTTOfUpdate',
486
+ );
487
+
488
+ child.updateType = UpdateType.WorldAlpha;
489
+ child.update(0, clippingRect);
490
+
491
+ expect(spy).toHaveBeenCalledOnce();
492
+ });
493
+
494
+ it('does NOT fire notifyParentRTTOfUpdate for Children flag alone', () => {
495
+ const { child } = makeRttChild();
496
+ const spy = vi.spyOn(
497
+ child as unknown as { notifyParentRTTOfUpdate(): void },
498
+ 'notifyParentRTTOfUpdate',
499
+ );
500
+
501
+ // Children is not in RTT_NOTIFY_MASK and hasRTTupdates is false
502
+ child.updateType = UpdateType.Children;
503
+ child.hasRTTupdates = false;
504
+ child.update(0, clippingRect);
505
+
506
+ expect(spy).not.toHaveBeenCalled();
507
+ });
508
+
509
+ it('does NOT fire notifyParentRTTOfUpdate for Autosize flag alone', () => {
510
+ const { child } = makeRttChild();
511
+ const spy = vi.spyOn(
512
+ child as unknown as { notifyParentRTTOfUpdate(): void },
513
+ 'notifyParentRTTOfUpdate',
514
+ );
515
+
516
+ child.updateType = UpdateType.Autosize;
517
+ child.hasRTTupdates = false;
518
+ child.update(0, clippingRect);
519
+
520
+ expect(spy).not.toHaveBeenCalled();
521
+ });
522
+
523
+ it('fires notifyParentRTTOfUpdate when hasRTTupdates=true even for non-visual flag (Children)', () => {
524
+ const { child } = makeRttChild();
525
+ const spy = vi.spyOn(
526
+ child as unknown as { notifyParentRTTOfUpdate(): void },
527
+ 'notifyParentRTTOfUpdate',
528
+ );
529
+
530
+ // hasRTTupdates=true short-circuits the mask check
531
+ child.updateType = UpdateType.Children;
532
+ child.hasRTTupdates = true;
533
+ child.update(0, clippingRect);
534
+
535
+ expect(spy).toHaveBeenCalledOnce();
536
+ });
537
+
538
+ it('does NOT fire notifyParentRTTOfUpdate when parentHasRenderTexture is false, even with visual flag', () => {
539
+ const { child } = makeRttChild();
540
+ const spy = vi.spyOn(
541
+ child as unknown as { notifyParentRTTOfUpdate(): void },
542
+ 'notifyParentRTTOfUpdate',
543
+ );
544
+
545
+ child.parentHasRenderTexture = false;
546
+ child.updateType = UpdateType.PremultipliedColors;
547
+ child.update(0, clippingRect);
548
+
549
+ expect(spy).not.toHaveBeenCalled();
550
+ });
551
+ });
@@ -474,28 +474,27 @@ export class WebGlRenderer extends CoreRenderer {
474
474
  private insertRTTNodeInOrder(node: CoreNode) {
475
475
  let insertIndex = this.rttNodes.length; // Default to the end of the array
476
476
 
477
+ // Build a one-shot index map so all lookups below are O(1) instead of O(n).
478
+ const rttNodes = this.rttNodes;
479
+ const indexMap = new Map<number, number>();
480
+ for (let i = 0; i < rttNodes.length; i++) {
481
+ indexMap.set(rttNodes[i]!.id, i);
482
+ }
483
+
477
484
  // 1. Traverse upwards to ensure the node is placed before its RTT parent (if any).
478
485
  let currentNode: CoreNode = node;
479
- while (currentNode) {
480
- if (!currentNode.parent) {
481
- break;
482
- }
483
-
484
- const parentIndex = this.rttNodes.indexOf(currentNode.parent);
485
- if (parentIndex !== -1) {
486
- // Found an RTT parent in the list; set insertIndex to place node before the parent
486
+ while (currentNode.parent !== null) {
487
+ const parentIndex = indexMap.get(currentNode.parent.id);
488
+ if (parentIndex !== undefined) {
487
489
  insertIndex = parentIndex;
488
490
  break;
489
491
  }
490
-
491
492
  currentNode = currentNode.parent;
492
493
  }
493
494
 
494
495
  // 2. Traverse downwards to ensure the node is placed after any RTT children.
495
- // Look through each child recursively to see if any are already in rttNodes.
496
- const maxChildIndex = this.findMaxChildRTTIndex(node);
496
+ const maxChildIndex = this.findMaxChildRTTIndex(node, indexMap);
497
497
  if (maxChildIndex !== -1) {
498
- // Adjust insertIndex to be after the last child RTT node
499
498
  insertIndex = Math.max(insertIndex, maxChildIndex + 1);
500
499
  }
501
500
 
@@ -503,25 +502,25 @@ export class WebGlRenderer extends CoreRenderer {
503
502
  this.rttNodes.splice(insertIndex, 0, node);
504
503
  }
505
504
 
506
- // Helper function to find the highest index of any RTT children of a node within rttNodes
507
- private findMaxChildRTTIndex(node: CoreNode): number {
505
+ // Iterative DFS to find the highest rttNodes index among all RTT descendants of node.
506
+ private findMaxChildRTTIndex(
507
+ node: CoreNode,
508
+ indexMap: Map<number, number>,
509
+ ): number {
508
510
  let maxIndex = -1;
509
-
510
- const traverseChildren = (currentNode: CoreNode) => {
511
- const currentIndex = this.rttNodes.indexOf(currentNode);
512
- if (currentIndex !== -1) {
513
- maxIndex = Math.max(maxIndex, currentIndex);
511
+ // Explicit stack avoids recursive arrow function allocation and call-stack growth.
512
+ const stack: CoreNode[] = [node];
513
+ while (stack.length !== 0) {
514
+ const current = stack.pop()!;
515
+ const idx = indexMap.get(current.id);
516
+ if (idx !== undefined && idx > maxIndex) {
517
+ maxIndex = idx;
514
518
  }
515
-
516
- // Recursively check all children of the current node
517
- for (const child of currentNode.children) {
518
- traverseChildren(child);
519
+ const children = current.children;
520
+ for (let i = 0; i < children.length; i++) {
521
+ stack.push(children[i]!);
519
522
  }
520
- };
521
-
522
- // Start traversal directly with the provided node
523
- traverseChildren(node);
524
-
523
+ }
525
524
  return maxIndex;
526
525
  }
527
526
 
@@ -724,4 +723,15 @@ export class WebGlRenderer extends CoreRenderer {
724
723
  normalized: normalizedColor,
725
724
  };
726
725
  }
726
+
727
+ override destroy(): void {
728
+ const loseCtx = this.glw.getExtension(
729
+ 'WEBGL_lose_context',
730
+ ) as WEBGL_lose_context | null;
731
+ loseCtx?.loseContext();
732
+ }
733
+
734
+ override deleteBuffer(buffer: WebGLBuffer): void {
735
+ this.glw.deleteBuffer(buffer);
736
+ }
727
737
  }