@solidtv/renderer 1.5.5 → 1.6.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.
- package/dist/src/core/CoreNode.js +2 -2
- package/dist/src/core/CoreNode.js.map +1 -1
- package/dist/src/core/Stage.js +9 -1
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +17 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +21 -0
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/colorParser.d.ts +4 -1
- package/dist/src/core/lib/colorParser.js +16 -6
- package/dist/src/core/lib/colorParser.js.map +1 -1
- package/dist/src/core/lib/validateImageBitmap.js +7 -0
- package/dist/src/core/lib/validateImageBitmap.js.map +1 -1
- package/dist/src/core/platforms/web/WebPlatform.js +39 -2
- package/dist/src/core/platforms/web/WebPlatform.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasRenderer.d.ts +4 -0
- package/dist/src/core/renderers/canvas/CanvasRenderer.js +28 -13
- package/dist/src/core/renderers/canvas/CanvasRenderer.js.map +1 -1
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js +1 -0
- package/dist/src/core/renderers/canvas/CanvasShaderNode.js.map +1 -1
- package/dist/src/core/renderers/webgl/SdfRenderOp.js +2 -2
- package/dist/src/core/renderers/webgl/SdfRenderOp.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlRenderer.d.ts +43 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +109 -10
- package/dist/src/core/renderers/webgl/WebGlRenderer.js.map +1 -1
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js +21 -13
- package/dist/src/core/renderers/webgl/WebGlShaderProgram.js.map +1 -1
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.d.ts +8 -3
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js +12 -11
- package/dist/src/core/renderers/webgl/internal/ShaderUtils.js.map +1 -1
- package/dist/src/core/shaders/canvas/Rounded.js +3 -3
- package/dist/src/core/shaders/canvas/Rounded.js.map +1 -1
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js +3 -3
- package/dist/src/core/shaders/canvas/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/canvas/utils/render.js +3 -3
- package/dist/src/core/shaders/canvas/utils/render.js.map +1 -1
- package/dist/src/core/shaders/webgl/Border.js +12 -11
- package/dist/src/core/shaders/webgl/Border.js.map +1 -1
- package/dist/src/core/shaders/webgl/HolePunch.js +12 -5
- package/dist/src/core/shaders/webgl/HolePunch.js.map +1 -1
- package/dist/src/core/shaders/webgl/LinearGradient.js +3 -19
- package/dist/src/core/shaders/webgl/LinearGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialGradient.js +3 -20
- package/dist/src/core/shaders/webgl/RadialGradient.js.map +1 -1
- package/dist/src/core/shaders/webgl/RadialProgress.js +15 -23
- package/dist/src/core/shaders/webgl/RadialProgress.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js +35 -32
- package/dist/src/core/shaders/webgl/RoundedWithBorder.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js +58 -45
- package/dist/src/core/shaders/webgl/RoundedWithBorderAndShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js +15 -13
- package/dist/src/core/shaders/webgl/RoundedWithShadow.js.map +1 -1
- package/dist/src/core/shaders/webgl/Shadow.js +4 -5
- package/dist/src/core/shaders/webgl/Shadow.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +23 -10
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/core/text-rendering/TextLayoutEngine.js +14 -2
- package/dist/src/core/text-rendering/TextLayoutEngine.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +20 -0
- package/dist/src/main-api/Renderer.js +6 -0
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.js +1 -1
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/package.json +1 -1
- package/src/core/CoreNode.ts +2 -2
- package/src/core/Stage.renderListUpdate.test.ts +73 -0
- package/src/core/Stage.ts +11 -1
- package/src/core/lib/WebGlContextWrapper.test.ts +23 -1
- package/src/core/lib/WebGlContextWrapper.ts +23 -1
- package/src/core/lib/colorParser.test.ts +46 -0
- package/src/core/lib/colorParser.ts +17 -6
- package/src/core/lib/validateImageBitmap.test.ts +15 -0
- package/src/core/lib/validateImageBitmap.ts +8 -0
- package/src/core/platforms/web/WebPlatform.loopError.test.ts +145 -0
- package/src/core/platforms/web/WebPlatform.ts +39 -2
- package/src/core/renderers/canvas/CanvasRenderer.test.ts +230 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +29 -15
- package/src/core/renderers/canvas/CanvasShaderNode.test.ts +73 -0
- package/src/core/renderers/canvas/CanvasShaderNode.ts +1 -0
- package/src/core/renderers/webgl/SdfRenderOp.ts +2 -2
- package/src/core/renderers/webgl/WebGlRenderer.reuseRenderOp.test.ts +122 -0
- package/src/core/renderers/webgl/WebGlRenderer.sdfBuffer.test.ts +302 -0
- package/src/core/renderers/webgl/WebGlRenderer.ts +151 -11
- package/src/core/renderers/webgl/WebGlShaderProgram.reuseRenderOp.test.ts +150 -0
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +27 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +4 -2
- package/src/core/renderers/webgl/internal/ShaderUtils.test.ts +124 -0
- package/src/core/renderers/webgl/internal/ShaderUtils.ts +14 -13
- package/src/core/shaders/canvas/Rounded.ts +3 -3
- package/src/core/shaders/canvas/RoundedWithShadow.ts +3 -3
- package/src/core/shaders/canvas/utils/render.ts +3 -3
- package/src/core/shaders/webgl/Border.ts +12 -11
- package/src/core/shaders/webgl/HolePunch.ts +12 -5
- package/src/core/shaders/webgl/LinearGradient.ts +3 -19
- package/src/core/shaders/webgl/RadialGradient.ts +3 -20
- package/src/core/shaders/webgl/RadialProgress.ts +15 -23
- package/src/core/shaders/webgl/RoundedWithBorder.ts +35 -32
- package/src/core/shaders/webgl/RoundedWithBorderAndShadow.ts +58 -45
- package/src/core/shaders/webgl/RoundedWithShadow.ts +15 -13
- package/src/core/shaders/webgl/Shadow.ts +4 -5
- package/src/core/text-rendering/SdfTextRenderer.test.ts +177 -0
- package/src/core/text-rendering/SdfTextRenderer.ts +38 -10
- package/src/core/text-rendering/TextLayoutEngine.ts +15 -2
- package/src/core/text-rendering/tests/TextLayoutEngine.test.ts +99 -0
- package/src/main-api/Renderer.ts +28 -0
- package/src/utils.ts +1 -1
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { WebGlRenderer } from './WebGlRenderer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the shader-node batching gate in WebGlRenderer.reuseRenderOp.
|
|
6
|
+
*
|
|
7
|
+
* A full renderer needs a live GL context, so the method is exercised on a
|
|
8
|
+
* minimal fake `this` holding only curRenderOp. Nodes and shader nodes are
|
|
9
|
+
* plain objects with the fields reuseRenderOp reads.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const clippingRect = { x: 0, y: 0, w: 0, h: 0, valid: false };
|
|
13
|
+
|
|
14
|
+
type FakeShaderNode = {
|
|
15
|
+
shaderKey: string;
|
|
16
|
+
program: { reuseRenderOp: ReturnType<typeof vi.fn> };
|
|
17
|
+
uniforms: object;
|
|
18
|
+
resolvedProps: Record<string, unknown>;
|
|
19
|
+
};
|
|
20
|
+
|
|
21
|
+
const makeShaderNode = (
|
|
22
|
+
program: FakeShaderNode['program'],
|
|
23
|
+
uniforms: object,
|
|
24
|
+
shaderKey = 'Rounded',
|
|
25
|
+
): FakeShaderNode => ({
|
|
26
|
+
shaderKey,
|
|
27
|
+
program,
|
|
28
|
+
uniforms,
|
|
29
|
+
resolvedProps: { radius: [16, 16, 16, 16] },
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
const makeProgram = (batches = true) => ({
|
|
33
|
+
reuseRenderOp: vi.fn(() => batches),
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
// Node shape shared by the op anchor and the incoming quad
|
|
37
|
+
const makeNode = (shader: FakeShaderNode) => ({
|
|
38
|
+
isCoreNode: true,
|
|
39
|
+
parentHasRenderTexture: false,
|
|
40
|
+
parentFramebufferDimensions: null,
|
|
41
|
+
rtt: false,
|
|
42
|
+
clippingRect,
|
|
43
|
+
props: { rtt: false, shader },
|
|
44
|
+
shader,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
const reuse = (curRenderOp: unknown, node: unknown): boolean =>
|
|
48
|
+
WebGlRenderer.prototype.reuseRenderOp.call(
|
|
49
|
+
{ curRenderOp } as WebGlRenderer,
|
|
50
|
+
node as never,
|
|
51
|
+
);
|
|
52
|
+
|
|
53
|
+
describe('WebGlRenderer.reuseRenderOp shader batching', () => {
|
|
54
|
+
it('rejects when there is no current render op', () => {
|
|
55
|
+
const shader = makeShaderNode(makeProgram(), {});
|
|
56
|
+
expect(reuse(null, makeNode(shader))).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('batches two quads using the same shader node', () => {
|
|
60
|
+
const shader = makeShaderNode(makeProgram(), {});
|
|
61
|
+
|
|
62
|
+
expect(reuse(makeNode(shader), makeNode(shader))).toBe(true);
|
|
63
|
+
expect(shader.program.reuseRenderOp.mock.calls.length).toBe(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('batches distinct shader nodes sharing program and uniform collection', () => {
|
|
67
|
+
const program = makeProgram();
|
|
68
|
+
const uniforms = {};
|
|
69
|
+
const shaderA = makeShaderNode(program, uniforms);
|
|
70
|
+
const shaderB = makeShaderNode(program, uniforms);
|
|
71
|
+
|
|
72
|
+
expect(reuse(makeNode(shaderA), makeNode(shaderB))).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('rejects distinct shader nodes with different uniform collections', () => {
|
|
76
|
+
const program = makeProgram();
|
|
77
|
+
const shaderA = makeShaderNode(program, {});
|
|
78
|
+
const shaderB = makeShaderNode(program, {});
|
|
79
|
+
|
|
80
|
+
expect(reuse(makeNode(shaderA), makeNode(shaderB))).toBe(false);
|
|
81
|
+
// Gate rejects before the program-level prop compare runs
|
|
82
|
+
expect(program.reuseRenderOp.mock.calls.length).toBe(0);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('rejects equal uniform collections from different programs', () => {
|
|
86
|
+
// The shader value key does not include the shader type, so a collection
|
|
87
|
+
// shared across programs (key collision) must never merge draw calls.
|
|
88
|
+
const uniforms = {};
|
|
89
|
+
const shaderA = makeShaderNode(makeProgram(), uniforms, 'TypeA');
|
|
90
|
+
const shaderB = makeShaderNode(makeProgram(), uniforms, 'TypeB');
|
|
91
|
+
|
|
92
|
+
expect(reuse(makeNode(shaderA), makeNode(shaderB))).toBe(false);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
it('batches default-shader quads without consulting the program', () => {
|
|
96
|
+
const shaderA = makeShaderNode(makeProgram(), {}, 'default');
|
|
97
|
+
const shaderB = makeShaderNode(makeProgram(), {}, 'default');
|
|
98
|
+
|
|
99
|
+
expect(reuse(makeNode(shaderA), makeNode(shaderB))).toBe(true);
|
|
100
|
+
expect(shaderA.program.reuseRenderOp.mock.calls.length).toBe(0);
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it('rejects when clipping rects differ', () => {
|
|
104
|
+
const shader = makeShaderNode(makeProgram(), {});
|
|
105
|
+
const node = makeNode(shader);
|
|
106
|
+
node.clippingRect = { x: 0, y: 0, w: 100, h: 100, valid: true };
|
|
107
|
+
|
|
108
|
+
expect(reuse(makeNode(shader), node)).toBe(false);
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
it('defers to the program-level check for batched shader nodes', () => {
|
|
112
|
+
const program = makeProgram(false);
|
|
113
|
+
const uniforms = {};
|
|
114
|
+
const shaderA = makeShaderNode(program, uniforms);
|
|
115
|
+
const shaderB = makeShaderNode(program, uniforms);
|
|
116
|
+
|
|
117
|
+
// Same program + shared collection passes the gate, but the program can
|
|
118
|
+
// still veto (e.g. worldAlpha mismatch on a u_alpha shader).
|
|
119
|
+
expect(reuse(makeNode(shaderA), makeNode(shaderB))).toBe(false);
|
|
120
|
+
expect(program.reuseRenderOp.mock.calls.length).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
@@ -0,0 +1,302 @@
|
|
|
1
|
+
import { describe, expect, it, vi } from 'vitest';
|
|
2
|
+
import { WebGlRenderer } from './WebGlRenderer.js';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Tests for the SDF buffer write paths and the upload skip.
|
|
6
|
+
*
|
|
7
|
+
* A full renderer needs a live GL context, so methods are exercised on a
|
|
8
|
+
* minimal fake `this` via the prototype, holding only the fields each method
|
|
9
|
+
* touches.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const FLOATS_PER_GLYPH = 24; // 4 vertices x 6 floats
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Build one glyph (4 vertices) of cached vertex data with a bit-pattern color.
|
|
16
|
+
* Layout per vertex: [x, y, u, v, color(uint32), distRange].
|
|
17
|
+
*/
|
|
18
|
+
const makeCachedGlyph = (color: number): Float32Array => {
|
|
19
|
+
const buf = new ArrayBuffer(FLOATS_PER_GLYPH * 4);
|
|
20
|
+
const f = new Float32Array(buf);
|
|
21
|
+
const u = new Uint32Array(buf);
|
|
22
|
+
const corners = [
|
|
23
|
+
[10, 20, 0.1, 0.2],
|
|
24
|
+
[30, 20, 0.3, 0.2],
|
|
25
|
+
[10, 40, 0.1, 0.4],
|
|
26
|
+
[30, 40, 0.3, 0.4],
|
|
27
|
+
];
|
|
28
|
+
for (let v = 0; v < 4; v++) {
|
|
29
|
+
const i = v * 6;
|
|
30
|
+
f[i] = corners[v]![0]!;
|
|
31
|
+
f[i + 1] = corners[v]![1]!;
|
|
32
|
+
f[i + 2] = corners[v]![2]!;
|
|
33
|
+
f[i + 3] = corners[v]![3]!;
|
|
34
|
+
u[i + 4] = color;
|
|
35
|
+
f[i + 5] = 4; // distanceRange
|
|
36
|
+
}
|
|
37
|
+
return f;
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
type SdfWriterFake = {
|
|
41
|
+
sdfBufferIdx: number;
|
|
42
|
+
sdfQuadCount: number;
|
|
43
|
+
sdfBuffer: ArrayBuffer;
|
|
44
|
+
fSdfBuffer: Float32Array;
|
|
45
|
+
uiSdfBuffer: Uint32Array;
|
|
46
|
+
sdfBufferChanged: boolean;
|
|
47
|
+
ensureSdfBufferCapacity: ReturnType<typeof vi.fn>;
|
|
48
|
+
finalizeSdfBatch: ReturnType<typeof vi.fn>;
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const makeWriterFake = (): SdfWriterFake => {
|
|
52
|
+
const sdfBuffer = new ArrayBuffer(1024 * 4);
|
|
53
|
+
return {
|
|
54
|
+
sdfBufferIdx: 0,
|
|
55
|
+
sdfQuadCount: 0,
|
|
56
|
+
sdfBuffer,
|
|
57
|
+
fSdfBuffer: new Float32Array(sdfBuffer),
|
|
58
|
+
uiSdfBuffer: new Uint32Array(sdfBuffer),
|
|
59
|
+
sdfBufferChanged: false,
|
|
60
|
+
ensureSdfBufferCapacity: vi.fn(),
|
|
61
|
+
finalizeSdfBatch: vi.fn(),
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
const finalizeArgs = [
|
|
66
|
+
{} as never, // atlasTexture
|
|
67
|
+
{ x: 0, y: 0, w: 0, h: 0, valid: false }, // clippingRect
|
|
68
|
+
1, // worldAlpha
|
|
69
|
+
100, // width
|
|
70
|
+
50, // height
|
|
71
|
+
false, // parentHasRenderTexture
|
|
72
|
+
null, // framebufferDimensions
|
|
73
|
+
{} as never, // sdfShader
|
|
74
|
+
] as const;
|
|
75
|
+
|
|
76
|
+
describe('WebGlRenderer.addSdfTranslatedQuads', () => {
|
|
77
|
+
it('copies cached vertices with the position delta applied', () => {
|
|
78
|
+
const fake = makeWriterFake();
|
|
79
|
+
const cached = makeCachedGlyph(0x00ff00ff);
|
|
80
|
+
|
|
81
|
+
WebGlRenderer.prototype.addSdfTranslatedQuads.call(
|
|
82
|
+
fake as never,
|
|
83
|
+
cached,
|
|
84
|
+
1,
|
|
85
|
+
5,
|
|
86
|
+
-3,
|
|
87
|
+
...finalizeArgs,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
const f = fake.fSdfBuffer;
|
|
91
|
+
// Positions shifted by (5, -3)
|
|
92
|
+
expect([f[0], f[1]]).toEqual([15, 17]);
|
|
93
|
+
expect([f[6], f[7]]).toEqual([35, 17]);
|
|
94
|
+
expect([f[12], f[13]]).toEqual([15, 37]);
|
|
95
|
+
expect([f[18], f[19]]).toEqual([35, 37]);
|
|
96
|
+
// UVs and distanceRange untouched
|
|
97
|
+
expect([f[2], f[3], f[5]]).toEqual([Math.fround(0.1), Math.fround(0.2), 4]);
|
|
98
|
+
expect(fake.sdfBufferIdx).toBe(FLOATS_PER_GLYPH);
|
|
99
|
+
expect(fake.sdfQuadCount).toBe(1);
|
|
100
|
+
expect(fake.sdfBufferChanged).toBe(true);
|
|
101
|
+
expect(fake.finalizeSdfBatch.mock.calls.length).toBe(1);
|
|
102
|
+
// startQuad 0, numGlyphs 1
|
|
103
|
+
expect(fake.finalizeSdfBatch.mock.calls[0]![0]).toBe(0);
|
|
104
|
+
expect(fake.finalizeSdfBatch.mock.calls[0]![1]).toBe(1);
|
|
105
|
+
// Source cache untouched
|
|
106
|
+
expect(cached[0]).toBe(10);
|
|
107
|
+
expect(cached[1]).toBe(20);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it('preserves packed color bits exactly, including float32 NaN patterns', () => {
|
|
111
|
+
const fake = makeWriterFake();
|
|
112
|
+
// 0xFFFFFFFF (white, full alpha) is a float32 NaN bit pattern — an
|
|
113
|
+
// element-wise float copy could canonicalize it. The memcpy must not.
|
|
114
|
+
const cached = makeCachedGlyph(0xffffffff);
|
|
115
|
+
|
|
116
|
+
WebGlRenderer.prototype.addSdfTranslatedQuads.call(
|
|
117
|
+
fake as never,
|
|
118
|
+
cached,
|
|
119
|
+
1,
|
|
120
|
+
100,
|
|
121
|
+
200,
|
|
122
|
+
...finalizeArgs,
|
|
123
|
+
);
|
|
124
|
+
|
|
125
|
+
for (let v = 0; v < 4; v++) {
|
|
126
|
+
expect(fake.uiSdfBuffer[v * 6 + 4]).toBe(0xffffffff);
|
|
127
|
+
}
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
it('appends after existing content and is a no-op for zero glyphs', () => {
|
|
131
|
+
const fake = makeWriterFake();
|
|
132
|
+
fake.sdfBufferIdx = FLOATS_PER_GLYPH;
|
|
133
|
+
fake.sdfQuadCount = 1;
|
|
134
|
+
const cached = makeCachedGlyph(0x000000ff);
|
|
135
|
+
|
|
136
|
+
WebGlRenderer.prototype.addSdfTranslatedQuads.call(
|
|
137
|
+
fake as never,
|
|
138
|
+
cached,
|
|
139
|
+
0,
|
|
140
|
+
5,
|
|
141
|
+
5,
|
|
142
|
+
...finalizeArgs,
|
|
143
|
+
);
|
|
144
|
+
expect(fake.sdfBufferIdx).toBe(FLOATS_PER_GLYPH);
|
|
145
|
+
expect(fake.sdfBufferChanged).toBe(false);
|
|
146
|
+
expect(fake.finalizeSdfBatch.mock.calls.length).toBe(0);
|
|
147
|
+
|
|
148
|
+
WebGlRenderer.prototype.addSdfTranslatedQuads.call(
|
|
149
|
+
fake as never,
|
|
150
|
+
cached,
|
|
151
|
+
1,
|
|
152
|
+
5,
|
|
153
|
+
5,
|
|
154
|
+
...finalizeArgs,
|
|
155
|
+
);
|
|
156
|
+
expect(fake.sdfBufferIdx).toBe(FLOATS_PER_GLYPH * 2);
|
|
157
|
+
expect(fake.fSdfBuffer[FLOATS_PER_GLYPH]).toBe(15);
|
|
158
|
+
// startQuad continues from the existing quad count
|
|
159
|
+
expect(fake.finalizeSdfBatch.mock.calls[0]![0]).toBe(1);
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
type UploadFake = {
|
|
164
|
+
sdfBufferIdx: number;
|
|
165
|
+
sdfBufferChanged: boolean;
|
|
166
|
+
lastUploadedSdfSize: number;
|
|
167
|
+
sdfBuffer: ArrayBuffer;
|
|
168
|
+
sdfQuadBufferCollection: { getBuffer: ReturnType<typeof vi.fn> };
|
|
169
|
+
glw: { arrayBufferData: ReturnType<typeof vi.fn>; DYNAMIC_DRAW: number };
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const makeUploadFake = (): UploadFake => ({
|
|
173
|
+
sdfBufferIdx: 0,
|
|
174
|
+
sdfBufferChanged: true,
|
|
175
|
+
lastUploadedSdfSize: 0,
|
|
176
|
+
sdfBuffer: new ArrayBuffer(1024),
|
|
177
|
+
sdfQuadBufferCollection: { getBuffer: vi.fn(() => ({})) },
|
|
178
|
+
glw: { arrayBufferData: vi.fn(), DYNAMIC_DRAW: 35048 },
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
const upload = (fake: UploadFake) =>
|
|
182
|
+
(
|
|
183
|
+
WebGlRenderer.prototype as unknown as {
|
|
184
|
+
uploadSdfBuffer: (this: unknown) => void;
|
|
185
|
+
}
|
|
186
|
+
).uploadSdfBuffer.call(fake);
|
|
187
|
+
|
|
188
|
+
describe('WebGlRenderer.uploadSdfBuffer skip', () => {
|
|
189
|
+
it('does nothing when no SDF glyphs were written', () => {
|
|
190
|
+
const fake = makeUploadFake();
|
|
191
|
+
|
|
192
|
+
upload(fake);
|
|
193
|
+
|
|
194
|
+
expect(fake.glw.arrayBufferData.mock.calls.length).toBe(0);
|
|
195
|
+
// Flag is kept so a later frame with content still uploads
|
|
196
|
+
expect(fake.sdfBufferChanged).toBe(true);
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
it('uploads when changed, then skips identical follow-up frames', () => {
|
|
200
|
+
const fake = makeUploadFake();
|
|
201
|
+
fake.sdfBufferIdx = 48;
|
|
202
|
+
|
|
203
|
+
upload(fake);
|
|
204
|
+
expect(fake.glw.arrayBufferData.mock.calls.length).toBe(1);
|
|
205
|
+
expect(fake.sdfBufferChanged).toBe(false);
|
|
206
|
+
expect(fake.lastUploadedSdfSize).toBe(48);
|
|
207
|
+
|
|
208
|
+
// Next frame: exact cache hits rewrote identical bytes, same size
|
|
209
|
+
upload(fake);
|
|
210
|
+
expect(fake.glw.arrayBufferData.mock.calls.length).toBe(1);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('uploads again when the changed flag is raised', () => {
|
|
214
|
+
const fake = makeUploadFake();
|
|
215
|
+
fake.sdfBufferIdx = 48;
|
|
216
|
+
upload(fake);
|
|
217
|
+
|
|
218
|
+
fake.sdfBufferChanged = true;
|
|
219
|
+
upload(fake);
|
|
220
|
+
|
|
221
|
+
expect(fake.glw.arrayBufferData.mock.calls.length).toBe(2);
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
it('uploads when the size differs even if the flag is clear', () => {
|
|
225
|
+
const fake = makeUploadFake();
|
|
226
|
+
fake.sdfBufferIdx = 48;
|
|
227
|
+
upload(fake);
|
|
228
|
+
|
|
229
|
+
fake.sdfBufferIdx = 24;
|
|
230
|
+
upload(fake);
|
|
231
|
+
|
|
232
|
+
expect(fake.glw.arrayBufferData.mock.calls.length).toBe(2);
|
|
233
|
+
expect(fake.lastUploadedSdfSize).toBe(24);
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
describe('sdfBufferChanged raisers', () => {
|
|
238
|
+
it('addSdfQuads marks the buffer changed', () => {
|
|
239
|
+
const fake = makeWriterFake();
|
|
240
|
+
// One glyph record: [x, y, w, h, u, v, uw, vh]
|
|
241
|
+
const glyphs = new Float32Array([0, 0, 10, 10, 0, 0, 0.1, 0.1]);
|
|
242
|
+
const transform = new Float32Array([1, 0, 0, 0, 1, 0, 0, 0, 1]);
|
|
243
|
+
|
|
244
|
+
WebGlRenderer.prototype.addSdfQuads.call(
|
|
245
|
+
fake as never,
|
|
246
|
+
glyphs,
|
|
247
|
+
1, // glyphCount
|
|
248
|
+
1, // fontScale
|
|
249
|
+
transform,
|
|
250
|
+
0xffffffff, // color
|
|
251
|
+
1, // worldAlpha
|
|
252
|
+
4, // distanceRange
|
|
253
|
+
{} as never, // atlasTexture
|
|
254
|
+
{ x: 0, y: 0, w: 0, h: 0, valid: false }, // clippingRect
|
|
255
|
+
100, // width
|
|
256
|
+
50, // height
|
|
257
|
+
false, // parentHasRenderTexture
|
|
258
|
+
null, // framebufferDimensions
|
|
259
|
+
{} as never, // sdfShader
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
expect(fake.sdfBufferChanged).toBe(true);
|
|
263
|
+
expect(fake.sdfBufferIdx).toBe(FLOATS_PER_GLYPH);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
it('invalidateQuadBuffer marks the buffer changed (render-list rebuild)', () => {
|
|
267
|
+
const fake = {
|
|
268
|
+
sdfBufferChanged: false,
|
|
269
|
+
curBufferIdx: 99,
|
|
270
|
+
lastUploadedBufferSize: 99,
|
|
271
|
+
needsFullUpload: false,
|
|
272
|
+
stage: { renderList: [] },
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
WebGlRenderer.prototype.invalidateQuadBuffer.call(fake as never);
|
|
276
|
+
|
|
277
|
+
expect(fake.sdfBufferChanged).toBe(true);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it('ensureSdfBufferCapacity marks the buffer changed on growth only', () => {
|
|
281
|
+
const sdfBuffer = new ArrayBuffer(FLOATS_PER_GLYPH * 4);
|
|
282
|
+
const fake = {
|
|
283
|
+
sdfBuffer,
|
|
284
|
+
fSdfBuffer: new Float32Array(sdfBuffer),
|
|
285
|
+
uiSdfBuffer: new Uint32Array(sdfBuffer),
|
|
286
|
+
sdfBufferChanged: false,
|
|
287
|
+
};
|
|
288
|
+
const ensure = (size: number) =>
|
|
289
|
+
(
|
|
290
|
+
WebGlRenderer.prototype as unknown as {
|
|
291
|
+
ensureSdfBufferCapacity: (this: unknown, n: number) => void;
|
|
292
|
+
}
|
|
293
|
+
).ensureSdfBufferCapacity.call(fake, size);
|
|
294
|
+
|
|
295
|
+
ensure(FLOATS_PER_GLYPH); // fits — no realloc
|
|
296
|
+
expect(fake.sdfBufferChanged).toBe(false);
|
|
297
|
+
|
|
298
|
+
ensure(FLOATS_PER_GLYPH * 8); // grows
|
|
299
|
+
expect(fake.sdfBufferChanged).toBe(true);
|
|
300
|
+
expect(fake.fSdfBuffer.length).toBeGreaterThanOrEqual(FLOATS_PER_GLYPH * 8);
|
|
301
|
+
});
|
|
302
|
+
});
|
|
@@ -127,6 +127,23 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
127
127
|
sdfQuadCount = 0;
|
|
128
128
|
sdfQuadBufferCollection: BufferCollection;
|
|
129
129
|
curSdfRenderOp: SdfRenderOp | null = null;
|
|
130
|
+
/**
|
|
131
|
+
* Whether the shared SDF buffer's bytes may differ from what the GPU
|
|
132
|
+
* currently holds. Set by every write path that produces fresh bytes
|
|
133
|
+
* (cache-miss recompute, translated copy), by anything that can shift
|
|
134
|
+
* offsets or resize the buffer (render-list rebuild, RTT partial upload,
|
|
135
|
+
* backing-store growth), and consumed by {@link uploadSdfBuffer}. The
|
|
136
|
+
* exact cache-hit mem-copy path deliberately does NOT set it — it writes
|
|
137
|
+
* byte-identical data at identical offsets. Conservative direction: when
|
|
138
|
+
* in doubt, set it — a redundant upload is correct, a wrong skip is a
|
|
139
|
+
* glitch.
|
|
140
|
+
*/
|
|
141
|
+
sdfBufferChanged = true;
|
|
142
|
+
/**
|
|
143
|
+
* Float32 length of the last main-pass SDF upload — the size half of the
|
|
144
|
+
* skip test in {@link uploadSdfBuffer}.
|
|
145
|
+
*/
|
|
146
|
+
lastUploadedSdfSize = 0;
|
|
130
147
|
|
|
131
148
|
/**
|
|
132
149
|
* When true, the entire quad buffer is re-uploaded to the GPU via bufferData
|
|
@@ -212,7 +229,7 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
212
229
|
gl,
|
|
213
230
|
options.disableVertexArrayObject,
|
|
214
231
|
));
|
|
215
|
-
glw.viewport(0, 0,
|
|
232
|
+
glw.viewport(0, 0, glw.canvasW, glw.canvasH);
|
|
216
233
|
|
|
217
234
|
this.attachContextLossListeners(options.canvas);
|
|
218
235
|
|
|
@@ -640,9 +657,21 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
640
657
|
return true;
|
|
641
658
|
}
|
|
642
659
|
|
|
643
|
-
//
|
|
660
|
+
// Distinct shader nodes can still batch when they resolve to the same
|
|
661
|
+
// program and the value-key cache handed both the same uniform collection:
|
|
662
|
+
// collections are immutable after fill and shared by reference across
|
|
663
|
+
// equal value keys, so reference equality implies value equality. This is
|
|
664
|
+
// the common TV-rail case — many same-size cards, each app-created with
|
|
665
|
+
// its own shader node but equal props. Program identity is required
|
|
666
|
+
// because the value key does not include the shader type, so equal
|
|
667
|
+
// collections from different programs must not merge.
|
|
644
668
|
if (curShader !== shader) {
|
|
645
|
-
|
|
669
|
+
if (
|
|
670
|
+
curShader.program !== shader.program ||
|
|
671
|
+
curShader.uniforms !== shader.uniforms
|
|
672
|
+
) {
|
|
673
|
+
return false;
|
|
674
|
+
}
|
|
646
675
|
}
|
|
647
676
|
|
|
648
677
|
if (
|
|
@@ -729,6 +758,9 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
729
758
|
return;
|
|
730
759
|
}
|
|
731
760
|
|
|
761
|
+
// Full recompute writes fresh bytes — the GPU copy is now stale.
|
|
762
|
+
this.sdfBufferChanged = true;
|
|
763
|
+
|
|
732
764
|
let idx = this.sdfBufferIdx;
|
|
733
765
|
this.ensureSdfBufferCapacity(idx + glyphCount * 24);
|
|
734
766
|
|
|
@@ -891,6 +923,76 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
891
923
|
);
|
|
892
924
|
}
|
|
893
925
|
|
|
926
|
+
/**
|
|
927
|
+
* Append cached SDF vertices translated by (dx, dy) to the shared buffer.
|
|
928
|
+
*
|
|
929
|
+
* @remarks
|
|
930
|
+
* The scroll fast path: a text node whose transform changed by pure
|
|
931
|
+
* translation reuses its world-space vertex cache — one mem-copy plus two
|
|
932
|
+
* adds per vertex instead of full per-glyph matrix math, and the cache
|
|
933
|
+
* keeps its original base so nothing is re-snapshotted per frame.
|
|
934
|
+
*
|
|
935
|
+
* The copy MUST stay a typed-array `set` (bit-exact memcpy): packed ABGR
|
|
936
|
+
* colors live in the same Float32Array and some bit patterns are float32
|
|
937
|
+
* NaNs, which element-wise float reads/writes may canonicalize and corrupt.
|
|
938
|
+
* Only the two position floats of each vertex are touched after the copy.
|
|
939
|
+
*/
|
|
940
|
+
addSdfTranslatedQuads(
|
|
941
|
+
cachedVertices: Float32Array,
|
|
942
|
+
numGlyphs: number,
|
|
943
|
+
dx: number,
|
|
944
|
+
dy: number,
|
|
945
|
+
atlasTexture: WebGlCtxTexture,
|
|
946
|
+
clippingRect: import('../../lib/utils.js').RectWithValid,
|
|
947
|
+
worldAlpha: number,
|
|
948
|
+
width: number,
|
|
949
|
+
height: number,
|
|
950
|
+
parentHasRenderTexture: boolean,
|
|
951
|
+
framebufferDimensions:
|
|
952
|
+
| import('../../../common/CommonTypes.js').Dimensions
|
|
953
|
+
| null,
|
|
954
|
+
sdfShader: WebGlShaderNode,
|
|
955
|
+
): void {
|
|
956
|
+
if (numGlyphs === 0) {
|
|
957
|
+
return;
|
|
958
|
+
}
|
|
959
|
+
|
|
960
|
+
// Translated positions are fresh bytes — the GPU copy is now stale.
|
|
961
|
+
this.sdfBufferChanged = true;
|
|
962
|
+
|
|
963
|
+
const startQuad = this.sdfQuadCount;
|
|
964
|
+
const idx = this.sdfBufferIdx;
|
|
965
|
+
|
|
966
|
+
this.ensureSdfBufferCapacity(idx + cachedVertices.length);
|
|
967
|
+
|
|
968
|
+
// Read the buffer reference only after ensureSdfBufferCapacity — growth
|
|
969
|
+
// swaps the backing store.
|
|
970
|
+
const f = this.fSdfBuffer;
|
|
971
|
+
f.set(cachedVertices, idx);
|
|
972
|
+
|
|
973
|
+
const end = idx + cachedVertices.length;
|
|
974
|
+
for (let i = idx; i < end; i += 6) {
|
|
975
|
+
f[i] = f[i]! + dx;
|
|
976
|
+
f[i + 1] = f[i + 1]! + dy;
|
|
977
|
+
}
|
|
978
|
+
|
|
979
|
+
this.sdfBufferIdx = end;
|
|
980
|
+
this.sdfQuadCount += numGlyphs;
|
|
981
|
+
|
|
982
|
+
this.finalizeSdfBatch(
|
|
983
|
+
startQuad,
|
|
984
|
+
numGlyphs,
|
|
985
|
+
atlasTexture,
|
|
986
|
+
clippingRect,
|
|
987
|
+
worldAlpha,
|
|
988
|
+
width,
|
|
989
|
+
height,
|
|
990
|
+
parentHasRenderTexture,
|
|
991
|
+
framebufferDimensions,
|
|
992
|
+
sdfShader,
|
|
993
|
+
);
|
|
994
|
+
}
|
|
995
|
+
|
|
894
996
|
/**
|
|
895
997
|
* Shared batching logic for SDF render ops.
|
|
896
998
|
* Called by both `addSdfQuads` (full compute) and `addSdfCachedQuads` (fast copy).
|
|
@@ -992,6 +1094,9 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
992
1094
|
this.sdfBuffer = newBuffer;
|
|
993
1095
|
this.fSdfBuffer = newFSdfBuffer;
|
|
994
1096
|
this.uiSdfBuffer = newUiSdfBuffer;
|
|
1097
|
+
|
|
1098
|
+
// New backing store — never skip the next upload.
|
|
1099
|
+
this.sdfBufferChanged = true;
|
|
995
1100
|
}
|
|
996
1101
|
|
|
997
1102
|
/**
|
|
@@ -1068,12 +1173,7 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1068
1173
|
}
|
|
1069
1174
|
|
|
1070
1175
|
// Upload the shared SDF buffer if any SDF glyphs were written this frame.
|
|
1071
|
-
|
|
1072
|
-
const sdfBuf =
|
|
1073
|
-
this.sdfQuadBufferCollection.getBuffer('a_position') || null;
|
|
1074
|
-
const sdfArr = new Float32Array(this.sdfBuffer, 0, this.sdfBufferIdx);
|
|
1075
|
-
glw.arrayBufferData(sdfBuf, sdfArr, glw.DYNAMIC_DRAW);
|
|
1076
|
-
}
|
|
1176
|
+
this.uploadSdfBuffer();
|
|
1077
1177
|
|
|
1078
1178
|
for (let i = 0, length = this.renderOps.length; i < length; i++) {
|
|
1079
1179
|
this.renderOps[i]!.draw(this);
|
|
@@ -1087,6 +1187,34 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1087
1187
|
this.numQuadsRendered = this.quadBufferUsage / QUAD_SIZE_IN_BYTES;
|
|
1088
1188
|
}
|
|
1089
1189
|
|
|
1190
|
+
/**
|
|
1191
|
+
* Upload the shared SDF buffer for the main pass, skipping the driver-side
|
|
1192
|
+
* `bufferData` copy when the bytes provably match what the GPU already
|
|
1193
|
+
* holds: every write this frame was an exact cache-hit mem-copy
|
|
1194
|
+
* (`sdfBufferChanged` false) and the total size matches the previous
|
|
1195
|
+
* upload. Exact hits write byte-identical data, and identical offsets are
|
|
1196
|
+
* guaranteed because every source of reorder or resize — cache-miss
|
|
1197
|
+
* recompute, translated copy, render-list rebuild, RTT partial upload,
|
|
1198
|
+
* backing-store growth — sets `sdfBufferChanged`.
|
|
1199
|
+
*/
|
|
1200
|
+
private uploadSdfBuffer(): void {
|
|
1201
|
+
if (this.sdfBufferIdx === 0) {
|
|
1202
|
+
return;
|
|
1203
|
+
}
|
|
1204
|
+
if (
|
|
1205
|
+
this.sdfBufferChanged === false &&
|
|
1206
|
+
this.sdfBufferIdx === this.lastUploadedSdfSize
|
|
1207
|
+
) {
|
|
1208
|
+
return;
|
|
1209
|
+
}
|
|
1210
|
+
const glw = this.glw;
|
|
1211
|
+
const sdfBuf = this.sdfQuadBufferCollection.getBuffer('a_position') || null;
|
|
1212
|
+
const sdfArr = new Float32Array(this.sdfBuffer, 0, this.sdfBufferIdx);
|
|
1213
|
+
glw.arrayBufferData(sdfBuf, sdfArr, glw.DYNAMIC_DRAW);
|
|
1214
|
+
this.lastUploadedSdfSize = this.sdfBufferIdx;
|
|
1215
|
+
this.sdfBufferChanged = false;
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1090
1218
|
getQuadCount(): number {
|
|
1091
1219
|
return this.numQuadsRendered;
|
|
1092
1220
|
}
|
|
@@ -1264,7 +1392,7 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1264
1392
|
// Bind the default framebuffer
|
|
1265
1393
|
glw.bindFramebuffer(null);
|
|
1266
1394
|
|
|
1267
|
-
glw.viewport(0, 0,
|
|
1395
|
+
glw.viewport(0, 0, glw.canvasW, glw.canvasH);
|
|
1268
1396
|
this.renderToTextureActive = false;
|
|
1269
1397
|
}
|
|
1270
1398
|
|
|
@@ -1323,6 +1451,9 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1323
1451
|
this.sdfQuadBufferCollection.getBuffer('a_position') || null;
|
|
1324
1452
|
const sdfArr = new Float32Array(this.sdfBuffer, 0, this.sdfBufferIdx);
|
|
1325
1453
|
glw.arrayBufferData(sdfBuf, sdfArr, glw.DYNAMIC_DRAW);
|
|
1454
|
+
// This partial upload replaced the GL buffer's contents and size — the
|
|
1455
|
+
// main pass upload can no longer be skipped this frame.
|
|
1456
|
+
this.sdfBufferChanged = true;
|
|
1326
1457
|
}
|
|
1327
1458
|
|
|
1328
1459
|
for (let i = 0, length = this.renderOps.length; i < length; i++) {
|
|
@@ -1331,7 +1462,10 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1331
1462
|
}
|
|
1332
1463
|
|
|
1333
1464
|
updateViewport(): void {
|
|
1334
|
-
|
|
1465
|
+
// Called after the canvas is resized (Renderer.updateAppDimensions) —
|
|
1466
|
+
// refresh the wrapper's cached dimensions before they're used again.
|
|
1467
|
+
this.glw.updateCanvasDimensions();
|
|
1468
|
+
this.glw.viewport(0, 0, this.glw.canvasW, this.glw.canvasH);
|
|
1335
1469
|
}
|
|
1336
1470
|
|
|
1337
1471
|
removeRTTNode(node: CoreNode) {
|
|
@@ -1486,6 +1620,12 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
1486
1620
|
* next addQuad() pass will reassign compact, contiguous slots starting from 0.
|
|
1487
1621
|
*/
|
|
1488
1622
|
override invalidateQuadBuffer(): void {
|
|
1623
|
+
// A render-list rebuild can reorder text nodes without touching their
|
|
1624
|
+
// SDF vertex caches; byte-identical glyph data would then land at
|
|
1625
|
+
// different offsets, so the upload skip must not fire on the rebuild
|
|
1626
|
+
// frame.
|
|
1627
|
+
this.sdfBufferChanged = true;
|
|
1628
|
+
|
|
1489
1629
|
if (!DIRTY_QUAD_BUFFER) {
|
|
1490
1630
|
return;
|
|
1491
1631
|
}
|