@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
|
@@ -116,7 +116,6 @@ export interface MemoryInfo {
|
|
|
116
116
|
export class TextureMemoryManager {
|
|
117
117
|
private memUsed = 0;
|
|
118
118
|
private loadedTextures: (Texture | null)[] = [];
|
|
119
|
-
private orphanedTextures: Texture[] = [];
|
|
120
119
|
private criticalThreshold: number = 124e6;
|
|
121
120
|
private targetThreshold: number = 0.5;
|
|
122
121
|
private cleanupInterval: number = 5000;
|
|
@@ -145,35 +144,6 @@ export class TextureMemoryManager {
|
|
|
145
144
|
this.updateSettings(settings);
|
|
146
145
|
}
|
|
147
146
|
|
|
148
|
-
/**
|
|
149
|
-
* Add a texture to the orphaned textures list
|
|
150
|
-
*
|
|
151
|
-
* @param texture - The texture to add to the orphaned textures list
|
|
152
|
-
*/
|
|
153
|
-
addToOrphanedTextures(texture: Texture) {
|
|
154
|
-
// if the texture is already in the orphaned textures list add it at the end
|
|
155
|
-
if (this.orphanedTextures.includes(texture)) {
|
|
156
|
-
this.removeFromOrphanedTextures(texture);
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// If the texture can be cleaned up, add it to the orphaned textures list
|
|
160
|
-
if (texture.preventCleanup === false) {
|
|
161
|
-
this.orphanedTextures.push(texture);
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Remove a texture from the orphaned textures list
|
|
167
|
-
*
|
|
168
|
-
* @param texture - The texture to remove from the orphaned textures list
|
|
169
|
-
*/
|
|
170
|
-
removeFromOrphanedTextures(texture: Texture) {
|
|
171
|
-
const index = this.orphanedTextures.indexOf(texture);
|
|
172
|
-
if (index !== -1) {
|
|
173
|
-
this.orphanedTextures.splice(index, 1);
|
|
174
|
-
}
|
|
175
|
-
}
|
|
176
|
-
|
|
177
147
|
/**
|
|
178
148
|
* Set the memory usage of a texture
|
|
179
149
|
*
|
|
@@ -415,4 +385,27 @@ export class TextureMemoryManager {
|
|
|
415
385
|
this.setTextureMemUse = () => {};
|
|
416
386
|
}
|
|
417
387
|
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Destroy the TextureMemoryManager and release all internal references.
|
|
391
|
+
*
|
|
392
|
+
* @remarks
|
|
393
|
+
* Clears the debug-logging interval (if active) and empties all internal
|
|
394
|
+
* texture tracking arrays so that held textures can be garbage-collected.
|
|
395
|
+
*/
|
|
396
|
+
destroy(): void {
|
|
397
|
+
if (this.loggingID) {
|
|
398
|
+
clearInterval(this.loggingID);
|
|
399
|
+
this.loggingID = 0 as unknown as ReturnType<typeof setInterval>;
|
|
400
|
+
}
|
|
401
|
+
// Free GPU resources for every loaded texture before clearing the array
|
|
402
|
+
for (let i = 0; i < this.loadedTextures.length; i++) {
|
|
403
|
+
const texture = this.loadedTextures[i];
|
|
404
|
+
if (texture !== null && texture !== undefined) {
|
|
405
|
+
this.destroyTexture(texture);
|
|
406
|
+
}
|
|
407
|
+
}
|
|
408
|
+
this.loadedTextures = [];
|
|
409
|
+
this.memUsed = 0;
|
|
410
|
+
}
|
|
418
411
|
}
|
|
@@ -89,6 +89,11 @@ export abstract class Platform {
|
|
|
89
89
|
*/
|
|
90
90
|
abstract startLoop(stage: Stage): void;
|
|
91
91
|
|
|
92
|
+
/**
|
|
93
|
+
* Stops the main rendering loop and releases platform resources.
|
|
94
|
+
*/
|
|
95
|
+
abstract stopLoop(): void;
|
|
96
|
+
|
|
92
97
|
/**
|
|
93
98
|
* Fetches a resource from the network.
|
|
94
99
|
* @param url - The URL of the resource to fetch.
|
|
@@ -48,6 +48,7 @@ export class WebPlatform extends Platform {
|
|
|
48
48
|
private useImageWorker: boolean;
|
|
49
49
|
private imageWorkerManager: ImageWorkerManager | null = null;
|
|
50
50
|
private hasWorker = !!self.Worker;
|
|
51
|
+
private stopped = false;
|
|
51
52
|
|
|
52
53
|
constructor(settings: PlatformSettings = {}) {
|
|
53
54
|
super(settings);
|
|
@@ -100,10 +101,12 @@ export class WebPlatform extends Platform {
|
|
|
100
101
|
////////////////////////
|
|
101
102
|
|
|
102
103
|
override startLoop(stage: Stage): void {
|
|
104
|
+
this.stopped = false;
|
|
103
105
|
let isIdle = false;
|
|
104
106
|
let lastFrameTime = 0;
|
|
105
107
|
|
|
106
108
|
const runLoop = (currentTime: number = 0) => {
|
|
109
|
+
if (this.stopped) return;
|
|
107
110
|
const targetFrameTime = stage.targetFrameTime;
|
|
108
111
|
|
|
109
112
|
// Check if we should throttle this frame
|
|
@@ -173,6 +176,16 @@ export class WebPlatform extends Platform {
|
|
|
173
176
|
requestAnimationFrame(runLoop);
|
|
174
177
|
}
|
|
175
178
|
|
|
179
|
+
override stopLoop(): void {
|
|
180
|
+
this.stopped = true;
|
|
181
|
+
if (this.imageWorkerManager !== null) {
|
|
182
|
+
for (const worker of this.imageWorkerManager.workers) {
|
|
183
|
+
worker.terminate();
|
|
184
|
+
}
|
|
185
|
+
this.imageWorkerManager = null;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
176
189
|
////////////////////////
|
|
177
190
|
// Image handling
|
|
178
191
|
////////////////////////
|
|
@@ -67,5 +67,15 @@ export abstract class CoreRenderer {
|
|
|
67
67
|
abstract getQuadCount(): number | null;
|
|
68
68
|
abstract updateViewport(): void;
|
|
69
69
|
abstract updateClearColor(color: number): void;
|
|
70
|
+
abstract destroy(): void;
|
|
70
71
|
getTextureCoords?(node: CoreNode): TextureCoords | undefined;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Delete a GPU buffer previously allocated by this renderer.
|
|
75
|
+
* No-op for renderers that do not use WebGL buffers (e.g. Canvas).
|
|
76
|
+
*/
|
|
77
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
78
|
+
deleteBuffer(_buffer: WebGLBuffer): void {
|
|
79
|
+
// no-op default — overridden by WebGlRenderer
|
|
80
|
+
}
|
|
71
81
|
}
|
|
@@ -255,4 +255,10 @@ export class CanvasRenderer extends CoreRenderer {
|
|
|
255
255
|
getDefaultShaderNode() {
|
|
256
256
|
return null;
|
|
257
257
|
}
|
|
258
|
+
|
|
259
|
+
override destroy(): void {
|
|
260
|
+
// Release canvas 2D context by resizing canvas to 0
|
|
261
|
+
this.canvas.width = 0;
|
|
262
|
+
this.canvas.height = 0;
|
|
263
|
+
}
|
|
258
264
|
}
|
|
@@ -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
|
+
});
|