@solidtv/renderer 1.5.0-0 → 1.5.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 (50) hide show
  1. package/dist/src/common/CommonTypes.d.ts +20 -0
  2. package/dist/src/core/Stage.d.ts +1 -0
  3. package/dist/src/core/Stage.js +13 -2
  4. package/dist/src/core/Stage.js.map +1 -1
  5. package/dist/src/core/TextureMemoryManager.d.ts +23 -0
  6. package/dist/src/core/TextureMemoryManager.js +37 -3
  7. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  8. package/dist/src/core/lib/WebGlContextWrapper.d.ts +1 -1
  9. package/dist/src/core/lib/WebGlContextWrapper.js +10 -3
  10. package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
  11. package/dist/src/core/lib/textureCompression.js +68 -4
  12. package/dist/src/core/lib/textureCompression.js.map +1 -1
  13. package/dist/src/core/renderers/CoreRenderer.d.ts +1 -0
  14. package/dist/src/core/renderers/CoreRenderer.js.map +1 -1
  15. package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +8 -0
  16. package/dist/src/core/renderers/webgl/WebGlRenderer.js +49 -13
  17. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  18. package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +2 -0
  19. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +18 -5
  20. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  21. package/dist/src/core/renderers/webgl/internal/RendererUtils.js +9 -2
  22. package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
  23. package/dist/src/core/text-rendering/SdfTextRenderer.js +16 -3
  24. package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
  25. package/dist/src/main-api/Renderer.d.ts +33 -1
  26. package/dist/src/main-api/Renderer.js +2 -0
  27. package/dist/src/main-api/Renderer.js.map +1 -1
  28. package/dist/src/utils.js +23 -7
  29. package/dist/src/utils.js.map +1 -1
  30. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +1 -1
  33. package/src/common/CommonTypes.ts +20 -0
  34. package/src/core/Stage.ts +16 -1
  35. package/src/core/TextureMemoryManager.test.ts +51 -2
  36. package/src/core/TextureMemoryManager.ts +42 -3
  37. package/src/core/lib/WebGlContextWrapper.test.ts +58 -0
  38. package/src/core/lib/WebGlContextWrapper.ts +13 -3
  39. package/src/core/lib/textureCompression.test.ts +158 -0
  40. package/src/core/lib/textureCompression.ts +78 -4
  41. package/src/core/renderers/CoreRenderer.ts +1 -0
  42. package/src/core/renderers/webgl/WebGlRenderer.ts +58 -15
  43. package/src/core/renderers/webgl/WebGlShaderProgram.ts +18 -5
  44. package/src/core/renderers/webgl/internal/RendererUtils.test.ts +73 -0
  45. package/src/core/renderers/webgl/internal/RendererUtils.ts +9 -2
  46. package/src/core/text-rendering/SdfTextRenderer.test.ts +11 -10
  47. package/src/core/text-rendering/SdfTextRenderer.ts +17 -3
  48. package/src/main-api/Renderer.ts +36 -1
  49. package/src/utils.test.ts +57 -0
  50. package/src/utils.ts +24 -8
