@solidtv/renderer 1.5.2 → 1.5.4
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 +33 -0
- package/dist/src/core/CoreNode.js +34 -6
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +3 -2
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/CoreTextureManager.d.ts +39 -3
- package/dist/src/core/CoreTextureManager.js +72 -61
- package/dist/src/core/CoreTextureManager.js.map +1 -1
- package/dist/src/core/Stage.js +8 -4
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.js +1 -1
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/collectionUtils.d.ts +1 -1
- package/dist/src/core/lib/collectionUtils.js +13 -31
- package/dist/src/core/lib/collectionUtils.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlCtxTexture.js +5 -3
- package/dist/src/core/renderers/webgl/WebGlCtxTexture.js.map +1 -1
- package/dist/src/core/textures/SubTexture.js +1 -1
- package/dist/src/core/textures/SubTexture.js.map +1 -1
- package/dist/src/core/textures/Texture.d.ts +1 -1
- package/dist/src/core/textures/Texture.js +10 -13
- package/dist/src/core/textures/Texture.js.map +1 -1
- package/dist/src/main-api/Inspector.js +1 -1
- package/dist/src/main-api/Inspector.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.test.ts +272 -0
- package/src/core/CoreNode.ts +62 -8
- package/src/core/CoreTextNode.test.ts +1 -0
- package/src/core/CoreTextNode.ts +3 -2
- package/src/core/CoreTextureManager.ts +75 -70
- package/src/core/Stage.ts +11 -4
- package/src/core/TextureMemoryManager.test.ts +2 -2
- package/src/core/TextureMemoryManager.ts +1 -1
- package/src/core/lib/collectionUtils.test.ts +91 -0
- package/src/core/lib/collectionUtils.ts +13 -34
- package/src/core/renderers/webgl/WebGlCtxTexture.ts +5 -4
- package/src/core/textures/SubTexture.ts +1 -1
- package/src/core/textures/Texture.test.ts +124 -0
- package/src/core/textures/Texture.ts +10 -13
- package/src/main-api/Inspector.ts +1 -1
|
@@ -13,6 +13,7 @@ import { premultiplyColorABGR } from '../utils.js';
|
|
|
13
13
|
describe('set color()', () => {
|
|
14
14
|
const defaultProps = (overrides?: Partial<CoreNodeProps>): CoreNodeProps => ({
|
|
15
15
|
alpha: 0,
|
|
16
|
+
ignoreParentAlpha: false,
|
|
16
17
|
autosize: false,
|
|
17
18
|
boundsMargin: null,
|
|
18
19
|
clipping: false,
|
|
@@ -264,6 +265,112 @@ describe('set color()', () => {
|
|
|
264
265
|
});
|
|
265
266
|
});
|
|
266
267
|
|
|
268
|
+
describe('ignoreParentAlpha', () => {
|
|
269
|
+
const makeParent = (worldAlpha: number) => {
|
|
270
|
+
const parent = new CoreNode(stage, defaultProps());
|
|
271
|
+
parent.globalTransform = Matrix3d.identity();
|
|
272
|
+
parent.worldAlpha = worldAlpha;
|
|
273
|
+
return parent;
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
it('multiplies parent world alpha by default', () => {
|
|
277
|
+
const parent = makeParent(0.5);
|
|
278
|
+
const node = new CoreNode(stage, defaultProps({ parent, alpha: 0.8 }));
|
|
279
|
+
|
|
280
|
+
node.update(0, clippingRect);
|
|
281
|
+
|
|
282
|
+
expect(node.worldAlpha).toBeCloseTo(0.4);
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
it('uses own alpha only when enabled', () => {
|
|
286
|
+
const parent = makeParent(0.5);
|
|
287
|
+
const node = new CoreNode(
|
|
288
|
+
stage,
|
|
289
|
+
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
node.update(0, clippingRect);
|
|
293
|
+
|
|
294
|
+
expect(node.worldAlpha).toBe(0.8);
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
it('keeps its own world alpha while the parent fades toward 0', () => {
|
|
298
|
+
const parent = makeParent(0.01);
|
|
299
|
+
const node = new CoreNode(
|
|
300
|
+
stage,
|
|
301
|
+
defaultProps({ parent, alpha: 1, ignoreParentAlpha: true }),
|
|
302
|
+
);
|
|
303
|
+
node.w = 100;
|
|
304
|
+
node.h = 100;
|
|
305
|
+
node.color = 0xffffffff;
|
|
306
|
+
|
|
307
|
+
node.update(0, clippingRect);
|
|
308
|
+
|
|
309
|
+
expect(node.worldAlpha).toBe(1);
|
|
310
|
+
expect(node.isRenderable).toBe(true);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
it('premultiplies colors with the node own alpha when enabled', () => {
|
|
314
|
+
const parent = makeParent(0.25);
|
|
315
|
+
const node = new CoreNode(
|
|
316
|
+
stage,
|
|
317
|
+
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
|
|
318
|
+
);
|
|
319
|
+
node.w = 100;
|
|
320
|
+
node.h = 100;
|
|
321
|
+
node.color = 0xff0000ff;
|
|
322
|
+
|
|
323
|
+
node.update(0, clippingRect);
|
|
324
|
+
|
|
325
|
+
expect(node.premultipliedColorTl).toBe(
|
|
326
|
+
premultiplyColorABGR(0xff0000ff, 0.8),
|
|
327
|
+
);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('toggling the setter recomputes world alpha', () => {
|
|
331
|
+
const parent = makeParent(0.5);
|
|
332
|
+
const node = new CoreNode(stage, defaultProps({ parent, alpha: 0.8 }));
|
|
333
|
+
|
|
334
|
+
node.update(0, clippingRect);
|
|
335
|
+
expect(node.worldAlpha).toBeCloseTo(0.4);
|
|
336
|
+
|
|
337
|
+
node.ignoreParentAlpha = true;
|
|
338
|
+
node.update(1, clippingRect);
|
|
339
|
+
expect(node.worldAlpha).toBe(0.8);
|
|
340
|
+
|
|
341
|
+
node.ignoreParentAlpha = false;
|
|
342
|
+
node.update(2, clippingRect);
|
|
343
|
+
expect(node.worldAlpha).toBeCloseTo(0.4);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('setting the same value does not flag an update', () => {
|
|
347
|
+
const node = new CoreNode(stage, defaultProps());
|
|
348
|
+
const updateTypeBefore = node.updateType;
|
|
349
|
+
|
|
350
|
+
node.ignoreParentAlpha = false;
|
|
351
|
+
|
|
352
|
+
expect(node.updateType).toBe(updateTypeBefore);
|
|
353
|
+
});
|
|
354
|
+
|
|
355
|
+
it('descendants inherit the node world alpha as usual', () => {
|
|
356
|
+
const parent = makeParent(0.5);
|
|
357
|
+
const node = new CoreNode(
|
|
358
|
+
stage,
|
|
359
|
+
defaultProps({ parent, alpha: 0.8, ignoreParentAlpha: true }),
|
|
360
|
+
);
|
|
361
|
+
const child = new CoreNode(
|
|
362
|
+
stage,
|
|
363
|
+
defaultProps({ parent: node, alpha: 0.5 }),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
node.update(0, clippingRect);
|
|
367
|
+
child.update(0, clippingRect);
|
|
368
|
+
|
|
369
|
+
expect(node.worldAlpha).toBe(0.8);
|
|
370
|
+
expect(child.worldAlpha).toBeCloseTo(0.4);
|
|
371
|
+
});
|
|
372
|
+
});
|
|
373
|
+
|
|
267
374
|
describe('autosize system', () => {
|
|
268
375
|
it('should initialize with autosize disabled', () => {
|
|
269
376
|
const node = new CoreNode(stage, defaultProps());
|
|
@@ -1605,4 +1712,169 @@ describe('set color()', () => {
|
|
|
1605
1712
|
expect(node.isRenderable).toBe(true);
|
|
1606
1713
|
});
|
|
1607
1714
|
});
|
|
1715
|
+
|
|
1716
|
+
describe('texture ownership cache', () => {
|
|
1717
|
+
// Same shape as the placeholderColor texture fake: a real EventEmitter so
|
|
1718
|
+
// loadTextureTask subscribes and we can drive freed/loaded by emitting.
|
|
1719
|
+
function emittingTexture(state: string): ImageTexture & {
|
|
1720
|
+
emit: (event: string, data?: unknown) => void;
|
|
1721
|
+
} {
|
|
1722
|
+
return Object.assign(new EventEmitter(), {
|
|
1723
|
+
state,
|
|
1724
|
+
preventCleanup: false,
|
|
1725
|
+
retryCount: 0,
|
|
1726
|
+
maxRetryCount: 1,
|
|
1727
|
+
dimensions: { w: 100, h: 100 },
|
|
1728
|
+
setRenderableOwner: vi.fn(),
|
|
1729
|
+
}) as unknown as ImageTexture & {
|
|
1730
|
+
emit: (event: string, data?: unknown) => void;
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
function visibleNode(): CoreNode {
|
|
1735
|
+
const parent = new CoreNode(stage, defaultProps());
|
|
1736
|
+
parent.globalTransform = Matrix3d.identity();
|
|
1737
|
+
parent.worldAlpha = 1;
|
|
1738
|
+
|
|
1739
|
+
const node = new CoreNode(stage, defaultProps({ parent }));
|
|
1740
|
+
node.alpha = 1;
|
|
1741
|
+
node.x = 0;
|
|
1742
|
+
node.y = 0;
|
|
1743
|
+
node.w = 100;
|
|
1744
|
+
node.h = 100;
|
|
1745
|
+
return node;
|
|
1746
|
+
}
|
|
1747
|
+
|
|
1748
|
+
const flushMicrotasks = () => Promise.resolve();
|
|
1749
|
+
|
|
1750
|
+
it('repeated updates with unchanged state call setRenderableOwner once', () => {
|
|
1751
|
+
const node = visibleNode();
|
|
1752
|
+
const texture = emittingTexture('loaded');
|
|
1753
|
+
node.texture = texture;
|
|
1754
|
+
node.textureLoaded = true;
|
|
1755
|
+
|
|
1756
|
+
// The texture setter registers ownership with the node's current
|
|
1757
|
+
// isRenderable (false here), so the cache starts false.
|
|
1758
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(1);
|
|
1759
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1760
|
+
expect.anything(),
|
|
1761
|
+
false,
|
|
1762
|
+
);
|
|
1763
|
+
|
|
1764
|
+
node.update(0, clippingRect);
|
|
1765
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(2);
|
|
1766
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1767
|
+
expect.anything(),
|
|
1768
|
+
true,
|
|
1769
|
+
);
|
|
1770
|
+
|
|
1771
|
+
// Steady-state scroll: ownership unchanged, no further calls.
|
|
1772
|
+
node.update(1, clippingRect);
|
|
1773
|
+
node.update(2, clippingRect);
|
|
1774
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(2);
|
|
1775
|
+
});
|
|
1776
|
+
|
|
1777
|
+
it('moving out of bounds releases ownership once, returning re-adds', () => {
|
|
1778
|
+
const node = visibleNode();
|
|
1779
|
+
const texture = emittingTexture('loaded');
|
|
1780
|
+
node.texture = texture;
|
|
1781
|
+
node.textureLoaded = true;
|
|
1782
|
+
|
|
1783
|
+
node.update(0, clippingRect);
|
|
1784
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1785
|
+
expect.anything(),
|
|
1786
|
+
true,
|
|
1787
|
+
);
|
|
1788
|
+
const callsAfterFirstUpdate = (
|
|
1789
|
+
texture.setRenderableOwner as ReturnType<typeof vi.fn>
|
|
1790
|
+
).mock.calls.length;
|
|
1791
|
+
|
|
1792
|
+
// Out of the 200x200 stage bounds entirely.
|
|
1793
|
+
node.x = 1000;
|
|
1794
|
+
node.update(1, clippingRect);
|
|
1795
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(
|
|
1796
|
+
callsAfterFirstUpdate + 1,
|
|
1797
|
+
);
|
|
1798
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1799
|
+
expect.anything(),
|
|
1800
|
+
false,
|
|
1801
|
+
);
|
|
1802
|
+
|
|
1803
|
+
// Still out of bounds: no repeated release.
|
|
1804
|
+
node.update(2, clippingRect);
|
|
1805
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(
|
|
1806
|
+
callsAfterFirstUpdate + 1,
|
|
1807
|
+
);
|
|
1808
|
+
|
|
1809
|
+
// Back in view: re-registered exactly once.
|
|
1810
|
+
node.x = 0;
|
|
1811
|
+
node.update(3, clippingRect);
|
|
1812
|
+
expect(texture.setRenderableOwner).toHaveBeenCalledTimes(
|
|
1813
|
+
callsAfterFirstUpdate + 2,
|
|
1814
|
+
);
|
|
1815
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1816
|
+
expect.anything(),
|
|
1817
|
+
true,
|
|
1818
|
+
);
|
|
1819
|
+
});
|
|
1820
|
+
|
|
1821
|
+
it('swapping textures releases the old owner and registers the new one', () => {
|
|
1822
|
+
const node = visibleNode();
|
|
1823
|
+
const textureA = emittingTexture('loaded');
|
|
1824
|
+
node.texture = textureA;
|
|
1825
|
+
node.textureLoaded = true;
|
|
1826
|
+
node.update(0, clippingRect);
|
|
1827
|
+
expect(textureA.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1828
|
+
expect.anything(),
|
|
1829
|
+
true,
|
|
1830
|
+
);
|
|
1831
|
+
|
|
1832
|
+
const textureB = emittingTexture('loaded');
|
|
1833
|
+
node.texture = textureB;
|
|
1834
|
+
|
|
1835
|
+
// A is released via unloadTexture.
|
|
1836
|
+
expect(textureA.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1837
|
+
expect.anything(),
|
|
1838
|
+
false,
|
|
1839
|
+
);
|
|
1840
|
+
// B is registered with the node's current renderable state (true),
|
|
1841
|
+
// proving the cache reset on swap — a stale cache would skip this.
|
|
1842
|
+
expect(textureB.setRenderableOwner).toHaveBeenCalledWith(
|
|
1843
|
+
expect.anything(),
|
|
1844
|
+
true,
|
|
1845
|
+
);
|
|
1846
|
+
});
|
|
1847
|
+
|
|
1848
|
+
it('freed texture is re-registered on the next update (reload trigger)', async () => {
|
|
1849
|
+
const node = visibleNode();
|
|
1850
|
+
const texture = emittingTexture('initial');
|
|
1851
|
+
node.texture = texture;
|
|
1852
|
+
node.update(0, clippingRect);
|
|
1853
|
+
|
|
1854
|
+
await flushMicrotasks();
|
|
1855
|
+
(texture as { state: string }).state = 'loaded';
|
|
1856
|
+
texture.emit('loaded', { w: 100, h: 100 });
|
|
1857
|
+
node.update(1, clippingRect);
|
|
1858
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1859
|
+
expect.anything(),
|
|
1860
|
+
true,
|
|
1861
|
+
);
|
|
1862
|
+
|
|
1863
|
+
// Memory manager frees the texture: the node must drop ownership...
|
|
1864
|
+
(texture as { state: string }).state = 'freed';
|
|
1865
|
+
texture.emit('freed');
|
|
1866
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1867
|
+
expect.anything(),
|
|
1868
|
+
false,
|
|
1869
|
+
);
|
|
1870
|
+
|
|
1871
|
+
// ...and the next update pass re-adds it, which is what triggers
|
|
1872
|
+
// Texture.load() for the reload. A stale cache would skip this call.
|
|
1873
|
+
node.update(2, clippingRect);
|
|
1874
|
+
expect(texture.setRenderableOwner).toHaveBeenLastCalledWith(
|
|
1875
|
+
expect.anything(),
|
|
1876
|
+
true,
|
|
1877
|
+
);
|
|
1878
|
+
});
|
|
1879
|
+
});
|
|
1608
1880
|
});
|
package/src/core/CoreNode.ts
CHANGED
|
@@ -46,7 +46,7 @@ import type { IAnimationController } from '../common/IAnimationController.js';
|
|
|
46
46
|
import { createAnimation } from './animations/CoreAnimation.js';
|
|
47
47
|
import type { CoreShaderNode } from './renderers/CoreShaderNode.js';
|
|
48
48
|
import { AutosizeMode, Autosizer } from './Autosizer.js';
|
|
49
|
-
import { removeChild } from './lib/collectionUtils.js';
|
|
49
|
+
import { removeChild, sortByZIndexStable } from './lib/collectionUtils.js';
|
|
50
50
|
|
|
51
51
|
export enum CoreNodeRenderState {
|
|
52
52
|
Init = 0,
|
|
@@ -63,11 +63,6 @@ const NO_CLIPPING_RECT: RectWithValid = {
|
|
|
63
63
|
valid: false,
|
|
64
64
|
};
|
|
65
65
|
|
|
66
|
-
// Hoisted so `sortChildren` doesn't allocate a fresh comparator closure on
|
|
67
|
-
// every z-index reorder.
|
|
68
|
-
const compareZIndex = (a: CoreNode, b: CoreNode): number =>
|
|
69
|
-
a.props.zIndex - b.props.zIndex;
|
|
70
|
-
|
|
71
66
|
const CoreNodeRenderStateMap: Map<CoreNodeRenderState, string> = new Map();
|
|
72
67
|
CoreNodeRenderStateMap.set(CoreNodeRenderState.Init, 'init');
|
|
73
68
|
CoreNodeRenderStateMap.set(CoreNodeRenderState.OutOfBounds, 'outOfBounds');
|
|
@@ -136,6 +131,7 @@ export enum UpdateType {
|
|
|
136
131
|
* @remarks
|
|
137
132
|
* CoreNode Properties Updated:
|
|
138
133
|
* - `worldAlpha` = `parent.worldAlpha` * `alpha`
|
|
134
|
+
* (or just `alpha` when `ignoreParentAlpha` is enabled)
|
|
139
135
|
*/
|
|
140
136
|
WorldAlpha = 64,
|
|
141
137
|
|
|
@@ -250,6 +246,29 @@ export interface CoreNodeProps {
|
|
|
250
246
|
* @default `1`
|
|
251
247
|
*/
|
|
252
248
|
alpha: number;
|
|
249
|
+
/**
|
|
250
|
+
* When enabled, the Node's world alpha is computed from its own
|
|
251
|
+
* {@link alpha} only, ignoring the alpha inherited from its ancestors.
|
|
252
|
+
*
|
|
253
|
+
* @remarks
|
|
254
|
+
* Normally `worldAlpha = parent.worldAlpha * alpha`, so fading a parent
|
|
255
|
+
* fades every descendant with it. With `ignoreParentAlpha` enabled this
|
|
256
|
+
* Node keeps rendering at its own alpha while its parent (and the rest of
|
|
257
|
+
* the subtree) fades.
|
|
258
|
+
*
|
|
259
|
+
* Subtrees whose world alpha reaches exactly 0 are culled from rendering
|
|
260
|
+
* entirely, so this Node still disappears once an ancestor hits alpha 0 —
|
|
261
|
+
* the prop only has an effect while every ancestor's alpha is above 0.
|
|
262
|
+
* This keeps the fully-transparent subtree cull free of bookkeeping.
|
|
263
|
+
*
|
|
264
|
+
* Descendants of this Node inherit from its world alpha as usual.
|
|
265
|
+
*
|
|
266
|
+
* Has no effect inside a render-to-texture subtree: the RTT root's
|
|
267
|
+
* composited quad is still faded as a single unit by its own world alpha.
|
|
268
|
+
*
|
|
269
|
+
* @default `false`
|
|
270
|
+
*/
|
|
271
|
+
ignoreParentAlpha: boolean;
|
|
253
272
|
/**
|
|
254
273
|
* Autosize
|
|
255
274
|
*
|
|
@@ -810,6 +829,14 @@ export class CoreNode extends EventEmitter {
|
|
|
810
829
|
private hasColorProps = false;
|
|
811
830
|
public textureLoaded = false;
|
|
812
831
|
|
|
832
|
+
/**
|
|
833
|
+
* Last ownership value sent to the current texture via
|
|
834
|
+
* {@link updateTextureOwnership}. Per (node, texture) pair — must reset to
|
|
835
|
+
* `false` whenever the texture is swapped or released, or a stale `true`
|
|
836
|
+
* would skip the re-registration that triggers `Texture.load()`.
|
|
837
|
+
*/
|
|
838
|
+
private textureOwnership = false;
|
|
839
|
+
|
|
813
840
|
/**
|
|
814
841
|
* True while this node should render its `placeholderColor` instead of its
|
|
815
842
|
* texture: `placeholderColor` is non-zero, a texture is set, and that
|
|
@@ -1066,6 +1093,7 @@ export class CoreNode extends EventEmitter {
|
|
|
1066
1093
|
texture.off('failed', this.onTextureFailed);
|
|
1067
1094
|
texture.off('freed', this.onTextureFreed);
|
|
1068
1095
|
texture.setRenderableOwner(this._id, false);
|
|
1096
|
+
this.textureOwnership = false;
|
|
1069
1097
|
}
|
|
1070
1098
|
|
|
1071
1099
|
protected onTextureLoaded: TextureLoadedEventHandler = (_, dimensions) => {
|
|
@@ -1453,7 +1481,10 @@ export class CoreNode extends EventEmitter {
|
|
|
1453
1481
|
}
|
|
1454
1482
|
|
|
1455
1483
|
if (updateType & UpdateType.WorldAlpha) {
|
|
1456
|
-
this.worldAlpha =
|
|
1484
|
+
this.worldAlpha =
|
|
1485
|
+
props.ignoreParentAlpha === true
|
|
1486
|
+
? props.alpha
|
|
1487
|
+
: parent.worldAlpha * props.alpha;
|
|
1457
1488
|
updateType |=
|
|
1458
1489
|
UpdateType.PremultipliedColors |
|
|
1459
1490
|
UpdateType.Children |
|
|
@@ -1896,6 +1927,10 @@ export class CoreNode extends EventEmitter {
|
|
|
1896
1927
|
* Changes the renderable state of the node.
|
|
1897
1928
|
*/
|
|
1898
1929
|
updateTextureOwnership(isRenderable: boolean) {
|
|
1930
|
+
if (this.textureOwnership === isRenderable) {
|
|
1931
|
+
return;
|
|
1932
|
+
}
|
|
1933
|
+
this.textureOwnership = isRenderable;
|
|
1899
1934
|
this.texture?.setRenderableOwner(this._id, isRenderable);
|
|
1900
1935
|
}
|
|
1901
1936
|
|
|
@@ -2180,7 +2215,7 @@ export class CoreNode extends EventEmitter {
|
|
|
2180
2215
|
}
|
|
2181
2216
|
|
|
2182
2217
|
sortChildren() {
|
|
2183
|
-
this.children
|
|
2218
|
+
sortByZIndexStable(this.children);
|
|
2184
2219
|
this.stage.requestRenderListUpdate();
|
|
2185
2220
|
}
|
|
2186
2221
|
|
|
@@ -2523,6 +2558,24 @@ export class CoreNode extends EventEmitter {
|
|
|
2523
2558
|
this.childUpdateType |= UpdateType.WorldAlpha;
|
|
2524
2559
|
}
|
|
2525
2560
|
|
|
2561
|
+
get ignoreParentAlpha(): boolean {
|
|
2562
|
+
return this.props.ignoreParentAlpha;
|
|
2563
|
+
}
|
|
2564
|
+
|
|
2565
|
+
set ignoreParentAlpha(value: boolean) {
|
|
2566
|
+
if (this.props.ignoreParentAlpha === value) {
|
|
2567
|
+
return;
|
|
2568
|
+
}
|
|
2569
|
+
this.props.ignoreParentAlpha = value;
|
|
2570
|
+
this.setUpdateType(
|
|
2571
|
+
UpdateType.PremultipliedColors |
|
|
2572
|
+
UpdateType.WorldAlpha |
|
|
2573
|
+
UpdateType.Children |
|
|
2574
|
+
UpdateType.IsRenderable,
|
|
2575
|
+
);
|
|
2576
|
+
this.childUpdateType |= UpdateType.WorldAlpha;
|
|
2577
|
+
}
|
|
2578
|
+
|
|
2526
2579
|
get autosize(): boolean {
|
|
2527
2580
|
return this.props.autosize;
|
|
2528
2581
|
}
|
|
@@ -3060,6 +3113,7 @@ export class CoreNode extends EventEmitter {
|
|
|
3060
3113
|
this.autosizer.setMode(AutosizeMode.Texture); // Set to texture size mode
|
|
3061
3114
|
}
|
|
3062
3115
|
value.setRenderableOwner(this._id, this.isRenderable);
|
|
3116
|
+
this.textureOwnership = this.isRenderable;
|
|
3063
3117
|
this.loadTexture();
|
|
3064
3118
|
}
|
|
3065
3119
|
|
package/src/core/CoreTextNode.ts
CHANGED
|
@@ -266,8 +266,9 @@ export class CoreTextNode extends CoreNode implements CoreTextNodeProps {
|
|
|
266
266
|
this.setRenderable(false);
|
|
267
267
|
|
|
268
268
|
if (this.renderState > CoreNodeRenderState.OutOfBounds) {
|
|
269
|
-
// We do want the texture to load immediately
|
|
270
|
-
|
|
269
|
+
// We do want the texture to load immediately. Routed through
|
|
270
|
+
// updateTextureOwnership so the ownership cache stays in sync.
|
|
271
|
+
this.updateTextureOwnership(true);
|
|
271
272
|
}
|
|
272
273
|
}
|
|
273
274
|
}
|
|
@@ -409,9 +409,9 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
409
409
|
|
|
410
410
|
// Anything that arrived before initialization completed is now safe to
|
|
411
411
|
// process. Without this, queued textures would sit until the next frame
|
|
412
|
-
// tick happens to
|
|
412
|
+
// tick happens to drain them.
|
|
413
413
|
if (this.uploadTextureQueue.size > 0) {
|
|
414
|
-
this.
|
|
414
|
+
this.processUntil(Infinity).catch((err) => {
|
|
415
415
|
console.error('Failed to drain pre-init texture queue:', err);
|
|
416
416
|
});
|
|
417
417
|
}
|
|
@@ -587,11 +587,41 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
587
587
|
}
|
|
588
588
|
|
|
589
589
|
/**
|
|
590
|
-
*
|
|
590
|
+
* Upload a single queued texture to the GPU.
|
|
591
591
|
*
|
|
592
|
-
* @
|
|
592
|
+
* @remarks
|
|
593
|
+
* Used while animations are running so uploads don't steal time from the
|
|
594
|
+
* animation. If the dequeued texture already died (failed/freed), nothing is
|
|
595
|
+
* uploaded this frame and the next call handles the following one.
|
|
596
|
+
*/
|
|
597
|
+
async processOne(): Promise<void> {
|
|
598
|
+
if (this.initialized === false) {
|
|
599
|
+
return;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const texture = this.uploadTextureQueue.shift();
|
|
603
|
+
if (texture === undefined) {
|
|
604
|
+
return;
|
|
605
|
+
}
|
|
606
|
+
|
|
607
|
+
await this.uploadQueued(texture);
|
|
608
|
+
}
|
|
609
|
+
|
|
610
|
+
/**
|
|
611
|
+
* Upload queued textures to the GPU until the per-frame time budget runs out.
|
|
612
|
+
*
|
|
613
|
+
* @remarks
|
|
614
|
+
* Called once per frame when idle. Textures are uploaded one-by-one; after
|
|
615
|
+
* each, the elapsed time is rechecked and processing stops once it exceeds
|
|
616
|
+
* `maxProcessingTime`, leaving the rest queued for the next frame.
|
|
617
|
+
*
|
|
618
|
+
* In normal operation a queued texture's data is already decoded
|
|
619
|
+
* (`loadTexture` awaits `getTextureData` before enqueuing), so this budgets
|
|
620
|
+
* GPU upload time. Pass `Infinity` to drain the whole queue.
|
|
621
|
+
*
|
|
622
|
+
* @param maxProcessingTime - The time budget for this frame, in milliseconds
|
|
593
623
|
*/
|
|
594
|
-
async
|
|
624
|
+
async processUntil(maxProcessingTime: number): Promise<void> {
|
|
595
625
|
if (this.initialized === false) {
|
|
596
626
|
return;
|
|
597
627
|
}
|
|
@@ -599,79 +629,54 @@ export class CoreTextureManager extends EventEmitter {
|
|
|
599
629
|
const platform = this.platform;
|
|
600
630
|
const startTime = platform.getTimeStamp();
|
|
601
631
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
const pending: Array<{ texture: Texture; data: Promise<unknown> }> = [];
|
|
608
|
-
|
|
609
|
-
// Helper avoids TS narrowing `texture.state` permanently after the first
|
|
610
|
-
// discriminated check — the property is mutable and can transition across
|
|
611
|
-
// awaits, so we need to re-read it freshly each time.
|
|
612
|
-
const isDead = (texture: Texture): boolean =>
|
|
613
|
-
texture.state === 'failed' || texture.state === 'freed';
|
|
614
|
-
|
|
615
|
-
const fillPrefetch = () => {
|
|
616
|
-
while (pending.length < prefetchLimit) {
|
|
617
|
-
const texture = this.uploadTextureQueue.shift();
|
|
618
|
-
if (texture === undefined) break;
|
|
619
|
-
|
|
620
|
-
if (isDead(texture)) {
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
623
|
-
|
|
624
|
-
// Swallow the rejection here so an early failure doesn't surface as
|
|
625
|
-
// an unhandled promise rejection while it sits in the prefetch
|
|
626
|
-
// window; we re-check state after awaiting.
|
|
627
|
-
const data =
|
|
628
|
-
texture.textureData === null
|
|
629
|
-
? texture.getTextureData().catch((err) => {
|
|
630
|
-
console.error('Failed to fetch texture data:', err);
|
|
631
|
-
return null;
|
|
632
|
-
})
|
|
633
|
-
: Promise.resolve(texture.textureData);
|
|
634
|
-
|
|
635
|
-
pending.push({ texture, data });
|
|
632
|
+
while (platform.getTimeStamp() - startTime < maxProcessingTime) {
|
|
633
|
+
const texture = this.uploadTextureQueue.shift();
|
|
634
|
+
if (texture === undefined) {
|
|
635
|
+
// Queue drained.
|
|
636
|
+
break;
|
|
636
637
|
}
|
|
637
|
-
};
|
|
638
638
|
|
|
639
|
-
|
|
639
|
+
await this.uploadQueued(texture);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
640
642
|
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
643
|
+
/**
|
|
644
|
+
* Decode (if needed) and upload a single already-dequeued texture.
|
|
645
|
+
*
|
|
646
|
+
* @remarks
|
|
647
|
+
* Shared by {@link processOne} and {@link processUntil}. Dead (failed/freed)
|
|
648
|
+
* textures and upload failures are skipped without throwing.
|
|
649
|
+
*/
|
|
650
|
+
private async uploadQueued(texture: Texture): Promise<void> {
|
|
651
|
+
if (this.isTextureDead(texture)) {
|
|
652
|
+
return;
|
|
653
|
+
}
|
|
650
654
|
|
|
651
|
-
|
|
652
|
-
|
|
655
|
+
try {
|
|
656
|
+
if (texture.textureData === null) {
|
|
657
|
+
await texture.getTextureData();
|
|
653
658
|
}
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
await next.data;
|
|
657
|
-
if (isDead(next.texture)) {
|
|
658
|
-
continue;
|
|
659
|
-
}
|
|
660
|
-
await this.uploadTexture(next.texture);
|
|
661
|
-
} catch (error) {
|
|
662
|
-
console.error('Failed to upload texture:', error);
|
|
663
|
-
// Continue with next texture instead of stopping entire queue
|
|
659
|
+
if (this.isTextureDead(texture)) {
|
|
660
|
+
return;
|
|
664
661
|
}
|
|
662
|
+
await this.uploadTexture(texture);
|
|
663
|
+
} catch (error) {
|
|
664
|
+
console.error('Failed to upload texture:', error);
|
|
665
|
+
// Skip this texture instead of stalling the queue.
|
|
665
666
|
}
|
|
667
|
+
}
|
|
666
668
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
669
|
+
/**
|
|
670
|
+
* A texture is "dead" once it has failed or been freed — both terminal for
|
|
671
|
+
* the upload pipeline.
|
|
672
|
+
*
|
|
673
|
+
* @remarks
|
|
674
|
+
* Kept as a method rather than an inline check so TypeScript doesn't
|
|
675
|
+
* permanently narrow `state` after the first comparison: the property is
|
|
676
|
+
* mutable and can transition across the awaits in {@link uploadQueued}.
|
|
677
|
+
*/
|
|
678
|
+
private isTextureDead(texture: Texture): boolean {
|
|
679
|
+
return texture.state === 'failed' || texture.state === 'freed';
|
|
675
680
|
}
|
|
676
681
|
|
|
677
682
|
public hasUpdates(): boolean {
|
package/src/core/Stage.ts
CHANGED
|
@@ -379,6 +379,7 @@ export class Stage {
|
|
|
379
379
|
w: appWidth,
|
|
380
380
|
h: appHeight,
|
|
381
381
|
alpha: 1,
|
|
382
|
+
ignoreParentAlpha: false,
|
|
382
383
|
autosize: false,
|
|
383
384
|
boundsMargin: null,
|
|
384
385
|
clipping: false,
|
|
@@ -594,11 +595,16 @@ export class Stage {
|
|
|
594
595
|
// Process some textures asynchronously but don't block the frame
|
|
595
596
|
// Use a background task to prevent frame drops
|
|
596
597
|
if (this.txManager.hasUpdates() === true) {
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
598
|
+
// While animating, upload at most one texture per frame so uploads don't
|
|
599
|
+
// steal time from the animation; otherwise fill the per-frame time budget.
|
|
600
|
+
const processing =
|
|
601
|
+
hasActiveAnimations === true
|
|
602
|
+
? this.txManager.processOne()
|
|
603
|
+
: this.txManager.processUntil(
|
|
604
|
+
this.options.textureProcessingTimeLimit,
|
|
605
|
+
);
|
|
600
606
|
|
|
601
|
-
|
|
607
|
+
processing.catch((err) => {
|
|
602
608
|
console.error('Error processing textures:', err);
|
|
603
609
|
});
|
|
604
610
|
}
|
|
@@ -1039,6 +1045,7 @@ export class Stage {
|
|
|
1039
1045
|
w: props.w ?? 0,
|
|
1040
1046
|
h: props.h ?? 0,
|
|
1041
1047
|
alpha: props.alpha ?? 1,
|
|
1048
|
+
ignoreParentAlpha: props.ignoreParentAlpha ?? false,
|
|
1042
1049
|
autosize: props.autosize ?? false,
|
|
1043
1050
|
boundsMargin: props.boundsMargin ?? null,
|
|
1044
1051
|
clipping: props.clipping ?? false,
|
|
@@ -175,7 +175,7 @@ function freedCachedTexture(cacheKey: string): Texture & {
|
|
|
175
175
|
state: 'freed',
|
|
176
176
|
type: TextureType.image,
|
|
177
177
|
preventCleanup: false,
|
|
178
|
-
renderableOwners:
|
|
178
|
+
renderableOwners: new Set<string | number>(),
|
|
179
179
|
cacheKey,
|
|
180
180
|
free: vi.fn(),
|
|
181
181
|
destroy: vi.fn(),
|
|
@@ -206,7 +206,7 @@ describe('TextureMemoryManager — orphaned freed texture eviction', () => {
|
|
|
206
206
|
it('keeps a freed texture that still has renderable owners', () => {
|
|
207
207
|
const { mgr, keyCache } = makeManager();
|
|
208
208
|
const texture = freedCachedTexture('img:poster.png');
|
|
209
|
-
|
|
209
|
+
texture.renderableOwners.add(1);
|
|
210
210
|
keyCache.set('img:poster.png', texture);
|
|
211
211
|
|
|
212
212
|
mgr.cleanup();
|