@volter/blender-engine 0.1.20 → 0.1.22
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.
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
PAGE_OWNED_DIRECTORIES,
|
|
38
38
|
SESSION_ROOT,
|
|
39
39
|
SESSION_SCRIPT,
|
|
40
|
+
sessionLevel,
|
|
40
41
|
sleep,
|
|
41
42
|
} from './blender-engine.mts';
|
|
42
43
|
import sessionPython from './session.py?raw';
|
|
@@ -195,10 +196,7 @@ export async function startEmscriptenBlenderEngine(
|
|
|
195
196
|
// session's OWN named conditions go there.
|
|
196
197
|
const say = (_level: 'log' | 'error', text: string) => {
|
|
197
198
|
if (text.startsWith('@@VOLTER-READY ')) readyLine = text.slice('@@VOLTER-READY '.length);
|
|
198
|
-
options.log(
|
|
199
|
-
text.startsWith('@@VOLTER-WARN') || text.startsWith('@@VOLTER-ERROR') ? 'error' : 'log',
|
|
200
|
-
text,
|
|
201
|
-
);
|
|
199
|
+
options.log(sessionLevel(text), text);
|
|
202
200
|
};
|
|
203
201
|
const module = await factory({
|
|
204
202
|
arguments: ['--background', '--factory-startup', '--python', SESSION_SCRIPT],
|
|
@@ -107,7 +107,7 @@ export interface BlenderFiles {
|
|
|
107
107
|
export interface BlenderEngineOptions {
|
|
108
108
|
/** The project's absolute path; the engine's filesystem mirrors it there. */
|
|
109
109
|
project: string;
|
|
110
|
-
log(level: 'log' | 'error', text: string): void;
|
|
110
|
+
log(level: 'log' | 'warn' | 'error', text: string): void;
|
|
111
111
|
/** What the session asks the TAB for, mid-call: a frame, and sometimes a
|
|
112
112
|
* photograph of it. Whatever this resolves to is what Python receives.
|
|
113
113
|
*
|
|
@@ -161,6 +161,14 @@ export interface BlenderEngine {
|
|
|
161
161
|
|
|
162
162
|
export const ARTIFACT_BASE = '/__editor/blender-wasm';
|
|
163
163
|
|
|
164
|
+
/** The console level of one line the session printed. Only the session's own named conditions
|
|
165
|
+
* reach the editor's console, each at the severity it names; Blender's streams stay page output. */
|
|
166
|
+
export function sessionLevel(line: string): 'log' | 'warn' | 'error' {
|
|
167
|
+
if (line.startsWith('@@VOLTER-ERROR')) return 'error';
|
|
168
|
+
if (line.startsWith('@@VOLTER-WARN')) return 'warn';
|
|
169
|
+
return 'log';
|
|
170
|
+
}
|
|
171
|
+
|
|
164
172
|
/**
|
|
165
173
|
* An artifact URL, resolved against THE MODULE — never against `self.location`.
|
|
166
174
|
*
|
|
@@ -52,6 +52,7 @@ import {
|
|
|
52
52
|
PAGE_OWNED_DIRECTORIES,
|
|
53
53
|
SESSION_ROOT,
|
|
54
54
|
SESSION_SCRIPT,
|
|
55
|
+
sessionLevel,
|
|
55
56
|
sleep,
|
|
56
57
|
} from './blender-engine.mts';
|
|
57
58
|
import sessionPython from './session.py?raw';
|
|
@@ -231,6 +232,110 @@ async function sharedRuntime(
|
|
|
231
232
|
return layer;
|
|
232
233
|
}
|
|
233
234
|
|
|
235
|
+
type StoreLayerFromIndex = (
|
|
236
|
+
root: string,
|
|
237
|
+
files: RuntimeIndex['files'],
|
|
238
|
+
store: string,
|
|
239
|
+
options: { mtimeMs: number; include?: (path: string) => boolean },
|
|
240
|
+
) => unknown;
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Blender's runtime tree as a STORE ON THE ORIGIN'S DISK the program reads
|
|
244
|
+
* (browser-substrate ADR-0048): installed once per version by the
|
|
245
|
+
* substrate's installer worker, and read file by file as Blender opens them,
|
|
246
|
+
* a large file by the ranges it reads. The shared layer before it copied all
|
|
247
|
+
* 96 MB into memory before Blender started (0.9 s of each boot) and held it
|
|
248
|
+
* for the run; a boot reads 268 of the 2,082 files (measured 2026-09-28).
|
|
249
|
+
* Undefined, and said, where it cannot be installed: the shared layer then
|
|
250
|
+
* carries the tree as before.
|
|
251
|
+
*/
|
|
252
|
+
async function installedRuntime(
|
|
253
|
+
fromIndex: StoreLayerFromIndex,
|
|
254
|
+
hold: ((projectId: string) => Promise<void>) | undefined,
|
|
255
|
+
log: BlenderEngineOptions['log'],
|
|
256
|
+
): Promise<unknown> {
|
|
257
|
+
const began = performance.now();
|
|
258
|
+
const indexAnswer = await fetch(artifactUrl('runtime.idx'));
|
|
259
|
+
if (!indexAnswer.ok)
|
|
260
|
+
throw new Error(`${ARTIFACT_BASE}/runtime.idx answered ${indexAnswer.status}`);
|
|
261
|
+
const text = await indexAnswer.text();
|
|
262
|
+
const index = JSON.parse(text) as RuntimeIndex;
|
|
263
|
+
// The tree's version is its index: the same files at the same sizes and modes, in the same order,
|
|
264
|
+
// and its newest file's time (`runtimeTag`'s validator), so a file rebuilt at the same size is a new version.
|
|
265
|
+
const digest = new Uint8Array(await crypto.subtle.digest('SHA-256', new TextEncoder().encode(text)));
|
|
266
|
+
const tag = [...digest.slice(0, 12)].map((byte) => byte.toString(16).padStart(2, '0')).join('');
|
|
267
|
+
const projectId = `${RUNTIME_STORE_FAMILY}${tag}`;
|
|
268
|
+
// THIS PAGE HOLDS ITS TREE IN USE from before it is installed to the end of the session, so
|
|
269
|
+
// another tab installing another version never removes it under this one's runs (ADR-0048).
|
|
270
|
+
if (hold) {
|
|
271
|
+
try {
|
|
272
|
+
await hold(projectId);
|
|
273
|
+
} catch (error) {
|
|
274
|
+
log('log', `blender: the runtime tree stays in memory: it cannot be held in use (${error instanceof Error ? error.message : String(error)})`);
|
|
275
|
+
return undefined;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
let worker: Worker;
|
|
279
|
+
try {
|
|
280
|
+
worker = new Worker(artifactUrl('wali/browser-wali/tree-store-worker.js'), { type: 'module' });
|
|
281
|
+
} catch (error) {
|
|
282
|
+
log('log', `blender: the runtime tree stays in memory: no installer worker (${error instanceof Error ? error.message : String(error)})`);
|
|
283
|
+
return undefined;
|
|
284
|
+
}
|
|
285
|
+
// A BOUNDED STALL, NOT A BOUNDED INSTALL: an install stalled in another tab (a throttled background
|
|
286
|
+
// tab) holds this tree's installer lock, or the network stops. Each batch written restarts the wait,
|
|
287
|
+
// so a slow link still installs. On a stall this boot carries the tree in memory, and the installer
|
|
288
|
+
// is left to finish for the next boot rather than stopped halfway.
|
|
289
|
+
const answer = await new Promise<{ ok: boolean; installed?: boolean; bytes?: number; error?: string }>((resolve) => {
|
|
290
|
+
let settled = false;
|
|
291
|
+
const settle = (value: { ok: boolean; installed?: boolean; bytes?: number; error?: string }): void => {
|
|
292
|
+
if (!settled) { settled = true; resolve(value); }
|
|
293
|
+
};
|
|
294
|
+
let timer = setTimeout(() => settle({ ok: false, error: `no progress in ${INSTALL_STALL_MS / 1000} s` }), INSTALL_STALL_MS);
|
|
295
|
+
worker.onmessage = (event) => {
|
|
296
|
+
clearTimeout(timer);
|
|
297
|
+
if (typeof event.data?.progress === 'number') {
|
|
298
|
+
if (!settled) timer = setTimeout(() => settle({ ok: false, error: `no progress in ${INSTALL_STALL_MS / 1000} s` }), INSTALL_STALL_MS);
|
|
299
|
+
return;
|
|
300
|
+
}
|
|
301
|
+
worker.terminate();
|
|
302
|
+
settle(event.data);
|
|
303
|
+
};
|
|
304
|
+
worker.onerror = (event) => {
|
|
305
|
+
clearTimeout(timer);
|
|
306
|
+
worker.terminate();
|
|
307
|
+
settle({ ok: false, error: event.message || 'the installer worker failed to start' });
|
|
308
|
+
};
|
|
309
|
+
worker.postMessage({
|
|
310
|
+
projectId,
|
|
311
|
+
family: RUNTIME_STORE_FAMILY,
|
|
312
|
+
root: RESOURCES,
|
|
313
|
+
files: index.files,
|
|
314
|
+
blobUrl: new URL(artifactUrl('runtime.bin'), location.href).href,
|
|
315
|
+
mtimeMs: Date.now(),
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
if (!answer.ok) {
|
|
319
|
+
log('log', `blender: the runtime tree stays in memory: it could not be installed (${answer.error})`);
|
|
320
|
+
return undefined;
|
|
321
|
+
}
|
|
322
|
+
const layer = fromIndex(RESOURCES, index.files, projectId, {
|
|
323
|
+
mtimeMs: Date.now(),
|
|
324
|
+
include: (path) => !PACK_RENDER_OVERRIDES.has(path),
|
|
325
|
+
});
|
|
326
|
+
log('log', `blender: Blender's ${index.files.length}-file runtime tree is on disk (${answer.installed ? `installed, ${Math.round((answer.bytes ?? 0) / 1e6)} MB` : 'installed before'}) in ${Math.round(performance.now() - began)} ms`);
|
|
327
|
+
return layer;
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
/**
|
|
331
|
+
* Installed runtime trees' store names begin so; installing one removes the others that no tab
|
|
332
|
+
* reads. `host-tree.` is the substrate's mark of an installed tree (`TREE_STORE_PREFIX`), which no
|
|
333
|
+
* project may take.
|
|
334
|
+
*/
|
|
335
|
+
const RUNTIME_STORE_FAMILY = 'host-tree.blender-runtime-';
|
|
336
|
+
/** How long a boot waits on an installer that writes nothing before it carries the tree in memory. */
|
|
337
|
+
const INSTALL_STALL_MS = 20_000;
|
|
338
|
+
|
|
234
339
|
/** The program's filesystem, as {@link BlenderFiles}. */
|
|
235
340
|
function programFiles(filesystem: BrowserFileSystemLike): BlenderFiles {
|
|
236
341
|
// One listener for the engine's life, and one waiter per wait: a change ends
|
|
@@ -316,7 +421,10 @@ export async function startWaliBlenderEngine(
|
|
|
316
421
|
])
|
|
317
422
|
filesystem.mkdirSync(directory, { recursive: true });
|
|
318
423
|
const fromIndex = layers['sharedFileLayerFromIndex'] as LayerFromIndex | undefined;
|
|
319
|
-
const
|
|
424
|
+
const fromStore = layers['storeFileLayerFromIndex'] as StoreLayerFromIndex | undefined;
|
|
425
|
+
const holdTree = layers['holdTreeStore'] as ((projectId: string) => Promise<void>) | undefined;
|
|
426
|
+
const runtimeLayer = (fromStore ? await installedRuntime(fromStore, holdTree, options.log) : undefined)
|
|
427
|
+
?? (fromIndex ? await sharedRuntime(fromIndex, options.log) : undefined);
|
|
320
428
|
if (!runtimeLayer) await stageRuntime(filesystem, options.log);
|
|
321
429
|
filesystem.writeFileSync(SESSION_SCRIPT, sessionPython);
|
|
322
430
|
|
|
@@ -381,10 +489,7 @@ export async function startWaliBlenderEngine(
|
|
|
381
489
|
// -- the same rule as the standalone engine, and for the same measured
|
|
382
490
|
// reason: only the session's own named conditions belong in the set an
|
|
383
491
|
// agent must drive to zero.
|
|
384
|
-
options.log(
|
|
385
|
-
line.startsWith('@@VOLTER-WARN') || line.startsWith('@@VOLTER-ERROR') ? 'error' : 'log',
|
|
386
|
-
line,
|
|
387
|
-
);
|
|
492
|
+
options.log(sessionLevel(line), line);
|
|
388
493
|
}
|
|
389
494
|
};
|
|
390
495
|
// THE PROGRAM DOES NOT RETURN. `session.py` loops forever, so `run` settles
|
package/browser/protocol.ts
CHANGED
|
@@ -109,7 +109,7 @@ export type WorkerReply =
|
|
|
109
109
|
* as the record of what the session submitted — the second side a
|
|
110
110
|
* displayed-versus-submitted comparison otherwise does not have. */
|
|
111
111
|
| { op: 'present'; id: number; frame: unknown; description: unknown; capture?: CaptureRequest }
|
|
112
|
-
| { op: 'log'; level: 'log' | 'error'; text: string }
|
|
112
|
+
| { op: 'log'; level: 'log' | 'warn' | 'error'; text: string }
|
|
113
113
|
/**
|
|
114
114
|
* HOW BIG THE MODULE'S LINEAR MEMORY IS, posted after every call.
|
|
115
115
|
*
|
package/browser/runtime.ts
CHANGED
|
@@ -44,7 +44,7 @@ export interface BlenderRuntimeOptions {
|
|
|
44
44
|
description: unknown,
|
|
45
45
|
capture?: CaptureRequest,
|
|
46
46
|
): Promise<PresentAnswer> | PresentAnswer;
|
|
47
|
-
log?(level: 'log' | 'error', text: string): void;
|
|
47
|
+
log?(level: 'log' | 'warn' | 'error', text: string): void;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
50
|
/**
|
package/browser/worker.ts
CHANGED
|
@@ -41,7 +41,7 @@ import type { CaptureRequest, FileEntry, WorkerReply, WorkerRequest } from './pr
|
|
|
41
41
|
import { columnsToTypedArrays, describeFrame } from './session-frame.mts';
|
|
42
42
|
|
|
43
43
|
const post = (reply: WorkerReply) => (self as unknown as Worker).postMessage(reply);
|
|
44
|
-
const log = (level: 'log' | 'error', text: string) => post({ op: 'log', level, text });
|
|
44
|
+
const log = (level: 'log' | 'warn' | 'error', text: string) => post({ op: 'log', level, text });
|
|
45
45
|
|
|
46
46
|
interface Session {
|
|
47
47
|
start(project: string): Promise<unknown>;
|