@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.
- 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/SdfFontHandler.js +7 -5
- package/dist/src/core/text-rendering/SdfFontHandler.js.map +1 -1
- 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/SdfFontHandler.ts +8 -5
- 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
|
@@ -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
|
-
|
|
481
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
507
|
-
private findMaxChildRTTIndex(
|
|
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
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
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
|
-
|
|
517
|
-
|
|
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
|
}
|