@solidtv/renderer 1.3.1 → 1.4.0
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/dist/src/core/CoreNode.d.ts +0 -10
- package/dist/src/core/CoreNode.js +7 -14
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/Stage.d.ts +7 -15
- package/dist/src/core/Stage.js +23 -41
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.js +4 -3
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/ImageWorker.d.ts +16 -1
- package/dist/src/core/lib/ImageWorker.js +52 -17
- package/dist/src/core/lib/ImageWorker.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +2 -32
- package/dist/src/core/lib/WebGlContextWrapper.js +26 -81
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/utils.d.ts +0 -1
- package/dist/src/core/lib/utils.js +0 -3
- package/dist/src/core/lib/utils.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.js +1 -0
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/CoreRenderer.d.ts +0 -1
- package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
- package/dist/src/core/renderers/CoreShaderNode.d.ts +16 -0
- package/dist/src/core/renderers/CoreShaderNode.js +22 -0
- package/dist/src/core/renderers/CoreShaderNode.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +23 -3
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.d.ts +0 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js +0 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasTexture.d.ts +1 -1
- package/dist/src/core/renderers/canvas/CanvasTexture.js +25 -6
- package/dist/src/core/renderers/canvas/CanvasTexture.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +1 -7
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +9 -13
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderNode.d.ts +2 -4
- package/dist/src/core/renderers/webgl/WebGlShaderNode.js +0 -1
- package/dist/src/core/renderers/webgl/WebGlShaderNode.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +0 -7
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +3 -19
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/RendererUtils.d.ts +0 -33
- package/dist/src/core/renderers/webgl/internal/RendererUtils.js +0 -50
- package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +0 -1
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasFontHandler.js +0 -5
- package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
- package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +1 -0
- package/dist/src/core/text-rendering/CanvasTextRenderer.js +23 -0
- package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +1 -0
- package/dist/src/core/text-rendering/SdfTextRenderer.js +50 -4
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextRenderer.d.ts +7 -0
- package/dist/src/core/textures/ImageTexture.js +11 -0
- package/dist/src/core/textures/ImageTexture.js.map +1 -1
- package/dist/src/core/textures/SubTexture.d.ts +1 -0
- package/dist/src/core/textures/SubTexture.js +12 -0
- package/dist/src/core/textures/SubTexture.js.map +1 -1
- package/dist/src/core/textures/Texture.js +10 -0
- package/dist/src/core/textures/Texture.js.map +1 -1
- package/dist/src/main-api/Inspector.d.ts +2 -1
- package/dist/src/main-api/Inspector.js +43 -15
- package/dist/src/main-api/Inspector.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +16 -10
- package/dist/src/main-api/Renderer.js +3 -3
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.d.ts +1 -1
- package/dist/src/utils.js +2 -2
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.ts +8 -26
- package/src/core/Stage.ts +24 -49
- package/src/core/TextureMemoryManager.ts +4 -3
- package/src/core/lib/ImageWorker.test.ts +106 -0
- package/src/core/lib/ImageWorker.ts +60 -22
- package/src/core/lib/WebGlContextWrapper.ts +32 -101
- package/src/core/lib/utils.ts +0 -4
- package/src/core/platforms/web/WebPlatform.outOfMemory.test.ts +1 -0
- package/src/core/platforms/web/WebPlatform.ts +1 -0
- package/src/core/renderers/CoreRenderer.ts +0 -1
- package/src/core/renderers/CoreShaderNode.test.ts +53 -0
- package/src/core/renderers/CoreShaderNode.ts +23 -0
- package/src/core/renderers/canvas/CanvasRenderer.test.ts +34 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +33 -4
- package/src/core/renderers/canvas/CanvasShaderNode.ts +0 -1
- package/src/core/renderers/canvas/CanvasTexture.test.ts +31 -0
- package/src/core/renderers/canvas/CanvasTexture.ts +35 -7
- package/src/core/renderers/webgl/WebGlRenderer.ts +9 -26
- package/src/core/renderers/webgl/WebGlShaderNode.ts +2 -4
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +3 -20
- package/src/core/renderers/webgl/internal/RendererUtils.ts +0 -87
- package/src/core/renderers/webgl/internal/ShaderUtils.ts +0 -1
- package/src/core/text-rendering/CanvasFontHandler.ts +0 -6
- package/src/core/text-rendering/CanvasTextRenderer.ts +26 -0
- package/src/core/text-rendering/SdfTextRenderer.test.ts +150 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +54 -4
- package/src/core/text-rendering/TextRenderer.ts +7 -0
- package/src/core/textures/ImageTexture.ts +18 -0
- package/src/core/textures/SubTexture.test.ts +45 -0
- package/src/core/textures/SubTexture.ts +13 -0
- package/src/core/textures/Texture.ts +12 -0
- package/src/main-api/Inspector.ts +46 -16
- package/src/main-api/Renderer.ts +20 -14
- package/src/utils.ts +1 -2
- package/src/core/renderers/webgl/internal/WebGlUtils.ts +0 -16
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
|
2
|
+
import type { CoreTextNodeProps } from '../CoreTextNode.js';
|
|
3
|
+
|
|
4
|
+
// Mock the font handler so renderText/generateTextLayout can run without a
|
|
5
|
+
// real loaded font. getFontData is only invoked on a layout-cache MISS, so the
|
|
6
|
+
// call count is our probe for cache hits vs misses.
|
|
7
|
+
vi.mock('./SdfFontHandler.js', () => {
|
|
8
|
+
const fontData = {
|
|
9
|
+
data: {
|
|
10
|
+
common: { base: 0, scaleW: 512, scaleH: 512, lineHeight: 50 },
|
|
11
|
+
info: { size: 42 },
|
|
12
|
+
distanceField: { distanceRange: 4 },
|
|
13
|
+
},
|
|
14
|
+
glyphMap: new Map(),
|
|
15
|
+
kernings: {},
|
|
16
|
+
atlasTexture: {},
|
|
17
|
+
metrics: {},
|
|
18
|
+
maxCharHeight: 50,
|
|
19
|
+
};
|
|
20
|
+
const metrics = {
|
|
21
|
+
ascender: 40,
|
|
22
|
+
descender: -10,
|
|
23
|
+
lineGap: 0,
|
|
24
|
+
capHeight: 30,
|
|
25
|
+
xHeight: 20,
|
|
26
|
+
};
|
|
27
|
+
return {
|
|
28
|
+
type: 'sdf',
|
|
29
|
+
init: vi.fn(),
|
|
30
|
+
getFontData: vi.fn(() => fontData),
|
|
31
|
+
getFontMetrics: vi.fn(() => metrics),
|
|
32
|
+
measureText: vi.fn((text: string) => text.length * 10),
|
|
33
|
+
getAtlas: vi.fn(() => null),
|
|
34
|
+
};
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
import SdfTextRenderer from './SdfTextRenderer.js';
|
|
38
|
+
import * as SdfFontHandler from './SdfFontHandler.js';
|
|
39
|
+
|
|
40
|
+
const makeProps = (text: string): CoreTextNodeProps =>
|
|
41
|
+
({
|
|
42
|
+
text,
|
|
43
|
+
fontFamily: 'Test',
|
|
44
|
+
fontStyle: 'normal',
|
|
45
|
+
fontSize: 42,
|
|
46
|
+
letterSpacing: 0,
|
|
47
|
+
lineHeight: 0,
|
|
48
|
+
maxHeight: 0,
|
|
49
|
+
maxWidth: 100000,
|
|
50
|
+
maxLines: 0,
|
|
51
|
+
textAlign: 'left',
|
|
52
|
+
wordBreak: 'normal',
|
|
53
|
+
overflowSuffix: '',
|
|
54
|
+
} as unknown as CoreTextNodeProps);
|
|
55
|
+
|
|
56
|
+
const initRenderer = (cacheSize: number): void => {
|
|
57
|
+
const fakeStage = {
|
|
58
|
+
options: { textLayoutCacheSize: cacheSize },
|
|
59
|
+
shManager: {
|
|
60
|
+
registerShaderType: vi.fn(),
|
|
61
|
+
createShader: vi.fn(() => ({})),
|
|
62
|
+
},
|
|
63
|
+
};
|
|
64
|
+
SdfTextRenderer.init(fakeStage as never);
|
|
65
|
+
};
|
|
66
|
+
|
|
67
|
+
const render = (text: string): void => {
|
|
68
|
+
SdfTextRenderer.renderText(makeProps(text));
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
describe('SdfTextRenderer layout cache', () => {
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
// Empty the module-level cache between tests, then reset call counts.
|
|
74
|
+
initRenderer(0);
|
|
75
|
+
SdfTextRenderer.cleanup();
|
|
76
|
+
vi.clearAllMocks();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('reuses the cached layout for identical strings', () => {
|
|
80
|
+
initRenderer(10);
|
|
81
|
+
|
|
82
|
+
render('Badge');
|
|
83
|
+
render('Badge');
|
|
84
|
+
|
|
85
|
+
// Second render is a cache hit: no fresh layout generation.
|
|
86
|
+
expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(1);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('caches long strings too (no length-based skip)', () => {
|
|
90
|
+
initRenderer(10);
|
|
91
|
+
const long = 'x'.repeat(500);
|
|
92
|
+
|
|
93
|
+
render(long);
|
|
94
|
+
render(long);
|
|
95
|
+
|
|
96
|
+
// Bounded purely by the LRU cap, not by length.
|
|
97
|
+
expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(1);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it('cleanup trims to the cap and evicts least-recently-used first', () => {
|
|
101
|
+
initRenderer(2);
|
|
102
|
+
|
|
103
|
+
render('A');
|
|
104
|
+
render('B');
|
|
105
|
+
render('C');
|
|
106
|
+
// Re-access 'A' so it becomes most-recently-used; 'B' is now the LRU.
|
|
107
|
+
render('A');
|
|
108
|
+
|
|
109
|
+
SdfTextRenderer.cleanup();
|
|
110
|
+
|
|
111
|
+
vi.clearAllMocks();
|
|
112
|
+
render('B'); // evicted -> miss
|
|
113
|
+
render('A'); // survived -> hit
|
|
114
|
+
render('C'); // survived -> hit
|
|
115
|
+
|
|
116
|
+
expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(1);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('cleanup is a no-op while under the cap', () => {
|
|
120
|
+
initRenderer(10);
|
|
121
|
+
|
|
122
|
+
render('one');
|
|
123
|
+
render('two');
|
|
124
|
+
|
|
125
|
+
SdfTextRenderer.cleanup();
|
|
126
|
+
|
|
127
|
+
vi.clearAllMocks();
|
|
128
|
+
render('one'); // still cached -> hit
|
|
129
|
+
render('two'); // still cached -> hit
|
|
130
|
+
|
|
131
|
+
expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(0);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('SdfTextRenderer lazy shader compile', () => {
|
|
136
|
+
it('registers the SDF shader at init but defers compilation', () => {
|
|
137
|
+
const shManager = {
|
|
138
|
+
registerShaderType: vi.fn(),
|
|
139
|
+
createShader: vi.fn(() => ({})),
|
|
140
|
+
};
|
|
141
|
+
const fakeStage = { options: { textLayoutCacheSize: 10 }, shManager };
|
|
142
|
+
|
|
143
|
+
SdfTextRenderer.init(fakeStage as never);
|
|
144
|
+
|
|
145
|
+
// Registration is cheap and stays at boot; the expensive compile + link is
|
|
146
|
+
// deferred until the first SDF glyph actually renders (see getSdfShader).
|
|
147
|
+
expect(shManager.registerShaderType).toHaveBeenCalledTimes(1);
|
|
148
|
+
expect(shManager.createShader).not.toHaveBeenCalled();
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -24,13 +24,41 @@ const type = 'sdf' as const;
|
|
|
24
24
|
|
|
25
25
|
let sdfShader: WebGlShaderNode | null = null;
|
|
26
26
|
|
|
27
|
+
// Upper bound on layoutCache entries, enforced on idle via `cleanup`.
|
|
28
|
+
// Overridden from stage options in `init`. The cache is allowed to grow past
|
|
29
|
+
// this during active rendering and is trimmed back to it when the stage idles.
|
|
30
|
+
let maxLayoutCacheSize = 250;
|
|
31
|
+
|
|
27
32
|
// Initialize the SDF text renderer
|
|
28
33
|
const init = (stage: Stage): void => {
|
|
29
34
|
SdfFontHandler.init();
|
|
30
35
|
|
|
31
|
-
|
|
36
|
+
const configuredCacheSize = stage.options.textLayoutCacheSize;
|
|
37
|
+
if (configuredCacheSize !== undefined) {
|
|
38
|
+
maxLayoutCacheSize = configuredCacheSize;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Register the SDF shader, but defer the (expensive) compile + link until the
|
|
42
|
+
// first SDF glyph actually renders — see getSdfShader / renderQuads. SDF fonts
|
|
43
|
+
// load asynchronously, so the first frame almost never needs the shader, and
|
|
44
|
+
// compiling it during Stage construction sits on the critical path to first
|
|
45
|
+
// paint for no benefit.
|
|
32
46
|
stage.shManager.registerShaderType('Sdf', Sdf);
|
|
33
|
-
sdfShader =
|
|
47
|
+
sdfShader = null;
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Lazily compile (and memoize) the SDF shader on first use.
|
|
52
|
+
*
|
|
53
|
+
* @remarks
|
|
54
|
+
* The shader program is compiled and linked the first time an SDF glyph is
|
|
55
|
+
* actually drawn, not at boot. Subsequent calls return the cached node.
|
|
56
|
+
*/
|
|
57
|
+
const getSdfShader = (stage: Stage): WebGlShaderNode => {
|
|
58
|
+
if (sdfShader === null) {
|
|
59
|
+
sdfShader = stage.shManager.createShader('Sdf') as WebGlShaderNode;
|
|
60
|
+
}
|
|
61
|
+
return sdfShader;
|
|
34
62
|
};
|
|
35
63
|
|
|
36
64
|
const font: FontHandler = SdfFontHandler;
|
|
@@ -58,6 +86,11 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
|
|
|
58
86
|
const cacheKey = getLayoutCacheKey(props);
|
|
59
87
|
let layout = layoutCache.get(cacheKey);
|
|
60
88
|
if (layout !== undefined) {
|
|
89
|
+
// Refresh LRU recency: re-insert moves the key to the most-recently-used
|
|
90
|
+
// end so idle `cleanup` evicts genuinely cold entries first. renderText
|
|
91
|
+
// runs on text/layout change, not per frame, so this re-insert is cheap.
|
|
92
|
+
layoutCache.delete(cacheKey);
|
|
93
|
+
layoutCache.set(cacheKey, layout);
|
|
61
94
|
return {
|
|
62
95
|
remainingLines: 0,
|
|
63
96
|
hasRemainingText: false,
|
|
@@ -126,6 +159,8 @@ const renderQuads = (
|
|
|
126
159
|
const webGlRenderer = renderer as WebGlRenderer;
|
|
127
160
|
const cache = renderProps.sdfCache;
|
|
128
161
|
const ctxTexture = atlasTexture.ctxTexture as WebGlCtxTexture;
|
|
162
|
+
// Compiles on the first real SDF draw; cheap memoized lookup thereafter.
|
|
163
|
+
const shader = getSdfShader(webGlRenderer.stage);
|
|
129
164
|
|
|
130
165
|
// --- Cache-hit fast path ------------------------------------------------
|
|
131
166
|
if (cache !== undefined && cache.vertices !== null) {
|
|
@@ -152,7 +187,7 @@ const renderQuads = (
|
|
|
152
187
|
layout.height,
|
|
153
188
|
renderProps.parentHasRenderTexture,
|
|
154
189
|
renderProps.framebufferDimensions,
|
|
155
|
-
|
|
190
|
+
shader,
|
|
156
191
|
);
|
|
157
192
|
return null;
|
|
158
193
|
}
|
|
@@ -174,7 +209,7 @@ const renderQuads = (
|
|
|
174
209
|
layout.height,
|
|
175
210
|
renderProps.parentHasRenderTexture,
|
|
176
211
|
renderProps.framebufferDimensions,
|
|
177
|
-
|
|
212
|
+
shader,
|
|
178
213
|
);
|
|
179
214
|
|
|
180
215
|
// Snapshot the written vertex data into the cache for future frames
|
|
@@ -357,6 +392,20 @@ const generateTextLayout = (
|
|
|
357
392
|
};
|
|
358
393
|
};
|
|
359
394
|
|
|
395
|
+
/**
|
|
396
|
+
* Trim the layout cache back down to `maxLayoutCacheSize`, evicting the
|
|
397
|
+
* least-recently-used entries first. Called when the stage goes idle so this
|
|
398
|
+
* never competes with active rendering. A fresh iterator is taken each step so
|
|
399
|
+
* we always delete the current front (oldest) key without iterator-invalidation
|
|
400
|
+
* concerns; this runs at most once per idle transition and only when over cap.
|
|
401
|
+
*/
|
|
402
|
+
const cleanup = (): void => {
|
|
403
|
+
while (layoutCache.size > maxLayoutCacheSize) {
|
|
404
|
+
const oldest = layoutCache.keys().next().value as string;
|
|
405
|
+
layoutCache.delete(oldest);
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
|
|
360
409
|
/**
|
|
361
410
|
* SDF Text Renderer - implements TextRenderer interface
|
|
362
411
|
*/
|
|
@@ -367,6 +416,7 @@ const SdfTextRenderer = {
|
|
|
367
416
|
addQuads,
|
|
368
417
|
renderQuads,
|
|
369
418
|
init,
|
|
419
|
+
cleanup,
|
|
370
420
|
};
|
|
371
421
|
|
|
372
422
|
export default SdfTextRenderer;
|
|
@@ -435,6 +435,13 @@ export interface TextRenderer {
|
|
|
435
435
|
renderProps: TextRenderProps,
|
|
436
436
|
) => void | SdfRenderOp | null;
|
|
437
437
|
init: (stage: Stage) => void;
|
|
438
|
+
/**
|
|
439
|
+
* Trim internal caches back down to their configured limits.
|
|
440
|
+
* Called when the stage goes idle so cache eviction never competes with
|
|
441
|
+
* active rendering. Backends with no bounded cache may implement this as a
|
|
442
|
+
* no-op.
|
|
443
|
+
*/
|
|
444
|
+
cleanup: () => void;
|
|
438
445
|
}
|
|
439
446
|
|
|
440
447
|
/**
|
|
@@ -269,6 +269,24 @@ export class ImageTexture extends Texture {
|
|
|
269
269
|
}
|
|
270
270
|
|
|
271
271
|
override async getTextureSource(): Promise<TextureData> {
|
|
272
|
+
// Compressed textures are not supported by the Canvas2D renderer.
|
|
273
|
+
// Fail fast here before incurring a network fetch or binary decode.
|
|
274
|
+
if (this.txManager.renderer?.mode === 'canvas') {
|
|
275
|
+
const { src, type } = this.props;
|
|
276
|
+
if (
|
|
277
|
+
type === 'compressed' ||
|
|
278
|
+
(typeof src === 'string' && isCompressedTextureContainer(src) === true)
|
|
279
|
+
) {
|
|
280
|
+
const err = new Error(
|
|
281
|
+
`ImageTexture: Compressed textures are not supported in Canvas2D render mode (src: ${String(
|
|
282
|
+
src,
|
|
283
|
+
)})`,
|
|
284
|
+
);
|
|
285
|
+
this.setState('failed', err);
|
|
286
|
+
return { data: null };
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
272
290
|
let resp: TextureData;
|
|
273
291
|
try {
|
|
274
292
|
resp = await this.determineImageTypeAndLoadImage();
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { SubTexture } from './SubTexture.js';
|
|
3
|
+
import { ImageTexture } from './ImageTexture.js';
|
|
4
|
+
import type { CoreTextureManager } from '../CoreTextureManager.js';
|
|
5
|
+
|
|
6
|
+
const flushMicrotasks = () => Promise.resolve();
|
|
7
|
+
|
|
8
|
+
describe('SubTexture lifecycle', () => {
|
|
9
|
+
it('detaches its parent-texture listeners on destroy', async () => {
|
|
10
|
+
// 'initial' state means the constructor's microtask attaches listeners
|
|
11
|
+
// without synchronously firing any of the state handlers.
|
|
12
|
+
const parent = {
|
|
13
|
+
state: 'initial',
|
|
14
|
+
dimensions: null,
|
|
15
|
+
error: null,
|
|
16
|
+
on: vi.fn(),
|
|
17
|
+
off: vi.fn(),
|
|
18
|
+
};
|
|
19
|
+
const txManager = {
|
|
20
|
+
maxRetryCount: 0,
|
|
21
|
+
platform: {},
|
|
22
|
+
resolveParentTexture: () => parent,
|
|
23
|
+
} as unknown as CoreTextureManager;
|
|
24
|
+
|
|
25
|
+
const parentImage = new ImageTexture(txManager, {} as never);
|
|
26
|
+
const sub = new SubTexture(txManager, {
|
|
27
|
+
texture: parentImage,
|
|
28
|
+
x: 0,
|
|
29
|
+
y: 0,
|
|
30
|
+
w: 10,
|
|
31
|
+
h: 10,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
// Listeners are attached in a microtask after construction.
|
|
35
|
+
await flushMicrotasks();
|
|
36
|
+
expect(parent.on).toHaveBeenCalledTimes(4);
|
|
37
|
+
|
|
38
|
+
sub.destroy();
|
|
39
|
+
|
|
40
|
+
const offEvents = (parent.off.mock.calls as Array<[string]>)
|
|
41
|
+
.map((c) => c[0])
|
|
42
|
+
.sort();
|
|
43
|
+
expect(offEvents).toEqual(['failed', 'freed', 'loaded', 'loading']);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -133,6 +133,19 @@ export class SubTexture extends Texture {
|
|
|
133
133
|
this.parentTexture.setRenderableOwner(this.subtextureId, isRenderable);
|
|
134
134
|
}
|
|
135
135
|
|
|
136
|
+
override destroy(): void {
|
|
137
|
+
// Detach from the parent texture's event emitter. The parent is typically a
|
|
138
|
+
// long-lived shared atlas (preventCleanup), so without this each destroyed
|
|
139
|
+
// SubTexture — and everything its handlers capture — would be retained by
|
|
140
|
+
// the parent's listener lists for the rest of the session.
|
|
141
|
+
const parentTx = this.parentTexture;
|
|
142
|
+
parentTx.off('loading', this.onParentTxLoading);
|
|
143
|
+
parentTx.off('loaded', this.onParentTxLoaded);
|
|
144
|
+
parentTx.off('failed', this.onParentTxFailed);
|
|
145
|
+
parentTx.off('freed', this.onParentTxFreed);
|
|
146
|
+
super.destroy();
|
|
147
|
+
}
|
|
148
|
+
|
|
136
149
|
override async getTextureSource(): Promise<TextureData> {
|
|
137
150
|
// Check if parent texture is loaded
|
|
138
151
|
return new Promise((resolve, reject) => {
|
|
@@ -240,6 +240,11 @@ export abstract class Texture extends EventEmitter {
|
|
|
240
240
|
return false;
|
|
241
241
|
}
|
|
242
242
|
|
|
243
|
+
// Don't cleanup a texture that is in the process of loading
|
|
244
|
+
if (this.state === 'loading') {
|
|
245
|
+
return false;
|
|
246
|
+
}
|
|
247
|
+
|
|
243
248
|
// Don't cleanup if not renderable
|
|
244
249
|
if (this.renderable === true) {
|
|
245
250
|
return false;
|
|
@@ -341,6 +346,9 @@ export abstract class Texture extends EventEmitter {
|
|
|
341
346
|
*/
|
|
342
347
|
free(): void {
|
|
343
348
|
this.ctxTexture?.free();
|
|
349
|
+
// Null out the freed ctxTexture so a subsequent reload re-creates it via
|
|
350
|
+
// getCtxTexture() instead of reusing a stale, already-freed reference.
|
|
351
|
+
this.ctxTexture = undefined;
|
|
344
352
|
}
|
|
345
353
|
|
|
346
354
|
/**
|
|
@@ -370,6 +378,10 @@ export abstract class Texture extends EventEmitter {
|
|
|
370
378
|
|
|
371
379
|
// Always free texture data regardless of state
|
|
372
380
|
this.freeTextureData();
|
|
381
|
+
|
|
382
|
+
// Drop any remaining subscribers so a texture destroyed while something
|
|
383
|
+
// still holds a listener does not retain it (and its captures).
|
|
384
|
+
this.removeAllListeners();
|
|
373
385
|
}
|
|
374
386
|
|
|
375
387
|
/**
|
|
@@ -230,6 +230,11 @@ const knownProperties = new Set<string>([
|
|
|
230
230
|
'parent',
|
|
231
231
|
'data',
|
|
232
232
|
'text',
|
|
233
|
+
'texture',
|
|
234
|
+
'rtt',
|
|
235
|
+
'mount',
|
|
236
|
+
'mountX',
|
|
237
|
+
'mountY',
|
|
233
238
|
'componentName',
|
|
234
239
|
'componentLocation',
|
|
235
240
|
]);
|
|
@@ -293,6 +298,10 @@ export class Inspector {
|
|
|
293
298
|
private scaleX = 1;
|
|
294
299
|
private scaleY = 1;
|
|
295
300
|
private textureMetrics = new Map<Texture, TextureMetrics>();
|
|
301
|
+
// Stable reference so the same function can be passed to both
|
|
302
|
+
// add/removeEventListener — `.bind()` returns a new fn each call, so binding
|
|
303
|
+
// inline in destroy() would never actually remove the listener.
|
|
304
|
+
private boundSetRootPosition = this.setRootPosition.bind(this);
|
|
296
305
|
|
|
297
306
|
// Performance monitoring for frequent setter calls
|
|
298
307
|
private static setterCallCount = new Map<
|
|
@@ -393,7 +402,7 @@ export class Inspector {
|
|
|
393
402
|
this.resizeObserver.observe(canvas);
|
|
394
403
|
|
|
395
404
|
//listen for changes on window
|
|
396
|
-
window.addEventListener('resize', this.
|
|
405
|
+
window.addEventListener('resize', this.boundSetRootPosition);
|
|
397
406
|
|
|
398
407
|
// Start animation stats timer if enabled
|
|
399
408
|
this.startAnimationStatsTimer();
|
|
@@ -1033,7 +1042,7 @@ export class Inspector {
|
|
|
1033
1042
|
});
|
|
1034
1043
|
coreNodeListeners.clear();
|
|
1035
1044
|
|
|
1036
|
-
this.destroyNode(node
|
|
1045
|
+
this.destroyNode(node);
|
|
1037
1046
|
originalDestroy.call(node, isChild);
|
|
1038
1047
|
},
|
|
1039
1048
|
configurable: true,
|
|
@@ -1151,7 +1160,7 @@ export class Inspector {
|
|
|
1151
1160
|
this.resizeObserver.disconnect();
|
|
1152
1161
|
|
|
1153
1162
|
// Remove resize listener
|
|
1154
|
-
window.removeEventListener('resize', this.
|
|
1163
|
+
window.removeEventListener('resize', this.boundSetRootPosition);
|
|
1155
1164
|
if (this.root && this.root.parentNode) {
|
|
1156
1165
|
this.root.remove();
|
|
1157
1166
|
}
|
|
@@ -1160,8 +1169,11 @@ export class Inspector {
|
|
|
1160
1169
|
Inspector.clearAnimationStats();
|
|
1161
1170
|
}
|
|
1162
1171
|
|
|
1163
|
-
destroyNode(
|
|
1164
|
-
|
|
1172
|
+
destroyNode(node: CoreNode) {
|
|
1173
|
+
// Use the mirror div stored on the node rather than a document-global
|
|
1174
|
+
// getElementById, which would collide across multiple renderer/inspector
|
|
1175
|
+
// instances sharing one document.
|
|
1176
|
+
const div = (node as CoreNode & { div?: HTMLElement }).div;
|
|
1165
1177
|
div?.remove();
|
|
1166
1178
|
}
|
|
1167
1179
|
|
|
@@ -1180,23 +1192,41 @@ export class Inspector {
|
|
|
1180
1192
|
* Special case for parent property
|
|
1181
1193
|
*/
|
|
1182
1194
|
if (property === 'parent') {
|
|
1183
|
-
|
|
1184
|
-
|
|
1185
|
-
//
|
|
1186
|
-
//
|
|
1187
|
-
|
|
1188
|
-
|
|
1189
|
-
|
|
1190
|
-
|
|
1195
|
+
// value is the parent CoreNode. Its mirror div is stored on the node
|
|
1196
|
+
// itself (see createNode/createTextNode), so use that reference instead
|
|
1197
|
+
// of a document-global getElementById — the latter collides across
|
|
1198
|
+
// multiple renderer/inspector instances sharing one document.
|
|
1199
|
+
//
|
|
1200
|
+
// The stage root (the only node with parent === null) is mirrored by
|
|
1201
|
+
// `this.root`, the container that is actually attached to the document.
|
|
1202
|
+
// createNodes() also makes a plain div for the root node, but that div is
|
|
1203
|
+
// never attached, so the root's direct children must be parented to
|
|
1204
|
+
// `this.root` instead — otherwise the whole mirror tree ends up detached
|
|
1205
|
+
// from the document. This replaces the old `parentId === 1` heuristic.
|
|
1206
|
+
const parentNode = value as CoreNode & { div?: HTMLElement };
|
|
1207
|
+
const parentDiv =
|
|
1208
|
+
parentNode.parent === null ? this.root : parentNode.div ?? this.root;
|
|
1209
|
+
parentDiv.appendChild(div);
|
|
1210
|
+
return;
|
|
1211
|
+
}
|
|
1191
1212
|
|
|
1192
|
-
|
|
1193
|
-
|
|
1213
|
+
// special case for mount changes — mount has no CSS equivalent, but it
|
|
1214
|
+
// shifts the node's left/top, so recompute position from the latest props.
|
|
1215
|
+
if (
|
|
1216
|
+
property === 'mount' ||
|
|
1217
|
+
property === 'mountX' ||
|
|
1218
|
+
property === 'mountY'
|
|
1219
|
+
) {
|
|
1220
|
+
div.style.left = `${props.x - props.w * (props.mountX ?? 0)}px`;
|
|
1221
|
+
div.style.top = `${props.y - props.h * (props.mountY ?? 0)}px`;
|
|
1194
1222
|
return;
|
|
1195
1223
|
}
|
|
1196
1224
|
|
|
1197
1225
|
// special case for text
|
|
1198
1226
|
if (property === 'text') {
|
|
1199
|
-
|
|
1227
|
+
// textContent (not innerHTML): text may contain <, &, etc. which would
|
|
1228
|
+
// corrupt the mirror tree or act as an injection vector via innerHTML.
|
|
1229
|
+
div.textContent = String(value);
|
|
1200
1230
|
|
|
1201
1231
|
// Keep DOM text invisible without breaking visibility checks
|
|
1202
1232
|
// Use very low opacity (0.001) instead of 0 so Playwright still detects it
|
package/src/main-api/Renderer.ts
CHANGED
|
@@ -393,6 +393,23 @@ export interface RendererRuntimeSettings {
|
|
|
393
393
|
* Configuration settings for {@link RendererMain}
|
|
394
394
|
*/
|
|
395
395
|
export type RendererMainSettings = RendererRuntimeSettings & {
|
|
396
|
+
/**
|
|
397
|
+
* Maximum number of entries kept in the SDF text layout cache
|
|
398
|
+
*
|
|
399
|
+
* @remarks
|
|
400
|
+
* The SDF text renderer caches the computed glyph layout for a given
|
|
401
|
+
* `text` + font + layout-prop combination so that identical strings (e.g.
|
|
402
|
+
* repeated badges/labels) are not re-laid-out. The cache is content-keyed
|
|
403
|
+
* and shared across nodes, and is trimmed down to this many (most recently
|
|
404
|
+
* used) entries whenever the stage goes idle.
|
|
405
|
+
*
|
|
406
|
+
* Set this higher for content-dense UIs with many simultaneous unique
|
|
407
|
+
* strings, or lower to cap memory more aggressively.
|
|
408
|
+
*
|
|
409
|
+
* @defaultValue `250`
|
|
410
|
+
*/
|
|
411
|
+
textLayoutCacheSize: number;
|
|
412
|
+
|
|
396
413
|
/**
|
|
397
414
|
* Include context call (i.e. WebGL) information in FPS updates
|
|
398
415
|
*
|
|
@@ -497,17 +514,6 @@ export type RendererMainSettings = RendererRuntimeSettings & {
|
|
|
497
514
|
*/
|
|
498
515
|
textBaselineMode: TextBaselineMode;
|
|
499
516
|
|
|
500
|
-
/**
|
|
501
|
-
* Force WebGL2
|
|
502
|
-
*
|
|
503
|
-
* @remarks
|
|
504
|
-
* Force the renderer to use WebGL2. This can be used to force the renderer to
|
|
505
|
-
* use WebGL2 even if the browser supports WebGL1.
|
|
506
|
-
*
|
|
507
|
-
* @defaultValue `false`
|
|
508
|
-
*/
|
|
509
|
-
forceWebGL2: boolean;
|
|
510
|
-
|
|
511
517
|
/**
|
|
512
518
|
* Canvas object to use for rendering
|
|
513
519
|
*
|
|
@@ -678,10 +684,10 @@ export class RendererMain extends EventEmitter {
|
|
|
678
684
|
fpsUpdateInterval: settings.fpsUpdateInterval || 0,
|
|
679
685
|
enableClear: settings.enableClear ?? true,
|
|
680
686
|
targetFPS: settings.targetFPS || 0,
|
|
687
|
+
textLayoutCacheSize: settings.textLayoutCacheSize ?? 250,
|
|
681
688
|
numImageWorkers:
|
|
682
689
|
settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2,
|
|
683
690
|
enableContextSpy: settings.enableContextSpy ?? false,
|
|
684
|
-
forceWebGL2: settings.forceWebGL2 ?? false,
|
|
685
691
|
inspector: settings.inspector ?? false,
|
|
686
692
|
inspectorOptions: settings.inspectorOptions ?? {},
|
|
687
693
|
renderEngine: settings.renderEngine,
|
|
@@ -743,7 +749,6 @@ export class RendererMain extends EventEmitter {
|
|
|
743
749
|
deviceLogicalPixelRatio,
|
|
744
750
|
devicePhysicalPixelRatio,
|
|
745
751
|
enableContextSpy: settings.enableContextSpy!,
|
|
746
|
-
forceWebGL2: settings.forceWebGL2!,
|
|
747
752
|
fpsUpdateInterval: settings.fpsUpdateInterval!,
|
|
748
753
|
enableClear: settings.enableClear!,
|
|
749
754
|
numImageWorkers: settings.numImageWorkers!,
|
|
@@ -755,6 +760,7 @@ export class RendererMain extends EventEmitter {
|
|
|
755
760
|
textBaselineMode: settings.textBaselineMode!,
|
|
756
761
|
inspector: settings.inspector !== null,
|
|
757
762
|
targetFPS: settings.targetFPS!,
|
|
763
|
+
textLayoutCacheSize: settings.textLayoutCacheSize!,
|
|
758
764
|
textureProcessingTimeLimit: settings.textureProcessingTimeLimit!,
|
|
759
765
|
createImageBitmapSupport: settings.createImageBitmapSupport!,
|
|
760
766
|
premultiplyAlphaHonored: settings.premultiplyAlphaHonored,
|
|
@@ -922,7 +928,7 @@ export class RendererMain extends EventEmitter {
|
|
|
922
928
|
*/
|
|
923
929
|
destroyNode(node: INode) {
|
|
924
930
|
if (ENABLE_INSPECTOR && this.inspector) {
|
|
925
|
-
this.inspector.destroyNode(node
|
|
931
|
+
this.inspector.destroyNode(node as unknown as CoreNode);
|
|
926
932
|
}
|
|
927
933
|
|
|
928
934
|
return node.destroy();
|
package/src/utils.ts
CHANGED
|
@@ -2,7 +2,6 @@ import type { ContextSpy } from './core/lib/ContextSpy.js';
|
|
|
2
2
|
|
|
3
3
|
export function createWebGLContext(
|
|
4
4
|
canvas: HTMLCanvasElement | OffscreenCanvas,
|
|
5
|
-
forceWebGL2 = false,
|
|
6
5
|
contextSpy: ContextSpy | null,
|
|
7
6
|
): WebGLRenderingContext {
|
|
8
7
|
const config: WebGLContextAttributes = {
|
|
@@ -20,7 +19,7 @@ export function createWebGLContext(
|
|
|
20
19
|
const gl =
|
|
21
20
|
// TODO: Remove this assertion once this issue is fixed in TypeScript
|
|
22
21
|
// https://github.com/microsoft/TypeScript/issues/53614
|
|
23
|
-
(canvas.getContext(
|
|
22
|
+
(canvas.getContext('webgl', config) ||
|
|
24
23
|
canvas.getContext(
|
|
25
24
|
'experimental-webgl' as 'webgl',
|
|
26
25
|
config,
|
|
@@ -1,16 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Generic WebGL Utility Functions
|
|
3
|
-
*
|
|
4
|
-
* @remarks
|
|
5
|
-
* Nothing here should be coupled to Renderer logic / types.
|
|
6
|
-
*
|
|
7
|
-
* @param gl
|
|
8
|
-
* @returns
|
|
9
|
-
*/
|
|
10
|
-
export function isWebGl2(
|
|
11
|
-
gl: WebGLRenderingContext,
|
|
12
|
-
): gl is WebGL2RenderingContext {
|
|
13
|
-
return (
|
|
14
|
-
self.WebGL2RenderingContext && gl instanceof self.WebGL2RenderingContext
|
|
15
|
-
);
|
|
16
|
-
}
|