@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.
Files changed (41) hide show
  1. package/dist/src/core/CoreNode.d.ts +33 -0
  2. package/dist/src/core/CoreNode.js +34 -6
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/CoreTextNode.js +3 -2
  5. package/dist/src/core/CoreTextNode.js.map +1 -1
  6. package/dist/src/core/CoreTextureManager.d.ts +39 -3
  7. package/dist/src/core/CoreTextureManager.js +72 -61
  8. package/dist/src/core/CoreTextureManager.js.map +1 -1
  9. package/dist/src/core/Stage.js +8 -4
  10. package/dist/src/core/Stage.js.map +1 -1
  11. package/dist/src/core/TextureMemoryManager.js +1 -1
  12. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  13. package/dist/src/core/lib/collectionUtils.d.ts +1 -1
  14. package/dist/src/core/lib/collectionUtils.js +13 -31
  15. package/dist/src/core/lib/collectionUtils.js.map +1 -1
  16. package/dist/src/core/renderers/webgl/WebGlCtxTexture.js +5 -3
  17. package/dist/src/core/renderers/webgl/WebGlCtxTexture.js.map +1 -1
  18. package/dist/src/core/textures/SubTexture.js +1 -1
  19. package/dist/src/core/textures/SubTexture.js.map +1 -1
  20. package/dist/src/core/textures/Texture.d.ts +1 -1
  21. package/dist/src/core/textures/Texture.js +10 -13
  22. package/dist/src/core/textures/Texture.js.map +1 -1
  23. package/dist/src/main-api/Inspector.js +1 -1
  24. package/dist/src/main-api/Inspector.js.map +1 -1
  25. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  26. package/package.json +1 -1
  27. package/src/core/CoreNode.test.ts +272 -0
  28. package/src/core/CoreNode.ts +62 -8
  29. package/src/core/CoreTextNode.test.ts +1 -0
  30. package/src/core/CoreTextNode.ts +3 -2
  31. package/src/core/CoreTextureManager.ts +75 -70
  32. package/src/core/Stage.ts +11 -4
  33. package/src/core/TextureMemoryManager.test.ts +2 -2
  34. package/src/core/TextureMemoryManager.ts +1 -1
  35. package/src/core/lib/collectionUtils.test.ts +91 -0
  36. package/src/core/lib/collectionUtils.ts +13 -34
  37. package/src/core/renderers/webgl/WebGlCtxTexture.ts +5 -4
  38. package/src/core/textures/SubTexture.ts +1 -1
  39. package/src/core/textures/Texture.test.ts +124 -0
  40. package/src/core/textures/Texture.ts +10 -13
  41. package/src/main-api/Inspector.ts +1 -1
