@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.
Files changed (108) hide show
  1. package/dist/src/core/CoreNode.d.ts +0 -10
  2. package/dist/src/core/CoreNode.js +7 -14
  3. package/dist/src/core/CoreNode.js.map +1 -1
  4. package/dist/src/core/Stage.d.ts +7 -15
  5. package/dist/src/core/Stage.js +23 -41
  6. package/dist/src/core/Stage.js.map +1 -1
  7. package/dist/src/core/TextureMemoryManager.js +4 -3
  8. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  9. package/dist/src/core/lib/ImageWorker.d.ts +16 -1
  10. package/dist/src/core/lib/ImageWorker.js +52 -17
  11. package/dist/src/core/lib/ImageWorker.js.map +1 -1
  12. package/dist/src/core/lib/WebGlContextWrapper.d.ts +2 -32
  13. package/dist/src/core/lib/WebGlContextWrapper.js +26 -81
  14. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  15. package/dist/src/core/lib/utils.d.ts +0 -1
  16. package/dist/src/core/lib/utils.js +0 -3
  17. package/dist/src/core/lib/utils.js.map +1 -1
  18. package/dist/src/core/platforms/web/WebPlatform.js +1 -0
  19. package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
  20. package/dist/src/core/renderers/CoreRenderer.d.ts +0 -1
  21. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  22. package/dist/src/core/renderers/CoreShaderNode.d.ts +16 -0
  23. package/dist/src/core/renderers/CoreShaderNode.js +22 -0
  24. package/dist/src/core/renderers/CoreShaderNode.js.map +1 -1
  25. package/dist/src/core/renderers/canvas/CanvasRenderer.js +23 -3
  26. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  27. package/dist/src/core/renderers/canvas/CanvasShaderNode.d.ts +0 -1
  28. package/dist/src/core/renderers/canvas/CanvasShaderNode.js +0 -1
  29. package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
  30. package/dist/src/core/renderers/canvas/CanvasTexture.d.ts +1 -1
  31. package/dist/src/core/renderers/canvas/CanvasTexture.js +25 -6
  32. package/dist/src/core/renderers/canvas/CanvasTexture.js.map +1 -1
  33. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +1 -7
  34. package/dist/src/core/renderers/webgl/WebGlRenderer.js +9 -13
  35. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  36. package/dist/src/core/renderers/webgl/WebGlShaderNode.d.ts +2 -4
  37. package/dist/src/core/renderers/webgl/WebGlShaderNode.js +0 -1
  38. package/dist/src/core/renderers/webgl/WebGlShaderNode.js.map +1 -1
  39. package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +0 -7
  40. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +3 -19
  41. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  42. package/dist/src/core/renderers/webgl/internal/RendererUtils.d.ts +0 -33
  43. package/dist/src/core/renderers/webgl/internal/RendererUtils.js +0 -50
  44. package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
  45. package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +0 -1
  46. package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
  47. package/dist/src/core/text-rendering/CanvasFontHandler.js +0 -5
  48. package/dist/src/core/text-rendering/CanvasFontHandler.js.map +1 -1
  49. package/dist/src/core/text-rendering/CanvasTextRenderer.d.ts +1 -0
  50. package/dist/src/core/text-rendering/CanvasTextRenderer.js +23 -0
  51. package/dist/src/core/text-rendering/CanvasTextRenderer.js.map +1 -1
  52. package/dist/src/core/text-rendering/SdfTextRenderer.d.ts +1 -0
  53. package/dist/src/core/text-rendering/SdfTextRenderer.js +50 -4
  54. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  55. package/dist/src/core/text-rendering/TextRenderer.d.ts +7 -0
  56. package/dist/src/core/textures/ImageTexture.js +11 -0
  57. package/dist/src/core/textures/ImageTexture.js.map +1 -1
  58. package/dist/src/core/textures/SubTexture.d.ts +1 -0
  59. package/dist/src/core/textures/SubTexture.js +12 -0
  60. package/dist/src/core/textures/SubTexture.js.map +1 -1
  61. package/dist/src/core/textures/Texture.js +10 -0
  62. package/dist/src/core/textures/Texture.js.map +1 -1
  63. package/dist/src/main-api/Inspector.d.ts +2 -1
  64. package/dist/src/main-api/Inspector.js +43 -15
  65. package/dist/src/main-api/Inspector.js.map +1 -1
  66. package/dist/src/main-api/Renderer.d.ts +16 -10
  67. package/dist/src/main-api/Renderer.js +3 -3
  68. package/dist/src/main-api/Renderer.js.map +1 -1
  69. package/dist/src/utils.d.ts +1 -1
  70. package/dist/src/utils.js +2 -2
  71. package/dist/src/utils.js.map +1 -1
  72. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  73. package/package.json +1 -1
  74. package/src/core/CoreNode.ts +8 -26
  75. package/src/core/Stage.ts +24 -49
  76. package/src/core/TextureMemoryManager.ts +4 -3
  77. package/src/core/lib/ImageWorker.test.ts +106 -0
  78. package/src/core/lib/ImageWorker.ts +60 -22
  79. package/src/core/lib/WebGlContextWrapper.ts +32 -101
  80. package/src/core/lib/utils.ts +0 -4
  81. package/src/core/platforms/web/WebPlatform.outOfMemory.test.ts +1 -0
  82. package/src/core/platforms/web/WebPlatform.ts +1 -0
  83. package/src/core/renderers/CoreRenderer.ts +0 -1
  84. package/src/core/renderers/CoreShaderNode.test.ts +53 -0
  85. package/src/core/renderers/CoreShaderNode.ts +23 -0
  86. package/src/core/renderers/canvas/CanvasRenderer.test.ts +34 -0
  87. package/src/core/renderers/canvas/CanvasRenderer.ts +33 -4
  88. package/src/core/renderers/canvas/CanvasShaderNode.ts +0 -1
  89. package/src/core/renderers/canvas/CanvasTexture.test.ts +31 -0
  90. package/src/core/renderers/canvas/CanvasTexture.ts +35 -7
  91. package/src/core/renderers/webgl/WebGlRenderer.ts +9 -26
  92. package/src/core/renderers/webgl/WebGlShaderNode.ts +2 -4
  93. package/src/core/renderers/webgl/WebGlShaderProgram.ts +3 -20
  94. package/src/core/renderers/webgl/internal/RendererUtils.ts +0 -87
  95. package/src/core/renderers/webgl/internal/ShaderUtils.ts +0 -1
  96. package/src/core/text-rendering/CanvasFontHandler.ts +0 -6
  97. package/src/core/text-rendering/CanvasTextRenderer.ts +26 -0
  98. package/src/core/text-rendering/SdfTextRenderer.test.ts +150 -0
  99. package/src/core/text-rendering/SdfTextRenderer.ts +54 -4
  100. package/src/core/text-rendering/TextRenderer.ts +7 -0
  101. package/src/core/textures/ImageTexture.ts +18 -0
  102. package/src/core/textures/SubTexture.test.ts +45 -0
  103. package/src/core/textures/SubTexture.ts +13 -0
  104. package/src/core/textures/Texture.ts +12 -0
  105. package/src/main-api/Inspector.ts +46 -16
  106. package/src/main-api/Renderer.ts +20 -14
  107. package/src/utils.ts +1 -2
  108. package/src/core/renderers/webgl/internal/WebGlUtils.ts +0 -16
