@aelionsdk/renderer-worker 0.1.0-beta.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/LICENSE +21 -0
- package/README.md +7 -0
- package/dist/client.d.ts +54 -0
- package/dist/client.d.ts.map +1 -0
- package/dist/client.js +264 -0
- package/dist/font-manager.d.ts +30 -0
- package/dist/font-manager.d.ts.map +1 -0
- package/dist/font-manager.js +87 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/ir-renderer.d.ts +62 -0
- package/dist/ir-renderer.d.ts.map +1 -0
- package/dist/ir-renderer.js +1163 -0
- package/dist/protocol.d.ts +117 -0
- package/dist/protocol.d.ts.map +1 -0
- package/dist/protocol.js +1 -0
- package/dist/webgl2-worker.d.ts +2 -0
- package/dist/webgl2-worker.d.ts.map +1 -0
- package/dist/webgl2-worker.js +1100 -0
- package/package.json +45 -0
|
@@ -0,0 +1,1163 @@
|
|
|
1
|
+
import { AelionError, throwIfAborted } from '@aelionsdk/core';
|
|
2
|
+
import { evaluateMaterialInstance, evaluateAnimatedValue, evaluateVisualState, layoutIrText, LOCAL_RGBA8_COLOR_CAPABILITY, preflightColorPipeline, } from '@aelionsdk/render-ir';
|
|
3
|
+
import { WorkerCompositor, } from './client.js';
|
|
4
|
+
function linkAbortSignals(first, second) {
|
|
5
|
+
if (first === undefined)
|
|
6
|
+
return { signal: second, detach: () => undefined };
|
|
7
|
+
const controller = new AbortController();
|
|
8
|
+
const abortFromFirst = () => controller.abort(first.reason);
|
|
9
|
+
const abortFromSecond = () => controller.abort(second.reason);
|
|
10
|
+
if (first.aborted)
|
|
11
|
+
abortFromFirst();
|
|
12
|
+
else if (second.aborted)
|
|
13
|
+
abortFromSecond();
|
|
14
|
+
else {
|
|
15
|
+
first.addEventListener('abort', abortFromFirst, { once: true });
|
|
16
|
+
second.addEventListener('abort', abortFromSecond, { once: true });
|
|
17
|
+
}
|
|
18
|
+
return {
|
|
19
|
+
signal: controller.signal,
|
|
20
|
+
detach: () => {
|
|
21
|
+
first.removeEventListener('abort', abortFromFirst);
|
|
22
|
+
second.removeEventListener('abort', abortFromSecond);
|
|
23
|
+
},
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
function percentile95(values) {
|
|
27
|
+
if (values.length === 0)
|
|
28
|
+
return null;
|
|
29
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
30
|
+
return sorted[Math.min(sorted.length - 1, Math.ceil(sorted.length * 0.95) - 1)] ?? null;
|
|
31
|
+
}
|
|
32
|
+
class AdaptiveBackendSelector {
|
|
33
|
+
#samples = {
|
|
34
|
+
webgpu: [],
|
|
35
|
+
webgl2: [],
|
|
36
|
+
};
|
|
37
|
+
#selectionCount = 0;
|
|
38
|
+
#webgpuCooldownUntil = 0;
|
|
39
|
+
select(requested = 'auto') {
|
|
40
|
+
if (requested !== 'auto')
|
|
41
|
+
return requested;
|
|
42
|
+
this.#selectionCount += 1;
|
|
43
|
+
return this.#selected();
|
|
44
|
+
}
|
|
45
|
+
record(requested, result) {
|
|
46
|
+
const samples = this.#samples[result.backend];
|
|
47
|
+
samples.push(result.timing.totalWorkerUs);
|
|
48
|
+
if (samples.length > 32)
|
|
49
|
+
samples.shift();
|
|
50
|
+
if (requested === 'webgpu' && result.backend !== 'webgpu') {
|
|
51
|
+
this.#webgpuCooldownUntil = this.#selectionCount + 60;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
snapshot() {
|
|
55
|
+
return {
|
|
56
|
+
selected: this.#selected(),
|
|
57
|
+
webgpuSamples: this.#samples.webgpu.length,
|
|
58
|
+
webgl2Samples: this.#samples.webgl2.length,
|
|
59
|
+
webgpuP95Us: percentile95(this.#samples.webgpu),
|
|
60
|
+
webgl2P95Us: percentile95(this.#samples.webgl2),
|
|
61
|
+
webgpuCooldownFrames: Math.max(0, this.#webgpuCooldownUntil - this.#selectionCount),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
#selected() {
|
|
65
|
+
if (this.#selectionCount < this.#webgpuCooldownUntil)
|
|
66
|
+
return 'webgl2';
|
|
67
|
+
if (this.#samples.webgl2.length < 3)
|
|
68
|
+
return 'webgl2';
|
|
69
|
+
if (this.#samples.webgpu.length < 3)
|
|
70
|
+
return 'webgpu';
|
|
71
|
+
const webgpuP95 = percentile95(this.#samples.webgpu) ?? Number.POSITIVE_INFINITY;
|
|
72
|
+
const webgl2P95 = percentile95(this.#samples.webgl2) ?? Number.POSITIVE_INFINITY;
|
|
73
|
+
return webgpuP95 < webgl2P95 ? 'webgpu' : 'webgl2';
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
function requiredProgram(material, mode) {
|
|
77
|
+
if (material.program !== undefined)
|
|
78
|
+
return material.program;
|
|
79
|
+
if (mode === 'preview' && material.previewPolicy === 'skippable-when-degraded')
|
|
80
|
+
return undefined;
|
|
81
|
+
throw new Error(`Material ${material.id} has no executable backend`);
|
|
82
|
+
}
|
|
83
|
+
/** Converts an owned compositor result and releases the source on every path. */
|
|
84
|
+
function takeBitmapFrame(bitmap, timestampUs) {
|
|
85
|
+
try {
|
|
86
|
+
return new VideoFrame(bitmap, { timestamp: timestampUs });
|
|
87
|
+
}
|
|
88
|
+
finally {
|
|
89
|
+
bitmap.close();
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
async function presentationBitmap(frame, signal) {
|
|
93
|
+
// A WebGL canvas exports premultiplied RGBA. Normalizing the public bitmap to
|
|
94
|
+
// straight alpha prevents some headless Chromium/ANGLE paths from applying
|
|
95
|
+
// alpha a second time when callers present it through Canvas 2D.
|
|
96
|
+
const bitmap = await createImageBitmap(frame, { premultiplyAlpha: 'none' });
|
|
97
|
+
try {
|
|
98
|
+
throwIfAborted(signal, 'Render IR presentation');
|
|
99
|
+
return bitmap;
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
bitmap.close();
|
|
103
|
+
throw error;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
const BASE_VISUAL_PROGRAM = {
|
|
107
|
+
backend: 'webgl2',
|
|
108
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
109
|
+
graphHash: 'builtin-visual-transform-v1',
|
|
110
|
+
inputPorts: ['source'],
|
|
111
|
+
uniforms: [
|
|
112
|
+
'positionX',
|
|
113
|
+
'positionY',
|
|
114
|
+
'anchorX',
|
|
115
|
+
'anchorY',
|
|
116
|
+
'scaleX',
|
|
117
|
+
'scaleY',
|
|
118
|
+
'rotationRad',
|
|
119
|
+
'opacity',
|
|
120
|
+
'outputWidth',
|
|
121
|
+
'outputHeight',
|
|
122
|
+
'cropLeft',
|
|
123
|
+
'cropTop',
|
|
124
|
+
'cropRight',
|
|
125
|
+
'cropBottom',
|
|
126
|
+
].map(id => ({
|
|
127
|
+
name: `u_parameter_${id}`,
|
|
128
|
+
type: 'float',
|
|
129
|
+
source: { kind: 'parameter', id },
|
|
130
|
+
})),
|
|
131
|
+
executionPlan: {
|
|
132
|
+
passes: [
|
|
133
|
+
{
|
|
134
|
+
id: 'builtin-visual-transform',
|
|
135
|
+
kind: 'draw',
|
|
136
|
+
nodes: ['builtin-visual-transform'],
|
|
137
|
+
estimatedTextureSamples: 1,
|
|
138
|
+
},
|
|
139
|
+
],
|
|
140
|
+
intermediateTextureCount: 0,
|
|
141
|
+
},
|
|
142
|
+
fragmentShader: `#version 300 es
|
|
143
|
+
precision highp float;
|
|
144
|
+
uniform sampler2D u_input_source;
|
|
145
|
+
uniform float u_parameter_positionX;
|
|
146
|
+
uniform float u_parameter_positionY;
|
|
147
|
+
uniform float u_parameter_anchorX;
|
|
148
|
+
uniform float u_parameter_anchorY;
|
|
149
|
+
uniform float u_parameter_scaleX;
|
|
150
|
+
uniform float u_parameter_scaleY;
|
|
151
|
+
uniform float u_parameter_rotationRad;
|
|
152
|
+
uniform float u_parameter_opacity;
|
|
153
|
+
uniform float u_parameter_outputWidth;
|
|
154
|
+
uniform float u_parameter_outputHeight;
|
|
155
|
+
uniform float u_parameter_cropLeft;
|
|
156
|
+
uniform float u_parameter_cropTop;
|
|
157
|
+
uniform float u_parameter_cropRight;
|
|
158
|
+
uniform float u_parameter_cropBottom;
|
|
159
|
+
in vec2 v_uv;
|
|
160
|
+
out vec4 out_color;
|
|
161
|
+
void main() {
|
|
162
|
+
vec2 position = vec2(
|
|
163
|
+
u_parameter_positionX / u_parameter_outputWidth,
|
|
164
|
+
u_parameter_positionY / u_parameter_outputHeight
|
|
165
|
+
);
|
|
166
|
+
vec2 offset = v_uv - position;
|
|
167
|
+
float c = cos(-u_parameter_rotationRad);
|
|
168
|
+
float s = sin(-u_parameter_rotationRad);
|
|
169
|
+
vec2 rotated = mat2(c, -s, s, c) * offset;
|
|
170
|
+
vec2 sourceUv = rotated / vec2(u_parameter_scaleX, u_parameter_scaleY)
|
|
171
|
+
+ vec2(u_parameter_anchorX, u_parameter_anchorY);
|
|
172
|
+
vec2 cropMin = vec2(u_parameter_cropLeft, u_parameter_cropTop);
|
|
173
|
+
vec2 cropMax = vec2(1.0 - u_parameter_cropRight, 1.0 - u_parameter_cropBottom);
|
|
174
|
+
if (any(lessThan(sourceUv, cropMin)) || any(greaterThan(sourceUv, cropMax))) {
|
|
175
|
+
out_color = vec4(0.0);
|
|
176
|
+
} else {
|
|
177
|
+
out_color = texture(u_input_source, sourceUv) * u_parameter_opacity;
|
|
178
|
+
}
|
|
179
|
+
}`,
|
|
180
|
+
webgpu: {
|
|
181
|
+
backend: 'webgpu',
|
|
182
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
183
|
+
graphHash: 'builtin-visual-transform-v1',
|
|
184
|
+
inputPorts: ['source'],
|
|
185
|
+
uniforms: [
|
|
186
|
+
'positionX',
|
|
187
|
+
'positionY',
|
|
188
|
+
'anchorX',
|
|
189
|
+
'anchorY',
|
|
190
|
+
'scaleX',
|
|
191
|
+
'scaleY',
|
|
192
|
+
'rotationRad',
|
|
193
|
+
'opacity',
|
|
194
|
+
'outputWidth',
|
|
195
|
+
'outputHeight',
|
|
196
|
+
'cropLeft',
|
|
197
|
+
'cropTop',
|
|
198
|
+
'cropRight',
|
|
199
|
+
'cropBottom',
|
|
200
|
+
].map(id => ({
|
|
201
|
+
name: `u_parameter_${id}`,
|
|
202
|
+
type: 'float',
|
|
203
|
+
source: { kind: 'parameter', id },
|
|
204
|
+
})),
|
|
205
|
+
executionPlan: {
|
|
206
|
+
passes: [
|
|
207
|
+
{
|
|
208
|
+
id: 'builtin-visual-transform',
|
|
209
|
+
kind: 'draw',
|
|
210
|
+
nodes: ['builtin-visual-transform'],
|
|
211
|
+
estimatedTextureSamples: 1,
|
|
212
|
+
},
|
|
213
|
+
],
|
|
214
|
+
intermediateTextureCount: 0,
|
|
215
|
+
},
|
|
216
|
+
shader: `
|
|
217
|
+
struct Uniforms { values: array<vec4f, 14> };
|
|
218
|
+
@group(0) @binding(0) var source_sampler: sampler;
|
|
219
|
+
@group(0) @binding(1) var input_source: texture_2d<f32>;
|
|
220
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
221
|
+
struct VertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
222
|
+
@vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
223
|
+
var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
224
|
+
var uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
|
|
225
|
+
return VertexOut(vec4f(positions[index], 0.0, 1.0), uvs[index]);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
@fragment fn fs(vertex: VertexOut) -> @location(0) vec4f {
|
|
229
|
+
let position = vec2f(
|
|
230
|
+
uniforms.values[0].x / uniforms.values[8].x,
|
|
231
|
+
uniforms.values[1].x / uniforms.values[9].x
|
|
232
|
+
);
|
|
233
|
+
let offset = vertex.uv - position;
|
|
234
|
+
let c = cos(-uniforms.values[6].x);
|
|
235
|
+
let s = sin(-uniforms.values[6].x);
|
|
236
|
+
let rotated = mat2x2f(c, -s, s, c) * offset;
|
|
237
|
+
let source_uv = rotated / vec2f(uniforms.values[4].x, uniforms.values[5].x)
|
|
238
|
+
+ vec2f(uniforms.values[2].x, uniforms.values[3].x);
|
|
239
|
+
let crop_min = vec2f(uniforms.values[10].x, uniforms.values[11].x);
|
|
240
|
+
let crop_max = vec2f(1.0 - uniforms.values[12].x, 1.0 - uniforms.values[13].x);
|
|
241
|
+
let sampled = textureSample(input_source, source_sampler, source_uv);
|
|
242
|
+
if (any(source_uv < crop_min) || any(source_uv > crop_max)) {
|
|
243
|
+
return vec4f(0.0);
|
|
244
|
+
}
|
|
245
|
+
return sampled * uniforms.values[7].x;
|
|
246
|
+
}`,
|
|
247
|
+
},
|
|
248
|
+
};
|
|
249
|
+
const COPY_PROGRAM = {
|
|
250
|
+
backend: 'webgl2',
|
|
251
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
252
|
+
graphHash: 'builtin-copy-v1',
|
|
253
|
+
inputPorts: ['source'],
|
|
254
|
+
uniforms: [],
|
|
255
|
+
executionPlan: {
|
|
256
|
+
passes: [
|
|
257
|
+
{
|
|
258
|
+
id: 'builtin-copy',
|
|
259
|
+
kind: 'draw',
|
|
260
|
+
nodes: ['builtin-copy'],
|
|
261
|
+
estimatedTextureSamples: 1,
|
|
262
|
+
},
|
|
263
|
+
],
|
|
264
|
+
intermediateTextureCount: 0,
|
|
265
|
+
},
|
|
266
|
+
fragmentShader: `#version 300 es
|
|
267
|
+
precision highp float;
|
|
268
|
+
uniform sampler2D u_input_source;
|
|
269
|
+
in vec2 v_uv;
|
|
270
|
+
out vec4 out_color;
|
|
271
|
+
void main() {
|
|
272
|
+
out_color = texture(u_input_source, v_uv);
|
|
273
|
+
}`,
|
|
274
|
+
webgpu: {
|
|
275
|
+
backend: 'webgpu',
|
|
276
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
277
|
+
graphHash: 'builtin-copy-v1',
|
|
278
|
+
inputPorts: ['source'],
|
|
279
|
+
uniforms: [],
|
|
280
|
+
executionPlan: {
|
|
281
|
+
passes: [
|
|
282
|
+
{
|
|
283
|
+
id: 'builtin-copy',
|
|
284
|
+
kind: 'draw',
|
|
285
|
+
nodes: ['builtin-copy'],
|
|
286
|
+
estimatedTextureSamples: 1,
|
|
287
|
+
},
|
|
288
|
+
],
|
|
289
|
+
intermediateTextureCount: 0,
|
|
290
|
+
},
|
|
291
|
+
shader: `
|
|
292
|
+
struct Uniforms { values: array<vec4f, 1> };
|
|
293
|
+
@group(0) @binding(0) var source_sampler: sampler;
|
|
294
|
+
@group(0) @binding(1) var input_source: texture_2d<f32>;
|
|
295
|
+
@group(0) @binding(2) var<uniform> uniforms: Uniforms;
|
|
296
|
+
struct VertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
297
|
+
@vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
298
|
+
var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
299
|
+
var uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
|
|
300
|
+
return VertexOut(vec4f(positions[index], 0.0, 1.0), uvs[index]);
|
|
301
|
+
}
|
|
302
|
+
@fragment fn fs(vertex: VertexOut) -> @location(0) vec4f {
|
|
303
|
+
return textureSample(input_source, source_sampler, vertex.uv)
|
|
304
|
+
+ vec4f(uniforms.values[0].x * 0.0);
|
|
305
|
+
}`,
|
|
306
|
+
},
|
|
307
|
+
};
|
|
308
|
+
const BLEND_PROGRAM = {
|
|
309
|
+
backend: 'webgl2',
|
|
310
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
311
|
+
graphHash: 'builtin-blend-v1',
|
|
312
|
+
inputPorts: ['base', 'overlay'],
|
|
313
|
+
uniforms: [
|
|
314
|
+
{
|
|
315
|
+
name: 'u_parameter_blendMode',
|
|
316
|
+
type: 'float',
|
|
317
|
+
source: { kind: 'parameter', id: 'blendMode' },
|
|
318
|
+
},
|
|
319
|
+
],
|
|
320
|
+
executionPlan: {
|
|
321
|
+
passes: [
|
|
322
|
+
{
|
|
323
|
+
id: 'builtin-blend',
|
|
324
|
+
kind: 'draw',
|
|
325
|
+
nodes: ['builtin-blend'],
|
|
326
|
+
estimatedTextureSamples: 2,
|
|
327
|
+
},
|
|
328
|
+
],
|
|
329
|
+
intermediateTextureCount: 0,
|
|
330
|
+
},
|
|
331
|
+
fragmentShader: `#version 300 es
|
|
332
|
+
precision highp float;
|
|
333
|
+
uniform sampler2D u_input_base;
|
|
334
|
+
uniform sampler2D u_input_overlay;
|
|
335
|
+
uniform float u_parameter_blendMode;
|
|
336
|
+
in vec2 v_uv;
|
|
337
|
+
out vec4 out_color;
|
|
338
|
+
vec3 blendColor(vec3 b, vec3 s, int mode) {
|
|
339
|
+
if (mode == 1) return b * s;
|
|
340
|
+
if (mode == 2) return 1.0 - (1.0 - b) * (1.0 - s);
|
|
341
|
+
if (mode == 3) return mix(2.0 * b * s, 1.0 - 2.0 * (1.0 - b) * (1.0 - s), step(0.5, b));
|
|
342
|
+
if (mode == 4) return min(b, s);
|
|
343
|
+
if (mode == 5) return max(b, s);
|
|
344
|
+
if (mode == 6) return min(vec3(1.0), b / max(vec3(0.00001), 1.0 - s));
|
|
345
|
+
if (mode == 7) return 1.0 - min(vec3(1.0), (1.0 - b) / max(vec3(0.00001), s));
|
|
346
|
+
if (mode == 8) return mix(2.0 * b * s, 1.0 - 2.0 * (1.0 - b) * (1.0 - s), step(0.5, s));
|
|
347
|
+
if (mode == 9) {
|
|
348
|
+
vec3 d = mix(((16.0 * b - 12.0) * b + 4.0) * b, sqrt(max(b, vec3(0.0))), step(0.25, b));
|
|
349
|
+
return mix(b - (1.0 - 2.0 * s) * b * (1.0 - b), b + (2.0 * s - 1.0) * (d - b), step(0.5, s));
|
|
350
|
+
}
|
|
351
|
+
if (mode == 10) return abs(b - s);
|
|
352
|
+
if (mode == 11) return b + s - 2.0 * b * s;
|
|
353
|
+
return s;
|
|
354
|
+
}
|
|
355
|
+
void main() {
|
|
356
|
+
vec4 base = texture(u_input_base, v_uv);
|
|
357
|
+
vec4 overlay = texture(u_input_overlay, v_uv);
|
|
358
|
+
vec3 b = base.a > 0.0 ? base.rgb / base.a : vec3(0.0);
|
|
359
|
+
vec3 s = overlay.a > 0.0 ? overlay.rgb / overlay.a : vec3(0.0);
|
|
360
|
+
vec3 blended = blendColor(b, s, int(floor(u_parameter_blendMode + 0.5)));
|
|
361
|
+
out_color = vec4(
|
|
362
|
+
(1.0 - overlay.a) * base.rgb + (1.0 - base.a) * overlay.rgb + base.a * overlay.a * blended,
|
|
363
|
+
overlay.a + base.a * (1.0 - overlay.a)
|
|
364
|
+
);
|
|
365
|
+
}`,
|
|
366
|
+
webgpu: {
|
|
367
|
+
backend: 'webgpu',
|
|
368
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
369
|
+
graphHash: 'builtin-blend-v1',
|
|
370
|
+
inputPorts: ['base', 'overlay'],
|
|
371
|
+
uniforms: [
|
|
372
|
+
{
|
|
373
|
+
name: 'u_parameter_blendMode',
|
|
374
|
+
type: 'float',
|
|
375
|
+
source: { kind: 'parameter', id: 'blendMode' },
|
|
376
|
+
},
|
|
377
|
+
],
|
|
378
|
+
executionPlan: {
|
|
379
|
+
passes: [
|
|
380
|
+
{
|
|
381
|
+
id: 'builtin-blend',
|
|
382
|
+
kind: 'draw',
|
|
383
|
+
nodes: ['builtin-blend'],
|
|
384
|
+
estimatedTextureSamples: 2,
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
intermediateTextureCount: 0,
|
|
388
|
+
},
|
|
389
|
+
shader: `
|
|
390
|
+
struct Uniforms { values: array<vec4f, 1> };
|
|
391
|
+
@group(0) @binding(0) var source_sampler: sampler;
|
|
392
|
+
@group(0) @binding(1) var input_base: texture_2d<f32>;
|
|
393
|
+
@group(0) @binding(2) var input_overlay: texture_2d<f32>;
|
|
394
|
+
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
|
395
|
+
struct VertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
396
|
+
@vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
397
|
+
var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
398
|
+
var uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
|
|
399
|
+
return VertexOut(vec4f(positions[index], 0.0, 1.0), uvs[index]);
|
|
400
|
+
}
|
|
401
|
+
fn blendColor(b: vec3f, s: vec3f, mode: i32) -> vec3f {
|
|
402
|
+
if (mode == 1) { return b * s; }
|
|
403
|
+
if (mode == 2) { return vec3f(1.0) - (vec3f(1.0) - b) * (vec3f(1.0) - s); }
|
|
404
|
+
if (mode == 3) { return mix(2.0 * b * s, vec3f(1.0) - 2.0 * (vec3f(1.0) - b) * (vec3f(1.0) - s), step(vec3f(0.5), b)); }
|
|
405
|
+
if (mode == 4) { return min(b, s); }
|
|
406
|
+
if (mode == 5) { return max(b, s); }
|
|
407
|
+
if (mode == 6) { return min(vec3f(1.0), b / max(vec3f(0.00001), vec3f(1.0) - s)); }
|
|
408
|
+
if (mode == 7) { return vec3f(1.0) - min(vec3f(1.0), (vec3f(1.0) - b) / max(vec3f(0.00001), s)); }
|
|
409
|
+
if (mode == 8) { return mix(2.0 * b * s, vec3f(1.0) - 2.0 * (vec3f(1.0) - b) * (vec3f(1.0) - s), step(vec3f(0.5), s)); }
|
|
410
|
+
if (mode == 9) {
|
|
411
|
+
let d = mix(((16.0 * b - vec3f(12.0)) * b + vec3f(4.0)) * b, sqrt(max(b, vec3f(0.0))), step(vec3f(0.25), b));
|
|
412
|
+
return mix(b - (vec3f(1.0) - 2.0 * s) * b * (vec3f(1.0) - b), b + (2.0 * s - vec3f(1.0)) * (d - b), step(vec3f(0.5), s));
|
|
413
|
+
}
|
|
414
|
+
if (mode == 10) { return abs(b - s); }
|
|
415
|
+
if (mode == 11) { return b + s - 2.0 * b * s; }
|
|
416
|
+
return s;
|
|
417
|
+
}
|
|
418
|
+
@fragment fn fs(vertex: VertexOut) -> @location(0) vec4f {
|
|
419
|
+
let base = textureSample(input_base, source_sampler, vertex.uv);
|
|
420
|
+
let overlay = textureSample(input_overlay, source_sampler, vertex.uv);
|
|
421
|
+
let b = select(vec3f(0.0), base.rgb / base.a, base.a > 0.0);
|
|
422
|
+
let s = select(vec3f(0.0), overlay.rgb / overlay.a, overlay.a > 0.0);
|
|
423
|
+
let blended = blendColor(b, s, i32(floor(uniforms.values[0].x + 0.5)));
|
|
424
|
+
return vec4f(
|
|
425
|
+
(1.0 - overlay.a) * base.rgb + (1.0 - base.a) * overlay.rgb + base.a * overlay.a * blended,
|
|
426
|
+
overlay.a + base.a * (1.0 - overlay.a)
|
|
427
|
+
);
|
|
428
|
+
}
|
|
429
|
+
`,
|
|
430
|
+
},
|
|
431
|
+
};
|
|
432
|
+
const BLEND_MODE_CODES = {
|
|
433
|
+
normal: 0,
|
|
434
|
+
multiply: 1,
|
|
435
|
+
screen: 2,
|
|
436
|
+
overlay: 3,
|
|
437
|
+
darken: 4,
|
|
438
|
+
lighten: 5,
|
|
439
|
+
'color-dodge': 6,
|
|
440
|
+
'color-burn': 7,
|
|
441
|
+
'hard-light': 8,
|
|
442
|
+
'soft-light': 9,
|
|
443
|
+
difference: 10,
|
|
444
|
+
exclusion: 11,
|
|
445
|
+
};
|
|
446
|
+
const MASK_PROGRAM = {
|
|
447
|
+
backend: 'webgl2',
|
|
448
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
449
|
+
graphHash: 'builtin-mask-v1',
|
|
450
|
+
inputPorts: ['source', 'mask'],
|
|
451
|
+
uniforms: ['maskMode', 'invert', 'featherUvX', 'featherUvY'].map(id => ({
|
|
452
|
+
name: `u_parameter_${id}`,
|
|
453
|
+
type: 'float',
|
|
454
|
+
source: { kind: 'parameter', id },
|
|
455
|
+
})),
|
|
456
|
+
executionPlan: {
|
|
457
|
+
passes: [
|
|
458
|
+
{
|
|
459
|
+
id: 'builtin-mask',
|
|
460
|
+
kind: 'draw',
|
|
461
|
+
nodes: ['builtin-mask'],
|
|
462
|
+
estimatedTextureSamples: 10,
|
|
463
|
+
},
|
|
464
|
+
],
|
|
465
|
+
intermediateTextureCount: 0,
|
|
466
|
+
},
|
|
467
|
+
fragmentShader: `#version 300 es
|
|
468
|
+
precision highp float;
|
|
469
|
+
uniform sampler2D u_input_source;
|
|
470
|
+
uniform sampler2D u_input_mask;
|
|
471
|
+
uniform float u_parameter_maskMode;
|
|
472
|
+
uniform float u_parameter_invert;
|
|
473
|
+
uniform float u_parameter_featherUvX;
|
|
474
|
+
uniform float u_parameter_featherUvY;
|
|
475
|
+
in vec2 v_uv;
|
|
476
|
+
out vec4 out_color;
|
|
477
|
+
float maskValue(vec2 uv) {
|
|
478
|
+
vec4 value = texture(u_input_mask, uv);
|
|
479
|
+
return u_parameter_maskMode < 0.5 ? value.a : dot(value.rgb, vec3(0.2126, 0.7152, 0.0722));
|
|
480
|
+
}
|
|
481
|
+
void main() {
|
|
482
|
+
vec2 radius = vec2(u_parameter_featherUvX, u_parameter_featherUvY);
|
|
483
|
+
float amount = 0.0;
|
|
484
|
+
amount += maskValue(v_uv);
|
|
485
|
+
amount += maskValue(v_uv + vec2(radius.x, 0.0));
|
|
486
|
+
amount += maskValue(v_uv - vec2(radius.x, 0.0));
|
|
487
|
+
amount += maskValue(v_uv + vec2(0.0, radius.y));
|
|
488
|
+
amount += maskValue(v_uv - vec2(0.0, radius.y));
|
|
489
|
+
amount += maskValue(v_uv + radius);
|
|
490
|
+
amount += maskValue(v_uv - radius);
|
|
491
|
+
amount += maskValue(v_uv + vec2(radius.x, -radius.y));
|
|
492
|
+
amount += maskValue(v_uv + vec2(-radius.x, radius.y));
|
|
493
|
+
amount /= 9.0;
|
|
494
|
+
if (u_parameter_invert > 0.5) amount = 1.0 - amount;
|
|
495
|
+
out_color = texture(u_input_source, v_uv) * clamp(amount, 0.0, 1.0);
|
|
496
|
+
}`,
|
|
497
|
+
webgpu: {
|
|
498
|
+
backend: 'webgpu',
|
|
499
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
500
|
+
graphHash: 'builtin-mask-v1',
|
|
501
|
+
inputPorts: ['source', 'mask'],
|
|
502
|
+
uniforms: ['maskMode', 'invert', 'featherUvX', 'featherUvY'].map(id => ({
|
|
503
|
+
name: `u_parameter_${id}`,
|
|
504
|
+
type: 'float',
|
|
505
|
+
source: { kind: 'parameter', id },
|
|
506
|
+
})),
|
|
507
|
+
executionPlan: {
|
|
508
|
+
passes: [
|
|
509
|
+
{
|
|
510
|
+
id: 'builtin-mask',
|
|
511
|
+
kind: 'draw',
|
|
512
|
+
nodes: ['builtin-mask'],
|
|
513
|
+
estimatedTextureSamples: 10,
|
|
514
|
+
},
|
|
515
|
+
],
|
|
516
|
+
intermediateTextureCount: 0,
|
|
517
|
+
},
|
|
518
|
+
shader: `
|
|
519
|
+
struct Uniforms { values: array<vec4f, 4> };
|
|
520
|
+
@group(0) @binding(0) var source_sampler: sampler;
|
|
521
|
+
@group(0) @binding(1) var input_source: texture_2d<f32>;
|
|
522
|
+
@group(0) @binding(2) var input_mask: texture_2d<f32>;
|
|
523
|
+
@group(0) @binding(3) var<uniform> uniforms: Uniforms;
|
|
524
|
+
struct VertexOut { @builtin(position) position: vec4f, @location(0) uv: vec2f };
|
|
525
|
+
@vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
526
|
+
var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
527
|
+
var uvs = array<vec2f, 3>(vec2f(0.0, 1.0), vec2f(2.0, 1.0), vec2f(0.0, -1.0));
|
|
528
|
+
return VertexOut(vec4f(positions[index], 0.0, 1.0), uvs[index]);
|
|
529
|
+
}
|
|
530
|
+
fn mask_value(uv: vec2f) -> f32 {
|
|
531
|
+
let value = textureSample(input_mask, source_sampler, uv);
|
|
532
|
+
let luma = dot(value.rgb, vec3f(0.2126, 0.7152, 0.0722));
|
|
533
|
+
return select(luma, value.a, uniforms.values[0].x < 0.5);
|
|
534
|
+
}
|
|
535
|
+
@fragment fn fs(vertex: VertexOut) -> @location(0) vec4f {
|
|
536
|
+
let radius = vec2f(uniforms.values[2].x, uniforms.values[3].x);
|
|
537
|
+
var amount = mask_value(vertex.uv);
|
|
538
|
+
amount += mask_value(vertex.uv + vec2f(radius.x, 0.0));
|
|
539
|
+
amount += mask_value(vertex.uv - vec2f(radius.x, 0.0));
|
|
540
|
+
amount += mask_value(vertex.uv + vec2f(0.0, radius.y));
|
|
541
|
+
amount += mask_value(vertex.uv - vec2f(0.0, radius.y));
|
|
542
|
+
amount += mask_value(vertex.uv + radius);
|
|
543
|
+
amount += mask_value(vertex.uv - radius);
|
|
544
|
+
amount += mask_value(vertex.uv + vec2f(radius.x, -radius.y));
|
|
545
|
+
amount += mask_value(vertex.uv + vec2f(-radius.x, radius.y));
|
|
546
|
+
amount /= 9.0;
|
|
547
|
+
let masked = select(amount, 1.0 - amount, uniforms.values[1].x > 0.5);
|
|
548
|
+
return textureSample(input_source, source_sampler, vertex.uv) * clamp(masked, 0.0, 1.0);
|
|
549
|
+
}`,
|
|
550
|
+
},
|
|
551
|
+
};
|
|
552
|
+
function blendModeCode(mode) {
|
|
553
|
+
const code = BLEND_MODE_CODES[mode];
|
|
554
|
+
if (code === undefined)
|
|
555
|
+
throw new TypeError(`BLEND_MODE_UNSUPPORTED: ${mode}`);
|
|
556
|
+
return code;
|
|
557
|
+
}
|
|
558
|
+
function record(value) {
|
|
559
|
+
return value !== null && typeof value === 'object' && !Array.isArray(value)
|
|
560
|
+
? value
|
|
561
|
+
: {};
|
|
562
|
+
}
|
|
563
|
+
function finite(value, fallback) {
|
|
564
|
+
return typeof value === 'number' && Number.isFinite(value) ? value : fallback;
|
|
565
|
+
}
|
|
566
|
+
function visualParameters(visual, projectWidth, projectHeight, outputWidth, outputHeight, sequenceTimeUs, ownerStartUs) {
|
|
567
|
+
const visualRecord = visual;
|
|
568
|
+
const transform = record(visualRecord.transform);
|
|
569
|
+
const evaluated = (value) => evaluateAnimatedValue(value, sequenceTimeUs, ownerStartUs);
|
|
570
|
+
const position = record(evaluated(transform.positionPx));
|
|
571
|
+
const anchor = record(evaluated(transform.anchor));
|
|
572
|
+
const scale = record(evaluated(transform.scale));
|
|
573
|
+
const crop = record(evaluated(visualRecord.crop));
|
|
574
|
+
const positionScaleX = outputWidth / projectWidth;
|
|
575
|
+
const positionScaleY = outputHeight / projectHeight;
|
|
576
|
+
return {
|
|
577
|
+
positionX: finite(position.x, projectWidth / 2) * positionScaleX,
|
|
578
|
+
positionY: finite(position.y, projectHeight / 2) * positionScaleY,
|
|
579
|
+
anchorX: finite(anchor.x, 0.5),
|
|
580
|
+
anchorY: finite(anchor.y, 0.5),
|
|
581
|
+
scaleX: finite(scale.x, 1),
|
|
582
|
+
scaleY: finite(scale.y, 1),
|
|
583
|
+
rotationRad: (finite(evaluated(transform.rotationDeg), 0) * Math.PI) / 180,
|
|
584
|
+
opacity: Math.max(0, Math.min(1, finite(evaluated(visualRecord.opacity), 1))),
|
|
585
|
+
outputWidth,
|
|
586
|
+
outputHeight,
|
|
587
|
+
cropLeft: finite(crop.left, 0),
|
|
588
|
+
cropTop: finite(crop.top, 0),
|
|
589
|
+
cropRight: finite(crop.right, 0),
|
|
590
|
+
cropBottom: finite(crop.bottom, 0),
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
function requiresBaseVisualPass(parameters, width, height) {
|
|
594
|
+
return (parameters.positionX !== width / 2 ||
|
|
595
|
+
parameters.positionY !== height / 2 ||
|
|
596
|
+
parameters.anchorX !== 0.5 ||
|
|
597
|
+
parameters.anchorY !== 0.5 ||
|
|
598
|
+
parameters.scaleX !== 1 ||
|
|
599
|
+
parameters.scaleY !== 1 ||
|
|
600
|
+
parameters.rotationRad !== 0 ||
|
|
601
|
+
parameters.opacity !== 1 ||
|
|
602
|
+
parameters.cropLeft !== 0 ||
|
|
603
|
+
parameters.cropTop !== 0 ||
|
|
604
|
+
parameters.cropRight !== 0 ||
|
|
605
|
+
parameters.cropBottom !== 0);
|
|
606
|
+
}
|
|
607
|
+
function canvasFont(style) {
|
|
608
|
+
const families = style.fontFamilies
|
|
609
|
+
.map(value => (/^[\w-]+$/u.test(value) ? value : `"${value.replaceAll('"', '\\"')}"`))
|
|
610
|
+
.join(', ');
|
|
611
|
+
return `${style.fontStyle} ${style.fontWeight.toString()} ${style.fontSizePx.toString()}px ${families}`;
|
|
612
|
+
}
|
|
613
|
+
function rasterTextFrame(clip, projectWidth, projectHeight, outputWidth, outputHeight, timestampUs) {
|
|
614
|
+
const canvas = new OffscreenCanvas(outputWidth, outputHeight);
|
|
615
|
+
const context = canvas.getContext('2d');
|
|
616
|
+
if (context === null)
|
|
617
|
+
throw new Error('TEXT_CANVAS_UNAVAILABLE');
|
|
618
|
+
const layout = layoutIrText(clip);
|
|
619
|
+
context.clearRect(0, 0, outputWidth, outputHeight);
|
|
620
|
+
context.save();
|
|
621
|
+
context.scale(outputWidth / projectWidth, outputHeight / projectHeight);
|
|
622
|
+
if (clip.overflow !== 'visible') {
|
|
623
|
+
context.beginPath();
|
|
624
|
+
context.rect(clip.box.x, clip.box.y, clip.box.width, clip.box.height);
|
|
625
|
+
context.clip();
|
|
626
|
+
}
|
|
627
|
+
context.textBaseline = 'top';
|
|
628
|
+
for (const line of layout.lines) {
|
|
629
|
+
for (const span of line.spans) {
|
|
630
|
+
context.font = canvasFont(span.style);
|
|
631
|
+
context.fillStyle = span.style.fill;
|
|
632
|
+
const y = line.y + Math.max(0, (line.height - span.style.lineHeightPx) / 2);
|
|
633
|
+
context.direction = span.style.direction;
|
|
634
|
+
const draw = (value, x) => {
|
|
635
|
+
if (span.style.stroke !== undefined && span.style.strokeWidthPx > 0) {
|
|
636
|
+
context.strokeStyle = span.style.stroke;
|
|
637
|
+
context.lineWidth = span.style.strokeWidthPx;
|
|
638
|
+
context.strokeText(value, x, y);
|
|
639
|
+
}
|
|
640
|
+
context.fillText(value, x, y);
|
|
641
|
+
};
|
|
642
|
+
if (span.style.direction === 'rtl') {
|
|
643
|
+
draw(span.text, span.x + span.advancePx);
|
|
644
|
+
}
|
|
645
|
+
else {
|
|
646
|
+
for (const glyph of span.glyphs)
|
|
647
|
+
draw(glyph.text, glyph.x);
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
context.restore();
|
|
652
|
+
return new VideoFrame(canvas, { timestamp: timestampUs });
|
|
653
|
+
}
|
|
654
|
+
function linearChannelToSrgb(value) {
|
|
655
|
+
const channel = Math.max(0, Math.min(1, value));
|
|
656
|
+
return channel <= 0.003_130_8 ? channel * 12.92 : 1.055 * channel ** (1 / 2.4) - 0.055;
|
|
657
|
+
}
|
|
658
|
+
function canvasColor(value, fallback = 'rgba(0, 0, 0, 0)') {
|
|
659
|
+
const color = record(value);
|
|
660
|
+
const rgba = color.rgba;
|
|
661
|
+
if (!Array.isArray(rgba) || rgba.length !== 4)
|
|
662
|
+
return fallback;
|
|
663
|
+
const values = rgba.map(value => finite(value, 0));
|
|
664
|
+
const red = values[0];
|
|
665
|
+
const green = values[1];
|
|
666
|
+
const blue = values[2];
|
|
667
|
+
const alpha = values[3];
|
|
668
|
+
if (red === undefined || green === undefined || blue === undefined || alpha === undefined) {
|
|
669
|
+
return fallback;
|
|
670
|
+
}
|
|
671
|
+
return `rgba(${Math.round(linearChannelToSrgb(red) * 255).toString()}, ${Math.round(linearChannelToSrgb(green) * 255).toString()}, ${Math.round(linearChannelToSrgb(blue) * 255).toString()}, ${Math.max(0, Math.min(1, alpha)).toString()})`;
|
|
672
|
+
}
|
|
673
|
+
function backgroundParameters(value) {
|
|
674
|
+
const rgba = record(value).rgba;
|
|
675
|
+
const components = Array.isArray(rgba) && rgba.length === 4
|
|
676
|
+
? rgba.map(component => finite(component, 0))
|
|
677
|
+
: [0, 0, 0, 0];
|
|
678
|
+
return {
|
|
679
|
+
red: linearChannelToSrgb(components[0] ?? 0),
|
|
680
|
+
green: linearChannelToSrgb(components[1] ?? 0),
|
|
681
|
+
blue: linearChannelToSrgb(components[2] ?? 0),
|
|
682
|
+
alpha: Math.max(0, Math.min(1, components[3] ?? 0)),
|
|
683
|
+
};
|
|
684
|
+
}
|
|
685
|
+
const BACKGROUND_PROGRAM = {
|
|
686
|
+
backend: 'webgl2',
|
|
687
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
688
|
+
graphHash: 'builtin-background-v1',
|
|
689
|
+
inputPorts: [],
|
|
690
|
+
uniforms: ['red', 'green', 'blue', 'alpha'].map(id => ({
|
|
691
|
+
name: `u_parameter_${id}`,
|
|
692
|
+
type: 'float',
|
|
693
|
+
source: { kind: 'parameter', id },
|
|
694
|
+
})),
|
|
695
|
+
executionPlan: {
|
|
696
|
+
passes: [
|
|
697
|
+
{
|
|
698
|
+
id: 'builtin-background',
|
|
699
|
+
kind: 'draw',
|
|
700
|
+
nodes: ['builtin-background'],
|
|
701
|
+
estimatedTextureSamples: 0,
|
|
702
|
+
},
|
|
703
|
+
],
|
|
704
|
+
intermediateTextureCount: 0,
|
|
705
|
+
},
|
|
706
|
+
fragmentShader: `#version 300 es
|
|
707
|
+
precision highp float;
|
|
708
|
+
uniform float u_parameter_red;
|
|
709
|
+
uniform float u_parameter_green;
|
|
710
|
+
uniform float u_parameter_blue;
|
|
711
|
+
uniform float u_parameter_alpha;
|
|
712
|
+
out vec4 out_color;
|
|
713
|
+
void main() {
|
|
714
|
+
out_color = vec4(
|
|
715
|
+
u_parameter_red,
|
|
716
|
+
u_parameter_green,
|
|
717
|
+
u_parameter_blue,
|
|
718
|
+
u_parameter_alpha
|
|
719
|
+
);
|
|
720
|
+
}`,
|
|
721
|
+
webgpu: {
|
|
722
|
+
backend: 'webgpu',
|
|
723
|
+
nodeSet: 'aelion.visual.builtin/1.0.0',
|
|
724
|
+
graphHash: 'builtin-background-v1',
|
|
725
|
+
inputPorts: [],
|
|
726
|
+
uniforms: ['red', 'green', 'blue', 'alpha'].map(id => ({
|
|
727
|
+
name: `u_parameter_${id}`,
|
|
728
|
+
type: 'float',
|
|
729
|
+
source: { kind: 'parameter', id },
|
|
730
|
+
})),
|
|
731
|
+
executionPlan: {
|
|
732
|
+
passes: [
|
|
733
|
+
{
|
|
734
|
+
id: 'builtin-background',
|
|
735
|
+
kind: 'draw',
|
|
736
|
+
nodes: ['builtin-background'],
|
|
737
|
+
estimatedTextureSamples: 0,
|
|
738
|
+
},
|
|
739
|
+
],
|
|
740
|
+
intermediateTextureCount: 0,
|
|
741
|
+
},
|
|
742
|
+
shader: `
|
|
743
|
+
struct Uniforms { values: array<vec4f, 4> };
|
|
744
|
+
@group(0) @binding(0) var unused_sampler: sampler;
|
|
745
|
+
@group(0) @binding(1) var<uniform> uniforms: Uniforms;
|
|
746
|
+
struct VertexOut { @builtin(position) position: vec4f };
|
|
747
|
+
@vertex fn vs(@builtin(vertex_index) index: u32) -> VertexOut {
|
|
748
|
+
var positions = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
|
|
749
|
+
return VertexOut(vec4f(positions[index], 0.0, 1.0));
|
|
750
|
+
}
|
|
751
|
+
@fragment fn fs() -> @location(0) vec4f {
|
|
752
|
+
return vec4f(
|
|
753
|
+
uniforms.values[0].x,
|
|
754
|
+
uniforms.values[1].x,
|
|
755
|
+
uniforms.values[2].x,
|
|
756
|
+
uniforms.values[3].x
|
|
757
|
+
);
|
|
758
|
+
}`,
|
|
759
|
+
},
|
|
760
|
+
};
|
|
761
|
+
function rasterGeneratorFrame(generator, width, height, timestampUs) {
|
|
762
|
+
const canvas = new OffscreenCanvas(width, height);
|
|
763
|
+
const context = canvas.getContext('2d');
|
|
764
|
+
if (context === null)
|
|
765
|
+
throw new Error('GENERATOR_CANVAS_UNAVAILABLE');
|
|
766
|
+
const properties = generator;
|
|
767
|
+
const colors = Array.isArray(properties.colors) ? properties.colors : [];
|
|
768
|
+
if (properties.kind === 'linear-gradient' && colors.length > 1) {
|
|
769
|
+
const radians = (finite(properties.angleDeg, 0) * Math.PI) / 180;
|
|
770
|
+
const radius = Math.abs(Math.cos(radians)) * width + Math.abs(Math.sin(radians)) * height;
|
|
771
|
+
const centerX = width / 2;
|
|
772
|
+
const centerY = height / 2;
|
|
773
|
+
const x = (Math.cos(radians) * radius) / 2;
|
|
774
|
+
const y = (Math.sin(radians) * radius) / 2;
|
|
775
|
+
const gradient = context.createLinearGradient(centerX - x, centerY - y, centerX + x, centerY + y);
|
|
776
|
+
colors.forEach((color, index) => {
|
|
777
|
+
gradient.addColorStop(index / (colors.length - 1), canvasColor(color));
|
|
778
|
+
});
|
|
779
|
+
context.fillStyle = gradient;
|
|
780
|
+
}
|
|
781
|
+
else {
|
|
782
|
+
context.fillStyle = canvasColor(colors[0], 'rgba(0, 0, 0, 0)');
|
|
783
|
+
}
|
|
784
|
+
context.fillRect(0, 0, width, height);
|
|
785
|
+
return new VideoFrame(canvas, { timestamp: timestampUs });
|
|
786
|
+
}
|
|
787
|
+
function roundedRectanglePath(context, x, y, width, height, radius) {
|
|
788
|
+
const clamped = Math.max(0, Math.min(radius, width / 2, height / 2));
|
|
789
|
+
context.moveTo(x + clamped, y);
|
|
790
|
+
context.lineTo(x + width - clamped, y);
|
|
791
|
+
context.quadraticCurveTo(x + width, y, x + width, y + clamped);
|
|
792
|
+
context.lineTo(x + width, y + height - clamped);
|
|
793
|
+
context.quadraticCurveTo(x + width, y + height, x + width - clamped, y + height);
|
|
794
|
+
context.lineTo(x + clamped, y + height);
|
|
795
|
+
context.quadraticCurveTo(x, y + height, x, y + height - clamped);
|
|
796
|
+
context.lineTo(x, y + clamped);
|
|
797
|
+
context.quadraticCurveTo(x, y, x + clamped, y);
|
|
798
|
+
}
|
|
799
|
+
function rasterShapeFrame(clip, projectWidth, projectHeight, outputWidth, outputHeight, timestampUs) {
|
|
800
|
+
const canvas = new OffscreenCanvas(outputWidth, outputHeight);
|
|
801
|
+
const context = canvas.getContext('2d');
|
|
802
|
+
if (context === null)
|
|
803
|
+
throw new Error('SHAPE_CANVAS_UNAVAILABLE');
|
|
804
|
+
const shape = record(clip.shape);
|
|
805
|
+
const box = record(shape.box);
|
|
806
|
+
const x = finite(box.x, 0);
|
|
807
|
+
const y = finite(box.y, 0);
|
|
808
|
+
const width = Math.max(0, finite(box.width, 0));
|
|
809
|
+
const height = Math.max(0, finite(box.height, 0));
|
|
810
|
+
if (width === 0 || height === 0)
|
|
811
|
+
throw new RangeError(`SHAPE_BOX_INVALID: ${clip.id}`);
|
|
812
|
+
context.save();
|
|
813
|
+
context.scale(outputWidth / projectWidth, outputHeight / projectHeight);
|
|
814
|
+
context.beginPath();
|
|
815
|
+
if (shape.kind === 'ellipse') {
|
|
816
|
+
context.ellipse(x + width / 2, y + height / 2, width / 2, height / 2, 0, 0, Math.PI * 2);
|
|
817
|
+
}
|
|
818
|
+
else if (shape.kind === 'polygon') {
|
|
819
|
+
const points = Array.isArray(shape.points) ? shape.points.map(value => record(value)) : [];
|
|
820
|
+
if (points.length < 3)
|
|
821
|
+
throw new RangeError(`SHAPE_POLYGON_POINTS_INVALID: ${clip.id}`);
|
|
822
|
+
points.forEach((point, index) => {
|
|
823
|
+
const pointX = x + finite(point.x, 0) * width;
|
|
824
|
+
const pointY = y + finite(point.y, 0) * height;
|
|
825
|
+
if (index === 0)
|
|
826
|
+
context.moveTo(pointX, pointY);
|
|
827
|
+
else
|
|
828
|
+
context.lineTo(pointX, pointY);
|
|
829
|
+
});
|
|
830
|
+
context.closePath();
|
|
831
|
+
}
|
|
832
|
+
else {
|
|
833
|
+
roundedRectanglePath(context, x, y, width, height, Math.max(0, finite(shape.cornerRadiusPx, 0)));
|
|
834
|
+
context.closePath();
|
|
835
|
+
}
|
|
836
|
+
context.fillStyle = canvasColor(shape.fill);
|
|
837
|
+
context.fill();
|
|
838
|
+
const strokeWidthPx = Math.max(0, finite(shape.strokeWidthPx, 0));
|
|
839
|
+
if (shape.stroke !== undefined && strokeWidthPx > 0) {
|
|
840
|
+
context.strokeStyle = canvasColor(shape.stroke);
|
|
841
|
+
context.lineWidth = strokeWidthPx;
|
|
842
|
+
context.stroke();
|
|
843
|
+
}
|
|
844
|
+
context.restore();
|
|
845
|
+
return new VideoFrame(canvas, { timestamp: timestampUs });
|
|
846
|
+
}
|
|
847
|
+
function rasterTransparentFrame(width, height, timestampUs) {
|
|
848
|
+
return new VideoFrame(new OffscreenCanvas(width, height), { timestamp: timestampUs });
|
|
849
|
+
}
|
|
850
|
+
export class RenderIrFrameRenderer {
|
|
851
|
+
#compositor;
|
|
852
|
+
#adaptiveBackend = new AdaptiveBackendSelector();
|
|
853
|
+
#disposeController = new AbortController();
|
|
854
|
+
#maxPendingFrames;
|
|
855
|
+
#renderTasks = new Map();
|
|
856
|
+
#disposeTask;
|
|
857
|
+
#pendingFrames = 0;
|
|
858
|
+
constructor(options = {}) {
|
|
859
|
+
this.#compositor = new WorkerCompositor({
|
|
860
|
+
...(options.workerUrl === undefined ? {} : { workerUrl: options.workerUrl }),
|
|
861
|
+
});
|
|
862
|
+
this.#maxPendingFrames = options.maxPendingFrames ?? 2;
|
|
863
|
+
if (!Number.isSafeInteger(this.#maxPendingFrames) || this.#maxPendingFrames <= 0) {
|
|
864
|
+
throw new RangeError('maxPendingFrames must be a positive safe integer');
|
|
865
|
+
}
|
|
866
|
+
}
|
|
867
|
+
get disposed() {
|
|
868
|
+
return this.#compositor.disposed;
|
|
869
|
+
}
|
|
870
|
+
snapshot() {
|
|
871
|
+
return {
|
|
872
|
+
disposed: this.disposed,
|
|
873
|
+
pendingFrames: this.#pendingFrames,
|
|
874
|
+
maxPendingFrames: this.#maxPendingFrames,
|
|
875
|
+
adaptiveBackend: this.#adaptiveBackend.snapshot(),
|
|
876
|
+
worker: this.#compositor.snapshot(),
|
|
877
|
+
};
|
|
878
|
+
}
|
|
879
|
+
async render(options) {
|
|
880
|
+
if (this.disposed)
|
|
881
|
+
throw new ReferenceError('RenderIrFrameRenderer is disposed');
|
|
882
|
+
const linked = linkAbortSignals(options.signal, this.#disposeController.signal);
|
|
883
|
+
throwIfAborted(linked.signal, 'Render IR frame');
|
|
884
|
+
if (this.#pendingFrames >= this.#maxPendingFrames) {
|
|
885
|
+
linked.detach();
|
|
886
|
+
throw new AelionError([
|
|
887
|
+
{
|
|
888
|
+
code: 'RENDERER_FRAME_QUEUE_FULL',
|
|
889
|
+
severity: 'error',
|
|
890
|
+
message: `Render IR frame queue reached its ${this.#maxPendingFrames.toString()} request limit`,
|
|
891
|
+
recoverable: true,
|
|
892
|
+
},
|
|
893
|
+
]);
|
|
894
|
+
}
|
|
895
|
+
this.#pendingFrames += 1;
|
|
896
|
+
const token = Symbol('render-task');
|
|
897
|
+
const task = (async () => {
|
|
898
|
+
try {
|
|
899
|
+
return await this.#renderFrame({ ...options, signal: linked.signal });
|
|
900
|
+
}
|
|
901
|
+
finally {
|
|
902
|
+
linked.detach();
|
|
903
|
+
this.#pendingFrames -= 1;
|
|
904
|
+
this.#renderTasks.delete(token);
|
|
905
|
+
}
|
|
906
|
+
})();
|
|
907
|
+
this.#renderTasks.set(token, task);
|
|
908
|
+
return await task;
|
|
909
|
+
}
|
|
910
|
+
async #renderFrame(options) {
|
|
911
|
+
const requestedScale = options.mode === 'export' ? 1 : (options.renderScale ?? 1);
|
|
912
|
+
if (!Number.isFinite(requestedScale) || requestedScale <= 0 || requestedScale > 1) {
|
|
913
|
+
throw new RangeError('renderScale must be greater than 0 and at most 1');
|
|
914
|
+
}
|
|
915
|
+
const width = Math.max(1, Math.round(options.ir.width * requestedScale));
|
|
916
|
+
const height = Math.max(1, Math.round(options.ir.height * requestedScale));
|
|
917
|
+
const renderScale = Math.min(width / options.ir.width, height / options.ir.height);
|
|
918
|
+
const color = preflightColorPipeline(options.ir, LOCAL_RGBA8_COLOR_CAPABILITY);
|
|
919
|
+
if (!color.ok)
|
|
920
|
+
throw new AelionError(color.issues);
|
|
921
|
+
const state = evaluateVisualState(options.ir, options.timeUs);
|
|
922
|
+
const backgroundId = '__aelion_background__';
|
|
923
|
+
const externalFrames = new Map();
|
|
924
|
+
const rendered = new Map();
|
|
925
|
+
const nodes = [];
|
|
926
|
+
const appliedMaterialIds = [];
|
|
927
|
+
let nextExternalId = 0;
|
|
928
|
+
let nextNodeId = 0;
|
|
929
|
+
let inputsTransferred = false;
|
|
930
|
+
const addExternal = (frame, label) => {
|
|
931
|
+
const id = `external:${(nextExternalId++).toString()}:${label}`;
|
|
932
|
+
externalFrames.set(id, frame);
|
|
933
|
+
return { kind: 'external', id };
|
|
934
|
+
};
|
|
935
|
+
const addNode = (label, inputs, program, parameters = {}, systems = {}) => {
|
|
936
|
+
const id = `node:${(nextNodeId++).toString()}:${label}`;
|
|
937
|
+
nodes.push({ id, inputs, program, parameters, systems });
|
|
938
|
+
return { kind: 'node', id };
|
|
939
|
+
};
|
|
940
|
+
rendered.set(backgroundId, addNode('background', {}, BACKGROUND_PROGRAM, backgroundParameters(options.ir.backgroundColor)));
|
|
941
|
+
try {
|
|
942
|
+
for (const active of state.clips) {
|
|
943
|
+
if (active.clip.kind === 'adjustment-clip')
|
|
944
|
+
continue;
|
|
945
|
+
let frame;
|
|
946
|
+
if (active.clip.kind === 'visual-clip') {
|
|
947
|
+
if (active.sourceTimeUs === null)
|
|
948
|
+
continue;
|
|
949
|
+
frame = await options.source.frameAt(active.clip.source.assetId, active.clip.source.streamIndex, active.sourceTimeUs, options.signal, { purpose: options.mode, maxDimension: Math.max(width, height) });
|
|
950
|
+
}
|
|
951
|
+
else if (active.clip.kind === 'text-clip') {
|
|
952
|
+
frame = rasterTextFrame(active.clip, options.ir.width, options.ir.height, width, height, options.timeUs);
|
|
953
|
+
}
|
|
954
|
+
else if (active.clip.kind === 'generator-clip') {
|
|
955
|
+
frame = rasterGeneratorFrame(active.clip.generator, width, height, options.timeUs);
|
|
956
|
+
}
|
|
957
|
+
else if (active.clip.kind === 'shape-clip') {
|
|
958
|
+
frame = rasterShapeFrame(active.clip, options.ir.width, options.ir.height, width, height, options.timeUs);
|
|
959
|
+
}
|
|
960
|
+
else if (active.clip.kind === 'material-content-clip') {
|
|
961
|
+
frame = rasterTransparentFrame(width, height, options.timeUs);
|
|
962
|
+
}
|
|
963
|
+
else {
|
|
964
|
+
if (active.sourceTimeUs === null)
|
|
965
|
+
continue;
|
|
966
|
+
const subgraph = options.ir.subgraphs?.[active.clip.source.sequenceId];
|
|
967
|
+
if (subgraph === undefined) {
|
|
968
|
+
throw new ReferenceError(`NESTED_SEQUENCE_MISSING: ${active.clip.source.sequenceId}`);
|
|
969
|
+
}
|
|
970
|
+
const nested = await this.#renderFrame({
|
|
971
|
+
...options,
|
|
972
|
+
ir: subgraph,
|
|
973
|
+
timeUs: active.sourceTimeUs,
|
|
974
|
+
});
|
|
975
|
+
frame = takeBitmapFrame(nested.bitmap, options.timeUs);
|
|
976
|
+
appliedMaterialIds.push(...nested.materialIds);
|
|
977
|
+
}
|
|
978
|
+
try {
|
|
979
|
+
throwIfAborted(options.signal, 'Render IR media decode');
|
|
980
|
+
let reference = addExternal(frame, active.clip.id);
|
|
981
|
+
frame = undefined;
|
|
982
|
+
const baseParameters = visualParameters(active.clip.visual, options.ir.width, options.ir.height, width, height, options.timeUs, active.clip.range.startUs);
|
|
983
|
+
if (requiresBaseVisualPass(baseParameters, width, height)) {
|
|
984
|
+
reference = addNode(`${active.clip.id}:visual`, { source: reference }, BASE_VISUAL_PROGRAM, baseParameters);
|
|
985
|
+
}
|
|
986
|
+
for (const material of active.materials) {
|
|
987
|
+
const program = requiredProgram(material, options.mode);
|
|
988
|
+
if (program === undefined)
|
|
989
|
+
continue;
|
|
990
|
+
const evaluated = evaluateMaterialInstance(material, options.timeUs, active.clip.range.startUs);
|
|
991
|
+
reference = addNode(`${active.clip.id}:material:${material.id}`, { source: reference }, program, evaluated.parameters, { qualityScale: renderScale });
|
|
992
|
+
appliedMaterialIds.push(material.id);
|
|
993
|
+
}
|
|
994
|
+
rendered.set(active.clip.id, reference);
|
|
995
|
+
}
|
|
996
|
+
finally {
|
|
997
|
+
frame?.close();
|
|
998
|
+
}
|
|
999
|
+
}
|
|
1000
|
+
const consumedMaskIds = new Set();
|
|
1001
|
+
for (const active of state.clips) {
|
|
1002
|
+
if (active.clip.kind === 'adjustment-clip')
|
|
1003
|
+
continue;
|
|
1004
|
+
const mask = active.clip.visual.mask;
|
|
1005
|
+
if (mask === undefined)
|
|
1006
|
+
continue;
|
|
1007
|
+
const target = rendered.get(active.clip.id);
|
|
1008
|
+
const maskSource = rendered.get(mask.sourceItemId);
|
|
1009
|
+
if (target === undefined || maskSource === undefined) {
|
|
1010
|
+
throw new ReferenceError(`MASK_SOURCE_MISSING: ${active.clip.id} -> ${mask.sourceItemId}`);
|
|
1011
|
+
}
|
|
1012
|
+
let maskReference = maskSource;
|
|
1013
|
+
if (mask.space === 'source') {
|
|
1014
|
+
const targetSpace = visualParameters(active.clip.visual, options.ir.width, options.ir.height, width, height, options.timeUs, active.clip.range.startUs);
|
|
1015
|
+
if (requiresBaseVisualPass(targetSpace, width, height)) {
|
|
1016
|
+
maskReference = addNode(`${active.clip.id}:source-mask-space`, { source: maskReference }, BASE_VISUAL_PROGRAM, targetSpace);
|
|
1017
|
+
}
|
|
1018
|
+
}
|
|
1019
|
+
rendered.set(active.clip.id, addNode(`${active.clip.id}:mask`, { source: target, mask: maskReference }, MASK_PROGRAM, {
|
|
1020
|
+
maskMode: mask.channel === 'luma' ? 1 : 0,
|
|
1021
|
+
invert: mask.invert ? 1 : 0,
|
|
1022
|
+
featherUvX: mask.featherPx / options.ir.width,
|
|
1023
|
+
featherUvY: mask.featherPx / options.ir.height,
|
|
1024
|
+
}));
|
|
1025
|
+
if (mask.consumeSource)
|
|
1026
|
+
consumedMaskIds.add(mask.sourceItemId);
|
|
1027
|
+
}
|
|
1028
|
+
let transitionLayerId;
|
|
1029
|
+
const layerIds = [
|
|
1030
|
+
backgroundId,
|
|
1031
|
+
...state.clips.map(active => active.clip.id).filter(id => !consumedMaskIds.has(id)),
|
|
1032
|
+
];
|
|
1033
|
+
const blendModes = new Map([
|
|
1034
|
+
[backgroundId, 'normal'],
|
|
1035
|
+
...state.clips.map(active => [active.clip.id, active.clip.visual.blendMode]),
|
|
1036
|
+
]);
|
|
1037
|
+
if (state.transition !== undefined) {
|
|
1038
|
+
const from = rendered.get(state.transition.transition.fromItemId);
|
|
1039
|
+
const to = rendered.get(state.transition.transition.toItemId);
|
|
1040
|
+
if (from === undefined || to === undefined) {
|
|
1041
|
+
throw new Error(`Transition ${state.transition.transition.id} is missing an input frame`);
|
|
1042
|
+
}
|
|
1043
|
+
const program = requiredProgram(state.transition.material, options.mode);
|
|
1044
|
+
if (program === undefined)
|
|
1045
|
+
throw new Error('A transition Material cannot be skipped');
|
|
1046
|
+
const evaluated = evaluateMaterialInstance(state.transition.material, options.timeUs, state.transition.transition.range.startUs);
|
|
1047
|
+
transitionLayerId = `transition:${state.transition.transition.id}`;
|
|
1048
|
+
rendered.set(transitionLayerId, addNode(transitionLayerId, { from, to }, program, evaluated.parameters, {
|
|
1049
|
+
transitionProgress: state.transition.progress,
|
|
1050
|
+
qualityScale: renderScale,
|
|
1051
|
+
}));
|
|
1052
|
+
blendModes.set(transitionLayerId, 'normal');
|
|
1053
|
+
appliedMaterialIds.push(state.transition.material.id);
|
|
1054
|
+
const fromIndex = layerIds.indexOf(state.transition.transition.fromItemId);
|
|
1055
|
+
const toIndex = layerIds.indexOf(state.transition.transition.toItemId);
|
|
1056
|
+
const insertionIndex = Math.max(0, Math.min(fromIndex, toIndex));
|
|
1057
|
+
const withoutInputs = layerIds.filter(id => id !== state.transition?.transition.fromItemId &&
|
|
1058
|
+
id !== state.transition?.transition.toItemId);
|
|
1059
|
+
withoutInputs.splice(insertionIndex, 0, transitionLayerId);
|
|
1060
|
+
layerIds.splice(0, layerIds.length, ...withoutInputs);
|
|
1061
|
+
}
|
|
1062
|
+
const layers = layerIds.flatMap(id => {
|
|
1063
|
+
const frame = rendered.get(id);
|
|
1064
|
+
return frame === undefined
|
|
1065
|
+
? []
|
|
1066
|
+
: [{ id, frame, blendMode: blendModes.get(id) ?? 'normal' }];
|
|
1067
|
+
});
|
|
1068
|
+
if (layers.length === 0)
|
|
1069
|
+
throw new Error('No decodable visual frame is active');
|
|
1070
|
+
const firstLayer = layers[0];
|
|
1071
|
+
if (firstLayer === undefined)
|
|
1072
|
+
throw new Error('No base visual frame is active');
|
|
1073
|
+
let composite = firstLayer.frame;
|
|
1074
|
+
for (let index = 1; index < layers.length; index += 1) {
|
|
1075
|
+
const layer = layers[index];
|
|
1076
|
+
if (layer === undefined)
|
|
1077
|
+
continue;
|
|
1078
|
+
composite = addNode(`blend:${layer.id}`, { base: composite, overlay: layer.frame }, BLEND_PROGRAM, { blendMode: blendModeCode(layer.blendMode) });
|
|
1079
|
+
}
|
|
1080
|
+
for (const active of state.clips) {
|
|
1081
|
+
if (active.clip.kind !== 'adjustment-clip' || active.materials.length === 0)
|
|
1082
|
+
continue;
|
|
1083
|
+
const original = composite;
|
|
1084
|
+
for (const material of active.materials) {
|
|
1085
|
+
const program = requiredProgram(material, options.mode);
|
|
1086
|
+
if (program === undefined)
|
|
1087
|
+
continue;
|
|
1088
|
+
const evaluated = evaluateMaterialInstance(material, options.timeUs, active.clip.range.startUs);
|
|
1089
|
+
composite = addNode(`${active.clip.id}:adjustment:${material.id}`, { source: composite }, program, evaluated.parameters, { qualityScale: renderScale });
|
|
1090
|
+
appliedMaterialIds.push(material.id);
|
|
1091
|
+
}
|
|
1092
|
+
const adjustmentParameters = visualParameters(active.clip.visual, options.ir.width, options.ir.height, width, height, options.timeUs, active.clip.range.startUs);
|
|
1093
|
+
if (requiresBaseVisualPass(adjustmentParameters, width, height)) {
|
|
1094
|
+
composite = addNode(`${active.clip.id}:adjustment-visual`, { source: composite }, BASE_VISUAL_PROGRAM, adjustmentParameters);
|
|
1095
|
+
composite = addNode(`${active.clip.id}:adjustment-blend`, { base: original, overlay: composite }, BLEND_PROGRAM, { blendMode: 0 });
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
if (composite.kind === 'external') {
|
|
1099
|
+
composite = addNode('final-copy', { source: composite }, COPY_PROGRAM);
|
|
1100
|
+
}
|
|
1101
|
+
throwIfAborted(options.signal, 'Render IR frame graph');
|
|
1102
|
+
const preferredBackend = this.#adaptiveBackend.select(options.preferredBackend ?? 'auto');
|
|
1103
|
+
inputsTransferred = true;
|
|
1104
|
+
const result = await this.#composeFrameGraphOwned({
|
|
1105
|
+
inputs: Object.fromEntries(externalFrames),
|
|
1106
|
+
nodes,
|
|
1107
|
+
outputNodeId: composite.id,
|
|
1108
|
+
width,
|
|
1109
|
+
height,
|
|
1110
|
+
preferredBackend,
|
|
1111
|
+
allowFallback: options.allowFallback ?? true,
|
|
1112
|
+
...(options.signal === undefined ? {} : { signal: options.signal }),
|
|
1113
|
+
});
|
|
1114
|
+
this.#adaptiveBackend.record(preferredBackend, result);
|
|
1115
|
+
const outputFrame = takeBitmapFrame(result.bitmap, options.timeUs);
|
|
1116
|
+
try {
|
|
1117
|
+
return {
|
|
1118
|
+
bitmap: await presentationBitmap(outputFrame, options.signal),
|
|
1119
|
+
backend: result.backend,
|
|
1120
|
+
diagnostics: result.diagnostics,
|
|
1121
|
+
materialIds: appliedMaterialIds,
|
|
1122
|
+
width,
|
|
1123
|
+
height,
|
|
1124
|
+
renderScale,
|
|
1125
|
+
};
|
|
1126
|
+
}
|
|
1127
|
+
finally {
|
|
1128
|
+
outputFrame.close();
|
|
1129
|
+
}
|
|
1130
|
+
}
|
|
1131
|
+
finally {
|
|
1132
|
+
if (!inputsTransferred)
|
|
1133
|
+
externalFrames.forEach(frame => frame.close());
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
dispose() {
|
|
1137
|
+
if (this.#disposeTask !== undefined)
|
|
1138
|
+
return this.#disposeTask;
|
|
1139
|
+
this.#disposeController.abort(new DOMException('RenderIrFrameRenderer was disposed', 'AbortError'));
|
|
1140
|
+
this.#compositor.dispose();
|
|
1141
|
+
const tasks = [...this.#renderTasks.values()];
|
|
1142
|
+
this.#disposeTask = Promise.allSettled(tasks).then(() => undefined);
|
|
1143
|
+
return this.#disposeTask;
|
|
1144
|
+
}
|
|
1145
|
+
async #composeFrameGraphOwned(options) {
|
|
1146
|
+
// No asynchronous code can interleave this state check with composeFrameGraph()'s
|
|
1147
|
+
// synchronous admission. Once admitted, WorkerCompositor closes or transfers
|
|
1148
|
+
// every input; only this already-disposed path retains local ownership.
|
|
1149
|
+
if (this.#compositor.disposed) {
|
|
1150
|
+
new Set(Object.values(options.inputs)).forEach(frame => frame.close());
|
|
1151
|
+
throw new ReferenceError('WorkerCompositor is disposed');
|
|
1152
|
+
}
|
|
1153
|
+
const result = await this.#compositor.composeFrameGraph(options);
|
|
1154
|
+
try {
|
|
1155
|
+
throwIfAborted(options.signal, 'Render IR composition');
|
|
1156
|
+
return result;
|
|
1157
|
+
}
|
|
1158
|
+
catch (error) {
|
|
1159
|
+
result.bitmap.close();
|
|
1160
|
+
throw error;
|
|
1161
|
+
}
|
|
1162
|
+
}
|
|
1163
|
+
}
|