@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
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 FoyonaCZY and AelionSDK contributors
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
# @aelionsdk/renderer-worker
|
|
2
|
+
|
|
3
|
+
Off-main-thread WebGL2 and WebGPU compositor for AelionSDK
|
|
4
|
+
|
|
5
|
+
Install with `npm install @aelionsdk/renderer-worker@next`.
|
|
6
|
+
|
|
7
|
+
Version 0.1.0-beta.1 is a prerelease and its API may change before the first stable release. This package is part of [AelionSDK](https://github.com/FoyonaCZY/AelionSDK); see the repository README for supported browsers, examples and deployment requirements.
|
package/dist/client.d.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type Disposable } from '@aelionsdk/core';
|
|
2
|
+
import type { JsonValue } from '@aelionsdk/core';
|
|
3
|
+
import type { WebGl2MaterialProgram } from '@aelionsdk/material-compiler';
|
|
4
|
+
import type { ComposeSuccess, FrameGraphNode } from './protocol.js';
|
|
5
|
+
export interface ComposeOptions {
|
|
6
|
+
readonly inputs: Readonly<Record<string, VideoFrame>>;
|
|
7
|
+
readonly program: WebGl2MaterialProgram;
|
|
8
|
+
readonly parameters?: Readonly<Record<string, JsonValue>>;
|
|
9
|
+
readonly systems?: Readonly<Record<string, number>>;
|
|
10
|
+
readonly width: number;
|
|
11
|
+
readonly height: number;
|
|
12
|
+
readonly preferredBackend?: 'webgpu' | 'webgl2';
|
|
13
|
+
readonly allowFallback?: boolean;
|
|
14
|
+
readonly signal?: AbortSignal;
|
|
15
|
+
/** @internal Used only by backend loss conformance tests. */
|
|
16
|
+
readonly debugSimulateLoss?: 'webgpu-device' | 'webgl2-context';
|
|
17
|
+
}
|
|
18
|
+
export interface ComposeFrameGraphOptions {
|
|
19
|
+
readonly inputs: Readonly<Record<string, VideoFrame>>;
|
|
20
|
+
readonly nodes: readonly FrameGraphNode[];
|
|
21
|
+
readonly outputNodeId: string;
|
|
22
|
+
readonly width: number;
|
|
23
|
+
readonly height: number;
|
|
24
|
+
readonly preferredBackend?: 'webgpu' | 'webgl2';
|
|
25
|
+
readonly allowFallback?: boolean;
|
|
26
|
+
readonly signal?: AbortSignal;
|
|
27
|
+
}
|
|
28
|
+
export interface WorkerCompositorSnapshot {
|
|
29
|
+
readonly disposed: boolean;
|
|
30
|
+
/** All client-admitted requests awaiting a terminal Worker response. */
|
|
31
|
+
readonly pendingRequests: number;
|
|
32
|
+
/** Admitted requests that have not been cancelled by their caller. */
|
|
33
|
+
readonly activeRequests: number;
|
|
34
|
+
/** Cancellation tombstones retained until the Worker acknowledges cleanup. */
|
|
35
|
+
readonly cancelledRequests: number;
|
|
36
|
+
readonly maxPendingRequests: number;
|
|
37
|
+
}
|
|
38
|
+
export interface WorkerCompositorOptions {
|
|
39
|
+
readonly maxPendingRequests?: number;
|
|
40
|
+
/** Host-resolved Worker URL for standard ESM/CDN deployments. */
|
|
41
|
+
readonly workerUrl?: string | URL;
|
|
42
|
+
/** Advanced host integration and deterministic tests. */
|
|
43
|
+
readonly workerFactory?: () => Worker;
|
|
44
|
+
}
|
|
45
|
+
export declare class WorkerCompositor implements Disposable {
|
|
46
|
+
#private;
|
|
47
|
+
constructor(options?: WorkerCompositorOptions);
|
|
48
|
+
get disposed(): boolean;
|
|
49
|
+
snapshot(): WorkerCompositorSnapshot;
|
|
50
|
+
compose(options: ComposeOptions): Promise<ComposeSuccess>;
|
|
51
|
+
composeFrameGraph(options: ComposeFrameGraphOptions): Promise<ComposeSuccess>;
|
|
52
|
+
dispose(): void;
|
|
53
|
+
}
|
|
54
|
+
//# sourceMappingURL=client.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../src/client.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,iBAAiB,CAAC;AACjD,OAAO,KAAK,EAAE,qBAAqB,EAAE,MAAM,8BAA8B,CAAC;AAE1E,OAAO,KAAK,EAIV,cAAc,EACd,cAAc,EAEf,MAAM,eAAe,CAAC;AAEvB,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,OAAO,EAAE,qBAAqB,CAAC;IACxC,QAAQ,CAAC,UAAU,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC,CAAC;IAC1D,QAAQ,CAAC,OAAO,CAAC,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC;IACpD,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;IAC9B,6DAA6D;IAC7D,QAAQ,CAAC,iBAAiB,CAAC,EAAE,eAAe,GAAG,gBAAgB,CAAC;CACjE;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,MAAM,EAAE,UAAU,CAAC,CAAC,CAAC;IACtD,QAAQ,CAAC,KAAK,EAAE,SAAS,cAAc,EAAE,CAAC;IAC1C,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,QAAQ,GAAG,QAAQ,CAAC;IAChD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,wBAAwB;IACvC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,wEAAwE;IACxE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,sEAAsE;IACtE,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,8EAA8E;IAC9E,QAAQ,CAAC,iBAAiB,EAAE,MAAM,CAAC;IACnC,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;CACrC;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,kBAAkB,CAAC,EAAE,MAAM,CAAC;IACrC,iEAAiE;IACjE,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;IAClC,yDAAyD;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,MAAM,MAAM,CAAC;CACvC;AA2BD,qBAAa,gBAAiB,YAAW,UAAU;;gBAO9B,OAAO,GAAE,uBAA4B;IAgBxD,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEM,QAAQ,IAAI,wBAAwB;IAgBpC,OAAO,CAAC,OAAO,EAAE,cAAc,GAAG,OAAO,CAAC,cAAc,CAAC;IA2EzD,iBAAiB,CAAC,OAAO,EAAE,wBAAwB,GAAG,OAAO,CAAC,cAAc,CAAC;IAqE7E,OAAO,IAAI,IAAI;CA+DvB"}
|
package/dist/client.js
ADDED
|
@@ -0,0 +1,264 @@
|
|
|
1
|
+
import { AelionError } from '@aelionsdk/core';
|
|
2
|
+
function failureError(response) {
|
|
3
|
+
return new AelionError([
|
|
4
|
+
{
|
|
5
|
+
code: response.code,
|
|
6
|
+
severity: 'error',
|
|
7
|
+
message: response.message,
|
|
8
|
+
recoverable: true,
|
|
9
|
+
},
|
|
10
|
+
]);
|
|
11
|
+
}
|
|
12
|
+
export class WorkerCompositor {
|
|
13
|
+
#worker;
|
|
14
|
+
#pending = new Map();
|
|
15
|
+
#nextId = 1;
|
|
16
|
+
#disposed = false;
|
|
17
|
+
#maxPendingRequests;
|
|
18
|
+
constructor(options = {}) {
|
|
19
|
+
const maxPendingRequests = options.maxPendingRequests ?? 8;
|
|
20
|
+
if (!Number.isSafeInteger(maxPendingRequests) || maxPendingRequests <= 0) {
|
|
21
|
+
throw new RangeError('maxPendingRequests must be a positive safe integer');
|
|
22
|
+
}
|
|
23
|
+
this.#maxPendingRequests = maxPendingRequests;
|
|
24
|
+
this.#worker =
|
|
25
|
+
options.workerFactory?.() ??
|
|
26
|
+
new Worker(options.workerUrl ?? new URL('./webgl2-worker.js', import.meta.url), {
|
|
27
|
+
type: 'module',
|
|
28
|
+
name: 'aelion-renderer-webgl2',
|
|
29
|
+
});
|
|
30
|
+
this.#worker.addEventListener('message', this.#handleMessage);
|
|
31
|
+
this.#worker.addEventListener('error', this.#handleWorkerError);
|
|
32
|
+
}
|
|
33
|
+
get disposed() {
|
|
34
|
+
return this.#disposed;
|
|
35
|
+
}
|
|
36
|
+
snapshot() {
|
|
37
|
+
let activeRequests = 0;
|
|
38
|
+
let cancelledRequests = 0;
|
|
39
|
+
for (const request of this.#pending.values()) {
|
|
40
|
+
if (request.state === 'pending')
|
|
41
|
+
activeRequests += 1;
|
|
42
|
+
else
|
|
43
|
+
cancelledRequests += 1;
|
|
44
|
+
}
|
|
45
|
+
return {
|
|
46
|
+
disposed: this.#disposed,
|
|
47
|
+
pendingRequests: this.#pending.size,
|
|
48
|
+
activeRequests,
|
|
49
|
+
cancelledRequests,
|
|
50
|
+
maxPendingRequests: this.#maxPendingRequests,
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
compose(options) {
|
|
54
|
+
if (this.#disposed)
|
|
55
|
+
return Promise.reject(new ReferenceError('WorkerCompositor is disposed'));
|
|
56
|
+
if (options.signal?.aborted) {
|
|
57
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
58
|
+
return Promise.reject(new DOMException('Composition aborted', 'AbortError'));
|
|
59
|
+
}
|
|
60
|
+
if (this.#pending.size >= this.#maxPendingRequests) {
|
|
61
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
62
|
+
return Promise.reject(new AelionError([
|
|
63
|
+
{
|
|
64
|
+
code: 'RENDERER_QUEUE_FULL',
|
|
65
|
+
severity: 'error',
|
|
66
|
+
message: `Worker compositor queue reached its ${this.#maxPendingRequests.toString()} request limit`,
|
|
67
|
+
recoverable: true,
|
|
68
|
+
},
|
|
69
|
+
]));
|
|
70
|
+
}
|
|
71
|
+
const id = this.#nextId;
|
|
72
|
+
this.#nextId += 1;
|
|
73
|
+
const request = {
|
|
74
|
+
type: 'compose',
|
|
75
|
+
id,
|
|
76
|
+
inputs: options.inputs,
|
|
77
|
+
program: options.program,
|
|
78
|
+
parameters: options.parameters ?? {},
|
|
79
|
+
systems: options.systems ?? {},
|
|
80
|
+
width: options.width,
|
|
81
|
+
height: options.height,
|
|
82
|
+
preferredBackend: options.preferredBackend ?? 'webgpu',
|
|
83
|
+
allowFallback: options.allowFallback ?? true,
|
|
84
|
+
...(options.debugSimulateLoss === undefined
|
|
85
|
+
? {}
|
|
86
|
+
: { debugSimulateLoss: options.debugSimulateLoss }),
|
|
87
|
+
};
|
|
88
|
+
return new Promise((resolve, reject) => {
|
|
89
|
+
const onAbort = () => {
|
|
90
|
+
const pending = this.#pending.get(id);
|
|
91
|
+
if (pending === undefined || pending.state === 'cancelled')
|
|
92
|
+
return;
|
|
93
|
+
pending.signal?.removeEventListener('abort', onAbort);
|
|
94
|
+
// Keep a lightweight tombstone in the admission map until the Worker
|
|
95
|
+
// confirms that the transferred frames/GPU work reached a terminal
|
|
96
|
+
// state. Otherwise cancel/retry loops can bypass maxPendingRequests.
|
|
97
|
+
this.#pending.set(id, { state: 'cancelled' });
|
|
98
|
+
try {
|
|
99
|
+
this.#worker.postMessage({ type: 'cancel', id });
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// A failed cancel post means no acknowledgement can arrive. Terminate
|
|
103
|
+
// the Worker so its in-flight resources are reclaimed, then drain all
|
|
104
|
+
// client admission state through the shared fatal-error path.
|
|
105
|
+
this.#failAll(new Error('Renderer Worker cancellation failed'));
|
|
106
|
+
}
|
|
107
|
+
pending.reject(new DOMException('Composition aborted', 'AbortError'));
|
|
108
|
+
};
|
|
109
|
+
this.#pending.set(id, {
|
|
110
|
+
state: 'pending',
|
|
111
|
+
resolve,
|
|
112
|
+
reject,
|
|
113
|
+
...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
|
|
114
|
+
});
|
|
115
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
116
|
+
try {
|
|
117
|
+
this.#worker.postMessage(request, Object.values(options.inputs));
|
|
118
|
+
}
|
|
119
|
+
catch (error) {
|
|
120
|
+
this.#pending.delete(id);
|
|
121
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
122
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
123
|
+
this.#failAll(error instanceof Error ? error : new Error('Renderer Worker request failed'));
|
|
124
|
+
reject(error instanceof Error ? error : new Error('Renderer Worker request failed'));
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
composeFrameGraph(options) {
|
|
129
|
+
if (this.#disposed)
|
|
130
|
+
return Promise.reject(new ReferenceError('WorkerCompositor is disposed'));
|
|
131
|
+
if (options.signal?.aborted) {
|
|
132
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
133
|
+
return Promise.reject(new DOMException('Frame graph aborted', 'AbortError'));
|
|
134
|
+
}
|
|
135
|
+
if (options.nodes.length === 0) {
|
|
136
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
137
|
+
return Promise.reject(new RangeError('Frame graph must contain at least one node'));
|
|
138
|
+
}
|
|
139
|
+
if (this.#pending.size >= this.#maxPendingRequests) {
|
|
140
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
141
|
+
return Promise.reject(new AelionError([
|
|
142
|
+
{
|
|
143
|
+
code: 'RENDERER_QUEUE_FULL',
|
|
144
|
+
severity: 'error',
|
|
145
|
+
message: `Worker compositor queue reached its ${this.#maxPendingRequests.toString()} request limit`,
|
|
146
|
+
recoverable: true,
|
|
147
|
+
},
|
|
148
|
+
]));
|
|
149
|
+
}
|
|
150
|
+
const id = this.#nextId;
|
|
151
|
+
this.#nextId += 1;
|
|
152
|
+
const request = {
|
|
153
|
+
type: 'compose-frame-graph',
|
|
154
|
+
id,
|
|
155
|
+
inputs: options.inputs,
|
|
156
|
+
nodes: options.nodes,
|
|
157
|
+
outputNodeId: options.outputNodeId,
|
|
158
|
+
width: options.width,
|
|
159
|
+
height: options.height,
|
|
160
|
+
preferredBackend: options.preferredBackend ?? 'webgl2',
|
|
161
|
+
allowFallback: options.allowFallback ?? true,
|
|
162
|
+
};
|
|
163
|
+
return new Promise((resolve, reject) => {
|
|
164
|
+
const onAbort = () => {
|
|
165
|
+
const pending = this.#pending.get(id);
|
|
166
|
+
if (pending === undefined || pending.state === 'cancelled')
|
|
167
|
+
return;
|
|
168
|
+
pending.signal?.removeEventListener('abort', onAbort);
|
|
169
|
+
this.#pending.set(id, { state: 'cancelled' });
|
|
170
|
+
try {
|
|
171
|
+
this.#worker.postMessage({ type: 'cancel', id });
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
this.#failAll(new Error('Renderer Worker cancellation failed'));
|
|
175
|
+
}
|
|
176
|
+
pending.reject(new DOMException('Frame graph aborted', 'AbortError'));
|
|
177
|
+
};
|
|
178
|
+
this.#pending.set(id, {
|
|
179
|
+
state: 'pending',
|
|
180
|
+
resolve,
|
|
181
|
+
reject,
|
|
182
|
+
...(options.signal === undefined ? {} : { signal: options.signal, onAbort }),
|
|
183
|
+
});
|
|
184
|
+
options.signal?.addEventListener('abort', onAbort, { once: true });
|
|
185
|
+
try {
|
|
186
|
+
this.#worker.postMessage(request, Object.values(options.inputs));
|
|
187
|
+
}
|
|
188
|
+
catch (error) {
|
|
189
|
+
this.#pending.delete(id);
|
|
190
|
+
options.signal?.removeEventListener('abort', onAbort);
|
|
191
|
+
Object.values(options.inputs).forEach(frame => frame.close());
|
|
192
|
+
this.#failAll(error instanceof Error ? error : new Error('Renderer Worker request failed'));
|
|
193
|
+
reject(error instanceof Error ? error : new Error('Renderer Worker request failed'));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
dispose() {
|
|
198
|
+
if (this.#disposed)
|
|
199
|
+
return;
|
|
200
|
+
this.#disposed = true;
|
|
201
|
+
try {
|
|
202
|
+
this.#worker.postMessage({ type: 'dispose' });
|
|
203
|
+
}
|
|
204
|
+
catch {
|
|
205
|
+
// Termination below is the authoritative cleanup path.
|
|
206
|
+
}
|
|
207
|
+
this.#worker.terminate();
|
|
208
|
+
this.#detachWorkerListeners();
|
|
209
|
+
for (const pending of this.#pending.values()) {
|
|
210
|
+
if (pending.state === 'pending' && pending.onAbort !== undefined) {
|
|
211
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
212
|
+
}
|
|
213
|
+
if (pending.state === 'pending') {
|
|
214
|
+
pending.reject(new ReferenceError('WorkerCompositor was disposed'));
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
this.#pending.clear();
|
|
218
|
+
}
|
|
219
|
+
#handleMessage = (event) => {
|
|
220
|
+
const response = event.data;
|
|
221
|
+
const pending = this.#pending.get(response.id);
|
|
222
|
+
if (pending === undefined) {
|
|
223
|
+
if (response.type === 'composed')
|
|
224
|
+
response.bitmap.close();
|
|
225
|
+
return;
|
|
226
|
+
}
|
|
227
|
+
this.#pending.delete(response.id);
|
|
228
|
+
if (pending.state === 'cancelled') {
|
|
229
|
+
if (response.type === 'composed')
|
|
230
|
+
response.bitmap.close();
|
|
231
|
+
return;
|
|
232
|
+
}
|
|
233
|
+
if (pending.onAbort !== undefined) {
|
|
234
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
235
|
+
}
|
|
236
|
+
if (response.type === 'failed')
|
|
237
|
+
pending.reject(failureError(response));
|
|
238
|
+
else if (response.type === 'cancelled') {
|
|
239
|
+
pending.reject(new DOMException('Composition cancelled by Worker', 'AbortError'));
|
|
240
|
+
}
|
|
241
|
+
else
|
|
242
|
+
pending.resolve(response);
|
|
243
|
+
};
|
|
244
|
+
#handleWorkerError = (event) => {
|
|
245
|
+
this.#failAll(new Error(event.message));
|
|
246
|
+
};
|
|
247
|
+
#failAll(error) {
|
|
248
|
+
this.#disposed = true;
|
|
249
|
+
this.#worker.terminate();
|
|
250
|
+
this.#detachWorkerListeners();
|
|
251
|
+
for (const pending of this.#pending.values()) {
|
|
252
|
+
if (pending.state === 'pending' && pending.onAbort !== undefined) {
|
|
253
|
+
pending.signal?.removeEventListener('abort', pending.onAbort);
|
|
254
|
+
}
|
|
255
|
+
if (pending.state === 'pending')
|
|
256
|
+
pending.reject(error);
|
|
257
|
+
}
|
|
258
|
+
this.#pending.clear();
|
|
259
|
+
}
|
|
260
|
+
#detachWorkerListeners() {
|
|
261
|
+
this.#worker.removeEventListener('message', this.#handleMessage);
|
|
262
|
+
this.#worker.removeEventListener('error', this.#handleWorkerError);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { type Disposable } from '@aelionsdk/core';
|
|
2
|
+
export interface FontAssetRegistration {
|
|
3
|
+
readonly id: string;
|
|
4
|
+
readonly family: string;
|
|
5
|
+
readonly source: ArrayBuffer;
|
|
6
|
+
readonly descriptors?: FontFaceDescriptors;
|
|
7
|
+
}
|
|
8
|
+
export interface FontManagerSnapshot {
|
|
9
|
+
readonly loadedFonts: number;
|
|
10
|
+
readonly loadedBytes: number;
|
|
11
|
+
readonly maxFonts: number;
|
|
12
|
+
readonly maxBytes: number;
|
|
13
|
+
}
|
|
14
|
+
/** Bounded explicit font loader. Export can fail closed instead of using an unknown system fallback. */
|
|
15
|
+
export declare class BrowserFontManager implements Disposable {
|
|
16
|
+
#private;
|
|
17
|
+
get disposed(): boolean;
|
|
18
|
+
constructor(options?: {
|
|
19
|
+
readonly fontSet?: FontFaceSet;
|
|
20
|
+
readonly maxFonts?: number;
|
|
21
|
+
readonly maxBytes?: number;
|
|
22
|
+
});
|
|
23
|
+
snapshot(): FontManagerSnapshot;
|
|
24
|
+
load(registration: FontAssetRegistration, signal?: AbortSignal): Promise<void>;
|
|
25
|
+
resolveFallback(families: readonly string[], sample?: string, fontSizePx?: number): string;
|
|
26
|
+
requireAvailable(families: readonly string[], sample?: string): string;
|
|
27
|
+
unload(id: string): boolean;
|
|
28
|
+
dispose(): void;
|
|
29
|
+
}
|
|
30
|
+
//# sourceMappingURL=font-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"font-manager.d.ts","sourceRoot":"","sources":["../src/font-manager.ts"],"names":[],"mappings":"AAAA,OAAO,EAAkB,KAAK,UAAU,EAAE,MAAM,iBAAiB,CAAC;AAElE,MAAM,WAAW,qBAAqB;IACpC,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;IACpB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,WAAW,CAAC,EAAE,mBAAmB,CAAC;CAC5C;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED,wGAAwG;AACxG,qBAAa,kBAAmB,YAAW,UAAU;;IAQnD,IAAW,QAAQ,IAAI,OAAO,CAE7B;gBAGC,OAAO,GAAE;QACP,QAAQ,CAAC,OAAO,CAAC,EAAE,WAAW,CAAC;QAC/B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;QAC3B,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;KACvB;IAqBD,QAAQ,IAAI,mBAAmB;IASzB,IAAI,CAAC,YAAY,EAAE,qBAAqB,EAAE,MAAM,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC;IAsBpF,eAAe,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,SAAU,EAAE,UAAU,SAAK,GAAG,MAAM;IAQvF,gBAAgB,CAAC,QAAQ,EAAE,SAAS,MAAM,EAAE,EAAE,MAAM,CAAC,EAAE,MAAM,GAAG,MAAM;IAQtE,MAAM,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAS3B,OAAO,IAAI,IAAI;CAOvB"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
import { throwIfAborted } from '@aelionsdk/core';
|
|
2
|
+
/** Bounded explicit font loader. Export can fail closed instead of using an unknown system fallback. */
|
|
3
|
+
export class BrowserFontManager {
|
|
4
|
+
#faces = new Map();
|
|
5
|
+
#fontSet;
|
|
6
|
+
#maxFonts;
|
|
7
|
+
#maxBytes;
|
|
8
|
+
#loadedBytes = 0;
|
|
9
|
+
#disposed = false;
|
|
10
|
+
get disposed() {
|
|
11
|
+
return this.#disposed;
|
|
12
|
+
}
|
|
13
|
+
constructor(options = {}) {
|
|
14
|
+
const defaultFontSet = typeof document === 'undefined' ? undefined : document.fonts;
|
|
15
|
+
const fontSet = options.fontSet ?? defaultFontSet;
|
|
16
|
+
if (fontSet === undefined) {
|
|
17
|
+
throw new DOMException('FontFaceSet is unavailable in this execution context', 'NotSupportedError');
|
|
18
|
+
}
|
|
19
|
+
this.#fontSet = fontSet;
|
|
20
|
+
this.#maxFonts = options.maxFonts ?? 32;
|
|
21
|
+
this.#maxBytes = options.maxBytes ?? 128 * 1024 * 1024;
|
|
22
|
+
if (!Number.isSafeInteger(this.#maxFonts) || this.#maxFonts <= 0) {
|
|
23
|
+
throw new RangeError('FONT_LIMIT_INVALID');
|
|
24
|
+
}
|
|
25
|
+
if (!Number.isSafeInteger(this.#maxBytes) || this.#maxBytes <= 0) {
|
|
26
|
+
throw new RangeError('FONT_BYTE_LIMIT_INVALID');
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
snapshot() {
|
|
30
|
+
return {
|
|
31
|
+
loadedFonts: this.#faces.size,
|
|
32
|
+
loadedBytes: this.#loadedBytes,
|
|
33
|
+
maxFonts: this.#maxFonts,
|
|
34
|
+
maxBytes: this.#maxBytes,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
async load(registration, signal) {
|
|
38
|
+
if (this.#disposed)
|
|
39
|
+
throw new ReferenceError('Font manager is disposed');
|
|
40
|
+
if (this.#faces.has(registration.id))
|
|
41
|
+
throw new TypeError('FONT_ID_EXISTS');
|
|
42
|
+
if (this.#faces.size >= this.#maxFonts ||
|
|
43
|
+
this.#loadedBytes + registration.source.byteLength > this.#maxBytes) {
|
|
44
|
+
throw new RangeError('FONT_RESOURCE_LIMIT');
|
|
45
|
+
}
|
|
46
|
+
throwIfAborted(signal, 'Font load');
|
|
47
|
+
const face = new FontFace(registration.family, registration.source.slice(0), registration.descriptors);
|
|
48
|
+
await face.load();
|
|
49
|
+
throwIfAborted(signal, 'Font load');
|
|
50
|
+
this.#fontSet.add(face);
|
|
51
|
+
this.#faces.set(registration.id, { face, bytes: registration.source.byteLength });
|
|
52
|
+
this.#loadedBytes += registration.source.byteLength;
|
|
53
|
+
}
|
|
54
|
+
resolveFallback(families, sample = 'Aa中🙂', fontSizePx = 16) {
|
|
55
|
+
for (const family of families) {
|
|
56
|
+
const escaped = family.replaceAll('"', '\\"');
|
|
57
|
+
if (this.#fontSet.check(`${fontSizePx.toString()}px "${escaped}"`, sample))
|
|
58
|
+
return family;
|
|
59
|
+
}
|
|
60
|
+
return 'sans-serif';
|
|
61
|
+
}
|
|
62
|
+
requireAvailable(families, sample) {
|
|
63
|
+
const resolved = this.resolveFallback(families, sample);
|
|
64
|
+
if (resolved === 'sans-serif' && !families.includes('sans-serif')) {
|
|
65
|
+
throw new ReferenceError(`FONT_MISSING: ${families.join(', ')}`);
|
|
66
|
+
}
|
|
67
|
+
return resolved;
|
|
68
|
+
}
|
|
69
|
+
unload(id) {
|
|
70
|
+
const entry = this.#faces.get(id);
|
|
71
|
+
if (entry === undefined)
|
|
72
|
+
return false;
|
|
73
|
+
this.#fontSet.delete(entry.face);
|
|
74
|
+
this.#faces.delete(id);
|
|
75
|
+
this.#loadedBytes -= entry.bytes;
|
|
76
|
+
return true;
|
|
77
|
+
}
|
|
78
|
+
dispose() {
|
|
79
|
+
if (this.#disposed)
|
|
80
|
+
return;
|
|
81
|
+
this.#disposed = true;
|
|
82
|
+
for (const entry of this.#faces.values())
|
|
83
|
+
this.#fontSet.delete(entry.face);
|
|
84
|
+
this.#faces.clear();
|
|
85
|
+
this.#loadedBytes = 0;
|
|
86
|
+
}
|
|
87
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,aAAa,CAAC;AAC5B,cAAc,kBAAkB,CAAC;AACjC,cAAc,eAAe,CAAC;AAC9B,cAAc,mBAAmB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { type Disposable } from '@aelionsdk/core';
|
|
2
|
+
import { type RenderIr } from '@aelionsdk/render-ir';
|
|
3
|
+
import { type WorkerCompositorSnapshot } from './client.js';
|
|
4
|
+
import type { RendererWorkerDiagnostic } from './protocol.js';
|
|
5
|
+
export type RenderMode = 'preview' | 'export';
|
|
6
|
+
export interface IrFrameRequest {
|
|
7
|
+
readonly purpose: RenderMode;
|
|
8
|
+
readonly maxDimension: number;
|
|
9
|
+
}
|
|
10
|
+
export interface IrFrameSource {
|
|
11
|
+
frameAt(assetId: string, streamIndex: number, sourceTimeUs: number, signal?: AbortSignal, request?: IrFrameRequest): Promise<VideoFrame>;
|
|
12
|
+
}
|
|
13
|
+
export interface RenderIrFrameOptions {
|
|
14
|
+
readonly ir: RenderIr;
|
|
15
|
+
readonly timeUs: number;
|
|
16
|
+
readonly source: IrFrameSource;
|
|
17
|
+
readonly mode: RenderMode;
|
|
18
|
+
readonly preferredBackend?: 'auto' | 'webgpu' | 'webgl2';
|
|
19
|
+
readonly allowFallback?: boolean;
|
|
20
|
+
/** Preview-only output scale. Export always renders at full Project resolution. */
|
|
21
|
+
readonly renderScale?: number;
|
|
22
|
+
readonly signal?: AbortSignal;
|
|
23
|
+
}
|
|
24
|
+
export interface RenderIrFrameResult {
|
|
25
|
+
readonly bitmap: ImageBitmap;
|
|
26
|
+
readonly backend: 'webgpu' | 'webgl2';
|
|
27
|
+
readonly diagnostics?: readonly RendererWorkerDiagnostic[];
|
|
28
|
+
readonly materialIds: readonly string[];
|
|
29
|
+
readonly width: number;
|
|
30
|
+
readonly height: number;
|
|
31
|
+
readonly renderScale: number;
|
|
32
|
+
}
|
|
33
|
+
export interface RenderIrFrameRendererOptions {
|
|
34
|
+
/** Maximum full frame evaluations in flight. Defaults to 2. */
|
|
35
|
+
readonly maxPendingFrames?: number;
|
|
36
|
+
/** Host-resolved renderer Worker URL. */
|
|
37
|
+
readonly workerUrl?: string | URL;
|
|
38
|
+
}
|
|
39
|
+
export interface RenderIrFrameRendererSnapshot {
|
|
40
|
+
readonly disposed: boolean;
|
|
41
|
+
readonly pendingFrames: number;
|
|
42
|
+
readonly maxPendingFrames: number;
|
|
43
|
+
readonly adaptiveBackend: AdaptiveBackendSnapshot;
|
|
44
|
+
readonly worker: WorkerCompositorSnapshot;
|
|
45
|
+
}
|
|
46
|
+
export interface AdaptiveBackendSnapshot {
|
|
47
|
+
readonly selected: 'webgpu' | 'webgl2';
|
|
48
|
+
readonly webgpuSamples: number;
|
|
49
|
+
readonly webgl2Samples: number;
|
|
50
|
+
readonly webgpuP95Us: number | null;
|
|
51
|
+
readonly webgl2P95Us: number | null;
|
|
52
|
+
readonly webgpuCooldownFrames: number;
|
|
53
|
+
}
|
|
54
|
+
export declare class RenderIrFrameRenderer implements Disposable {
|
|
55
|
+
#private;
|
|
56
|
+
constructor(options?: RenderIrFrameRendererOptions);
|
|
57
|
+
get disposed(): boolean;
|
|
58
|
+
snapshot(): RenderIrFrameRendererSnapshot;
|
|
59
|
+
render(options: RenderIrFrameOptions): Promise<RenderIrFrameResult>;
|
|
60
|
+
dispose(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
//# sourceMappingURL=ir-renderer.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"ir-renderer.d.ts","sourceRoot":"","sources":["../src/ir-renderer.ts"],"names":[],"mappings":"AAAA,OAAO,EAAe,KAAK,UAAU,EAAkC,MAAM,iBAAiB,CAAC;AAE/F,OAAO,EAWL,KAAK,QAAQ,EACd,MAAM,sBAAsB,CAAC;AAE9B,OAAO,EAGL,KAAK,wBAAwB,EAC9B,MAAM,aAAa,CAAC;AACrB,OAAO,KAAK,EAIV,wBAAwB,EACzB,MAAM,eAAe,CAAC;AAEvB,MAAM,MAAM,UAAU,GAAG,SAAS,GAAG,QAAQ,CAAC;AAE9C,MAAM,WAAW,cAAc;IAC7B,QAAQ,CAAC,OAAO,EAAE,UAAU,CAAC;IAC7B,QAAQ,CAAC,YAAY,EAAE,MAAM,CAAC;CAC/B;AAED,MAAM,WAAW,aAAa;IAC5B,OAAO,CACL,OAAO,EAAE,MAAM,EACf,WAAW,EAAE,MAAM,EACnB,YAAY,EAAE,MAAM,EACpB,MAAM,CAAC,EAAE,WAAW,EACpB,OAAO,CAAC,EAAE,cAAc,GACvB,OAAO,CAAC,UAAU,CAAC,CAAC;CACxB;AAED,MAAM,WAAW,oBAAoB;IACnC,QAAQ,CAAC,EAAE,EAAE,QAAQ,CAAC;IACtB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC;IAC/B,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC;IAC1B,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,QAAQ,CAAC;IACzD,QAAQ,CAAC,aAAa,CAAC,EAAE,OAAO,CAAC;IACjC,mFAAmF;IACnF,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;IAC9B,QAAQ,CAAC,MAAM,CAAC,EAAE,WAAW,CAAC;CAC/B;AAED,MAAM,WAAW,mBAAmB;IAClC,QAAQ,CAAC,MAAM,EAAE,WAAW,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACtC,QAAQ,CAAC,WAAW,CAAC,EAAE,SAAS,wBAAwB,EAAE,CAAC;IAC3D,QAAQ,CAAC,WAAW,EAAE,SAAS,MAAM,EAAE,CAAC;IACxC,QAAQ,CAAC,KAAK,EAAE,MAAM,CAAC;IACvB,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;CAC9B;AAED,MAAM,WAAW,4BAA4B;IAC3C,+DAA+D;IAC/D,QAAQ,CAAC,gBAAgB,CAAC,EAAE,MAAM,CAAC;IACnC,yCAAyC;IACzC,QAAQ,CAAC,SAAS,CAAC,EAAE,MAAM,GAAG,GAAG,CAAC;CACnC;AAED,MAAM,WAAW,6BAA6B;IAC5C,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,gBAAgB,EAAE,MAAM,CAAC;IAClC,QAAQ,CAAC,eAAe,EAAE,uBAAuB,CAAC;IAClD,QAAQ,CAAC,MAAM,EAAE,wBAAwB,CAAC;CAC3C;AAED,MAAM,WAAW,uBAAuB;IACtC,QAAQ,CAAC,QAAQ,EAAE,QAAQ,GAAG,QAAQ,CAAC;IACvC,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,WAAW,EAAE,MAAM,GAAG,IAAI,CAAC;IACpC,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;CACvC;AA05BD,qBAAa,qBAAsB,YAAW,UAAU;;gBASnC,OAAO,GAAE,4BAAiC;IAU7D,IAAW,QAAQ,IAAI,OAAO,CAE7B;IAEM,QAAQ,IAAI,6BAA6B;IAUnC,MAAM,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,mBAAmB,CAAC;IAuWzE,OAAO,IAAI,OAAO,CAAC,IAAI,CAAC;CA4BhC"}
|