@solidtv/renderer 1.5.0-1 → 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 +4 -1
- package/dist/src/core/Stage.js.map +1 -1
- package/dist/src/core/TextureMemoryManager.d.ts +62 -2
- package/dist/src/core/TextureMemoryManager.js +90 -4
- package/dist/src/core/TextureMemoryManager.js.map +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.d.ts +1 -1
- package/dist/src/core/lib/WebGlContextWrapper.js +10 -3
- package/dist/src/core/lib/WebGlContextWrapper.js.map +1 -1
- package/dist/src/core/lib/textureCompression.js +68 -4
- package/dist/src/core/lib/textureCompression.js.map +1 -1
- package/dist/src/core/renderers/CoreRenderer.d.ts +1 -0
- package/dist/src/core/renderers/CoreRenderer.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.d.ts +8 -0
- package/dist/src/core/renderers/webgl/WebGlRenderer.js +54 -14
- 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/core/renderers/webgl/internal/RendererUtils.js +9 -2
- package/dist/src/core/renderers/webgl/internal/RendererUtils.js.map +1 -1
- package/dist/src/core/text-rendering/SdfTextRenderer.js +16 -3
- package/dist/src/core/text-rendering/SdfTextRenderer.js.map +1 -1
- package/dist/src/main-api/Renderer.d.ts +16 -0
- package/dist/src/main-api/Renderer.js +2 -0
- package/dist/src/main-api/Renderer.js.map +1 -1
- package/dist/src/utils.js +23 -7
- package/dist/src/utils.js.map +1 -1
- package/dist/tsconfig.dist.tsbuildinfo +1 -1
- package/dist/tsconfig.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 +4 -0
- package/src/core/TextureMemoryManager.test.ts +323 -7
- package/src/core/TextureMemoryManager.ts +101 -5
- package/src/core/lib/WebGlContextWrapper.test.ts +58 -0
- package/src/core/lib/WebGlContextWrapper.ts +13 -3
- package/src/core/lib/textureCompression.test.ts +158 -0
- package/src/core/lib/textureCompression.ts +78 -4
- package/src/core/renderers/CoreRenderer.ts +1 -0
- package/src/core/renderers/canvas/CanvasRenderer.ts +7 -1
- package/src/core/renderers/webgl/WebGlRenderer.ts +64 -16
- package/src/core/renderers/webgl/WebGlShaderProgram.ts +78 -15
- package/src/core/renderers/webgl/WebGlShaderProgram.uniformDedup.test.ts +233 -0
- package/src/core/renderers/webgl/internal/RendererUtils.test.ts +73 -0
- package/src/core/renderers/webgl/internal/RendererUtils.ts +9 -2
- package/src/core/text-rendering/SdfTextRenderer.test.ts +11 -10
- package/src/core/text-rendering/SdfTextRenderer.ts +17 -3
- package/src/main-api/Renderer.ts +19 -0
- package/src/utils.test.ts +57 -0
- package/src/utils.ts +24 -8
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { describe, it, expect, vi } from 'vitest';
|
|
2
|
+
import { uploadCompressedTexture } from './textureCompression.js';
|
|
3
|
+
import type { WebGlContextWrapper } from './WebGlContextWrapper.js';
|
|
4
|
+
import type { CompressedData } from '../textures/Texture.js';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* A compressed format enum is only valid in compressedTexImage2D after its
|
|
8
|
+
* owning extension has been enabled via getExtension; otherwise the driver
|
|
9
|
+
* rejects it with GL_INVALID_ENUM (1280). These tests pin that every upload
|
|
10
|
+
* path enables its extension *before* uploading, and fails loudly when the
|
|
11
|
+
* device exposes none of the candidates.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
interface MockGlw {
|
|
15
|
+
glw: WebGlContextWrapper;
|
|
16
|
+
order: string[];
|
|
17
|
+
getExtension: ReturnType<typeof vi.fn>;
|
|
18
|
+
compressedTexImage2D: ReturnType<typeof vi.fn>;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function makeGlw(supported: Set<string>): MockGlw {
|
|
22
|
+
const order: string[] = [];
|
|
23
|
+
const getExtension = vi.fn((name: string) => {
|
|
24
|
+
order.push(`getExtension:${name}`);
|
|
25
|
+
return supported.has(name) === true ? {} : null;
|
|
26
|
+
});
|
|
27
|
+
const compressedTexImage2D = vi.fn(() => {
|
|
28
|
+
order.push('compressedTexImage2D');
|
|
29
|
+
});
|
|
30
|
+
const glw = {
|
|
31
|
+
getExtension,
|
|
32
|
+
compressedTexImage2D,
|
|
33
|
+
bindTexture: vi.fn(),
|
|
34
|
+
texParameteri: vi.fn(),
|
|
35
|
+
TEXTURE_WRAP_S: 0,
|
|
36
|
+
TEXTURE_WRAP_T: 0,
|
|
37
|
+
TEXTURE_MAG_FILTER: 0,
|
|
38
|
+
TEXTURE_MIN_FILTER: 0,
|
|
39
|
+
CLAMP_TO_EDGE: 0,
|
|
40
|
+
LINEAR: 0,
|
|
41
|
+
LINEAR_MIPMAP_LINEAR: 0,
|
|
42
|
+
} as unknown as WebGlContextWrapper;
|
|
43
|
+
return { glw, order, getExtension, compressedTexImage2D };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function makeData(
|
|
47
|
+
type: 'ktx' | 'pvr' | 'astc',
|
|
48
|
+
glInternalFormat: number,
|
|
49
|
+
): CompressedData {
|
|
50
|
+
return {
|
|
51
|
+
type,
|
|
52
|
+
glInternalFormat,
|
|
53
|
+
w: 4,
|
|
54
|
+
h: 4,
|
|
55
|
+
mipmaps: [new ArrayBuffer(16)],
|
|
56
|
+
blockInfo: { width: 4, height: 4, bytes: 16 },
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const texture = {} as WebGLTexture;
|
|
61
|
+
|
|
62
|
+
// COMPRESSED_RGBA_S3TC_DXT5_EXT
|
|
63
|
+
const S3TC_DXT5 = 0x83f3;
|
|
64
|
+
// COMPRESSED_RGB_PVRTC_4BPPV1_IMG
|
|
65
|
+
const PVRTC_4BPP = 0x8c00;
|
|
66
|
+
// COMPRESSED_RGB_ETC1_WEBGL
|
|
67
|
+
const ETC1 = 0x8d64;
|
|
68
|
+
// COMPRESSED_RGBA_ASTC_4x4_KHR
|
|
69
|
+
const ASTC_4x4 = 0x93b0;
|
|
70
|
+
|
|
71
|
+
describe('compressed texture extension guards', () => {
|
|
72
|
+
it('KTX enables the s3tc extension before uploading', () => {
|
|
73
|
+
const m = makeGlw(new Set(['WEBGL_compressed_texture_s3tc']));
|
|
74
|
+
uploadCompressedTexture.ktx!(m.glw, texture, makeData('ktx', S3TC_DXT5));
|
|
75
|
+
|
|
76
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
77
|
+
'WEBGL_compressed_texture_s3tc',
|
|
78
|
+
);
|
|
79
|
+
expect(m.compressedTexImage2D).toHaveBeenCalled();
|
|
80
|
+
// getExtension must precede the first compressedTexImage2D call
|
|
81
|
+
expect(m.order.indexOf('getExtension:WEBGL_compressed_texture_s3tc')).toBe(
|
|
82
|
+
0,
|
|
83
|
+
);
|
|
84
|
+
expect(
|
|
85
|
+
m.order.indexOf('getExtension:WEBGL_compressed_texture_s3tc') <
|
|
86
|
+
m.order.indexOf('compressedTexImage2D'),
|
|
87
|
+
).toBe(true);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it('KTX throws (no silent 1280) when the s3tc extension is unavailable', () => {
|
|
91
|
+
const m = makeGlw(new Set());
|
|
92
|
+
expect(() =>
|
|
93
|
+
uploadCompressedTexture.ktx!(m.glw, texture, makeData('ktx', S3TC_DXT5)),
|
|
94
|
+
).toThrow(/not supported/);
|
|
95
|
+
expect(m.compressedTexImage2D).not.toHaveBeenCalled();
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('KTX enables the etc1 extension for ETC1 formats', () => {
|
|
99
|
+
const m = makeGlw(new Set(['WEBGL_compressed_texture_etc1']));
|
|
100
|
+
uploadCompressedTexture.ktx!(m.glw, texture, makeData('ktx', ETC1));
|
|
101
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
102
|
+
'WEBGL_compressed_texture_etc1',
|
|
103
|
+
);
|
|
104
|
+
expect(m.compressedTexImage2D).toHaveBeenCalled();
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
it('PVR enables the pvrtc extension before uploading', () => {
|
|
108
|
+
const m = makeGlw(new Set(['WEBGL_compressed_texture_pvrtc']));
|
|
109
|
+
uploadCompressedTexture.pvr!(m.glw, texture, makeData('pvr', PVRTC_4BPP));
|
|
110
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
111
|
+
'WEBGL_compressed_texture_pvrtc',
|
|
112
|
+
);
|
|
113
|
+
expect(
|
|
114
|
+
m.order.indexOf('getExtension:WEBGL_compressed_texture_pvrtc') <
|
|
115
|
+
m.order.indexOf('compressedTexImage2D'),
|
|
116
|
+
).toBe(true);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('PVR falls back to the WebKit-prefixed pvrtc extension', () => {
|
|
120
|
+
const m = makeGlw(new Set(['WEBKIT_WEBGL_compressed_texture_pvrtc']));
|
|
121
|
+
uploadCompressedTexture.pvr!(m.glw, texture, makeData('pvr', PVRTC_4BPP));
|
|
122
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
123
|
+
'WEBGL_compressed_texture_pvrtc',
|
|
124
|
+
);
|
|
125
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
126
|
+
'WEBKIT_WEBGL_compressed_texture_pvrtc',
|
|
127
|
+
);
|
|
128
|
+
expect(m.compressedTexImage2D).toHaveBeenCalled();
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
it('PVR throws when neither pvrtc extension is available', () => {
|
|
132
|
+
const m = makeGlw(new Set());
|
|
133
|
+
expect(() =>
|
|
134
|
+
uploadCompressedTexture.pvr!(m.glw, texture, makeData('pvr', PVRTC_4BPP)),
|
|
135
|
+
).toThrow(/not supported/);
|
|
136
|
+
expect(m.compressedTexImage2D).not.toHaveBeenCalled();
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
it('ASTC enables the astc extension before uploading', () => {
|
|
140
|
+
const m = makeGlw(new Set(['WEBGL_compressed_texture_astc']));
|
|
141
|
+
uploadCompressedTexture.astc!(m.glw, texture, makeData('astc', ASTC_4x4));
|
|
142
|
+
expect(m.getExtension).toHaveBeenCalledWith(
|
|
143
|
+
'WEBGL_compressed_texture_astc',
|
|
144
|
+
);
|
|
145
|
+
expect(
|
|
146
|
+
m.order.indexOf('getExtension:WEBGL_compressed_texture_astc') <
|
|
147
|
+
m.order.indexOf('compressedTexImage2D'),
|
|
148
|
+
).toBe(true);
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
it('ASTC throws when the astc extension is unavailable', () => {
|
|
152
|
+
const m = makeGlw(new Set());
|
|
153
|
+
expect(() =>
|
|
154
|
+
uploadCompressedTexture.astc!(m.glw, texture, makeData('astc', ASTC_4x4)),
|
|
155
|
+
).toThrow(/not supported/);
|
|
156
|
+
expect(m.compressedTexImage2D).not.toHaveBeenCalled();
|
|
157
|
+
});
|
|
158
|
+
});
|
|
@@ -171,18 +171,90 @@ const loadASTC = async function (view: DataView): Promise<TextureData> {
|
|
|
171
171
|
};
|
|
172
172
|
};
|
|
173
173
|
|
|
174
|
+
// Candidate extension name lists, hoisted to module constants so the per-upload
|
|
175
|
+
// resolver returns a shared reference instead of allocating a new array each
|
|
176
|
+
// call (zero GC pressure on the texture-upload path).
|
|
177
|
+
const EXT_ASTC = ['WEBGL_compressed_texture_astc'];
|
|
178
|
+
const EXT_S3TC = ['WEBGL_compressed_texture_s3tc'];
|
|
179
|
+
const EXT_ETC1 = ['WEBGL_compressed_texture_etc1'];
|
|
180
|
+
const EXT_ETC = ['WEBGL_compressed_texture_etc'];
|
|
181
|
+
// WebKit-prefixed name is the legacy fallback.
|
|
182
|
+
const EXT_PVRTC = [
|
|
183
|
+
'WEBGL_compressed_texture_pvrtc',
|
|
184
|
+
'WEBKIT_WEBGL_compressed_texture_pvrtc',
|
|
185
|
+
];
|
|
186
|
+
const EXT_NONE: string[] = [];
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Resolve the WebGL extension(s) that must be enabled before a given compressed
|
|
190
|
+
* GL internal format may be used.
|
|
191
|
+
*
|
|
192
|
+
* @remarks
|
|
193
|
+
* `getExtension` is the call that actually enables a compressed format on a
|
|
194
|
+
* context — until it is called, the format enum is rejected by
|
|
195
|
+
* `compressedTexImage2D` with `GL_INVALID_ENUM` (1280). Listed in priority
|
|
196
|
+
* order; the first name the device exposes is used.
|
|
197
|
+
*/
|
|
198
|
+
const requiredExtensionsForFormat = (glInternalFormat: number): string[] => {
|
|
199
|
+
// ASTC (incl. sRGB variants): 0x93b0–0x93d5
|
|
200
|
+
if (glInternalFormat >= 0x93b0 && glInternalFormat <= 0x93d5) {
|
|
201
|
+
return EXT_ASTC;
|
|
202
|
+
}
|
|
203
|
+
// S3TC / DXTn: 0x83f0–0x83f3
|
|
204
|
+
if (glInternalFormat >= 0x83f0 && glInternalFormat <= 0x83f3) {
|
|
205
|
+
return EXT_S3TC;
|
|
206
|
+
}
|
|
207
|
+
// ETC1: 0x8d64
|
|
208
|
+
if (glInternalFormat === 0x8d64) {
|
|
209
|
+
return EXT_ETC1;
|
|
210
|
+
}
|
|
211
|
+
// ETC2 / EAC: 0x9274–0x9279
|
|
212
|
+
if (glInternalFormat >= 0x9274 && glInternalFormat <= 0x9279) {
|
|
213
|
+
return EXT_ETC;
|
|
214
|
+
}
|
|
215
|
+
// PVRTC: 0x8c00–0x8c03
|
|
216
|
+
if (glInternalFormat >= 0x8c00 && glInternalFormat <= 0x8c03) {
|
|
217
|
+
return EXT_PVRTC;
|
|
218
|
+
}
|
|
219
|
+
return EXT_NONE;
|
|
220
|
+
};
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Enable the extension owning `glInternalFormat` so the format enum is valid in
|
|
224
|
+
* `compressedTexImage2D`, throwing a clear error if the device exposes none of
|
|
225
|
+
* the candidate extensions (instead of leaking a silent `GL_INVALID_ENUM`).
|
|
226
|
+
*/
|
|
227
|
+
const ensureCompressedFormatEnabled = (
|
|
228
|
+
glw: WebGlContextWrapper,
|
|
229
|
+
glInternalFormat: number,
|
|
230
|
+
): void => {
|
|
231
|
+
const names = requiredExtensionsForFormat(glInternalFormat);
|
|
232
|
+
const len = names.length;
|
|
233
|
+
if (len === 0) {
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
for (let i = 0; i < len; i++) {
|
|
237
|
+
if (glw.getExtension(names[i]!) !== null) {
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
throw new Error(
|
|
242
|
+
`Compressed texture format 0x${glInternalFormat.toString(
|
|
243
|
+
16,
|
|
244
|
+
)} is not supported by this device (requires ${names.join(' or ')})`,
|
|
245
|
+
);
|
|
246
|
+
};
|
|
247
|
+
|
|
174
248
|
const uploadASTC = function (
|
|
175
249
|
glw: WebGlContextWrapper,
|
|
176
250
|
texture: WebGLTexture,
|
|
177
251
|
data: CompressedData,
|
|
178
252
|
) {
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
}
|
|
253
|
+
const { glInternalFormat, mipmaps, w, h } = data;
|
|
254
|
+
ensureCompressedFormatEnabled(glw, glInternalFormat);
|
|
182
255
|
|
|
183
256
|
glw.bindTexture(texture);
|
|
184
257
|
|
|
185
|
-
const { glInternalFormat, mipmaps, w, h } = data;
|
|
186
258
|
if (mipmaps === undefined) {
|
|
187
259
|
return;
|
|
188
260
|
}
|
|
@@ -277,6 +349,7 @@ const uploadKTX = function (
|
|
|
277
349
|
data: CompressedData,
|
|
278
350
|
) {
|
|
279
351
|
const { glInternalFormat, mipmaps, w: width, h: height, blockInfo } = data;
|
|
352
|
+
ensureCompressedFormatEnabled(glw, glInternalFormat);
|
|
280
353
|
if (mipmaps === undefined) {
|
|
281
354
|
return;
|
|
282
355
|
}
|
|
@@ -415,6 +488,7 @@ const uploadPVR = function (
|
|
|
415
488
|
data: CompressedData,
|
|
416
489
|
) {
|
|
417
490
|
const { glInternalFormat, mipmaps, w: width, h: height } = data;
|
|
491
|
+
ensureCompressedFormatEnabled(glw, glInternalFormat);
|
|
418
492
|
if (mipmaps === undefined) {
|
|
419
493
|
return;
|
|
420
494
|
}
|
|
@@ -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;
|
|
@@ -48,6 +48,19 @@ const GL_OUT_OF_MEMORY = 0x0505;
|
|
|
48
48
|
*/
|
|
49
49
|
const MAX_DRAINED_GL_ERRORS = 8;
|
|
50
50
|
|
|
51
|
+
/**
|
|
52
|
+
* Dirty-ratio cutoff that flips the per-frame quad upload from surgical
|
|
53
|
+
* `bufferSubData` (one call per changed node) to a single full `bufferData`.
|
|
54
|
+
*
|
|
55
|
+
* The surgical path wins when few nodes change per frame (typical UI), but
|
|
56
|
+
* degrades to one GL call per node when most of the scene moves at once
|
|
57
|
+
* (full-screen animation, scroll, zoom). At that point a single bulk upload is
|
|
58
|
+
* cheaper than N driver round-trips, so when the number of nodes we would
|
|
59
|
+
* `bufferSubData` exceeds this fraction of the render list we upload everything
|
|
60
|
+
* in one call instead. Range 0..1; ~0.4 balances the two regimes.
|
|
61
|
+
*/
|
|
62
|
+
const FULL_UPLOAD_DIRTY_RATIO = 0.4;
|
|
63
|
+
|
|
51
64
|
export type WebGlRenderOp = CoreNode | SdfRenderOp;
|
|
52
65
|
|
|
53
66
|
export class WebGlRenderer extends CoreRenderer {
|
|
@@ -161,6 +174,14 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
161
174
|
* capacity, requiring a full re-upload even when needsFullUpload is false.
|
|
162
175
|
*/
|
|
163
176
|
lastUploadedBufferSize = 0;
|
|
177
|
+
/**
|
|
178
|
+
* Count of main-scene nodes whose quad data changed this frame and which
|
|
179
|
+
* own a buffer slot. Accumulated for free during the addQuad pass (which
|
|
180
|
+
* already branches on isQuadDirty) and consumed in render() to choose
|
|
181
|
+
* between surgical bufferSubData uploads and a single full bufferData,
|
|
182
|
+
* avoiding a separate counting loop. Reset each frame in reset().
|
|
183
|
+
*/
|
|
184
|
+
dirtyQuadCount = 0;
|
|
164
185
|
/**
|
|
165
186
|
* Whether the renderer is currently rendering to a texture.
|
|
166
187
|
*/
|
|
@@ -187,7 +208,10 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
187
208
|
options.forceWebGL2,
|
|
188
209
|
options.contextSpy,
|
|
189
210
|
);
|
|
190
|
-
const glw = (this.glw = new WebGlContextWrapper(
|
|
211
|
+
const glw = (this.glw = new WebGlContextWrapper(
|
|
212
|
+
gl,
|
|
213
|
+
options.disableVertexArrayObject,
|
|
214
|
+
));
|
|
191
215
|
glw.viewport(0, 0, options.canvas.width, options.canvas.height);
|
|
192
216
|
|
|
193
217
|
this.attachContextLossListeners(options.canvas);
|
|
@@ -346,6 +370,7 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
346
370
|
}
|
|
347
371
|
this.curRenderOp = null;
|
|
348
372
|
this.curSdfRenderOp = null;
|
|
373
|
+
this.dirtyQuadCount = 0;
|
|
349
374
|
this.sdfBufferIdx = 0;
|
|
350
375
|
this.sdfQuadCount = 0;
|
|
351
376
|
this.renderOps.length = 0;
|
|
@@ -446,7 +471,12 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
446
471
|
}
|
|
447
472
|
|
|
448
473
|
const props = node.props;
|
|
449
|
-
|
|
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!;
|
|
450
480
|
|
|
451
481
|
if (tx.type === TextureType.subTexture) {
|
|
452
482
|
tx = (tx as SubTexture).parentTexture;
|
|
@@ -496,6 +526,14 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
496
526
|
// The GPU upload is deferred to render().
|
|
497
527
|
// During RTT, always write since the buffer is rebuilt from scratch.
|
|
498
528
|
if (!DIRTY_QUAD_BUFFER || isRTT || node.isQuadDirty) {
|
|
529
|
+
// Count main-scene dirty nodes here, while we already have the node in
|
|
530
|
+
// hand, so render() can pick full vs. surgical upload without a second
|
|
531
|
+
// pass over the render list. Slot is guaranteed assigned at this point
|
|
532
|
+
// (i = quadBufferIndex above), so no quadBufferIndex !== -1 guard needed.
|
|
533
|
+
if (DIRTY_QUAD_BUFFER && isRTT === false && node.isQuadDirty === true) {
|
|
534
|
+
this.dirtyQuadCount++;
|
|
535
|
+
}
|
|
536
|
+
|
|
499
537
|
const rc = node.renderCoords!;
|
|
500
538
|
const tc = node.textureCoords || this.defaultTextureCoords;
|
|
501
539
|
|
|
@@ -966,31 +1004,41 @@ export class WebGlRenderer extends CoreRenderer {
|
|
|
966
1004
|
const BYTES = Float32Array.BYTES_PER_ELEMENT;
|
|
967
1005
|
|
|
968
1006
|
if (DIRTY_QUAD_BUFFER) {
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
1007
|
+
const renderList = this.stage.renderList;
|
|
1008
|
+
const len = renderList.length;
|
|
1009
|
+
|
|
1010
|
+
// Growth/realloc always forces a full upload (new nodes, structural
|
|
1011
|
+
// reorders, or curBufferIdx grown past the last uploaded size).
|
|
1012
|
+
let fullUpload =
|
|
1013
|
+
this.needsFullUpload || this.curBufferIdx > this.lastUploadedBufferSize;
|
|
1014
|
+
|
|
1015
|
+
// Otherwise decide adaptively: if the number of nodes we would upload
|
|
1016
|
+
// surgically exceeds FULL_UPLOAD_DIRTY_RATIO of the render list, a single
|
|
1017
|
+
// bufferData is cheaper than that many bufferSubData calls. The count was
|
|
1018
|
+
// accumulated for free during the addQuad pass (dirtyQuadCount), so no
|
|
1019
|
+
// separate counting loop is needed here.
|
|
1020
|
+
if (fullUpload === false) {
|
|
1021
|
+
fullUpload = this.dirtyQuadCount > len * FULL_UPLOAD_DIRTY_RATIO;
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
if (fullUpload === true) {
|
|
1025
|
+
// Full GPU re-allocation: covers the growth/realloc cases above and the
|
|
1026
|
+
// "most of the scene changed" case where one bulk upload beats N
|
|
1027
|
+
// per-node uploads. Uses DYNAMIC_DRAW to signal to the driver that the
|
|
1028
|
+
// buffer will be updated frequently going forward.
|
|
979
1029
|
const arr = new Float32Array(quadBuffer, 0, this.curBufferIdx);
|
|
980
1030
|
glw.arrayBufferData(buffer, arr, glw.DYNAMIC_DRAW);
|
|
981
1031
|
this.needsFullUpload = false;
|
|
982
1032
|
this.lastUploadedBufferSize = this.curBufferIdx;
|
|
983
1033
|
|
|
984
1034
|
// Clear dirty flags since we just uploaded everything.
|
|
985
|
-
|
|
986
|
-
for (let i = 0, len = renderList.length; i < len; i++) {
|
|
1035
|
+
for (let i = 0; i < len; i++) {
|
|
987
1036
|
renderList[i]!.isQuadDirty = false;
|
|
988
1037
|
}
|
|
989
1038
|
} else {
|
|
990
1039
|
// Surgical per-node uploads: only write the 20 float32s for nodes
|
|
991
1040
|
// whose quad data changed since the last frame.
|
|
992
|
-
|
|
993
|
-
for (let i = 0, len = renderList.length; i < len; i++) {
|
|
1041
|
+
for (let i = 0; i < len; i++) {
|
|
994
1042
|
const node = renderList[i]!;
|
|
995
1043
|
if (node.isQuadDirty && node.quadBufferIndex !== -1) {
|
|
996
1044
|
const byteOffset = node.quadBufferIndex * BYTES;
|
|
@@ -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]!;
|