@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.
- package/dist/src/common/EventEmitter.d.ts +8 -0
- package/dist/src/common/EventEmitter.js +20 -0
- package/dist/src/common/EventEmitter.js.map +1 -1
- package/dist/src/core/Autosizer.js +3 -1
- package/dist/src/core/Autosizer.js.map +1 -1
- package/dist/src/core/CoreNode.d.ts +39 -0
- package/dist/src/core/CoreNode.js +115 -25
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +11 -4
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/Stage.d.ts +6 -0
- package/dist/src/core/Stage.js +9 -0
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.d.ts +39 -2
- package/dist/src/core/TextureMemoryManager.js +53 -1
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +5 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +5 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.d.ts +28 -0
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +70 -10
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +23 -0
- package/dist/src/main-api/Renderer.js +2 -0
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/common/EventEmitter.ts +21 -0
- package/src/core/Autosizer.ts +3 -1
- package/src/core/CoreNode.test.ts +425 -0
- package/src/core/CoreNode.ts +146 -26
- package/src/core/CoreTextNode.test.ts +62 -0
- package/src/core/CoreTextNode.ts +13 -4
- package/src/core/Stage.ts +9 -0
- package/src/core/TextureMemoryManager.test.ts +272 -5
- package/src/core/TextureMemoryManager.ts +59 -2
- package/src/core/renderers/canvas/CanvasRenderer.ts +7 -1
- package/src/core/renderers/webgl/WebGlRenderer.ts +6 -1
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +78 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +233 -0
- 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
|
+
});
|
package/src/main-api/Renderer.ts
CHANGED
|
@@ -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,
|