@solidtv/renderer 1.5.0 → 1.5.2

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 (42) hide show
  1. package/dist/src/common/EventEmitter.d.ts +8 -0
  2. package/dist/src/common/EventEmitter.js +20 -0
  3. package/dist/src/common/EventEmitter.js.map +1 -1
  4. package/dist/src/core/Autosizer.js +3 -1
  5. package/dist/src/core/Autosizer.js.map +1 -1
  6. package/dist/src/core/CoreNode.d.ts +39 -0
  7. package/dist/src/core/CoreNode.js +115 -25
  8. package/dist/src/core/CoreNode.js.map +1 -1
  9. package/dist/src/core/CoreTextNode.js +11 -4
  10. package/dist/src/core/CoreTextNode.js.map +1 -1
  11. package/dist/src/core/Stage.d.ts +6 -0
  12. package/dist/src/core/Stage.js +9 -0
  13. package/dist/src/core/Stage.js.map +1 -1
  14. package/dist/src/core/TextureMemoryManager.d.ts +39 -2
  15. package/dist/src/core/TextureMemoryManager.js +53 -1
  16. package/dist/src/core/TextureMemoryManager.js.map +1 -1
  17. package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -1
  18. package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
  19. package/dist/src/core/renderers/webgl/WebGlRenderer.js +5 -1
  20. package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
  21. package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +28 -0
  22. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +70 -10
  23. package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
  24. package/dist/src/main-api/Renderer.d.ts +23 -0
  25. package/dist/src/main-api/Renderer.js +2 -0
  26. package/dist/src/main-api/Renderer.js.map +1 -1
  27. package/dist/tsconfig.dist.tsbuildinfo +1 -1
  28. package/package.json +1 -1
  29. package/src/common/EventEmitter.ts +21 -0
  30. package/src/core/Autosizer.ts +3 -1
  31. package/src/core/CoreNode.test.ts +425 -0
  32. package/src/core/CoreNode.ts +146 -26
  33. package/src/core/CoreTextNode.test.ts +62 -0
  34. package/src/core/CoreTextNode.ts +13 -4
  35. package/src/core/Stage.ts +9 -0
  36. package/src/core/TextureMemoryManager.test.ts +272 -5
  37. package/src/core/TextureMemoryManager.ts +59 -2
  38. package/src/core/renderers/canvas/CanvasRenderer.ts +7 -1
  39. package/src/core/renderers/webgl/WebGlRenderer.ts +6 -1
  40. package/src/core/renderers/webgl/WebGlShaderProgram.ts +78 -15
  41. package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +233 -0
  42. package/src/main-api/Renderer.ts +26 -0
