@volter/blender-engine 0.1.50 → 0.1.52
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/browser/blender-emscripten-engine.mts +3 -1
- package/browser/blender-engine.mts +3 -0
- package/browser/frame-transfer.mts +19 -0
- package/browser/stream-file.mts +54 -0
- package/browser/worker.ts +22 -28
- package/package.json +1 -1
- package/wasm/BUNDLE.json +9 -11
- package/wasm/blender_browser.wasm.br +0 -0
|
@@ -27,6 +27,7 @@
|
|
|
27
27
|
* `session.py` chmods its own root as its first act.
|
|
28
28
|
*/
|
|
29
29
|
|
|
30
|
+
import { writeStreamedFile, type StreamFileSystem } from './stream-file.mts';
|
|
30
31
|
import {
|
|
31
32
|
artifactUrl,
|
|
32
33
|
type BlenderArtifactStatus,
|
|
@@ -44,7 +45,7 @@ import sessionPython from './session.py?raw';
|
|
|
44
45
|
|
|
45
46
|
/** What the module factory is, in the only shape this file uses. */
|
|
46
47
|
interface BlenderModule {
|
|
47
|
-
FS: {
|
|
48
|
+
FS: StreamFileSystem & {
|
|
48
49
|
readFile(path: string): Uint8Array;
|
|
49
50
|
writeFile(path: string, data: string | Uint8Array): void;
|
|
50
51
|
mkdirTree(path: string): void;
|
|
@@ -115,6 +116,7 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
|
|
|
115
116
|
readFile: async (path) => FS.readFile(path),
|
|
116
117
|
// Not `async`: a write that throws throws here, so `request` never starts `.done` after a failed `.json`.
|
|
117
118
|
writeFile: (path, data) => { FS.writeFile(path, data); return Promise.resolve(); },
|
|
119
|
+
writeFileStream: (path, body, size) => writeStreamedFile(FS, path, body, size),
|
|
118
120
|
mkdirTree: async (path) => FS.mkdirTree(path),
|
|
119
121
|
readdir: async (path) => FS.readdir(path),
|
|
120
122
|
stat: async (path) => {
|
|
@@ -85,6 +85,9 @@ export interface BlenderFileStat {
|
|
|
85
85
|
export interface BlenderFiles {
|
|
86
86
|
readFile(path: string): Promise<Uint8Array>;
|
|
87
87
|
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
88
|
+
/** Stream a known-size file into the engine without a second whole-file JS buffer.
|
|
89
|
+
* A failed transfer leaves the previous file intact. */
|
|
90
|
+
writeFileStream?(path: string, body: ReadableStream<Uint8Array>, size: number): Promise<void>;
|
|
88
91
|
mkdirTree(path: string): Promise<void>;
|
|
89
92
|
readdir(path: string): Promise<string[]>;
|
|
90
93
|
/** Null when the path is not there. */
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/** Buffers at every depth of a frame move to the presenter exactly once. */
|
|
2
|
+
export function frameTransferBuffers(frame: unknown): ArrayBuffer[] {
|
|
3
|
+
const buffers = new Set<ArrayBuffer>();
|
|
4
|
+
const visited = new Set<object>();
|
|
5
|
+
const walk = (value: unknown): void => {
|
|
6
|
+
if (value === null || typeof value !== 'object' || visited.has(value)) return;
|
|
7
|
+
visited.add(value);
|
|
8
|
+
if (value instanceof ArrayBuffer) {
|
|
9
|
+
buffers.add(value);
|
|
10
|
+
} else if (ArrayBuffer.isView(value)) {
|
|
11
|
+
// Shared Wasm memory cannot be transferred; arena readers make owned copies.
|
|
12
|
+
if (value.buffer instanceof ArrayBuffer) buffers.add(value.buffer);
|
|
13
|
+
} else {
|
|
14
|
+
for (const child of Object.values(value)) walk(child);
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
walk(frame);
|
|
18
|
+
return [...buffers];
|
|
19
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/** File imports publish only after every declared byte arrived. WasmFS stores the one final
|
|
2
|
+
* backing allocation; each write and its temporary Wasm copy are bounded to one MiB. */
|
|
3
|
+
export interface StreamFileSystem {
|
|
4
|
+
open(path: string, flags: string): { fd: number };
|
|
5
|
+
close(stream: { fd: number }): void;
|
|
6
|
+
write(stream: { fd: number }, data: Uint8Array, offset: number, length: number, position: number): number;
|
|
7
|
+
truncate(path: string, length: number): void;
|
|
8
|
+
rename(from: string, to: string): void;
|
|
9
|
+
unlink(path: string): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export async function writeStreamedFile(
|
|
13
|
+
FS: StreamFileSystem,
|
|
14
|
+
path: string,
|
|
15
|
+
body: ReadableStream<Uint8Array>,
|
|
16
|
+
size: number,
|
|
17
|
+
): Promise<void> {
|
|
18
|
+
if (!Number.isSafeInteger(size) || size < 0) throw new Error(`${path}: invalid file size ${size}`);
|
|
19
|
+
const temporary = `${path}.volter-stage-${crypto.randomUUID()}`;
|
|
20
|
+
const reader = body.getReader();
|
|
21
|
+
let stream: { fd: number } | undefined;
|
|
22
|
+
let complete = false;
|
|
23
|
+
try {
|
|
24
|
+
stream = FS.open(temporary, 'w');
|
|
25
|
+
// Reserve the exact backing once: growing WasmFS's vector while importing can otherwise
|
|
26
|
+
// hold both the old and the new allocation in the non-shrinking Wasm heap.
|
|
27
|
+
FS.truncate(temporary, size);
|
|
28
|
+
let position = 0;
|
|
29
|
+
for (;;) {
|
|
30
|
+
const { done, value } = await reader.read();
|
|
31
|
+
if (done) break;
|
|
32
|
+
if (position + value.byteLength > size) throw new Error(`${path}: file grew during import`);
|
|
33
|
+
for (let offset = 0; offset < value.byteLength;) {
|
|
34
|
+
const length = Math.min(1024 * 1024, value.byteLength - offset);
|
|
35
|
+
const written = FS.write(stream, value, offset, length, position);
|
|
36
|
+
if (written <= 0 || written > length) throw new Error(`${path}: invalid file write (${written})`);
|
|
37
|
+
offset += written;
|
|
38
|
+
position += written;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
if (position !== size) throw new Error(`${path}: expected ${size} bytes, received ${position}`);
|
|
42
|
+
FS.close(stream);
|
|
43
|
+
stream = undefined;
|
|
44
|
+
FS.rename(temporary, path);
|
|
45
|
+
complete = true;
|
|
46
|
+
} finally {
|
|
47
|
+
if (!complete) await reader.cancel().catch(() => {});
|
|
48
|
+
reader.releaseLock();
|
|
49
|
+
if (stream) FS.close(stream);
|
|
50
|
+
if (!complete) {
|
|
51
|
+
try { FS.unlink(temporary); } catch { /* open may have failed before creating the file */ }
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
package/browser/worker.ts
CHANGED
|
@@ -39,6 +39,7 @@
|
|
|
39
39
|
|
|
40
40
|
import { type BlenderEngine, type BlenderFiles, startBlenderEngine } from './blender-engine.mts';
|
|
41
41
|
import type { CaptureRequest, FileEntry, WorkerReply, WorkerRequest } from './protocol';
|
|
42
|
+
import { frameTransferBuffers } from './frame-transfer.mts';
|
|
42
43
|
import { documentChunks } from './document-chunks.mts';
|
|
43
44
|
import { columnsToTypedArrays, describeFrame, isColumnDescriptor } from './session-frame.mts';
|
|
44
45
|
|
|
@@ -75,35 +76,24 @@ function presentToTab(
|
|
|
75
76
|
const id = ++presentId;
|
|
76
77
|
// A drawn mesh's buffers move to the tab rather than being copied: the
|
|
77
78
|
// frame is the tab's from here on.
|
|
78
|
-
const transfer
|
|
79
|
-
// A picture travels the same way and for the same reason: an entry in the
|
|
80
|
-
// frame's `images` carries a raw RGBA raster the tab uploads into a
|
|
81
|
-
// `DataTexture` (`blender-runtime-view.ts`), once per image per revision.
|
|
82
|
-
const parts = frame as {
|
|
83
|
-
meshes?: Record<string, unknown>;
|
|
84
|
-
images?: Record<string, unknown>;
|
|
85
|
-
} | null;
|
|
86
|
-
for (const carrier of [parts?.meshes ?? {}, parts?.images ?? {}])
|
|
87
|
-
for (const held of Object.values(carrier))
|
|
88
|
-
for (const value of Object.values(held as Record<string, unknown>))
|
|
89
|
-
if (
|
|
90
|
-
ArrayBuffer.isView(value) &&
|
|
91
|
-
value.buffer instanceof ArrayBuffer &&
|
|
92
|
-
!transfer.includes(value.buffer)
|
|
93
|
-
)
|
|
94
|
-
transfer.push(value.buffer);
|
|
79
|
+
const transfer = frameTransferBuffers(frame);
|
|
95
80
|
return new Promise((resolve, reject) => {
|
|
96
81
|
pendingPresents.set(id, { resolve, reject });
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
82
|
+
try {
|
|
83
|
+
(self as unknown as Worker).postMessage(
|
|
84
|
+
{
|
|
85
|
+
op: 'present',
|
|
86
|
+
id,
|
|
87
|
+
frame,
|
|
88
|
+
description,
|
|
89
|
+
...(capture ? { capture } : {}),
|
|
90
|
+
} satisfies WorkerReply,
|
|
91
|
+
transfer,
|
|
92
|
+
);
|
|
93
|
+
} catch (error) {
|
|
94
|
+
pendingPresents.delete(id);
|
|
95
|
+
reject(error);
|
|
96
|
+
}
|
|
107
97
|
});
|
|
108
98
|
}
|
|
109
99
|
|
|
@@ -535,7 +525,11 @@ async function stageProjectFiles(files_: BlenderFiles, project: string): Promise
|
|
|
535
525
|
}
|
|
536
526
|
const dir = path.slice(0, path.lastIndexOf('/'));
|
|
537
527
|
if (dir) await files_.mkdirTree(dir);
|
|
538
|
-
|
|
528
|
+
if (files_.writeFileStream && answer.body) {
|
|
529
|
+
await files_.writeFileStream(path, answer.body, file.size);
|
|
530
|
+
} else {
|
|
531
|
+
await files_.writeFile(path, new Uint8Array(await answer.arrayBuffer()));
|
|
532
|
+
}
|
|
539
533
|
staged.set(path, { host: stamp, engine: await stampOf(files_, path) });
|
|
540
534
|
}
|
|
541
535
|
}
|
package/package.json
CHANGED
package/wasm/BUNDLE.json
CHANGED
|
@@ -1,12 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"blender": "5.2.0 LTS",
|
|
3
|
-
"source": "
|
|
4
|
-
"reviewedSource": "
|
|
3
|
+
"source": "e5d7eec67572dfc7c73f8f4d3f601d03442546f9",
|
|
4
|
+
"reviewedSource": "e5d7eec67572dfc7c73f8f4d3f601d03442546f9",
|
|
5
5
|
"upstream": "fbe6228777e7d9afefcd61a413844e790ae75db7",
|
|
6
6
|
"sourceRepository": "https://github.com/volter-ai/blender",
|
|
7
|
-
"sourceRecipe": "https://github.com/volter-ai/blender/blob/
|
|
8
|
-
"sourceRelease": "https://github.com/volter-ai/blender/releases/tag/blender-5.2.0-wasm.4",
|
|
9
|
-
"verification": "https://github.com/volter-ai/blender/releases/download/blender-5.2.0-wasm.4/VERIFICATION.json",
|
|
7
|
+
"sourceRecipe": "https://github.com/volter-ai/blender/blob/e5d7eec67572dfc7c73f8f4d3f601d03442546f9/build_files/emscripten/RELEASE.md",
|
|
10
8
|
"factoryEngine": "BLENDER_EEVEE",
|
|
11
9
|
"files": {
|
|
12
10
|
"blender_browser.js": "raw",
|
|
@@ -19,8 +17,8 @@
|
|
|
19
17
|
"sha256": "0c63c29e7d4b1ba1fce4e94057778b0757767b25ea4f6f01d329e99351568f30"
|
|
20
18
|
},
|
|
21
19
|
"blender_browser.wasm": {
|
|
22
|
-
"bytes":
|
|
23
|
-
"sha256": "
|
|
20
|
+
"bytes": 86317655,
|
|
21
|
+
"sha256": "0ba94ac8388de893bcec4da70516aea69128addaa490ef27f17ca06262ee6546"
|
|
24
22
|
},
|
|
25
23
|
"blender_browser.data": {
|
|
26
24
|
"bytes": 52698568,
|
|
@@ -33,8 +31,8 @@
|
|
|
33
31
|
"sha256": "0c63c29e7d4b1ba1fce4e94057778b0757767b25ea4f6f01d329e99351568f30"
|
|
34
32
|
},
|
|
35
33
|
"blender_browser.wasm.br": {
|
|
36
|
-
"bytes":
|
|
37
|
-
"sha256": "
|
|
34
|
+
"bytes": 18596913,
|
|
35
|
+
"sha256": "64af23954e1911ffcb879f69a9e8c9bbd83b5531939b586228f44fcea8211e3b"
|
|
38
36
|
},
|
|
39
37
|
"blender_browser.data.br": {
|
|
40
38
|
"bytes": 8056563,
|
|
@@ -45,9 +43,9 @@
|
|
|
45
43
|
"sha256": "4085e9336266cf66f061d4d5c962a5929cc96d5732c746b5c13ea05cd104a75b"
|
|
46
44
|
}
|
|
47
45
|
},
|
|
48
|
-
"note": "
|
|
46
|
+
"note": "Unreleased Stoneguard candidate. Clean source build at the named commit; Release -O3 objects and -O2 link, Emscripten 6.0.5. Focused geometry regression matches shipped engine across original, vertex edit and subdivision-level edit. Full historical battery is absent from current editor checkout; no full-battery claim.",
|
|
49
47
|
"callTrampoline": "CPython 3.13.13 is compiled with PY_CALL_TRAMPOLINE for Blender CPython slot arity casts. This applies to CPython dispatch, not arbitrary indirect calls.",
|
|
50
48
|
"engineIds": "The existing session registers Python render engines under CYCLES, BLENDER_EEVEE and BLENDER_WORKBENCH; the headless build permits built-in IDs to be shadowed.",
|
|
51
|
-
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build.",
|
|
49
|
+
"exportDoor": "The source-owned bpy_web_export.cc exports Principled physical inputs, normal images, eight named UV layers and evaluated corner normals, and takes graph_materials/graph_images/graph_generated (a graph-drawn shader mix reduced to the one Principled BSDF it reaches, graph images at the door revisions with every UDIM tile, orco for deformed meshes whose graph reads Generated coordinates). A frame can defer its changed meshes and pictures (defer) for the caller to pull one at a time (export_mesh, export_image), each call freeing the previous one's columns and a picture it decoded; an undo step keeps the revisions when Blender kept the depsgraph (session_undo); Blender's CPU subdivision descriptor is released once a mesh is read; the memory door reports malloc in-use and free. Datablock revisions are keyed by session_uid. writefile.cc writes one copy of the document per save in the browser build. CPU evaluators adopt stencil tables without cloning; browser subdivision descriptors are released immediately after CPU tessellation/deformation.",
|
|
52
50
|
"preloadedPayload": "releasePreloadedFileData releases the JS preload buffer after session ready. The measured working payload exclusions are retained."
|
|
53
51
|
}
|
|
Binary file
|