@volter/blender-engine 0.1.148 → 0.1.149
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 +16 -0
- package/README.md +4 -11
- package/browser/addons/vrm/LICENSE_(OPTION1)_MIT.txt +22 -0
- package/browser/addons/vrm/LICENSE_(OPTION2)_GPL-3_0.txt +674 -0
- package/browser/addons/vrm/LICENSE_MAIN.txt +1 -0
- package/browser/addons/vrm/README.txt +3 -0
- package/browser/addons/vrm/__init__.py +41 -0
- package/browser/addons/vrm/blender_manifest.toml +16 -0
- package/browser/addons/vrm/common/__init__.py +1 -0
- package/browser/addons/vrm/common/animation.py +58 -0
- package/browser/addons/vrm/common/blender_manifest.py +82 -0
- package/browser/addons/vrm/common/char.py +16 -0
- package/browser/addons/vrm/common/convert.py +287 -0
- package/browser/addons/vrm/common/convert_any.py +48 -0
- package/browser/addons/vrm/common/debug.py +40 -0
- package/browser/addons/vrm/common/deep.py +162 -0
- package/browser/addons/vrm/common/error_dialog.py +274 -0
- package/browser/addons/vrm/common/fs.py +71 -0
- package/browser/addons/vrm/common/gl.py +20 -0
- package/browser/addons/vrm/common/gltf.py +1149 -0
- package/browser/addons/vrm/common/human_bone_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/human_bone_mapper/biped_mapping.py +84 -0
- package/browser/addons/vrm/common/human_bone_mapper/cats_blender_plugin_fix_model_mapping.py +63 -0
- package/browser/addons/vrm/common/human_bone_mapper/human_bone_mapper.py +203 -0
- package/browser/addons/vrm/common/human_bone_mapper/microsoft_rocketbox_mapping.py +69 -0
- package/browser/addons/vrm/common/human_bone_mapper/mixamo_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/mmd_mapping.py +115 -0
- package/browser/addons/vrm/common/human_bone_mapper/mpfb_mapping.py +120 -0
- package/browser/addons/vrm/common/human_bone_mapper/ready_player_me_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/reallusion_mapping.py +94 -0
- package/browser/addons/vrm/common/human_bone_mapper/rigify_meta_rig_mapping.py +65 -0
- package/browser/addons/vrm/common/human_bone_mapper/structure_based_mapping.py +891 -0
- package/browser/addons/vrm/common/human_bone_mapper/unreal_mapping.py +62 -0
- package/browser/addons/vrm/common/human_bone_mapper/vrm_addon_mapping.py +112 -0
- package/browser/addons/vrm/common/human_bone_mapper/vroid_mapping.py +89 -0
- package/browser/addons/vrm/common/legacy_gltf.py +16 -0
- package/browser/addons/vrm/common/logger.py +84 -0
- package/browser/addons/vrm/common/mtoon0.blend +0 -0
- package/browser/addons/vrm/common/mtoon1.blend +0 -0
- package/browser/addons/vrm/common/mtoon1_outline.blend +0 -0
- package/browser/addons/vrm/common/mtoon_auto_setup.blend +0 -0
- package/browser/addons/vrm/common/mtoon_unversioned.py +62 -0
- package/browser/addons/vrm/common/ops/__init__.py +10 -0
- package/browser/addons/vrm/common/ops/export_scene.py +67 -0
- package/browser/addons/vrm/common/ops/icyp.py +53 -0
- package/browser/addons/vrm/common/ops/import_scene.py +57 -0
- package/browser/addons/vrm/common/ops/vrm.py +2380 -0
- package/browser/addons/vrm/common/ops/wm.py +167 -0
- package/browser/addons/vrm/common/preferences.py +362 -0
- package/browser/addons/vrm/common/progress.py +87 -0
- package/browser/addons/vrm/common/rotation.py +89 -0
- package/browser/addons/vrm/common/safe_removal.py +46 -0
- package/browser/addons/vrm/common/scene_watcher.py +230 -0
- package/browser/addons/vrm/common/shader.py +2243 -0
- package/browser/addons/vrm/common/shape_key_mapper/__init__.py +1 -0
- package/browser/addons/vrm/common/shape_key_mapper/arkit_mapping.py +168 -0
- package/browser/addons/vrm/common/shape_key_mapper/mmd_mapping.py +62 -0
- package/browser/addons/vrm/common/shape_key_mapper/ready_player_me_mapping.py +52 -0
- package/browser/addons/vrm/common/shape_key_mapper/vrchat_mapping.py +17 -0
- package/browser/addons/vrm/common/third_party_user_extension.py +214 -0
- package/browser/addons/vrm/common/transparent_z_write.blend +0 -0
- package/browser/addons/vrm/common/version.py +161 -0
- package/browser/addons/vrm/common/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm0/human_bone.py +662 -0
- package/browser/addons/vrm/common/vrm0/material_property.py +346 -0
- package/browser/addons/vrm/common/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/common/vrm1/expression_preset.py +207 -0
- package/browser/addons/vrm/common/vrm1/human_bone.py +822 -0
- package/browser/addons/vrm/common/workspace.py +227 -0
- package/browser/addons/vrm/common/writable_context.py +77 -0
- package/browser/addons/vrm/documentation/en-us/animation/index.md +63 -0
- package/browser/addons/vrm/documentation/en-us/create-humanoid-vrm-from-scratch/index.md +131 -0
- package/browser/addons/vrm/documentation/en-us/create-simple-vrm-from-scratch/index.md +30 -0
- package/browser/addons/vrm/documentation/en-us/development/index.md +136 -0
- package/browser/addons/vrm/documentation/en-us/index.md +41 -0
- package/browser/addons/vrm/documentation/en-us/installation/index.md +152 -0
- package/browser/addons/vrm/documentation/en-us/material-mtoon/index.md +122 -0
- package/browser/addons/vrm/documentation/en-us/material-pbr/index.md +94 -0
- package/browser/addons/vrm/documentation/en-us/scripting-api/index.md +391 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrm/index.md +57 -0
- package/browser/addons/vrm/documentation/en-us/ui/export_scene.vrma/index.md +17 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrm/index.md +61 -0
- package/browser/addons/vrm/documentation/en-us/ui/import_scene.vrma/index.md +13 -0
- package/browser/addons/vrm/documentation/ja-jp/animation/index.md +57 -0
- package/browser/addons/vrm/documentation/ja-jp/create-humanoid-vrm-from-scratch/index.md +129 -0
- package/browser/addons/vrm/documentation/ja-jp/create-simple-vrm-from-scratch/index.md +29 -0
- package/browser/addons/vrm/documentation/ja-jp/development/index.md +137 -0
- package/browser/addons/vrm/documentation/ja-jp/index.md +37 -0
- package/browser/addons/vrm/documentation/ja-jp/installation/index.md +150 -0
- package/browser/addons/vrm/documentation/ja-jp/material-mtoon/index.md +113 -0
- package/browser/addons/vrm/documentation/ja-jp/material-pbr/index.md +90 -0
- package/browser/addons/vrm/documentation/ja-jp/scripting-api/index.md +389 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrm/index.md +55 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/export_scene.vrma/index.md +19 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrm/index.md +56 -0
- package/browser/addons/vrm/documentation/ja-jp/ui/import_scene.vrma/index.md +14 -0
- package/browser/addons/vrm/editor/__init__.py +1 -0
- package/browser/addons/vrm/editor/extension.py +425 -0
- package/browser/addons/vrm/editor/extension_accessor.py +75 -0
- package/browser/addons/vrm/editor/handler.py +84 -0
- package/browser/addons/vrm/editor/khr_character/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_character/panel.py +69 -0
- package/browser/addons/vrm/editor/khr_character/property_group.py +27 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/__init__.py +1 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ops.py +464 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/panel.py +136 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/property_group.py +83 -0
- package/browser/addons/vrm/editor/khr_xmp_json_ld/ui_list.py +198 -0
- package/browser/addons/vrm/editor/make_armature.py +709 -0
- package/browser/addons/vrm/editor/menu.py +147 -0
- package/browser/addons/vrm/editor/migration.py +297 -0
- package/browser/addons/vrm/editor/mtoon1/__init__.py +1 -0
- package/browser/addons/vrm/editor/mtoon1/handler.py +20 -0
- package/browser/addons/vrm/editor/mtoon1/migration.py +536 -0
- package/browser/addons/vrm/editor/mtoon1/ops.py +304 -0
- package/browser/addons/vrm/editor/mtoon1/panel.py +442 -0
- package/browser/addons/vrm/editor/mtoon1/property_group.py +4977 -0
- package/browser/addons/vrm/editor/mtoon1/scene_watcher.py +381 -0
- package/browser/addons/vrm/editor/node_constraint1/__init__.py +1 -0
- package/browser/addons/vrm/editor/node_constraint1/panel.py +317 -0
- package/browser/addons/vrm/editor/node_constraint1/property_group.py +25 -0
- package/browser/addons/vrm/editor/ops.py +471 -0
- package/browser/addons/vrm/editor/panel.py +315 -0
- package/browser/addons/vrm/editor/property_group.py +888 -0
- package/browser/addons/vrm/editor/search.py +682 -0
- package/browser/addons/vrm/editor/spring_bone1/__init__.py +1 -0
- package/browser/addons/vrm/editor/spring_bone1/handler.py +994 -0
- package/browser/addons/vrm/editor/spring_bone1/menu.py +246 -0
- package/browser/addons/vrm/editor/spring_bone1/migration.py +86 -0
- package/browser/addons/vrm/editor/spring_bone1/ops.py +1679 -0
- package/browser/addons/vrm/editor/spring_bone1/panel.py +605 -0
- package/browser/addons/vrm/editor/spring_bone1/property_group.py +1457 -0
- package/browser/addons/vrm/editor/spring_bone1/ui_list.py +408 -0
- package/browser/addons/vrm/editor/subscription.py +123 -0
- package/browser/addons/vrm/editor/t_pose.py +713 -0
- package/browser/addons/vrm/editor/validation.py +1271 -0
- package/browser/addons/vrm/editor/vrm0/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm0/gizmo_group.py +63 -0
- package/browser/addons/vrm/editor/vrm0/handler.py +23 -0
- package/browser/addons/vrm/editor/vrm0/menu.py +145 -0
- package/browser/addons/vrm/editor/vrm0/migration.py +761 -0
- package/browser/addons/vrm/editor/vrm0/ops.py +2145 -0
- package/browser/addons/vrm/editor/vrm0/panel.py +1333 -0
- package/browser/addons/vrm/editor/vrm0/property_group.py +1818 -0
- package/browser/addons/vrm/editor/vrm0/ui_list.py +688 -0
- package/browser/addons/vrm/editor/vrm1/__init__.py +1 -0
- package/browser/addons/vrm/editor/vrm1/handler.py +28 -0
- package/browser/addons/vrm/editor/vrm1/menu.py +76 -0
- package/browser/addons/vrm/editor/vrm1/migration.py +321 -0
- package/browser/addons/vrm/editor/vrm1/ops.py +2679 -0
- package/browser/addons/vrm/editor/vrm1/panel.py +1243 -0
- package/browser/addons/vrm/editor/vrm1/property_group.py +2087 -0
- package/browser/addons/vrm/editor/vrm1/scene_watcher.py +90 -0
- package/browser/addons/vrm/editor/vrm1/ui_list.py +533 -0
- package/browser/addons/vrm/exporter/__init__.py +1 -0
- package/browser/addons/vrm/exporter/abstract_base_vrm_exporter.py +628 -0
- package/browser/addons/vrm/exporter/export_scene.py +930 -0
- package/browser/addons/vrm/exporter/gltf2_export_user_extension.py +45 -0
- package/browser/addons/vrm/exporter/khr_character_exporter.py +34 -0
- package/browser/addons/vrm/exporter/vrm0_exporter.py +4020 -0
- package/browser/addons/vrm/exporter/vrm1_exporter.py +3682 -0
- package/browser/addons/vrm/exporter/vrm_animation_exporter.py +1035 -0
- package/browser/addons/vrm/external/__init__.py +1 -0
- package/browser/addons/vrm/external/auto_rig_pro_support.py +3 -0
- package/browser/addons/vrm/external/io_scene_gltf2_support.py +418 -0
- package/browser/addons/vrm/external/rigify_support.py +1 -0
- package/browser/addons/vrm/importer/__init__.py +1 -0
- package/browser/addons/vrm/importer/abstract_base_vrm_importer.py +1548 -0
- package/browser/addons/vrm/importer/file_handler.py +34 -0
- package/browser/addons/vrm/importer/gltf2_import_user_extension.py +268 -0
- package/browser/addons/vrm/importer/import_scene.py +646 -0
- package/browser/addons/vrm/importer/license_validation.py +198 -0
- package/browser/addons/vrm/importer/vrm0_importer.py +1723 -0
- package/browser/addons/vrm/importer/vrm1_importer.py +2014 -0
- package/browser/addons/vrm/importer/vrm_animation_importer.py +763 -0
- package/browser/addons/vrm/importer/vrm_diff.py +316 -0
- package/browser/addons/vrm/locale/__init__.py +1 -0
- package/browser/addons/vrm/locale/ja_jp.py +826 -0
- package/browser/addons/vrm/locale/ruff.toml +8 -0
- package/browser/addons/vrm/locale/translation_dictionary.py +10 -0
- package/browser/addons/vrm/locale/zh_hans.py +928 -0
- package/browser/addons/vrm/registration.py +670 -0
- package/browser/addons/vrm.json +18 -0
- package/browser/blender-emscripten-engine.mts +60 -128
- package/browser/blender-engine.mts +20 -186
- package/browser/blender-wali-engine.mts +35 -81
- package/browser/describe-thrown.mts +3 -6
- package/browser/document-chunks.mts +4 -16
- package/browser/gltf_export.py +3161 -0
- package/browser/heap-growth.mts +23 -0
- package/browser/index.ts +2 -0
- package/browser/protocol.ts +119 -35
- package/browser/pull-job.mts +2 -10
- package/browser/rna.ts +190 -73
- package/browser/runtime.ts +197 -82
- package/browser/session-addons.mts +33 -0
- package/browser/session-frame.mts +5 -24
- package/browser/session.py +1338 -1427
- package/browser/three/attach-presenter.ts +11 -39
- package/browser/three/blender-base64.ts +1 -36
- package/browser/three/blender-corner-normals.ts +1 -23
- package/browser/three/blender-display-shaders.json +1 -1
- package/browser/three/blender-display-shaders.lut +0 -0
- package/browser/three/blender-display-transform.ts +2 -1
- package/browser/three/blender-frame-columns.ts +5 -13
- package/browser/three/blender-graph-material.ts +18 -3
- package/browser/three/blender-node-graph.ts +10 -34
- package/browser/three/blender-physical-material.ts +2 -31
- package/browser/three/blender-runtime-armature.ts +7 -60
- package/browser/three/blender-runtime-frame.ts +9 -23
- package/browser/three/blender-runtime-geometry.ts +22 -26
- package/browser/three/blender-runtime-lighting.ts +48 -178
- package/browser/three/blender-runtime-shadows.ts +187 -1
- package/browser/three/blender-runtime-skeleton.ts +53 -1
- package/browser/three/blender-runtime-view.ts +505 -283
- package/browser/three/blender-runtime-weights.ts +9 -72
- package/browser/three/blender-sky.ts +13 -33
- package/browser/three/blender-triangulate.ts +7 -35
- package/browser/three/blender-viewport-sampling.ts +235 -0
- package/browser/three/blender-workbench-material.ts +25 -29
- package/browser/three/blender-workbench-outline.ts +199 -0
- package/browser/three/blender-world-volume.ts +62 -10
- package/browser/three/presenter.ts +10 -26
- package/browser/three/release.ts +5 -21
- package/browser/three/sky-precompute-worker.ts +1 -18
- package/browser/three/sky-worker.ts +5 -23
- package/browser/three/world-field-sampler.ts +12 -26
- package/browser/volter_three.py +9 -105
- package/browser/worker.ts +156 -151
- package/package.json +5 -2
- package/wasm/BUNDLE.json +17 -17
- package/wasm/DEPENDENCY-LICENSES.txt +14 -0
- package/wasm/blender_browser.data.br +0 -0
- package/wasm/blender_browser.js +1 -1
- package/wasm/blender_browser.wasm.br +0 -0
- package/browser/frame-transfer.mts +0 -19
- package/browser/three/agx-base-srgb.lut +0 -0
- package/browser/three/agx-look-medium-high-contrast.lut +0 -0
- package/browser/three/agx-look-punchy.lut +0 -0
- package/browser/three/blender-agx.ts +0 -235
- package/browser/three/blender-display-lut.ts +0 -145
- package/browser/three/blender-filmic.ts +0 -49
- package/browser/three/blender-standard.ts +0 -68
- package/browser/three/blender-world-visibility.ts +0 -278
- package/browser/three/filmic-srgb.lut +0 -0
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "VRM Add-on for Blender",
|
|
3
|
+
"addon": "VRM format",
|
|
4
|
+
"version": "4.10.3",
|
|
5
|
+
"repository": "https://github.com/saturday06/VRM-Addon-for-Blender",
|
|
6
|
+
"release": "v4.10.3",
|
|
7
|
+
"commit": "8c0fd6aaef3925b125cf35208cee2458e257164c",
|
|
8
|
+
"archive": {
|
|
9
|
+
"name": "VRM_Addon_for_Blender-4_10_3.zip",
|
|
10
|
+
"url": "https://github.com/saturday06/VRM-Addon-for-Blender/releases/download/v4.10.3/VRM_Addon_for_Blender-4_10_3.zip",
|
|
11
|
+
"sha256": "dcf6d55f3d28de8c0de2f72abcc7fe06b2b300195e6b4da2e9cf476fd59165a2",
|
|
12
|
+
"root": "VRM_Addon_for_Blender-release"
|
|
13
|
+
},
|
|
14
|
+
"blender": { "min": "4.2.0", "maxExclusive": "5.3.0" },
|
|
15
|
+
"license": "MIT OR GPL-3.0-or-later",
|
|
16
|
+
"conveyedUnder": "GPL-3.0-or-later",
|
|
17
|
+
"copyright": ["2018 iCyP", "2022 saturday06", "2020 Isamu Mogi"]
|
|
18
|
+
}
|
|
@@ -1,33 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
* The STANDALONE skew: Blender in this worker's own WebAssembly module.
|
|
3
|
-
*
|
|
4
|
-
* `blender-engine.mts` is the seam and says how the skew is chosen; this is
|
|
5
|
-
* one of its two implementations, and the one `@volter/editor-blender` ships. It has no
|
|
6
|
-
* in-house dependency of any kind and reaches nothing but the editor's own
|
|
7
|
-
* routes.
|
|
8
|
-
*
|
|
9
|
-
* THREE THINGS THE BUILD REQUIRES, each measured rather than assumed:
|
|
10
|
-
*
|
|
11
|
-
* 1. CROSS-ORIGIN ISOLATION. The build is `-sPROXY_TO_PTHREAD` with pthreads,
|
|
12
|
-
* so it needs `SharedArrayBuffer`. The editor's dev server already sets
|
|
13
|
-
* `Cross-Origin-Opener-Policy: same-origin` and
|
|
14
|
-
* `Cross-Origin-Embedder-Policy: credentialless` ahead of every route
|
|
15
|
-
* (`packages/editor/server/dev.ts`) — `credentialless` rather than
|
|
16
|
-
* `require-corp` precisely so cross-origin images and iframes keep loading.
|
|
17
|
-
* A worker without `crossOriginIsolated` is refused BY NAME here rather
|
|
18
|
-
* than left to fail inside the runtime.
|
|
19
|
-
*
|
|
20
|
-
* 2. THE GLUE IS PATCHED SERVER-SIDE, not here, because the runtime spawns its
|
|
21
|
-
* pthreads by loading that same URL as a classic worker
|
|
22
|
-
* (`Module.mainScriptUrlOrBlob`). See `blender-wasm-artifact.ts`.
|
|
23
|
-
*
|
|
24
|
-
* 3. FILES ARE STAGED WITH `FS_createDataFile`, before the runtime starts.
|
|
25
|
-
* This is a WasmFS build: `Module.FS` exists and works from this thread,
|
|
26
|
-
* but its preload directories come out non-writable, which is why
|
|
27
|
-
* `session.py` chmods its own root as its first act.
|
|
28
|
-
*/
|
|
1
|
+
|
|
29
2
|
|
|
30
3
|
import { checkpointStream } from './pull-job.mts';
|
|
4
|
+
import { watchHeapGrowthSource } from './heap-growth.mts';
|
|
31
5
|
import { writeStreamedFile, type StreamFileSystem } from './stream-file.mts';
|
|
32
6
|
import {
|
|
33
7
|
artifactUrl,
|
|
@@ -44,6 +18,9 @@ import {
|
|
|
44
18
|
sleep,
|
|
45
19
|
} from './blender-engine.mts';
|
|
46
20
|
import sessionPython from './session.py?raw';
|
|
21
|
+
// Imported by session.py’s gltf-export operation from the same directory.
|
|
22
|
+
import gltfExportPython from './gltf_export.py?raw';
|
|
23
|
+
import { sessionAddonFiles } from './session-addons.mts';
|
|
47
24
|
|
|
48
25
|
/** What the module factory is, in the only shape this file uses. */
|
|
49
26
|
interface BlenderModule {
|
|
@@ -59,13 +36,8 @@ interface BlenderModule {
|
|
|
59
36
|
ENV: Record<string, string>;
|
|
60
37
|
/** The export door's arena, read by `readArena`. */
|
|
61
38
|
HEAPU8: Uint8Array;
|
|
62
|
-
/**
|
|
63
|
-
|
|
64
|
-
* `--post-js` (the spike's `recipe/release-preloaded-file-data.js`, whose
|
|
65
|
-
* header carries the measurement); absent means the module predates it,
|
|
66
|
-
* which is why the call below is guarded and says so.
|
|
67
|
-
*/
|
|
68
|
-
releasePreloadedFileData?(): { files: number; bytes: number };
|
|
39
|
+
/** Release the packed preload payload after WasmFS has copied its files into linear memory. */
|
|
40
|
+
releasePreloadedFileData(): { files: number; bytes: number };
|
|
69
41
|
_blender_web_export_buffer(): number;
|
|
70
42
|
_blender_web_export_buffer_size(): number;
|
|
71
43
|
FS_createPath(parent: string, name: string, canRead: boolean, canWrite: boolean): void;
|
|
@@ -100,6 +72,19 @@ async function loadFactory(glueUrl: string): Promise<BlenderModuleFactory> {
|
|
|
100
72
|
return factory;
|
|
101
73
|
}
|
|
102
74
|
|
|
75
|
+
/** Report uncaught pthread stacks through Module.printErr before the error crosses to the parent worker. Wasm function indices can be resolved with a symbol map from the same build. */
|
|
76
|
+
function pthreadEntry(glueUrl: string): string {
|
|
77
|
+
const source = `${watchHeapGrowthSource("(line) => postMessage({ cmd: 9, handler: 'printErr', args: [line] })")}
|
|
78
|
+
self.addEventListener('error', (event) => {
|
|
79
|
+
const error = event.error;
|
|
80
|
+
const stack = error && error.stack ? String(error.stack) : String(event.message) + ' at ' + event.filename + ':' + event.lineno + ':' + event.colno;
|
|
81
|
+
try { postMessage({ cmd: 9, handler: 'printErr', args: ['Blender thread crashed: ' + stack] }); } catch (_) {}
|
|
82
|
+
});
|
|
83
|
+
importScripts(${JSON.stringify(new URL(glueUrl, self.location.href).href)});
|
|
84
|
+
`;
|
|
85
|
+
return URL.createObjectURL(new Blob([source], { type: 'text/javascript' }));
|
|
86
|
+
}
|
|
87
|
+
|
|
103
88
|
const utf8 = new TextEncoder();
|
|
104
89
|
|
|
105
90
|
function mkdirp(module: BlenderModule, directory: string): void {
|
|
@@ -117,7 +102,11 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
|
|
|
117
102
|
return {
|
|
118
103
|
readFile: async (path) => FS.readFile(path),
|
|
119
104
|
// Not `async`: a write that throws throws here, so `request` never starts `.done` after a failed `.json`.
|
|
120
|
-
writeFile: (path, data) => {
|
|
105
|
+
writeFile: (path, data) => {
|
|
106
|
+
try { FS.unlink(path); } catch { /* nothing there to replace */ }
|
|
107
|
+
FS.writeFile(path, data);
|
|
108
|
+
return Promise.resolve();
|
|
109
|
+
},
|
|
121
110
|
writeFileStream: (path, body, size) => writeStreamedFile(FS, path, body, size),
|
|
122
111
|
mkdirTree: async (path) => FS.mkdirTree(path),
|
|
123
112
|
readdir: async (path) => FS.readdir(path),
|
|
@@ -139,28 +128,7 @@ function moduleFiles(module: BlenderModule): BlenderFiles {
|
|
|
139
128
|
};
|
|
140
129
|
}
|
|
141
130
|
|
|
142
|
-
/**
|
|
143
|
-
* BLENDER'S ARTIFACTS ARE FETCHED ONCE PER BUILD -- WHERE THE SERVER RUNS IN THE TAB. The wasm
|
|
144
|
-
* (86 MB decoded), the `.data` package (53 MB) and the Essentials payload are immutable for a
|
|
145
|
-
* build, and a page whose editor server runs in its own tab (browser-substrate; the door says so
|
|
146
|
-
* as `status.inTab`) keeps them in Cache Storage under the digest the door reports, so a later
|
|
147
|
-
* open reads them at the browser's own speed instead of from that server. Measured there: the
|
|
148
|
-
* three took 7.2 s per open (4.2 s for the wasm alone), about half of it decoding their brotli
|
|
149
|
-
* in the tab. A file the door gives no digest for is fetched as it always was.
|
|
150
|
-
*
|
|
151
|
-
* NOT WHERE THE SERVER IS A PROCESS ON THIS MACHINE. There the door answers from local disk over
|
|
152
|
-
* loopback with `no-cache` and an ETag, brotli as stored to a client that accepts it
|
|
153
|
-
* (`editor-blender/serving/blender-routes.ts`), so the browser's HTTP cache keeps the bytes as
|
|
154
|
-
* sent (~38 MB) and a repeat open is a 304 and the browser's own decode. Cache Storage there
|
|
155
|
-
* held a second, decoded copy (~144 MiB) per origin, and an editor origin is per project folder
|
|
156
|
-
* (`editor-<id>.localhost:<port>`, before 0.5.190 `127.0.0.1:<port>`): measured 2026-10-07 in
|
|
157
|
-
* one Chrome profile, 113 origins held ~16.3 GB. The HTTP cache is size-bounded and evicted by the browser; Cache Storage is
|
|
158
|
-
* kept until the origin deletes it. So such a page fetches through the HTTP cache and releases
|
|
159
|
-
* any copy an earlier build left in its origin ({@link releaseArtifactCache}).
|
|
160
|
-
*
|
|
161
|
-
* The cache was never made persistent (nothing here calls `navigator.storage.persist`), so it is
|
|
162
|
-
* best-effort storage Chrome may evict under storage pressure; it stays that way.
|
|
163
|
-
*/
|
|
131
|
+
/** Cache immutable artifacts by digest when the server runs in the tab. Process-backed sessions use the browser HTTP cache and release this origin’s artifact-cache copies. Cache Storage remains best-effort storage. */
|
|
164
132
|
const ARTIFACT_CACHE = 'volter-blender-artifacts';
|
|
165
133
|
|
|
166
134
|
/**
|
|
@@ -179,12 +147,7 @@ function releaseArtifactCache(note: (text: string) => void): void {
|
|
|
179
147
|
);
|
|
180
148
|
}
|
|
181
149
|
|
|
182
|
-
/**
|
|
183
|
-
* THE CACHE SPEEDS AN OPEN UP; IT NEVER DECIDES WHETHER BLENDER OPENS. A Cache Storage that cannot
|
|
184
|
-
* open or answer (measured 2026-10-07, headless Chrome on Windows: `caches.open` threw "Unexpected
|
|
185
|
-
* internal error", and the model failed to open on it) means the artifact is fetched from the editor
|
|
186
|
-
* as it would be with no digest; bytes that cannot be kept are used anyway. `note` says which, per artifact, as page output (not an unresolved console entry).
|
|
187
|
-
*/
|
|
150
|
+
/** Fetch from the editor when Cache Storage cannot answer; report cache failures as page output without preventing Blender from opening. */
|
|
188
151
|
async function cachedArtifact(file: string, digest: string | undefined, checkpoint: () => Promise<void>, note: (text: string) => void): Promise<Response> {
|
|
189
152
|
const bounded = (response: Response): Response => response.ok && response.body
|
|
190
153
|
? new Response(checkpointStream(response.body, checkpoint), { status: response.status, headers: response.headers })
|
|
@@ -223,11 +186,12 @@ export async function startEmscriptenBlenderEngine(
|
|
|
223
186
|
options: BlenderEngineOptions,
|
|
224
187
|
status: BlenderArtifactStatus,
|
|
225
188
|
): Promise<BlenderEngine> {
|
|
226
|
-
if (!self.crossOriginIsolated)
|
|
189
|
+
if (!self.crossOriginIsolated || typeof SharedArrayBuffer === 'undefined')
|
|
227
190
|
throw new Error(
|
|
228
|
-
'
|
|
229
|
-
'
|
|
230
|
-
'
|
|
191
|
+
'Blender needs browser isolation and shared memory that this page does not provide. ' +
|
|
192
|
+
'Open this address in a current desktop Chrome or Edge. If this message remains there, ' +
|
|
193
|
+
'the site must serve Cross-Origin-Opener-Policy: same-origin and a ' +
|
|
194
|
+
'Cross-Origin-Embedder-Policy on every response before Blender can start.',
|
|
231
195
|
);
|
|
232
196
|
const glueUrl = artifactUrl('blender_browser.js');
|
|
233
197
|
// Page output, not an editor-console condition: nothing in the project can resolve the browser's cache.
|
|
@@ -241,36 +205,26 @@ export async function startEmscriptenBlenderEngine(
|
|
|
241
205
|
const artifact = (file: string) => cachedArtifact(file, digests[file], () => checkpoint(`artifact/${file}`), noteCache);
|
|
242
206
|
// The `.data` package is handed to the glue whole (`getPreloadedPackage`),
|
|
243
207
|
// so it is read before the module starts; the wasm streams in beside it.
|
|
244
|
-
const [factory, preloaded] = await Promise.all([
|
|
208
|
+
const [factory, preloaded, addonFiles] = await Promise.all([
|
|
245
209
|
loadFactory(glueUrl),
|
|
246
210
|
artifact('blender_browser.data').then(async (response) => {
|
|
247
211
|
const url = artifactUrl('blender_browser.data');
|
|
248
212
|
if (!response.ok) throw new Error(`${url}: HTTP ${response.status}`);
|
|
249
213
|
return startupOperation(url, 'read data package body', () => response.arrayBuffer());
|
|
250
214
|
}),
|
|
215
|
+
sessionAddonFiles(),
|
|
251
216
|
]);
|
|
252
217
|
await checkpoint('runtime-package-ready');
|
|
253
218
|
let bootError: unknown = null;
|
|
254
219
|
const started = performance.now();
|
|
255
220
|
let readyLine: string | null = null;
|
|
256
|
-
//
|
|
257
|
-
// Measured on the first live run: routing `printErr` at `error` put every
|
|
258
|
-
// ordinary startup line -- the WebGPU preinit notice, a Python
|
|
259
|
-
// DeprecationWarning, the ready banner itself -- into the session's
|
|
260
|
-
// unresolved console set, which is the set an agent must drive to zero. The
|
|
261
|
-
// editor's console door is for conditions somebody must resolve, so only the
|
|
262
|
-
// session's OWN named conditions go there.
|
|
221
|
+
// Route Blender’s stdout and stderr to page output; session conditions use their own reporting channel.
|
|
263
222
|
const say = (_level: 'log' | 'error', text: string) => {
|
|
264
223
|
if (text.startsWith('@@VOLTER-READY ')) readyLine = text.slice('@@VOLTER-READY '.length);
|
|
265
224
|
options.log(sessionLevel(text), text);
|
|
266
225
|
};
|
|
267
|
-
//
|
|
268
|
-
|
|
269
|
-
// no export the door calls does. MEASURED 2026-09-29 on the Stoneguard scene from
|
|
270
|
-
// game-benchmarks main: a mesh's columns at 1.14 GB were read through a 433 MB view, and the
|
|
271
|
-
// pull failed "outside the export arena". The glue builds its memory with `new
|
|
272
|
-
// WebAssembly.Memory` and exports no handle to it, so that constructor is observed while the
|
|
273
|
-
// factory runs; every read below takes a fresh view of `memory.buffer`.
|
|
226
|
+
// Observe the factory’s WebAssembly.Memory and read a fresh buffer view after pthread heap growth. Module.HEAPU8 can retain its previous length until the glue refreshes it.
|
|
227
|
+
new Function('report', watchHeapGrowthSource('report'))((line: string) => say('error', line));
|
|
274
228
|
const Memory = WebAssembly.Memory;
|
|
275
229
|
let memory: WebAssembly.Memory | null = null;
|
|
276
230
|
(WebAssembly as { Memory: unknown }).Memory = function (descriptor: WebAssembly.MemoryDescriptor) {
|
|
@@ -294,8 +248,9 @@ export async function startEmscriptenBlenderEngine(
|
|
|
294
248
|
})().catch((error: unknown) => { bootError = error; });
|
|
295
249
|
return {};
|
|
296
250
|
},
|
|
297
|
-
// The pthreads load the SAME patched glue this worker just evaluated
|
|
298
|
-
|
|
251
|
+
// The pthreads load the SAME patched glue this worker just evaluated, behind a line that sends
|
|
252
|
+
// a crashing thread's own stack here (`pthreadEntry`).
|
|
253
|
+
mainScriptUrlOrBlob: pthreadEntry(glueUrl),
|
|
299
254
|
print: (text: string) => say('log', text),
|
|
300
255
|
printErr: (text: string) => say('error', text),
|
|
301
256
|
preRun: [
|
|
@@ -334,6 +289,12 @@ export async function startEmscriptenBlenderEngine(
|
|
|
334
289
|
true,
|
|
335
290
|
true,
|
|
336
291
|
);
|
|
292
|
+
mod.FS_createDataFile(SESSION_ROOT, 'gltf_export.py', utf8.encode(gltfExportPython), true, true, true);
|
|
293
|
+
for (const { path, bytes } of addonFiles) {
|
|
294
|
+
const at = path.lastIndexOf('/');
|
|
295
|
+
mkdirp(mod, path.slice(0, at));
|
|
296
|
+
mod.FS_createDataFile(path.slice(0, at), path.slice(at + 1), bytes, true, true, true);
|
|
297
|
+
}
|
|
337
298
|
},
|
|
338
299
|
],
|
|
339
300
|
}).finally(() => {
|
|
@@ -349,43 +310,18 @@ export async function startEmscriptenBlenderEngine(
|
|
|
349
310
|
const bootMs = performance.now() - started;
|
|
350
311
|
const banner = JSON.parse(readyLine) as { blender: string; engines: string[] };
|
|
351
312
|
|
|
352
|
-
//
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
// long before the ready line, and anything staged after this takes
|
|
361
|
-
// `FS_createDataFile`'s post-flush branch, which never consults the array.
|
|
362
|
-
let releasedPayloadBytes: number | null = null;
|
|
363
|
-
if (typeof module.releasePreloadedFileData === 'function') {
|
|
364
|
-
const released = module.releasePreloadedFileData();
|
|
365
|
-
releasedPayloadBytes = released.bytes;
|
|
366
|
-
options.log(
|
|
367
|
-
'log',
|
|
368
|
-
`blender: released the preloaded ${(released.bytes / 1048576).toFixed(0)} MB ` +
|
|
369
|
-
`payload (${released.files} files); WasmFS already holds it`,
|
|
370
|
-
);
|
|
371
|
-
} else {
|
|
372
|
-
options.log(
|
|
373
|
-
'error',
|
|
374
|
-
'blender: this bundle has no releasePreloadedFileData, so the ~55 MB packed ' +
|
|
375
|
-
'.data payload stays resident beside the copy WasmFS made of it — relink with ' +
|
|
376
|
-
"the recipe's --post-js (recipe/release-preloaded-file-data.js).",
|
|
377
|
-
);
|
|
378
|
-
}
|
|
313
|
+
// WasmFS has copied the preload package before the ready line. Release its redundant JS buffers before staging project files through the post-flush filesystem path.
|
|
314
|
+
const released = module.releasePreloadedFileData();
|
|
315
|
+
const releasedPayloadBytes = released.bytes;
|
|
316
|
+
options.log(
|
|
317
|
+
'log',
|
|
318
|
+
`blender: released the preloaded ${(released.bytes / 1048576).toFixed(0)} MB ` +
|
|
319
|
+
`payload (${released.files} files); WasmFS already holds it`,
|
|
320
|
+
);
|
|
379
321
|
|
|
380
322
|
const FS = module.FS;
|
|
381
323
|
|
|
382
|
-
//
|
|
383
|
-
// ignores its `canWrite` argument in this build, so the project root and
|
|
384
|
-
// everything above it come out read-only and the first `mkdirTree` under
|
|
385
|
-
// them raises `ErrnoError` -- measured on the first live round trip
|
|
386
|
-
// (2026-09-17: `stageProjectFiles` threw before any project file was
|
|
387
|
-
// staged). The session chmods its own root from Python for the same reason;
|
|
388
|
-
// this is the other half, for the paths Python never touches.
|
|
324
|
+
// FS_createPath ignores canWrite in this build. Make preloaded ancestors writable before staging project files.
|
|
389
325
|
let climb = '';
|
|
390
326
|
for (const part of `${options.project}`.split('/').filter(Boolean)) {
|
|
391
327
|
climb += `/${part}`;
|
|
@@ -413,12 +349,7 @@ export async function startEmscriptenBlenderEngine(
|
|
|
413
349
|
banner,
|
|
414
350
|
files,
|
|
415
351
|
request,
|
|
416
|
-
//
|
|
417
|
-
// `-sPROXY_TO_PTHREAD` that memory is SHARED, so `session-frame.mts`
|
|
418
|
-
// slices every column it reads out of here; this call itself is free.
|
|
419
|
-
// A DIRECT ARENA (address 0) IS THE WHOLE HEAP, at its length now: the size the door reports
|
|
420
|
-
// is `emscripten_get_heap_size()`, which is Blender's pthread's own view of the heap and is as
|
|
421
|
-
// stale as this worker's was (measured: 431 MB while a column lay at 2.0 GB).
|
|
352
|
+
// Return a view of shared wasm memory; session-frame.mts copies the transferred columns. A direct arena spans the current memory buffer.
|
|
422
353
|
readArena: async () => {
|
|
423
354
|
const base = module._blender_web_export_buffer();
|
|
424
355
|
return base === 0 ? heap() : heap().subarray(base, base + module._blender_web_export_buffer_size());
|
|
@@ -429,9 +360,10 @@ export async function startEmscriptenBlenderEngine(
|
|
|
429
360
|
};
|
|
430
361
|
}
|
|
431
362
|
|
|
432
|
-
/**
|
|
433
|
-
*
|
|
434
|
-
*
|
|
363
|
+
/**
|
|
364
|
+
* Load assets separately from the engine so data updates do not require relinking Wasm. Check
|
|
365
|
+
* payload and per-file bounds before writing Blender’s filesystem.
|
|
366
|
+
*/
|
|
435
367
|
async function mountEssentials(files: BlenderFiles, digest: string | undefined, checkpoint: (phase: string) => Promise<void>, noteCache: (text: string) => void): Promise<void> {
|
|
436
368
|
const indexUrl = artifactUrl('essentials.json');
|
|
437
369
|
const payloadUrl = artifactUrl('essentials.bin');
|
|
@@ -1,47 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
* Headless Blender, running in the editor tab's worker -- THE SEAM, and the
|
|
3
|
-
* choice of which Blender answers it.
|
|
4
|
-
*
|
|
5
|
-
* THE BLENDER IN THE TAB IS BLENDER (ARCHITECTURE-CORE, owner ruling
|
|
6
|
-
* 2026-09-17), and since the 2026-09-18 amendment there are TWO BUILDS of it
|
|
7
|
-
* and neither retires the other:
|
|
8
|
-
*
|
|
9
|
-
* emscripten -- the STANDALONE skew. Blender in this worker's own module,
|
|
10
|
-
* `@volter/editor-blender` with zero in-house dependencies. What the
|
|
11
|
-
* package ships under `wasm/`.
|
|
12
|
-
* wali -- the SUBSTRATE skew. The same Blender linked for
|
|
13
|
-
* `wasm32-wali-linux-musl` and run as an ordinary Linux
|
|
14
|
-
* program through `@volter/browser-wali`: argv, stdio, an
|
|
15
|
-
* exit code, syscalls, a filesystem.
|
|
16
|
-
*
|
|
17
|
-
* ONE GATE, BOTH SKEWS: the scene battery passes on each at the same counts,
|
|
18
|
-
* from the same `session.py`. A skew may REFUSE a capability by name; it may
|
|
19
|
-
* not answer the same question differently. So this file is where the two
|
|
20
|
-
* meet and everything above it -- `worker.ts`, `session-frame.mts`,
|
|
21
|
-
* `blender-runtime-host.ts`, the presenter, the battery, the document -- is
|
|
22
|
-
* written once.
|
|
23
|
-
*
|
|
24
|
-
* HOW THE SKEW IS CHOSEN: IT IS NOT. It is READ off the artifact this editor
|
|
25
|
-
* serves, through the door that already exists for "is there a Blender here at
|
|
26
|
-
* all" (`/__editor/blender-wasm/status`, `blender-wasm-artifact.ts`). One
|
|
27
|
-
* editor serves one Blender; the served directory holds either a
|
|
28
|
-
* `blender_browser.js` bundle or a `blender.wasm` beside its `runtime/` tree,
|
|
29
|
-
* and that IS the answer. There is no manifest field, no setting and no flag,
|
|
30
|
-
* because nothing a user does should depend on the skew -- if it did, the
|
|
31
|
-
* skews would be answering differently, which the ruling forbids.
|
|
32
|
-
* `VOLTER_BLENDER_WASM_DIR` is the existing door for pointing the editor at a
|
|
33
|
-
* different build and is how the substrate skew is reached in development.
|
|
34
|
-
*
|
|
35
|
-
* WHAT DIFFERS, and it is four things, all below this line:
|
|
36
|
-
* 1. how the module is booted (a `--post-js` glue factory / a WALI program);
|
|
37
|
-
* 2. where the session's `in`/`out`/`ask` files live (`Module.FS` / the
|
|
38
|
-
* program's filesystem);
|
|
39
|
-
* 3. how the export door's arena is read (`HEAPU8` / the file
|
|
40
|
-
* `export_frame`'s `buffer_path` names -- see `readArena`);
|
|
41
|
-
* 4. cross-origin isolation, which only the Emscripten build asks of the page.
|
|
42
|
-
* Everything else, including the request/ask protocol below, is one
|
|
43
|
-
* implementation over {@link BlenderFiles}.
|
|
44
|
-
*/
|
|
1
|
+
|
|
45
2
|
|
|
46
3
|
import { describeThrown } from './describe-thrown.mts';
|
|
47
4
|
|
|
@@ -49,22 +6,16 @@ export const SESSION_ROOT = '/work/.volter-session';
|
|
|
49
6
|
export const SESSION_SCRIPT = `${SESSION_ROOT}/session.py`;
|
|
50
7
|
|
|
51
8
|
/**
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
* ONE WRITER PER DIRECTORY is the channel's ownership rule (see
|
|
56
|
-
* {@link openSessionChannel}), and a directory's owner is also the side that
|
|
57
|
-
* CREATES it: `in/` and `reply/` are made here, before the program starts;
|
|
58
|
-
* `out/` and `ask/` are made by `session.py`, which owns them. An engine that
|
|
59
|
-
* pre-created all four put a guest `mkdir`/`chmod` on a path the page had
|
|
60
|
-
* already touched, which is the same crossing edit at a smaller scale.
|
|
9
|
+
* The page creates and writes the in/ and reply/ directories before the program starts. session.py
|
|
10
|
+
* creates and writes out/ and ask/. Keep one writer per directory so snapshot-backed filesystem
|
|
11
|
+
* patches do not cross another writer’s edits.
|
|
61
12
|
*/
|
|
62
13
|
export const PAGE_OWNED_DIRECTORIES = [`${SESSION_ROOT}/in`, `${SESSION_ROOT}/reply`] as const;
|
|
63
14
|
|
|
64
15
|
/** Which build of Blender this editor serves. */
|
|
65
|
-
|
|
16
|
+
type BlenderSkew = 'emscripten' | 'wali';
|
|
66
17
|
|
|
67
|
-
|
|
18
|
+
interface BlenderFileStat {
|
|
68
19
|
size: number;
|
|
69
20
|
/** The raw mode, S_IFDIR and all: `listModuleFiles` reads the type bits. */
|
|
70
21
|
mode: number;
|
|
@@ -78,14 +29,11 @@ export interface BlenderFileStat {
|
|
|
78
29
|
* a `BrowserFileSystem` whose bytes are a promise by contract. The Emscripten
|
|
79
30
|
* side answers from `Module.FS` and resolves immediately.
|
|
80
31
|
*
|
|
81
|
-
* `stat`
|
|
82
|
-
* asks "is the answer there yet" thousands of times per call, and the version
|
|
83
|
-
* of this that threw sorted a genuine failure from an expected absence by
|
|
84
|
-
* matching `/ENOENT|no such|FS error|errno/i` against the message -- a filter
|
|
85
|
-
* that swallows any real error whose text happens to say "errno".
|
|
32
|
+
* `stat` returns null for an absent path and propagates other filesystem failures.
|
|
86
33
|
*/
|
|
87
34
|
export interface BlenderFiles {
|
|
88
35
|
readFile(path: string): Promise<Uint8Array>;
|
|
36
|
+
/** Replace the contents at path, including when the file already exists. */
|
|
89
37
|
writeFile(path: string, data: string | Uint8Array): Promise<void>;
|
|
90
38
|
/** Stream a known-size file into the engine without a second whole-file JS buffer.
|
|
91
39
|
* A failed transfer leaves the previous file intact. */
|
|
@@ -95,17 +43,7 @@ export interface BlenderFiles {
|
|
|
95
43
|
/** Null when the path is not there. */
|
|
96
44
|
stat(path: string): Promise<BlenderFileStat | null>;
|
|
97
45
|
unlink(path: string): Promise<void>;
|
|
98
|
-
/**
|
|
99
|
-
* Resolves at the next change to this filesystem or after `within` ms,
|
|
100
|
-
* whichever is first, when the engine can say so. The call loop waits on it
|
|
101
|
-
* in place of its poll's sleep, so an answer is read when it lands rather
|
|
102
|
-
* than at the next timer: a browser clamps a chain of `setTimeout(1)` to
|
|
103
|
-
* 4 ms, which was most of a no-op call's return leg (measured 2026-09-28:
|
|
104
|
-
* 6-7 ms of 8-10). Each wait is its own and is gone when it resolves; racing
|
|
105
|
-
* one shared promise against every poll's sleep kept a reaction per poll
|
|
106
|
-
* until the next change (review of d36c8004: 43 MB in an hour-long call).
|
|
107
|
-
* Without it the loop sleeps as before.
|
|
108
|
-
*/
|
|
46
|
+
/** Resolve on the next filesystem change or after within milliseconds. Each wait releases its own subscription when it settles; without this capability, the call loop polls. */
|
|
109
47
|
changed?(within: number): Promise<void>;
|
|
110
48
|
}
|
|
111
49
|
|
|
@@ -128,7 +66,7 @@ export interface BlenderEngineOptions {
|
|
|
128
66
|
* each of them (`mesh` or `image`, with its `piece`), then the frame again with `present`, which is
|
|
129
67
|
* answered as a whole frame is.
|
|
130
68
|
*/
|
|
131
|
-
|
|
69
|
+
interface SessionAsk {
|
|
132
70
|
evaluation?: 'render';
|
|
133
71
|
checkpoint?: string;
|
|
134
72
|
frame?: unknown;
|
|
@@ -142,9 +80,7 @@ export interface SessionAsk {
|
|
|
142
80
|
}
|
|
143
81
|
|
|
144
82
|
export interface BlenderEngine {
|
|
145
|
-
/**
|
|
146
|
-
* that differs between the skews is a defect in one of them and the board
|
|
147
|
-
* has to be able to say which one it measured. */
|
|
83
|
+
/** The engine build identity reported by blender-start. */
|
|
148
84
|
readonly skew: BlenderSkew;
|
|
149
85
|
readonly banner: { blender: string; engines: string[] };
|
|
150
86
|
/** The session's filesystem. */
|
|
@@ -193,22 +129,7 @@ export function sessionLevel(line: string): 'log' | 'warn' | 'error' {
|
|
|
193
129
|
return 'log';
|
|
194
130
|
}
|
|
195
131
|
|
|
196
|
-
/**
|
|
197
|
-
* An artifact URL, resolved against THE MODULE — never against `self.location`.
|
|
198
|
-
*
|
|
199
|
-
* A worker's `location` is where its SCRIPT was loaded from, and that is not
|
|
200
|
-
* always an http url of this editor: when the page is a frame this editor does
|
|
201
|
-
* not own (the Code-OSS workbench, docs/CODE-OSS.md §Boot, DESKTOP), a
|
|
202
|
-
* cross-origin worker can only be constructed through a same-origin `blob:`
|
|
203
|
-
* doorway, so `self.location.href` reads `blob:vscode-file://vscode-app/<uuid>`
|
|
204
|
-
* — an opaque base against which `new URL('/__editor/...', base)` THROWS
|
|
205
|
-
* `TypeError: Invalid URL`. Measured 2026-09-19: that is what `blender-start`
|
|
206
|
-
* answered on the desktop shape once its message reached the worker at all.
|
|
207
|
-
*
|
|
208
|
-
* `import.meta.url` is this module's own url and is always the http one the
|
|
209
|
-
* editor served, whatever loaded the worker. Every artifact url is built from
|
|
210
|
-
* it, and nothing here reads `self.location`.
|
|
211
|
-
*/
|
|
132
|
+
/** Resolve artifacts against import.meta.url. Cross-origin workers may run through a blob URL, which cannot resolve the editor’s artifact paths. */
|
|
212
133
|
export function artifactUrl(file: string): string {
|
|
213
134
|
return new URL(`${ARTIFACT_BASE}/${file}`, import.meta.url).href;
|
|
214
135
|
}
|
|
@@ -239,7 +160,7 @@ export interface BlenderArtifactStatus {
|
|
|
239
160
|
/** Emscripten: each file's SHA-256 as it decodes, the key a page keeps it under. */
|
|
240
161
|
digests?: Record<string, string>;
|
|
241
162
|
/** Emscripten: the server answering runs in this tab (browser-substrate). Only then are the
|
|
242
|
-
* digests used to keep the files in Cache Storage; absent
|
|
163
|
+
* digests used to keep the files in Cache Storage; absent means false. */
|
|
243
164
|
inTab?: boolean;
|
|
244
165
|
/** WALI only: the `sha256-` SRI of `blender.wasm`, which the program loader
|
|
245
166
|
* requires for any non-blob URL. */
|
|
@@ -249,7 +170,7 @@ export interface BlenderArtifactStatus {
|
|
|
249
170
|
workers?: { pool: number; blender: number };
|
|
250
171
|
}
|
|
251
172
|
|
|
252
|
-
|
|
173
|
+
async function artifactStatus(): Promise<BlenderArtifactStatus> {
|
|
253
174
|
const url = artifactUrl('status');
|
|
254
175
|
const answer = await startupOperation(url, 'fetch status', () => fetch(url));
|
|
255
176
|
if (!answer.ok)
|
|
@@ -303,83 +224,11 @@ export const sleep = (ms: number) => new Promise((resolve) => setTimeout(resolve
|
|
|
303
224
|
|
|
304
225
|
const decoder = new TextDecoder();
|
|
305
226
|
|
|
306
|
-
/**
|
|
307
|
-
* THE DIRECTORY CHANNEL, written once for both skews.
|
|
308
|
-
*
|
|
309
|
-
* `session.py`'s header states the other half and says why it is a directory
|
|
310
|
-
* rather than stdin. Nothing here is toolchain-specific: a request is
|
|
311
|
-
* `in/<id>.json` then `in/<id>.done`, an answer is `out/<id>.json` +
|
|
312
|
-
* `out/<id>.done`, and an `ask` the session raises mid-call is served from
|
|
313
|
-
* inside this poll loop -- which is why `saveDocument` cannot be raised from
|
|
314
|
-
* Python's idle loop (`worker.ts` holds that clock).
|
|
315
|
-
*
|
|
316
|
-
* ONE WRITER PER DIRECTORY. This is the channel's single structural rule and
|
|
317
|
-
* it is what keeps the WALI skew alive for a whole session:
|
|
318
|
-
*
|
|
319
|
-
* in/ the PAGE writes and the PAGE unlinks; the guest only reads.
|
|
320
|
-
* out/ the GUEST writes and the GUEST unlinks; the page only reads.
|
|
321
|
-
* ask/ the GUEST writes and the GUEST unlinks; the page only reads.
|
|
322
|
-
* reply/ the PAGE writes and the PAGE unlinks; the guest only reads.
|
|
323
|
-
*
|
|
324
|
-
* WHY IT IS STRUCTURAL AND NOT A TUNING. Under WALI the two sides are two
|
|
325
|
-
* snapshot-backed views of one tree, reconciled by patches: the guest sends
|
|
326
|
-
* the paths IT changed, and `applyProcessFilesystemPatch` refuses the patch
|
|
327
|
-
* (`assertPatchBase`) when a path in it no longer holds what the guest's base
|
|
328
|
-
* said -- after which the host TERMINATES the program's worker. A path can
|
|
329
|
-
* only disagree if somebody other than the patch's author moved it. So with
|
|
330
|
-
* the sets of paths the two sides mutate DISJOINT, the assertion has nothing
|
|
331
|
-
* left to fire on. MEASURED 2026-09-19 (WS-X): with this page unlinking four
|
|
332
|
-
* files in the guest's own `out/` after every answer, a render loop died at
|
|
333
|
-
* call 37 (`base`: a file, `current`: null) and at call 14 (a different
|
|
334
|
-
* mtime), and narrowing the window moved the death 1 -> 13 -> 37 without
|
|
335
|
-
* closing it.
|
|
336
|
-
*
|
|
337
|
-
* THE ACK IS THE REQUEST FILE'S DISAPPEARANCE, so it costs no file and no
|
|
338
|
-
* extra write. Each side needs to know when the other has taken what it left:
|
|
339
|
-
*
|
|
340
|
-
* - this page unlinks `in/<id>.json` and then `in/<id>.done` ONLY after it
|
|
341
|
-
* has read `out/<id>.json`. So `in/<id>.done` being gone IS "the page
|
|
342
|
-
* took the answer" -- a fact the guest reads out of a directory it never
|
|
343
|
-
* writes to, from the `listdir` its loop already does. It then removes
|
|
344
|
-
* its own `out/<id>.*`.
|
|
345
|
-
* - the session unlinks `ask/<id>.json` and then `ask/<id>.done` only after
|
|
346
|
-
* it has read `reply/<id>.json`, so `ask/<id>.done` being gone is "the
|
|
347
|
-
* guest took the reply" -- read here out of the `readdir` `serveAsks`
|
|
348
|
-
* already does, after which this page removes its own `reply/<id>.*`.
|
|
349
|
-
*
|
|
350
|
-
* `.done` IS ALWAYS UNLINKED LAST, on both sides, because `.done` is the
|
|
351
|
-
* token the other side watches: a `.done` without its `.json` would be a
|
|
352
|
-
* half-visible message.
|
|
353
|
-
*
|
|
354
|
-
* NOTHING HERE CATCHES ITS OWN UNLINK. These are files this page wrote into a
|
|
355
|
-
* directory nothing else writes to; an ENOENT would mean the ownership rule
|
|
356
|
-
* had been broken, and the loud failure of the call that found it is the
|
|
357
|
-
* proof the rule holds.
|
|
358
|
-
*/
|
|
227
|
+
/** Directory protocol for both engines: requests use in/<id>.json then in/<id>.done; answers use out/<id>.json then out/<id>.done. The page owns writes and cleanup in in/ and reply/; the guest owns out/ and ask/. Each writer removes .done last after the peer has acknowledged by removing its request. Disjoint writer sets keep WALI filesystem patches consistent. */
|
|
359
228
|
export function openSessionChannel(
|
|
360
229
|
files: BlenderFiles,
|
|
361
230
|
options: BlenderEngineOptions,
|
|
362
|
-
/**
|
|
363
|
-
* WHY THE POLL LOOP NEEDS A LIVENESS PREDICATE, and it is the difference
|
|
364
|
-
* between a board and a wasted afternoon.
|
|
365
|
-
*
|
|
366
|
-
* The loop below waits for `out/<id>.done` and nothing else. When the
|
|
367
|
-
* program behind the directory DIES, that file is never written, so the
|
|
368
|
-
* call waits forever: `cyclotron status` reports it IN FLIGHT for as long as
|
|
369
|
-
* anyone looks, `blender-start` keeps answering from the host's cached
|
|
370
|
-
* banner, and the harness has no answer to time out against. MEASURED
|
|
371
|
-
* 2026-09-19 three times on the WALI skew -- 507 s, 621 s and 964 s of a
|
|
372
|
-
* battery model spent waiting on a Blender that had already ended with
|
|
373
|
-
* `[object WebAssembly.Exception]`, each ending in a killed process and no
|
|
374
|
-
* result record at all.
|
|
375
|
-
*
|
|
376
|
-
* This returns null while the program is alive and the reason it ended
|
|
377
|
-
* otherwise. A caller that supplies one turns that infinite wait into a
|
|
378
|
-
* named failure naming the recovery door. The standalone skew supplies
|
|
379
|
-
* none: its module is the worker, so a death there ends the worker rather
|
|
380
|
-
* than stranding a reader, and inventing a predicate for a failure nobody
|
|
381
|
-
* has measured would be a fiction.
|
|
382
|
-
*/
|
|
231
|
+
/** Return null while the program is alive, or its termination reason. The WALI call loop uses this to refuse a request whose program ended; standalone failures end the worker itself. */
|
|
383
232
|
programEnded?: () => string | null,
|
|
384
233
|
): {
|
|
385
234
|
request(payload: Record<string, unknown>): Promise<unknown>;
|
|
@@ -440,13 +289,7 @@ export function openSessionChannel(
|
|
|
440
289
|
|
|
441
290
|
async function request(payload: Record<string, unknown>): Promise<unknown> {
|
|
442
291
|
const id = String(++sequence);
|
|
443
|
-
//
|
|
444
|
-
// synchronously inside `writeFile` and throw from it, so the order is kept
|
|
445
|
-
// and a failed `.json` never leaves a `.done` behind; and the WALI
|
|
446
|
-
// engine carries one turn's writes to the program as one patch, where an
|
|
447
|
-
// await between them sent the request as two imports in series, the
|
|
448
|
-
// second waiting for the first's acknowledgement (measured 2026-09-28:
|
|
449
|
-
// most of a no-op call's 1.4-1.9 ms inbound leg).
|
|
292
|
+
// Begin the JSON and .done writes in one turn, in that order. Both engines write synchronously, and WALI carries that turn as one filesystem patch.
|
|
450
293
|
await Promise.all([
|
|
451
294
|
files.writeFile(`${SESSION_ROOT}/in/${id}.json`, JSON.stringify(payload)),
|
|
452
295
|
files.writeFile(`${SESSION_ROOT}/in/${id}.done`, '1'),
|
|
@@ -458,18 +301,9 @@ export function openSessionChannel(
|
|
|
458
301
|
const body = JSON.parse(
|
|
459
302
|
decoder.decode(await files.readFile(`${SESSION_ROOT}/out/${id}.json`)),
|
|
460
303
|
) as { result?: unknown; error?: string };
|
|
461
|
-
//
|
|
462
|
-
//
|
|
463
|
-
//
|
|
464
|
-
// above): the guest's patch for `out/<id>.done` crossed this page's
|
|
465
|
-
// unlink of it, `assertPatchBase` refused the patch, and the host
|
|
466
|
-
// terminated the program's worker -- no guest exit path ran, which is
|
|
467
|
-
// why every instrument on `SYS_exit` was silent through it.
|
|
468
|
-
//
|
|
469
|
-
// These two unlinks are also the ACK. The session watches for
|
|
470
|
-
// `in/<id>.done` to vanish and takes that as "the page has the
|
|
471
|
-
// answer", so `.done` goes last and neither is attempted before
|
|
472
|
-
// `out/<id>.json` has been read.
|
|
304
|
+
// Unlink only the page-owned in/ request files after reading out/<id>.json. Removing
|
|
305
|
+
// in/<id>.done last acknowledges the answer to the session, which removes its own out/
|
|
306
|
+
// files.
|
|
473
307
|
await files.unlink(`${SESSION_ROOT}/in/${id}.json`);
|
|
474
308
|
await files.unlink(`${SESSION_ROOT}/in/${id}.done`);
|
|
475
309
|
if (body.error) throw new Error(body.error);
|