@solidtv/renderer 1.5.0 → 1.5.1
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 +105 -25
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/CoreTextNode.js +4 -2
- package/dist/src/core/CoreTextNode.js.map +1 -1
- package/dist/src/core/Stage.js +2 -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/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 +283 -0
- package/src/core/CoreNode.ts +131 -26
- package/src/core/CoreTextNode.test.ts +1 -0
- package/src/core/CoreTextNode.ts +6 -2
- package/src/core/Stage.ts +2 -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
|
@@ -205,11 +205,20 @@ export class TextureMemoryManager {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
/**
|
|
208
|
-
* Destroy a texture and remove it from the memory
|
|
208
|
+
* Destroy a texture, evict its cache entry, and remove it from the memory
|
|
209
|
+
* manager.
|
|
210
|
+
*
|
|
211
|
+
* @remarks
|
|
212
|
+
* Private on purpose: `destroy()` calls `removeAllListeners()`, so running
|
|
213
|
+
* this on a texture that still has subscribers severs a live `CoreNode`'s
|
|
214
|
+
* (or `SubTexture`'s) connection — the blank-poster bug. The only safe
|
|
215
|
+
* entry point is {@link evictOrphanedTextures}, which proves the texture
|
|
216
|
+
* is unreferenced first. For memory pressure use {@link freeTexture},
|
|
217
|
+
* which is reversible.
|
|
209
218
|
*
|
|
210
219
|
* @param texture - The texture to destroy
|
|
211
220
|
*/
|
|
212
|
-
destroyTexture(texture: Texture) {
|
|
221
|
+
private destroyTexture(texture: Texture) {
|
|
213
222
|
if (this.debugLogging === true) {
|
|
214
223
|
console.log(
|
|
215
224
|
`[TextureMemoryManager] Destroying texture. State: ${texture.state}`,
|
|
@@ -278,6 +287,8 @@ export class TextureMemoryManager {
|
|
|
278
287
|
}
|
|
279
288
|
}
|
|
280
289
|
|
|
290
|
+
this.evictOrphanedTextures();
|
|
291
|
+
|
|
281
292
|
if (this.memUsed >= this.criticalThreshold) {
|
|
282
293
|
this.stage.queueFrameEvent('criticalCleanupFailed', {
|
|
283
294
|
memUsed: this.memUsed,
|
|
@@ -300,6 +311,52 @@ export class TextureMemoryManager {
|
|
|
300
311
|
}
|
|
301
312
|
}
|
|
302
313
|
|
|
314
|
+
/**
|
|
315
|
+
* Destroy-and-evict freed textures that nothing references anymore.
|
|
316
|
+
*
|
|
317
|
+
* @remarks
|
|
318
|
+
* {@link freeTexture} intentionally keeps the texture's cache entry and
|
|
319
|
+
* listeners so a live `CoreNode` can reload it in place. But once the last
|
|
320
|
+
* referencing node is destroyed (`unloadTexture` removes its listeners and
|
|
321
|
+
* owner), the freed texture's `keyCache` entry can never be displayed again
|
|
322
|
+
* — without eviction the cache grows unboundedly in apps cycling many
|
|
323
|
+
* unique textures.
|
|
324
|
+
*
|
|
325
|
+
* A texture is an orphan only when it has zero `renderableOwners` AND zero
|
|
326
|
+
* event listeners: every live referencer (`CoreNode.loadTextureTask`,
|
|
327
|
+
* `SubTexture`) subscribes via `on()`. A texture with any listener must
|
|
328
|
+
* NEVER be destroyed here — `destroy()` calls `removeAllListeners()`, which
|
|
329
|
+
* would sever the node's subscription and reintroduce the blank-poster bug
|
|
330
|
+
* that {@link freeTexture} exists to prevent.
|
|
331
|
+
*
|
|
332
|
+
* `'initial'` and `'failed'` orphans leak the same way (created or failed,
|
|
333
|
+
* then the node was destroyed before the texture ever loaded; nothing will
|
|
334
|
+
* ever retry a `'failed'` texture without a listener). They are evicted only
|
|
335
|
+
* after the startup grace period: a node created this same frame subscribes
|
|
336
|
+
* in a queued microtask, so a fresh texture can look orphaned during a
|
|
337
|
+
* same-frame cleanup. In-flight states (`'fetching'`/`'loading'`/`'fetched'`)
|
|
338
|
+
* are never evicted; `'loaded'` orphans go through the pressure-driven free
|
|
339
|
+
* loop first and are swept here as `'freed'` on a later pass.
|
|
340
|
+
*/
|
|
341
|
+
private evictOrphanedTextures(): void {
|
|
342
|
+
const keyCache = this.stage.txManager.keyCache;
|
|
343
|
+
for (const texture of keyCache.values()) {
|
|
344
|
+
const state = texture.state;
|
|
345
|
+
const evictable =
|
|
346
|
+
state === 'freed' ||
|
|
347
|
+
((state === 'initial' || state === 'failed') &&
|
|
348
|
+
texture.isWithinStartupGracePeriod() === false);
|
|
349
|
+
if (
|
|
350
|
+
evictable === true &&
|
|
351
|
+
texture.preventCleanup === false &&
|
|
352
|
+
texture.renderableOwners.length === 0 &&
|
|
353
|
+
texture.hasListeners() === false
|
|
354
|
+
) {
|
|
355
|
+
this.destroyTexture(texture);
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
|
|
303
360
|
/**
|
|
304
361
|
* Get the current texture memory usage information
|
|
305
362
|
*
|
|
@@ -52,7 +52,13 @@ export class CanvasRenderer extends CoreRenderer {
|
|
|
52
52
|
const ctx = this.context;
|
|
53
53
|
const { tx, ty, ta, tb, tc, td } = node.globalTransform!;
|
|
54
54
|
const clippingRect = node.clippingRect;
|
|
55
|
-
|
|
55
|
+
// While a placeholder is showing, render the color-rect path (the default
|
|
56
|
+
// ColorTexture) tinted by the node's premultiplied placeholder color.
|
|
57
|
+
let texture = (
|
|
58
|
+
node.placeholderActive === true
|
|
59
|
+
? this.stage.defaultTexture
|
|
60
|
+
: node.props.texture || this.stage.defaultTexture
|
|
61
|
+
) as Texture;
|
|
56
62
|
// The Canvas2D renderer only supports image textures, no textures are used for color blocks
|
|
57
63
|
if (texture !== null) {
|
|
58
64
|
const textureType = texture.type;
|
|
@@ -471,7 +471,12 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
471
471
|
}
|
|
472
472
|
|
|
473
473
|
const props = node.props;
|
|
474
|
-
|
|
474
|
+
// While a placeholder is showing, the quad samples the shared 1x1 white
|
|
475
|
+
// texture tinted by the node's premultiplied placeholder color.
|
|
476
|
+
let tx =
|
|
477
|
+
node.placeholderActive === true
|
|
478
|
+
? this.stage.defaultTexture!
|
|
479
|
+
: props.texture || this.stage.defaultTexture!;
|
|
475
480
|
|
|
476
481
|
if (tx.type === TextureType.subTexture) {
|
|
477
482
|
tx = (tx as SubTexture).parentTexture;
|
|
@@ -30,6 +30,35 @@ export class WebGlShaderProgram implements CoreShaderProgram {
|
|
|
30
30
|
public isDestroyed = false;
|
|
31
31
|
supportsIndexedTextures = false;
|
|
32
32
|
|
|
33
|
+
/**
|
|
34
|
+
* Shadow copies of this program's GL uniform state, used by
|
|
35
|
+
* {@link bindRenderOp} to skip redundant gl.uniform* calls.
|
|
36
|
+
*
|
|
37
|
+
* @remarks
|
|
38
|
+
* GL uniform values are state on the program object and persist across
|
|
39
|
+
* useProgram switches, and bindRenderOp is the only writer of these
|
|
40
|
+
* uniforms, so the shadows always mirror GL truth — even when ops using
|
|
41
|
+
* other programs are interleaved between two ops of this program.
|
|
42
|
+
*
|
|
43
|
+
* `lastBoundUniforms` tracks the shader node's uniform collection by
|
|
44
|
+
* identity: collections are created once, filled once, and shared by
|
|
45
|
+
* reference across shader nodes with the same value key (see
|
|
46
|
+
* WebGlShaderNode.update), so reference equality implies value equality.
|
|
47
|
+
* The failure direction is safe — a new collection holding identical
|
|
48
|
+
* values just causes a redundant upload, never a wrong skip.
|
|
49
|
+
*
|
|
50
|
+
* Sentinels: -1 never collides with real pixel ratios, resolutions,
|
|
51
|
+
* alphas, dimensions, or time values (all >= 0).
|
|
52
|
+
*/
|
|
53
|
+
protected lastBoundUniforms: unknown = null;
|
|
54
|
+
protected lastPixelRatio = -1;
|
|
55
|
+
protected lastResolutionW = -1;
|
|
56
|
+
protected lastResolutionH = -1;
|
|
57
|
+
protected lastAlpha = -1;
|
|
58
|
+
protected lastDimensionsW = -1;
|
|
59
|
+
protected lastDimensionsH = -1;
|
|
60
|
+
protected lastTime = -1;
|
|
61
|
+
|
|
33
62
|
/**
|
|
34
63
|
* Cached Vertex Array Objects, keyed by the buffer collection they capture.
|
|
35
64
|
*
|
|
@@ -198,44 +227,78 @@ export class WebGlShaderProgram implements CoreShaderProgram {
|
|
|
198
227
|
return;
|
|
199
228
|
}
|
|
200
229
|
|
|
201
|
-
//
|
|
202
|
-
//
|
|
230
|
+
// Resolve target pixel ratio / resolution, then compare-and-set against
|
|
231
|
+
// the program's shadow state. Each gl.uniform* call below crosses into
|
|
232
|
+
// the GPU-process command buffer, so skipping value-identical re-uploads
|
|
233
|
+
// is a real per-op CPU saving on embedded targets.
|
|
234
|
+
let pixelRatio: number;
|
|
235
|
+
let resolutionW: number;
|
|
236
|
+
let resolutionH: number;
|
|
203
237
|
if (USE_RTT && parentHasRenderTexture === true && framebufferDimensions) {
|
|
204
|
-
const { w, h } = framebufferDimensions;
|
|
205
238
|
// Force pixel ratio to 1.0 for render textures since they are always 1:1
|
|
206
239
|
// the final render texture will be rendered to the screen with the correct pixel ratio
|
|
207
|
-
|
|
208
|
-
|
|
240
|
+
pixelRatio = 1.0;
|
|
209
241
|
// Set resolution to the framebuffer dimensions
|
|
210
|
-
|
|
242
|
+
resolutionW = framebufferDimensions.w;
|
|
243
|
+
resolutionH = framebufferDimensions.h;
|
|
211
244
|
} else {
|
|
212
|
-
|
|
245
|
+
pixelRatio = renderOp.stage.pixelRatio;
|
|
246
|
+
resolutionW = this.glw.canvas.width;
|
|
247
|
+
resolutionH = this.glw.canvas.height;
|
|
248
|
+
}
|
|
213
249
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
this.glw.canvas.height,
|
|
218
|
-
);
|
|
250
|
+
if (pixelRatio !== this.lastPixelRatio) {
|
|
251
|
+
this.glw.uniform1f('u_pixelRatio', pixelRatio);
|
|
252
|
+
this.lastPixelRatio = pixelRatio;
|
|
219
253
|
}
|
|
220
254
|
|
|
221
|
-
if (
|
|
255
|
+
if (
|
|
256
|
+
resolutionW !== this.lastResolutionW ||
|
|
257
|
+
resolutionH !== this.lastResolutionH
|
|
258
|
+
) {
|
|
259
|
+
this.glw.uniform2f('u_resolution', resolutionW, resolutionH);
|
|
260
|
+
this.lastResolutionW = resolutionW;
|
|
261
|
+
this.lastResolutionH = resolutionH;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
if (this.useTimeValue === true && renderOp.time !== this.lastTime) {
|
|
222
265
|
this.glw.uniform1f('u_time', renderOp.time);
|
|
266
|
+
this.lastTime = renderOp.time;
|
|
223
267
|
}
|
|
224
268
|
|
|
225
|
-
if (
|
|
269
|
+
if (
|
|
270
|
+
this.useSystemAlpha === true &&
|
|
271
|
+
renderOp.worldAlpha !== this.lastAlpha
|
|
272
|
+
) {
|
|
226
273
|
this.glw.uniform1f('u_alpha', renderOp.worldAlpha);
|
|
274
|
+
this.lastAlpha = renderOp.worldAlpha;
|
|
227
275
|
}
|
|
228
276
|
|
|
229
|
-
if (
|
|
277
|
+
if (
|
|
278
|
+
this.useSystemDimensions === true &&
|
|
279
|
+
(renderOp.w !== this.lastDimensionsW ||
|
|
280
|
+
renderOp.h !== this.lastDimensionsH)
|
|
281
|
+
) {
|
|
230
282
|
this.glw.uniform2f('u_dimensions', renderOp.w, renderOp.h);
|
|
283
|
+
this.lastDimensionsW = renderOp.w;
|
|
284
|
+
this.lastDimensionsH = renderOp.h;
|
|
231
285
|
}
|
|
232
286
|
|
|
233
287
|
const shader = renderOp.shader as WebGlShaderNode;
|
|
234
288
|
if (shader.props !== undefined) {
|
|
235
289
|
/**
|
|
236
290
|
* loop over all precalculated uniform types
|
|
291
|
+
*
|
|
292
|
+
* Collections are immutable after being filled and shared by reference
|
|
293
|
+
* across shader nodes with equal value keys, so when the same object is
|
|
294
|
+
* already bound the GL program still holds exactly these values and the
|
|
295
|
+
* whole pass (loops + gl calls) can be skipped.
|
|
237
296
|
*/
|
|
238
297
|
const uniforms = shader.uniforms;
|
|
298
|
+
if ((uniforms as unknown) === this.lastBoundUniforms) {
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
this.lastBoundUniforms = uniforms;
|
|
239
302
|
|
|
240
303
|
for (const key in uniforms.single) {
|
|
241
304
|
const { method, value } = uniforms.single[key]!;
|
|
@@ -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
|
+
});
|