@@ -349,7 +349,7 @@ export class TextureMemoryManager {
349
349
  if (
350
350
  evictable === true &&
351
351
  texture.preventCleanup === false &&
352
- texture.renderableOwners.length === 0 &&
352
+ texture.renderableOwners.size === 0 &&
353
353
  texture.hasListeners() === false
354
354
  ) {
355
355
  this.destroyTexture(texture);
@@ -0,0 +1,91 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import type { CoreNode } from '../CoreNode.js';
3
+ import { sortByZIndexStable } from './collectionUtils.js';
4
+
5
+ const makeNode = (zIndex: number, tag: number): CoreNode =>
6
+ ({ props: { zIndex }, tag } as unknown as CoreNode);
7
+
8
+ const tags = (nodes: CoreNode[]): number[] => {
9
+ const out: number[] = [];
10
+ for (let i = 0; i < nodes.length; i++) {
11
+ out.push((nodes[i] as unknown as { tag: number }).tag);
12
+ }
13
+ return out;
14
+ };
15
+
16
+ describe('sortByZIndexStable', () => {
17
+ it('should sort nodes ascending by zIndex', () => {
18
+ const nodes = [makeNode(3, 0), makeNode(1, 1), makeNode(2, 2)];
19
+
20
+ sortByZIndexStable(nodes);
21
+
22
+ expect(tags(nodes)).toEqual([1, 2, 0]);
23
+ });
24
+
25
+ it('should preserve insertion order for equal zIndex values', () => {
26
+ const nodes = [
27
+ makeNode(1, 0),
28
+ makeNode(0, 1),
29
+ makeNode(1, 2),
30
+ makeNode(0, 3),
31
+ makeNode(1, 4),
32
+ ];
33
+
34
+ sortByZIndexStable(nodes);
35
+
36
+ expect(tags(nodes)).toEqual([1, 3, 0, 2, 4]);
37
+ });
38
+
39
+ it('should be stable for arrays longer than 10 elements', () => {
40
+ // V8 on Chrome < 70 switched to an unstable quicksort above 10 elements;
41
+ // this guards the case the native sort got wrong.
42
+ const nodes: CoreNode[] = [];
43
+ for (let i = 0; i < 20; i++) {
44
+ nodes.push(makeNode(i % 2, i));
45
+ }
46
+
47
+ sortByZIndexStable(nodes);
48
+
49
+ const expected: number[] = [];
50
+ for (let i = 0; i < 20; i += 2) {
51
+ expected.push(i);
52
+ }
53
+ for (let i = 1; i < 20; i += 2) {
54
+ expected.push(i);
55
+ }
56
+ expect(tags(nodes)).toEqual(expected);
57
+ });
58
+
59
+ it('should handle negative and fractional zIndex values', () => {
60
+ const nodes = [
61
+ makeNode(0.5, 0),
62
+ makeNode(-2, 1),
63
+ makeNode(1000000000, 2),
64
+ makeNode(0, 3),
65
+ makeNode(-2, 4),
66
+ ];
67
+
68
+ sortByZIndexStable(nodes);
69
+
70
+ expect(tags(nodes)).toEqual([1, 4, 3, 0, 2]);
71
+ });
72
+
73
+ it('should leave an already sorted array unchanged', () => {
74
+ const nodes = [makeNode(0, 0), makeNode(0, 1), makeNode(1, 2)];
75
+
76
+ sortByZIndexStable(nodes);
77
+
78
+ expect(tags(nodes)).toEqual([0, 1, 2]);
79
+ });
80
+
81
+ it('should handle empty and single-element arrays', () => {
82
+ const empty: CoreNode[] = [];
83
+ const single = [makeNode(5, 0)];
84
+
85
+ sortByZIndexStable(empty);
86
+ sortByZIndexStable(single);
87
+
88
+ expect(empty.length).toBe(0);
89
+ expect(tags(single)).toEqual([0]);
90
+ });
91
+ });
@@ -1,44 +1,23 @@
1
1
  import type { CoreNode } from '../CoreNode.js';
2
2
 
3
- //Bucket sort implementation for sorting CoreNode arrays by zIndex
4
- export const bucketSortByZIndex = (nodes: CoreNode[], min: number): void => {
5
- const buckets: CoreNode[][] = [];
6
- const bucketIndices: number[] = [];
7
- //distribute nodes into buckets
8
- for (let i = 0; i < nodes.length; i++) {
3
+ // Stable in-place sort by zIndex. Array.prototype.sort is not stable on
4
+ // Chrome < 70 (V8 quicksorts arrays longer than 10), and equal-zIndex
5
+ // siblings must keep insertion order or paint order silently reshuffles.
6
+ // Children arrays are small and nearly sorted (kept sorted; a re-sort
7
+ // usually follows a single zIndex change), so insertion sort is O(n) in
8
+ // practice and allocation-free.
9
+ export const sortByZIndexStable = (nodes: CoreNode[]): void => {
10
+ const len = nodes.length;
11
+ for (let i = 1; i < len; i++) {
9
12
  const node = nodes[i]!;
10
- const index = node.props.zIndex - min;
11
- //create bucket if it doesn't exist
12
- if (buckets[index] === undefined) {
13
- buckets[index] = [];
14
- bucketIndices.push(index);
15
- }
16
- buckets[index]!.push(node);
17
- }
18
-
19
- //sort each bucket using insertion sort
20
- for (let i = 1; i < bucketIndices.length; i++) {
21
- const key = bucketIndices[i]!;
13
+ const z = node.props.zIndex;
22
14
  let j = i - 1;
23
- while (j >= 0 && bucketIndices[j]! > key) {
24
- bucketIndices[j + 1] = bucketIndices[j]!;
15
+ while (j >= 0 && nodes[j]!.props.zIndex > z) {
16
+ nodes[j + 1] = nodes[j]!;
25
17
  j--;
26
18
  }
27
- bucketIndices[j + 1] = key;
19
+ nodes[j + 1] = node;
28
20
  }
29
-
30
- //flatten buckets
31
- let idx = 0;
32
- for (let i = 0; i < bucketIndices.length; i++) {
33
- const bucket = buckets[bucketIndices[i]!]!;
34
- for (let j = 0; j < bucket.length; j++) {
35
- nodes[idx++] = bucket[j]!;
36
- }
37
- }
38
-
39
- //clean up
40
- buckets.length = 0;
41
- bucketIndices.length = 0;
42
21
  };
43
22
 
44
23
  export const incrementalRepositionByZIndex = (
@@ -161,10 +161,11 @@ export class WebGlCtxTexture extends CoreContextTexture {
161
161
  );
162
162
  }
163
163
 
164
- // Set to a 1x1 transparent texture
165
- glw.texImage2D(0, glw.RGBA, 1, 1, 0, glw.RGBA, glw.UNSIGNED_BYTE, null);
166
- this.setTextureMemUse(TRANSPARENT_TEXTURE_DATA.byteLength);
167
-
164
+ // No placeholder upload here: every branch below fully (re)uploads the
165
+ // texture before this method returns, and there is no `await` in between,
166
+ // so no frame can render an intermediate 1x1. The empty-texture case
167
+ // (`tdata === null`) does its own 1x1 upload. Uploading a throwaway 1x1
168
+ // first would be a wasted texImage2D + setTextureMemUse per texture.
168
169
  let w = 0;
169
170
  let h = 0;
170
171
 
@@ -79,7 +79,7 @@ export class SubTexture extends Texture {
79
79
  // Resolve parent texture from cache or fallback to provided texture
80
80
  this.parentTexture = txManager.resolveParentTexture(this.props.texture);
81
81
 
82
- if (this.renderableOwners.length > 0) {
82
+ if (this.renderableOwners.size > 0) {
83
83
  this.parentTexture.setRenderableOwner(this.subtextureId, true);
84
84
  }
85
85
 
@@ -0,0 +1,124 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { Texture, type TextureData } from './Texture.js';
3
+ import type { CoreTextureManager } from '../CoreTextureManager.js';
4
+
5
+ class TestTexture extends Texture {
6
+ override async getTextureSource(): Promise<TextureData> {
7
+ return { data: null };
8
+ }
9
+ }
10
+
11
+ function makeTexture(): {
12
+ texture: TestTexture;
13
+ loadTexture: ReturnType<typeof vi.fn>;
14
+ onChangeIsRenderable: ReturnType<typeof vi.fn>;
15
+ } {
16
+ const loadTexture = vi.fn();
17
+ const txManager = {
18
+ maxRetryCount: 3,
19
+ loadTexture,
20
+ } as unknown as CoreTextureManager;
21
+ const texture = new TestTexture(txManager);
22
+ const onChangeIsRenderable = vi.fn();
23
+ texture.onChangeIsRenderable = onChangeIsRenderable;
24
+ return { texture, loadTexture, onChangeIsRenderable };
25
+ }
26
+
27
+ describe('Texture.setRenderableOwner', () => {
28
+ it('adding an owner fires the 0→1 transition exactly once and loads', () => {
29
+ const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
30
+
31
+ texture.setRenderableOwner(1, true);
32
+
33
+ expect(texture.renderableOwners.size).toBe(1);
34
+ expect(texture.renderable).toBe(true);
35
+ expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
36
+ expect(onChangeIsRenderable).toHaveBeenCalledWith(true);
37
+ expect(loadTexture).toHaveBeenCalledTimes(1);
38
+ });
39
+
40
+ it('double-add of the same owner is a no-op (size stays 1, one event)', () => {
41
+ const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
42
+
43
+ texture.setRenderableOwner(1, true);
44
+ texture.setRenderableOwner(1, true);
45
+
46
+ expect(texture.renderableOwners.size).toBe(1);
47
+ expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
48
+ expect(loadTexture).toHaveBeenCalledTimes(1);
49
+ });
50
+
51
+ it('a second distinct owner does not re-fire the transition', () => {
52
+ const { texture, loadTexture, onChangeIsRenderable } = makeTexture();
53
+
54
+ texture.setRenderableOwner(1, true);
55
+ texture.setRenderableOwner(2, true);
56
+
57
+ expect(texture.renderableOwners.size).toBe(2);
58
+ expect(onChangeIsRenderable).toHaveBeenCalledTimes(1);
59
+ expect(loadTexture).toHaveBeenCalledTimes(1);
60
+ });
61
+
62
+ it('removing a non-member owner does not fire a transition', () => {
63
+ const { texture, onChangeIsRenderable } = makeTexture();
64
+
65
+ texture.setRenderableOwner(1, false);
66
+
67
+ expect(texture.renderableOwners.size).toBe(0);
68
+ expect(texture.renderable).toBe(false);
69
+ expect(onChangeIsRenderable).not.toHaveBeenCalled();
70
+ });
71
+
72
+ it('1→0 fires onChangeIsRenderable(false) exactly once', () => {
73
+ const { texture, onChangeIsRenderable } = makeTexture();
74
+
75
+ texture.setRenderableOwner(1, true);
76
+ texture.setRenderableOwner(1, false);
77
+ texture.setRenderableOwner(1, false);
78
+
79
+ expect(texture.renderableOwners.size).toBe(0);
80
+ expect(texture.renderable).toBe(false);
81
+ expect(onChangeIsRenderable).toHaveBeenCalledTimes(2);
82
+ expect(onChangeIsRenderable).toHaveBeenLastCalledWith(false);
83
+ });
84
+
85
+ it('re-adding an owner after 1→0 triggers load again (freed→reload cycle)', () => {
86
+ const { texture, loadTexture } = makeTexture();
87
+
88
+ texture.setRenderableOwner(1, true);
89
+ texture.setRenderableOwner(1, false);
90
+ texture.setRenderableOwner(1, true);
91
+
92
+ expect(texture.renderable).toBe(true);
93
+ expect(loadTexture).toHaveBeenCalledTimes(2);
94
+ });
95
+
96
+ it('string and number owners coexist (node ids vs subtexture/font keys)', () => {
97
+ const { texture } = makeTexture();
98
+
99
+ texture.setRenderableOwner(7, true);
100
+ texture.setRenderableOwner('subtexture-7', true);
101
+
102
+ expect(texture.renderableOwners.size).toBe(2);
103
+
104
+ texture.setRenderableOwner(7, false);
105
+ expect(texture.renderableOwners.size).toBe(1);
106
+ expect(texture.renderable).toBe(true);
107
+
108
+ texture.setRenderableOwner('subtexture-7', false);
109
+ expect(texture.renderableOwners.size).toBe(0);
110
+ expect(texture.renderable).toBe(false);
111
+ });
112
+
113
+ it('canBeCleanedUp respects remaining owners', () => {
114
+ const { texture } = makeTexture();
115
+ // Skip the startup grace period so owners are the deciding factor.
116
+ texture.isWithinStartupGracePeriod = () => false;
117
+
118
+ texture.setRenderableOwner(1, true);
119
+ expect(texture.canBeCleanedUp()).toBe(false);
120
+
121
+ texture.setRenderableOwner(1, false);
122
+ expect(texture.canBeCleanedUp()).toBe(true);
123
+ });
124
+ });
@@ -141,7 +141,7 @@ export abstract class Texture extends EventEmitter {
141
141
  // aggregate state
142
142
  public state: TextureState = 'initial';
143
143
 
144
- readonly renderableOwners: any[] = [];
144
+ readonly renderableOwners: Set<string | number> = new Set();
145
145
 
146
146
  readonly renderable: boolean = false;
147
147
 
@@ -251,7 +251,7 @@ export abstract class Texture extends EventEmitter {
251
251
  }
252
252
 
253
253
  // Don't cleanup if there are still renderable owners
254
- if (this.renderableOwners.length > 0) {
254
+ if (this.renderableOwners.size > 0) {
255
255
  return false;
256
256
  }
257
257
 
@@ -274,28 +274,25 @@ export abstract class Texture extends EventEmitter {
274
274
  * @param renderable
275
275
  */
276
276
  setRenderableOwner(owner: string | number, renderable: boolean): void {
277
- const oldSize = this.renderableOwners.length;
278
- const hasOwnerIndex = this.renderableOwners.indexOf(owner);
277
+ const owners = this.renderableOwners;
279
278
 
280
279
  if (renderable === true) {
281
- if (hasOwnerIndex === -1) {
282
- // Add the owner to the set
283
- this.renderableOwners.push(owner);
280
+ if (owners.has(owner) === true) {
281
+ return;
284
282
  }
285
283
 
286
- const newSize = this.renderableOwners.length;
287
- if (oldSize !== newSize && newSize === 1) {
284
+ owners.add(owner);
285
+ if (owners.size === 1) {
288
286
  (this.renderable as boolean) = true;
289
287
  this.onChangeIsRenderable?.(true);
290
288
  this.load();
291
289
  }
292
290
  } else {
293
- if (hasOwnerIndex !== -1) {
294
- this.renderableOwners.splice(hasOwnerIndex, 1);
291
+ if (owners.delete(owner) === false) {
292
+ return;
295
293
  }
296
294
 
297
- const newSize = this.renderableOwners.length;
298
- if (oldSize !== newSize && newSize === 0) {
295
+ if (owners.size === 0) {
299
296
  (this.renderable as boolean) = false;
300
297
  this.onChangeIsRenderable?.(false);
301
298
 
@@ -1104,7 +1104,7 @@ export class Inspector {
1104
1104
  // Update renderable owners count
1105
1105
  div.setAttribute(
1106
1106
  'data-texture-owners',
1107
- String(texture.renderableOwners.length),
1107
+ String(texture.renderableOwners.size),
1108
1108
  );
1109
1109
 
1110
1110
  // Update retry count