@@ -11,10 +11,17 @@ export function createIndexBuffer(
11
11
  glw: WebGlContextWrapper,
12
12
  size: number,
13
13
  ): WebGLBuffer | null {
14
- const maxQuads = ~~(size / 80);
14
+ // 4 vertices per quad. Element indices are Uint16, so the largest vertex id
15
+ // we can address is 65535 — i.e. 16384 quads (16384 * 4 = 65536). Never claim
16
+ // more than that, regardless of the requested byte budget.
17
+ const maxQuads = Math.min(~~(size / 80), 16384);
15
18
  const indices = new Uint16Array(maxQuads * 6);
16
19
 
17
- for (let i = 0, j = 0; i < maxQuads; i += 6, j += 4) {
20
+ // i walks the index slot (6 per quad), j the vertex base (4 per quad). The
21
+ // bound must be maxQuads * 6 to fill every slot — stopping at maxQuads left
22
+ // ~5/6 of the buffer zeroed, collapsing every quad past ~maxQuads/6 into a
23
+ // degenerate triangle (the cause of tail geometry silently disappearing).
24
+ for (let i = 0, j = 0; i < maxQuads * 6; i += 6, j += 4) {
18
25
  indices[i] = j;
19
26
  indices[i + 1] = j + 1;
20
27
  indices[i + 2] = j + 2;
@@ -97,21 +97,22 @@ describe('SdfTextRenderer layout cache', () => {
97
97
  expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(1);
98
98
  });
99
99
 
100
- it('cleanup trims to the cap and evicts least-recently-used first', () => {
100
+ it('eagerly bounds the cache on insert (does not wait for idle cleanup)', () => {
101
+ // An animating scene never goes idle, so the cache must be bounded on insert
102
+ // rather than relying solely on idle `cleanup`. With cap 2, inserting a 3rd
103
+ // entry must immediately evict the least-recently-used (oldest) one — 'A'.
101
104
  initRenderer(2);
102
105
 
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();
106
+ render('A'); // {A}
107
+ render('B'); // {A, B}
108
+ render('C'); // insert -> over cap -> eagerly evict LRU 'A' -> {B, C}
110
109
 
111
110
  vi.clearAllMocks();
112
- render('B'); // evicted -> miss
113
- render('A'); // survived -> hit
111
+ // Probe survivors first (hits only re-order, they don't change membership),
112
+ // then the evicted entry last so its re-insert can't perturb the assertion.
114
113
  render('C'); // survived -> hit
114
+ render('B'); // survived -> hit
115
+ render('A'); // evicted -> miss (one layout regen)
115
116
 
116
117
  expect(SdfFontHandler.getFontData).toHaveBeenCalledTimes(1);
117
118
  });
@@ -24,9 +24,14 @@ 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.
27
+ // Upper bound on layoutCache entries. Overridden from stage options in `init`.
28
+ // Enforced both eagerly on insert (see `renderText`) and in bulk on idle (see
29
+ // `cleanup`). The eager bound matters because a continuously animating scene
30
+ // never goes idle, so idle-only eviction would let apps with ever-changing text
31
+ // (clocks, counters, score/fps readouts) grow the cache without limit until the
32
+ // page runs out of memory. The per-insert cost is a single Map delete on a
33
+ // cache miss (i.e. only when new text is laid out), so it does not compete with
34
+ // steady-state rendering.
30
35
  let maxLayoutCacheSize = 250;
31
36
 
32
37
  // Initialize the SDF text renderer
@@ -114,6 +119,15 @@ const renderText = (props: CoreTextNodeProps): TextRenderInfo => {
114
119
  layout = generateTextLayout(props, fontData);
115
120
  layoutCache.set(cacheKey, layout);
116
121
 
122
+ // Eagerly bound the cache. Idle `cleanup` alone is not enough: an animating
123
+ // scene never idles, so without this, ever-changing text grows the cache
124
+ // without limit. The Map is insertion-ordered and cache hits re-insert
125
+ // (delete + set) to the end, so the first key is the least-recently-used.
126
+ if (layoutCache.size > maxLayoutCacheSize) {
127
+ const oldest = layoutCache.keys().next().value as string;
128
+ layoutCache.delete(oldest);
129
+ }
130
+
117
131
  // For SDF renderer, ImageData is null since we render via WebGL
118
132
  return {
119
133
  remainingLines: 0,
@@ -30,7 +30,8 @@ import { Platform } from '../core/platforms/Platform.js';
30
30
  * @example
31
31
  * ```typescript
32
32
  * renderer.on('fpsUpdate', (_target, data) => {
33
- * console.log(`Current FPS: ${data.fps}`);
33
+ * console.log(`${data.fps} fps, ${data.renderOps} draw calls`);
34
+ * console.log('backend:', data.capabilities.renderMode, data.capabilities.webGlVersion);
34
35
  * if (data.contextSpyData) {
35
36
  * console.log('WebGL calls:', data.contextSpyData);
36
37
  * }
@@ -42,6 +43,21 @@ export interface RendererMainFpsUpdateEvent {
42
43
  fps: number;
43
44
  /** Context spy data (if enabled) - contains WebGL call statistics */
44
45
  contextSpyData?: unknown;
46
+ /**
47
+ * Draw calls (render operations) submitted for the most recent frame.
48
+ * `0` on the Canvas backend.
49
+ */
50
+ renderOps: number;
51
+ /** Quads rendered in the most recent frame. `0` on the Canvas backend. */
52
+ quads: number;
53
+ /**
54
+ * Active backend + device capabilities (constant for the renderer's
55
+ * lifetime). Included so each periodic sample is self-describing for
56
+ * telemetry — e.g. slice fps by `webGlVersion` or `vertexArrayObject`.
57
+ *
58
+ * @see {@link RendererCapabilities}
59
+ */
60
+ capabilities: RendererCapabilities;
45
61
  }
46
62
 
47
63
  /**
@@ -526,6 +542,23 @@ export type RendererMainSettings = RendererRuntimeSettings & {
526
542
  */
527
543
  forceWebGL2: boolean;
528
544
 
545
+ /**
546
+ * Disable Vertex Array Objects
547
+ *
548
+ * @remarks
549
+ * By default the WebGL renderer caches each shader program's attribute layout
550
+ * in a Vertex Array Object (native on WebGL2, or via the
551
+ * `OES_vertex_array_object` extension on WebGL1) and binds it with a single
552
+ * call per draw instead of re-pointing every attribute. Set this to `true` to
553
+ * force the per-draw attribute-binding path instead.
554
+ *
555
+ * This is primarily a diagnostic/benchmarking switch — it lets you A/B the VAO
556
+ * optimization on a target device. It has no effect on the Canvas renderer.
557
+ *
558
+ * @defaultValue `false`
559
+ */
560
+ disableVertexArrayObject: boolean;
561
+
529
562
  /**
530
563
  * Canvas object to use for rendering
531
564
  *
@@ -701,6 +734,7 @@ export class RendererMain extends EventEmitter {
701
734
  settings.numImageWorkers !== undefined ? settings.numImageWorkers : 2,
702
735
  enableContextSpy: settings.enableContextSpy ?? false,
703
736
  forceWebGL2: settings.forceWebGL2 ?? false,
737
+ disableVertexArrayObject: settings.disableVertexArrayObject ?? false,
704
738
  inspector: settings.inspector ?? false,
705
739
  inspectorOptions: settings.inspectorOptions ?? {},
706
740
  renderEngine: settings.renderEngine,
@@ -763,6 +797,7 @@ export class RendererMain extends EventEmitter {
763
797
  devicePhysicalPixelRatio,
764
798
  enableContextSpy: settings.enableContextSpy!,
765
799
  forceWebGL2: settings.forceWebGL2!,
800
+ disableVertexArrayObject: settings.disableVertexArrayObject!,
766
801
  fpsUpdateInterval: settings.fpsUpdateInterval!,
767
802
  enableClear: settings.enableClear!,
768
803
  numImageWorkers: settings.numImageWorkers!,
@@ -0,0 +1,57 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { createWebGLContext } from './utils.js';
3
+ import { ContextSpy } from './core/lib/ContextSpy.js';
4
+
5
+ describe('createWebGLContext context spy', () => {
6
+ // Build a minimal fake GL context whose getExtension returns a fake
7
+ // OES_vertex_array_object so we can assert the spy counts calls on it.
8
+ function makeContext(spy: ContextSpy) {
9
+ let vaoCounter = 0;
10
+ const ext = {
11
+ createVertexArrayOES: () => ({ id: ++vaoCounter }),
12
+ bindVertexArrayOES: (_vao: unknown) => undefined,
13
+ deleteVertexArrayOES: (_vao: unknown) => undefined,
14
+ };
15
+ const gl = {
16
+ getExtension: (name: string) =>
17
+ name === 'OES_vertex_array_object' ? ext : null,
18
+ viewport: () => undefined,
19
+ };
20
+ const canvas = {
21
+ getContext: () => gl,
22
+ } as unknown as HTMLCanvasElement;
23
+ return createWebGLContext(canvas, false, spy);
24
+ }
25
+
26
+ it('counts methods on extension objects returned by getExtension', () => {
27
+ const spy = new ContextSpy();
28
+ const gl = makeContext(spy);
29
+
30
+ // WebGL1 VAO calls route through the extension object.
31
+ const ext = gl.getExtension('OES_vertex_array_object') as unknown as {
32
+ createVertexArrayOES: () => unknown;
33
+ bindVertexArrayOES: (vao: unknown) => void;
34
+ };
35
+ const vao = ext.createVertexArrayOES();
36
+ ext.bindVertexArrayOES(vao);
37
+ ext.bindVertexArrayOES(null);
38
+
39
+ const data = spy.getData();
40
+ expect(data['getExtension']).toBeGreaterThan(0);
41
+ expect(data['createVertexArrayOES']).toBe(1);
42
+ expect(data['bindVertexArrayOES']).toBe(2);
43
+ });
44
+
45
+ it('passes through a null extension without throwing', () => {
46
+ const spy = new ContextSpy();
47
+ const gl = makeContext(spy);
48
+ expect(gl.getExtension('NOPE' as 'OES_vertex_array_object')).toBe(null);
49
+ });
50
+
51
+ it('still counts direct context calls', () => {
52
+ const spy = new ContextSpy();
53
+ const gl = makeContext(spy);
54
+ gl.viewport(0, 0, 1, 1);
55
+ expect(spy.getData()['viewport']).toBe(1);
56
+ });
57
+ });
package/src/utils.ts CHANGED
@@ -29,17 +29,33 @@ export function createWebGLContext(
29
29
  throw new Error('Unable to create WebGL context');
30
30
  }
31
31
  if (contextSpy) {
32
- // Proxy the GL context to log all GL calls
33
- return new Proxy(gl, {
32
+ // Proxy the GL context and any extension object it hands out — so every
33
+ // call is counted. The renderer routes WebGL1 Vertex Array Object calls
34
+ // through the OES_vertex_array_object extension object; without wrapping
35
+ // that object its methods (bindVertexArrayOES / createVertexArrayOES /
36
+ // deleteVertexArrayOES) would bypass the spy entirely. On WebGL2 the same
37
+ // calls live on the context itself and are already captured.
38
+ const handler: ProxyHandler<object> = {
34
39
  get(target, prop) {
35
- const value = target[prop as never] as unknown;
36
- if (typeof value === 'function') {
37
- contextSpy.increment(String(prop));
38
- return value.bind(target);
40
+ const value = (target as Record<string | symbol, unknown>)[prop];
41
+ if (typeof value !== 'function') {
42
+ return value;
39
43
  }
40
- return value;
44
+ contextSpy.increment(String(prop));
45
+ const fn = value as (...args: unknown[]) => unknown;
46
+ if (prop === 'getExtension') {
47
+ return (...args: unknown[]): unknown => {
48
+ const ext = fn.apply(target, args);
49
+ if (ext !== null && typeof ext === 'object') {
50
+ return new Proxy(ext, handler);
51
+ }
52
+ return ext;
53
+ };
54
+ }
55
+ return fn.bind(target);
41
56
  },
42
- });
57
+ };
58
+ return new Proxy(gl as object, handler) as unknown as WebGLRenderingContext;
43
59
  }
44
60
 
45
61
  return gl;