package/src/core/Stage.ts CHANGED
@@ -37,7 +37,6 @@ import {
37
37
  import { CoreRenderer } from './renderers/CoreRenderer.js';
38
38
  import { CoreTextNode, type CoreTextNodeProps } from './CoreTextNode.js';
39
39
  import { santizeCustomDataMap } from '../main-api/utils.js';
40
- import { pointInBound } from './lib/utils.js';
41
40
  import type { CoreShaderNode } from './renderers/CoreShaderNode.js';
42
41
  import { Matrix3d } from './lib/Matrix3d.js';
43
42
  import { createBound, createPreloadBounds, type Bound } from './lib/utils.js';
@@ -71,11 +70,6 @@ export type StageFrameTickHandler = (
71
70
  frameTickData: FrameTickPayload,
72
71
  ) => void;
73
72
 
74
- export interface Point {
75
- x: number;
76
- y: number;
77
- }
78
-
79
73
  const autoStart = true;
80
74
 
81
75
  export class Stage {
@@ -88,7 +82,6 @@ export class Stage {
88
82
  public readonly shManager: CoreShaderManager;
89
83
  public readonly renderer: CoreRenderer;
90
84
  public readonly root: CoreNode;
91
- public readonly interactiveNodes: Set<CoreNode> = new Set();
92
85
  public boundsMargin: [number, number, number, number];
93
86
  public readonly defShaderNode: CoreShaderNode | null = null;
94
87
  public strictBound: Bound;
@@ -173,7 +166,6 @@ export class Stage {
173
166
  appHeight,
174
167
  boundsMargin,
175
168
  enableContextSpy,
176
- forceWebGL2,
177
169
  numImageWorkers,
178
170
  textureMemory,
179
171
  renderEngine,
@@ -244,7 +236,6 @@ export class Stage {
244
236
  stage: this,
245
237
  canvas,
246
238
  contextSpy: this.contextSpy,
247
- forceWebGL2,
248
239
  });
249
240
 
250
241
  this.shManager = new CoreShaderManager(this);
@@ -254,6 +245,17 @@ export class Stage {
254
245
 
255
246
  const renderMode = this.renderer.mode || 'webgl';
256
247
 
248
+ // Canvas2D textures are plain JS heap objects managed by the browser GC.
249
+ // Threshold-based upload blocking makes no sense for JS heap — disable it
250
+ // by setting criticalThreshold to 0 while keeping the eviction machinery.
251
+ if (renderMode === 'canvas') {
252
+ this.txMemManager.updateSettings({
253
+ ...textureMemory,
254
+ criticalThreshold: 0,
255
+ doNotExceedCriticalThreshold: false,
256
+ });
257
+ }
258
+
257
259
  this.createDefaultTexture();
258
260
  setPremultiplyMode(renderMode);
259
261
 
@@ -509,6 +511,19 @@ export class Stage {
509
511
  );
510
512
  }
511
513
 
514
+ /**
515
+ * Trim text renderer caches back to their configured limits.
516
+ *
517
+ * Called when the stage goes idle so layout-cache eviction never competes
518
+ * with active rendering.
519
+ */
520
+ cleanupTextRenderers() {
521
+ const textRenderers = this.textRenderers;
522
+ for (const key in textRenderers) {
523
+ textRenderers[key]!.cleanup();
524
+ }
525
+ }
526
+
512
527
  /**
513
528
  * Start a new frame draw
514
529
  */
@@ -897,45 +912,6 @@ export class Stage {
897
912
  this.root.childUpdateType |= UpdateType.RenderBounds;
898
913
  }
899
914
 
900
- /** Find all nodes at a given point
901
- * @param data
902
- */
903
- findNodesAtPoint(data: Point): CoreNode[] {
904
- const x = data.x / this.options.deviceLogicalPixelRatio;
905
- const y = data.y / this.options.deviceLogicalPixelRatio;
906
- const nodes: CoreNode[] = [];
907
- for (const node of this.interactiveNodes) {
908
- if (node.isRenderable === false) {
909
- continue;
910
- }
911
- if (pointInBound(x, y, node.renderBound!) === true) {
912
- nodes.push(node);
913
- }
914
- }
915
- return nodes;
916
- }
917
-
918
- /**
919
- * Find the top node at a given point
920
- * @param data
921
- * @returns
922
- */
923
- getNodeFromPosition(data: Point): CoreNode | null {
924
- const nodes: CoreNode[] = this.findNodesAtPoint(data);
925
- if (nodes.length === 0) {
926
- return null;
927
- }
928
-
929
- //get last node in array (as top node)
930
- let topNode = nodes[nodes.length - 1] as CoreNode;
931
- for (let i = 0; i < nodes.length; i++) {
932
- if (nodes[i]!.zIndex > topNode.zIndex) {
933
- topNode = nodes[i]!;
934
- }
935
- }
936
- return topNode || null;
937
- }
938
-
939
915
  /**
940
916
  * add node to timeNodes arrays
941
917
  * @param node
@@ -1062,7 +1038,6 @@ export class Stage {
1062
1038
  rtt: props.rtt ?? false,
1063
1039
  data,
1064
1040
  imageType: props.imageType,
1065
- interactive: props.interactive ?? false,
1066
1041
  preventDestroy: props.preventDestroy,
1067
1042
  componentName: props.componentName,
1068
1043
  componentLocation: props.componentLocation,
@@ -148,7 +148,7 @@ export class TextureMemoryManager {
148
148
  this.loadedTextures.add(texture);
149
149
  }
150
150
 
151
- if (this.memUsed > this.criticalThreshold) {
151
+ if (this.criticalThreshold > 0 && this.memUsed > this.criticalThreshold) {
152
152
  this.criticalCleanupRequested = true;
153
153
  }
154
154
  }
@@ -156,13 +156,14 @@ export class TextureMemoryManager {
156
156
  checkCleanup() {
157
157
  return (
158
158
  this.criticalCleanupRequested ||
159
- (this.memUsed > this.targetThreshold &&
159
+ (this.criticalThreshold > 0 &&
160
+ this.memUsed > this.targetThreshold &&
160
161
  this.frameTime - this.lastCleanupTime >= this.cleanupInterval)
161
162
  );
162
163
  }
163
164
 
164
165
  checkCriticalCleanup() {
165
- return this.memUsed > this.criticalThreshold;
166
+ return this.criticalThreshold > 0 && this.memUsed > this.criticalThreshold;
166
167
  }
167
168
 
168
169
  /**
@@ -0,0 +1,106 @@
1
+ import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
2
+ import { ImageWorkerManager } from './ImageWorker.js';
3
+ import type { CreateImageBitmapSupport } from './validateImageBitmap.js';
4
+
5
+ /**
6
+ * Minimal Web Worker stand-in. The real Worker spawns a thread that parses the
7
+ * blob script; here we only care about how many are constructed and when, so we
8
+ * record every instance and never auto-respond (so `getImage` calls stay
9
+ * "in flight" and accumulate load, simulating concurrency).
10
+ */
11
+ class FakeWorker {
12
+ static instances: FakeWorker[] = [];
13
+ static reset() {
14
+ FakeWorker.instances = [];
15
+ }
16
+ onmessage: ((e: unknown) => void) | null = null;
17
+ onerror: ((e: unknown) => void) | null = null;
18
+ onmessageerror: ((e: unknown) => void) | null = null;
19
+ postMessage = vi.fn();
20
+ terminate = vi.fn();
21
+ constructor(public url: string) {
22
+ FakeWorker.instances.push(this);
23
+ }
24
+ }
25
+
26
+ const support: CreateImageBitmapSupport = {
27
+ basic: true,
28
+ options: true,
29
+ full: true,
30
+ premultiplyHonored: true,
31
+ };
32
+
33
+ let createObjectURL: ReturnType<typeof vi.fn>;
34
+ let revokeObjectURL: ReturnType<typeof vi.fn>;
35
+
36
+ beforeEach(() => {
37
+ FakeWorker.reset();
38
+ createObjectURL = vi.fn(() => 'blob:fake-url');
39
+ revokeObjectURL = vi.fn();
40
+ // The manager builds the worker source via Blob + object URL, then spawns
41
+ // Workers. None of these exist in the node test env, so stub them.
42
+ vi.stubGlobal(
43
+ 'Blob',
44
+ class {
45
+ constructor(public parts: unknown[], public opts: unknown) {}
46
+ },
47
+ );
48
+ vi.stubGlobal('URL', { createObjectURL, revokeObjectURL });
49
+ vi.stubGlobal('self', { URL: { createObjectURL, revokeObjectURL } });
50
+ vi.stubGlobal('Worker', FakeWorker);
51
+ });
52
+
53
+ afterEach(() => {
54
+ vi.unstubAllGlobals();
55
+ });
56
+
57
+ const load = (mgr: ImageWorkerManager) =>
58
+ void mgr.getImage('img.png', null, null, null, null, null);
59
+
60
+ describe('ImageWorkerManager pool spawning', () => {
61
+ it('does not spawn any workers at construction', () => {
62
+ new ImageWorkerManager(3, support);
63
+ // Spawning is lazy — nothing happens until the first image request.
64
+ expect(FakeWorker.instances.length).toBe(0);
65
+ });
66
+
67
+ it('spawns the whole pool at once on the first image request', () => {
68
+ const mgr = new ImageWorkerManager(3, support);
69
+ load(mgr);
70
+ expect(FakeWorker.instances.length).toBe(3);
71
+ });
72
+
73
+ it('does not respawn the pool on subsequent requests', () => {
74
+ const mgr = new ImageWorkerManager(3, support);
75
+ load(mgr); // spawns the pool
76
+ expect(FakeWorker.instances.length).toBe(3);
77
+ // Subsequent requests reuse the existing pool — no new workers.
78
+ load(mgr);
79
+ load(mgr);
80
+ load(mgr);
81
+ expect(FakeWorker.instances.length).toBe(3);
82
+ });
83
+
84
+ it('spawns exactly maxWorkers when that is 1', () => {
85
+ const mgr = new ImageWorkerManager(1, support);
86
+ load(mgr);
87
+ expect(FakeWorker.instances.length).toBe(1);
88
+ load(mgr);
89
+ load(mgr);
90
+ expect(FakeWorker.instances.length).toBe(1);
91
+ });
92
+
93
+ it('serializes the worker source once, reusing the blob per worker', () => {
94
+ const mgr = new ImageWorkerManager(3, support);
95
+ load(mgr);
96
+ // One object URL created+revoked per spawned worker (3), serialized once.
97
+ expect(createObjectURL).toHaveBeenCalledTimes(3);
98
+ expect(revokeObjectURL).toHaveBeenCalledTimes(3);
99
+ });
100
+
101
+ it('routes each request to a worker via postMessage', () => {
102
+ const mgr = new ImageWorkerManager(2, support);
103
+ load(mgr);
104
+ expect(FakeWorker.instances[0]!.postMessage).toHaveBeenCalledTimes(1);
105
+ });
106
+ });
@@ -177,20 +177,32 @@ export class ImageWorkerManager {
177
177
  workers: Worker[] = [];
178
178
  workerLoad: number[] = [];
179
179
  nextId = 0;
180
+ /** Upper bound on the pool size, from the `numImageWorkers` setting. */
181
+ private readonly maxWorkers: number;
182
+ /** Retained so the worker source is serialized once, when the pool spawns. */
183
+ private workerBlob: Blob | null = null;
184
+ private readonly createImageBitmapSupport: CreateImageBitmapSupport;
180
185
 
181
186
  constructor(
182
187
  numImageWorkers: number,
183
188
  createImageBitmapSupport: CreateImageBitmapSupport,
184
189
  ) {
185
- this.workers = this.createWorkers(
186
- numImageWorkers,
187
- createImageBitmapSupport,
188
- );
189
- this.workers.forEach((worker, index) => {
190
- worker.onmessage = (event) => this.handleMessage(event, index);
191
- worker.onerror = (event) => this.handleWorkerError(event, index);
192
- worker.onmessageerror = (event) => this.handleWorkerError(event, index);
193
- });
190
+ this.maxWorkers = numImageWorkers;
191
+ this.createImageBitmapSupport = createImageBitmapSupport;
192
+ }
193
+
194
+ /**
195
+ * Build the shared worker source once and spawn the full pool in a single
196
+ * burst. Called lazily on the first image request. No-op once spawned.
197
+ */
198
+ private spawnWorkers(): void {
199
+ if (this.workers.length > 0) {
200
+ return;
201
+ }
202
+ this.workerBlob = this.createWorkerBlob(this.createImageBitmapSupport);
203
+ for (let i = 0; i < this.maxWorkers; i++) {
204
+ this.spawnWorker();
205
+ }
194
206
  }
195
207
 
196
208
  private handleMessage(event: MessageEvent, workerIndex: number) {
@@ -231,10 +243,9 @@ export class ImageWorkerManager {
231
243
  this.workerLoad[workerIndex] = 0;
232
244
  }
233
245
 
234
- private createWorkers(
235
- numWorkers = 1,
246
+ private createWorkerBlob(
236
247
  createImageBitmapSupport: CreateImageBitmapSupport,
237
- ): Worker[] {
248
+ ): Blob {
238
249
  let workerCode = `(${createImageWorker.toString()})()`;
239
250
 
240
251
  // Replace placeholders with actual initialization values
@@ -265,19 +276,39 @@ export class ImageWorkerManager {
265
276
  }
266
277
 
267
278
  workerCode = workerCode.replace('"use strict";', '');
268
- const blob: Blob = new Blob([workerCode], {
279
+ return new Blob([workerCode], {
269
280
  type: 'application/javascript',
270
281
  });
271
- const urlFactory = self.URL ? URL : webkitURL;
272
- const blobURL: string = urlFactory.createObjectURL(blob);
273
- const workers: Worker[] = [];
274
- for (let i = 0; i < numWorkers; i++) {
275
- workers.push(new Worker(blobURL));
276
- this.workerLoad.push(0);
282
+ }
283
+
284
+ /**
285
+ * Spawn a single worker from the shared blob and wire up its handlers.
286
+ * No-op once the pool has reached `maxWorkers`.
287
+ */
288
+ private spawnWorker(): void {
289
+ if (this.workerBlob === null || this.workers.length >= this.maxWorkers) {
290
+ return;
277
291
  }
278
- // Workers retain the script; the URL itself is no longer needed.
292
+
293
+ const index = this.workers.length;
294
+ const urlFactory = self.URL ? URL : webkitURL;
295
+ const blobURL: string = urlFactory.createObjectURL(this.workerBlob);
296
+ const worker = new Worker(blobURL);
297
+ // The worker retains the script after construction; the URL is no longer
298
+ // needed once the Worker has been created from it.
279
299
  urlFactory.revokeObjectURL(blobURL);
280
- return workers;
300
+
301
+ worker.onmessage = (event) => this.handleMessage(event, index);
302
+ worker.onerror = (event) => this.handleWorkerError(event, index);
303
+ worker.onmessageerror = (event) => this.handleWorkerError(event, index);
304
+
305
+ this.workers.push(worker);
306
+ this.workerLoad.push(0);
307
+
308
+ // Pool is full — the blob will never be needed again, so release it.
309
+ if (this.workers.length >= this.maxWorkers) {
310
+ this.workerBlob = null;
311
+ }
281
312
  }
282
313
 
283
314
  private getNextWorkerIndex(): number {
@@ -311,7 +342,14 @@ export class ImageWorkerManager {
311
342
  ): Promise<TextureData> {
312
343
  return new Promise((resolve, reject) => {
313
344
  try {
314
- const nextWorkerIndex = this.getNextWorkerIndex();
345
+ let nextWorkerIndex = this.getNextWorkerIndex();
346
+ if (nextWorkerIndex === -1) {
347
+ // Pool not spawned yet — spin up all workers at once on the first
348
+ // image request, off the boot/first-render critical path.
349
+ this.spawnWorkers();
350
+ nextWorkerIndex = this.getNextWorkerIndex();
351
+ }
352
+
315
353
  if (nextWorkerIndex === -1) {
316
354
  reject(new Error('No image workers available'));
317
355
  return;
@@ -6,13 +6,12 @@ import type {
6
6
  Vec3,
7
7
  Vec4,
8
8
  } from '../renderers/webgl/internal/ShaderUtils.js';
9
- import { isWebGl2 } from '../renderers/webgl/internal/WebGlUtils.js';
10
9
 
11
10
  /**
12
11
  * Optimized WebGL Context Wrapper
13
12
  *
14
13
  * @remarks
15
- * This class contains the subset of the WebGLRenderingContext & WebGL2RenderingContext
14
+ * This class contains the subset of the WebGLRenderingContext
16
15
  * API that is used by the renderer. Select high volume WebGL methods include
17
16
  * caching optimizations to avoid making WebGL calls if the state is already set
18
17
  * to the desired value.
@@ -94,56 +93,40 @@ export class WebGlContextWrapper {
94
93
  public readonly INVALID_OPERATION: number;
95
94
  //#endregion WebGL Enums
96
95
 
97
- constructor(private gl: WebGLRenderingContext | WebGL2RenderingContext) {
98
- // The following code extracts the current state of the WebGL context
99
- // to our local JavaScript cached version of it. This is so we can
100
- // avoid making WebGL calls if we don't need to.
101
- // We could assume that the WebGL context is in a default state, but
102
- // in the future we may want to support restoring a broken WebGL context
103
- // and this will help with that.
104
- this.activeTextureUnit =
105
- (gl.getParameter(gl.ACTIVE_TEXTURE) as number) - gl.TEXTURE0;
96
+ constructor(private gl: WebGLRenderingContext) {
97
+ // A freshly created WebGL context is in a fully specified default state.
98
+ // Rather than reading that state back with getParameter/isEnabled each a
99
+ // synchronous CPU<->GPU round-trip, and previously ~one per texture unit
100
+ // (16-32) just to enumerate the bindings we seed the cache with those
101
+ // known defaults. The engine never restores a lost context in place (it
102
+ // reloads the app on `contextLost`), so there is no live state to recover.
103
+ this.activeTextureUnit = 0;
106
104
  const maxTextureUnits = gl.getParameter(
107
105
  gl.MAX_TEXTURE_IMAGE_UNITS,
108
106
  ) as number;
109
- // save current texture units
110
- this.texture2dUnits = new Array<undefined>(maxTextureUnits)
111
- .fill(undefined)
112
- .map((_, i) => {
113
- this.activeTexture(i);
114
- return gl.getParameter(gl.TEXTURE_BINDING_2D) as WebGLTexture;
115
- });
116
- // restore active texture unit
117
- this.activeTexture(this.activeTextureUnit);
118
- this.scissorEnabled = gl.isEnabled(gl.SCISSOR_TEST);
119
-
120
- const scissorBox = gl.getParameter(gl.SCISSOR_BOX) as [
121
- number,
122
- number,
123
- number,
124
- number,
125
- ];
126
- this.scissorX = scissorBox[0];
127
- this.scissorY = scissorBox[1];
128
- this.scissorWidth = scissorBox[2];
129
- this.scissorHeight = scissorBox[3];
130
-
131
- this.blendEnabled = gl.isEnabled(gl.BLEND);
132
- this.blendSrcRgb = gl.getParameter(gl.BLEND_SRC_RGB) as number;
133
- this.blendDstRgb = gl.getParameter(gl.BLEND_DST_RGB) as number;
134
- this.blendSrcAlpha = gl.getParameter(gl.BLEND_SRC_ALPHA) as number;
135
- this.blendDstAlpha = gl.getParameter(gl.BLEND_DST_ALPHA) as number;
136
-
137
- this.boundArrayBuffer = gl.getParameter(
138
- gl.ARRAY_BUFFER_BINDING,
139
- ) as WebGLBuffer;
140
- this.boundElementArrayBuffer = gl.getParameter(
141
- gl.ELEMENT_ARRAY_BUFFER_BINDING,
142
- ) as WebGLBuffer;
143
-
144
- this.curProgram = gl.getParameter(
145
- gl.CURRENT_PROGRAM,
146
- ) as WebGLProgram | null;
107
+ // All texture units start unbound (TEXTURE_BINDING_2D === null).
108
+ this.texture2dUnits = new Array<WebGLTexture | null>(maxTextureUnits).fill(
109
+ null,
110
+ );
111
+
112
+ // SCISSOR_TEST disabled; SCISSOR_BOX defaults to the full drawing buffer.
113
+ this.scissorEnabled = false;
114
+ this.scissorX = 0;
115
+ this.scissorY = 0;
116
+ this.scissorWidth = gl.drawingBufferWidth;
117
+ this.scissorHeight = gl.drawingBufferHeight;
118
+
119
+ // BLEND disabled; blend func defaults to (ONE, ZERO) for both RGB and alpha.
120
+ this.blendEnabled = false;
121
+ this.blendSrcRgb = gl.ONE;
122
+ this.blendDstRgb = gl.ZERO;
123
+ this.blendSrcAlpha = gl.ONE;
124
+ this.blendDstAlpha = gl.ZERO;
125
+
126
+ // No buffers or program bound on a fresh context.
127
+ this.boundArrayBuffer = null;
128
+ this.boundElementArrayBuffer = null;
129
+ this.curProgram = null;
147
130
 
148
131
  this.canvas = gl.canvas;
149
132
 
@@ -175,9 +158,6 @@ export class WebGlContextWrapper {
175
158
  this.UNSIGNED_SHORT = gl.UNSIGNED_SHORT;
176
159
  this.ONE = gl.ONE;
177
160
  this.ONE_MINUS_SRC_ALPHA = gl.ONE_MINUS_SRC_ALPHA;
178
- this.MAX_VERTEX_TEXTURE_IMAGE_UNITS = gl.MAX_VERTEX_TEXTURE_IMAGE_UNITS;
179
- this.TRIANGLES = gl.TRIANGLES;
180
- this.UNSIGNED_SHORT = gl.UNSIGNED_SHORT;
181
161
  this.VERTEX_SHADER = gl.VERTEX_SHADER;
182
162
  this.FRAGMENT_SHADER = gl.FRAGMENT_SHADER;
183
163
  this.STATIC_DRAW = gl.STATIC_DRAW;
@@ -188,15 +168,6 @@ export class WebGlContextWrapper {
188
168
  this.INVALID_ENUM = gl.INVALID_ENUM;
189
169
  this.INVALID_OPERATION = gl.INVALID_OPERATION;
190
170
  }
191
- /**
192
- * Returns true if the WebGL context is WebGL2
193
- *
194
- * @returns
195
- */
196
- isWebGl2() {
197
- return isWebGl2(this.gl);
198
- }
199
-
200
171
  /**
201
172
  * ```
202
173
  * gl.activeTexture(textureUnit + gl.TEXTURE0);
@@ -1109,33 +1080,6 @@ export class WebGlContextWrapper {
1109
1080
  return this.gl.getError();
1110
1081
  }
1111
1082
 
1112
- /**
1113
- * ```
1114
- * gl.createVertexArray();
1115
- * ```
1116
- *
1117
- * @returns
1118
- */
1119
- createVertexArray() {
1120
- if (this.gl instanceof WebGL2RenderingContext) {
1121
- return this.gl.createVertexArray();
1122
- }
1123
- return undefined;
1124
- }
1125
-
1126
- /**
1127
- * ```
1128
- * gl.bindVertexArray(vertexArray);
1129
- * ```
1130
- *
1131
- * @param vertexArray
1132
- */
1133
- bindVertexArray(vertexArray: WebGLVertexArrayObject | null) {
1134
- if (this.gl instanceof WebGL2RenderingContext) {
1135
- this.gl.bindVertexArray(vertexArray);
1136
- }
1137
- }
1138
-
1139
1083
  /**
1140
1084
  * ```
1141
1085
  * gl.getAttribLocation(program, name);
@@ -1341,19 +1285,6 @@ export class WebGlContextWrapper {
1341
1285
  }
1342
1286
  }
1343
1287
 
1344
- /**
1345
- * ```
1346
- * gl.deleteVertexArray(vertexArray);
1347
- * ```
1348
- *
1349
- * @param vertexArray - The vertex array object to delete
1350
- */
1351
- deleteVertexArray(vertexArray: WebGLVertexArrayObject) {
1352
- if (this.isWebGl2()) {
1353
- (this.gl as WebGL2RenderingContext).deleteVertexArray(vertexArray);
1354
- }
1355
- }
1356
-
1357
1288
  /**
1358
1289
  * Check for WebGL errors and return error information
1359
1290
  * @param operation Description of the operation for error reporting
@@ -251,10 +251,6 @@ export function boundLargeThanBound(bound1: Bound, bound2: Bound) {
251
251
  );
252
252
  }
253
253
 
254
- export function pointInBound(x: number, y: number, bound: Bound) {
255
- return !(x < bound.x1 || x > bound.x2 || y < bound.y1 || y > bound.y2);
256
- }
257
-
258
254
  export function isBoundPositive(bound: Bound): boolean {
259
255
  return bound.x1 < bound.x2 && bound.y1 < bound.y2;
260
256
  }
@@ -19,6 +19,7 @@ function makeIdleStage(outOfMemory: boolean) {
19
19
  drawFrame: vi.fn(),
20
20
  flushFrameEvents: vi.fn(),
21
21
  shManager: { cleanup: vi.fn() },
22
+ cleanupTextRenderers: vi.fn(),
22
23
  eventBus: { emit: vi.fn() },
23
24
  txMemManager: {
24
25
  checkCleanup: vi.fn(() => false),
@@ -86,6 +86,7 @@ export class WebPlatform extends Platform {
86
86
  stage.txMemManager.handleOutOfMemory();
87
87
  }
88
88
  stage.shManager.cleanup();
89
+ stage.cleanupTextRenderers();
89
90
  stage.eventBus.emit('idle');
90
91
  isIdle = true;
91
92
  }
@@ -10,7 +10,6 @@ export interface CoreRendererOptions {
10
10
  stage: Stage;
11
11
  canvas: HTMLCanvasElement | OffscreenCanvas;
12
12
  contextSpy: ContextSpy | null;
13
- forceWebGL2: boolean;
14
13
  }
15
14
 
16
15
  export interface BufferInfo {
@@ -0,0 +1,53 @@
1
+ import { describe, it, expect, vi } from 'vitest';
2
+ import { CoreShaderNode } from './CoreShaderNode.js';
3
+ import type { Stage } from '../Stage.js';
4
+ import type { CoreShaderType } from './CoreShaderNode.js';
5
+
6
+ const makeNode = (mutate: ReturnType<typeof vi.fn>) => {
7
+ const stage = {
8
+ shManager: { mutateShaderValueUsage: mutate },
9
+ } as unknown as Stage;
10
+ return new CoreShaderNode(
11
+ 'test',
12
+ { time: undefined } as CoreShaderType,
13
+ stage,
14
+ );
15
+ };
16
+
17
+ describe('CoreShaderNode.detachNode', () => {
18
+ it('releases the held value-cache key so idle cleanup can evict it', () => {
19
+ const mutate = vi.fn();
20
+ const node = makeNode(mutate);
21
+ node.valueKey = 'color:1;node-width:10node-height:10';
22
+
23
+ node.detachNode();
24
+
25
+ expect(mutate).toHaveBeenCalledTimes(1);
26
+ expect(mutate).toHaveBeenCalledWith(
27
+ 'color:1;node-width:10node-height:10',
28
+ -1,
29
+ );
30
+ // Key is cleared so a double-detach can't decrement twice.
31
+ expect(node.valueKey).toBe('');
32
+ });
33
+
34
+ it('is a no-op when no value key is held', () => {
35
+ const mutate = vi.fn();
36
+ const node = makeNode(mutate);
37
+
38
+ node.detachNode();
39
+
40
+ expect(mutate).not.toHaveBeenCalled();
41
+ });
42
+
43
+ it('does not double-decrement on repeated detach', () => {
44
+ const mutate = vi.fn();
45
+ const node = makeNode(mutate);
46
+ node.valueKey = 'k';
47
+
48
+ node.detachNode();
49
+ node.detachNode();
50
+
51
+ expect(mutate).toHaveBeenCalledTimes(1);
52
+ });
53
+ });