@@ -0,0 +1,233 @@
1
+ import { describe, expect, it, vi } from 'vitest';
2
+ import { WebGlShaderProgram } from './WebGlShaderProgram.js';
3
+
4
+ /**
5
+ * Tests for the redundant-uniform-upload skip in bindRenderOp.
6
+ *
7
+ * The program instance is created without running the constructor (which
8
+ * compiles GLSL against a live GL context); the fields bindRenderOp touches
9
+ * are populated manually, and bindTextures/bindBufferCollection are stubbed.
10
+ */
11
+
12
+ type FakeGlw = {
13
+ uniform1f: ReturnType<typeof vi.fn>;
14
+ uniform2f: ReturnType<typeof vi.fn>;
15
+ uniform4f: ReturnType<typeof vi.fn>;
16
+ canvas: { width: number; height: number };
17
+ };
18
+
19
+ const makeProgram = (): { program: WebGlShaderProgram; glw: FakeGlw } => {
20
+ const glw: FakeGlw = {
21
+ uniform1f: vi.fn(),
22
+ uniform2f: vi.fn(),
23
+ uniform4f: vi.fn(),
24
+ canvas: { width: 1920, height: 1080 },
25
+ };
26
+
27
+ const program = Object.create(
28
+ WebGlShaderProgram.prototype,
29
+ ) as WebGlShaderProgram;
30
+ const p = program as unknown as Record<string, unknown>;
31
+ p['glw'] = glw;
32
+ p['useSystemAlpha'] = true;
33
+ p['useSystemDimensions'] = true;
34
+ p['useTimeValue'] = false;
35
+ p['lastBoundUniforms'] = null;
36
+ p['lastPixelRatio'] = -1;
37
+ p['lastResolutionW'] = -1;
38
+ p['lastResolutionH'] = -1;
39
+ p['lastAlpha'] = -1;
40
+ p['lastDimensionsW'] = -1;
41
+ p['lastDimensionsH'] = -1;
42
+ p['lastTime'] = -1;
43
+ // Stub the buffer/texture binding done before the uniform pass
44
+ p['bindTextures'] = vi.fn();
45
+ p['bindBufferCollection'] = vi.fn();
46
+ return { program, glw };
47
+ };
48
+
49
+ const makeUniforms = () => ({
50
+ single: {
51
+ u_borderGap: { method: 'uniform1f', value: 0 },
52
+ },
53
+ vec2: {},
54
+ vec3: {},
55
+ vec4: {
56
+ u_radius: { method: 'uniform4f', value: [16, 16, 16, 16] },
57
+ },
58
+ });
59
+
60
+ const makeOp = (
61
+ uniforms: ReturnType<typeof makeUniforms>,
62
+ overrides?: Record<string, unknown>,
63
+ ) => ({
64
+ isCoreNode: true,
65
+ rtt: false,
66
+ parentHasRenderTexture: false,
67
+ parentFramebufferDimensions: null,
68
+ framebufferDimensions: null,
69
+ stage: { pixelRatio: 2 },
70
+ time: 0,
71
+ worldAlpha: 1,
72
+ w: 200,
73
+ h: 100,
74
+ renderOpTextures: [],
75
+ quadBufferCollection: {},
76
+ shader: { props: {}, uniforms },
77
+ ...overrides,
78
+ });
79
+
80
+ const totalCalls = (glw: FakeGlw): number =>
81
+ glw.uniform1f.mock.calls.length +
82
+ glw.uniform2f.mock.calls.length +
83
+ glw.uniform4f.mock.calls.length;
84
+
85
+ describe('bindRenderOp uniform dedup', () => {
86
+ it('should upload everything on the first bind', () => {
87
+ const { program, glw } = makeProgram();
88
+ const uniforms = makeUniforms();
89
+
90
+ program.bindRenderOp(makeOp(uniforms) as never);
91
+
92
+ // u_pixelRatio, u_alpha, u_borderGap (1f) + u_resolution, u_dimensions (2f) + u_radius (4f)
93
+ expect(glw.uniform1f.mock.calls.length).toBe(3);
94
+ expect(glw.uniform2f.mock.calls.length).toBe(2);
95
+ expect(glw.uniform4f.mock.calls.length).toBe(1);
96
+ });
97
+
98
+ it('should issue zero uniform calls on an identical re-bind', () => {
99
+ const { program, glw } = makeProgram();
100
+ const uniforms = makeUniforms();
101
+
102
+ program.bindRenderOp(makeOp(uniforms) as never);
103
+ glw.uniform1f.mockClear();
104
+ glw.uniform2f.mockClear();
105
+ glw.uniform4f.mockClear();
106
+
107
+ program.bindRenderOp(makeOp(uniforms) as never);
108
+
109
+ expect(totalCalls(glw)).toBe(0);
110
+ });
111
+
112
+ it('should skip across interleaved ops (shared collection, different op objects)', () => {
113
+ const { program, glw } = makeProgram();
114
+ const uniforms = makeUniforms();
115
+
116
+ // Two distinct ops (different cards) sharing the value-cached collection
117
+ program.bindRenderOp(makeOp(uniforms) as never);
118
+ glw.uniform1f.mockClear();
119
+ glw.uniform2f.mockClear();
120
+ glw.uniform4f.mockClear();
121
+
122
+ // GL uniform state persists on the program across useProgram switches,
123
+ // so an op of another program in between changes nothing here.
124
+ program.bindRenderOp(makeOp(uniforms) as never);
125
+
126
+ expect(totalCalls(glw)).toBe(0);
127
+ });
128
+
129
+ it('should re-upload only the changed system uniform', () => {
130
+ const { program, glw } = makeProgram();
131
+ const uniforms = makeUniforms();
132
+
133
+ program.bindRenderOp(makeOp(uniforms) as never);
134
+ glw.uniform1f.mockClear();
135
+ glw.uniform2f.mockClear();
136
+ glw.uniform4f.mockClear();
137
+
138
+ program.bindRenderOp(makeOp(uniforms, { worldAlpha: 0.5 }) as never);
139
+
140
+ expect(glw.uniform1f.mock.calls.length).toBe(1);
141
+ expect(glw.uniform1f.mock.calls[0]![0]).toBe('u_alpha');
142
+ expect(glw.uniform1f.mock.calls[0]![1]).toBe(0.5);
143
+ expect(glw.uniform2f.mock.calls.length).toBe(0);
144
+ expect(glw.uniform4f.mock.calls.length).toBe(0);
145
+ });
146
+
147
+ it('should re-upload dimensions when the op size differs', () => {
148
+ const { program, glw } = makeProgram();
149
+ const uniforms = makeUniforms();
150
+
151
+ program.bindRenderOp(makeOp(uniforms) as never);
152
+ glw.uniform2f.mockClear();
153
+
154
+ program.bindRenderOp(makeOp(uniforms, { w: 300, h: 150 }) as never);
155
+
156
+ expect(glw.uniform2f.mock.calls.length).toBe(1);
157
+ expect(glw.uniform2f.mock.calls[0]![0]).toBe('u_dimensions');
158
+ });
159
+
160
+ it('should re-run the collection pass for a different collection object', () => {
161
+ const { program, glw } = makeProgram();
162
+ const uniformsA = makeUniforms();
163
+ const uniformsB = makeUniforms(); // identical values, distinct identity
164
+
165
+ program.bindRenderOp(makeOp(uniformsA) as never);
166
+ glw.uniform1f.mockClear();
167
+ glw.uniform4f.mockClear();
168
+
169
+ program.bindRenderOp(makeOp(uniformsB) as never);
170
+
171
+ // Conservative direction: new object => redundant upload, never a skip
172
+ expect(glw.uniform1f.mock.calls.length).toBe(1); // u_borderGap
173
+ expect(glw.uniform4f.mock.calls.length).toBe(1); // u_radius
174
+ });
175
+
176
+ it('should re-upload pixel ratio and resolution across an RTT flip', () => {
177
+ const { program, glw } = makeProgram();
178
+ const uniforms = makeUniforms();
179
+
180
+ program.bindRenderOp(makeOp(uniforms) as never);
181
+ glw.uniform1f.mockClear();
182
+ glw.uniform2f.mockClear();
183
+
184
+ // RTT op: pixelRatio forced to 1, resolution = framebuffer dimensions
185
+ program.bindRenderOp(
186
+ makeOp(uniforms, {
187
+ isCoreNode: false,
188
+ parentHasRenderTexture: true,
189
+ framebufferDimensions: { w: 512, h: 256 },
190
+ }) as never,
191
+ );
192
+
193
+ const calls1f = glw.uniform1f.mock.calls;
194
+ const calls2f = glw.uniform2f.mock.calls;
195
+ expect(calls1f.some((c) => c[0] === 'u_pixelRatio' && c[1] === 1)).toBe(
196
+ true,
197
+ );
198
+ expect(calls2f.some((c) => c[0] === 'u_resolution' && c[1] === 512)).toBe(
199
+ true,
200
+ );
201
+
202
+ glw.uniform1f.mockClear();
203
+ glw.uniform2f.mockClear();
204
+
205
+ // Back to screen: values flip back and must re-upload
206
+ program.bindRenderOp(makeOp(uniforms) as never);
207
+ expect(
208
+ glw.uniform1f.mock.calls.some(
209
+ (c) => c[0] === 'u_pixelRatio' && c[1] === 2,
210
+ ),
211
+ ).toBe(true);
212
+ expect(
213
+ glw.uniform2f.mock.calls.some(
214
+ (c) => c[0] === 'u_resolution' && c[1] === 1920,
215
+ ),
216
+ ).toBe(true);
217
+ });
218
+
219
+ it('should skip the collection pass entirely for propless shaders', () => {
220
+ const { program, glw } = makeProgram();
221
+
222
+ program.bindRenderOp(
223
+ makeOp(makeUniforms(), {
224
+ shader: { props: undefined, uniforms: makeUniforms() },
225
+ }) as never,
226
+ );
227
+
228
+ // Only system uniforms: pixelRatio + alpha (1f), resolution + dimensions (2f)
229
+ expect(glw.uniform1f.mock.calls.length).toBe(2);
230
+ expect(glw.uniform2f.mock.calls.length).toBe(2);
231
+ expect(glw.uniform4f.mock.calls.length).toBe(0);
232
+ });
233
+ });
@@ -296,6 +296,30 @@ export interface RendererRuntimeSettings {
296
296
  */
297
297
  boundsMargin: number | [number, number, number, number];
298
298
 
299
+ /**
300
+ * Only submit quads for Nodes that intersect the visible viewport.
301
+ *
302
+ * @remarks
303
+ * Nodes inside the bounds margin (`boundsMargin`) but outside the visible
304
+ * viewport still update and still load their textures (the margin remains
305
+ * the preload runway), but they stay out of the render list until they
306
+ * actually intersect the viewport — no quad writes, texture binds, or draw
307
+ * calls for content the GPU would clip anyway.
308
+ *
309
+ * Set to `false` to restore the previous behavior, where margin-ring Nodes
310
+ * are fully rendered every frame and clipped by the GPU. That keeps
311
+ * render-list membership stable ahead of scrolling at the cost of
312
+ * per-frame CPU for the off-screen ring.
313
+ *
314
+ * Trade-offs when enabled: the `renderable` event and autosize patching
315
+ * fire at viewport entry instead of margin entry, render-list rebuilds
316
+ * move to the visible edge (same frequency, different timing), and
317
+ * margin-ring content inside RTT subtrees is skipped.
318
+ *
319
+ * @defaultValue `true`
320
+ */
321
+ renderOnlyInViewport: boolean;
322
+
299
323
  /**
300
324
  * Factor to convert app-authored logical coorindates to device logical coordinates
301
325
  *
@@ -722,6 +746,7 @@ export class RendererMain extends EventEmitter {
722
746
  appHeight: settings.appHeight || 1080,
723
747
  textureMemory: resolvedTxSettings,
724
748
  boundsMargin: settings.boundsMargin || 0,
749
+ renderOnlyInViewport: settings.renderOnlyInViewport ?? true,
725
750
  deviceLogicalPixelRatio: settings.deviceLogicalPixelRatio || 1,
726
751
  devicePhysicalPixelRatio:
727
752
  settings.devicePhysicalPixelRatio || this.windowDevicePixelRatio() || 1,
@@ -791,6 +816,7 @@ export class RendererMain extends EventEmitter {
791
816
  appWidth,
792
817
  appHeight,
793
818
  boundsMargin: settings.boundsMargin!,
819
+ renderOnlyInViewport: settings.renderOnlyInViewport!,
794
820
  clearColor: settings.clearColor!,
795
821
  canvas: this.canvas,
796
822
  deviceLogicalPixelRatio,