@volter/blender-engine 0.1.62 → 0.1.63
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.
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* `session.py` chmods its own root as its first act.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { checkpointStream } from './pull-job.mts';
|
|
30
31
|
import { writeStreamedFile, type StreamFileSystem } from './stream-file.mts';
|
|
31
32
|
import {
|
|
32
33
|
artifactUrl,
|
|
@@ -149,14 +150,17 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
|
|
|
149
150
|
*/
|
|
150
151
|
const ARTIFACT_CACHE = 'volter-blender-artifacts';
|
|
151
152
|
|
|
152
|
-
async function cachedArtifact(file: string, digest: string | undefined): Promise<Response> {
|
|
153
|
+
async function cachedArtifact(file: string, digest: string | undefined, checkpoint: () => Promise<void>): Promise<Response> {
|
|
154
|
+
const bounded = (response: Response): Response => response.ok && response.body
|
|
155
|
+
? new Response(checkpointStream(response.body, checkpoint), { status: response.status, headers: response.headers })
|
|
156
|
+
: response;
|
|
153
157
|
const url = artifactUrl(file);
|
|
154
|
-
if (!digest || typeof caches === 'undefined') return fetch(url);
|
|
158
|
+
if (!digest || typeof caches === 'undefined') return bounded(await fetch(url));
|
|
155
159
|
const key = `${url}?sha256=${digest}`;
|
|
156
160
|
const cache = await caches.open(ARTIFACT_CACHE);
|
|
157
161
|
const hit = await cache.match(key);
|
|
158
|
-
if (hit) return hit;
|
|
159
|
-
const response = await fetch(url);
|
|
162
|
+
if (hit) return bounded(hit);
|
|
163
|
+
const response = bounded(await fetch(url));
|
|
160
164
|
if (!response.ok) return response;
|
|
161
165
|
// One build's bytes per file: an older build's are dropped as this one lands.
|
|
162
166
|
for (const old of await cache.keys()) if (old.url.startsWith(`${url}?sha256=`) && old.url !== key) await cache.delete(old);
|
|
@@ -177,15 +181,18 @@ export async function startEmscriptenBlenderEngine(
|
|
|
177
181
|
);
|
|
178
182
|
const glueUrl = artifactUrl('blender_browser.js');
|
|
179
183
|
const digests = status.digests ?? {};
|
|
184
|
+
const checkpoint = async (phase: string): Promise<void> => { await options.ask({ checkpoint: phase }); };
|
|
185
|
+
const artifact = (file: string) => cachedArtifact(file, digests[file], () => checkpoint(`artifact/${file}`));
|
|
180
186
|
// The `.data` package is handed to the glue whole (`getPreloadedPackage`),
|
|
181
187
|
// so it is read before the module starts; the wasm streams in beside it.
|
|
182
188
|
const [factory, preloaded] = await Promise.all([
|
|
183
189
|
loadFactory(glueUrl),
|
|
184
|
-
|
|
190
|
+
artifact('blender_browser.data').then(async (response) => {
|
|
185
191
|
if (!response.ok) throw new Error(`blender_browser.data: HTTP ${response.status}`);
|
|
186
192
|
return response.arrayBuffer();
|
|
187
193
|
}),
|
|
188
194
|
]);
|
|
195
|
+
await checkpoint('runtime-package-ready');
|
|
189
196
|
let bootError: unknown = null;
|
|
190
197
|
const started = performance.now();
|
|
191
198
|
let readyLine: string | null = null;
|
|
@@ -221,9 +228,10 @@ export async function startEmscriptenBlenderEngine(
|
|
|
221
228
|
getPreloadedPackage: () => preloaded,
|
|
222
229
|
instantiateWasm: (imports: WebAssembly.Imports, receive: (instance: WebAssembly.Instance, module: WebAssembly.Module) => void) => {
|
|
223
230
|
void (async () => {
|
|
224
|
-
const response = await
|
|
231
|
+
const response = await artifact('blender_browser.wasm');
|
|
225
232
|
if (!response.ok) throw new Error(`blender_browser.wasm: HTTP ${response.status}`);
|
|
226
233
|
const { instance, module: compiled } = await WebAssembly.instantiateStreaming(response, imports);
|
|
234
|
+
await checkpoint('wasm-instantiated');
|
|
227
235
|
receive(instance, compiled);
|
|
228
236
|
})().catch((error: unknown) => { bootError = error; });
|
|
229
237
|
return {};
|
|
@@ -273,6 +281,7 @@ export async function startEmscriptenBlenderEngine(
|
|
|
273
281
|
}).finally(() => {
|
|
274
282
|
(WebAssembly as { Memory: unknown }).Memory = Memory;
|
|
275
283
|
});
|
|
284
|
+
await checkpoint('runtime-initialized');
|
|
276
285
|
// `main()` runs on a pthread (`-sPROXY_TO_PTHREAD`), so the factory resolves
|
|
277
286
|
// long before the session exists. The ready line is what says it does.
|
|
278
287
|
while (readyLine === null) {
|
|
@@ -337,7 +346,8 @@ export async function startEmscriptenBlenderEngine(
|
|
|
337
346
|
|
|
338
347
|
const files = moduleFiles(module);
|
|
339
348
|
FS.chmod('/bw/datafiles', 0o755);
|
|
340
|
-
await
|
|
349
|
+
await checkpoint('python-ready');
|
|
350
|
+
await mountEssentials(files, digests['essentials.bin'], checkpoint);
|
|
341
351
|
const { request } = openSessionChannel(files, options);
|
|
342
352
|
|
|
343
353
|
return {
|
|
@@ -364,9 +374,9 @@ export async function startEmscriptenBlenderEngine(
|
|
|
364
374
|
/** Assets are data, not a second engine. Ship them separately so a data update
|
|
365
375
|
* does not relink the 86 MB Wasm binary. Both payload and per-file bounds are
|
|
366
376
|
* checked before anything enters Blender's filesystem. */
|
|
367
|
-
async function mountEssentials(files: BlenderFiles, digest: string | undefined): Promise<void> {
|
|
377
|
+
async function mountEssentials(files: BlenderFiles, digest: string | undefined, checkpoint: (phase: string) => Promise<void>): Promise<void> {
|
|
368
378
|
const [indexResponse, payloadResponse] = await Promise.all([
|
|
369
|
-
fetch(artifactUrl('essentials.json')), cachedArtifact('essentials.bin', digest),
|
|
379
|
+
fetch(artifactUrl('essentials.json')), cachedArtifact('essentials.bin', digest, () => checkpoint('artifact/essentials.bin')),
|
|
370
380
|
]);
|
|
371
381
|
if (!indexResponse.ok || !payloadResponse.ok)
|
|
372
382
|
throw new Error(`Blender Essentials assets are missing (${indexResponse.status}/${payloadResponse.status})`);
|
|
@@ -380,6 +390,7 @@ async function mountEssentials(files: BlenderFiles, digest: string | undefined):
|
|
|
380
390
|
if (payload.byteLength !== index.bytes || hash !== index.sha256)
|
|
381
391
|
throw new Error('Blender Essentials payload does not match its source manifest');
|
|
382
392
|
for (const file of index.files) {
|
|
393
|
+
await checkpoint('essentials-file');
|
|
383
394
|
if (!file.path || file.path.includes('\\') || file.path.split('/').some(p => !p || p === '.' || p === '..') ||
|
|
384
395
|
!Number.isInteger(file.offset) || !Number.isInteger(file.bytes) ||
|
|
385
396
|
file.offset < 0 || file.bytes < 0 || file.offset + file.bytes > payload.byteLength)
|
|
@@ -258,6 +258,7 @@ export async function artifactStatus(): Promise<BlenderArtifactStatus> {
|
|
|
258
258
|
*/
|
|
259
259
|
export async function startBlenderEngine(options: BlenderEngineOptions): Promise<BlenderEngine> {
|
|
260
260
|
const status = await artifactStatus();
|
|
261
|
+
await options.ask({ checkpoint: 'artifact-status' });
|
|
261
262
|
if (!status.available)
|
|
262
263
|
throw new Error(
|
|
263
264
|
`The headless Blender WebAssembly build is not served by this editor: ${status.missing.join('; ')}`,
|
package/browser/runtime.ts
CHANGED
|
@@ -219,6 +219,8 @@ export class BlenderRuntime {
|
|
|
219
219
|
for (const id of [...this.#pending.keys()]) this.#settled(id);
|
|
220
220
|
for (const pending of this.#pending.values()) pending.reject(error);
|
|
221
221
|
this.#pending.clear();
|
|
222
|
+
// Queued logical operations must reject too; this worker cannot answer them.
|
|
223
|
+
this.terminate();
|
|
222
224
|
};
|
|
223
225
|
}
|
|
224
226
|
|