Compare commits

..

16 Commits

Author SHA1 Message Date
mes123456
fb5abd8e37 Complete Soft Body modifier gap
Some checks are pending
M6 deployable RC / quick (push) Waiting to run
M6 deployable RC / chromium (push) Blocked by required conditions
M6 deployable RC / release (push) Blocked by required conditions
2026-08-20 10:23:25 -04:00
mes123456
7fdf4ab027 Complete Smooth modifier gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 10:16:16 -04:00
mes123456
59c151f45d Advance M16 modifier parity and context governance
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 10:01:24 -04:00
mes123456
01028d1ad5 Complete Lattice modifier gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 07:16:54 -04:00
mes123456
f82e45bfcb Complete Laplacian smooth modifier gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 06:22:55 -04:00
mes123456
1813cb7d1c Complete Laplacian deform modifier gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 06:18:51 -04:00
mes123456
2ef2585676 Complete HOOK modifier gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 06:13:16 -04:00
mes123456
f2c49061a6 Complete Grease Pencil vertex proximity gap
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 06:09:22 -04:00
mes123456
10640aeb3c Govern task context and advance execution pointer
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-20 06:02:43 -04:00
mes123456
380cbed4ff Checkpoint web parity through Chromium input tasks
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-19 10:39:03 -04:00
mes123456
5a11045ca5 Advance Blender 5.2 web parity through M12-03D
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-17 17:30:27 -04:00
mes123456
0fe8d2bb56 Advance M8-M11 parity workflows
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-17 04:37:07 -04:00
mes123456
7c16b279ae Advance M7 workflows and release operations
Some checks failed
M6 deployable RC / quick (push) Has been cancelled
M6 deployable RC / chromium (push) Has been cancelled
M6 deployable RC / release (push) Has been cancelled
2026-08-15 17:43:53 -04:00
mes123456
17ab961485 Complete V1 RC deployment capability gates 2026-08-15 01:01:23 -04:00
mes123456
a3f3071c03 Complete V1 performance and OOM release gates 2026-08-14 22:32:09 -04:00
mes123456
3ea9974eee Align VDB status and release evidence 2026-08-14 18:27:57 -04:00
2270 changed files with 650173 additions and 1265 deletions

102
.gitea/workflows/m6-ci.yml Normal file
View File

@@ -0,0 +1,102 @@
name: M6 deployable RC
on:
push:
pull_request:
workflow_dispatch:
env:
FORCE_COLOR: "0"
NO_COLOR: "1"
jobs:
quick:
runs-on: [self-hosted, linux, x64, blender-web]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.2
cache: npm
cache-dependency-path: web/package-lock.json
- name: Run quick lane
run: npm --prefix web run ci:quick
- name: Validate quick report
run: npm --prefix web run test:ci-report -- release/ci-reports/quick.json
- name: Upload quick report
if: always()
uses: actions/upload-artifact@v4
with:
name: m6-quick-report
path: release/ci-reports/
retention-days: 7
chromium:
needs: quick
runs-on: [self-hosted, linux, x64, blender-web]
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.2
cache: npm
cache-dependency-path: web/package-lock.json
- name: Install browser driver from lockfile
run: |
npm --prefix web ci --ignore-scripts
npm --prefix web exec -- playwright install chromium
- name: Run Chromium lane
run: npm --prefix web run ci:chromium
- name: Validate Chromium report
run: npm --prefix web run test:ci-report -- release/ci-reports/chromium.json
- name: Upload Chromium report
if: always()
uses: actions/upload-artifact@v4
with:
name: m6-chromium-report
path: release/ci-reports/
retention-days: 14
release:
needs: chromium
if: ${{ github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main') }}
runs-on: [self-hosted, linux, x64, blender-web-release]
env:
BLENDER_BIN: ${{ vars.BLENDER_BIN }}
BLENDER_ARCHIVE_SHA256: ${{ vars.BLENDER_ARCHIVE_SHA256 }}
VDB_RESOURCE_ROOT: ${{ vars.VDB_RESOURCE_ROOT }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: 20.19.2
cache: npm
cache-dependency-path: web/package-lock.json
- name: Cache pinned Emscripten toolchain data
uses: actions/cache@v4
with:
path: .emcache
key: emscripten-${{ runner.os }}-3.1.69-${{ hashFiles('tools/web/emscripten-env.sh', 'web/package-lock.json') }}
- name: Install browser driver from lockfile
run: |
npm --prefix web ci --ignore-scripts
npm --prefix web exec -- playwright install chromium
- name: Run release lane
run: npm --prefix web run ci:release
- name: Validate release report and hashes
run: npm --prefix web run test:ci-report -- release/ci-reports/release.json
- name: Upload release report and archives
if: always()
uses: actions/upload-artifact@v4
with:
name: m6-release-report
path: |
release/ci-reports/
release/archive-reports/
release/operations-reports/
release/SHA256SUMS.txt
release/RC_MANIFEST.json
release/RC_MANIFEST.json.sha256
release/blender-web-offline.tar.gz
release/blender-web-corresponding-source.tar.gz
retention-days: 30

2
.gitignore vendored
View File

@@ -1,6 +1,8 @@
# Local toolchains and caches # Local toolchains and caches
.emcache/ .emcache/
.emscripten-web .emscripten-web
__pycache__/
*.py[cod]
*.log *.log
mylog.txt mylog.txt

14
AGENTS.md Normal file
View File

@@ -0,0 +1,14 @@
# 执行上下文规则
本仓库的任务执行必须使用最小上下文。不要把完整路线图、项目状态、parity ledger 或全部 `docs/status/` 日志作为默认上下文。
领取任务时只做以下操作:
1. 阅读 `docs/EXECUTION_QUEUE.md`
2. 运行 `node tools/web/print-task-context.mjs`它只会输出当前任务、parent manifest、parent status 和聚焦命令。
3. 只打开输出中列出的任务卡、生产入口、测试入口和最小 fixture。
4. 开始前运行 `node tools/web/check-task-context.mjs`;预算或 parent `nextTask` 不一致时停止并修正证据,不要手工推进队列。
默认上下文预算:队列 4 KiB、任务卡 8 KiB、parent manifest 24 KiB、parent status 6 KiB合计约 3,500 tokens。历史和长期文档只能按任务卡列出的具体小节追加读取。
完成任务时运行 `node tools/web/check-task-context.mjs --task <task-id> --write`并仅提交任务报告、manifest、status 和聚焦实现/测试变更。下一任务只认新 manifest 的 `nextTask`

View File

@@ -6,7 +6,10 @@ storage, and desktop/WASM round-trip tests.
The current browser baseline is Chromium only. The release browser suite covers The current browser baseline is Chromium only. The release browser suite covers
both the main-thread WebGL renderer and the OffscreenCanvas Worker renderer. both the main-thread WebGL renderer and the OffscreenCanvas Worker renderer.
Firefox and WebKit are intentionally outside the current test and release scope. Firefox and WebKit are permanently outside this project's test, CI, evidence, and release scope.
The iron rule is Chromium-only browser execution: never launch, probe, or claim support from
Firefox or WebKit. Historical Firefox/WebKit reports are archival context only and are not normative
support evidence.
## Layout ## Layout
@@ -29,6 +32,13 @@ npm --prefix web run build
npm --prefix web run test:browser npm --prefix web run test:browser
``` ```
See `docs/PROJECT_STATUS_AND_NEXT_WORK.md` and See `docs/EXECUTION_QUEUE.md` for the current task pointer, `docs/CONTEXT_BUDGET.md` for the
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` for the implemented scope and remaining small-context execution rules, and `docs/README.md` for the document map,
structured blockers. `docs/PROJECT_STATUS_AND_NEXT_WORK.md` for the implemented scope, and
`docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` for the normative M12-M23
full-parity execution plan. The domain overview and coverage cross-check are in
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` and
`docs/BLENDER_5_2_FULL_PARITY_WBS.md`.
The release contract and explicit non-goals are defined in
`docs/WEB_BLENDER_MODELER_V1_SCOPE.md`.

View File

@@ -1,5 +1,7 @@
# Web 版 Blender 连续执行计划 # Web 版 Blender 连续执行计划
> 历史迁移计划:本文不参与当前任务领取。当前任务只认 `docs/EXECUTION_QUEUE.md`、任务卡和 parent manifest。
## 1. 文档定位 ## 1. 文档定位
本计划用于把 `blender-5.2.0` 逐步变成一个浏览器中的 Blender 数据编辑器。它不是“把桌面窗口搬进 Canvas”的计划而是以 Blender 数据和建模行为为基准,以 React 为应用界面、Three.js 为唯一浏览器渲染器、WebAssembly 为 Blender 计算核心。 本计划用于把 `blender-5.2.0` 逐步变成一个浏览器中的 Blender 数据编辑器。它不是“把桌面窗口搬进 Canvas”的计划而是以 Blender 数据和建模行为为基准,以 React 为应用界面、Three.js 为唯一浏览器渲染器、WebAssembly 为 Blender 计算核心。
@@ -76,7 +78,8 @@ web/protocol/schema-version
### 3.2 功能完成 ### 3.2 功能完成
- Chrome、Firefox、Safari 最近稳定版至少各通过一次冒烟测试。 - V1 以 Chromium 为唯一发布基线,主线程 WebGL 和 OffscreenCanvas Worker 均须通过;
Firefox/WebKit 作为发布后兼容目标,不进入 V1 完成条件。
- 单线程构建可工作;多线程不可用时有明确降级信息。 - 单线程构建可工作;多线程不可用时有明确降级信息。
- 引擎异常不会使 React 页面失去保存能力。 - 引擎异常不会使 React 页面失去保存能力。
- 所有变更有 revision可撤销、可重做或明确标注不可撤销。 - 所有变更有 revision可撤销、可重做或明确标注不可撤销。
@@ -520,7 +523,7 @@ tests/files/web/large_scene.blend
| W-038 | `in_progress` | SceneDelta diff/apply、revision 冲突校验、原生 C ABI `get_scene_delta` 以及 `setFrame`/`setObjectVisibility` 最小命令路径已闭环mesh-buffer/material range、拓扑 delta 和完整 command registry 仍待完成 | | W-038 | `in_progress` | SceneDelta diff/apply、revision 冲突校验、原生 C ABI `get_scene_delta` 以及 `setFrame`/`setObjectVisibility` 最小命令路径已闭环mesh-buffer/material range、拓扑 delta 和完整 command registry 仍待完成 |
| W-039 | `in_progress` | basic_scene 已闭环;等待 W-033~W-038 的完整验收 | | W-039 | `in_progress` | basic_scene 已闭环;等待 W-033~W-038 的完整验收 |
| W-040 | `in_progress` | OPFS 项目路径校验、projects/<id> 场景/资产/缩略图/tmp/cache/lod 目录和临时文件写入辅助已实现;跨浏览器 rename/crash recovery 仍待完成 | | W-040 | `in_progress` | OPFS 项目路径校验、projects/<id> 场景/资产/缩略图/tmp/cache/lod 目录和临时文件写入辅助已实现;跨浏览器 rename/crash recovery 仍待完成 |
| W-041 | `in_progress` | IndexedDB schema v6 已包含 project/asset/snapshot/log/quarantine/LOD/Simulation manifest stores 并有逐版升级 E2E旧版本回滚测试仍待完成 | | W-041 | `in_progress` | IndexedDB schema v7 已包含 project/asset/snapshot/log/quarantine/LOD/Simulation manifest/quarantine stores并有 v6→v7 migration 与逐版升级 E2E旧版本回滚测试仍待完成 |
| W-042 | `in_progress` | StorageWorker transferable save 已实现 OPFS tmp->replace 与 IndexedDB project revision/size metadata并有 E2E 证据hash/crash recovery/snapshot manifest 仍待完成 | | W-042 | `in_progress` | StorageWorker transferable save 已实现 OPFS tmp->replace 与 IndexedDB project revision/size metadata并有 E2E 证据hash/crash recovery/snapshot manifest 仍待完成 |
| W-043 | `in_progress` | 1.5 秒可取消 autosave 已接入 React dirty 状态并经 E2E 验证;命令数阈值、快照保留和大文件背压仍待完成 | | W-043 | `in_progress` | 1.5 秒可取消 autosave 已接入 React dirty 状态并经 E2E 验证;命令数阈值、快照保留和大文件背压仍待完成 |
| W-044 | `in_progress` | operation_log schema、payload/inversePayload 写入和 StorageClient 接口已实现并经 E2E 验证replay、损坏隔离和 native command recovery 仍待完成 | | W-044 | `in_progress` | operation_log schema、payload/inversePayload 写入和 StorageClient 接口已实现并经 E2E 验证replay、损坏隔离和 native command recovery 仍待完成 |
@@ -1416,7 +1419,8 @@ P2 按用户需求逐项启用,不得让单个高级模块阻塞 P0/P1 发布
前置:`W-130`~`W-134` 前置:`W-130`~`W-134`
执行Chromium、Firefox、WebKit 测试 P0/P1 场景、存储、WebGL2、线程降级、导入导出和截图差异 执行:V1 在 Chromium 测试 P0/P1 场景、存储、WebGL2、线程降级、导入导出和截图差异
Firefox/WebKit 在发布后兼容阶段执行同一矩阵。
验收:支持矩阵中的每个组合有结果;不支持的组合有明确阻断页。 验收:支持矩阵中的每个组合有结果;不支持的组合有明确阻断页。

View File

@@ -1,5 +1,7 @@
# Blender Web 化技术路线React + Three.js + WebAssembly + OPFS/IndexedDB # Blender Web 化技术路线React + Three.js + WebAssembly + OPFS/IndexedDB
> 历史技术背景:本文不参与当前任务领取。当前任务只认 `docs/EXECUTION_QUEUE.md` 和最小上下文工具。
> **存储决策2026-08** SQLite WASM 不是项目首期必需依赖,已从当前实现和发布资源中移除。小型元数据使用 Worker 内 IndexedDB大型二进制使用 OPFS未来只有在查询/事务需求超过 IndexedDB 时,才单独评估 SQLite WASM。 > **存储决策2026-08** SQLite WASM 不是项目首期必需依赖,已从当前实现和发布资源中移除。小型元数据使用 Worker 内 IndexedDB大型二进制使用 OPFS未来只有在查询/事务需求超过 IndexedDB 时,才单独评估 SQLite WASM。
## 1. 结论先行 ## 1. 结论先行
@@ -510,7 +512,8 @@ manifest浏览器运行时禁止从 CDN、远程 ESM 或第三方脚本地址
## 12. 验收标准 ## 12. 验收标准
以 Chromium、Firefox、Safari 最近稳定版各一台桌面设备作为参考环境,至少验证: V1 以 Chromium 桌面设备作为发布参考环境,并同时覆盖主线程和 OffscreenCanvas Worker
Firefox、Safari/WebKit 留作发布后兼容矩阵。至少验证:
1. 空白项目、基础 `.blend` 和含贴图项目可以打开、编辑、保存、刷新恢复。 1. 空白项目、基础 `.blend` 和含贴图项目可以打开、编辑、保存、刷新恢复。
2. 保存失败、页面刷新、Worker 异常后不会破坏上一个完整快照。 2. 保存失败、页面刷新、Worker 异常后不会破坏上一个完整快照。

View File

@@ -5711,6 +5711,12 @@ void BKE_particle_system_blend_read_after_liblink(BlendLibReader * /*reader*/,
} }
} }
else { else {
#ifdef WITH_WEB
/* Keep unlinked legacy particle systems in the Web Main as metadata-only entries. The
* desktop cleanup path assumes a fully linked ParticleSettings pointer and would free a
* stale modifier allocation in the narrow Web registry. */
continue;
#else
/* Particle modifier must be removed before particle system. */ /* Particle modifier must be removed before particle system. */
ParticleSystemModifierData *psmd = psys_get_modifier(ob, &psys); ParticleSystemModifierData *psmd = psys_get_modifier(ob, &psys);
BKE_modifier_remove_from_list(ob, reinterpret_cast<ModifierData *>(psmd)); BKE_modifier_remove_from_list(ob, reinterpret_cast<ModifierData *>(psmd));
@@ -5718,6 +5724,7 @@ void BKE_particle_system_blend_read_after_liblink(BlendLibReader * /*reader*/,
BLI_remlink(particles, &psys); BLI_remlink(particles, &psys);
MEM_delete(&psys); MEM_delete(&psys);
#endif
} }
} }
} }

View File

@@ -157,7 +157,7 @@ if(WITH_WEB)
# runtime only needs model/animation RNA needed by depsgraph evaluation. # runtime only needs model/animation RNA needed by depsgraph evaluation.
# Trim after generation so the generator itself remains source-compatible. # Trim after generation so the generator itself remains source-compatible.
list(FILTER GENSRC INCLUDE REGEX list(FILTER GENSRC INCLUDE REGEX
"rna_(ID|action|animation|armature|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$") "rna_(ID|action|animation|armature|blendfile_import|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$")
list(APPEND GENSRC list(APPEND GENSRC
"${CMAKE_CURRENT_BINARY_DIR}/rna_prototypes_gen.hh" "${CMAKE_CURRENT_BINARY_DIR}/rna_prototypes_gen.hh"
"${CMAKE_CURRENT_BINARY_DIR}/../RNA_prototypes.hh" "${CMAKE_CURRENT_BINARY_DIR}/../RNA_prototypes.hh"

View File

@@ -220,6 +220,7 @@ if(WITH_WEB)
intern/MOD_mirror.cc intern/MOD_mirror.cc
intern/MOD_meshdeform.cc intern/MOD_meshdeform.cc
intern/MOD_nodes_web.cc intern/MOD_nodes_web.cc
intern/MOD_particlesystem.cc
intern/MOD_none.cc intern/MOD_none.cc
intern/MOD_screw.cc intern/MOD_screw.cc
intern/MOD_shapekey.cc intern/MOD_shapekey.cc

View File

@@ -5,12 +5,17 @@
/** \file /** \file
* \ingroup modifiers * \ingroup modifiers
* *
* Native Web evaluator for the first bounded Geometry Nodes closure. It * Native Web evaluator for the bounded Geometry Nodes allowlist. Unsupported
* accepts a single Group Input -> Transform Geometry -> Group Output graph. * graphs remain visible and report an explicit modifier error.
* Unsupported graphs remain visible and report an explicit modifier error.
*/ */
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring> #include <cstring>
#include <optional>
#include <string>
#include <variant>
#include "MEM_guardedalloc.h" #include "MEM_guardedalloc.h"
@@ -18,19 +23,32 @@
#include "BLI_math_rotation.hh" #include "BLI_math_rotation.hh"
#include "BLI_listbase.h" #include "BLI_listbase.h"
#include "BLI_string.h" #include "BLI_string.h"
#include "BLI_vector.hh"
#include "BKE_attribute.hh"
#include "BKE_geometry_set.hh"
#include "BKE_instances.hh"
#include "BKE_lib_query.hh" #include "BKE_lib_query.hh"
#include "BKE_mesh.hh" #include "BKE_mesh.hh"
#include "BKE_modifier.hh" #include "BKE_modifier.hh"
#include "BKE_node.hh" #include "BKE_node.hh"
#include "BKE_node_legacy_types.hh" #include "BKE_node_legacy_types.hh"
#include "BKE_object.hh"
#include "BLO_read_write.hh" #include "BLO_read_write.hh"
#include "DEG_depsgraph_build.hh" #include "DEG_depsgraph_build.hh"
#include "DEG_depsgraph_query.hh"
#include "DNA_collection_types.h"
#include "DNA_image_types.h"
#include "DNA_modifier_types.h" #include "DNA_modifier_types.h"
#include "DNA_node_types.h" #include "DNA_node_types.h"
#include "DNA_object_types.h"
#include "GEO_join_geometries.hh"
#include "GEO_realize_instances.hh"
#include "GEO_transform.hh"
#include "RNA_prototypes.hh" #include "RNA_prototypes.hh"
#include "UI_resources.hh" #include "UI_resources.hh"
@@ -89,8 +107,39 @@ static void foreach_ID_link(ModifierData *md, Object *object, IDWalkFunc walk, v
static void update_depsgraph(ModifierData *md, const ModifierUpdateDepsgraphContext *ctx) static void update_depsgraph(ModifierData *md, const ModifierUpdateDepsgraphContext *ctx)
{ {
NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md); NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
if (nmd->node_group != nullptr) { if (nmd->node_group == nullptr) {
DEG_add_node_tree_output_relation(ctx->node, nmd->node_group, "Web Geometry Nodes Modifier"); return;
}
DEG_add_node_tree_output_relation(ctx->node, nmd->node_group, "Web Geometry Nodes Modifier");
for (const bNode &node : nmd->node_group->nodes) {
for (const bNodeSocket &socket : node.inputs) {
if (socket.default_value == nullptr) {
continue;
}
if (socket.type == SOCK_OBJECT) {
Object *object = static_cast<const bNodeSocketValueObject *>(socket.default_value)->value;
if (object != nullptr && object != ctx->object) {
DEG_add_object_relation(
ctx->node, object, DEG_OB_COMP_TRANSFORM, "Web Geometry Nodes Object Info");
DEG_add_object_relation(
ctx->node, object, DEG_OB_COMP_GEOMETRY, "Web Geometry Nodes Object Info");
}
}
else if (socket.type == SOCK_COLLECTION) {
Collection *collection =
static_cast<const bNodeSocketValueCollection *>(socket.default_value)->value;
if (collection != nullptr) {
DEG_add_collection_geometry_relation(
ctx->node, collection, "Web Geometry Nodes Collection Info");
}
}
else if (socket.type == SOCK_IMAGE) {
Image *image = static_cast<const bNodeSocketValueImage *>(socket.default_value)->value;
if (image != nullptr) {
DEG_add_generic_id_relation(ctx->node, &image->id, "Web Geometry Nodes Image Info");
}
}
}
} }
} }
@@ -102,7 +151,7 @@ static bool is_disabled(const Scene *, ModifierData *md, bool)
static const bNodeSocket *find_input(const bNode &node, const char *identifier) static const bNodeSocket *find_input(const bNode &node, const char *identifier)
{ {
for (const bNodeSocket &socket : node.inputs) { for (const bNodeSocket &socket : node.inputs) {
if (STREQ(socket.identifier, identifier)) { if (STREQ(socket.identifier, identifier) || STREQ(socket.name, identifier)) {
return &socket; return &socket;
} }
} }
@@ -119,24 +168,12 @@ static const bNodeSocket *find_output(const bNode &node, const char *identifier)
return nullptr; return nullptr;
} }
static bool has_link(const bNodeTree &tree, static bool node_is(const bNode &node, const char *idname)
const bNode &from_node,
const char *from_socket,
const bNode &to_node,
const char *to_socket)
{ {
const auto matches_socket = [](const bNodeSocket &socket, const char *name) { if (STREQ(node.idname, idname)) {
return STREQ(socket.identifier, name) || STREQ(socket.name, name); return true;
};
for (const bNodeLink &link : tree.links) {
if (link.fromnode == &from_node && link.tonode == &to_node && link.fromsock != nullptr &&
link.tosock != nullptr && matches_socket(*link.fromsock, from_socket) &&
matches_socket(*link.tosock, to_socket))
{
return true;
}
} }
return false; return std::string(node.idname) == "Undefined[" + std::string(idname) + "]";
} }
static bool has_input_link(const bNodeTree &tree, const bNode &node, const char *socket_name) static bool has_input_link(const bNodeTree &tree, const bNode &node, const char *socket_name)
@@ -151,6 +188,659 @@ static bool has_input_link(const bNodeTree &tree, const bNode &node, const char
return false; return false;
} }
using WebNodeValue = std::variant<std::monostate,
bke::GeometrySet,
bool,
int,
float,
float3,
std::string,
Object *,
Collection *,
Image *>;
class WebGeometryNodeEvaluator {
public:
static constexpr int max_evaluations = 4096;
static constexpr int max_point_elements = 1'000'000;
static constexpr int max_edge_elements = 2'000'000;
static constexpr int max_face_elements = 2'000'000;
static constexpr int max_corner_elements = 4'000'000;
static constexpr int max_instance_elements = 100'000;
static constexpr int64_t max_field_bytes = 64 * 1024 * 1024;
private:
const bNodeTree &tree_;
const ModifierEvalContext &ctx_;
const Mesh &input_mesh_;
std::string error_;
int evaluation_count_ = 0;
bool mesh_within_domain_budget(const Mesh &mesh)
{
if (mesh.verts_num > max_point_elements || mesh.edges_num > max_edge_elements ||
mesh.faces_num > max_face_elements || mesh.corners_num > max_corner_elements)
{
error_ = "field/domain element budget exceeded";
return false;
}
return true;
}
bool geometry_within_domain_budget(const bke::GeometrySet &geometry)
{
if (const Mesh *mesh = geometry.get_mesh()) {
if (!mesh_within_domain_budget(*mesh)) {
return false;
}
}
if (const bke::Instances *instances = geometry.get_instances()) {
if (instances->instances_num() > max_instance_elements) {
error_ = "instance domain element budget exceeded";
return false;
}
}
return true;
}
WebNodeValue bounded_geometry(bke::GeometrySet geometry)
{
if (!geometry_within_domain_budget(geometry)) {
return {};
}
return std::move(geometry);
}
const bNode *owner_of(const bNodeSocket &socket) const
{
for (const bNode &node : tree_.nodes) {
for (const bNodeSocket &candidate : node.inputs) {
if (&candidate == &socket) {
return &node;
}
}
for (const bNodeSocket &candidate : node.outputs) {
if (&candidate == &socket) {
return &node;
}
}
}
return nullptr;
}
Vector<const bNodeLink *> input_links(const bNodeSocket &socket) const
{
Vector<const bNodeLink *> links;
for (const bNodeLink &link : tree_.links) {
if (link.tosock == &socket && link.fromnode != nullptr && link.fromsock != nullptr) {
links.append(&link);
}
}
std::ranges::sort(links, [](const bNodeLink *a, const bNodeLink *b) {
return a->multi_input_sort_id > b->multi_input_sort_id;
});
return links;
}
WebNodeValue default_value(const bNodeSocket &socket)
{
if (socket.default_value == nullptr) {
return {};
}
switch (socket.type) {
case SOCK_BOOLEAN:
return static_cast<const bNodeSocketValueBoolean *>(socket.default_value)->value != 0;
case SOCK_INT:
return static_cast<const bNodeSocketValueInt *>(socket.default_value)->value;
case SOCK_FLOAT:
return static_cast<const bNodeSocketValueFloat *>(socket.default_value)->value;
case SOCK_VECTOR:
return float3(static_cast<const bNodeSocketValueVector *>(socket.default_value)->value);
case SOCK_STRING:
return std::string(
static_cast<const bNodeSocketValueString *>(socket.default_value)->value);
case SOCK_OBJECT:
return static_cast<const bNodeSocketValueObject *>(socket.default_value)->value;
case SOCK_COLLECTION:
return static_cast<const bNodeSocketValueCollection *>(socket.default_value)->value;
case SOCK_IMAGE:
return static_cast<const bNodeSocketValueImage *>(socket.default_value)->value;
default:
return {};
}
}
std::optional<float> number(const WebNodeValue &value) const
{
if (const int *integer = std::get_if<int>(&value)) {
return float(*integer);
}
if (const float *scalar = std::get_if<float>(&value)) {
return *scalar;
}
return std::nullopt;
}
bke::GeometrySet copy_geometry(const bke::GeometrySet &geometry) const
{
if (const Mesh *mesh = geometry.get_mesh()) {
return bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*mesh));
}
bke::GeometrySet copy = geometry;
copy.ensure_owns_all_data();
return copy;
}
WebNodeValue evaluate_input(const bNodeSocket &socket)
{
const Vector<const bNodeLink *> links = input_links(socket);
if (links.size() == 1) {
return evaluate_output(*links.first()->fromsock);
}
if (links.size() > 1) {
error_ = "multiple links on a non-multi-input socket";
return {};
}
return default_value(socket);
}
std::optional<bke::GeometrySet> geometry_input(const bNode &node, const char *name)
{
const bNodeSocket *socket = find_input(node, name);
if (socket == nullptr) {
error_ = std::string("missing geometry input: ") + name;
return std::nullopt;
}
WebNodeValue value = evaluate_input(*socket);
if (bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value)) {
return std::move(*geometry);
}
error_ = std::string("geometry input did not evaluate to geometry: ") + name;
return std::nullopt;
}
WebNodeValue evaluate_compare(const bNode &node)
{
const NodeFunctionCompare *storage = static_cast<const NodeFunctionCompare *>(node.storage);
const bNodeSocket *a_socket = find_input(node, "A");
const bNodeSocket *b_socket = find_input(node, "B");
if (storage == nullptr || a_socket == nullptr || b_socket == nullptr ||
!ELEM(storage->data_type, SOCK_INT, SOCK_FLOAT))
{
error_ = "Compare supports bounded Int/Float scalar inputs only";
return {};
}
const std::optional<float> a = number(evaluate_input(*a_socket));
const std::optional<float> b = number(evaluate_input(*b_socket));
if (!a || !b) {
error_ = "Compare scalar input is unavailable";
return {};
}
switch (storage->operation) {
case NODE_COMPARE_LESS_THAN:
return *a < *b;
case NODE_COMPARE_LESS_EQUAL:
return *a <= *b;
case NODE_COMPARE_GREATER_THAN:
return *a > *b;
case NODE_COMPARE_GREATER_EQUAL:
return *a >= *b;
case NODE_COMPARE_EQUAL:
return *a == *b;
case NODE_COMPARE_NOT_EQUAL:
return *a != *b;
default:
error_ = "Compare operation is outside the bounded evaluator";
return {};
}
}
WebNodeValue evaluate_math(const bNode &node)
{
const bNodeSocket *a_socket = find_input(node, "Value");
const bNodeSocket *b_socket = nullptr;
for (const bNodeSocket &socket : node.inputs) {
if (STREQ(socket.identifier, "Value_001")) {
b_socket = &socket;
}
}
if (a_socket == nullptr || b_socket == nullptr) {
error_ = "Math sockets are incomplete";
return {};
}
const std::optional<float> a = number(evaluate_input(*a_socket));
const std::optional<float> b = number(evaluate_input(*b_socket));
if (!a || !b) {
error_ = "Math scalar input is unavailable";
return {};
}
switch (node.custom1) {
case NODE_MATH_ADD:
return *a + *b;
case NODE_MATH_SUBTRACT:
return *a - *b;
case NODE_MATH_MULTIPLY:
return *a * *b;
case NODE_MATH_DIVIDE:
return *b == 0.0f ? 0.0f : *a / *b;
case NODE_MATH_MINIMUM:
return std::min(*a, *b);
case NODE_MATH_MAXIMUM:
return std::max(*a, *b);
default:
error_ = "Math operation is outside the bounded evaluator";
return {};
}
}
WebNodeValue evaluate_collection_info(const bNode &node)
{
const bNodeSocket *collection_socket = find_input(node, "Collection");
const bNodeSocket *separate_socket = find_input(node, "Separate Children");
const bNodeSocket *reset_socket = find_input(node, "Reset Children");
if (collection_socket == nullptr || separate_socket == nullptr || reset_socket == nullptr) {
error_ = "Collection Info sockets are incomplete";
return {};
}
const WebNodeValue collection_value = evaluate_input(*collection_socket);
const WebNodeValue separate_value = evaluate_input(*separate_socket);
const WebNodeValue reset_value = evaluate_input(*reset_socket);
Collection *const *collection = std::get_if<Collection *>(&collection_value);
const bool *separate_children = std::get_if<bool>(&separate_value);
const bool *reset_children = std::get_if<bool>(&reset_value);
if (collection == nullptr || *collection == nullptr || separate_children == nullptr ||
reset_children == nullptr || !*separate_children)
{
error_ = "Collection Info requires a concrete collection with Separate Children enabled";
return {};
}
Vector<Object *> objects;
for (const CollectionObject &entry : (*collection)->gobject) {
if (entry.ob != nullptr && entry.ob != ctx_.object) {
objects.append(entry.ob);
}
}
if (objects.size() > max_instance_elements) {
error_ = "Collection Info instance domain budget exceeded";
return {};
}
auto instances = std::make_unique<bke::Instances>();
instances->resize(objects.size());
MutableSpan<int> handles = instances->reference_handles_for_write();
MutableSpan<float4x4> transforms = instances->transforms_for_write();
for (const int index : objects.index_range()) {
Object *evaluated = reinterpret_cast<Object *>(
DEG_get_evaluated_id(ctx_.depsgraph, &objects[index]->id));
if (evaluated == nullptr) {
error_ = "Collection Info object is not available in the evaluated dependency graph";
return {};
}
handles[index] = instances->add_reference(*evaluated);
transforms[index] = *reset_children ? float4x4::identity() : evaluated->object_to_world();
}
instances->tag_reference_handles_changed();
return bounded_geometry(bke::GeometrySet::from_instances(std::move(instances)));
}
WebNodeValue evaluate_output(const bNodeSocket &socket)
{
if (++evaluation_count_ > max_evaluations) {
error_ = "evaluation budget exceeded";
return {};
}
const bNode *node = owner_of(socket);
if (node == nullptr) {
error_ = "output socket owner is missing";
return {};
}
if (node_is(*node, "NodeGroupInput")) {
if (socket.type != SOCK_GEOMETRY) {
error_ = "only the Geometry group input is supported";
return {};
}
if (!mesh_within_domain_budget(input_mesh_)) {
return {};
}
return bounded_geometry(
bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(input_mesh_)));
}
if (node_is(*node, "FunctionNodeInputInt")) {
const NodeInputInt *storage = static_cast<const NodeInputInt *>(node->storage);
return storage == nullptr ? WebNodeValue{} : WebNodeValue{storage->integer};
}
if (node_is(*node, "FunctionNodeInputVector")) {
const NodeInputVector *storage = static_cast<const NodeInputVector *>(node->storage);
if (storage == nullptr || storage->dimensions != 3) {
error_ = "Vector input requires exactly three dimensions";
return {};
}
return float3(storage->vector);
}
if (node_is(*node, "ShaderNodeValue")) {
return default_value(socket);
}
if (node_is(*node, "ShaderNodeMath")) {
return evaluate_math(*node);
}
if (node_is(*node, "FunctionNodeCompare")) {
return evaluate_compare(*node);
}
if (node_is(*node, "GeometryNodeImageInfo")) {
const bNodeSocket *image_socket = find_input(*node, "Image");
if (image_socket == nullptr) {
error_ = "Image Info image input is missing";
return {};
}
const WebNodeValue image_value = evaluate_input(*image_socket);
Image *const *image = std::get_if<Image *>(&image_value);
if (image == nullptr || *image == nullptr) {
error_ = "Image Info image is unavailable";
return {};
}
if (STREQ(socket.identifier, "Width")) {
return (*image)->gen_x;
}
if (STREQ(socket.identifier, "Height")) {
return (*image)->gen_y;
}
if (STREQ(socket.identifier, "Has Alpha")) {
return true;
}
if (STREQ(socket.identifier, "Frame Count")) {
return 1;
}
if (STREQ(socket.identifier, "FPS")) {
return 0.0f;
}
error_ = "Image Info output is unsupported";
return {};
}
if (node_is(*node, "GeometryNodeObjectInfo")) {
const bNodeSocket *object_socket = find_input(*node, "Object");
if (object_socket == nullptr) {
error_ = "Object Info object input is missing";
return {};
}
const WebNodeValue object_value = evaluate_input(*object_socket);
Object *const *object = std::get_if<Object *>(&object_value);
if (object == nullptr || *object == nullptr || *object == ctx_.object) {
error_ = "Object Info target is unavailable or recursive";
return {};
}
Object *evaluated = reinterpret_cast<Object *>(
DEG_get_evaluated_id(ctx_.depsgraph, &(*object)->id));
if (evaluated == nullptr) {
error_ = "Object Info target is not evaluated";
return {};
}
if (STREQ(socket.identifier, "Location")) {
return float3(evaluated->object_to_world().location());
}
if (STREQ(socket.identifier, "Scale")) {
return float3(evaluated->scale);
}
if (STREQ(socket.identifier, "Geometry")) {
Mesh *target_mesh = BKE_object_get_evaluated_mesh(evaluated);
if (target_mesh == nullptr) {
error_ = "Object Info target has no evaluated mesh";
return {};
}
if (!mesh_within_domain_budget(*target_mesh)) {
return {};
}
return bounded_geometry(
bke::GeometrySet::from_mesh(BKE_mesh_copy_for_eval(*target_mesh)));
}
error_ = "Object Info output is outside the bounded evaluator";
return {};
}
if (node_is(*node, "GeometryNodeCollectionInfo")) {
return evaluate_collection_info(*node);
}
if (node_is(*node, "GeometryNodeJoinGeometry")) {
const bNodeSocket *input = find_input(*node, "Geometry");
if (input == nullptr) {
error_ = "Join Geometry input is missing";
return {};
}
Vector<bke::GeometrySet> geometries;
for (const bNodeLink *link : input_links(*input)) {
WebNodeValue value = evaluate_output(*link->fromsock);
bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value);
if (geometry == nullptr) {
error_ = "Join Geometry received a non-geometry input";
return {};
}
geometries.append(std::move(*geometry));
}
if (geometries.is_empty()) {
return bke::GeometrySet();
}
int64_t points = 0;
int64_t edges = 0;
int64_t faces = 0;
int64_t corners = 0;
int64_t instances = 0;
for (const bke::GeometrySet &geometry : geometries) {
if (const Mesh *mesh = geometry.get_mesh()) {
points += mesh->verts_num;
edges += mesh->edges_num;
faces += mesh->faces_num;
corners += mesh->corners_num;
}
if (const bke::Instances *geometry_instances = geometry.get_instances()) {
instances += geometry_instances->instances_num();
}
}
if (points > max_point_elements || edges > max_edge_elements ||
faces > max_face_elements || corners > max_corner_elements ||
instances > max_instance_elements)
{
error_ = "Join Geometry field/domain budget exceeded";
return {};
}
return bounded_geometry(geometry::join_geometries(
geometries.as_span(), bke::AttributeFilter::default_filter()));
}
if (node_is(*node, "GeometryNodeSeparateGeometry")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
if (!geometry || selection_socket == nullptr) {
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const bool *selection = std::get_if<bool>(&selection_value);
if (selection == nullptr) {
error_ = "Separate Geometry requires a bounded constant selection";
return {};
}
const bool selected_output = STREQ(socket.identifier, "Selection");
const bool inverted_output = STREQ(socket.identifier, "Inverted");
if (!selected_output && !inverted_output) {
error_ = "Separate Geometry output is invalid";
return {};
}
return bounded_geometry((*selection == selected_output) ? std::move(*geometry) :
bke::GeometrySet());
}
if (node_is(*node, "GeometryNodeRealizeInstances")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
if (!geometry) {
return {};
}
geometry::RealizeInstancesOptions options;
options.keep_original_ids = false;
options.realize_instance_attributes = true;
options.realize_to_point_domain = true;
geometry::RealizeInstancesResult result = geometry::realize_instances(std::move(*geometry),
options);
if (!result.errors.is_empty()) {
error_ = result.errors.first();
return {};
}
return bounded_geometry(std::move(result.geometry));
}
if (node_is(*node, "GeometryNodeStoreNamedAttribute")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
const bNodeSocket *name_socket = find_input(*node, "Name");
const bNodeSocket *value_socket = find_input(*node, "Value");
const NodeGeometryStoreNamedAttribute *storage =
static_cast<const NodeGeometryStoreNamedAttribute *>(node->storage);
if (!geometry || selection_socket == nullptr || name_socket == nullptr ||
value_socket == nullptr || storage == nullptr || storage->data_type != CD_PROP_FLOAT ||
storage->domain != int8_t(bke::AttrDomain::Point))
{
error_ = "Store Named Attribute supports Float point attributes only";
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const WebNodeValue name_value = evaluate_input(*name_socket);
const std::optional<float> attribute_value = number(evaluate_input(*value_socket));
const bool *selection = std::get_if<bool>(&selection_value);
const std::string *name = std::get_if<std::string>(&name_value);
if (selection == nullptr || name == nullptr || name->empty() || name->size() > 64 ||
!attribute_value)
{
error_ = "Store Named Attribute inputs are invalid";
return {};
}
bke::GeometrySet result = copy_geometry(*geometry);
if (*selection) {
Mesh *result_mesh = result.get_mesh_for_write();
if (result_mesh == nullptr) {
error_ = "Store Named Attribute requires mesh geometry";
return {};
}
if (!mesh_within_domain_budget(*result_mesh) ||
int64_t(result_mesh->verts_num) * int64_t(sizeof(float)) > max_field_bytes)
{
error_ = "Store Named Attribute field budget exceeded";
return {};
}
bke::SpanAttributeWriter<float> writer =
result_mesh->attributes_for_write().lookup_or_add_for_write_span<float>(
*name, bke::AttrDomain::Point);
if (!writer) {
error_ = "Store Named Attribute could not allocate the point attribute";
return {};
}
writer.span.fill(*attribute_value);
writer.finish();
}
return bounded_geometry(std::move(result));
}
if (node_is(*node, "GeometryNodeTransform")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *translation_socket = find_input(*node, "Translation");
const bNodeSocket *rotation_socket = find_input(*node, "Rotation");
const bNodeSocket *scale_socket = find_input(*node, "Scale");
if (!geometry || translation_socket == nullptr || rotation_socket == nullptr ||
scale_socket == nullptr || translation_socket->default_value == nullptr ||
rotation_socket->default_value == nullptr || scale_socket->default_value == nullptr ||
has_input_link(tree_, *node, "Rotation") || has_input_link(tree_, *node, "Scale"))
{
error_ = "Transform Geometry inputs are outside the bounded evaluator";
return {};
}
WebNodeValue translation_value = evaluate_input(*translation_socket);
const float3 *translation = std::get_if<float3>(&translation_value);
if (translation == nullptr) {
error_ = "Transform Geometry translation is unavailable";
return {};
}
const bNodeSocketValueRotation &rotation_value =
*static_cast<const bNodeSocketValueRotation *>(rotation_socket->default_value);
const math::Quaternion rotation = math::to_quaternion(
math::EulerXYZ(float3(rotation_value.value_euler)));
const float3 scale(
static_cast<const bNodeSocketValueVector *>(scale_socket->default_value)->value);
bke::GeometrySet result = copy_geometry(*geometry);
geometry::transform_geometry(
result, math::from_loc_rot_scale<float4x4>(*translation, rotation, scale));
return bounded_geometry(std::move(result));
}
if (node_is(*node, "GeometryNodeSetPosition")) {
std::optional<bke::GeometrySet> geometry = geometry_input(*node, "Geometry");
const bNodeSocket *selection_socket = find_input(*node, "Selection");
const bNodeSocket *offset_socket = find_input(*node, "Offset");
if (!geometry || selection_socket == nullptr || offset_socket == nullptr ||
has_input_link(tree_, *node, "Position"))
{
error_ = "Set Position inputs are outside the bounded evaluator";
return {};
}
const WebNodeValue selection_value = evaluate_input(*selection_socket);
const WebNodeValue offset_value = evaluate_input(*offset_socket);
const bool *selection = std::get_if<bool>(&selection_value);
const float3 *offset = std::get_if<float3>(&offset_value);
if (selection == nullptr || offset == nullptr) {
error_ = "Set Position requires a constant selection and vector offset";
return {};
}
bke::GeometrySet result = copy_geometry(*geometry);
if (*selection) {
geometry::translate_geometry(result, *offset);
}
return bounded_geometry(std::move(result));
}
error_ = std::string("unsupported node type: ") + node->idname;
return {};
}
public:
WebGeometryNodeEvaluator(const bNodeTree &tree,
const ModifierEvalContext &ctx,
const Mesh &input_mesh)
: tree_(tree), ctx_(ctx), input_mesh_(input_mesh)
{
}
Mesh *evaluate()
{
const bNode *group_output = nullptr;
for (const bNode &node : tree_.nodes) {
if (node_is(node, "NodeGroupOutput")) {
if (group_output != nullptr) {
error_ = "multiple Group Output nodes are unsupported";
return nullptr;
}
group_output = &node;
}
}
const bNodeSocket *geometry_socket = group_output == nullptr ? nullptr :
find_input(*group_output,
"Geometry");
if (geometry_socket == nullptr) {
error_ = "Geometry Group Output is missing";
return nullptr;
}
WebNodeValue value = evaluate_input(*geometry_socket);
bke::GeometrySet *geometry = std::get_if<bke::GeometrySet>(&value);
if (geometry == nullptr || !error_.empty()) {
if (error_.empty()) {
error_ = "Geometry Group Output did not evaluate to geometry";
}
return nullptr;
}
if (!geometry_within_domain_budget(*geometry)) {
return nullptr;
}
bke::MeshComponent &mesh_component =
geometry->get_component_for_write<bke::MeshComponent>();
mesh_component.ensure_owns_direct_data();
Mesh *result = mesh_component.release();
return result == nullptr ? BKE_mesh_new_nomain(0, 0, 0, 0) : result;
}
const std::string &error() const
{
return error_;
}
};
static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh *mesh) static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh *mesh)
{ {
const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(md); const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(md);
@@ -168,13 +858,8 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
return mesh; return mesh;
} }
const bNode *group_input = nullptr;
const bNode *group_output = nullptr;
const bNode *transform = nullptr;
const bNode *set_position = nullptr;
bool has_simulation_node = false; bool has_simulation_node = false;
int node_count = 0; int node_count = 0;
int link_count = 0;
for (const bNode &node : tree->nodes) { for (const bNode &node : tree->nodes) {
node_count++; node_count++;
if (ELEM(node.type_legacy, if (ELEM(node.type_legacy,
@@ -188,25 +873,6 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
{ {
has_simulation_node = true; has_simulation_node = true;
} }
if (find_input(node, "Translation") != nullptr && find_input(node, "Rotation") != nullptr &&
find_input(node, "Scale") != nullptr)
{
transform = &node;
}
else if (find_input(node, "Offset") != nullptr && find_input(node, "Position") != nullptr &&
find_input(node, "Selection") != nullptr)
{
set_position = &node;
}
else if (node.inputs.first == nullptr && node.outputs.first != nullptr) {
group_input = &node;
}
else if (node.inputs.first != nullptr && node.outputs.first == nullptr) {
group_output = &node;
}
}
for ([[maybe_unused]] const bNodeLink &link : tree->links) {
link_count++;
} }
if (has_simulation_node) { if (has_simulation_node) {
@@ -217,85 +883,20 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
return mesh; return mesh;
} }
const bNode *geometry_node = transform != nullptr ? transform : set_position; if (node_count > WebGeometryNodeEvaluator::max_evaluations) {
const bool input_link_valid = group_input != nullptr && geometry_node != nullptr &&
has_link(*tree, *group_input, "Geometry", *geometry_node, "Geometry");
const bool output_link_valid = geometry_node != nullptr && group_output != nullptr &&
has_link(*tree, *geometry_node, "Geometry", *group_output, "Geometry");
if (node_count != 3 || link_count != 2 || group_input == nullptr ||
group_output == nullptr || geometry_node == nullptr || (transform != nullptr && set_position != nullptr) ||
!input_link_valid || !output_link_valid)
{
BKE_modifier_set_error(
ctx->object,
md,
"Web Geometry Nodes supports only a single constant Transform Geometry or Set Position node");
return mesh;
}
if (set_position != nullptr) {
const bNodeSocket *selection_socket = find_input(*set_position, "Selection");
const bNodeSocket *offset_socket = find_input(*set_position, "Offset");
if (selection_socket == nullptr || offset_socket == nullptr ||
selection_socket->default_value == nullptr || offset_socket->default_value == nullptr ||
has_input_link(*tree, *set_position, "Selection") ||
has_input_link(*tree, *set_position, "Position") ||
has_input_link(*tree, *set_position, "Offset"))
{
BKE_modifier_set_error(
ctx->object, md, "Web Geometry Nodes Set Position supports only unlinked constant inputs");
return mesh;
}
Mesh *result = BKE_mesh_copy_for_eval(*mesh);
const bool selected = static_cast<const bNodeSocketValueBoolean *>(
selection_socket->default_value)
->value;
if (selected) {
const float3 offset(
static_cast<const bNodeSocketValueVector *>(offset_socket->default_value)->value);
for (float3 &position : result->vert_positions_for_write()) {
position += offset;
}
result->tag_positions_changed();
}
return result;
}
const bNodeSocket *mode_socket = find_input(*transform, "Mode");
const int mode = mode_socket && mode_socket->default_value ?
static_cast<const bNodeSocketValueMenu *>(mode_socket->default_value)->value :
GEO_NODE_TRANSFORM_MODE_COMPONENTS;
if (mode != GEO_NODE_TRANSFORM_MODE_COMPONENTS) {
BKE_modifier_set_error(ctx->object, BKE_modifier_set_error(ctx->object,
md, md,
"Web Geometry Nodes Transform supports Components mode only"); "WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: node budget exceeded");
return mesh; return mesh;
} }
const bNodeSocket *translation_socket = find_input(*transform, "Translation"); WebGeometryNodeEvaluator evaluator(*tree, *ctx, *mesh);
const bNodeSocket *rotation_socket = find_input(*transform, "Rotation"); Mesh *result = evaluator.evaluate();
const bNodeSocket *scale_socket = find_input(*transform, "Scale"); if (result == nullptr) {
if (translation_socket == nullptr || rotation_socket == nullptr || scale_socket == nullptr || const std::string message = "WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: " + evaluator.error();
translation_socket->default_value == nullptr || rotation_socket->default_value == nullptr || BKE_modifier_set_error(ctx->object, md, "%s", message.c_str());
scale_socket->default_value == nullptr)
{
BKE_modifier_set_error(ctx->object, md, "Web Geometry Nodes Transform sockets are incomplete");
return mesh; return mesh;
} }
const float3 translation(
static_cast<const bNodeSocketValueVector *>(translation_socket->default_value)->value);
const bNodeSocketValueRotation &rotation_value =
*static_cast<const bNodeSocketValueRotation *>(rotation_socket->default_value);
const math::Quaternion rotation = math::to_quaternion(
math::EulerXYZ(float3(rotation_value.value_euler)));
const float3 scale(
static_cast<const bNodeSocketValueVector *>(scale_socket->default_value)->value);
Mesh *result = BKE_mesh_copy_for_eval(*mesh);
bke::mesh_transform(
*result, math::from_loc_rot_scale<float4x4>(translation, rotation, scale), false);
return result; return result;
} }

View File

@@ -8,6 +8,7 @@
#include "MOD_modifiertypes.hh" #include "MOD_modifiertypes.hh"
#include "UI_resources.hh" #include "UI_resources.hh"
#include "RNA_prototypes.hh" #include "RNA_prototypes.hh"
@@ -23,6 +24,7 @@ static bool is_disabled(const Scene * /*scene*/, ModifierData * /*md*/, bool /*u
return true; return true;
} }
ModifierTypeInfo modifierType_None = { ModifierTypeInfo modifierType_None = {
/*idname*/ "None", /*idname*/ "None",
/*name*/ "None", /*name*/ "None",

View File

@@ -252,6 +252,13 @@ static void blend_read(BlendDataReader *reader, ModifierData *md)
psmd->mesh_original = nullptr; psmd->mesh_original = nullptr;
/* This is written as part of ob->particlesystem. */ /* This is written as part of ob->particlesystem. */
BLO_read_struct(reader, ParticleSystem, &psmd->psys); BLO_read_struct(reader, ParticleSystem, &psmd->psys);
#ifdef WITH_WEB
/* Web metadata fixtures may contain an unlinked legacy particle pointer. Keep this modifier
* observable while preventing traversal into stale file-space list links. */
psmd->psys = nullptr;
psmd->modifier.next = nullptr;
psmd->modifier.prev = nullptr;
#endif
psmd->flag &= ~eParticleSystemFlag_psys_updated; psmd->flag &= ~eParticleSystemFlag_psys_updated;
psmd->flag |= eParticleSystemFlag_file_loaded; psmd->flag |= eParticleSystemFlag_file_loaded;
} }

View File

@@ -129,7 +129,7 @@ target_link_options(web_engine PRIVATE
"-sINITIAL_MEMORY=67108864" "-sINITIAL_MEMORY=67108864"
"-sMAXIMUM_MEMORY=2147483648" "-sMAXIMUM_MEMORY=2147483648"
"-sSTACK_SIZE=8388608" "-sSTACK_SIZE=8388608"
"-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']" "-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_append_library_object','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']"
"-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString']" "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString']"
) )
if(WEB_ENGINE_ENABLE_UNSAFE_DEPSGRAPH) if(WEB_ENGINE_ENABLE_UNSAFE_DEPSGRAPH)

View File

@@ -829,6 +829,13 @@ bool refresh_scene_from_main(EngineState &engine,
return false; return false;
} }
snapshot["greasePencils"] = json::parse(grease_pencils_json); snapshot["greasePencils"] = json::parse(grease_pencils_json);
std::string geometry_node_graphs_json;
if (!web_engine_blend_main_geometry_node_graphs_json(
engine.authoritative_main, geometry_node_graphs_json, error))
{
return false;
}
snapshot["geometryNodeGraphs"] = json::parse(geometry_node_graphs_json);
std::string physics_json; std::string physics_json;
if (!web_engine_blend_main_physics_simulation_json( if (!web_engine_blend_main_physics_simulation_json(
engine.authoritative_main, physics_json, error)) engine.authoritative_main, physics_json, error))
@@ -836,6 +843,16 @@ bool refresh_scene_from_main(EngineState &engine,
return false; return false;
} }
snapshot["physicsSimulation"] = json::parse(physics_json); snapshot["physicsSimulation"] = json::parse(physics_json);
std::string particle_settings_json;
if (!web_engine_blend_main_particle_settings_json(
engine.authoritative_main, particle_settings_json, error))
{
return false;
}
const json particle_settings = json::parse(particle_settings_json)["settings"];
if (!particle_settings.empty() || snapshot.value("particleSettings", json::array()).empty()) {
snapshot["particleSettings"] = particle_settings;
}
if (!active_object_id.empty()) snapshot["activeObjectId"] = active_object_id; if (!active_object_id.empty()) snapshot["activeObjectId"] = active_object_id;
engine.blend_bytes.assign(reinterpret_cast<const char *>(bytes.data()), bytes.size()); engine.blend_bytes.assign(reinterpret_cast<const char *>(bytes.data()), bytes.size());
engine.packed_assets = parsed.packed_assets; engine.packed_assets = parsed.packed_assets;
@@ -853,7 +870,7 @@ const char *empty_scene_snapshot_json()
"\"source\":{\"kind\":\"blend\"}," "\"source\":{\"kind\":\"blend\"},"
"\"coordinateSystem\":{\"upAxis\":\"Z\",\"forwardAxis\":\"-Y\",\"handedness\":\"RIGHT\",\"unitSystem\":0,\"unitScale\":1}," "\"coordinateSystem\":{\"upAxis\":\"Z\",\"forwardAxis\":\"-Y\",\"handedness\":\"RIGHT\",\"unitSystem\":0,\"unitScale\":1},"
"\"nodes\":[],\"meshes\":[],\"materials\":[],\"cameras\":[],\"lights\":[],\"worlds\":[],\"images\":[],\"animations\":[]," "\"nodes\":[],\"meshes\":[],\"materials\":[],\"cameras\":[],\"lights\":[],\"worlds\":[],\"images\":[],\"animations\":[],"
"\"collections\":[],\"scenes\":[],\"activeObjectId\":null," "\"collections\":[],\"scenes\":[],\"particleSettings\":[],\"sounds\":[],\"activeObjectId\":null,"
"\"frame\":{\"current\":1,\"start\":1,\"end\":250}}"; "\"frame\":{\"current\":1,\"start\":1,\"end\":250}}";
} }
@@ -1132,6 +1149,15 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
return last_error_code; return last_error_code;
} }
opened_snapshot["greasePencils"] = json::parse(grease_pencils_json); opened_snapshot["greasePencils"] = json::parse(grease_pencils_json);
std::string geometry_node_graphs_json;
if (!web_engine_blend_main_geometry_node_graphs_json(
authoritative_main, geometry_node_graphs_json, main_error))
{
web_engine_blend_main_free(authoritative_main);
set_error(WEB_ENGINE_BLEND_READ_FAILED, main_error.c_str());
return last_error_code;
}
opened_snapshot["geometryNodeGraphs"] = json::parse(geometry_node_graphs_json);
std::string physics_json; std::string physics_json;
if (!web_engine_blend_main_physics_simulation_json( if (!web_engine_blend_main_physics_simulation_json(
authoritative_main, physics_json, main_error)) authoritative_main, physics_json, main_error))
@@ -1141,6 +1167,18 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
return last_error_code; return last_error_code;
} }
opened_snapshot["physicsSimulation"] = json::parse(physics_json); opened_snapshot["physicsSimulation"] = json::parse(physics_json);
std::string particle_settings_json;
if (!web_engine_blend_main_particle_settings_json(
authoritative_main, particle_settings_json, main_error))
{
web_engine_blend_main_free(authoritative_main);
set_error(WEB_ENGINE_BLEND_READ_FAILED, main_error.c_str());
return last_error_code;
}
const json particle_settings = json::parse(particle_settings_json)["settings"];
if (!particle_settings.empty() || opened_snapshot.value("particleSettings", json::array()).empty()) {
opened_snapshot["particleSettings"] = particle_settings;
}
} }
catch (const std::exception &exception) { catch (const std::exception &exception) {
web_engine_blend_main_free(authoritative_main); web_engine_blend_main_free(authoritative_main);
@@ -1543,9 +1581,10 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
type == "moveObjectToCollection" || type == "renameId" || type == "joinObjects" || type == "moveObjectToCollection" || type == "renameId" || type == "joinObjects" ||
type == "separateMeshFaces" || type == "applyObjectTransform" || type == "setObjectOrigin" || type == "separateMeshFaces" || type == "applyObjectTransform" || type == "setObjectOrigin" ||
type == "setCurveControlPoints" || type == "setCurveHandle" || type == "setCurveTopology" || type == "setCurveSplines" || type == "setSurfaceTopology" || type == "setFontBody" || type == "setCurveControlPoints" || type == "setCurveHandle" || type == "setCurveTopology" || type == "setCurveSplines" || type == "setSurfaceTopology" || type == "setFontBody" ||
type == "setFontProperties" || type == "setFontAdvanced" || type == "setFontLinks" || type == "setVolumeProperties" || type == "deleteNonMeshData" || type == "setFontProperties" || type == "setFontAdvanced" || type == "importVFont" || type == "setFontLinks" || type == "setVolumeProperties" || type == "deleteNonMeshData" ||
type == "setMetaballElements" || type == "createGreasePencilLayer" || type == "setMetaballElements" || type == "createGreasePencilLayer" ||
type == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" || type == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" ||
type == "moveGreasePencilFrame" ||
type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" || type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" ||
type == "setGreasePencilStrokes" || type == "setVertexColors" || type == "setGreasePencilStrokes" || type == "setVertexColors" ||
type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" || type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" ||
@@ -1919,6 +1958,13 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(), characters, engine->authoritative_main, command.value("dataId", "").c_str(), characters,
text_boxes, command.value("activeTextBox", -1), main_error); text_boxes, command.value("activeTextBox", -1), main_error);
} }
else if (type == "importVFont") {
std::string font_id;
applied = web_engine_blend_main_import_vfont(
engine->authoritative_main, command.value("name", "").c_str(),
command.value("sourcePath", "").c_str(), command.value("base64", "").c_str(),
font_id, main_error);
}
else if (type == "setFontLinks") { else if (type == "setFontLinks") {
const json links = command.value("links", json::object()); const json links = command.value("links", json::object());
const std::array<const char *, 4> fields = {"regular", "bold", "italic", "boldItalic"}; const std::array<const char *, 4> fields = {"regular", "bold", "italic", "boldItalic"};
@@ -1984,6 +2030,12 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(), engine->authoritative_main, command.value("dataId", "").c_str(),
command.value("layerId", "").c_str(), command.value("direction", "UP").c_str(), main_error); command.value("layerId", "").c_str(), command.value("direction", "UP").c_str(), main_error);
} }
else if (type == "moveGreasePencilFrame") {
applied = web_engine_blend_main_move_grease_pencil_frame(
engine->authoritative_main, command.value("dataId", "").c_str(),
command.value("layerId", "").c_str(), command.value("frame", -1),
command.value("targetFrame", -1), command.value("drawingId", "").c_str(), main_error);
}
else if (type == "insertGreasePencilFrame") { else if (type == "insertGreasePencilFrame") {
applied = web_engine_blend_main_insert_grease_pencil_frame( applied = web_engine_blend_main_insert_grease_pencil_frame(
engine->authoritative_main, command.value("dataId", "").c_str(), engine->authoritative_main, command.value("dataId", "").c_str(),
@@ -2066,7 +2118,8 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("vertexGroup", "").c_str(), command.value("vertexGroup", "").c_str(),
command.value("indices", std::vector<uint32_t>()), command.value("indices", std::vector<uint32_t>()),
command.value("values", std::vector<float>()), command.value("values", std::vector<float>()),
command.value("normalize", false), command.value("mirror", false), main_error); command.value("normalize", false), command.value("limit", 0u), command.value("mirror", false),
command.value("mirrorAxis", 0), command.value("mirrorTolerance", 1e-4f), main_error);
} }
else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties") else if (type == "setCameraProperties" || type == "setLightProperties" || type == "setWorldProperties")
{ {
@@ -2114,6 +2167,139 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
} }
} }
EMSCRIPTEN_KEEPALIVE int web_engine_append_library_object(const int handle,
const uint8_t *source_data,
const uint32_t source_length,
const uint8_t *metadata,
const uint32_t metadata_length)
{
EngineState *engine = find_engine(handle);
if (engine == nullptr) return last_error_code;
if (source_data == nullptr || source_length == 0 ||
source_length > 64u * 1024u * 1024u || metadata == nullptr || metadata_length == 0)
{
set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_BUDGET_EXCEEDED: library append input is empty or exceeds 64 MiB");
return last_error_code;
}
if (!engine->authoritative_commands || engine->authoritative_main == nullptr) {
set_error(WEB_ENGINE_NOT_IMPLEMENTED,
"COMMAND_REQUIRES_MAIN_AUTHORITY: library append requires an authoritative Main");
return last_error_code;
}
try {
const json request = json::parse(reinterpret_cast<const char *>(metadata),
reinterpret_cast<const char *>(metadata) + metadata_length);
if (!request.is_object() || request.size() != 5 || request.value("schemaVersion", 0) != 1 ||
!request.contains("baseRevision") || !request["baseRevision"].is_number_unsigned() ||
!request.contains("sourceLocator") || !request["sourceLocator"].is_string() ||
!request.contains("sourceDataBlockId") || !request["sourceDataBlockId"].is_string() ||
!request.contains("expectedClosure") || !request["expectedClosure"].is_object())
{
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"ASSET_MANIFEST_INVALID: native library append metadata is malformed");
return last_error_code;
}
for (const char *key : {"schemaVersion", "baseRevision", "sourceLocator", "sourceDataBlockId", "expectedClosure"}) {
if (!request.contains(key)) {
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"ASSET_MANIFEST_INVALID: native library append metadata fields are not exact");
return last_error_code;
}
}
const uint64_t base_revision = request["baseRevision"].get<uint64_t>();
if (base_revision != engine->revision) {
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"REVISION_CONFLICT: library append base revision does not match the current SceneIR");
return last_error_code;
}
const json &closure = request["expectedClosure"];
if (closure.size() != 4) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_MANIFEST_INVALID: append closure fields are not exact");
return last_error_code;
}
auto closure_name = [&](const char *field, const char *prefix) -> std::string {
if (!closure.contains(field) || !closure[field].is_string()) return {};
const std::string value = closure[field].get<std::string>();
const size_t prefix_length = strlen(prefix);
if (value.size() <= prefix_length || value.compare(0, prefix_length, prefix) != 0) return {};
return value.substr(prefix_length);
};
const std::string object_name = closure_name("object", "Object/");
const std::string mesh_name = closure_name("mesh", "Mesh/");
const std::string material_name = closure_name("material", "Material/");
const std::string image_name = closure_name("image", "Image/");
if (object_name.empty() || mesh_name.empty() || material_name.empty() || image_name.empty() ||
request["sourceDataBlockId"].get<std::string>() != "Object/" + object_name)
{
set_error(WEB_ENGINE_INVALID_ARGUMENT,
"ASSET_SOURCE_HASH_MISMATCH: native append root does not match the expected closure");
return last_error_code;
}
HistoryEntry previous_state;
std::string history_error;
if (!capture_history_entry(*engine, previous_state, history_error)) {
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str());
return last_error_code;
}
const uint64_t previous_revision = engine->revision;
const std::string previous_snapshot = engine->scene_snapshot.empty() ?
std::string(empty_scene_snapshot_json()) :
engine->scene_snapshot;
const std::string previous_delta = engine->scene_delta;
const auto previous_packed_assets = engine->packed_assets;
auto rollback = [&]() {
std::string rollback_error;
if (restore_history_entry(*engine, previous_state, rollback_error)) {
engine->revision = previous_revision;
engine->scene_snapshot = previous_snapshot;
engine->scene_delta = previous_delta;
engine->packed_assets = previous_packed_assets;
engine->binary_revision = std::numeric_limits<uint64_t>::max();
}
};
std::string object_id;
std::string append_error;
if (!web_engine_blend_main_append_object(engine->authoritative_main,
source_data,
source_length,
request["sourceLocator"].get_ref<const std::string &>().c_str(),
object_name.c_str(),
mesh_name.c_str(),
material_name.c_str(),
image_name.c_str(),
object_id,
append_error))
{
rollback();
set_error(append_error.rfind("BLEND_READ_FAILED:", 0) == 0 ?
WEB_ENGINE_BLEND_READ_FAILED : WEB_ENGINE_INVALID_ARGUMENT,
append_error.c_str());
return last_error_code;
}
json snapshot;
if (!refresh_scene_from_main(*engine, snapshot, object_id, append_error)) {
rollback();
set_error(WEB_ENGINE_BLEND_WRITE_FAILED, append_error.c_str());
return last_error_code;
}
engine->authoritative_dirty = true;
push_history(engine->undo_history, std::move(previous_state));
engine->redo_history.clear();
engine->scene_snapshot = snapshot.dump();
engine->revision++;
engine->scene_delta = scene_delta_json(previous_snapshot, engine->scene_snapshot);
set_error(WEB_ENGINE_OK, "ok");
return WEB_ENGINE_OK;
}
catch (const std::exception &exception) {
set_error(WEB_ENGINE_INVALID_ARGUMENT, exception.what());
return last_error_code;
}
}
EMSCRIPTEN_KEEPALIVE int web_engine_undo(const int handle) EMSCRIPTEN_KEEPALIVE int web_engine_undo(const int handle)
{ {
EngineState *engine = find_engine(handle); EngineState *engine = find_engine(handle);

View File

@@ -28,6 +28,11 @@ uint32_t web_engine_get_live_handles(void);
uint32_t web_engine_get_allocated_bytes(void); uint32_t web_engine_get_allocated_bytes(void);
int web_engine_open_blend(int handle, const uint8_t *data, uint32_t length); int web_engine_open_blend(int handle, const uint8_t *data, uint32_t length);
int web_engine_apply_command(int handle, const uint8_t *data, uint32_t length); int web_engine_apply_command(int handle, const uint8_t *data, uint32_t length);
int web_engine_append_library_object(int handle,
const uint8_t *source_data,
uint32_t source_length,
const uint8_t *metadata,
uint32_t metadata_length);
int web_engine_undo(int handle); int web_engine_undo(int handle);
int web_engine_redo(int handle); int web_engine_redo(int handle);
int web_engine_get_scene_snapshot(int handle, const uint8_t **data, uint32_t *length); int web_engine_get_scene_snapshot(int handle, const uint8_t **data, uint32_t *length);

View File

@@ -32,6 +32,7 @@
#include "DNA_genfile.h" #include "DNA_genfile.h"
#include "DNA_curve_enums.h" #include "DNA_curve_enums.h"
#include "DNA_lattice_types.h"
#include "DNA_mask_types.h" #include "DNA_mask_types.h"
#include "DNA_modifier_types.h" #include "DNA_modifier_types.h"
#include "DNA_node_types.h" #include "DNA_node_types.h"
@@ -48,6 +49,7 @@ using namespace blender;
constexpr uint64_t MAX_BLEND_BLOCK_BYTES = UINT64_C(1024) * 1024 * 1024; constexpr uint64_t MAX_BLEND_BLOCK_BYTES = UINT64_C(1024) * 1024 * 1024;
constexpr size_t MAX_NON_MESH_POINTS = 1000000; constexpr size_t MAX_NON_MESH_POINTS = 1000000;
constexpr size_t MAX_LATTICE_POINTS = 64 * 64 * 64;
constexpr size_t MAX_SCRIPT_SOURCE_BYTES = 1024 * 1024; constexpr size_t MAX_SCRIPT_SOURCE_BYTES = 1024 * 1024;
uint32_t rotate_right(const uint32_t value, const uint32_t bits) uint32_t rotate_right(const uint32_t value, const uint32_t bits)
@@ -539,6 +541,13 @@ std::string id_prefix(const std::string &type_name)
if (type_name == "Library") return "library"; if (type_name == "Library") return "library";
if (type_name == "WorkSpace") return "workspace"; if (type_name == "WorkSpace") return "workspace";
if (type_name == "bScreen") return "screen"; if (type_name == "bScreen") return "screen";
if (type_name == "Brush") return "brush";
if (type_name == "FreestyleLineStyle") return "line-style";
if (type_name == "Lattice") return "lattice";
if (type_name == "ParticleSettings") return "particle-settings";
if (type_name == "bSound") return "sound";
if (type_name == "Speaker") return "speaker";
if (type_name == "Tex") return "texture";
return "datablock"; return "datablock";
} }
@@ -552,7 +561,9 @@ bool is_exported_id_type(const std::string &type_name)
type_name == "MetaBall" || type_name == "PointCloud" || type_name == "Curves" || type_name == "MetaBall" || type_name == "PointCloud" || type_name == "Curves" ||
type_name == "Volume" || type_name == "GreasePencil" || type_name == "Text" || type_name == "Volume" || type_name == "GreasePencil" || type_name == "Text" ||
type_name == "VFont" || type_name == "Mask" || type_name == "Library" || type_name == "VFont" || type_name == "Mask" || type_name == "Library" ||
type_name == "WorkSpace" || type_name == "bScreen"; type_name == "WorkSpace" || type_name == "bScreen" || type_name == "Brush" ||
type_name == "FreestyleLineStyle" || type_name == "Lattice" ||
type_name == "ParticleSettings" || type_name == "bSound" || type_name == "Speaker" || type_name == "Tex";
} }
std::string unique_id(const std::string &prefix, std::string unique_id(const std::string &prefix,
@@ -613,6 +624,17 @@ std::string object_type_name(const int64_t type)
} }
} }
const char *lattice_interpolation_name(const int64_t type)
{
switch (type) {
case KEY_LINEAR: return "KEY_LINEAR";
case KEY_CARDINAL: return "KEY_CARDINAL";
case KEY_BSPLINE: return "KEY_BSPLINE";
case KEY_CATMULL_ROM: return "KEY_CATMULL_ROM";
default: return "UNKNOWN";
}
}
std::array<float, 16> trs_matrix(const std::vector<float> &location, std::array<float, 16> trs_matrix(const std::vector<float> &location,
const std::vector<float> &rotation, const std::vector<float> &rotation,
const std::vector<float> &scale) const std::vector<float> &scale)
@@ -1446,14 +1468,14 @@ std::string modifier_type_name(const int64_t type)
"BEVEL", "SHRINKWRAP", "FLUID_SIM", "MASK", "SIMPLE_DEFORM", "MULTIRES", "BEVEL", "SHRINKWRAP", "FLUID_SIM", "MASK", "SIMPLE_DEFORM", "MULTIRES",
"SURFACE", "UNSUPPORTED_31", "SHAPE_KEY", "SOLIDIFY", "SCREW", "WARP", "WEIGHT_VG_EDIT", "SURFACE", "UNSUPPORTED_31", "SHAPE_KEY", "SOLIDIFY", "SCREW", "WARP", "WEIGHT_VG_EDIT",
"WEIGHT_VG_MIX", "WEIGHT_VG_PROXIMITY", "OCEAN", "DYNAMIC_PAINT", "REMESH", "SKIN", "WEIGHT_VG_MIX", "WEIGHT_VG_PROXIMITY", "OCEAN", "DYNAMIC_PAINT", "REMESH", "SKIN",
"LAPLACIAN_SMOOTH", "TRIANGULATE", "UV_WARP", "MESH_CACHE", "LAPLACIAN_DEFORM", "WIREFRAME", "LAPLACIANSMOOTH", "TRIANGULATE", "UV_WARP", "MESH_CACHE", "LAPLACIANDEFORM", "WIREFRAME",
"DATA_TRANSFER", "NORMAL_EDIT", "CORRECTIVE_SMOOTH", "MESH_SEQUENCE_CACHE", "SURFACE_DEFORM", "DATA_TRANSFER", "NORMAL_EDIT", "CORRECTIVE_SMOOTH", "MESH_SEQUENCE_CACHE", "SURFACE_DEFORM",
"WEIGHTED_NORMAL", "WELD", "FLUID", "NODES", "MESH_TO_VOLUME", "VOLUME_DISPLACE", "VOLUME_TO_MESH", "WEIGHTED_NORMAL", "WELD", "FLUID", "NODES", "MESH_TO_VOLUME", "VOLUME_DISPLACE", "VOLUME_TO_MESH",
"GREASE_PENCIL_OPACITY", "GREASE_PENCIL_SUBDIV", "GREASE_PENCIL_COLOR", "GREASE_PENCIL_TINT", "GREASE_PENCIL_OPACITY", "GREASE_PENCIL_SUBDIV", "GREASE_PENCIL_COLOR", "GREASE_PENCIL_TINT",
"GREASE_PENCIL_SMOOTH", "GREASE_PENCIL_OFFSET", "GREASE_PENCIL_NOISE", "GREASE_PENCIL_MIRROR", "GREASE_PENCIL_SMOOTH", "GREASE_PENCIL_OFFSET", "GREASE_PENCIL_NOISE", "GREASE_PENCIL_MIRROR",
"GREASE_PENCIL_THICKNESS", "GREASE_PENCIL_LATTICE", "GREASE_PENCIL_DASH", "GREASE_PENCIL_MULTIPLY", "GREASE_PENCIL_THICKNESS", "GREASE_PENCIL_LATTICE", "GREASE_PENCIL_DASH", "GREASE_PENCIL_MULTIPLY",
"GREASE_PENCIL_LENGTH", "GREASE_PENCIL_WEIGHT_ANGLE", "GREASE_PENCIL_ARRAY", "GREASE_PENCIL_LENGTH", "GREASE_PENCIL_VERTEX_WEIGHT_ANGLE", "GREASE_PENCIL_ARRAY",
"GREASE_PENCIL_WEIGHT_PROXIMITY", "GREASE_PENCIL_HOOK", "GREASE_PENCIL_LINEART", "GREASE_PENCIL_VERTEX_WEIGHT_PROXIMITY", "GREASE_PENCIL_HOOK", "LINEART",
"GREASE_PENCIL_ARMATURE", "GREASE_PENCIL_TIME", "GREASE_PENCIL_ENVELOPE", "GREASE_PENCIL_ARMATURE", "GREASE_PENCIL_TIME", "GREASE_PENCIL_ENVELOPE",
"GREASE_PENCIL_OUTLINE", "GREASE_PENCIL_SHRINKWRAP", "GREASE_PENCIL_BUILD", "GREASE_PENCIL_OUTLINE", "GREASE_PENCIL_SHRINKWRAP", "GREASE_PENCIL_BUILD",
"GREASE_PENCIL_SIMPLIFY", "GREASE_PENCIL_TEXTURE"}; "GREASE_PENCIL_SIMPLIFY", "GREASE_PENCIL_TEXTURE"};
@@ -1501,7 +1523,8 @@ json modifier_parameters(const ParsedBlend &blend,
case 4: case 4:
parameters["start"] = read_float(*blend.sdna, modifier, "start").value_or(1.0f); parameters["start"] = read_float(*blend.sdna, modifier, "start").value_or(1.0f);
parameters["length"] = read_float(*blend.sdna, modifier, "length").value_or(100.0f); parameters["length"] = read_float(*blend.sdna, modifier, "length").value_or(100.0f);
parameters["randomize"] = read_float(*blend.sdna, modifier, "randomize").value_or(0.0f); parameters["randomize"] = read_integer(*blend.sdna, modifier, "randomize").value_or(0);
parameters["seed"] = read_integer(*blend.sdna, modifier, "seed").value_or(0);
break; break;
case 5: case 5:
parameters["flag"] = read_integer(*blend.sdna, modifier, "flag").value_or( parameters["flag"] = read_integer(*blend.sdna, modifier, "flag").value_or(
@@ -1580,8 +1603,10 @@ json modifier_parameters(const ParsedBlend &blend,
parameters["meshDeformFlags"] = read_integer(*blend.sdna, modifier, "flag").value_or(0); parameters["meshDeformFlags"] = read_integer(*blend.sdna, modifier, "flag").value_or(0);
break; break;
case 16: case 16:
parameters["vertexGroup"] = read_string(*blend.sdna, modifier, "defgrp_name");
parameters["factor"] = read_float(*blend.sdna, modifier, "fac").value_or(0.5f); parameters["factor"] = read_float(*blend.sdna, modifier, "fac").value_or(0.5f);
parameters["repeat"] = read_integer(*blend.sdna, modifier, "repeat").value_or(1); parameters["repeat"] = read_integer(*blend.sdna, modifier, "repeat").value_or(1);
parameters["smoothFlags"] = read_integer(*blend.sdna, modifier, "flag").value_or(14);
break; break;
case 17: case 17:
append_target("object"); append_target("object");
@@ -1603,6 +1628,11 @@ json modifier_parameters(const ParsedBlend &blend,
parameters["deformMethod"] = read_integer(*blend.sdna, modifier, "mode").value_or(0); parameters["deformMethod"] = read_integer(*blend.sdna, modifier, "mode").value_or(0);
parameters["deformAxis"] = read_integer(*blend.sdna, modifier, "deform_axis").value_or(0); parameters["deformAxis"] = read_integer(*blend.sdna, modifier, "deform_axis").value_or(0);
break; break;
case 42:
parameters["branchSmoothing"] = read_float(*blend.sdna, modifier, "branch_smoothing").value_or(0.0f);
parameters["skinFlags"] = read_integer(*blend.sdna, modifier, "flag").value_or(0);
parameters["symmetryAxes"] = read_integer(*blend.sdna, modifier, "symmetry_axes").value_or(1);
break;
case 33: case 33:
parameters["thickness"] = read_float(*blend.sdna, modifier, "offset_fac").value_or(0.01f); parameters["thickness"] = read_float(*blend.sdna, modifier, "offset_fac").value_or(0.01f);
parameters["offset"] = read_float(*blend.sdna, modifier, "offset").value_or(-1.0f); parameters["offset"] = read_float(*blend.sdna, modifier, "offset").value_or(-1.0f);
@@ -1661,9 +1691,9 @@ json modifier_stack_from_object(const ParsedBlend &blend,
for (const json &target : parameters["targetObjectIds"]) depends_on.push_back(target); for (const json &target : parameters["targetObjectIds"]) depends_on.push_back(target);
} }
const bool known_metadata = type == 1 || type == 2 || type == 3 || type == 4 || type == 5 || const bool known_metadata = type == 1 || type == 2 || type == 3 || type == 4 || type == 5 ||
type == 6 || type == 7 || type == 8 || type == 9 || type == 11 || type == 6 || type == 7 || type == 8 || type == 9 || type == 10 || type == 11 ||
type == 12 || type == 13 || type == 14 || type == 16 || type == 17 || type == 12 || type == 13 || type == 14 || type == 16 || type == 17 ||
type == 18 || type == 24 || type == 25 || type == 28 || type == 33 || type == 18 || type == 24 || type == 25 || type == 28 || type == 42 || type == 33 ||
type == 34 || type == 44 || type == 48 || type == 53 || type == 54 || type == 34 || type == 44 || type == 48 || type == 53 || type == 54 ||
type == 55 || type == 57; type == 55 || type == 57;
stack.push_back({{"uuid", uuid}, stack.push_back({{"uuid", uuid},
@@ -3124,6 +3154,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
json lights = json::array(); json lights = json::array();
json worlds = json::array(); json worlds = json::array();
json images = json::array(); json images = json::array();
json brushes = json::array();
json line_styles = json::array();
json lattices = json::array();
json particle_settings = json::array();
json sounds = json::array();
json speakers = json::array();
json texts = json::array();
json textures = json::array();
json workspace_resources = json::array();
json non_mesh_data = json::array(); json non_mesh_data = json::array();
json vfonts = json::array(); json vfonts = json::array();
json libraries = json::array(); json libraries = json::array();
@@ -3289,10 +3328,12 @@ json scene_ir_from_blend(const ParsedBlend &blend,
if (data_pointer) { if (data_pointer) {
const auto data_id = ids_by_pointer.find(*data_pointer); const auto data_id = ids_by_pointer.find(*data_pointer);
node["dataId"] = data_id != ids_by_pointer.end() ? json(data_id->second) : json(nullptr); node["dataId"] = data_id != ids_by_pointer.end() ? json(data_id->second) : json(nullptr);
const json object_modifier_stack = modifier_stack_from_object(blend, element, record.id, ids_by_pointer);
if (!object_modifier_stack.empty()) node["modifierStack"] = object_modifier_stack;
if (data_id != ids_by_pointer.end()) { if (data_id != ids_by_pointer.end()) {
if (const std::optional<ElementRef> data = element_for_pointer(blend, *data_pointer)) { if (const std::optional<ElementRef> data = element_for_pointer(blend, *data_pointer)) {
if (data->type_name == "Mesh") { if (data->type_name == "Mesh") {
const json stack = modifier_stack_from_object(blend, element, record.id, ids_by_pointer); const json stack = object_modifier_stack;
if (!stack.empty()) modifier_stacks_by_mesh_id.emplace(data_id->second, stack); if (!stack.empty()) modifier_stacks_by_mesh_id.emplace(data_id->second, stack);
} }
} }
@@ -3513,11 +3554,169 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"emissionStrength", std::clamp(emission_strength, 0.0f, 1000000.0f)} }; {"emissionStrength", std::clamp(emission_strength, 0.0f, 1000000.0f)} };
if (!normal_image_id.empty()) material["normalImageId"] = normal_image_id; if (!normal_image_id.empty()) material["normalImageId"] = normal_image_id;
if (!image_ids.empty()) material["imageIds"] = image_ids; if (!image_ids.empty()) material["imageIds"] = image_ids;
if (!material_nodes.empty()) material["nodes"] = std::move(material_nodes); if (!material_nodes.empty()) {
/* The browser compiler binds its report to this exact reader graph. */
material["shaderGraphHash"] = sha256_hex(json({
{"schemaVersion", 1},
{"materialId", record.id},
{"nodes", material_nodes},
{"links", material_links},
}).dump());
material["nodes"] = std::move(material_nodes);
}
if (!material_links.empty()) material["links"] = std::move(material_links); if (!material_links.empty()) material["links"] = std::move(material_links);
if (!warnings.empty()) material["warnings"] = warnings; if (!warnings.empty()) material["warnings"] = warnings;
materials.push_back(std::move(material)); materials.push_back(std::move(material));
} }
else if (record.type_name == "Brush") {
json brush = {
{"id", record.id},
{"name", record.name},
{"size", std::clamp<int64_t>(read_integer(*blend.sdna, element, "size").value_or(0), 0, 100000)},
{"alpha", std::clamp<float>(read_float(*blend.sdna, element, "alpha").value_or(1.0f), 0.0f, 1.0f)},
{"hardness", std::clamp<float>(read_float(*blend.sdna, element, "hardness").value_or(0.0f), 0.0f, 1.0f)},
{"spacing", std::clamp<int64_t>(read_integer(*blend.sdna, element, "spacing").value_or(0), 1, 1000)},
{"jitter", std::clamp<float>(read_float(*blend.sdna, element, "jitter").value_or(0.0f), 0.0f, 1.0f)},
{"sculptTool", read_integer(*blend.sdna, element, "sculpt_brush_type").value_or(0)},
};
brushes.push_back(std::move(brush));
}
else if (record.type_name == "FreestyleLineStyle") {
const std::vector<float> color = {read_float(*blend.sdna, element, "r").value_or(0.0f),
read_float(*blend.sdna, element, "g").value_or(0.0f),
read_float(*blend.sdna, element, "b").value_or(0.0f)};
const int64_t flag = read_integer(*blend.sdna, element, "flag").value_or(0);
line_styles.push_back({
{"id", record.id},
{"name", record.name},
{"color", float_array_or(color, {0.0f, 0.0f, 0.0f})},
{"alpha", std::clamp(read_float(*blend.sdna, element, "alpha").value_or(1.0f), 0.0f, 1.0f)},
{"thickness", std::clamp(read_float(*blend.sdna, element, "thickness").value_or(1.0f), 0.0f, 10000.0f)},
{"chaining", read_integer(*blend.sdna, element, "chaining").value_or(0)},
{"caps", read_integer(*blend.sdna, element, "caps").value_or(0)},
{"dashed", (flag & (1 << 2)) != 0},
{"modifierCounts", {
{"color", linked_list_elements(blend, element, "color_modifiers").size()},
{"alpha", linked_list_elements(blend, element, "alpha_modifiers").size()},
{"thickness", linked_list_elements(blend, element, "thickness_modifiers").size()},
{"geometry", linked_list_elements(blend, element, "geometry_modifiers").size()}}}
});
}
else if (record.type_name == "Lattice") {
const int64_t points_u = read_integer(*blend.sdna, element, "pntsu").value_or(0);
const int64_t points_v = read_integer(*blend.sdna, element, "pntsv").value_or(0);
const int64_t points_w = read_integer(*blend.sdna, element, "pntsw").value_or(0);
const bool dimensions_valid = points_u >= 1 && points_u <= 64 &&
points_v >= 1 && points_v <= 64 &&
points_w >= 1 && points_w <= 64;
const size_t point_count = dimensions_valid ?
size_t(points_u) * size_t(points_v) * size_t(points_w) :
0;
json points = json::array();
bool points_complete = point_count > 0 && point_count <= MAX_LATTICE_POINTS;
if (const std::optional<uint64_t> points_pointer = read_pointer(*blend.sdna, element, "def");
points_complete && points_pointer)
{
for (size_t index = 0; index < point_count; index++) {
const std::optional<ElementRef> point = raw_array_element(
blend, *points_pointer, "BPoint", index);
if (!point) {
points_complete = false;
break;
}
const std::vector<float> coordinate = read_float_array(
*blend.sdna, *point, "vec", 4);
if (coordinate.size() != 4) {
points_complete = false;
break;
}
points.push_back({
{"coDeform", {coordinate[0], coordinate[1], coordinate[2]}},
{"weight", std::clamp(read_float(*blend.sdna, *point, "weight").value_or(0.0f),
0.01f,
100.0f)},
{"selected", (read_integer(*blend.sdna, *point, "f1").value_or(0) & 1) != 0},
});
}
}
else {
points_complete = false;
}
const int64_t flag = read_integer(*blend.sdna, element, "flag").value_or(0);
json lattice = {
{"id", record.id},
{"name", record.name},
{"dimensions", {points_u, points_v, points_w}},
{"pointCount", point_count},
{"interpolation", {
lattice_interpolation_name(read_integer(*blend.sdna, element, "typeu").value_or(-1)),
lattice_interpolation_name(read_integer(*blend.sdna, element, "typev").value_or(-1)),
lattice_interpolation_name(read_integer(*blend.sdna, element, "typew").value_or(-1))}},
{"useOutside", (flag & LT_OUTSIDE) != 0},
{"activePoint", read_integer(*blend.sdna, element, "actbp").value_or(LT_ACTBP_NONE)},
{"vertexGroup", read_string(*blend.sdna, element, "vgroup")},
{"status", points_complete ? "AVAILABLE" : "BLOCKED"},
};
if (points_complete) lattice["points"] = std::move(points);
else lattice["errorCode"] = dimensions_valid ? "LATTICE_POINTS_UNREADABLE" :
"LATTICE_DIMENSIONS_INVALID";
lattices.push_back(std::move(lattice));
}
else if (record.type_name == "ParticleSettings") {
const int64_t particle_type = read_integer(*blend.sdna, element, "type").value_or(0);
const int64_t particle_from = read_integer(*blend.sdna, element, "from").value_or(0);
const int64_t particle_physics = read_integer(*blend.sdna, element, "phystype").value_or(0);
particle_settings.push_back({
{"id", record.id},
{"name", record.name},
{"type", particle_type == 2 ? 1 : 0},
{"from", particle_from == 1 ? 0 : particle_from == 0 ? 1 : 2},
{"distribution", std::max<int64_t>(0, read_integer(*blend.sdna, element, "distr").value_or(0))},
{"physicsType", particle_physics == 1 ? 0 : particle_physics == 2 ? 1 : particle_physics == 3 ? 2 : 3},
{"totalParticles", std::max<int64_t>(0, read_integer(*blend.sdna, element, "totpart").value_or(0))},
{"start", std::max(0.0f, read_float(*blend.sdna, element, "sta").value_or(0.0f))},
{"end", std::max(0.0f, read_float(*blend.sdna, element, "end").value_or(0.0f))},
{"lifetime", std::max(0.0f, read_float(*blend.sdna, element, "lifetime").value_or(0.0f))},
{"size", std::max(0.0f, read_float(*blend.sdna, element, "size").value_or(0.0f))},
{"drawSize", std::max(0.0f, read_float(*blend.sdna, element, "draw_size").value_or(0.0f))},
});
}
else if (record.type_name == "bSound") {
const std::string filepath = read_string(*blend.sdna, element, "filepath");
const std::optional<uint64_t> packed_pointer = read_pointer(*blend.sdna, element, "packedfile");
const std::vector<uint8_t> packed_bytes = packed_pointer ? packed_file_bytes(blend, *packed_pointer) : std::vector<uint8_t>();
json sound = {{"id", record.id},
{"name", record.name},
{"sourcePath", filepath},
{"packed", !packed_bytes.empty()},
{"volume", std::max(0.0f, read_float(*blend.sdna, element, "volume").value_or(1.0f))},
{"pitch", std::max(0.0f, read_float(*blend.sdna, element, "pitch").value_or(1.0f))},
{"audioChannels", std::max<int64_t>(0, read_integer(*blend.sdna, element, "audio_channels").value_or(0))},
{"sampleRate", std::max<int64_t>(0, read_integer(*blend.sdna, element, "samplerate").value_or(0))}};
if (!packed_bytes.empty()) sound["packedByteLength"] = packed_bytes.size();
sounds.push_back(std::move(sound));
}
else if (record.type_name == "Speaker") {
json speaker = {
{"id", record.id},
{"name", record.name},
{"volumeMax", std::max(0.0f, read_float(*blend.sdna, element, "volume_max").value_or(1.0f))},
{"volumeMin", std::max(0.0f, read_float(*blend.sdna, element, "volume_min").value_or(0.0f))},
{"distanceMax", std::max(0.0f, read_float(*blend.sdna, element, "distance_max").value_or(0.0f))},
{"distanceReference", std::max(0.0f, read_float(*blend.sdna, element, "distance_reference").value_or(1.0f))},
{"attenuation", std::max(0.0f, read_float(*blend.sdna, element, "attenuation").value_or(1.0f))},
{"coneAngleOuter", std::clamp(read_float(*blend.sdna, element, "cone_angle_outer").value_or(360.0f), 0.0f, 360.0f)},
{"coneAngleInner", std::clamp(read_float(*blend.sdna, element, "cone_angle_inner").value_or(360.0f), 0.0f, 360.0f)},
{"coneVolumeOuter", std::clamp(read_float(*blend.sdna, element, "cone_volume_outer").value_or(1.0f), 0.0f, 1.0f)},
{"volume", std::max(0.0f, read_float(*blend.sdna, element, "volume").value_or(1.0f))},
{"pitch", std::max(0.0f, read_float(*blend.sdna, element, "pitch").value_or(1.0f))},
};
if (const std::optional<uint64_t> sound_pointer = read_pointer(*blend.sdna, element, "sound")) {
const auto sound_id = ids_by_pointer.find(*sound_pointer);
if (sound_id != ids_by_pointer.end()) speaker["soundId"] = sound_id->second;
}
speakers.push_back(std::move(speaker));
}
else if (record.type_name == "Image") { else if (record.type_name == "Image") {
std::string filepath = read_string(*blend.sdna, element, "filepath"); std::string filepath = read_string(*blend.sdna, element, "filepath");
const std::vector<ElementRef> image_views = linked_list_elements(blend, element, "views"); const std::vector<ElementRef> image_views = linked_list_elements(blend, element, "views");
@@ -3615,6 +3814,18 @@ json scene_ir_from_blend(const ParsedBlend &blend,
non_mesh_data.push_back(non_mesh_data_from_record(blend, element, record, ids_by_pointer)); non_mesh_data.push_back(non_mesh_data_from_record(blend, element, record, ids_by_pointer));
} }
else if (record.type_name == "Text") { else if (record.type_name == "Text") {
if (const std::optional<json> text = script_source_from_record(blend, element, record)) {
json resource = {{"id", record.id}, {"name", record.name}};
resource["source"] = text->value("source", std::string());
resource["sourceSha256"] = text->value("sourceSha256", std::string());
resource["byteLength"] = text->value("byteLength", size_t(0));
resource["lineCount"] = text->value("lineCount", size_t(0));
resource["internal"] = text->value("internal", true);
resource["isDirty"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 0)) != 0;
resource["useModule"] = (read_integer(*blend.sdna, element, "flags").value_or(0) & (1 << 4)) != 0;
if (text->contains("sourcePath")) resource["sourcePath"] = text->at("sourcePath");
texts.push_back(std::move(resource));
}
if (script_sources.size() >= 1024) { if (script_sources.size() >= 1024) {
script_sources_blocked = true; script_sources_blocked = true;
} }
@@ -3634,11 +3845,37 @@ json scene_ir_from_blend(const ParsedBlend &blend,
script_sources_blocked = true; script_sources_blocked = true;
} }
} }
else if (record.type_name == "Tex") {
textures.push_back({
{"id", record.id},
{"name", record.name},
{"type", read_integer(*blend.sdna, element, "type").value_or(0)},
{"noiseScale", std::max(0.0f, read_float(*blend.sdna, element, "noisesize").value_or(0.25f))},
{"noiseDepth", std::clamp<int64_t>(read_integer(*blend.sdna, element, "noisedepth").value_or(2), 0, 30)},
{"intensity", std::max(0.0f, read_float(*blend.sdna, element, "bright").value_or(1.0f))},
{"contrast", std::max(0.0f, read_float(*blend.sdna, element, "contrast").value_or(1.0f))},
{"saturation", std::max(0.0f, read_float(*blend.sdna, element, "saturation").value_or(1.0f))},
});
}
else if (record.type_name == "WorkSpace") {
workspace_resources.push_back({{"id", record.id}, {"name", record.name}, {"screenCount", linked_list_elements(blend, element, "layouts").size()}});
}
else if (record.type_name == "VFont") { else if (record.type_name == "VFont") {
const std::string filepath = read_string(*blend.sdna, element, "filepath"); const std::string filepath = read_string(*blend.sdna, element, "filepath");
vfonts.push_back({{"id", record.id}, {"name", record.name}, {"sourcePath", filepath}, const std::optional<uint64_t> packed_file = read_pointer(*blend.sdna, element, "packedfile");
{"builtin", filepath == "<builtin>"}, const std::vector<uint8_t> packed_bytes = packed_file ? packed_file_bytes(blend, *packed_file) :
{"packed", read_pointer(*blend.sdna, element, "packedfile").value_or(0) != 0}}); std::vector<uint8_t>();
json resource = {{"id", record.id},
{"name", record.name},
{"sourcePath", filepath},
{"builtin", filepath == "<builtin>"},
{"packed", !packed_bytes.empty()}};
if (!packed_bytes.empty()) {
resource["packedByteLength"] = packed_bytes.size();
resource["sha256"] = sha256_hex(std::string(
reinterpret_cast<const char *>(packed_bytes.data()), packed_bytes.size()));
}
vfonts.push_back(std::move(resource));
} }
else if (record.type_name == "Camera") { else if (record.type_name == "Camera") {
const int64_t camera_type = read_integer(*blend.sdna, element, "type").value_or(0); const int64_t camera_type = read_integer(*blend.sdna, element, "type").value_or(0);
@@ -3927,6 +4164,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
sort_by_id(lights); sort_by_id(lights);
sort_by_id(worlds); sort_by_id(worlds);
sort_by_id(images); sort_by_id(images);
sort_by_id(brushes);
sort_by_id(line_styles);
sort_by_id(lattices);
sort_by_id(particle_settings);
sort_by_id(sounds);
sort_by_id(speakers);
sort_by_id(texts);
sort_by_id(textures);
sort_by_id(workspace_resources);
sort_by_id(non_mesh_data); sort_by_id(non_mesh_data);
sort_by_id(vfonts); sort_by_id(vfonts);
sort_by_id(libraries); sort_by_id(libraries);
@@ -3965,6 +4211,15 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"lights", std::move(lights)}, {"lights", std::move(lights)},
{"worlds", std::move(worlds)}, {"worlds", std::move(worlds)},
{"images", std::move(images)}, {"images", std::move(images)},
{"brushes", std::move(brushes)},
{"lineStyles", std::move(line_styles)},
{"lattices", std::move(lattices)},
{"particleSettings", std::move(particle_settings)},
{"sounds", std::move(sounds)},
{"speakers", std::move(speakers)},
{"texts", std::move(texts)},
{"textures", std::move(textures)},
{"workspaces", std::move(workspace_resources)},
{"nonMeshData", std::move(non_mesh_data)}, {"nonMeshData", std::move(non_mesh_data)},
{"vfonts", std::move(vfonts)}, {"vfonts", std::move(vfonts)},
{"libraryStatus", libraries_blocked ? "BLOCKED" : "AVAILABLE"}, {"libraryStatus", libraries_blocked ? "BLOCKED" : "AVAILABLE"},
@@ -3978,6 +4233,7 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"scenes", std::move(scenes)}, {"scenes", std::move(scenes)},
{"activeObjectId", active_object_id.empty() ? json(nullptr) : json(active_object_id)}, {"activeObjectId", active_object_id.empty() ? json(nullptr) : json(active_object_id)},
{"frame", {{"current", frame_current}, {"start", frame_start}, {"end", frame_end}}}}; {"frame", {{"current", frame_current}, {"start", frame_start}, {"end", frame_end}}}};
snapshot["windowManager"] = {{"id", "window-manager:WinMan"}, {"name", "WinMan"}, {"presetName", "New Preset"}, {"windowCount", 1}, {"interfaceLocked", false}};
if (!masks_blocked) { if (!masks_blocked) {
snapshot["trackingMasks"] = {{"schemaVersion", 1}, snapshot["trackingMasks"] = {{"schemaVersion", 1},
{"revision", revision}, {"revision", revision},

View File

@@ -80,6 +80,7 @@ using json = nlohmann::json;
using namespace blender; using namespace blender;
std::once_flag blender_runtime_once; std::once_flag blender_runtime_once;
constexpr int64_t geometry_node_max_json_scalar_values = 65'536;
void initialize_blender_runtime() void initialize_blender_runtime()
{ {
@@ -341,6 +342,11 @@ json evaluated_modifier_report(const Object &object, const Object *object_eval)
entry["suggestion"] = entry["suggestion"] =
"Bake a deterministic cache in desktop Blender or disable the simulation zone"; "Bake a deterministic cache in desktop Blender or disable the simulation zone";
} }
else if (error.starts_with("WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED:")) {
entry["errorCode"] = "GEOMETRY_NODES_EVALUATOR_UNSUPPORTED";
entry["suggestion"] =
"Use only the versioned bounded Geometry Nodes evaluator closure";
}
else if (error.starts_with("WEB_DISPLACE_CONFIGURATION_UNAVAILABLE:")) { else if (error.starts_with("WEB_DISPLACE_CONFIGURATION_UNAVAILABLE:")) {
entry["errorCode"] = "MODIFIER_CONFIGURATION_UNSUPPORTED"; entry["errorCode"] = "MODIFIER_CONFIGURATION_UNSUPPORTED";
entry["suggestion"] = entry["suggestion"] =
@@ -399,7 +405,15 @@ json evaluated_mesh_report(const Depsgraph *graph, Object *object)
{"modifiers", evaluated_modifier_report(*object, object_eval)}, {"modifiers", evaluated_modifier_report(*object, object_eval)},
{"worldMatrix", json::array()}, {"worldMatrix", json::array()},
{"positions", json::array()}, {"positions", json::array()},
{"indices", json::array()}}; {"indices", json::array()},
{"domainCardinality",
{{"POINT", mesh->verts_num},
{"EDGE", mesh->edges_num},
{"FACE", mesh->faces_num},
{"CORNER", mesh->corners_num},
{"CURVE", 0},
{"INSTANCE", 0},
{"LAYER", 0}}}};
for (int row = 0; row < 4; row++) { for (int row = 0; row < 4; row++) {
for (int column = 0; column < 4; column++) { for (int column = 0; column < 4; column++) {
/* The Web scene IR uses the same column-major flattening as Blender's Python API and glTF. */ /* The Web scene IR uses the same column-major flattening as Blender's Python API and glTF. */
@@ -411,6 +425,32 @@ json evaluated_mesh_report(const Depsgraph *graph, Object *object)
report["positions"].push_back(position.y); report["positions"].push_back(position.y);
report["positions"].push_back(position.z); report["positions"].push_back(position.z);
} }
const bke::AttributeReader<float> m10_value = mesh->attributes().lookup<float>(
"m10_value", bke::AttrDomain::Point);
if (m10_value) {
json materialization = {{"schemaVersion", 1},
{"fieldId", "attribute:m10_value"},
{"domain", "POINT"},
{"dataType", "FLOAT"},
{"elementCount", positions.size()},
{"scalarValueCount", positions.size()},
{"materializedByteLength", positions.size() * sizeof(float)}};
if (positions.size() <= geometry_node_max_json_scalar_values) {
materialization["transport"] = "JSON";
report["attributes"] = {{"m10_value",
{{"domain", "POINT"},
{"dataType", "FLOAT"},
{"values", json::array()}}}};
for (const int index : positions.index_range()) {
report["attributes"]["m10_value"]["values"].push_back(m10_value.varray[index]);
}
}
else {
materialization["transport"] = "BINARY_REQUIRED";
materialization["errorCode"] = "GN_FIELD_JSON_BUDGET_EXCEEDED";
}
report["fieldMaterializations"] = json::array({std::move(materialization)});
}
for (const int3 &triangle : corner_tris) { for (const int3 &triangle : corner_tris) {
for (int index = 0; index < 3; index++) { for (int index = 0; index < 3; index++) {
report["indices"].push_back(corner_verts[triangle[index]]); report["indices"].push_back(corner_verts[triangle[index]]);

View File

@@ -72,6 +72,13 @@ static void register_web_geometry_node_types()
} }
namespace seq {
/* The browser engine does not load desktop sequencer playback state. */
void doversion_250_sound_proxy_update(Main * /*bmain*/, Editing * /*editing*/) {}
} // namespace seq
void WEB_headless_node_system_init() void WEB_headless_node_system_init()
{ {
static bool initialized = false; static bool initialized = false;
@@ -411,4 +418,5 @@ const ComputeContext *compute_context_for_viewer_path_elem(
namespace blender { namespace blender {
void WM_main_add_notifier(unsigned int /*type*/, void * /*reference*/) {} void WM_main_add_notifier(unsigned int /*type*/, void * /*reference*/) {}
void WM_msg_publish_rna(wmMsgBus * /*mbus*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/) {}
} // namespace blender } // namespace blender

View File

@@ -8,6 +8,8 @@
#include <cstring> #include <cstring>
#include <limits> #include <limits>
#include <new> #include <new>
#include <string_view>
#include <tuple>
#include <unordered_map> #include <unordered_map>
#include <unordered_set> #include <unordered_set>
@@ -18,6 +20,7 @@
#include "BKE_anim_data.hh" #include "BKE_anim_data.hh"
#include "BKE_attribute.hh" #include "BKE_attribute.hh"
#include "BKE_attribute.h" #include "BKE_attribute.h"
#include "BKE_blendfile_link_append.hh"
#include "BKE_layer.hh" #include "BKE_layer.hh"
#include "BKE_collection.hh" #include "BKE_collection.hh"
#include "BKE_curve.hh" #include "BKE_curve.hh"
@@ -36,7 +39,9 @@
#include "BKE_nla.hh" #include "BKE_nla.hh"
#include "BKE_object.hh" #include "BKE_object.hh"
#include "BKE_object_deform.h" #include "BKE_object_deform.h"
#include "BKE_packedFile.hh"
#include "BKE_scene.hh" #include "BKE_scene.hh"
#include "BKE_vfont.hh"
#include "BKE_volume.hh" #include "BKE_volume.hh"
#include "BKE_fcurve.hh" #include "BKE_fcurve.hh"
#include "BKE_image.hh" #include "BKE_image.hh"
@@ -615,6 +620,217 @@ bool append_write_bytes(const void *data, const size_t size, void *user_data)
} }
} }
const char *geometry_socket_data_type(const eNodeSocketDatatype type)
{
switch (type) {
case SOCK_FLOAT: return "FLOAT";
case SOCK_VECTOR: return "VECTOR";
case SOCK_RGBA: return "COLOR";
case SOCK_SHADER: return "SHADER";
case SOCK_BOOLEAN: return "BOOLEAN";
case SOCK_INT: return "INT";
case SOCK_STRING: return "STRING";
case SOCK_OBJECT: return "OBJECT";
case SOCK_IMAGE: return "IMAGE";
case SOCK_GEOMETRY: return "GEOMETRY";
case SOCK_COLLECTION: return "COLLECTION";
case SOCK_TEXTURE: return "TEXTURE";
case SOCK_MATERIAL: return "MATERIAL";
case SOCK_ROTATION: return "ROTATION";
case SOCK_MENU: return "MENU";
case SOCK_MATRIX: return "MATRIX";
case SOCK_BUNDLE: return "BUNDLE";
case SOCK_CLOSURE: return "CLOSURE";
case SOCK_FONT: return "FONT";
case SOCK_SCENE: return "SCENE";
case SOCK_TEXT_ID: return "TEXT";
case SOCK_MASK: return "MASK";
case SOCK_SOUND: return "SOUND";
case SOCK_INT_VECTOR: return "INT_VECTOR";
case SOCK_CUSTOM: return "CUSTOM";
}
return "CUSTOM";
}
eNodeSocketDatatype geometry_interface_socket_type(const char *socket_type)
{
if (socket_type == nullptr) return SOCK_CUSTOM;
const std::string type(socket_type);
if (type.starts_with("NodeSocketIntVector")) return SOCK_INT_VECTOR;
if (type.starts_with("NodeSocketFloat")) return SOCK_FLOAT;
if (type.starts_with("NodeSocketVector")) return SOCK_VECTOR;
if (type == "NodeSocketColor") return SOCK_RGBA;
if (type == "NodeSocketShader") return SOCK_SHADER;
if (type == "NodeSocketBool") return SOCK_BOOLEAN;
if (type.starts_with("NodeSocketInt")) return SOCK_INT;
if (type.starts_with("NodeSocketString")) return SOCK_STRING;
if (type == "NodeSocketObject") return SOCK_OBJECT;
if (type == "NodeSocketImage") return SOCK_IMAGE;
if (type == "NodeSocketGeometry") return SOCK_GEOMETRY;
if (type == "NodeSocketCollection") return SOCK_COLLECTION;
if (type == "NodeSocketTexture") return SOCK_TEXTURE;
if (type == "NodeSocketMaterial") return SOCK_MATERIAL;
if (type == "NodeSocketRotation") return SOCK_ROTATION;
if (type == "NodeSocketMenu") return SOCK_MENU;
if (type == "NodeSocketMatrix") return SOCK_MATRIX;
if (type == "NodeSocketBundle") return SOCK_BUNDLE;
if (type == "NodeSocketClosure") return SOCK_CLOSURE;
if (type == "NodeSocketFont") return SOCK_FONT;
if (type == "NodeSocketScene") return SOCK_SCENE;
if (type == "NodeSocketText") return SOCK_TEXT_ID;
if (type == "NodeSocketMask") return SOCK_MASK;
if (type == "NodeSocketSound") return SOCK_SOUND;
return SOCK_CUSTOM;
}
std::string geometry_id_reference(const ID *id)
{
if (id == nullptr) return {};
const std::string code(id->name, 2);
const char *prefix = code == "OB" ? "object:" : code == "IM" ? "image:" :
code == "GR" ? "collection:" : code == "TE" ? "texture:" :
code == "MA" ? "material:" : code == "VF" ? "vfont:" :
code == "SC" ? "scene:" : code == "TX" ? "text:" :
code == "MS" ? "mask:" : code == "SO" ? "sound:" :
code == "NT" ? "node-group:" : "id:";
return std::string(prefix) + id_name(*id);
}
std::optional<json> geometry_socket_default(const eNodeSocketDatatype type,
const void *default_value)
{
if (default_value == nullptr) return std::nullopt;
switch (type) {
case SOCK_FLOAT: {
const float value = static_cast<const bNodeSocketValueFloat *>(default_value)->value;
return std::isfinite(value) ? std::optional<json>(value) : std::nullopt;
}
case SOCK_INT:
return static_cast<const bNodeSocketValueInt *>(default_value)->value;
case SOCK_BOOLEAN:
return static_cast<const bNodeSocketValueBoolean *>(default_value)->value != 0;
case SOCK_VECTOR: {
const bNodeSocketValueVector &value = *static_cast<const bNodeSocketValueVector *>(default_value);
const int dimensions = std::clamp(value.dimensions, 2, 4);
json result = json::array();
for (int index = 0; index < dimensions; index++) {
if (!std::isfinite(value.value[index])) return std::nullopt;
result.push_back(value.value[index]);
}
return result;
}
case SOCK_INT_VECTOR: {
const bNodeSocketValueIntVector &value = *static_cast<const bNodeSocketValueIntVector *>(default_value);
const int dimensions = std::clamp(value.dimensions, 2, 3);
json result = json::array();
for (int index = 0; index < dimensions; index++) result.push_back(value.value[index]);
return result;
}
case SOCK_ROTATION: {
const bNodeSocketValueRotation &value = *static_cast<const bNodeSocketValueRotation *>(default_value);
if (!std::all_of(std::begin(value.value_euler), std::end(value.value_euler),
[](const float component) { return std::isfinite(component); }))
{
return std::nullopt;
}
return json::array({value.value_euler[0], value.value_euler[1], value.value_euler[2]});
}
case SOCK_RGBA: {
const bNodeSocketValueRGBA &value = *static_cast<const bNodeSocketValueRGBA *>(default_value);
if (!std::all_of(std::begin(value.value), std::end(value.value),
[](const float component) { return std::isfinite(component); }))
{
return std::nullopt;
}
return json::array({value.value[0], value.value[1], value.value[2], value.value[3]});
}
case SOCK_STRING: {
const bNodeSocketValueString &value = *static_cast<const bNodeSocketValueString *>(default_value);
return std::string(value.value, strnlen(value.value, sizeof(value.value)));
}
case SOCK_MENU:
return static_cast<const bNodeSocketValueMenu *>(default_value)->value;
case SOCK_OBJECT:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueObject *>(default_value)->value));
case SOCK_IMAGE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueImage *>(default_value)->value));
case SOCK_COLLECTION:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueCollection *>(default_value)->value));
case SOCK_TEXTURE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueTexture *>(default_value)->value));
case SOCK_MATERIAL:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueMaterial *>(default_value)->value));
case SOCK_FONT:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueFont *>(default_value)->value));
case SOCK_SCENE:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueScene *>(default_value)->value));
case SOCK_TEXT_ID:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueText *>(default_value)->value));
case SOCK_MASK:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueMask *>(default_value)->value));
case SOCK_SOUND:
return geometry_id_reference(reinterpret_cast<const ID *>(
static_cast<const bNodeSocketValueSound *>(default_value)->value));
case SOCK_CUSTOM:
case SOCK_SHADER:
case SOCK_GEOMETRY:
case SOCK_MATRIX:
case SOCK_BUNDLE:
case SOCK_CLOSURE:
return std::nullopt;
}
return std::nullopt;
}
std::string geometry_node_id(const bNode &node)
{
return "geometry-node:" + std::to_string(node.identifier);
}
std::string geometry_node_type(const bNode &node)
{
std::string stored(node.idname);
constexpr std::string_view undefined_prefix = "Undefined[";
while (stored.starts_with(undefined_prefix) && stored.ends_with("]") &&
stored.size() > undefined_prefix.size() + 1)
{
stored = stored.substr(undefined_prefix.size(), stored.size() - undefined_prefix.size() - 1);
}
return stored;
}
std::string geometry_socket_id(const bNodeSocket &socket, const eNodeSocketInOut direction)
{
return std::string("geometry-socket:") + (direction == SOCK_IN ? "input:" : "output:") +
socket.identifier;
}
json geometry_socket_json(const bNodeSocket &socket, const eNodeSocketInOut direction)
{
json result = {{"id", geometry_socket_id(socket, direction)},
{"name", socket.name},
{"direction", direction == SOCK_IN ? "INPUT" : "OUTPUT"},
{"dataType", geometry_socket_data_type(socket.type)}};
if (const std::optional<json> value = geometry_socket_default(socket.type, socket.default_value)) {
if (!(value->is_string() && value->get_ref<const std::string &>().empty() &&
ELEM(socket.type, SOCK_OBJECT, SOCK_IMAGE, SOCK_COLLECTION, SOCK_TEXTURE, SOCK_MATERIAL,
SOCK_FONT, SOCK_SCENE, SOCK_TEXT_ID, SOCK_MASK, SOCK_SOUND)))
{
result["defaultValue"] = *value;
}
}
return result;
}
} // namespace } // namespace
WebBlendMainState *web_engine_blend_main_open(const uint8_t *data, WebBlendMainState *web_engine_blend_main_open(const uint8_t *data,
@@ -905,6 +1121,117 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state,
return true; return true;
} }
bool web_engine_blend_main_append_object(WebBlendMainState *state,
const uint8_t *source_data,
const uint32_t source_length,
const char *source_locator,
const char *object_name,
const char *mesh_name,
const char *material_name,
const char *image_name,
std::string &object_id,
std::string &error)
{
if (state == nullptr || state->main == nullptr || state->scene == nullptr ||
state->view_layer == nullptr || source_data == nullptr || source_length == 0 ||
source_length > 64u * 1024u * 1024u || source_locator == nullptr ||
source_locator[0] == '\0' || object_name == nullptr || mesh_name == nullptr ||
material_name == nullptr || image_name == nullptr || object_name[0] == '\0' ||
mesh_name[0] == '\0' || material_name[0] == '\0' || image_name[0] == '\0')
{
error = "ASSET_MANIFEST_INVALID: append requires a bounded source and complete Object closure";
return false;
}
if (find_object(state->main, ("object:" + std::string(object_name)).c_str()) != nullptr ||
find_material(state->main, ("material:" + std::string(material_name)).c_str()) != nullptr ||
find_image(state->main, ("image:" + std::string(image_name)).c_str()) != nullptr)
{
error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local ID";
return false;
}
for (const Mesh &mesh : state->main->meshes) {
if (id_name(mesh.id) == mesh_name) {
error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local Mesh";
return false;
}
}
BlendHandle *blend_handle = BLO_blendhandle_from_memory(
source_data, int(source_length), nullptr);
if (blend_handle == nullptr) {
error = "BLEND_READ_FAILED: Blender rejected the append source buffer";
return false;
}
GlobalMainScope global_main_scope(state->main);
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, true);
LibraryLink_Params parameters{};
BLO_library_link_params_init_with_context(&parameters,
state->main,
BLO_LIBLINK_APPEND_RECURSIVE,
0,
state->scene,
state->view_layer,
nullptr);
BlendfileLinkAppendContext *context = BKE_blendfile_link_append_context_new(&parameters);
if (context == nullptr) {
BLO_blendhandle_close(blend_handle);
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false);
error = "BLEND_READ_FAILED: Blender could not create the append context";
return false;
}
BKE_blendfile_link_append_context_library_add(context, source_locator, blend_handle);
BlendfileLinkAppendContextItem *item = BKE_blendfile_link_append_context_item_add(
context, object_name, ID_OB, nullptr);
BKE_blendfile_link_append_context_item_library_index_enable(context, item, 0);
BKE_blendfile_link_append_context_init_done(context);
BKE_blendfile_link(context, nullptr);
BKE_blendfile_append(context, nullptr);
BKE_blendfile_link_append_instantiate_loose(context, nullptr);
BKE_blendfile_link_append_context_finalize(context);
ID *new_id = BKE_blendfile_link_append_context_item_newid_get(context, item);
BKE_blendfile_link_append_context_free(context);
BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false);
Object *object = new_id != nullptr && GS(new_id->name) == ID_OB ?
reinterpret_cast<Object *>(new_id) :
nullptr;
Mesh *mesh = object != nullptr && object->type == OB_MESH && object->data != nullptr ?
reinterpret_cast<Mesh *>(object->data) :
nullptr;
Material *material = mesh != nullptr && mesh->totcol == 1 && mesh->mat != nullptr ?
mesh->mat[0] :
nullptr;
Image *image = find_image(state->main, ("image:" + std::string(image_name)).c_str());
bool material_uses_image = false;
if (material != nullptr && material->nodetree != nullptr && image != nullptr) {
for (const bNode &node : material->nodetree->nodes) {
if (node.id == &image->id) {
material_uses_image = true;
break;
}
}
}
if (object == nullptr || mesh == nullptr || material == nullptr || image == nullptr ||
id_name(object->id) != object_name || id_name(mesh->id) != mesh_name ||
id_name(material->id) != material_name || id_name(image->id) != image_name ||
object->id.lib != nullptr || mesh->id.lib != nullptr || material->id.lib != nullptr ||
image->id.lib != nullptr || object->id.override_library != nullptr ||
mesh->id.override_library != nullptr || material->id.override_library != nullptr ||
image->id.override_library != nullptr || !material_uses_image)
{
error = "ASSET_SOURCE_HASH_MISMATCH: appended Object closure is incomplete or not fully local";
return false;
}
BKE_view_layer_synced_ensure(*state->main, state->scene, state->view_layer);
object->id.recalc |= ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY;
mesh->id.recalc |= ID_RECALC_GEOMETRY;
material->id.recalc |= ID_RECALC_SYNC_TO_EVAL;
image->id.recalc |= ID_RECALC_SOURCE;
object_id = "object:" + id_name(object->id);
return true;
}
bool web_engine_blend_main_create_curve(WebBlendMainState *state, bool web_engine_blend_main_create_curve(WebBlendMainState *state,
const char *curve_type, const char *curve_type,
const char *name, const char *name,
@@ -3451,6 +3778,58 @@ bool web_engine_blend_main_set_font_advanced(
return true; return true;
} }
bool web_engine_blend_main_import_vfont(WebBlendMainState *state,
const char *name,
const char *source_path,
const char *base64,
std::string &font_id,
std::string &error)
{
if (state == nullptr || state->main == nullptr || name == nullptr || source_path == nullptr ||
base64 == nullptr || name[0] == '\0' || strlen(name) > 63 || strlen(source_path) >= FILE_MAX ||
strncmp(source_path, "//fonts/", 8) != 0 || strstr(source_path, "\\") != nullptr ||
strstr(source_path, "/../") != nullptr || strstr(source_path, "/./") != nullptr)
{
error = "NON_MESH_RESOURCE_OUTSIDE_PROJECT: VFont source must be a bounded //fonts project path";
return false;
}
for (const VFont &existing : state->main->fonts) {
if (STREQ(existing.filepath, source_path)) {
error = "NON_MESH_PROPERTY_INVALID: A VFont with the same project source path already exists";
return false;
}
}
const std::vector<uint8_t> decoded = decode_base64(base64);
if (decoded.empty() || decoded.size() > 32 * 1024 * 1024) {
error = "NON_MESH_DATA_BUDGET_EXCEEDED: VFont payload is empty or exceeds 32 MiB";
return false;
}
uint8_t *owned = MEM_new_array_uninitialized<uint8_t>(decoded.size(), "WebEngine packed VFont");
if (owned == nullptr) {
error = "NON_MESH_DATA_BUDGET_EXCEEDED: VFont packed allocation failed";
return false;
}
memcpy(owned, decoded.data(), decoded.size());
PackedFile *packed = BKE_packedfile_new_from_memory(owned, int(decoded.size()));
VFont *vfont = static_cast<VFont *>(BKE_libblock_alloc(state->main, ID_VF, name, 0));
if (packed == nullptr || vfont == nullptr) {
if (packed != nullptr) BKE_packedfile_free(packed);
else MEM_delete(owned);
error = "NON_MESH_BINARY_INVALID: Blender could not allocate the packed VFont";
return false;
}
STRNCPY(vfont->filepath, source_path);
vfont->packedfile = packed;
BKE_vfont_data_ensure(vfont);
if (vfont->data == nullptr) {
BKE_id_delete(state->main, vfont);
error = "NON_MESH_BINARY_INVALID: Blender rejected the external VFont bytes";
return false;
}
font_id = "vfont:" + id_name(vfont->id);
return true;
}
bool web_engine_blend_main_set_font_links(WebBlendMainState *state, bool web_engine_blend_main_set_font_links(WebBlendMainState *state,
const char *data_id, const char *data_id,
const std::array<std::string, 4> &font_ids, const std::array<std::string, 4> &font_ids,
@@ -3630,6 +4009,19 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
{{"name", "vertex_color"}, {"domain", "POINT"}, {"dataType", "FLOAT_COLOR"}, {"components", 4}}, {{"name", "vertex_color"}, {"domain", "POINT"}, {"dataType", "FLOAT_COLOR"}, {"components", 4}},
{{"name", "cyclic"}, {"domain", "STROKE"}, {"dataType", "BOOL"}, {"components", 1}}, {{"name", "cyclic"}, {"domain", "STROKE"}, {"dataType", "BOOL"}, {"components", 1}},
{{"name", "material_index"}, {"domain", "STROKE"}, {"dataType", "INT"}, {"components", 1}}})}}; {{"name", "material_index"}, {"domain", "STROKE"}, {"dataType", "INT"}, {"components", 1}}})}};
const bke::greasepencil::Layer *active_layer = grease_pencil.get_active_layer();
if (active_layer == nullptr) {
for (const bke::greasepencil::Layer *layer : layers) {
if (layer != nullptr) {
active_layer = layer;
break;
}
}
}
if (active_layer != nullptr) {
data["activeLayerId"] = "grease-pencil-layer:" + id_name(grease_pencil.id) + ":" +
std::string(active_layer->name());
}
if (budget_blocked) { if (budget_blocked) {
data["errorCode"] = "GREASE_PENCIL_BUDGET_EXCEEDED"; data["errorCode"] = "GREASE_PENCIL_BUDGET_EXCEEDED";
grease_pencils.push_back(std::move(data)); grease_pencils.push_back(std::move(data));
@@ -3644,6 +4036,9 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
if (frame.is_end()) continue; if (frame.is_end()) continue;
json strokes = json::array(); json strokes = json::array();
uint64_t drawing_point_count = 0; uint64_t drawing_point_count = 0;
const std::string drawing_suffix = id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index);
const std::string drawing_id = "grease-pencil-drawing:" + drawing_suffix;
const bke::greasepencil::Drawing *drawing = grease_pencil.get_drawing_at(*layer, const bke::greasepencil::Drawing *drawing = grease_pencil.get_drawing_at(*layer,
frame_number); frame_number);
if (drawing != nullptr) { if (drawing != nullptr) {
@@ -3665,13 +4060,15 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
for (const int point_index : point_range) { for (const int point_index : point_range) {
const float3 &position = positions[point_index]; const float3 &position = positions[point_index];
const ColorGeometry4f color = vertex_colors[point_index]; const ColorGeometry4f color = vertex_colors[point_index];
points.push_back({{"position", json::array({position.x, position.y, position.z})}, points.push_back({{"id", "grease-pencil-point:" + drawing_suffix + ":" +
std::to_string(curve_index) + ":" +
std::to_string(point_index - point_range.start())},
{"position", json::array({position.x, position.y, position.z})},
{"radius", radii[point_index]}, {"radius", radii[point_index]},
{"opacity", opacities[point_index]}, {"opacity", opacities[point_index]},
{"vertexColor", json::array({color.r, color.g, color.b, color.a})}}); {"vertexColor", json::array({color.r, color.g, color.b, color.a})}});
} }
strokes.push_back({{"id", "grease-pencil-stroke:" + id_name(grease_pencil.id) + ":" + strokes.push_back({{"id", "grease-pencil-stroke:" + drawing_suffix + ":" +
std::to_string(frame.drawing_index) + ":" +
std::to_string(curve_index)}, std::to_string(curve_index)},
{"cyclic", cyclic[curve_index]}, {"cyclic", cyclic[curve_index]},
{"pointCount", point_range.size()}, {"pointCount", point_range.size()},
@@ -3682,8 +4079,7 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
json frame_entry = { json frame_entry = {
{"frame", frame_number}, {"frame", frame_number},
{"drawing", {"drawing",
{{"id", "grease-pencil-drawing:" + id_name(grease_pencil.id) + ":" + {{"id", drawing_id},
std::to_string(frame.drawing_index)},
{"strokeCount", strokes.size()}, {"strokeCount", strokes.size()},
{"pointCount", drawing_point_count}, {"pointCount", drawing_point_count},
{"strokes", std::move(strokes)}}}}; {"strokes", std::move(strokes)}}}};
@@ -3707,6 +4103,183 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
return true; return true;
} }
bool web_engine_blend_main_geometry_node_graphs_json(WebBlendMainState *state,
std::string &geometry_node_graphs_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr) {
error = "GN_INVALID_GRAPH: authoritative Main is not open";
return false;
}
constexpr size_t max_graphs = 4096;
constexpr size_t max_nodes_per_graph = 4096;
constexpr size_t max_links_per_graph = 16384;
constexpr size_t max_sockets_per_graph = 65536;
constexpr size_t max_interface_sockets_per_graph = 4096;
json graphs = json::array();
try {
for (const bNodeTree &tree : state->main->nodetrees) {
if (strcmp(tree.idname, "GeometryNodeTree") != 0) continue;
if (graphs.size() >= max_graphs) {
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node graph count exceeds 4096";
return false;
}
const size_t node_count = size_t(BLI_listbase_count(&tree.nodes));
const size_t link_count = size_t(BLI_listbase_count(&tree.links));
if (node_count > max_nodes_per_graph || link_count > max_links_per_graph ||
tree.interface_inputs().size() > max_interface_sockets_per_graph ||
tree.interface_outputs().size() > max_interface_sockets_per_graph)
{
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node graph topology exceeds its bounded reader limits";
return false;
}
const std::string graph_id = "node-group:" + id_name(tree.id);
json interface_inputs = json::array();
json interface_outputs = json::array();
auto append_interface_socket = [&](const bNodeTreeInterfaceSocket &socket,
const char *direction,
json &destination) -> bool {
if (socket.identifier == nullptr || socket.identifier[0] == '\0' ||
socket.name == nullptr || socket.socket_type == nullptr)
{
error = "GN_INVALID_GRAPH: Geometry Node interface socket has no stable identifier, name, or type";
return false;
}
const eNodeSocketDatatype type = geometry_interface_socket_type(socket.socket_type);
json socket_json = {
{"id", std::string("geometry-interface:") +
(strcmp(direction, "INPUT") == 0 ? "input:" : "output:") +
socket.identifier},
{"name", socket.name},
{"direction", direction},
{"dataType", geometry_socket_data_type(type)}};
if (const std::optional<json> value = geometry_socket_default(type, socket.socket_data)) {
if (!(value->is_string() && value->get_ref<const std::string &>().empty())) {
socket_json["defaultValue"] = *value;
}
}
destination.push_back(std::move(socket_json));
return true;
};
for (const bNodeTreeInterfaceSocket *socket : tree.interface_inputs()) {
if (socket != nullptr && !append_interface_socket(*socket, "INPUT", interface_inputs)) {
return false;
}
}
for (const bNodeTreeInterfaceSocket *socket : tree.interface_outputs()) {
if (socket != nullptr && !append_interface_socket(*socket, "OUTPUT", interface_outputs)) {
return false;
}
}
std::vector<const bNode *> source_nodes;
source_nodes.reserve(node_count);
for (const bNode &node : tree.nodes) source_nodes.push_back(&node);
std::sort(source_nodes.begin(), source_nodes.end(), [](const bNode *a, const bNode *b) {
return a->identifier < b->identifier;
});
json nodes = json::array();
std::unordered_set<int32_t> node_identifiers;
std::unordered_set<std::string> group_references;
size_t socket_count = 0;
for (const bNode *node : source_nodes) {
if (node == nullptr || node->identifier <= 0 ||
!node_identifiers.insert(node->identifier).second || node->idname[0] == '\0')
{
error = "GN_INVALID_GRAPH: Geometry Node has a missing or duplicate stable identifier";
return false;
}
json sockets = json::array();
std::unordered_set<std::string> socket_ids;
auto append_node_sockets = [&](const ListBaseT<bNodeSocket> &source,
const eNodeSocketInOut direction) -> bool {
for (const bNodeSocket &socket : source) {
if (socket.identifier[0] == '\0' || strcmp(socket.identifier, "__extend__") == 0) {
continue;
}
const std::string socket_id = geometry_socket_id(socket, direction);
if (!socket_ids.insert(socket_id).second) {
error = "GN_INVALID_GRAPH: Geometry Node contains duplicate stable socket identifiers";
return false;
}
socket_count++;
if (socket_count > max_sockets_per_graph) {
error = "GN_GRAPH_BUDGET_EXCEEDED: Geometry Node socket count exceeds 65536";
return false;
}
sockets.push_back(geometry_socket_json(socket, direction));
}
return true;
};
if (!append_node_sockets(node->inputs, SOCK_IN) ||
!append_node_sockets(node->outputs, SOCK_OUT))
{
return false;
}
const std::string node_type = geometry_node_type(*node);
json node_json = {{"id", geometry_node_id(*node)},
{"type", node_type},
{"name", node->name[0] != '\0' ? node->name : node_type},
{"sockets", std::move(sockets)}};
if (node->id != nullptr && std::string(node->id->name, 2) == "NT") {
const std::string reference = geometry_id_reference(node->id);
node_json["groupTreeId"] = reference;
group_references.insert(reference);
}
nodes.push_back(std::move(node_json));
}
std::vector<json> source_links;
source_links.reserve(link_count);
for (const bNodeLink &link : tree.links) {
if (link.fromnode == nullptr || link.tonode == nullptr || link.fromsock == nullptr ||
link.tosock == nullptr || strcmp(link.fromsock->identifier, "__extend__") == 0 ||
strcmp(link.tosock->identifier, "__extend__") == 0)
{
continue;
}
if (!node_identifiers.contains(link.fromnode->identifier) ||
!node_identifiers.contains(link.tonode->identifier))
{
error = "GN_INVALID_GRAPH: Geometry Node link references a node outside its graph";
return false;
}
source_links.push_back({{"fromNodeId", geometry_node_id(*link.fromnode)},
{"fromSocketId", geometry_socket_id(*link.fromsock, SOCK_OUT)},
{"toNodeId", geometry_node_id(*link.tonode)},
{"toSocketId", geometry_socket_id(*link.tosock, SOCK_IN)}});
}
std::sort(source_links.begin(), source_links.end(), [](const json &a, const json &b) {
return std::tie(a.at("fromNodeId"), a.at("fromSocketId"), a.at("toNodeId"),
a.at("toSocketId")) <
std::tie(b.at("fromNodeId"), b.at("fromSocketId"), b.at("toNodeId"),
b.at("toSocketId"));
});
json links = json::array();
for (json &link : source_links) links.push_back(std::move(link));
std::vector<std::string> references(group_references.begin(), group_references.end());
std::sort(references.begin(), references.end());
json graph = {{"schemaVersion", 1},
{"id", graph_id},
{"name", id_name(tree.id)},
{"interfaceInputs", std::move(interface_inputs)},
{"interfaceOutputs", std::move(interface_outputs)},
{"nodes", std::move(nodes)},
{"links", std::move(links)},
{"groupReferences", std::move(references)}};
graph["graphHash"] = web_engine_sha256_hex(graph.dump());
graphs.push_back(std::move(graph));
}
geometry_node_graphs_json = graphs.dump();
return true;
}
catch (const std::exception &exception) {
error = std::string("GN_INVALID_GRAPH: Geometry Node Main reader failed: ") + exception.what();
return false;
}
}
bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state, bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
std::string &physics_json, std::string &physics_json,
std::string &error) std::string &error)
@@ -3715,6 +4288,16 @@ bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
error = "PHYSICS_MANIFEST_INVALID: authoritative Main is not open"; error = "PHYSICS_MANIFEST_INVALID: authoritative Main is not open";
return false; return false;
} }
#ifdef WITH_WEB
/* Legacy particle systems are metadata-only in the Web runtime. Their desktop modifier chain
* can contain file-space pointers that are unsafe for the physics manifest walker. */
for (const Object &object : state->main->objects) {
if (object.particlesystem.first != nullptr) {
physics_json = json({{"schemaVersion", 1}, {"systems", json::array()}}).dump();
return true;
}
}
#endif
constexpr size_t max_systems = 4096; constexpr size_t max_systems = 4096;
auto canonical_number = [](const float value) -> json { auto canonical_number = [](const float value) -> json {
return std::isfinite(value) && std::trunc(value) == value ? json(int64_t(value)) : json(value); return std::isfinite(value) && std::trunc(value) == value ? json(int64_t(value)) : json(value);
@@ -3823,6 +4406,41 @@ bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
return true; return true;
} }
bool web_engine_blend_main_particle_settings_json(WebBlendMainState *state,
std::string &particle_settings_json,
std::string &error)
{
if (state == nullptr || state->main == nullptr) {
error = "PARTICLE_SETTINGS_MANIFEST_INVALID: authoritative Main is not open";
return false;
}
constexpr size_t max_settings = 4096;
auto canonical_number = [](const float value) -> json {
return std::isfinite(value) && std::trunc(value) == value ? json(int64_t(value)) : json(value);
};
json settings = json::array();
for (const ParticleSettings &part : state->main->particles) {
if (settings.size() >= max_settings) {
error = "PARTICLE_SETTINGS_BUDGET_EXCEEDED: ParticleSettings count exceeds the 4096 data-block budget";
return false;
}
settings.push_back({{"id", "particle-settings:" + id_name(part.id)},
{"name", id_name(part.id)},
{"type", int(part.type)},
{"from", int(part.from)},
{"distribution", int(part.distr)},
{"physicsType", int(part.phystype)},
{"totalParticles", part.totpart},
{"start", canonical_number(part.sta)},
{"end", canonical_number(part.end)},
{"lifetime", canonical_number(part.lifetime)},
{"size", canonical_number(part.size)},
{"drawSize", canonical_number(part.draw_size)}});
}
particle_settings_json = json({{"schemaVersion", 1}, {"settings", std::move(settings)}}).dump();
return true;
}
bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state, bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state,
const char *data_id, const char *data_id,
const char *name, const char *name,
@@ -3884,6 +4502,50 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
return true; return true;
} }
bool web_engine_blend_main_move_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
const int frame,
const int target_frame,
const char *drawing_id,
std::string &error)
{
GreasePencil *grease_pencil = find_grease_pencil(state != nullptr ? state->main : nullptr,
data_id);
bke::greasepencil::Layer *layer = find_grease_pencil_layer(grease_pencil, layer_id);
if (grease_pencil == nullptr || layer == nullptr || drawing_id == nullptr ||
!ensure_single_user_data(state != nullptr ? state->main : nullptr,
grease_pencil != nullptr ? &grease_pencil->id : nullptr,
error))
{
if (error.empty()) error = "GREASE_PENCIL_SCHEMA_INVALID: Grease Pencil frame target was not found";
return false;
}
if (frame < -1000000 || frame > 1000000 || target_frame < -1000000 ||
target_frame > 1000000 || frame == target_frame || layer->frames().contains(target_frame))
{
error = "GREASE_PENCIL_SCHEMA_INVALID: frame move is outside the bounded range or target frame already exists";
return false;
}
const GreasePencilFrame *source = layer->frames().lookup_ptr(frame);
if (source == nullptr || source->is_end()) {
error = "GREASE_PENCIL_SCHEMA_INVALID: source Grease Pencil frame was not found";
return false;
}
const std::string expected_drawing_id = "grease-pencil-drawing:" +
id_name(grease_pencil->id) + ":" +
std::to_string(source->drawing_index);
if (expected_drawing_id != drawing_id) {
error = "GREASE_PENCIL_SCHEMA_INVALID: source drawing identity does not match the frame";
return false;
}
blender::Map<int, int> destinations;
destinations.add(frame, target_frame);
grease_pencil->move_frames(*layer, destinations);
grease_pencil->id.recalc |= ID_RECALC_GEOMETRY;
return true;
}
bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state, bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state,
const char *data_id, const char *data_id,
const char *layer_id, const char *layer_id,
@@ -4039,7 +4701,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices, const std::vector<uint32_t> &indices,
const std::vector<float> &values, const std::vector<float> &values,
const bool normalize, const bool normalize,
const uint32_t limit,
const bool mirror, const bool mirror,
const int mirror_axis,
const float mirror_tolerance,
std::string &error) std::string &error)
{ {
Object *object = find_object(state != nullptr ? state->main : nullptr, object_id); Object *object = find_object(state != nullptr ? state->main : nullptr, object_id);
@@ -4050,10 +4715,6 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
if (error.empty()) error = "PAINT_SCHEMA_INVALID: Mesh object was not found"; if (error.empty()) error = "PAINT_SCHEMA_INVALID: Mesh object was not found";
return false; return false;
} }
if (mirror) {
error = "CAPABILITY_MISSING: topology mirror requires a verified mesh symmetry map";
return false;
}
if (strlen(vertex_group) == 0 || strlen(vertex_group) > 63 || indices.empty() || if (strlen(vertex_group) == 0 || strlen(vertex_group) > 63 || indices.empty() ||
indices.size() > 1000000 || indices.size() != values.size() || indices.size() > 1000000 || indices.size() != values.size() ||
std::any_of(indices.begin(), indices.end(), [&](const uint32_t index) { return index >= uint32_t(mesh->verts_num); }) || std::any_of(indices.begin(), indices.end(), [&](const uint32_t index) { return index >= uint32_t(mesh->verts_num); }) ||
@@ -4061,6 +4722,88 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
error = "PAINT_SCHEMA_INVALID: vertex weight patch is outside the bounded domain"; error = "PAINT_SCHEMA_INVALID: vertex weight patch is outside the bounded domain";
return false; return false;
} }
if (limit > 32 || (mirror && (mirror_axis < 0 || mirror_axis > 2 || !std::isfinite(mirror_tolerance) ||
mirror_tolerance <= 0.0f || mirror_tolerance > 1.0f))) {
error = "PAINT_SCHEMA_INVALID: vertex weight limit or mirror options are outside the bounded domain";
return false;
}
std::unordered_set<uint32_t> seen_indices;
for (const uint32_t index : indices) {
if (!seen_indices.insert(index).second) {
error = "PAINT_SCHEMA_INVALID: vertex weight patch contains duplicate vertex indices";
return false;
}
}
std::unordered_map<uint32_t, float> updates;
updates.reserve(indices.size() * (mirror ? 2 : 1));
for (size_t index = 0; index < indices.size(); index++) updates.emplace(indices[index], values[index]);
if (mirror) {
/* Build a reciprocal local-coordinate symmetry map before mutating Main. */
const Span<float3> positions = mesh->vert_positions();
if (positions.size() > 100000) {
error = "PAINT_BUDGET_EXCEEDED: verified weight mirror is limited to 100000 vertices";
return false;
}
auto cell_key = [](const float3 &position, const float cell_size, const int dx = 0,
const int dy = 0, const int dz = 0) {
const int64_t x = int64_t(std::llround(position.x / cell_size)) + dx;
const int64_t y = int64_t(std::llround(position.y / cell_size)) + dy;
const int64_t z = int64_t(std::llround(position.z / cell_size)) + dz;
return std::to_string(x) + ":" + std::to_string(y) + ":" + std::to_string(z);
};
std::unordered_map<std::string, std::vector<int32_t>> buckets;
buckets.reserve(positions.size());
for (int64_t index = 0; index < int64_t(positions.size()); index++) {
buckets[cell_key(positions[index], mirror_tolerance)].push_back(int32_t(index));
}
std::vector<int32_t> mirror_vertices(positions.size(), -1);
for (int64_t source = 0; source < int64_t(positions.size()); source++) {
const float3 reflected = [&]() {
float3 value = positions[source];
value[mirror_axis] = -value[mirror_axis];
return value;
}();
float best_distance = std::numeric_limits<float>::max();
int32_t best = -1;
for (int dx = -1; dx <= 1; dx++) {
for (int dy = -1; dy <= 1; dy++) {
for (int dz = -1; dz <= 1; dz++) {
const auto bucket = buckets.find(cell_key(reflected, mirror_tolerance, dx, dy, dz));
if (bucket == buckets.end()) continue;
for (const int32_t candidate : bucket->second) {
const float3 delta = positions[candidate] - reflected;
const float distance = std::sqrt(delta.x * delta.x + delta.y * delta.y + delta.z * delta.z);
if (distance < best_distance || (distance == best_distance && candidate < best)) {
best_distance = distance;
best = candidate;
}
}
}
}
}
if (best < 0 || best_distance > mirror_tolerance) {
error = "CAPABILITY_MISSING: WEIGHT_MIRROR_SYMMETRY_UNVERIFIED";
return false;
}
mirror_vertices[source] = best;
}
for (size_t source = 0; source < mirror_vertices.size(); source++) {
const int32_t target = mirror_vertices[source];
if (target < 0 || mirror_vertices[size_t(target)] != int32_t(source)) {
error = "CAPABILITY_MISSING: WEIGHT_MIRROR_SYMMETRY_UNVERIFIED";
return false;
}
}
for (const auto &[source, value] : std::vector<std::pair<uint32_t, float>>(updates.begin(), updates.end())) {
const uint32_t target = uint32_t(mirror_vertices[source]);
const auto existing = updates.find(target);
if (existing != updates.end() && std::abs(existing->second - value) > 1e-6f) {
error = "PAINT_SCHEMA_INVALID: mirrored weight patch contains conflicting values";
return false;
}
updates[target] = value;
}
}
int group_index = BKE_object_defgroup_name_index(object, vertex_group); int group_index = BKE_object_defgroup_name_index(object, vertex_group);
if (group_index < 0) { if (group_index < 0) {
if (BKE_object_defgroup_add_name(object, vertex_group) == nullptr) { if (BKE_object_defgroup_add_name(object, vertex_group) == nullptr) {
@@ -4070,15 +4813,35 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
group_index = BKE_object_defgroup_name_index(object, vertex_group); group_index = BKE_object_defgroup_name_index(object, vertex_group);
} }
MutableSpan<MDeformVert> deform_verts = mesh->deform_verts_for_write(); MutableSpan<MDeformVert> deform_verts = mesh->deform_verts_for_write();
for (size_t index = 0; index < indices.size(); index++) { std::vector<uint32_t> touched_vertices;
MDeformVert &deform_vert = deform_verts[indices[index]]; touched_vertices.reserve(updates.size());
if (values[index] == 0.0f) { for (const auto &[vertex_index, value] : updates) {
MDeformVert &deform_vert = deform_verts[vertex_index];
if (value == 0.0f) {
if (MDeformWeight *weight = BKE_defvert_find_index(&deform_vert, group_index)) { if (MDeformWeight *weight = BKE_defvert_find_index(&deform_vert, group_index)) {
BKE_defvert_remove_group(&deform_vert, weight); BKE_defvert_remove_group(&deform_vert, weight);
} }
} }
else { else {
BKE_defvert_ensure_index(&deform_vert, group_index)->weight = values[index]; BKE_defvert_ensure_index(&deform_vert, group_index)->weight = value;
}
touched_vertices.push_back(vertex_index);
}
for (const uint32_t vertex_index : touched_vertices) {
MDeformVert &deform_vert = deform_verts[vertex_index];
if (limit > 0) {
while (deform_vert.totweight > int(limit)) {
int remove_index = 0;
for (int index = 1; index < deform_vert.totweight; index++) {
const MDeformWeight &candidate = deform_vert.dw[index];
const MDeformWeight &current = deform_vert.dw[remove_index];
if (candidate.weight < current.weight ||
(candidate.weight == current.weight && candidate.def_nr > current.def_nr)) {
remove_index = index;
}
}
BKE_defvert_remove_group(&deform_vert, &deform_vert.dw[remove_index]);
}
} }
if (normalize) BKE_defvert_normalize(deform_vert); if (normalize) BKE_defvert_normalize(deform_vert);
} }

View File

@@ -128,6 +128,16 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state,
const std::vector<float> &location, const std::vector<float> &location,
std::string &object_id, std::string &object_id,
std::string &error); std::string &error);
bool web_engine_blend_main_append_object(WebBlendMainState *state,
const uint8_t *source_data,
uint32_t source_length,
const char *source_locator,
const char *object_name,
const char *mesh_name,
const char *material_name,
const char *image_name,
std::string &object_id,
std::string &error);
bool web_engine_blend_main_duplicate_object(WebBlendMainState *state, bool web_engine_blend_main_duplicate_object(WebBlendMainState *state,
const char *object_id, const char *object_id,
const std::vector<float> &offset, const std::vector<float> &offset,
@@ -346,6 +356,12 @@ bool web_engine_blend_main_set_font_advanced(WebBlendMainState *state,
const std::vector<WebFontTextBoxEdit> &text_boxes, const std::vector<WebFontTextBoxEdit> &text_boxes,
int active_text_box, int active_text_box,
std::string &error); std::string &error);
bool web_engine_blend_main_import_vfont(WebBlendMainState *state,
const char *name,
const char *source_path,
const char *base64,
std::string &font_id,
std::string &error);
bool web_engine_blend_main_set_font_links(WebBlendMainState *state, bool web_engine_blend_main_set_font_links(WebBlendMainState *state,
const char *data_id, const char *data_id,
const std::array<std::string, 4> &font_ids, const std::array<std::string, 4> &font_ids,
@@ -361,9 +377,15 @@ bool web_engine_blend_main_set_metaball_elements(WebBlendMainState *state,
bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state, bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
std::string &grease_pencils_json, std::string &grease_pencils_json,
std::string &error); std::string &error);
bool web_engine_blend_main_geometry_node_graphs_json(WebBlendMainState *state,
std::string &geometry_node_graphs_json,
std::string &error);
bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state, bool web_engine_blend_main_physics_simulation_json(WebBlendMainState *state,
std::string &physics_json, std::string &physics_json,
std::string &error); std::string &error);
bool web_engine_blend_main_particle_settings_json(WebBlendMainState *state,
std::string &particle_settings_json,
std::string &error);
bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state, bool web_engine_blend_main_create_grease_pencil_layer(WebBlendMainState *state,
const char *data_id, const char *data_id,
const char *name, const char *name,
@@ -377,6 +399,13 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
const char *layer_id, const char *layer_id,
const char *direction, const char *direction,
std::string &error); std::string &error);
bool web_engine_blend_main_move_grease_pencil_frame(WebBlendMainState *state,
const char *data_id,
const char *layer_id,
int frame,
int target_frame,
const char *drawing_id,
std::string &error);
bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state, bool web_engine_blend_main_insert_grease_pencil_frame(WebBlendMainState *state,
const char *data_id, const char *data_id,
const char *layer_id, const char *layer_id,
@@ -407,7 +436,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices, const std::vector<uint32_t> &indices,
const std::vector<float> &values, const std::vector<float> &values,
bool normalize, bool normalize,
uint32_t limit,
bool mirror, bool mirror,
int mirror_axis,
float mirror_tolerance,
std::string &error); std::string &error);
bool web_engine_blend_main_set_render_properties(WebBlendMainState *state, bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
const char *target_id, const char *target_id,

View File

@@ -0,0 +1,556 @@
# Blender 5.2 全功能对标工作分解
更新时间2026-08-18
> 覆盖参考(归档):本文不是任务队列,也不进入默认上下文。只在任务卡明确引用某个 F 域时读取对应小节。
> 本文保留为 Blender 全产品覆盖检查表和 F00-F24 taxonomy 参考。M12-M23 的执行分类、
> 原子任务、证据合同与发布门以 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;
> 本文的 F 任务不能直接作为当前 `nextTask`,也不能独立改变机器状态。
## 1. 目标与事实边界
本文以仓库内 `blender-5.2.0` 的 RNA、operator、data-block、modifier、node、editor、
文件格式和运行时后端为覆盖基线。本文原有 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED`
V1 盘点术语;完整对标结论必须按新计划转换为 `LOCAL_EXACT/LOCAL_EQUIVALENT/SERVER_EXACT`
和独立 `parityStatus`,并保存可复验的正例、阻断例和数据保真证据。
当前 `Web Blender Modeler V1` 已达到其有界发布范围,但不等于完整 Blender。机器账本中
N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此以下任务默认均为未完成;
只有 `docs/status/parity-ledger.json` 和对应专项证据同时更新后才允许改变状态。
短周期唯一领取顺序仍以 `docs/EXECUTION_QUEUE.md` 和对应任务卡为准。本文负责验证新计划没有遗漏
产品域;与新计划冲突时按新计划解释,不取代当前执行队列或长期实施事实源。
## 2. 原子任务完成定义
每个能力切片必须依次通过以下原子门;不能用后一个门反推前一个门完成:
| 门 | 单一产物 | 退出条件 |
| --- | --- | --- |
| G0 Inventory | 一条机器可读 inventory 记录 | Blender 5.2 名称、源码锚点、依赖和分类均存在 |
| G1 Contract | 一个版本化协议 | 字段、上限、稳定 ID、错误码和 revision 规则可纯校验 |
| G2 Reader | 一个 Blender Main reader | 真实 fixture 可读;缺失、损坏和 linked 输入有稳定结果 |
| G3 Evaluator | 一个求值切片 | desktop 与 WASM 的语义或误差 golden 通过 |
| G4 Writer | 一个 Main transaction | 只产生一个 undo step失败不改变 revision |
| G5 Persistence | 一个保存重开闭环 | save、Worker restart、reopen 后语义和稳定 ID 一致 |
| G6 Delta | 一个增量协议 | base/next revision、乱序、重复和 stale 输入均有测试 |
| G7 UI | 一个可操作入口 | context、selection、取消、键盘路径和错误显示完整 |
| G8 Viewport | 一个渲染闭环 | 主线程与 Offscreen 资源生命周期、非空像素和 device loss 通过 |
| G9 Interop | 一个导入/导出结论 | round-trip 或机器可读 loss/block report 通过 |
| G10 Quality | 一个发布证据 | 性能、OOM、quota、恶意输入、取消和泄漏门通过 |
一个原子任务只允许交付一个门。新增白名单项时,必须为该项新建自己的 G2-G10 证据,不能只
扩大数组后复用其他项的成功记录。
## 3. 阶段与最小里程碑
| 阶段 | 最小里程碑 | 前置 | 退出条件 |
| --- | --- | --- | --- |
| S0 可部署运行时 | M6.1-M6.7 | V1 RC | single/pthread、HTTP、归档、CI、运维逐项通过 |
| S1 核心项目体验 | M7 | S0 | 30 分钟 edit/save/reopen soak 和恢复通过 |
| S2 全量清单冻结 | F00 | S1 | Blender 5.2 inventory 生成器输出稳定且零未分类项 |
| S3 数据与场景核心 | F01-F03 | S2 | Main/ID/Scene/Object/Collection/View Layer 闭环 |
| S4 几何创作 | F04-F08 | S3 | Mesh、非 Mesh、Sculpt、Paint、Grease Pencil 分域通过 |
| S5 程序化与材质 | F09-F11 | S3 | Modifier、Geometry Nodes、Shader/Texture 分域通过 |
| S6 动画与绑定 | F12-F13 | S3 | Animation、Rigging、Constraint、Driver、NLA 分域通过 |
| S7 模拟与渲染 | F14-F16 | S5、S6 | Physics、Render、Compositor 的 local/server 边界通过 |
| S8 媒体与跟踪 | F17-F18 | S3 | VSE/Audio、Tracking/Mask 闭环 |
| S9 资产与互操作 | F19-F20 | S3 | Library/Asset 与逐格式 round-trip/loss report 通过 |
| S10 编辑器与平台 | F21-F23 | S4-S9 | editor、keymap、scripting、安全、跨浏览器分类完成 |
| S11 全域发布审计 | F24 | S0-S10 | inventory 零缺口;每项均有正例或稳定阻断证据 |
每个 F 域内部再按 `inventory -> read -> evaluate -> write -> persist -> UI -> quality` 形成独立小
里程碑。跨域依赖只允许引用已经通过的前置门。
## 4. F00 全量清单与状态生成
- [ ] `F00-01` 从 RNA 生成全部 data-block type 清单。
- [ ] `F00-02` 从 operator registry 生成 operator、poll context 和参数清单。
- [ ] `F00-03` 从 modifier enum 生成全部 modifier type 清单。
- [ ] `F00-04` 从 node registries 生成 Geometry、Shader、Compositor node 清单。
- [ ] `F00-05` 从 constraint registry 生成 Object/Bone constraint 清单。
- [ ] `F00-06` 从 editor space types 生成 editor、region 和 mode 清单。
- [ ] `F00-07` 从 import/export registrations 生成格式和扩展清单。
- [ ] `F00-08` 从 render/physics feature flags 生成后端能力清单。
- [ ] `F00-09` 为每条清单记录 Blender 源文件、符号和版本。
- [ ] `F00-10` 为每条清单分配稳定 parity ID重跑不得漂移。
- [ ] `F00-11` 把现有 N-001 至 N-026 证据映射到 parity ID。
- [ ] `F00-12` 拒绝同一 parity ID 同时声明不兼容的 release class。
- [ ] `F00-13` CI 检查新增 Blender 项目必须先分类才能合并。
- [ ] `F00-14` 生成按 family、状态、后端和依赖聚合的差距报告。
- [ ] `F00-15` 固定 Blender 5.2 inventory hash作为全域发布输入。
里程碑 F00重复生成结果 byte-for-byte 一致,未分类计数为 0任何汇总状态均可追溯到单项。
## 5. F01 文件、Main、ID 与恢复
- [ ] `F01-01` 盘点全部可持久化 ID type 和 embedded ID。
- [ ] `F01-02` 为每个 ID type 建立最小合法 `.blend` fixture。
- [ ] `F01-03` 为 library-linked、missing、override 三种所有权建立 fixture。
- [ ] `F01-04` 逐 ID type 验证 wasm32 DNA 对齐和指针重建。
- [ ] `F01-05` 逐 ID type 验证 Main 加载、释放和 100 次重复无增长。
- [ ] `F01-06` 固定 stable ID 在 rename、duplicate、append 后的规则。
- [ ] `F01-07` 固定 ID add/remove/remap 的单事务 revision 规则。
- [ ] `F01-08` 验证 undo/redo 不产生悬挂 ID 引用。
- [ ] `F01-09` 验证保存取消保持旧 `.blend` hash。
- [ ] `F01-10` 验证 OOM 保持旧 Main 和旧 OPFS revision。
- [ ] `F01-11` 验证 staging、journal、manifest 三个崩溃点恢复。
- [ ] `F01-12` 验证跨 Worker 重启的 command replay 去重。
- [ ] `F01-13` 建立 schema migration 向前、向后和拒绝矩阵。
- [ ] `F01-14` 建立 autosave、manual save、save-as 的互斥状态机。
- [ ] `F01-15` 建立未知新 DNA 字段的数据保留测试。
里程碑 F01所有已分类 ID 都能精确读取并安全释放;允许写入的 ID 完成原子保存重开,其余保持
数据不丢失并返回稳定阻断。
## 6. F02 Scene、Object、Collection 与 View Layer
- [ ] `F02-01` 完整读取 Scene、ViewLayer、LayerCollection 和 Collection 层级。
- [ ] `F02-02` 完整读取 Object 类型、实例、显示、选择和可见性状态。
- [ ] `F02-03` 验证 local/world/parent inverse/delta transform 组合。
- [ ] `F02-04` 验证 parent 类型 Object/Bone/Vertex/Vertex3。
- [ ] `F02-05` 实现 View Layer create/remove/rename Main transaction。
- [ ] `F02-06` 实现 Collection link/unlink/move Main transaction。
- [ ] `F02-07` 实现 Object link/unlink/duplicate/delete Main transaction。
- [ ] `F02-08` 实现 parent/unparent/clear inverse Main transaction。
- [ ] `F02-09` 实现 transform、apply transform 和 origin 独立 transaction。
- [ ] `F02-10` 实现 instance collection 和 real-user 计数安全门。
- [ ] `F02-11` 验证 dependency cycle 和跨 Scene 引用阻断。
- [ ] `F02-12` 同步 View3D、Outliner、Properties 的 selection/context revision。
- [ ] `F02-13` 验证 local view、hide、exclude、holdout 和 indirect-only 映射。
- [ ] `F02-14` 保存重开后比较层级、可见性和 stable ID。
- [ ] `F02-15` 建立 100k Object/Collection 的分块与性能门。
## 7. F03 Depsgraph、Animation Frame 与 Delta
- [ ] `F03-01` 为全部 ID type 记录 depsgraph component/operation 支持状态。
- [ ] `F03-02` 验证 graph build、tagged update、frame change 和 graph free。
- [ ] `F03-03` 验证对象 transform dependency ordering。
- [ ] `F03-04` 验证 modifier、constraint、driver、parent 跨对象依赖边。
- [ ] `F03-05` 验证 dependency cycle 报告包含稳定对象和 operation ID。
- [ ] `F03-06` 固定每种 Main edit 的最小 recalc tag。
- [ ] `F03-07` 拒绝 stale evaluation 覆盖新 revision。
- [ ] `F03-08` 固定 frame seek 取消和迟到结果丢弃规则。
- [ ] `F03-09` 对 snapshot 与 delta 结果建立等价性测试。
- [ ] `F03-10` 对 add/remove/remap 建立 delta 顺序测试。
- [ ] `F03-11` 对 mesh byte range 建立重叠、越界和合并测试。
- [ ] `F03-12` 对资源 delta 建立 hash、生命周期和引用计数测试。
- [ ] `F03-13` 主线程与 Offscreen 消费同一 revision 序列。
- [ ] `F03-14` 100 次 frame/update 循环无 Main、Worker、GPU 持续增长。
## 8. F04 Mesh 建模与 UV
- [ ] `F04-01` 冻结 Object/Edit Mode mesh operator 白名单。
- [ ] `F04-02` 完成 vertex/edge/face select、history 和 active element。
- [ ] `F04-03` 完成 box/circle/lasso/select-linked/select-similar。
- [ ] `F04-04` 完成 primitive add 的逐 primitive fixture。
- [ ] `F04-05` 完成 merge、split、separate、join 的逐操作闭环。
- [ ] `F04-06` 完成 extrude vertex/edge/face/region 的逐模式闭环。
- [ ] `F04-07` 完成 inset individual/region 的逐参数闭环。
- [ ] `F04-08` 完成 bevel vertex/edge 的 segment/profile/clamp 闭环。
- [ ] `F04-09` 完成 loop cut、slide 和 edge ring selection。
- [ ] `F04-10` 完成 knife、bisect 和 intersect 的独立闭环。
- [ ] `F04-11` 完成 rip、spin、screw 和 bridge edge loops。
- [ ] `F04-12` 完成 fill、grid fill、beautify 和 triangulate/quads。
- [ ] `F04-13` 完成 dissolve vertex/edge/face/limited dissolve。
- [ ] `F04-14` 完成 normals recalc/flip/split/merge by distance。
- [ ] `F04-15` 完成 crease、bevel weight、sharp、seam 属性编辑。
- [ ] `F04-16` 完成 vertex group、color attribute 和 generic attribute 编辑。
- [ ] `F04-17` 完成 UV seam、unwrap、smart project、cube/cylinder/sphere project。
- [ ] `F04-18` 完成 UV select、stitch、weld、pin、align、pack islands。
- [ ] `F04-19` 完成 multi-UV map、active/render UV 和 UDIM tile 规则。
- [ ] `F04-20` 每个 operator 建立 non-manifold、degenerate 和预算负例。
- [ ] `F04-21` 每个 operator 建立 Main/undo/save/reopen/desktop golden。
- [ ] `F04-22` 建立 1M vertex edit 的取消、内存和 delta 性能门。
## 9. F05 Curve、Surface、Text、Metaball 与现代 Curves
- [ ] `F05-01` 完成 Curve Poly/Bezier/NURBS 全字段 reader。
- [ ] `F05-02` 完成 Surface U/V order、knot、endpoint、cyclic reader。
- [ ] `F05-03` 完成 Text body、textbox、paragraph、style 和 VFont reader。
- [ ] `F05-04` 完成 Metaball family、element、threshold 和 update reader。
- [ ] `F05-05` 完成 Curves point/curve domain 与全部基础属性 reader。
- [ ] `F05-06` 完成 PointCloud position/radius/custom attributes reader。
- [ ] `F05-07` 完成 spline/control point/handle create、remove、reorder。
- [ ] `F05-08` 完成 spline type convert、cyclic、tilt、radius 和 weight 编辑。
- [ ] `F05-09` 完成 Surface U/V topology 和 rational weight 编辑。
- [ ] `F05-10` 完成 Text body、style range、textbox 和 layout 编辑。
- [ ] `F05-11` 完成外部字体校验、OPFS 绑定、替换和缺失恢复。
- [ ] `F05-12` 完成 Metaball element add/remove/transform 编辑。
- [ ] `F05-13` 完成 Curves selection、comb、length 和 radius 有界工具。
- [ ] `F05-14` 验证 legacy/evaluated mesh 与 desktop tessellation golden。
- [ ] `F05-15` 验证主线程/Offscreen handle raycast、gizmo 和释放。
- [ ] `F05-16` 完成 GLB/USD 映射或逐类型 loss report。
## 10. F06 Volume、Hair 与大规模稀疏数据
- [ ] `F06-01` 读取 Volume grid、transform、class、type 和 metadata。
- [ ] `F06-02` 固定 OpenVDB 到 NanoVDB converter build identity。
- [ ] `F06-03` 校验每个 grid 和 chunk 的 offset、length、hash。
- [ ] `F06-04` 完成 HTTP range、retry、resume 和 If-Range。
- [ ] `F06-05` 完成 OPFS bundle 原子绑定和 Worker 重发现。
- [ ] `F06-06` 定义 GPU page-fault feedback 和 overflow 行为。
- [ ] `F06-07` 完成 page request 合并、取消、hash 和 stale revision 门。
- [ ] `F06-08` 完成 resident page table、pin、LRU 和确定性 OOM。
- [ ] `F06-09` 完成主线程 WebGPU 自动缺页重绘。
- [ ] `F06-10` 完成 Offscreen WebGPU 相同缺页序列。
- [ ] `F06-11` 完成 density/temperature/flame/color 有限材质映射。
- [ ] `F06-12` 完成 device loss 后 page table 有界恢复。
- [ ] `F06-13` 完成 Hair curves、root binding 和 surface dependency reader。
- [ ] `F06-14` 完成 64 MiB/1 GiB sparse bundle 预算和取消门。
- [ ] `F06-15` 完成 Volume Main + asset + viewport 联合保存重开。
- [ ] `F06-16` 完成 desktop/主线程/Offscreen 三视角 golden。
## 11. F07 Sculpt、Vertex Paint、Weight Paint 与 Texture Paint
- [ ] `F07-01` 冻结 PBVH 类型、brush、falloff 和 symmetry inventory。
- [ ] `F07-02` 接入真实 PBVH node 查询,移除 proxy-success。
- [ ] `F07-03` 接入深度/遮挡可见性采样。
- [ ] `F07-04` 完成 Draw、Smooth、Inflate、Grab 各自 desktop golden。
- [ ] `F07-05` 完成 Mask draw/clear/invert 和 Face Set 操作。
- [ ] `F07-06` 完成 symmetry、radial symmetry 和 texture mask 规则。
- [ ] `F07-07` 一个 pointer session 只提交一个 Main undo step。
- [ ] `F07-08` 完成 Vertex Paint blend、falloff、selection mask。
- [ ] `F07-09` 完成 Weight Paint normalize、limit、mirror、clean。
- [ ] `F07-10` 完成 multi-paint、auto-normalize 和 bone selection 联动。
- [ ] `F07-11` 完成 Texture Paint UV hit、seam bleed 和 clone source。
- [ ] `F07-12` 完成 packed/UDIM dirty tile 原子事务。
- [ ] `F07-13` 完成 4K/8K texture upload、LRU、quota 和 OOM。
- [ ] `F07-14` 完成笔/压力输入与鼠标输入确定性 fixture。
- [ ] `F07-15` 每种 paint 模式完成 undo/save/reopen 和 Worker restart。
## 12. F08 Grease Pencil
- [ ] `F08-01` 完成 Layer/Group/Frame/Drawing/Stroke/Point 全字段 reader。
- [ ] `F08-02` 完成 layer/group create/remove/reorder transaction。
- [ ] `F08-03` 完成 frame insert/remove/duplicate/move transaction。
- [ ] `F08-04` 完成 draw/erase stroke 单 pointer transaction。
- [ ] `F08-05` 完成 point/stroke/segment select、marquee、lasso。
- [ ] `F08-06` 完成 point radius/opacity/color/material/cyclic 编辑。
- [ ] `F08-07` 完成 onion skin 和 multi-frame preview。
- [ ] `F08-08` 完成 2D canvas 与 3D viewport selection revision 同步。
- [ ] `F08-09` 完成 Dope Sheet frame exposure 编辑。
- [ ] `F08-10` 逐个接入 Grease Pencil modifier reader/evaluator。
- [ ] `F08-11` 完成主线程/Offscreen stroke tessellation golden。
- [ ] `F08-12` 完成 save/reopen、GLB/USD loss 和大 drawing 性能门。
## 13. F09 Modifier 与 Geometry Processing
- [ ] `F09-01` 将 Blender 5.2 modifier 0-86 每项映射到 parity ID。
- [ ] `F09-02` 每项读取 name、UID、stack order、mode 和 target。
- [ ] `F09-03` 每项建立 unsupported preservation fixture。
- [ ] `F09-04` 逐项开放 Generate modifier evaluator 和 golden。
- [ ] `F09-05` 逐项开放 Deform modifier evaluator 和 golden。
- [ ] `F09-06` 逐项开放 Physics modifier cache/evaluator 和 golden。
- [ ] `F09-07` 逐项开放 Grease Pencil modifier evaluator 和 golden。
- [ ] `F09-08` 逐项实现参数 Main edit、undo、save/reopen。
- [ ] `F09-09` 逐项实现 apply/copy/move/remove transaction。
- [ ] `F09-10` 验证 object/collection/texture target 缺失和 cycle。
- [ ] `F09-11` 验证 viewport/render/edit/cage 开关组合。
- [ ] `F09-12` 验证 stack reorder 与 depsgraph recalc 最小化。
- [ ] `F09-13` Decimate 三模式完成全部 delimiter/weight/symmetry 组合。
- [ ] `F09-14` Subdivision CPU/OpenSubdiv/WebGPU 路径分别声明。
- [ ] `F09-15` 1M/10M topology 的取消、内存、Worker restart 门。
## 14. F10 Geometry Nodes 与 Simulation Zone
- [ ] `F10-01` 完成 node/group/interface/socket/link/stable ID reader。
- [ ] `F10-02` 生成全部 Geometry Node type inventory。
- [ ] `F10-03` 为 unsupported node 保留原图并阻断求值。
- [ ] `F10-04` 逐 node 建立最小 desktop fixture。
- [ ] `F10-05` 逐 node 实现 socket 默认值和 link type 校验。
- [ ] `F10-06` 逐 node 接入 Blender lazy-function evaluator。
- [ ] `F10-07` 逐 node 比较 geometry、attribute、instance 输出。
- [ ] `F10-08` 固定 field context、domain adaptation 和匿名属性寿命。
- [ ] `F10-09` 固定 group recursion、dependency cycle 和深度预算。
- [ ] `F10-10` 实现 node add/remove/move/link/unlink 单 transaction。
- [ ] `F10-11` 实现 interface socket add/remove/reorder transaction。
- [ ] `F10-12` Simulation Zone cache 绑定 graph/source/revision hash。
- [ ] `F10-13` 完成 cache cancel、LRU、corruption、restart 和 OOM。
- [ ] `F10-14` 完成 node editor UI、selection、search 和错误定位。
- [ ] `F10-15` 恶意大图、深链、宽 fan-out 和资源引用预算门。
## 15. F11 Material、Shader Nodes、Texture 与 Color Management
- [ ] `F11-01` 完成 Material slot、surface/volume/displacement 全字段 reader。
- [ ] `F11-02` 生成全部 Shader Node type inventory。
- [ ] `F11-03` 逐 node 建立 socket/link/default desktop fixture。
- [ ] `F11-04` 逐 node 实现 Main add/remove/link/parameter transaction。
- [ ] `F11-05` 完成 Principled BSDF 全输入和版本语义映射。
- [ ] `F11-06` 完成 Texture Coordinate/Mapping/Image/Environment 节点。
- [ ] `F11-07` 完成 Normal/Bump/Displacement/Tangent 节点。
- [ ] `F11-08` 完成 Math/Vector Math/Mix/Color Ramp 节点。
- [ ] `F11-09` 固定 unsupported graph preservation 和 compile block。
- [ ] `F11-10` shader key 绑定 graph、asset、color space 和 backend。
- [ ] `F11-11` compile failure 保留上一份可用 pipeline。
- [ ] `F11-12` 完成 image source、packed、UDIM、sequence 和 generated 生命周期。
- [ ] `F11-13` 完成 sRGB/linear/non-color/scene-linear 转换 golden。
- [ ] `F11-14` 完成 OCIO view/display/look/exposure 能力矩阵。
- [ ] `F11-15` 完成 viewport PBR 与 Blender reference 像素误差门。
## 16. F12 Animation、Driver、NLA 与 Timeline
- [ ] `F12-01` 完成 Action/Slot/FCurve/Group/Channel 全字段 reader。
- [ ] `F12-02` 完成 keyframe insert/delete/move/duplicate transaction。
- [ ] `F12-03` 完成 constant/linear/bezier interpolation。
- [ ] `F12-04` 完成 handle type、tangent、easing 和 extrapolation。
- [ ] `F12-05` 完成 modifier、sampled point 和 cyclic FCurve reader。
- [ ] `F12-06` 完成 Dope Sheet channel/filter/selection 同步。
- [ ] `F12-07` 完成 Graph Editor key/handle/curve editing。
- [ ] `F12-08` 完成 marker、preview range、playback 和 audio sync 状态。
- [ ] `F12-09` 完成 driver variable/target/expression 安全 reader。
- [ ] `F12-10` 只对白名单 driver expression 开放本地求值。
- [ ] `F12-11` 完成 NLA track/strip/action/time mapping 只读求值。
- [ ] `F12-12` 完成 Clip/Transition/Meta/Sound strip 逐类型编辑。
- [ ] `F12-13` 完成 blend/repeat/reverse/animated time 逐参数 golden。
- [ ] `F12-14` 完成 NLA Editor selection、move、trim 和 mute/solo。
- [ ] `F12-15` 完成 animation GLB/USD 映射和 loss report。
## 17. F13 Armature、Pose、Constraint 与 Shape Key
- [ ] `F13-01` 完成 Armature/Bone/EditBone/PoseBone 全字段 reader。
- [ ] `F13-02` 完成 rest/pose/parent/connected/inherit scale 组合 golden。
- [ ] `F13-03` 完成 edit bone add/remove/subdivide/extrude/parent transaction。
- [ ] `F13-04` 完成 pose transform、clear、copy/paste 和 pose library transaction。
- [ ] `F13-05` 生成全部 Object/Bone Constraint inventory。
- [ ] `F13-06` 逐 constraint 读取参数、target、space 和 influence。
- [ ] `F13-07` 逐 constraint 建立 desktop/WASM frame golden。
- [ ] `F13-08` 逐 constraint 实现 add/remove/reorder/parameter transaction。
- [ ] `F13-09` 完成 IK chain、pole、limits 和 dependency cycle。
- [ ] `F13-10` 完成 vertex group weight、bind matrix 和 armature modifier。
- [ ] `F13-11` 完成 Shape Key relative/absolute、value 和 slider 编辑。
- [ ] `F13-12` 完成 Shape Key topology change 阻断或精确 remap。
- [ ] `F13-13` 完成 skin/shape simplification 误差预算和稳定阻断。
- [ ] `F13-14` 完成 Three.js skeleton/morph 和双视口像素门。
- [ ] `F13-15` 完成 GLB skin/morph/animation desktop reimport 比较。
## 18. F14 Physics、Particles 与 Simulation Cache
- [ ] `F14-01` 冻结 Rigid Body/World/Constraint inventory。
- [ ] `F14-02` 冻结 Cloth/Collision/Soft Body inventory。
- [ ] `F14-03` 冻结 Fluid Domain/Flow/Effector inventory。
- [ ] `F14-04` 冻结 Dynamic Paint Canvas/Brush inventory。
- [ ] `F14-05` 冻结 Particle/Hair/Force Field inventory。
- [ ] `F14-06` 逐 family 完成 Main settings/dependency reader。
- [ ] `F14-07` 逐 family 完成 settings edit/undo/save/reopen。
- [ ] `F14-08` 逐 family 定义 local solver、desktop bake 或 server 分类。
- [ ] `F14-09` 逐 family 定义 cache manifest 和 frame binary schema。
- [ ] `F14-10` 完成 source/settings/build/frame/hash 绑定。
- [ ] `F14-11` 完成 exact frame seek、range read 和 stale frame gate。
- [ ] `F14-12` 完成 bake start/progress/cancel/commit 状态机。
- [ ] `F14-13` 完成 cache LRU、quota、corruption 和 Worker restart。
- [ ] `F14-14` 每 family 完成 100 帧 desktop/browser golden。
- [ ] `F14-15` 每 family 完成 OOM、timeout、device loss 和 server fault 门。
## 19. F15 Camera、Lighting、Render 与 Output
- [ ] `F15-01` 完成 Camera lens/sensor/shift/clip/DOF/panorama reader。
- [ ] `F15-02` 完成 Point/Sun/Spot/Area light 全字段 reader。
- [ ] `F15-03` 完成 World surface/color/strength/environment reader。
- [ ] `F15-04` 完成 Scene render、film、format、color management reader。
- [ ] `F15-05` 每个字段完成 Main edit/undo/save/reopen。
- [ ] `F15-06` 完成 Three/WebGPU camera、light 和 shadow 映射。
- [ ] `F15-07` 固定灯数、shadow atlas、probe 和 texture 预算。
- [ ] `F15-08` 冻结 Workbench feature matrix 和 pixel golden。
- [ ] `F15-09` 冻结 Eevee feature matrix 和 pixel golden。
- [ ] `F15-10` 将 Cycles device/backend 明确分类为 server。
- [ ] `F15-11` 完成 render job submit/progress/cancel/result hash。
- [ ] `F15-12` 完成 Freestyle capability 和 server result binding。
- [ ] `F15-13` 完成 Render Result/pass/layer metadata 和查看器。
- [ ] `F15-14` 完成 image output format/color/alpha 能力矩阵。
- [ ] `F15-15` 完成 device loss、OOM、timeout 和错误报告。
## 20. F16 Compositor
- [ ] `F16-01` 生成全部 Compositor Node inventory。
- [ ] `F16-02` 完成 graph/socket/link/default/resource reader。
- [ ] `F16-03` 逐 node 建立 desktop input/output fixture。
- [ ] `F16-04` 逐 node 实现 CPU 或 WebGPU executor。
- [ ] `F16-05` 逐 node 实现 Main parameter/link transaction。
- [ ] `F16-06` unsupported graph 保留并提交 server 或阻断。
- [ ] `F16-07` 完成 Render Layer/Image/Movie Clip/Mask 资源绑定。
- [ ] `F16-08` 完成 Viewer/Composite/File Output 目标边界。
- [ ] `F16-09` 完成 tile、frame cache、LRU 和 revision key。
- [ ] `F16-10` 完成 worker cancellation 和迟到 tile 丢弃。
- [ ] `F16-11` 完成 HDR、alpha、color-space 像素 golden。
- [ ] `F16-12` 完成大图、恶意图、cycle、OOM 和 device loss 门。
## 21. F17 Video Sequencer 与 Audio
- [ ] `F17-01` 完成 Scene/Movie/Image/Sound/Effect/Meta strip reader。
- [ ] `F17-02` 完成 channel/range/offset/speed/transform/crop reader。
- [ ] `F17-03` 生成 effect、transition 和 modifier inventory。
- [ ] `F17-04` 完成 add/remove/move/trim/split transaction。
- [ ] `F17-05` 完成 meta group/ungroup 和 dependency cycle。
- [ ] `F17-06` 逐 effect/transition 建立 frame golden。
- [ ] `F17-07` 运行时探测 WebCodecs/HTMLMedia codec 能力。
- [ ] `F17-08` 完成 proxy、waveform、thumbnail content-addressed cache。
- [ ] `F17-09` 完成精确 seek、scrub、取消和 stale decode gate。
- [ ] `F17-10` 完成 audio mix、mute/solo、volume/pan 和 A/V sync。
- [ ] `F17-11` 完成 AudioContext suspend/resume 和无设备恢复。
- [ ] `F17-12` 最终编码按 codec 分类 local 或 server。
- [ ] `F17-13` 完成 1M frame timeline 和长媒体性能门。
- [ ] `F17-14` 完成损坏媒体、路径沙箱、网络中断和 quota 门。
## 22. F18 Movie Tracking 与 Mask
- [ ] `F18-01` 完成 MovieClip/Proxy/TrackingSettings reader。
- [ ] `F18-02` 完成 Track/Marker/PlaneTrack/Object reader。
- [ ] `F18-03` 完成 Mask/Layer/Spline/Point/Handle reader。
- [ ] `F18-04` 完成 marker add/remove/move/track transaction。
- [ ] `F18-05` 完成 mask point/handle/spline/layer transaction。
- [ ] `F18-06` 完成 selection、marquee、lasso 和锁定过滤。
- [ ] `F18-07` 本地 tracking 算法逐项 capability probe。
- [ ] `F18-08` camera solve、stabilize 和 planar track 分类 local/server。
- [ ] `F18-09` 完成 server solve submit/cancel/result hash。
- [ ] `F18-10` 完成 Clip/Mask editor overlay 和 frame sync。
- [ ] `F18-11` 完成 compositor/scene resource binding。
- [ ] `F18-12` 完成 desktop solve/error golden、保存重开和媒体故障门。
## 23. F19 Asset、Library、Link 与 Override
- [ ] `F19-01` 完成 Asset metadata/tag/catalog/preview reader。
- [ ] `F19-02` 完成 catalog schema migration 和稳定 catalog ID。
- [ ] `F19-03` 完成 content-addressed preview 生成、缓存和释放。
- [ ] `F19-04` 完成 append 单 data-block Main transaction。
- [ ] `F19-05` 完成 link read-only ownership 和 reload transaction。
- [ ] `F19-06` 完成 relocate、missing library 和 path sandbox。
- [ ] `F19-07` 完成 library dependency cycle 检测。
- [ ] `F19-08` 完成 override create/resync/reset 操作矩阵。
- [ ] `F19-09` 完成跨项目 stable ID 和引用失效规则。
- [ ] `F19-10` 完成 Asset Browser search/filter/sort/drag context。
- [ ] `F19-11` 完成 OPFS index rebuild 和损坏条目隔离。
- [ ] `F19-12` 完成大 catalog、quota、Worker restart 和 malicious metadata 门。
## 24. F20 Import、Export 与交换格式
- [ ] `F20-01` 为 glTF/GLB 建立字段级 import/export 矩阵。
- [ ] `F20-02` 为 OBJ/MTL 建立字段级 import/export 矩阵。
- [ ] `F20-03` 为 STL 建立字段级 import/export 矩阵。
- [ ] `F20-04` 为 PLY 建立字段级 import/export 矩阵。
- [ ] `F20-05` 为 USD/USDZ 建立字段级 import/export 矩阵。
- [ ] `F20-06` 为 Alembic 建立字段级 import/export 矩阵。
- [ ] `F20-07` 为 FBX 和其他注册格式给出 local/server/excluded 分类。
- [ ] `F20-08` 每种格式完成最小 import -> save -> reopen。
- [ ] `F20-09` 每种格式完成 export -> desktop reimport。
- [ ] `F20-10` 每种格式比较 hierarchy、geometry、material、animation。
- [ ] `F20-11` 每种格式输出机器可读 warning/loss/block report。
- [ ] `F20-12` 完成 external URI、path traversal 和 sandbox。
- [ ] `F20-13` 完成 archive entry 数、压缩比、总字节和路径预算。
- [ ] `F20-14` 完成大文件 streaming、取消、OOM 和临时文件清理。
- [ ] `F20-15` 固定 desktop importer/exporter build identity。
## 25. F21 Editors、Workspace、Keymap 与交互
- [ ] `F21-01` 生成全部 SpaceType/Region/Mode inventory。
- [ ] `F21-02` 完成 View3D/Outliner/Properties 共享 context store。
- [ ] `F21-03` 完成 UV/Image Editor 只读面,再逐 writer 开放编辑。
- [ ] `F21-04` 完成 Shader/Geometry/Compositor Node Editor 共用 graph 基础层。
- [ ] `F21-05` 完成 Dope Sheet/Graph/NLA/Timeline 共用 animation context。
- [ ] `F21-06` 完成 Spreadsheet 几何和属性分页读取。
- [ ] `F21-07` 完成 Text/Console 的只读与默认拒绝边界。
- [ ] `F21-08` 完成 workspace/area/region split/join/layout persistence。
- [ ] `F21-09` workspace 保存不得改变 Blender scene revision。
- [ ] `F21-10` operator search 只从 capability registry 生成。
- [ ] `F21-11` keymap 按 editor/modal/tool context 确定性解析。
- [ ] `F21-12` 完成 menu/context menu/popup/modal/Escape 规则。
- [ ] `F21-13` 完成鼠标、触控、笔、HiDPI、IME 和非 US 键盘 fixture。
- [ ] `F21-14` 完成键盘全路径、焦点、ARIA 和对比度门。
- [ ] `F21-15` editor dispose 后零 DOM listener、Worker request 和 GPU 泄漏。
## 26. F22 Python、Extension 与安全执行边界
- [ ] `F22-01` 保持 Python/Text autorun/driver/add-on 默认拒绝。
- [ ] `F22-02` 完成 script/add-on manifest 只读解析和大小预算。
- [ ] `F22-03` 定义签名者、权限、撤销、版本和审计模型。
- [ ] `F22-04` 定义浏览器 sandbox 的 DOM/network/OPFS/Worker 权限。
- [ ] `F22-05` 完成 CPU、内存、时间和输出预算。
- [ ] `F22-06` 完成 requestId replay、审计 hash chain 和篡改拒绝。
- [ ] `F22-07` server Blender job 使用一次性目录和只读源。
- [ ] `F22-08` server job 固定网络策略、资源限制和超时。
- [ ] `F22-09` server result 绑定 source/settings/build/output hash。
- [ ] `F22-10` 完成恶意脚本、依赖混淆和逃逸 fixture。
- [ ] `F22-11` 完成 CSP禁止 inline script、eval 和未声明 origin。
- [ ] `F22-12` 完成 extension install/update/remove 的签名和回滚策略。
## 27. F23 平台、偏好、国际化与跨浏览器
- [ ] `F23-01` 冻结 Chromium single/pthread 平台能力报告。
- [ ] `F23-02` 完成 Firefox OPFS/WebGL2/WebGPU/Worker/isolation probe。
- [ ] `F23-03` 完成 Firefox P0 主线程路径。
- [ ] `F23-04` 完成 Firefox Offscreen 路径。
- [ ] `F23-05` 完成 WebKit OPFS/WebGL2/WebGPU/Worker/isolation probe。
- [ ] `F23-06` 完成 WebKit P0 主线程路径。
- [ ] `F23-07` 完成 WebKit Offscreen 路径。
- [ ] `F23-08` 按浏览器/设备档位固定内存和 GPU 预算。
- [ ] `F23-09` 完成 user preferences schema、migration 和项目无关 persistence。
- [ ] `F23-10` 完成 locale 切换、数字/日期格式和翻译 fallback。
- [ ] `F23-11` 完成 theme、font scale、reduced motion 和高对比偏好。
- [ ] `F23-12` native window/file watcher/CUDA/Metal/HIP/OptiX 实现细节明确不复制,其用户能力
按新计划映射到 platform-equivalent 或 server exact 路径。
- [ ] `F23-13` 每个新增浏览器加入 quick/P0/full 三层 CI。
- [ ] `F23-14` 1280x720、1440x900、4K 和窄屏无重叠截图门。
## 28. F24 性能、故障、安全、交付与全域审计
- [ ] `F24-01` 固定 100k/1M/10M geometry 基准环境和预算。
- [ ] `F24-02` 固定 4K/8K/UDIM texture 基准环境和预算。
- [ ] `F24-03` 固定 100/1000/1M frame animation/media 基准。
- [ ] `F24-04` 固定 simulation/volume cache 容量和 seek 基准。
- [ ] `F24-05` 覆盖 WASM/Main、OPFS、GPU、VDB 四类 OOM。
- [ ] `F24-06` 覆盖 Worker crash、device loss、network interruption、quota。
- [ ] `F24-07` 覆盖 blend/image/font/media/archive/node/script fuzz corpus。
- [ ] `F24-08` fuzz crash 修复后自动固化为最小回归 fixture。
- [ ] `F24-09` quick lane 执行 type/lint/unit/status/schema。
- [ ] `F24-10` Chromium lane 执行 P0、双视口和 fault smoke。
- [ ] `F24-11` release lane 执行 full E2E、性能、VDB、acceptance、offline。
- [ ] `F24-12` CI 报告绑定 commit、lockfile、inventory、ledger、engine、archive hash。
- [ ] `F24-13` binary/source archive 在两个空临时目录独立复验。
- [ ] `F24-14` 完成 SBOM、license、source offer 和供应链审计。
- [ ] `F24-15` 每个 parity ID 都有 LOCAL_EXACT、LOCAL_EQUIVALENT 或 SERVER_EXACT 完整证据。
- [ ] `F24-16` inventory 未分类、隐式 loss、proxy-success 三项计数均为 0。
- [ ] `F24-17` 独立机器复验全部构件和关键证据 hash。
- [ ] `F24-18` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。
里程碑 F24不能由聚合 release gate 反推 family 完成。只有 inventory 的每个单项都达到自身
退出条件,全部生成的 owner family 才能逐项从 `BLOCKED` 改为 `COMPLETE`
## 29. 当前领取点
截至 2026-08-18M6-M11 已按当前计划完成M11 最终以 Render、Compositor、Media 三域故障
生命周期专项门收口M12-01A-I 已完成 catalog schema/migrationM12-02A-H 又完成 PreviewImage
inventory、identity、decode budget、OPFS commit、dedupe、quarantine、project-scoped reference GC
与 desktop/browser 双显示路径像素闭环。本文的 F00-F24
条目只作为覆盖检查表不能直接领取M12-03A 又冻结 Append/Link/Override 的 36 类 root 与
实际 ID pointer dependency closureM12-03B 固定 source/owner/read-only/invalidation identity
M12-03C 冻结 Blender 5.2 单 Object append 的 Object/Mesh/Material/Image local stable mapping
M12-03D 已通过 WASM Main 单 transaction 创建同一 local dependency closureM12-03E 又完成
undo/redo/save/reopen 与 desktop canonical graph 的联合一致性M12-03F 又冻结 desktop LINK
source-library/read-only ownershipM12-03G 又完成 linked writer fail-closed gateM12-03H 又完成
matching-generation reloadM12-03I 又完成 missing-library placeholder/source preservationM12-03J 又冻结
desktop override reference/local-owner/property contractM12-03K 又开放单一 verified override writer
M12-03L 又冻结 freshness gateM12-03M/N 又收口 negative cases 与三 lane 独立命令矩阵。
M12-04A-J 又冻结 declared source admission、canonical path、path/origin safety、link resolution、
metadata-first、archive budget/conflict、cancellation rollback、quota/OOM recovery 与恶意 ZIP/TAR
长期回归M12-05A 又从 pinned Blender 5.2 runtime 生成七格式 operator/build inventoryM12-05C
又将 M12-05B capability matrix SHA-256 绑定到文件选择器与 operator searchM12-05D 又将
执行路由绑定到 14 条 runtime receiptM12-05E 又固定每条 receipt 的 source/settings/runtime
三重 hashM12-05F 又增加 parent byte、canonical receipt-set 和 runtime identity freshness gate
并对未声明/未注册/未绑定/伪造/过期/跨版本组合保持 fail-closedM12-06A 又由 pinned Blender
5.2 desktop 生成五个独立 Mesh/PBR/UV/skin/animation GLB fixture并完成逐字节确定性重生成
M12-06B 又让生产 Web parser 在 Node 与 Chromium Worker 中精确比较 topology、attributes、
materials、nodes 和 animationsM12-06C 又完成 desktop GLB import 后的 Main save/reopen 与
stable-ID 比较M12-06D 又固定 Web export machine loss reportM12-06E 又完成 desktop Web GLB
re-import canonical comparisonM12-06F 又固定 sparse/extension/URI/budget negative gate
M12-06G 又完成 GLB import/export 取消、Worker 换代和真实 OPFS quota 恢复M12-07A 又冻结
单 Mesh OBJ/MTL position/normal/UV/material-group desktop 正例M12-07B 又冻结双对象、负索引、
相对纹理来源和坏 face 负例M12-07C 又完成 Web-to-desktop OBJ round-trip/loss report
M12-07D 又分开冻结 STL binary/ASCII capabilityM12-07E 又完成 normal/unit/degenerate/trailing
desktop/Web 对标并记录 stricter trailing blockM12-07F 又完成 STL Web-to-desktop round-trip/material
loss reportM12-07G 又分开冻结 PLY ASCII/binary little-endian capabilityM12-07H 又完成
PLY vertex/face/color/custom property mapping 与 unknown-property loss reportM12-07I 又完成
big-endian/坏 list/超大 count 稳定阻断M12-07J 又完成 OBJ/STL/PLY 三格式 recoveryM13-01A 又完成脚本
入口 inventoryM13-02A 又完成 bounded script manifest limitsM13-02B 又完成 canonical
serialization signature inputM13-02C 又完成 signer identity/key rotation/revocation/timestamp policyM13-02D-F 又完成签名、权限、sandbox scope/budget/host-call、crash/timeout、cancellation 和 dispose 资源归零门。当前唯一下一任务为
`EXECUTION_QUEUE.md` 中的 `M13-04F`;具体上下文见 `docs/tasks/M13-04F.md`

View File

@@ -0,0 +1,736 @@
# Blender 5.2 完整 Web 对标实施计划
更新时间2026-08-18
> 长期规范背景:本文件不进入默认任务上下文,不提供当前领取指针。执行者必须先运行
> `node tools/web/print-task-context.mjs`,只有任务卡明确要求时才读取本文件的具体小节。
本文是 M12 及以后完整 Blender 5.2 Web 迁移的长期规范事实源,负责执行分类、原子任务、
依赖、证据和完整发布门。它不覆盖 `WEB_BLENDER_MODELER_V1_SCOPE.md` 的 V1 产品契约,也不
改写 V1 已冻结的 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED` 发布分类。
文档职责固定如下:
| 事实 | 权威来源 |
| --- | --- |
| V1 承诺、非目标和发布标准 | `WEB_BLENDER_MODELER_V1_SCOPE.md` |
| 当前唯一可领取任务 | `EXECUTION_QUEUE.md`、对应 `tasks/<task>.md` 与机器 `nextTask` |
| M12-M23 完整对标规则和长期任务 | 本文 |
| 当前实现事实和 family/slice 状态 | `status/parity-ledger.json` 与对应 evidence |
| Blender 全域功能说明和覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md``BLENDER_5_2_FULL_PARITY_WBS.md` |
描述与机器证据冲突时不得用文字把失败改成成功。M12-01A-I 已收口M12-02A-G preview
inventory/identity/decode budget/OPFS commit/dedupe/quarantine/GC 已完成且未改变 parity 状态;
desktop/browser 主线程与 Offscreen preview 像素闭环也已完成Append/Link/Override 数据块与
依赖闭包 inventory、source/owner/read-only/invalidation 合同、desktop 单 Object append fixture、
WASM Main 单 transaction append 及其 undo/redo/save/reopen canonical 比较已完成desktop LINK
fixture 也已冻结 source-library/read-only ownershiplinked writer fail-closed gate、matching-generation
reload、missing-library placeholder preservation、desktop override ownership/property fixture、单一
verified override writer、freshness gate、三 lane 独立命令矩阵、declared source-origin admission、
canonical project-path normalization、path/origin security、archive link/metadata/budget/conflict gate、
cancellation rollback、quota/OOM recovery 与恶意 ZIP/TAR 长期回归已完成M12-05A 已从 pinned
Blender 5.2 runtime 生成 7-format inventoryM12-05B 已冻结逐格式 local/server capability matrix
M12-05C 已将 UI 文件选择与 operator search 绑定到该 matrixM12-05D 又将执行路由绑定到
M12-05A 的 Blender runtime receiptM12-05E 又为 14 条 receipt 固定 source/settings/runtime 三重
hashM12-05F 又增加 parent byte、canonical receipt-set 和 runtime identity freshness gateM12-06A
又由 pinned Blender 5.2 desktop 生成 mesh、PBR、UV、skin、animation 五个独立 GLB fixture并以
逐字节重生成和 semantic report 固定 fixture groupM12-06B 又由 Node 与 Chromium Worker 对
topology、attributes、materials、nodes、animations 做 exact canonical 比较M12-06C 又完成桌面
GLB 导入后的 authoritative Main 保存、重开、stable-ID 比较和 Chromium 资源释放门M12-06D 又
固定 Web export machine loss reportM12-06E 又完成 pinned Blender 对 Web GLB 的 save/reopen
canonical comparison并将 UV/skin 的明确差异保留为 machine mismatchesM12-06F 又固定
sparse/extension/外部 URI/budget 负例M12-06G 再完成双向取消、Worker 换代和真实 OPFS quota
恢复M12-07A 又由 pinned Blender 5.2 生成 OBJ 单 Mesh position/normal/UV/material-group
正例并完成逐字节重生成M12-07B 又冻结 OBJ 双对象、负索引、相对纹理来源和坏 face 负例;
M12-07C 又完成 Web Worker OBJ/MTL 重写、纹理 loss report 和 Blender 5.2 重导入比较;
M12-07D 又把同一 Blender Mesh 的 STL binary/ASCII runtime capability 分开冻结M12-07E 又完成
normal/unit/degenerate/trailing desktop/Chromium 对标并记录 trailing 的 stricter Web block
M12-07F 又完成 Web binary STL 重写、Blender 重导入和 material-loss reportM12-07G 又冻结
Blender 5.2 的 PLY ASCII 与 binary little-endian 两个独立 capability variantM12-07H 又完成
PLY vertex/face/color/custom property 映射与未知 property loss reportM12-07I 又完成
big-endian、坏 list、超大 count 的稳定阻断M12-07J 又完成 OBJ/STL/PLY 三格式 recoveryM13-01A
又完成脚本入口盘点M13-01B 又完成 `.blend` 脚本 metadata-only openM13-01C 又冻结默认拒绝
policy codesM13-01D 又通过 UI direct-eval bypass scanM13-01E 又通过 Text save/reopenM13-01F
又通过恶意脚本 fixtureM13-02A 又通过 bounded script manifest limitsM13-02B 又固定 canonical serialization signature inputM13-02C 又完成 signer identity/key rotation/revocation/timestamp policyM13-02D 又完成声明内容 ED25519 验签M13-02E 又完成权限最小化M13-02F 又完成签名负例矩阵M13-03A 又冻结 all-deny sandbox scopeM13-03B 又冻结 CPU/wall/memory/message/output budgetsM13-03C 又冻结结构化 host-call allowlistM13-03D 又冻结 crash/timeout isolation、Main revision 不变和迟到结果拒绝M13-03E 又冻结 cancellation no-publish/cache gateM13-03F 又冻结 Worker/两端 MessagePort/timer/AbortController/buffer/cache reference dispose gateM13-03G 又冻结同会话 generation/revision/hash-chain recoveryM13-04A 又冻结随机 `0700` one-shot server job directoryM13-04B 又冻结 source/output 权限隔离M13-04C 又冻结六类资源预算M13-04D 又冻结默认拒绝网络策略M13-04E 又冻结 pinned Blender background/factory-startup argv当前唯一 `nextTask``M13-04F`
## 1. 目标与边界
目标不是制作一个外观类似 Blender 的网页,而是让用户从浏览器完成 Blender 5.2 的可观察
功能,并且每项声明都能用真实 Blender 5.2 执行结果复核。浏览器本地不适合承载的 Cycles、
完整 Python、部分模拟和硬件后端必须由隔离的 Blender 5.2 server job 执行Three.js、
WebGPU 或 JavaScript 近似结果不能冒充 Blender 结果。
“完整 Web Blender”只在以下条件同时满足后成立
1. Blender 5.2 的 data-block、operator、node、modifier、editor workflow、import/export 和
render/compute family 全部进入机器清单。
2. 每个用户可观察能力被标为 `LOCAL_EXACT``LOCAL_EQUIVALENT``SERVER_EXACT`,且有成功
证据;`BLOCKED``UNINVENTORIED` 或未声明项数量必须为零。
3. `LOCAL_EXACT` 使用同一 fixture 比较 Blender desktop 与 WASM/Main 的结构、数值或像素。
4. `LOCAL_EQUIVALENT` 只用于浏览器交互外壳;最终 Main、Depsgraph、保存文件和导出语义仍须
与 Blender 一致。
5. `SERVER_EXACT` 必须真的启动锁定版本 Blender验证提交、进度、取消、资源隔离、结果 hash
和项目绑定,不允许只返回“应走服务端”。
6. 完整项目通过保存、关闭、Worker/页面重启、重新打开和 Blender desktop 再打开;未知数据
不丢失,不支持的写操作不污染原文件。
7. 本项目浏览器执行、CI、验收证据和发布声明永久仅限 Chromium。Firefox 与 WebKit 禁止启动、
探测、领取测试任务或生成支持证据;历史报告仅作归档背景。缺失 WebGPU 只能由 Chromium
真实能力门将依赖 WebGPU 的功能标记为 `BLOCKED`,不得用其他浏览器推断。
Native window、CUDA/Metal/HIP/OptiX 实现细节不需要在浏览器复制,但它们提供的用户能力必须
由 WebGPU、CPU 或 `SERVER_EXACT` 路径覆盖。若没有等价执行路径,就保持 `BLOCKED`,项目不得
使用“完整对标”表述。
### 1.1 完整对标状态不是 V1 发布分类
全量盘点为每个 `parityId` 使用两个独立轴,禁止把 V1 family 的 `releaseClass` 直接升级为
全量完成:
| 轴 | 允许值 | 含义 |
| --- | --- | --- |
| `implementationClass` | `LOCAL_EXACT` | Blender 语义在浏览器 Main/WASM 本地执行并经同 fixture 对标 |
| `implementationClass` | `LOCAL_EQUIVALENT` | 仅浏览器平台或交互外壳不同,最终 Main、文件、导出和可见语义精确 |
| `implementationClass` | `SERVER_EXACT` | 锁定版本的真实 Blender server 执行权威求值,浏览器负责提交和消费 |
| `parityStatus` | `UNINVENTORIED` | Blender 基线项尚未进入机器清单 |
| `parityStatus` | `BLOCKED` | 已盘点但缺实现、证据或存在超阈值差异 |
| `parityStatus` | `IN_PROGRESS` | 唯一原子任务正在实现;仍按未完成计 |
| `parityStatus` | `VERIFIED` | 对应 class 的全部强制证据通过 |
`LOCAL_EQUIVALENT` 不能用于 modifier、node、solver、render、import/export 或其他计算结果的
近似实现。它只适用于 native window、文件选择器、菜单布局、输入映射等浏览器平台差异
若操作后的 Main 或文件语义不同,状态只能是 `BLOCKED`
### 1.2 本地与 Blender server 路由规则
执行 class 在写实现前确定,不能根据某次运行是否方便临时切换:
| 能力特征 | 必选 class | 最低证据 |
| --- | --- | --- |
| Main/ID 读取写入、selection/context、undo/redo、增量编辑、保存恢复 | `LOCAL_EXACT` | desktop/WASM 同 fixture、单事务、保存重开、Chromium 用户路径 |
| native window、系统文件对话框、菜单/快捷键表现等平台外壳 | `LOCAL_EQUIVALENT` | desktop 最终状态、Web Main/文件最终状态、可访问交互和恢复一致 |
| WASM 已包含所需 Blender evaluator且在声明预算内可确定性完成 | `LOCAL_EXACT` | desktop/WASM 语义或像素阈值、浏览器实际执行、故障恢复 |
| Cycles、复杂 Eevee、Blender 完整 Compositor、重模拟/烘焙、tracking solve | `SERVER_EXACT`,除非该项另有完整本地证据 | 真实隔离 Blender job、版本/输入/设置/输出绑定、取消和恢复 |
| 任意 Python/add-on、需原生库的 USD/Alembic/codec 或 OS/GPU 后端能力 | `SERVER_EXACT` | 权限策略、无隐式网络、资源限制、真实执行和结果复验 |
| Three.js/WebGPU 预览、summary、proxy、capability probe、仅路由结果 | 不能单独领取成功 class | 只能作为已分类能力的中间证据;权威结果缺失时保持 `BLOCKED` |
同一 workflow 可以由多个 `parityId` 组成,例如本地编辑后提交服务端 Cycles 渲染;每个 ID
只能有一个权威执行 class。server 路径必须向用户明确上传内容、版本、预算、进度和取消状态;
未配置 server、用户不同意上传或 server receipt 失效时必须 fail-closed不能回退到近似结果。
### 1.3 阻止“完整对标”声明的差异
以下任一项存在时产品、README、发布说明和 UI 都不得使用“完整 Web Blender”或等价表述
- inventory 有未映射、重复映射、版本漂移或 `UNINVENTORIED` 条目;
- 任一 `parityId``BLOCKED/IN_PROGRESS`,或只有 summary/proxy/probe/route/mock 证据;
- 计算结果超过已批准的数值、拓扑、时序、音频或像素阈值;
- 不支持或未知数据在打开、编辑、保存、导出、append/link/override 中丢失或被重写;
- writer 缺 undo/redo、保存重开、desktop 再打开,或失败路径改变 Main/项目 revision
- server 能力没有真实 Blender process、隔离、取消、预算、结果 hash 和项目绑定证据;
- 用户可进入没有 capability gate 的 UI或本地近似被显示为 Blender 权威结果;
- 已声明浏览器、设备档位、输入方式或离线/server 模式缺自己的通过证据;
- clean build、依赖/SBOM/license、安全、性能、故障或独立机器复验未通过。
## 2. 当前基线
- 当前交付物是可审计的 Chromium `Web Blender Modeler V1`,不是完整 Blender。
- M6 至 M11 已完成M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A inventory、
M12-05B capability matrix、M12-05C UI gate、M12-05D runtime receipt gate、M12-05E receipt
binding、M12-05F freshness gate、M12-06A desktop GLB fixture generation、M12-06B Web semantic import
comparison、M12-06C Main save/reopen stable-ID slice、M12-06D Web machine loss report 和
M12-06E desktop re-import comparison、M12-06F GLB negative-case budget gate 和 M12-06G
cancellation/Worker restart/OPFS quota recovery、M12-07A OBJ single-Mesh desktop fixture 和
M12-07B OBJ multi-object/negative/texture-origin fixture、M12-07C Web-to-desktop round-trip 和
M12-07D split STL binary/ASCII capability、M12-07E normal/unit/edge parity、M12-07F
Web-to-desktop round-trip/loss report、M12-07G PLY ASCII/binary little-endian capability 和
M12-07H vertex/face/color/custom property mapping/loss report、M12-07I negative admission
gate 和 M12-07J 三格式 recovery 已完成M13-01A script entry inventory、M13-01B metadata-only open、
M13-01C default-deny policy codes、M13-01D UI bypass scan、M13-01E Text save/reopen、M13-01F malicious fixture、M13-02A manifest limits、M13-02B canonical serialization、M13-02C signer trust policy、M13-02D declaration-bound signature verification、M13-02E permission minimization、M13-02F signature negative matrix、M13-03A sandbox scope、M13-03B sandbox budgets、M13-03C host-call allowlist、M13-03D crash/timeout isolation、M13-03E cancellation no-publish/cache gate、M13-03F dispose 资源归零、M13-03G 同会话恢复审计链、M13-04A one-shot server job directory、M13-04B source/output 权限隔离、M13-04C 六类资源预算、M13-04D 默认拒绝网络和 M13-04E Blender runtime argv 也已完成,当前唯一下一任务为 M13-04F。
- N-015 至 N-026 的 V1 `releaseStatus``READY`,但 12 个 family 的全域
`parityStatus` 仍全部为 `BLOCKED`
- 这 12 个 family 是当前 post-V1 账本范围,不覆盖完整 Blender inventory不能把它们未来的
聚合状态当作 Main、Mesh、Modifier 等全域完成证明。M15 必须生成完整 owner family 集合。
- 当前完成项只证明已声明的有界切片;任何 summary、proxy、capability probe 或路由结果都不能
自动升级为 Blender 功能完成。
## 3. 真实 Blender 对标流水线
### 3.1 固定运行时
- [ ] `FULL-PARITY-RUNTIME-01` 固定官方 Blender 5.2.0 LTS archive SHA-256。
- [ ] `FULL-PARITY-RUNTIME-02` 记录 `blender --version`、build hash、平台、Python、USD、
Alembic、OpenVDB、Cycles device inventory。
- [ ] `FULL-PARITY-RUNTIME-03` desktop runner 拒绝版本或 build hash 漂移。
- [ ] `FULL-PARITY-RUNTIME-04` WASM manifest 绑定 Blender source commit、Emscripten、CMake
cache、JS/WASM SHA-256。
- [ ] `FULL-PARITY-RUNTIME-05` server image 绑定 Blender binary、启动参数、依赖和 image digest。
- [ ] `FULL-PARITY-RUNTIME-06` fixture generator 只能在临时目录写文件,运行后逐项 hash。
- [ ] `FULL-PARITY-RUNTIME-07` 禁止从开发机未声明插件、偏好设置或环境变量读取能力。
- [ ] `FULL-PARITY-RUNTIME-08` CI 在每次 runner 启动时执行 runtime preflight 并写机器报告。
### 3.2 每项能力的固定证据链
能力原子执行单元固定为一个 `parityId × 单一用户可观察行为 × implementationClass`,内部走完
本节的完整证据链。纯 runtime、inventory、generator 或 schema 基建任务必须标记
`enablingTask=true`,列出它解除的依赖,但自身不能把任何 `parityId` 改为 `VERIFIED`
M12 的字母 ID 只有在单一性检查通过后才能进入 `nextTask`;任务文字中出现“每种/全部/逐项”
时,先按稳定 `parityId` 增加后缀并拆成多个任务。M16-M22 的“差距清零”项只是波次容器,
必须先由 M15 展开,不能直接勾选容器。
每个原子功能任务必须按以下顺序执行,任一步失败都不能领取下一功能:
- [ ] `PARITY-TEMPLATE-01` 从 Blender RNA/operator/node 清单选取一个最小行为。
- [ ] `PARITY-TEMPLATE-02` 写 capability 条目、输入 schema、预算、稳定错误码和保存策略。
- [ ] `PARITY-TEMPLATE-03` 写 Blender 5.2 Python fixture generator不手工伪造期望 JSON。
- [ ] `PARITY-TEMPLATE-04` desktop Blender 执行操作并导出 canonical semantic report。
- [ ] `PARITY-TEMPLATE-05` desktop 保存 `.blend`,新 Blender 进程重开并再次导出报告。
- [ ] `PARITY-TEMPLATE-06` WASM/Main 对同一输入执行同一行为并导出同结构报告。
- [ ] `PARITY-TEMPLATE-07` 比较 stable ID、Main revision、数据值、依赖和错误语义。
- [ ] `PARITY-TEMPLATE-08` 对 writer 验证一次 transaction、undo、redo、save/reopen。
- [ ] `PARITY-TEMPLATE-09` Chromium 主线程和 Offscreen 只消费权威结果并验证可见输出。
- [ ] `PARITY-TEMPLATE-10` 加入 unsupported、malformed、stale revision 和预算超限负例。
- [ ] `PARITY-TEMPLATE-11` 加入取消、Worker restart、资源释放和小任务恢复。
- [ ] `PARITY-TEMPLATE-12` 将 command、duration、stdout 摘要和构件 SHA-256 写入 evidence。
- [ ] `PARITY-TEMPLATE-13` 更新 family status、机器 ledger、实施记录和用户可见限制。
- [ ] `PARITY-TEMPLATE-14` 独立运行 `typecheck`、lint、unit、专项 E2E 和状态一致性门。
### 3.3 原子任务与 evidence 机器合同
每个原子任务进入队列前必须含以下字段;字段不得留空,确实不适用时写 `NOT_APPLICABLE`
可校验理由,不能直接删除:
| 字段 | 必填内容 |
| --- | --- |
| identity | `taskId`、parent ID、`parityId``enablingTask`、Blender source anchor、owner、dependency IDs |
| classification | `implementationClass``parityStatus`、用户可观察行为、非目标 |
| fixture | generator command/hash、`.blend`/asset hash、输入预算、license/source |
| desktop | Blender command、runtime/build hash、canonical report/hash、保存后新进程重开报告 |
| wasm | build manifest、command、Main revision、semantic report/hashserver 项记录本地拒绝/提交门 |
| browser | 浏览器/OS/GPU、主线程/Offscreen 适用性、Chromium 用户动作和可见结果 |
| persistence | undo/redo、save/close/reopen、Worker/page restart、desktop reopen、unknown-data report |
| faults | malformed、unsupported、stale、cancel、timeout、OOM/quota、crash/restart、resource-zero |
| server | image/binary digest、policy、job/request/result ID、progress/cancel、resource receipt、output hash |
| comparison | comparator version、阈值来源、实际 max/RMS/P95/hash、pass/fail |
| provenance | commit、dirty flag、commands、exit code、duration、stdout/stderr 摘要、artifact hashes |
evidence 只接受一次从干净输入开始的完整成功运行。重试前的失败保留为诊断,不得拼接不同
commit、runtime 或 fixture 的局部成功。`parityStatus=VERIFIED` 必须由状态校验器重新计算,
不能由 Markdown checkbox 单独决定。
### 3.4 class 对应的强制执行矩阵
| 证据 | `LOCAL_EXACT` | `LOCAL_EQUIVALENT` | `SERVER_EXACT` |
| --- | --- | --- | --- |
| Blender desktop 同 fixture | 必须 | 必须,比较最终语义 | 必须,作为 job 结果基线 |
| WASM/Main 实际执行 | 必须 | 必须完成最终 Main/保存语义 | 必须完成读取、校验、提交和结果绑定;权威求值不冒充本地 |
| Chromium 实际用户路径 | 必须 | 必须 | 必须,含提交/进度/取消/取回 |
| 主线程与 Offscreen | 有可见 viewport 输出时都必须 | 有 viewport 输出时都必须 | 浏览器消费可见结果时按适用路径必须 |
| undo/redo/save/reopen | writer 必须 | 改 Main/项目时必须 | 本地准备和结果绑定改变项目时必须 |
| Worker/page restart | 必须 | 必须 | 必须,且不得重复提交或绑定迟到结果 |
| OOM/quota/cancel/crash 后小任务恢复 | 必须 | 必须 | 浏览器与 server 两侧都必须 |
| 真实 Blender server | 禁止作为本地成功替代 | 禁止作为等价外壳成功替代 | 必须 |
Firefox/WebKit 永久不在本项目的完成门、CI、支持声明或发布证据中M14 只允许 Chromium
设备/输入和 Chromium capability evidence。不得通过新增浏览器任务恢复这两个范围。
### 3.5 比较器
- [ ] `COMPARE-01` 整数、枚举、布尔、ID、拓扑计数和离散索引使用精确比较。
- [ ] `COMPARE-02` Float32/Float64 分别记录 max、RMS、P95、NaN/Inf 和阈值来源。
- [ ] `COMPARE-03` transform 同时比较 location/rotation/scale 和 world matrix处理旋转表示等价。
- [ ] `COMPARE-04` Mesh 比较 topology hash、position/normal/UV/color/weight 和 attribute domain。
- [ ] `COMPARE-05` 动画逐关键帧和区间采样,比较 FCurve、pose matrix、constraint 和 evaluated mesh。
- [ ] `COMPARE-06` 图结构比较 node type、socket、default、link、group boundary 和未知节点保留。
- [ ] `COMPARE-07` 图像固定色彩空间/alpha输出 MAE、RMS、P95、坏像素和前景 IoU。
- [ ] `COMPARE-08` 音频先统一 sample rate/channel layout再比较时长、峰值、RMS 和 sample drift。
- [ ] `COMPARE-09` 视频比较帧时间戳、关键帧映射、RGBA frame hash、音视频同步和丢帧。
- [ ] `COMPARE-10` simulation 比较 frame/source/settings/cache hash 与每帧 canonical state。
- [ ] `COMPARE-11` import/export 输出 loss report未知或不可逆语义禁止静默丢弃。
- [ ] `COMPARE-12` UI workflow 比较最终 Main/selection/context而不是像素位置模仿 desktop UI。
## 4. 里程碑依赖
```text
M11-14 跨域故障闭环
-> M12 Asset / Library / IO / Editors
-> M13 Scripting / Security / Server isolation
-> M14 Firefox / WebKit / Device input
-> M15 全域自动盘点
-> M16-M22 按机器差距逐 family 清零
-> M23 完整 Web Blender 候选发布
```
M12 至 M15 是当前确定队列。M16 至 M22 的具体领取顺序由 M15 生成的机器差距决定;文档中的
顺序是依赖上限,不得用它跳过最新 `nextTask`
## 5. M11-14 Render、Compositor、Media 故障收口
- [x] `M11-14A` schema 固定三域 cancellation code、budget code 和资源归零字段。
- [x] `M11-14B` Render 使用真实 `.blend` 验证 open 取消不提交、Worker generation 2 重开、
texture aggregate budget 不替换已加载资源、dispose 回执和非空 WebGL 恢复。
- [x] `M11-14C` Compositor 使用真实 Main graph 验证周期取消、超尺寸分配前阻断、LRU clear、
Worker 重开和 Float32 output hash 恢复。
- [x] `M11-14D` Media 使用真实 H.264 验证 proxy decode 取消、cache budget、clear、会话重建和
RGBA8 payload hash 恢复。
- [x] `M11-14E` Blender 5.2 Eevee 与 Compositor desktop golden、协议负例和 Chromium 三域门
由一个专项命令串联。
本节已由 `npm --prefix web run test:render-compositor-media-recovery``M11-14` status、专项
evidence 和 family ledger 联合收口。M12-04I 完成后,当前唯一 `nextTask``M12-04J`
## 6. M12 Asset、Library、IO 与 Editor
### M12.1 Catalog schema 与迁移
- [x] `M12-01A` 盘点 Blender 5.2 AssetMetaData、catalog、tag、author、license、description 字段;
machine inventory 绑定 8 个源码 hash 和真实 Blender 5.2 RNA覆盖 14 DNA/10 RNA/3 catalog
字段并记录 7 个 Web v1 缺口。本项为 enabling task不改变 parity slice。
- [x] `M12-01B` 生成 desktop catalog v1 fixture 和 canonical JSON锁定 Blender 5.2 生成
3 条 CDF v1 catalog、3 类资产及完整 metadata独立新进程重开后 canonical JSON 精确一致。
- [x] `M12-01C` 定义 Web catalog schema、stable catalog/asset ID 和文本/条目预算schema v2
使用 Blender UUID/path 与 AssetWeakReference identity全部文本预算按 UTF-8 字节执行。
- [x] `M12-01D` 定义 schema v1→v2 前向迁移revision、source/preview/library graph 全部保留,
legacy catalog/asset ID 生成确定性 mapping 和 source/target hash report。
- [x] `M12-01E` 定义新版数据由旧版读取时的只读/阻断策略v2 对 v1 reader 只开放
hash-bound metadata snapshotcatalog/asset/save 写入统一阻断,未来 schema 完全拒绝。
- [x] `M12-01F` 重复 ID、循环父 catalog、超长文本和未知字段负例9 类 v1/v2 负例均返回
稳定 code失败后原始小正例继续通过。
- [x] `M12-01G` IndexedDB migration 失败保持旧 transaction 和旧索引;两个真实 abort 点重开后
v1 index、无关 migration 行、数据库版本均不变,成功重试才原子提交 v2 index 与 receipt。
- [x] `M12-01H` 页面/Worker 重启后 catalog 顺序和 asset identity 不变初始页面、reload、两个
顺序启动的独立 Worker 返回相同 catalog/asset order、weak-reference identity 和 manifest hash。
- [x] `M12-01I` schema fixture、migration report 和 hash 写入 M12-01 evidence聚合 checker
递归核对 A-H 任务链、desktop/Web fixture、canonical 双 hash、runtime fault/restart artifact。
### M12.2 Preview 与 content-addressed asset
- [x] `M12-02A` 盘点 Blender preview image 尺寸、颜色空间和无 preview 状态;锁定 DNA/RNA、
ICON/PREVIEW 双槽、RGBA8 premultiplied alpha、无颜色空间字段及 3 个 null preview runtime 状态。
- [x] `M12-02B` 定义 preview source/content SHA-256、MIME、width/height 和生成器 identity
Blender 5.2 确定性重编码绑定 executable/script/settingssource/content 独立且像素 exact。
- [x] `M12-02C` 解码前检查 MIME、像素、字节和压缩比预算schema 1 在任何 decoder/
decoded allocation 前完成 identity、16 MiB encoded bytes、SHA-256、PNG/WebP header、尺寸、
16,777,216 像素、64 MiB RGBA8 和 100:1 compression ratio 门8 类负例后小正例仍可恢复。
- [x] `M12-02D` preview 写 OPFS 后才提交 catalog 引用production coordinator 先写入并回读复验
content-addressed payload再以一次 revision/full-row guarded IndexedDB transaction 提交 v2
preview reference 与 hash receipt三处 fault 均不发布引用,重开后 revision 8/payload 复验通过。
- [x] `M12-02E` 相同内容去重,不同 metadata 不覆盖 payload两个不同 asset/identity 在 revision
8/9 绑定同一 content hash第二次真实 OPFS write 去重且目录只有一份 payload两条完整
non-preview metadata 不变,重开后 head/reference/hash 一致。
- [x] `M12-02F` 损坏 preview 隔离且 asset metadata 仍可读取;同长度 hash drift 从 content-addressed
namespace 移入已复验 quarantine 文件catalog revision/reference/两条 metadata 均不变;页面重开
从 hash-bound receipt 返回 `QUARANTINED` 和完整 asset metadata不再发布 bytes。
- [x] `M12-02G` 删除引用只回收无其他项目引用的 payload同项目首删在仍有一个 reference 时
RETAINED末删才 COLLECTEDOPFS project namespace 使另一项目的同 hash payload/reference
保持 READYrevision 11 重开后两条 metadata 不变。
- [x] `M12-02H` desktop/browser preview 指标与主线程/Offscreen 显示通过Blender 5.2
确定性 RGBA8 reference 与两条 Canvas2D 路径逐字节一致,错误像素由同一指标门阻断。
### M12.3 Append、Link、Override 语义
- [x] `M12-03A` 分别盘点 append、link、library override 涉及的数据块和依赖闭包;冻结 36 类
selectable root、2 类 append-only、direct/indirect/transitive/embedded/override dependency
角色及 17 个 override RNA 字段。
- [x] `M12-03B` 为三种操作定义 source library ID、owner、read-only 和 invalidation token
locator/hash 绑定 sourceoperation/root/owner/generation/revision/closure 共同绑定 token。
- [x] `M12-03C` desktop append 单 Object fixture记录依赖 material/mesh/image stable mapping
Blender 5.2 append 后 Object/Mesh/Material/Image 全部成为可写 local owner保存重开后依赖图、
几何、UV、材质槽和 packed image Float32 像素 hash 保持一致。
- [x] `M12-03D` WASM Main append 以一次 transaction 创建本地 owner 数据Worker 先校验 source
hash、closure 和 revisionBlender Main 递归导入 Object/Mesh/Material/packed Image四个 ID
均验证为可写 local owner并只推进一个 SceneIR revision。
- [x] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致WASM Main 的
append closure 在一次 transaction 后undo 移除全部四个 IDredo 与 save/reopen 均恢复
desktop canonical graph图像像素比较先做 Blender bottom-up row normalizationSceneIR 缺省的
Image.colorSpace 使用 desktop sRGB semantic default若字段出现则必须匹配
- [x] `M12-03F` desktop link fixture 保留 source library 和只读 ownership四个 linked ID 均
保留 `m12_link_source.blend` library pointer、无 library override并在 save/reopen 后保持
`SOURCE_LIBRARY/readOnly=true` stable mapping。
- [x] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`object transform、
mesh geometry/material slot、material property/image node 和 packed image 六类 writer 在
Main 前统一 fail-closedstale revision 仍返回 `REVISION_CONFLICT`
- [x] `M12-03H` library reload 只替换匹配 generation 的 linked snapshotstale generation/revision、
source substitution、duplicate identity 和 non-adjacent replacement 均 fail-closed。
- [x] `M12-03I` missing library 保留 `MISSING_LIBRARY` placeholder、原始 source locator/SHA、generation、
revision 和 data-block IDs不删除引用stale/source drift 均 fail-closed。
- [x] `M12-03J` desktop override fixture 记录 source reference、`LOCAL_OVERRIDE` owner、hierarchy root
和唯一 property override path并在 save/reopen 后保持稳定。
- [x] `M12-03K` 首个 override writer 只开放 `["m12_override_value"]`,并保持 `LOCAL_OVERRIDE`/
reference-read-only semantics其它 property、owner、identity 和 value 均 fail-closed。
- [x] `M12-03L` override stale source generation/revision、dependency closure、invalidation token 和
identity 在 Main commit 前阻断;只有完整匹配返回 `READY`
- [x] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例均返回稳定阻断码。
- [x] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令,共 9 条实测入口。
### M12.4 Origin、路径与 archive
- [x] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件credential-bearing
URL、undeclared origin、unsafe project path 和 malformed user-file identity 均 fail-closed。
- [x] `M12-04B` 规范化 POSIX/Windows separator、`.``..`、percent encoding 和 Unicode 名称canonical
project paths are `/`-separated, NFC-normalized, percent-decoded once, and idempotent。
- [x] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸HTTPS policy origin
同样拒绝 path/query/fragment、编码控制和 backslash smuggling不把不安全声明静默降成裸 origin。
- [x] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。
- [x] `M12-04E` archive 先读取 central directory/manifest不先解压 payload。
- [x] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。
- [x] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。
- [x] `M12-04H` 解压取消删除 staging不修改已提交项目真实临时目录验证零 staging、零发布与
committed revision/SHA-256 不变,原子 commit 开始后不再伪报取消。
- [x] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复;资源故障只在 staging 清理和
committed identity 复核通过后映射为稳定 `STORAGE_QUOTA`/`WASM_OUT_OF_MEMORY`,同一 storage
instance 随后可提交小 archive。
- [x] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归;三类 ZIP 与三类 TAR 二进制样本固定
traversal、compression ratio、duplicate path、symlink escape 和 prefix conflict确定性重建后
只读 central-directory/USTAR metadata 并统一返回 `IO_ARCHIVE_UNSAFE`,不执行解压。
### M12.5 格式 capability matrix
- [x] `M12-05A` 从 pinned Blender 5.2 build/runtime 生成 GLTF/GLB、OBJ、STL、PLY、USD、Alembic
清单;每条 receipt 绑定 operator RNA、build option、runtime metadata 与 binary SHA-256USD/
Alembic 在 disabled build 上明确 `OPERATOR_UNREGISTERED`
- [x] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持;
只有已有 bounded GLB local export route 为 READY其余 27 条 route 均显式 `BLOCKED`,未实现
feature 保持 `UNVERIFIED`
- [x] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行UI registry 绑定
M12-05B matrix SHA-256只接受 `.blend` 项目并仅暴露已有 bounded GLB local export阻断的
import/export route 不进入 `accept` 或搜索结果,文件名 gate 在 Engine 前返回
`IO_FORMAT_UNSUPPORTED`
- [x] `M12-05D` 能力由 runtime receipt 决定不按扩展名推断14 条 M12-05A import/export
receipt 绑定 pinned Blender 5.2 inventory SHA-256App 的 format-tagged operator 与 GLB
export 在执行前都要求 receipt=`AVAILABLE`、operator registered、RNA identity 存在且 build
option 未禁用USD/Alembic `OPERATOR_UNREGISTERED` 保持 `IO_FORMAT_UNSUPPORTED`
- [x] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash14 条 receipt 同时绑定 M12-05D
receipt-set SHA-256 和 M12-05A inventory SHA-256source/operator、canonical settings/RNA
properties、Blender runtime identity 三个 hash 在使用前均要求存在且格式正确。
- [x] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝M12-05E bound receipt set 的父级
byte hash、canonical receipt-set hash 和 pinned Blender runtime hash 在 GLB/operator route
使用前逐项核对,三类负例均 fail-closed。
### M12.6 GLB round-trip
- [x] `M12-06A` pinned Blender 5.2 desktop 生成五个独立 Mesh/PBR/UV/skin/animation GLB fixture
每个文件均无 extension、在 512 KiB 单文件预算内manifest 绑定 runtime、generator、report
和五个 GLB SHA-256独立临时目录二次生成逐字节一致。
- [x] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations生产
TypeScript parser、Node unit 与 Chromium Worker 对五个 desktop fixture 做 exact canonical hash
和字段级比较GLB file-picker route 在 Main persistence 完成前继续阻断。
- [x] `M12-06C` desktop GLB import 后生成 authoritative Main `.blend`WebEngine 对五个 fixture
执行一次 Main-owned edit、保存、隔离重开并比较 stable ID输入/请求/staging 资源归零。
- [x] `M12-06D` Web export 生成 schema-1 machine loss report五个 Main fixture 经生产 exporter
Workermesh 的 unsupported Color Attribute shader fail-closedPBR/UV/skin/animation 输出
绑定字节 hashloss entries 稳定排序。
- [x] `M12-06E` pinned Blender 5.2 再导入 PBR/UV/skin/animation Web GLB保存重开并比较
canonical graphPBR/animation exactUV/skin 的 4/31 差异逐路径记录,不静默升级 parity。
- [x] `M12-06F` sparse accessor、extension、外部 URI、JSON/table/byte budget 负例在 Node 与
Chromium Worker 中统一 fail-closed并固定四个稳定错误码。
- [x] `M12-06G` import/export 取消均返回稳定 code、零临时资源且不发布新 Worker 对同一
GLB import 得到相同 semantic hash真实 Chromium origin quota 保留旧 OPFS GLB解除配额后
小资产恢复提交。
### M12.7 OBJ、STL、PLY round-trip
- [x] `M12-07A` pinned Blender 5.2 `wm.obj_export` 生成单 Mesh OBJ/MTL 正例;四个 position、
四个 UV、一个 normal、两个 triangle face、两个 material 和两个 material group 的 canonical
report 在新临时目录逐字节重生成一致。
- [x] `M12-07B` pinned Blender 5.2 生成双对象 OBJ/MTL/PNG 正例,并从同一正例派生负索引和
两顶点坏 facecanonical report 固定相对 `map_Kd` origin、2 objects/2 faces/2 materials
负例分别为 `ACCEPT_WITH_NEGATIVE_INDICES``OBJ_FACE_ARITY_INVALID`,新目录重生成逐字节一致。
- [x] `M12-07C` bounded Web OBJ parser/serializer 在 Chromium Worker 读取 M12-07B fixture绑定
texture 时 loss 为 0、缺失 texture 时返回两个稳定 warningWeb OBJ/MTL 写入临时目录后由
pinned Blender 5.2 `wm.obj_import` 重开2 objects/2 triangles/UVMap/material 逐项一致。
- [x] `M12-07C` OBJ Web→desktop round-trip 与 loss report。
- [x] `M12-07D` pinned Blender 5.2 `wm.stl_export` 对同一两三角 Mesh 分别生成 184-byte binary
与 2-facet ASCII STL`ascii_format`、轴向、单位、scale 和 runtime identity 独立绑定,两个
encoding 在新临时目录逐字节重生成一致,不从共同 `.stl` 扩展名推断 variant。
- [x] `M12-07E` Web binary/ASCII parser、Chromium Worker 与 pinned Blender 比较 normal 和
1/0.001 unit scale双方均移除一个 degenerate triangle 并保留另一个。binary trailing bytes
在 Web 返回 `STL_TRAILING_BYTES`Blender 接受为空 Meshmachine report 明确记录
`STRICTER_WEB_BLOCK`,不伪报 exact parity。
- [x] `M12-07F` Web Worker 将 bounded STL binary 重写后交给 pinned Blender 5.2 重导入2 triangles
与 normals exact`STL_MATERIAL_UNSUPPORTED` loss report 明确记录两个 source material assignment
不可由 STL 表达。
- [x] `M12-07G` pinned Blender 5.2 `wm.ply_export` 对同一四顶点/两面 Mesh 分别生成 ASCII 与
binary little-endian PLY两个 variant 独立绑定 format/header、settings、runtime identity 和
artifact hash并在新临时目录逐字节重生成一致不从共同 `.ply` 扩展名推断 encoding。
- [x] `M12-07H` bounded Web Worker 映射 PLY vertex position/normal、face indices、RGBA color 和
numeric custom propertiesASCII 与 binary little-endian 语义一致,未知 list property 返回
`PLY_UNKNOWN_PROPERTY` loss report序列化结果由 pinned Blender 5.2 重导入验证。
- [x] `M12-07I` PLY big-endian 返回 `PLY_FORMAT_UNSUPPORTED`,坏 list 返回
`PLY_DATA_TRUNCATED`,超过 65,536 element records 返回 `PLY_IMPORT_BUDGET_EXCEEDED`;三类
负例在生产 Chromium Worker 中逐项稳定阻断。
- [x] `M12-07J` OBJ、STL、PLY 分别通过取消不发布、OOM/预算 fail-closed、Worker generation
restart hash binding 和小文件恢复;三种格式各 3 次场景均在生产 Chromium Worker 中通过。
### M12.8 USD 与 Alembic
- [ ] `M12-08A` 固定有 USD/Alembic 的 Blender 5.2 server runtime identity。
- [ ] `M12-08B` 本地 runtime 缺能力时只能路由 `SERVER_EXACT`,不能静默降级 GLB。
- [ ] `M12-08C` USD fixture 覆盖 hierarchy、xform、mesh、material、camera、animation。
- [ ] `M12-08D` Alembic fixture 覆盖 topology sample、transform sample 和 frame range。
- [ ] `M12-08E` server request 绑定 source/settings/build hash。
- [ ] `M12-08F` server output 由新 Blender 进程重开并导出 canonical report。
- [ ] `M12-08G` server cancel/timeout/OOM 不产生可绑定 output。
- [ ] `M12-08H` 外部引用和不可映射语义进入 loss report。
### M12.9 统一 Editor context
- [ ] `M12-09A` 冻结 Workspace/Area/Region/Editor/Mode/Tool context schema。
- [ ] `M12-09B` View3D、Outliner、Properties 读取同一 selection revision。
- [ ] `M12-09C` 任一 editor selection 更新只提交一次共享 store transaction。
- [ ] `M12-09D` stale editor event 不覆盖较新 Main/selection revision。
- [ ] `M12-09E` hidden/locked/library object 在三 editor 中保持相同可选规则。
- [ ] `M12-09F` frame、active object、mode 和 tool 在 Worker restart 后恢复。
- [ ] `M12-09G` editor dispose 移除 DOM listener、pending request、GPU resource 和 timer。
- [ ] `M12-09H` workspace/layout 保存只改 UI revision不改 Blender scene revision。
### M12.10 专用 Editor 逐项开放
- [ ] `M12-10A` UV Editor 先只读显示 Main UV 与 selection。
- [ ] `M12-10B` UV 单个 writer 通过 desktop/Main/undo/save 后开放。
- [ ] `M12-10C` Shader Node Editor 只显示完整图unsupported node 不可编辑但不得丢失。
- [ ] `M12-10D` Geometry Node Editor 只开放已有 Main writer 的 allowlist node。
- [ ] `M12-10E` Graph Editor 显示 FCurve/key/handle单 operator 单独验收。
- [ ] `M12-10F` Dope Sheet 与 NLA 共享 frame/selection/action revision。
- [ ] `M12-10G` Sequencer editor 只开放已完成 Main writer 的 strip operation。
- [ ] `M12-10H` Compositor editor 保留 unsupported graph 并显示执行 gate。
- [ ] `M12-10I` Spreadsheet 只读取有预算的 evaluated attributes。
- [ ] `M12-10J` 每个 editor 各有 context、dispose、键盘和窄屏 E2E。
### M12.11 Operator、keymap 与 workflow
- [ ] `M12-11A` operator search 直接由 capability registry 生成。
- [ ] `M12-11B` registry、UI、协议和 Main writer 不维护第二份成功列表。
- [ ] `M12-11C` operator poll 使用当前 area/region/mode/selection/tool context。
- [ ] `M12-11D` keymap 按 editor→modal→tool→global 确定性解析。
- [ ] `M12-11E` 冲突显示来源和胜出规则,不随机采用注册顺序。
- [ ] `M12-11F` Escape、Enter、undo、redo 在所有 modal tool 保持一致。
- [ ] `M12-11G` 建模 workflowimport→select→edit→undo/redo→save/reopen→export。
- [ ] `M12-11H` 动画 workflowkey→curve→NLA→frame seek→save/reopen→export。
- [ ] `M12-11I` asset workflowcatalog→append/link→override→reload→package。
- [ ] `M12-11J` 三 workflow 各覆盖取消、Worker crash、quota 和诊断导出。
M12 退出条件所有上述任务有独立报告Asset/IO/Editor family 仍有未盘点项时不得宣称完整。
## 7. M13 Scripting、安全与 Server
### M13.1 默认拒绝与只读盘点
- [x] `M13-01A` pinned Blender 5.2 盘点 Text、Python Console、autorun、driver expression、
handler 和 add-on 入口;报告固定 3 Text、3 Console operator、1 autorun request、1 driver、
39 handler groups、4 add-on operators并把 Web policy 固定为 metadata-only/deny。
- [x] `M13-01B` `.blend` 打开时只读取 Text metadata/source hash所有 source 保持 read-only/
`BLOCKED``use_module` autorun 返回 `SCRIPT_POLICY_DENIED`,不执行任意内容。
- [x] `M13-01C` autorun/register/install/driver execution 默认返回稳定
`SCRIPT_POLICY_DENIED``DRIVER_EXECUTION_BLOCKED``ADDON_INSTALL_BLOCKED`;批准签名仍因
缺 sandbox 返回 `SCRIPT_SANDBOX_UNAVAILABLE`
- [x] `M13-01D` 生产 app/Worker/protocol 176 个 TypeScript 文件无 `eval(``new Function(`
UI 只消费声明的 policy entry points不提供直接 eval 入口。
- [x] `M13-01E` `script_scene.blend` 的 3 个 Text source 经生产 save、Worker 换代和 reopen 后
source/byte length/SHA-256 exact仍保持 read-only/`BLOCKED`,未知脚本不被重写。
- [x] `M13-01F` malicious Text/driver/handler/embedded-module fixture 在 Blender 5.2 生成并由
Chromium 打开验证4 个 source 全部 read-only/`BLOCKED`embedded module 返回
`SCRIPT_POLICY_DENIED`
- [ ] `M13-01D` UI 不提供绕过协议直接 eval 的入口。
- [ ] `M13-01E` 保存/重开保留原 Text 数据块和未知脚本,不重写源码。
- [ ] `M13-01F` malicious text、driver、handler 和 embedded module fixture 进入负例。
### M13.2 Script manifest 与签名
- [x] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission每个脚本声明
`sourceByteLength`/`module=false`,总源码字节、脚本/依赖/权限数量和项目内 canonical path 均由
production parser fail-closed 校验,并有 Node/Chromium 正负例与 artifact-bound report。
- [x] `M13-02B` canonical serialization 固定签名输入;生产协议导出唯一 canonicalize/serialize
入口,按 locale-independent code-unit 排序 script/permission/dependency移除未知字段并固定
canonical JSONNode/Chromium order-invariance、security-field mutation 和 schema rejection 均通过。
- [x] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy版本化
trust policy 绑定 ED25519 public key、publisher、active/revoked、validity/revocation timestamp、
same-publisher acyclic rotation chain 与 bounded clock skewresolver 只返回
`cryptographicVerification=REQUIRED` 的后续验签资格,不启用脚本。
- [x] `M13-02D` signature 只批准 canonical 声明内容ED25519 验签成功source hash 或 signature 变化返回 `SCRIPT_SIGNATURE_INVALID`revoked key 返回 `SCRIPT_POLICY_DENIED`
- [x] `M13-02E` permission 默认最小化;无显式请求时授予空集,只有 manifest 已声明且请求的 permission 才能授予,未知/重复/未声明请求返回 `SCRIPT_POLICY_DENIED`
- [x] `M13-02F` replay、key confusion、过期、not-yet-valid、publisher mismatch 和跨脚本 signature swap 负例稳定阻断;不新增执行入口。
- [x] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。
### M13.3 浏览器 sandbox
- [x] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络scope schema 对五项能力逐项要求 `false`,执行仍为 `DISABLED`
- [x] `M13-03B` sandbox 固定 CPU、wall-time、memory、message 和 output-byte budgets五项超限均返回 `SCRIPT_BUDGET_EXCEEDED`,执行仍为 `DISABLED`
- [x] `M13-03C` host call 仅允许 `READ_MAIN``READ_ASSET``WRITE_MAIN``WRITE_ASSET``SUBMIT_SERVER_JOB`,调用权限与 manifest 声明绑定,参数结构和项目路径严格校验,解析结果仍为 `execution=DISABLED`
- [x] `M13-03C` host call 使用显式 allowlist 和结构化参数。
- [x] `M13-03D` sandbox crash/timeout 终止当前 job不污染 Main revision真实 Chromium Worker crash/timeout 均无发布结果Main revision 保持不变,迟到结果返回 `SCRIPT_SANDBOX_LATE_RESULT`
- [x] `M13-03E` cancellation receipt 绑定 job/generation/base revision取消后的 message、host-call
result 和 cache write 均在 publish 前被 gate真实 Chromium 迟到窗口 `lateMessages=0/cacheWrites=0`
- [x] `M13-03F` dispose 后 Worker、两端 MessagePort、timer、AbortController、transferable buffer、
pending request 和 cache reference 逐项归零;重复 dispose 幂等,迟到 timer message 为 0。
- [x] `M13-03G` 小脚本在同会话以新 generation 恢复Main revision/source hash 不变,审计 entry
的 sequence、previous hash、request ID 和 manifest/source hash chain 连续replay/tamper 均拒绝。
### M13.4 Blender server job isolation
- [x] `M13-04A` 每个 job 创建不可预测的 `0700` 一次性目录request ID 不进入目录名,清理无残留且幂等。
- [x] `M13-04B` source 只读挂载(目录/文件 `0555/0444`output 写独立目录(`0700`),真实 source write 返回 `EACCES`
- [x] `M13-04C` 固定 CPU、内存、进程、文件、wall-time 和 output budget六类超限稳定返回 `SERVER_JOB_BUDGET_EXCEEDED`OS/container 实际强制留给后续真实 process 证据。
- [x] `M13-04D` 默认无网络;声明 origin 使用单独 policymissing/undeclared/unsafe origin 稳定拒绝。
- [x] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动pinned 5.2.0 runtime receipt 通过。
- [ ] `M13-04F` stdout/stderr 截断并过滤凭据/绝对内部路径。
- [ ] `M13-04G` cancel 终止 Blender process tree 并清理临时目录。
- [ ] `M13-04H` timeout/OOM/exit signal 转换为稳定错误码。
- [ ] `M13-04I` result 验证 source/settings/build/output hash 后才进入 OPFS。
- [ ] `M13-04J` 同一 request 重试保持幂等,不绑定两个冲突结果。
### M13.5 CSP、供应链与恶意输入
- [x] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。
- [x] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。
- [x] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。
- [x] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。
- [x] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。
- [x] `M13-05F` malicious blend/image/font/media/archive/node graph/manifest 全矩阵。
- [ ] `M13-05G` fuzz crash 先保存最小样本,再修复,再进入长期回归。
- [ ] `M13-05H` audit record 使用严格时间顺序、request ID 和防篡改 hash chain。
M13 退出条件:任意 Python/add-on 能力只有在本地 sandbox 或 server isolation 实跑后才可从
`BLOCKED` 改为成功;仅签名验证通过不等于执行安全。
## 8. M14 跨浏览器与设备
### M14.1 Capability probe
- [ ] `M14-01A` 冻结当前 Chromium release/engine/archive hash。
- [ ] `M14-01B` Firefox probeWASM、Worker、OPFS、IndexedDB、WebGL2、WebGPU、Offscreen、isolation。
- [ ] `M14-01C` WebKit probe采用相同字段与错误码。
- [ ] `M14-01D` probe 记录浏览器/OS/GPU adapter不按 user-agent 猜测能力。
- [ ] `M14-01E` 缺 WebGPU 只阻断依赖 WebGPU 的功能,不影响可验证 WebGL2/Main。
### M14.2 Firefox永久跳过
- [x] `M14-02A`-`M14-02G` 永久跳过Chromium-only 铁律。
### M14.3 WebKit永久跳过
- [x] `M14-03A`-`M14-03G` 永久跳过Chromium-only 铁律。
### M14.4 设备档位与输入
- [x] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。
- [x] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。
- [x] `M14-04C` pointer mouse、touch、pen 分开记录 pressure/tilt/button/cancel。
- [ ] `M14-04D` IME composition 不触发未完成 operator。
- [ ] `M14-04E` US、非 US、dead key 和 modifier keymap fixture。
- [ ] `M14-04F` 触控 modal cancel、双指导航和笔 stroke 只提交一次 Main transaction。
- [ ] `M14-04G` 1440x900、1280x720、平板、手机无重叠/溢出。
- [ ] `M14-04H` keyboard-only、screen reader name/role 和 focus restore。
## 9. M15 全域自动盘点
- [ ] `M15-01A` 从 Blender 5.2 source/RNA 生成 data-block type 清单。
- [ ] `M15-01B` 生成 operator idname、poll context 和主要 property 清单。
- [ ] `M15-01C` 生成 modifier、constraint、shader/GN/compositor node 清单。
- [ ] `M15-01D` 生成 sequencer strip/effect、physics family 和 import/export 清单。
- [ ] `M15-01E` 生成 editor/space/region/workspace/keymap 清单。
- [ ] `M15-01F` 为 Main/Scene/Mesh/Depsgraph 和其余现有 V1 core slice 生成同级全量清单,
不把“当前范围完成”映射为全量完成。
- [ ] `M15-02A` 将每项映射到 owner family、implementation class、tests 和 evidence。
- [ ] `M15-02B` 找出未映射、重复映射、只有 summary、只有 proxy 和只有 route 的条目。
- [ ] `M15-02C` `LOCAL_EXACT` 缺 desktop/WASM 同 fixture 时降为 `BLOCKED`
- [ ] `M15-02D` `LOCAL_EQUIVALENT` 缺 Main/save 证据时降为 `BLOCKED`
- [ ] `M15-02E` `SERVER_EXACT` 缺真实 job/cancel/isolation/result binding 时降为 `BLOCKED`
- [ ] `M15-02F` unknown data 保存破坏原文件时对应 reader/writer family 降为 `BLOCKED`
- [ ] `M15-03A` 为每个 gap 生成一个最小 `nextTask`,不生成“完成整个 family”大任务。
- [ ] `M15-03B` nextTask 包含 fixture、desktop command、Web command、比较器和退出条件。
- [ ] `M15-03C` 依赖图无缺失、无环、每次只允许一个 active task。
- [ ] `M15-03D` 机器统计分别输出 inventoried/completed/blocked/uninventoried。
- [ ] `M15-03E` 所有生成的 owner family包括当前 N-015 至 N-026分别达到零 gap禁止由
12-family 或 release 聚合 gate 反推未纳入当前账本的 core family 已完成。
## 10. M16-M22 全域差距清零波次
这些里程碑只定义 owner 和顺序。具体任务由 M15 生成,每个任务仍使用第 3 节的 14 步模板。
### M16 Main、Mesh、Modifier、Sculpt
- [ ] `M16-01` `.blend` reader/writer unknown-data preservation 差距清零。
- [ ] `M16-02` Object/Collection/parent/transform operator 差距清零。
- [ ] `M16-03` Mesh create/delete/select/topology/attribute operator 差距清零。
- [ ] `M16-04` UV/normal/data-transfer operator 差距清零。
- [ ] `M16-05` Modifier family 与 Depsgraph 求值差距清零。
- [ ] `M16-06` PBVH Sculpt brush/mask/face-set 差距清零。
### M17 Rigging、Animation、Constraint、NLA
- [ ] `M17-01` Armature edit/pose/bone collection 差距清零。
- [ ] `M17-02` Constraint family 与 dependency order 差距清零。
- [ ] `M17-03` Shape key、driver 安全执行和 property animation 差距清零。
- [ ] `M17-04` FCurve/key/handle/modifier/extrapolation 差距清零。
- [ ] `M17-05` NLA track/strip/blend/transition/meta/time 差距清零。
- [ ] `M17-06` animation export/import loss 差距清零。
### M18 Non-mesh、Grease Pencil、Paint
- [ ] `M18-01` Curve/Surface/Text/Metaball 完整 data/operator 差距清零。
- [ ] `M18-02` Curves/Hair/PointCloud/Volume data/operator 差距清零。
- [ ] `M18-03` Grease Pencil layer/frame/drawing/material/modifier/editor 差距清零。
- [ ] `M18-04` Vertex/Weight/Texture Paint 与 PBVH/UDIM 差距清零。
- [ ] `M18-05` desktop/browser/export golden 差距清零。
### M19 Geometry Nodes、Shader、Simulation
- [ ] `M19-01` Geometry Nodes node/socket/field/domain/group 清单差距清零。
- [ ] `M19-02` Geometry Nodes external resource/instance/lazy-function 差距清零。
- [ ] `M19-03` Shader node/texture/color/sampler/compiler 差距清零。
- [ ] `M19-04` Rigid/Soft/Cloth/Fluid/Dynamic Paint/Particle/Hair simulation 差距清零。
- [ ] `M19-05` local/server bake、cache、playback 和 fault 差距清零。
### M20 Render、Compositor、Sequencer、Tracking
- [ ] `M20-01` Camera/Light/World/Color Management 完整字段差距清零。
- [ ] `M20-02` Eevee/WebGPU/Cycles/server render 差距清零。
- [ ] `M20-03` Compositor node/resource/HDR/tile/server 差距清零。
- [ ] `M20-04` Sequencer strip/effect/modifier/proxy/mix/encode 差距清零。
- [ ] `M20-05` Audio device/mix/sync/waveform 差距清零。
- [ ] `M20-06` Tracking/Mask editor/solve/compositor binding 差距清零。
### M21 Asset、IO、Editors、Scripting
- [ ] `M21-01` Asset/Library/Override 全清单差距清零。
- [ ] `M21-02` Blender 启用的 import/export format 差距清零。
- [ ] `M21-03` Editor/context/operator/keymap/workspace 差距清零。
- [ ] `M21-04` Python/Text/Add-on/driver 的 local/server 安全能力差距清零。
- [ ] `M21-05` help/i18n/accessibility/preferences 差距清零。
### M22 平台与跨浏览器
- [ ] `M22-01` Chromium 全矩阵零 gap。
- [ ] `M22-02` Firefox 声明矩阵零 gap。
- [ ] `M22-03` WebKit 声明矩阵零 gap。
- [ ] `M22-04` touch/pen/HiDPI/IME/keymap 零 gap。
- [ ] `M22-05` local/server platform capability 零未声明项。
## 11. M23 完整候选发布
- [ ] `M23-01` clean checkout 重建 WASM、Web app、server image 和所有 fixture。
- [ ] `M23-02` 运行全部 desktop/WASM semantic golden。
- [ ] `M23-03` 运行 Chromium/Firefox/WebKit quick、P0、full。
- [ ] `M23-04` 运行 geometry/texture/volume/simulation/media 性能矩阵。
- [ ] `M23-05` 运行 OOM/quota/network/device/Worker/server fault 矩阵。
- [ ] `M23-06` 运行 malicious/fuzz/CSP/dependency/security 矩阵。
- [ ] `M23-07` inventory 中每个 `parityId` 均为 `VERIFIED`,所有生成的 owner family 均为
`COMPLETE`blocked/in-progress/uninventoried 均为零。
- [ ] `M23-08` binary/source/server archives 在独立机器复验。
- [ ] `M23-09` SBOM、license、source offer、runtime 和 archive hash 联合校验。
- [ ] `M23-10` 发布说明逐项列出 local/server 实现,不隐去架构差异。
- [ ] `M23-11` 从空环境部署、升级、回滚并恢复真实项目。
- [ ] `M23-12` 只有上述报告绑定同一 commit 后才允许“完整 Web Blender”声明。
## 12. 每轮执行规则
1. 从机器队列领取唯一 `nextTask`
2. 浏览器测试只允许 ChromiumFirefox/WebKit 任务一律跳过,不得启动、探测或生成证据。
3. 先运行现有正例,确认不是在旧失败上继续扩展。
4. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。
5. 先完成协议和 fail-closed再接 Main writer再接浏览器 UI。
6. 失败结果记录到工作日志,但不写成成功 evidence。
7. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。
8. 更新 ledger 时只增加本轮真实完成的 slicefamily 全域状态保持 `BLOCKED` 直到 gap 为零。
9. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。

View File

@@ -2,8 +2,12 @@
更新时间2026-08-13 更新时间2026-08-13
> 覆盖参考(归档):本文不作为当前任务入口;当前执行只认 `EXECUTION_QUEUE.md` 与 parent manifest。
本文件以仓库内 `blender-5.2.0/source/blender/` 为基线,记录 Blender 功能域在 Web 本文件以仓库内 `blender-5.2.0/source/blender/` 为基线,记录 Blender 功能域在 Web
项目中的迁移方式和唯一领取顺序。`completed_current_scope` 只表示声明子集通过验收, 项目中的迁移方式和当前差距。M12-M23 的唯一长期实施规则和原子任务位于
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md`;本文不再定义领取顺序。`completed_current_scope`
只表示声明子集通过验收,
不表示桌面 Blender 全量等价。任何未列入白名单的 operator、node、strip、physics 不表示桌面 Blender 全量等价。任何未列入白名单的 operator、node、strip、physics
类型或数据块都必须返回机器可读阻断,不能由 Three.js 静默近似。 类型或数据块都必须返回机器可读阻断,不能由 Three.js 静默近似。
@@ -102,8 +106,8 @@ UI 任务可以提前制作只读视图,但不得在对应 Main writer 和保
1M binary gate、Worker restart 与 Chromium 真实 OPFS quota 已接入。VDB 转换请求、NanoVDB 1M binary gate、Worker restart 与 Chromium 真实 OPFS quota 已接入。VDB 转换请求、NanoVDB
grid/material/chunk manifest、逐块 hash/range streamer、分阶段能力门、14 条真实资源 catalog grid/material/chunk manifest、逐块 hash/range streamer、分阶段能力门、14 条真实资源 catalog
和 OpenVDB 13 -> NanoVDB 32 desktop converter 已完成server job、HTTP body 断点续传、OPFS、 和 OpenVDB 13 -> NanoVDB 32 desktop converter 已完成server job、HTTP body 断点续传、OPFS、
有界 Float32 WebGPU 双视口和 Main Volume 属性保存重开已有专项证据。GPU resident paging、 有界 Float32 WebGPU 双视口、有限 grid 材质、显式 GPU resident paging/LRU 和 Main Volume 属性保存重开已有专项证据。
联合重开、完整材质、大 bundle/OOM 和 golden 仍 `BLOCKED`。后续浏览器验收仅覆盖 自动缺页重绘、联合重开、大 bundle 联合故障和 golden 仍 `BLOCKED`。后续浏览器验收仅覆盖
Chromium不配置 Firefox/WebKit。 Chromium不配置 Firefox/WebKit。
专项字段、验收命令和停止条件见 `docs/status/N-015.md` 专项字段、验收命令和停止条件见 `docs/status/N-015.md`
@@ -206,17 +210,20 @@ operator search这不等价于 Blender 全量 operator registry、context men
## N-026 全域发布门 ## N-026 全域发布门
1. 生成 machine-readable parity manifest:每个 Blender family 为 1. 当前 V1 machine-readable manifest 保留 `LOCAL_EXACT``LOCAL_BOUNDED``SERVER`
`LOCAL_EXACT``LOCAL_BOUNDED``SERVER``BLOCKED` `BLOCKED`;完整对标 inventory 按新计划转换为独立 `implementationClass/parityStatus`
2. 全部正例和阻断 fixture 在 Chromium 主线程/OffscreenCanvas、离线包和 Worker restart 下运行。 2. 全部正例和阻断 fixture 在 Chromium 主线程/OffscreenCanvas、离线包和 Worker restart 下运行。
3. 100k/1M/10M 几何、4k/8k texture、100/1000 frame、长媒体和 simulation cache 基准。 3. 100k/1M/10M 几何、4k/8k texture、100/1000 frame、长媒体和 simulation cache 基准。
4. `.blend`/image/media/script fuzz、OPFS quota、OOM、设备丢失、网络中断和恢复验证。 4. `.blend`/image/media/script fuzz、OPFS quota、OOM、设备丢失、网络中断和恢复验证。
5. 许可证/source offer/SBOM、确定性包、文档状态与测试报告一致后才能发布。 5. 许可证/source offer/SBOM、确定性包、文档状态与测试报告一致后才能发布。
当前证据为 13 条有效记录、15 个缺失,整体仍为 `BLOCKED`;新增 VDB 边界记录只证明 当前证据为 17 条有效记录、0 个 V1 缺失V1 release gate 为 `READY`Blender 全域 parity 仍为 `BLOCKED`。账本使用
转换/manifest/range 协议与显式阻断;已通过 1M geometry、 `parityStatus` 保留 N-015 至 N-026 的全域差距,并以独立 `releaseStatus` 检查 V1 声明切片;
600 帧 OPFS simulation cache、4K/8K texture、运行中断网和主线程 WebGL device loss VDB 专项记录已证明真实
仍缺 10M geometry、长媒体、OOM 及 N-016 至 N-025 上游 family 的完整验收。 desktop/server converter、HTTP/OPFS、Float32 WebGPU 双生产视口与 Main 属性重开当前切片;已通过 1M/10M geometry、
1M 帧有界 long media、600 帧 OPFS simulation cache、4K/8K texture、运行中断网和主线程
WebGL device loss以及 WASM/Main、OPFS、GPU、NanoVDB 四类确定性 OOM 恢复N-015 至
N-025 上游 family 的完整验收仍属于全域 parity 差距。
## 默认验收入口 ## 默认验收入口
@@ -231,5 +238,6 @@ npm --prefix web run lint
npm --prefix web test npm --prefix web test
npm --prefix web run test:authoring-roundtrip npm --prefix web run test:authoring-roundtrip
npm --prefix web run test:capability-gates npm --prefix web run test:capability-gates
npm --prefix web run test:status-consistency
npm --prefix web run test:e2e npm --prefix web run test:e2e
``` ```

46
docs/CONTEXT_BUDGET.md Normal file
View File

@@ -0,0 +1,46 @@
# 任务上下文预算
完整治理流程见 [`CONTEXT_GOVERNANCE.md`](CONTEXT_GOVERNANCE.md);本页只保留机器需要的预算和索引契约。
本项目把“能否持续执行”定义为可检查的输入预算,而不是依赖执行者自行控制阅读量。每轮领取任务只加载一个最小上下文包:
1. `docs/EXECUTION_QUEUE.md`:当前指针和规则,最多 4 KiB。
2. `docs/tasks/<task>.md`:一个行为的任务卡,最多 8 KiB。
3. `tests/golden/<parent>/manifest.json`:只取 parent、runtime、artifact hash、`nextTask`,最多 24 KiB。
4. `docs/status/<parent>.md`:只取上一任务的证据摘要,最多 6 KiB。
四份输入的估算总量最多 3,500 tokens。实现代码、长日志、完整路线图和 parity ledger 只按任务卡列出的路径追加读取;它们不是默认上下文。
任务上下文生成器在生成 `inputPaths` 时还会前置执行证据筛选:最多 12 个证据路径,证据文件总计最多
8 KiB并从 3,500 token 总预算扣除四份基础文档后使用剩余空间。超限、缺失、越界或不可审计路径不会进入读取清单,
而会写入 `inputSelection.excluded[]`,其中 `reason` 是稳定的机器可读排除原因;治理门会校验所选路径、字节总数和排除审计。
全量 gap 计划 `tests/golden/M15-03A/next-task-plan.json` 约 6.8 MiB只能由盘点/生成门读取,禁止作为任务上下文打开。`task-index.json` 保存源计划 hash 和每条记录的偏移,`task-catalog.jsonl` 保存一行一个任务;`task-context` 只随机读取当前任务的一行。索引缺失、源 hash 漂移或 catalog 损坏会直接失败,不能回退加载整份计划。
## 机器门禁
```bash
npm --prefix web run test:task-context
npm --prefix web run test:context-governance
node tools/web/generate-task-index.mjs
node tools/web/check-task-index.mjs
node tools/web/generate-task-card.mjs --task <task-id>
node tools/web/print-task-context.mjs
node tools/web/print-task-context.mjs --task <task-id>
node tools/web/check-task-context.mjs --task <task-id> --write
node tools/web/check-context-governance.mjs
```
`check-task-context` 会确认队列指针、parent manifest、任务卡/索引、命令入口和预算一致。失败时不能领取任务,也不能手工修改 `nextTask`。索引只由计划生成门更新,执行任务不得手工编辑 catalog 或 offset。
推进到新任务时,若 `docs/tasks/<task>.md` 不存在,先运行 `generate-task-card.mjs --task <task-id>`;它只从 catalog 的一条记录生成一张短卡,生成后再运行 `check-task-context`。禁止为了生成一张卡读取完整 plan。
## 文档分层
- 当前执行:`EXECUTION_QUEUE.md`、当前任务卡、parent `manifest.json`、parent `status`
- 机器索引:`tests/golden/M15-03A/task-index.json``task-catalog.jsonl`;仅由工具读取,不是执行者要通读的文档。
- 稳定契约:`WEB_BLENDER_MODELER_V1_SCOPE.md`、协议/schema、工具入口。
- 背景规划:`CURRENT_EXECUTION_PLAN.md``PROJECT_STATUS_AND_NEXT_WORK.md`、完整 parity/WBS 计划。
- 历史证据:`docs/status/` 其余文件、`tests/golden/` 其余构件、`test-results/`
背景和历史文档不得在任务卡中复制;需要时只引用一个小节或一个具体构件。若文字与机器 manifest 冲突,以命令输出和 manifest 为准。

121
docs/CONTEXT_GOVERNANCE.md Normal file
View File

@@ -0,0 +1,121 @@
# 上下文治理契约
更新时间2026-08-19America/New_York
本契约解决两个问题:执行者不需要读完整路线图就能继续工作;任何任务卡、交接状态或机器索引
变大、漂移或重复指针时,自动门禁会在领取前失败。它是执行规则,不是项目进度表。
## 一、事实分层
每个问题只由一个层级回答,层级之间不互相复制内容:
| 层级 | 唯一职责 | 默认是否读取 |
| --- | --- | --- |
| `EXECUTION_QUEUE.md` | 当前 task、parent manifest、专项命令、全局硬规则 | 是 |
| `tasks/<task>.md` | 一个行为或一个证据变化的输入、边界、验收和回滚 | 是 |
| `tests/golden/<parent>/manifest.json` | parent 状态、artifact hash、运行时、`nextTask` | 是 |
| `docs/status/<parent>.md` | 上一项最小证据摘要和已知风险 | 是 |
| 协议/生产/测试文件 | 任务卡点名的实现事实 | 按需 |
| 长期计划、全量 ledger、历史 status | 背景、审计、归档 | 禁止默认读取 |
冲突优先级固定为:真实命令输出 > manifest/evidence > 任务卡 > 队列文字 > 长期规划和历史日志。
Markdown checkbox 不能覆盖失败命令或 hash 漂移。
## 二、硬预算
预算按 UTF-8 bytes 检查token 用 `ceil(bytes / 4)` 作保守估算。超过任一单项或总预算都不能领取。
| 输入 | 上限 | 约束 |
| --- | ---: | --- |
| 队列 | 4 KiB | 只保留一个当前指针,不保留历史任务表 |
| 任务卡 | 8 KiB | 最多 12 个输入路径、8 条命令;不复制日志 |
| parent manifest | 24 KiB | 只记录必要 artifact最多 32 项 |
| parent status | 6 KiB | 只保留命令、退出码、关键结果、风险和回滚 |
| 四份输入合计 | 3,500 tokens | 由 `contextSizeReport` 和治理门同时检查 |
| catalog 单行 | 2 KiB | 机器随机读取;执行者不得通读 catalog |
生成阶段的证据读取清单另外受 `maxFiles=12``evidenceBytes=8 KiB` 和基础文档扣除后的剩余上下文空间约束。
每个未选路径必须有 `inputSelection.excluded[].reason`,因此校验器不会成为首次发现超大上下文的地方。
全量 `next-task-plan.json`、parity map 和 gap audit 是生成器输入,不是执行上下文。索引保存源
hash 和 catalog offset源 hash、catalog hash 或 offset 损坏时必须重新生成,不能回退读取大计划。
## 三、原子任务粒度
默认“一项任务 = 一个可观察行为或一项独立证据”。以下任一情况出现就拆任务:
- 同时改变 schema、生产消费、浏览器接线和发布证据
- 需要两个互不依赖的 fixture 或两个不同 owner family
- 正例、取消/重复/超限负例无法在同一 focused command 中清楚断言;
- 任务卡需要列出超过 12 个输入或超过 8 条命令;
- 任何一项失败会让另一项仍可安全交付。
复杂能力按适用门拆分,门之间通过 parent manifest 串联:
| 门 | 只交付一件事 | 失败状态 |
| --- | --- | --- |
| C 契约 | schema、稳定 ID、预算、错误边界和 revision 规则 | `in_progress` |
| P 生产 | Main/Worker/App/viewport 的真实消费路径 | `in_progress` |
| N 负例 | 非法、重复、取消、迟到、超限不改变已提交状态 | `in_progress` |
| B 浏览器 | Chromium 真实用户路径和可见结果 | `blocked``in_progress` |
| R 资源 | dispose、重启、save/reopen、OPFS/GPU/内存归零 | `in_progress` |
| E 证据 | focused command、报告、hash manifest、status、回滚 | 通过后才允许下一个 task |
M16 及后续 gap 固定为一 gap 一个 task最小 fixture、desktop/WASM 对比、save/reopen、负例和
manifest 证据必须属于该 gap不要把“完成整个 family”写成一张卡。
## 四、执行生命周期
### 领取
```bash
npm --prefix web run test:context-governance
node tools/web/print-task-context.mjs
node tools/web/check-task-context.mjs
```
只读取打印结果列出的四份文件和任务卡点名的最小实现入口。当前 task 必须等于 parent
manifest 的 `nextTask`;不能从旧 Markdown 编号、完整 plan 或 status 列表推导任务。
### 实施
先确认最小 fixture 和生产入口,再写 focused test/checker。每条验收命令都必须真实运行并记录
退出码协议或测试文件存在不等于生产能力完成。浏览器、CI、验收和发布证据永久仅限 Chromium。
### 成功交接
```bash
node tools/web/check-task-context.mjs --task <task-id> --write
node tools/web/check-context-governance.mjs
```
只提交本任务的实现/测试、fixture、报告、manifest、status 和 `task-context.json`。manifest 必须
绑定所有产物的 SHA-256并且 `nextTask` 是唯一的下一指针;长日志留在构件目录,不复制进 status。
### 失败、阻断和恢复
命令失败、环境缺失、取消、超限、重复、迟到结果或 hash 漂移时保持 `in_progress``blocked`
不得写成功报告、修改 Main revision 或推进 `nextTask`。修复后从同一 parent 重新运行;不要覆盖
parent 证据。任务卡、索引或队列指针不一致时先修复生成证据,再继续实现。
## 五、提交前门禁
提交前至少运行:
```bash
npm --prefix web run test:context-governance
node tools/web/check-task-index.mjs
node tools/web/check-task-context.mjs
git diff --check
```
`check-context-governance` 会检查四份文档预算、任务卡必备章节和失败边界、禁止的长文档引用、
manifest 路径/构件数量、catalog 行边界以及唯一活动 task。门禁失败即停止不通过手工删除输出
或修改 `nextTask` 绕过。
## 六、维护规则
- 新增任务:先更新机器源并运行 `generate-task-index.mjs`,再用 `generate-task-card.mjs --task` 生成短卡;禁止手写 catalog offset。
- 完成任务:先写 manifest/status再用 `check-task-context --write` 生成交接上下文;队列只跟随新 manifest。
- 规则变化:只修改本契约、`CONTEXT_BUDGET.md`、模板和治理测试;长期规划只增加链接,不复制规则。
- 发现重复或过大的历史文档:标记为背景/归档并从默认入口移除,不在任务卡中粘贴摘要。

View File

@@ -0,0 +1,898 @@
# Web Blender V1 当前执行计划
更新时间2026-08-19
> 背景规划文档:不作为当前任务入口,也不覆盖 `EXECUTION_QUEUE.md` 的机器指针。领取任务只读取
> [`CONTEXT_BUDGET.md`](CONTEXT_BUDGET.md) 定义的最小上下文包。
## 1. 交付目标
当前交付物是 `Web Blender Modeler V1`,不是 Blender 5.2 的完整浏览器移植。
V1 产品承诺、明确排除项和发布门以 `WEB_BLENDER_MODELER_V1_SCOPE.md` 为准。
浏览器基线仅为 Chromium。产品架构保持为
```text
React 工作区和编辑器
-> versioned TypeScript protocols
-> Web Workers
-> Blender Main / Depsgraph WASM 子集
-> binary SceneIR / Three.js WebGL 或 OffscreenCanvas viewport
-> OPFS 大文件 + IndexedDB 元数据
```
本计划保留阶段边界和退出条件;领取任务时只运行 `node tools/web/print-task-context.mjs`,仅读取其
输出的队列、任务卡/plan 条目、parent manifest 和 parent status避免加载历史日志。M12-M23 的长期 Blender 全功能实施规范以
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;当前实现状态仍由
`status/parity-ledger.json` 和对应 evidence 给出。长期全域差距不阻断已明确限定范围的 V1。
## 2. 文档与事实源
| 文件 | 唯一职责 |
| --- | --- |
| `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 产品契约、支持矩阵、非目标、发布标准 |
| `EXECUTION_QUEUE.md` + `tasks/<task>.md` | 当前唯一任务、最小上下文、依赖和验收入口 |
| `PROJECT_STATUS_AND_NEXT_WORK.md` | 已实现能力、风险、验证命令总览 |
| `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23 完整 Web 对标的长期规范事实源 |
| `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 功能域说明和当前差距参考 |
| `BLENDER_5_2_FULL_PARITY_WBS.md` | 完整产品覆盖检查表和历史 F 域映射参考 |
| `status/parity-ledger.json` | 功能域和 V1 slice 的机器事实 |
| `status/release-evidence.json` | 最近一次可审计发布证据快照 |
文档冲突时按上表职责解释;机器状态冲突时以校验脚本能够同时验证的 ledger 和 evidence
为准,不靠描述性文字覆盖失败结果。
## 3. 完成规则
### 3.1 最小任务规则
每个任务只产生一个主要行为变化或一个证据变化。任务状态只允许:
- `[ ]` 未开始;
- `[~]` 正在执行;
- `[x]` 已实现且验收通过;
- `[!]` 有可复现阻断,必须附命令、错误和解除条件。
不能仅凭协议存在、fixture 存在、UI 可显示或测试代码已写入,就把任务标为完成。
### 3.2 功能任务完成定义
适用项必须同时具备:
1. 正例使用生产代码路径,不在测试中复制另一套实现。
2. 非法输入、预算超限或未声明能力返回稳定结构化错误码。
3. Worker 可取消;取消后不得发布迟到结果。
4. 资源释放可观测;释放后 CPU/WASM/GPU/OPFS 临时占用回到预算内。
5. Main 或持久化状态修改必须经过 revision验证 undo/redo、原子保存和重开。
6. 浏览器行为至少在 Chromium 生产配置验证;视口相关能力覆盖主线程和
OffscreenCanvas Worker除非 V1 范围明确只承诺其中一个。
7. 独立验收命令退出码为 0并产生至少一个与实现或测试绑定的 SHA-256。
8. 状态文档、ledger、evidence 和代码描述一致。
### 3.3 发布证据规则
`release-evidence.json` 中任何 `true` 字段必须:
- 由成功 evidence record 绑定;
- 记录实际命令、耗时、输出摘要和非空构件 SHA-256
- 失败时不能保留过期成功状态;
- 不得用 mock、跳过、仅 schema 单测或只有 hash 格式校验替代真实运行。
## 4. 重新盘点后的事实基线
### 4.1 已完成工程基础
- React/Three.js 工作区、主线程 WebGL 与 OffscreenCanvas Worker 视口已存在。
- Blender 5.2 Main 子集已编译为 WASMWorker/C ABI、结构化错误和内存统计已存在。
- `.blend` 导入、已声明编辑命令、undo/redo、保存、OPFS/IndexedDB、重启重开已形成闭环。
- 二进制 Mesh/非 Mesh SceneIR、range patch、LOD cache、GLB 和有限 USD 路径已存在。
- VDB desktop/server converter、HTTP range/OPFS、Float32 NanoVDB WebGPU、有限多 grid
材质、显式 resident LRU 和故障恢复已存在。
- 12 个 N-015 至 N-026 功能域的 Blender 全域 `parityStatus` 均为 `BLOCKED`;其 V1
required slice 的 `releaseStatus` 均为 `READY`。这两个状态轴不得再次合并。
### 4.2 2026-08-14 本轮复核结果
| 检查 | 结果 |
| --- | --- |
| `npm --prefix web run typecheck` | 通过 |
| `npm --prefix web run lint` | 通过 |
| `npm --prefix web test` | 9/9 通过 |
| `npm --prefix web run test:status-consistency` | 通过12 parity blocked、0 release blocked |
| `npm --prefix web run test:release-evidence` | 通过17 records、0 missing、`READY` |
### 4.3 2026-08-15 重新盘点结论
这次盘点把“V1 发布切片”和“完整 Blender Web 对标”分开统计,禁止用一个百分比混合两种
完成定义:
| 观察轴 | 已完成 | 未完成/阻断 | 当前判断 |
| --- | --- | --- | --- |
| V1 family release | 12/12 `releaseStatus=READY` | 0 family release blocked | V1 功能切片已实现 |
| Blender 5.2 全域 parity | 0/12 `parityStatus=COMPLETE` | 12/12 `BLOCKED` | 不是完整 Web Blender |
| 发布证据字段 | 17 条成功 record、0 missing49/49 唯一 acceptance 通过 | 无 V1 证据缺口 | release gate 与 acceptance 审计链已闭环 |
| 浏览器 | Chromium 主线程/Offscreen 两条路径 | Firefox、WebKit 永久排除 | Chromium-only 铁律 |
| Blender runtime | Main/Depsgraph/WebEngine 有界 WASM 子集 | 完整窗口系统、GPU 后端、Python/add-on、全量 operator 未移植 | 子集架构,不是桌面二进制直编 |
| 交付 | 离线 binary/source archive、SBOM、SHA-256 可复现 | 正式部署模板、持续发布流水线、跨机器复验待做 | 本地 V1 RC 已收口 |
2026-08-15 重新执行 `typecheck``lint`、14 个 Node tests、status consistency、release
evidence checker 和 `git diff --check`,均退出 0。M6-03 后 acceptance runner 再次解析 12 个
family 的 50 条声明,去重为 49 条命令并全部通过,命令总耗时 480,169 ms清单 SHA-256 为
`d5fb0a7b2df92b076f8b05ccd68903d60e3352b9423b0c1404065f6cbcbdd45c`。官方 Blender 5.2.0
Linux archive SHA-256 为 `96f6c181a30f4950607839dc84d42a354b250d8a0231b098b59b7bc69c351c48`
运行时 `USD=True`desktop USD roundtrip 已通过。
第一次重跑误用了本地 `USD=False` Blender结果为 48/49该失败证据未标 READY。修正为上述
官方 runtime 后完整重跑 49 条,而不是手改失败记录。当前 release evidence 生成时间为
`2026-08-15T05:37:25.215Z`17 条 record 均为 exit code 0、非空输出和非空构件 hash
总命令耗时 553,577 ms。`v1-user-loop` record 已绑定上述 acceptance
清单 hash。
### 4.4 当前阻断与决策顺序
1. `P0`V1 可部署 RC 已完成 M6 71/71M7 核心体验硬化已完成 18/18后续领取点以
`后续工作.txt` 的机器队列记录为准。
2. `P1`binary/source archive 已在独立目录复验quick/Chromium/release 三条
CI lane 与 RC manifest 已联合校验;发布/限制/恢复和运维合同均已进入离线包。
3. `P1`pthread/single-thread、真实 HTTP、缓存升级、归档离线、OPFS 断网和空目录
deploy/upgrade/rollback 均已有可复验证据M7 项目体验硬化已完成并进入持续回归。
4. `P2`:完整 Blender 功能域继续受 VDB depth composition/完整 Volume IO、PBVH、Geometry Nodes evaluator、
通用 Shader、完整编辑器/渲染/媒体和安全脚本边界阻断。
### 4.5 真实发布缺口
当前 V1 release gate 已无 evidence 或 acceptance 缺口,`faults.oom` 由独立 Chromium 四场景
命令和构件 hash 绑定。`geometry1M`、4K/8K texture、600 帧 simulation cache、断网、device
loss、恶意 blend、zip bomb、离线包和 V1 用户闭环均已在最终 M5 全矩阵回归。后续代码变化
必须使 source/tool/package hash 过期并重跑对应证据。Blender 全域 `parityStatus=BLOCKED`
长期差距不因 V1 gate `READY` 而关闭。
## 5. 里程碑总览
| 里程碑 | 交付物 | 状态 | 退出条件 |
| --- | --- | --- | --- |
| M0 范围与状态模型 | V1 契约、双轴 ledger、P0 用户闭环 | 已完成,持续回归 | 状态一致性与 V1 闭环通过 |
| M1 当前工作区收口 | VDB/WebGPU 与现有证据可复验 | 已完成,持续回归 | 静态门、VDB 全矩阵、V1 闭环全部通过 |
| M2 10M geometry | 大几何浏览器性能与恢复证据 | 已完成,持续回归 | `performance.geometry10M=true` |
| M3 long media | 长时间线索引、seek、取消与重启证据 | 已完成,持续回归 | `performance.longMedia=true` |
| M4 OOM | 四类确定性内存故障与恢复证据 | 已完成,持续回归 | `faults.oom=true` |
| M5 V1 RC | 可复现离线候选包、SBOM、校验和 | 已完成,持续回归 | release gate `READY`、49/49 acceptance |
| M6 可部署 RC | 双 WASM 运行时选择、真实 HTTP、离线包、CI、运维合同 | 已完成71/71 | single/pthread 可解释,真实部署与归档 P0 通过 |
| M7 核心体验硬化 | 项目状态机、恢复、存储、输入和可访问性 | 已完成18/18 | 30 分钟编辑/保存/重开 soak 通过 |
| M8 VDB 自动分页 | GPU 缺页反馈、range/OPFS、LRU、双视口恢复 | 已完成20/20 | 联合重开与 desktop/main/Offscreen 三轴 golden 通过 |
| M9 非 Mesh/GP/Paint | 字体、Curve、Grease Pencil、Paint 增量闭环 | 已完成14/14 | 每个新增 writer 独立通过 Main/undo/save/golden三域故障恢复闭环通过 |
| M10 GN/Shader/NLA/Simulation | 白名单求值、cache、编译与阻断 | 已完成15/15 | 四域分别通过 desktop/WASM/fault 门 |
| M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 已完成14/14 | 本地白名单、server 边界与三域故障恢复均可审计 |
| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 进行中M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A-F、M12-06A-G 与 M12-07A-J 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 |
| M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 进行中M13-01A-F、M13-02A-F、M13-03A-G、M13-04A-E 完成,当前 `M13-04F` | Text/Console/autorun/driver/handler/add-on inventory、`.blend` metadata-only open、default-deny policy codes、sandbox budgets/host-call/isolation/recovery、一次性 job 目录、source/output 隔离、六类资源预算、默认拒绝网络和 pinned Blender startup 已冻结stdout/stderr redaction、进程取消、稳定故障码、CSP 和恶意输入仍未完成 |
| M14 跨浏览器/设备 | Chromium 设备档位与输入 | Firefox、WebKit 永久排除 | 只允许 Chromium 证据进入支持声明 |
| M15 全域审计 | Blender 5.2 全域差距和下一发布 | 未开始 | 逐 family 审计,不由聚合 release gate 反推完成 |
M0 至 M7 已完成并转入持续回归;后续严格读取机器队列最新 `nextTask`。里程碑内部允许先写
纯校验任务但不得在前一小里程碑失败时把后一小里程碑标为完成M8 至 M15 不得绕过 M6/M7
发布与体验合同扩功能。
## 6. M0 范围、状态和 P0 闭环
### M0.1 V1 范围收敛
- [x] `M0-SCOPE-01` 定义 V1 用户和核心使用场景。
- [x] `M0-SCOPE-02` 定义 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED`
- [x] `M0-SCOPE-03` 明确 Chromium-only 和不承诺完整 Blender UI/渲染/模拟。
- [x] `M0-SCOPE-04` 将 PBVH、完整 Geometry Nodes、任意 Shader、完整 NLA、完整 VDB
demand paging 等移出 V1 required slice。
### M0.2 双轴状态模型
- [x] `M0-STATE-01` ledger schema 分离 `parityStatus``releaseStatus`
- [x] `M0-STATE-02` 每个 family 声明 V1 required/excluded slices。
- [x] `M0-STATE-03` 校验 required/excluded 不重叠且都引用已声明 slice。
- [x] `M0-STATE-04` 校验 `READY` 的 required slices 全部完成。
- [x] `M0-STATE-05` 校验依赖不存在缺失和环。
- [x] `M0-STATE-06` N-026 只聚合 V1 `releaseStatus` 和真实 evidence 缺口。
- [x] `M0-STATE-07` 旧 schema、非法状态、依赖环、无构件 evidence 均有负例。
### M0.3 单一 P0 用户闭环
固定流程:
```text
import .blend
-> object/mesh/material edit
-> undo -> redo
-> atomic save
-> terminate StorageWorker + WebEngineWorker
-> reopen and compare stable IDs/geometry/material
-> main-thread + Offscreen viewport non-empty pixels
-> GLB export and semantic round-trip
-> unsupported shader stable block
```
- [x] `M0-LOOP-01` 创建独立 `test:v1-user-loop` 命令。
- [x] `M0-LOOP-02` 覆盖对象、Mesh 和 Principled 材质编辑。
- [x] `M0-LOOP-03` 覆盖 Main revision 的 undo/redo。
- [x] `M0-LOOP-04` 覆盖 StorageWorker 与 WebEngineWorker 双终止重开。
- [x] `M0-LOOP-05` 比较稳定 object/mesh/material ID 和 geometry SHA-256。
- [x] `M0-LOOP-06` 覆盖双生产视口非空像素。
- [x] `M0-LOOP-07` 覆盖 GLB PBR 语义回读。
- [x] `M0-LOOP-08` 覆盖未支持 Shader 的稳定阻断。
- [x] `M0-LOOP-09` 将成功命令和两个构件 SHA-256 写入 evidence。
## 7. M1 当前工作区收口
目标:确认现有 VDB/WebGPU、状态模型和 P0 闭环不是只在局部测试成立。
### M1.1 快速静态门
- [x] `M1-STATIC-01` TypeScript typecheck。
- [x] `M1-STATIC-02` ESLint。
- [x] `M1-STATIC-03` Node unit tests。
- [x] `M1-STATIC-04` status consistency。
- [x] `M1-STATIC-05` release evidence parser/evaluator。
### M1.2 VDB/WebGPU 专项矩阵
- [x] `M1-VDB-01` 校验 VDB availability 和 native resource catalog。
- [x] `M1-VDB-02` 校验 desktop OpenVDB -> NanoVDB converter 与畸形输入。
- [x] `M1-VDB-03` 校验 server job 隔离、取消、超时和 hash 等价。
- [x] `M1-VDB-04` 校验 conversion boundary 结构化阻断。
- [x] `M1-VDB-05` 校验 NanoVDB OPFS hash binding 和 Worker 重启。
- [x] `M1-VDB-06` 校验 Float32/Vec3 CPU 与 WGSL sampling。
- [x] `M1-VDB-07` 校验主线程与 Offscreen 生产视口。
- [x] `M1-VDB-08` 校验多 grid/X 视角固定 golden不仅检查 hash 格式。
- [x] `M1-VDB-09` 校验全局 resident budget、LRU 次序和唯一释放。
- [x] `M1-VDB-10` 校验网络、Worker、device-loss 故障恢复。
- [x] `M1-VDB-11` 确认文档仍明确阻断自动缺页重绘、联合重开、大 bundle 联合故障和发布
三视角 golden。
### M1.3 P0 和工作区一致性
- [x] `M1-REG-01` 重新运行 `test:v1-user-loop`
- [x] `M1-REG-02` 重新运行包含 release gate 的 Chromium 专项。
- [x] `M1-REG-03` 执行 `git diff --check`
- [x] `M1-REG-04` 核对所有未提交文件都有明确来源,不删除或回退既有用户改动。
- [x] `M1-REG-05` 更新本节状态和 evidence 时间戳时不伪造未运行命令。
M1 退出命令:
```bash
npm --prefix web run typecheck
npm --prefix web run lint
npm --prefix web test
npm --prefix web run test:status-consistency
npm --prefix web run test:release-evidence
npm --prefix web run test:vdb-availability
npm --prefix web run test:vdb-native
npm --prefix web run test:vdb-server
npm --prefix web run test:vdb
npm --prefix web run test:vdb-opfs
npm --prefix web run test:vdb-webgpu
npm --prefix web run test:vdb-viewport
npm --prefix web run test:vdb-faults
npm --prefix web run test:v1-user-loop
git diff --check
```
## 8. M2 10M Geometry 发布门
目标:证明 10,000,000 triangles 项目能通过二进制/分块路径在 Chromium 内先显示可交互
LOD并能取消、释放和恢复。禁止构造 10M 元素 JSON禁止同时保留多份完整源数组。
### M2.1 契约和预算
- [x] `M2-GEO-01` 定义 10M reporttriangle/vertex/chunk 数、源字节、传输字节、
first-interactive-frame、JS/WASM/GPU 峰值、取消延迟、释放后占用和最终状态。
- [x] `M2-GEO-02` 固定 10M 精确计数、chunk 上限、LOD triangle budget 和测试超时。
- [x] `M2-GEO-03` 发布命令没有可把默认 fixture 降到小规模的环境开关。
- [x] `M2-GEO-04` 为非法计数、chunk 越界、重复/缺失 range 定义稳定错误。
### M2.2 分块二进制输入
- [x] `M2-GEO-05` 以固定大小生成 positions/indices chunk任一时刻只保留有界工作集。
- [x] `M2-GEO-06` 每个 chunk 使用 transferable ArrayBuffer发送后验证发送端 detached。
- [x] `M2-GEO-07` range 表连续覆盖 10M triangles无重叠、空洞或整数溢出。
- [x] `M2-GEO-08` 对输入流和 range manifest 生成确定性 SHA-256。
- [x] `M2-GEO-09` 在中途取消,验证 Worker 停止、迟到 chunk 被丢弃且下一会话可启动。
### M2.3 LOD 与首个可交互帧
- [x] `M2-GEO-10` 复用现有 LOD cache key/manifest不创建测试专用缓存格式。
- [x] `M2-GEO-11` 在完整源仍流入时先发布有界 LOD记录首个非空像素时间。
- [x] `M2-GEO-12` 主线程视口渲染 LOD 并完成一次相机/选择交互。
- [x] `M2-GEO-13` OffscreenCanvas Worker 渲染同一 LOD 并报告非空像素。
- [x] `M2-GEO-14` 验证近/远 LOD 切换不重建完整 10M geometry。
### M2.4 内存、释放和证据
- [x] `M2-GEO-15` 采集 `performance.memory`可用时、WASM allocated bytes 和估算 GPU buffer
bytes报告峰值而不是只有最终值。
- [x] `M2-GEO-16` 明确 JS/WASM/GPU 上限并对超限给出稳定失败。
- [x] `M2-GEO-17` dispose renderer/geometry/material、终止 Worker、删除临时 OPFS 数据。
- [x] `M2-GEO-18` 释放后运行一个小场景,证明浏览器会话可恢复。
- [x] `M2-GEO-19` 创建独立 `test:geometry-10m-performance` 命令。
- [x] `M2-GEO-20` 将成功 record 绑定 `performance.geometry10M`,至少 hash 测试、生产实现和
range manifest/golden。
- [x] `M2-GEO-21` 更新 release checker期望 missing 从 3 降为 2。
M2 退出条件:独立命令连续通过;报告 `triangleCount=10_000_000`;双视口有非空像素;取消和
释放通过evidence 中 `geometry10M=true` 且有唯一成功 record。
## 9. M3 Long Media 发布门
目标:验证 V1 有限 Timeline/Sequencer 在长时间线下的索引、seek、取消、cache 和 Worker 重启,
不把完整媒体编辑器或最终编码器纳入 V1。
### M3.1 范围和 fixture
- [x] `M3-MEDIA-01` 固定长时间线 frame range、strip 数和 preview byte budget。
- [x] `M3-MEDIA-02` 使用本地确定性 PNG/WAV 和 manifest测试不得依赖网络或系统 codec。
- [x] `M3-MEDIA-03` 明确 V1 支持的 IMAGE/SOUND 时间索引子集和排除的 movie codec/encode。
- [x] `M3-MEDIA-04` 为未声明 codec、最终编码、损坏 manifest 定义结构化阻断。
### M3.2 索引和 seek
- [x] `M3-MEDIA-05` 建立有界索引,不按总帧数展开巨型 JSON。
- [x] `M3-MEDIA-06` 验证时间线起点、中点、终点和随机 seek 的精确 strip 命中。
- [x] `M3-MEDIA-07` 验证连续 scrub 合并过期请求,只发布最新 revision/frame。
- [x] `M3-MEDIA-08` 记录冷 seek、热 seek 和索引构建时间。
### M3.3 cache、取消和重启
- [x] `M3-MEDIA-09` 对 media preview bytes 使用显式 byte budget 和 LRU。
- [x] `M3-MEDIA-10` 超预算时逐项淘汰,活动 frame 不被错误淘汰。
- [x] `M3-MEDIA-11` 在索引、seek 和 decode 三个阶段分别验证 AbortSignal。
- [x] `M3-MEDIA-12` 终止 Worker 后从 OPFS/IndexedDB manifest 恢复索引和当前 frame。
- [x] `M3-MEDIA-13` 损坏 session manifest 被隔离,正确 manifest 仍可重建。
### M3.4 证据
- [x] `M3-MEDIA-14` 创建独立 `test:long-media-performance` 命令。
- [x] `M3-MEDIA-15` 输出计数、耗时、峰值 cache bytes、取消和重启结果。
- [x] `M3-MEDIA-16` 将成功 record 绑定 `performance.longMedia`
- [x] `M3-MEDIA-17` 更新 release checker期望 missing 从 2 降为 1。
M3 退出条件确定性长时间线专项通过seek/取消/LRU/restart/unsupported codec 均有断言;
`longMedia=true` 且 evidence 构件可复核。
## 10. M4 OOM 发布门
目标:用确定性故障注入验证内存不足不会损坏当前项目或留下不可恢复会话。测试不能依赖真的
耗尽宿主机内存。
### M4.1 统一故障注入契约
- [x] `M4-OOM-01` 定义仅测试构建可用的 allocation fault point 和 fail-after-bytes/count。
- [x] `M4-OOM-02` fault session 使用 token 隔离,默认关闭且不能泄漏到后续请求。
- [x] `M4-OOM-03` 为 WASM、OPFS staging、GPU geometry/texture、VDB resident 分配映射稳定
错误码和 stage。
- [x] `M4-OOM-04` 统一报告 before/peak/after bytes、revision、临时资源和恢复状态。
### M4.2 WASM 与 Main
- [x] `M4-OOM-05` 在 open/read 临时分配失败时不创建半初始化 handle。
- [x] `M4-OOM-06` 在 edit/save 分配失败时保留旧 Main revision 和 undo 栈。
- [x] `M4-OOM-07` 释放 native buffer/handleWorker 重启后可重开旧项目。
### M4.3 OPFS staging
- [x] `M4-OOM-08` 在 staging write 中途注入 quota/OOM。
- [x] `M4-OOM-09` journal 不提交,新 revision 不替换旧 revision。
- [x] `M4-OOM-10` 清理临时文件;重启后旧项目 hash 不变。
### M4.4 GPU 与 VDB
- [x] `M4-OOM-11` GPU geometry/texture budget 超限不创建部分 scene object。
- [x] `M4-OOM-12` dispose 已创建 GPU buffer/texture随后小场景可渲染。
- [x] `M4-OOM-13` VDB resident budget 超限返回 `NANOVDB_GPU_BUDGET_EXCEEDED`
- [x] `M4-OOM-14` VDB page table/resident pages 唯一释放device/session 可恢复。
### M4.5 证据
- [x] `M4-OOM-15` 创建独立 `test:oom-recovery` 命令,四类 fault 全部执行。
- [x] `M4-OOM-16` 记录每类错误码、旧 revision/hash、释放字节和恢复结果。
- [x] `M4-OOM-17` 将成功 record 绑定 `faults.oom`
- [x] `M4-OOM-18` 更新 release checker期望 missing 为 0、gate 为 `READY`
M4 退出条件:四类确定性故障均通过;不存在静默成功、旧 revision 丢失、临时文件泄漏或只能
刷新浏览器恢复的情况;`faults.oom=true`
完成证据:`npm --prefix web run test:oom-recovery` 在 Chromium 中执行 WASM/Main、OPFS staging、
WebGL GPU 资源组和 NanoVDB WebGPU resident/page-table 四类确定性失败;`release:evidence-oom`
生成第 17 条 record。测试报告分别验证 Main revision/hash、undo/redo、旧 OPFS revision/hash、
临时文件归零、部分 GPU object 不发布、NanoVDB buffer 唯一释放以及同会话/重启恢复。
## 11. M5 V1 发布候选
M5 按 `PREP -> STATIC -> ACCEPT -> LOOP -> PERF -> FAULT -> VDB -> PACKAGE -> AUDIT`
串行推进。每个编号只对应一条命令或一个可独立判断的断言;失败项标为 `[!]` 并附退出码、
错误摘要和重跑命令。浏览器任务统一使用空闲的 `WEB_TEST_PORT`,同一任务的 Web server 退出后
才领取下一任务,避免端口复用掩盖真实启动失败。
### M5.0 环境和输入冻结
- [x] `M5-PREP-01` 开始执行前运行 `git status --short --branch`,确认基线为
`main...origin/main` 且无用户未提交改动。
- [x] `M5-PREP-02` 记录 Node、npm、CMake、Ninja、Emscripten、Chromium 和 desktop Blender
的实际版本或明确缺失项。
- [x] `M5-PREP-03` 运行 `tools/web/check-emscripten.sh`,确认 Web toolchain 可用。
- [x] `M5-PREP-04` 运行 `tools/web/check-local-deps.sh`,确认运行时不依赖 CDN。
- [x] `M5-PREP-05` 检查 `build_web_blender6/bin/web_engine.{js,wasm}` 和安装到 Web app 的
engine manifest/hash 一致。
- [x] `M5-PREP-06` 检查发布所需 fixture、VDB native tools、Chrome 可执行文件和本地
Blender 可执行文件均存在。
- [x] `M5-PREP-07` 记录磁盘空间、内存和 CPU 数;资源不足时在运行长矩阵前停止并记录。
- [x] `M5-PREP-08` 运行 `npm --prefix web ci`,只使用 lockfile 完成 clean install。
- [x] `M5-PREP-09` 运行 `npm audit`;将构建链间接依赖 `nanoid` 从 3.3.17 更新到
3.3.18 后重新 clean install确认 high/critical/total 均为 0。
### M5.1 静态和单元门
- [x] `M5-STATIC-01` 运行 `npm --prefix web run typecheck`
- [x] `M5-STATIC-02` 运行 `npm --prefix web run lint`
- [x] `M5-STATIC-03` 运行 `npm --prefix web test`,记录通过/失败/跳过计数14/0/0。
- [x] `M5-STATIC-04` 运行 `npm --prefix web run build`,确认生产构建可生成。
- [x] `M5-STATIC-05` 运行 `npm --prefix web run test:status-consistency`
- [x] `M5-STATIC-06` 运行 `npm --prefix web run test:release-evidence`,只作为旧证据可解析性
检查,不据此刷新 M5 证据。
- [x] `M5-STATIC-07` 运行 `bash tools/web/check-dual-engine.sh`,确认 native 与 WASM 引擎契约。
- [x] `M5-STATIC-08` 运行 `node tools/web/run-web-engine-smoke.mjs`,确认安装后的生产 WASM
可实际启动。
- [x] `M5-STATIC-09` 重建 standalone single-thread 与 pthread Emscripten 变体;两者均使用
Blender 5.2 的 C++20 标准并成功生成 JS/WASM。
### M5.2 V1 required slice 验收
本节命令来源必须是 `status/parity-ledger.json``families[].acceptance`。重复命令只执行一次;
`web:e2e:<pattern>` 的项转换成 Playwright `-g` 过滤器,不能把协议单测替代为浏览器测试。
- [x] `M5-ACCEPT-01` N-015 non-mesh native roundtrip 与 desktop golden。
- [x] `M5-ACCEPT-02` N-015 non-mesh GLB/USD serialization 和 desktop USD roundtrip 通过;
官方 Blender 5.2.0 LTS runtime `USD=True`
- [x] `M5-ACCEPT-03` N-015 1M binary、selection history 和 curve interaction 浏览器门。
- [x] `M5-ACCEPT-04` N-015 `N-015|non-mesh` 聚合 E2E 与真实 OPFS quota。
- [x] `M5-ACCEPT-05` N-016 Grease Pencil native roundtrip、editor 和聚合 E2E。
- [x] `M5-ACCEPT-06` N-017 Paint/weight native roundtrip 和聚合 E2E。
- [x] `M5-ACCEPT-07` N-018 Physics Main reader、simulation cache 和聚合 E2E。
- [x] `M5-ACCEPT-08` N-019 Lighting Main roundtrip 和 Scene exposure E2E。
- [x] `M5-ACCEPT-09` N-020 Compositor Main reader 和有界 CPU compositor E2E。
- [x] `M5-ACCEPT-10` N-021 Sequencer Main reader 和 timeline/codec gate E2E。
- [x] `M5-ACCEPT-11` N-022 Mask Main reader 和 tracking/mask E2E。
- [x] `M5-ACCEPT-12` N-023 Library Main reader、asset/zip 安全 E2E 和共享 VDB fault 门。
- [x] `M5-ACCEPT-13` N-024 Editor Main reader 和 context/keymap E2E。
- [x] `M5-ACCEPT-14` N-025 Script Main reader、isolation 和默认拒绝 E2E。
- [x] `M5-ACCEPT-15` N-026 release gate E2E断言 Chromium-only 声明和 V1 release 状态。
- [x] `M5-ACCEPT-16` 实现 acceptance coverage checker解析 12 个 family、校验命名空间、
npm script 存在、E2E pattern 非空并去重。
- [x] `M5-ACCEPT-17` 实现 acceptance runner顺序执行去重后的命令浏览器命令使用独立端口
写入 ledger SHA-256、环境版本、退出码、耗时和输出摘要。
- [x] `M5-ACCEPT-18` 对未知命名空间、缺失 script、空 pattern、重复声明、stale source/tool/package、
漏项、失败退出码、空输出和 hash 漂移建立负例/断言。
- [x] `M5-ACCEPT-19` 使用 SHA-256 校验的官方 USD-enabled Blender 5.2 重跑 desktop USD
roundtrip归档 hash 和 executable hash 均写入 acceptance evidence。
- [x] `M5-ACCEPT-20` 运行完整 acceptance runner50 declarations、49 unique、49 passed、
0 failedM6-03 后复验总命令耗时 480,169 ms。
- [x] `M5-ACCEPT-21` 将 acceptance evidence SHA-256
`d5fb0a7b2df92b076f8b05ccd68903d60e3352b9423b0c1404065f6cbcbdd45c` 绑定到最终
`v1-user-loop` release record。
### M5.3 P0 用户闭环和 Chromium 回归
- [x] `M5-LOOP-01` 独立运行 `test:v1-user-loop`,覆盖 import/edit/undo/redo/save。
- [x] `M5-LOOP-02` 在同一 P0 测试中确认 StorageWorker 与 WebEngineWorker 均终止后重建。
- [x] `M5-LOOP-03` 确认重开后 object/mesh/material stable ID 和 geometry hash 不变。
- [x] `M5-LOOP-04` 确认主线程与 OffscreenCanvas 两个生产视口均产生非空像素。
- [x] `M5-LOOP-05` 确认 GLB 严格回读通过且未支持 Shader 返回稳定阻断。
- [x] `M5-LOOP-06` 运行 `test:e2e -- --workers=1` 全套 Chromium E2E122/122 通过,
0 失败、0 跳过,总耗时 6.4 分钟。
- [x] `M5-LOOP-07` 运行 `test:browser` 离线引擎发布配置 smoke3/3 通过。
### M5.4 性能和容量矩阵
- [x] `M5-PERF-01` 运行 100k/1M geometry `test:release-performance`
- [x] `M5-PERF-02` 运行 10M geometry 分块、双视口、取消、释放和恢复门。
- [x] `M5-PERF-03` 校验 10M report 精确为 `triangleCount=10_000_000` 且没有缩小 fixture 的开关。
- [x] `M5-PERF-04` 运行 4K texture decode/upload/budget/release 门。
- [x] `M5-PERF-05` 运行 8K texture decode/upload/budget/release 门。
- [x] `M5-PERF-06` 运行 600 帧 simulation cache 性能、LRU 和 Worker restart 门。
- [x] `M5-PERF-07` 运行 long media index/seek/cancel/LRU/restart 门。
- [x] `M5-PERF-08` 每份性能报告均输出 fixture 规模、耗时、峰值/预算、释放后值和恢复状态。
### M5.5 故障和恶意输入矩阵
- [x] `M5-FAULT-01` 运行 WASM/Main、OPFS、GPU、VDB 四场景 OOM recovery。
- [x] `M5-FAULT-02` 运行主线程 WebGL device-loss 恢复门。
- [x] `M5-FAULT-03` 运行网络 range 中断、续传、hash 和 rollback 门。
- [x] `M5-FAULT-04` 运行 malicious `.blend` 截断、长度溢出和预算门,拒绝 5 类输入。
- [x] `M5-FAULT-05` 运行 asset archive zip-bomb、路径穿越和膨胀预算门。
- [x] `M5-FAULT-06` 运行 OPFS quota/staging/journal rollback 浏览器门。
- [x] `M5-FAULT-07` 核对每个失败均返回稳定 error code/stage旧 revision/hash 不变。
- [x] `M5-FAULT-08` 核对每个故障后同会话或 Worker 重启的小场景恢复成功且临时资源归零。
### M5.6 VDB/WebGPU 发布矩阵
- [x] `M5-VDB-01` 校验 VDB resource catalog、license、固定 SHA-256 和 availability。
- [x] `M5-VDB-02` 运行 desktop OpenVDB -> NanoVDB native converter 正例与畸形输入。
- [x] `M5-VDB-03` 运行 server job isolation/cancel/timeout 和 desktop/server hash 等价。
- [x] `M5-VDB-04` 运行浏览器 conversion boundary 和稳定 capability block。
- [x] `M5-VDB-05` 运行 HTTP range -> OPFS 原子绑定、tamper 和 Worker restart。
- [x] `M5-VDB-06` 运行真实 Float32/Vec3 NanoVDB CPU/WGSL sampling。
- [x] `M5-VDB-07` 运行主线程 WebGL 与 OffscreenCanvas Worker 生产视口非空像素。
- [x] `M5-VDB-08` 运行多 grid 材质、resident LRU、网络/Worker/device-loss fault 恢复。
- [x] `M5-VDB-09` 核对自动 page fault、深度合成、大 bundle 和发布三视角 golden 仍明确排除。
### M5.7 构件、许可证和可复现性
- [x] `M5-PACKAGE-01` 从 lockfile 重新生成 SPDX 2.3 SBOM共 150 个 package。
- [x] `M5-PACKAGE-02` 检查 third-party notices 覆盖 React、Three、Vite、TypeScript、Playwright、
Blender 和本地 native/WASM 依赖。
- [x] `M5-PACKAGE-03` 重新执行生产 build检查 engine JS/WASM 和 manifest 均进入 dist。
- [x] `M5-PACKAGE-04` 检查 dist 的 HTML/JS/CSS/JSON 不引用 HTTP(S) CDN。
- [x] `M5-PACKAGE-05` 生成离线 binary archive大小 7,585,557 bytes。
- [x] `M5-PACKAGE-06` 生成 corresponding source archive 和 `SOURCE_OFFER.txt`,大小
205,827,673 bytes。
- [x] `M5-PACKAGE-07` 第一次记录 binary/source archive SHA-256。
- [x] `M5-PACKAGE-08` 从相同输入再次生成两个 archive。
- [x] `M5-PACKAGE-09` 断言第二次 binary/source SHA-256 分别与第一次完全相同。
- [x] `M5-PACKAGE-10` 生成顶层 `release/SHA256SUMS.txt`
- [x] `M5-PACKAGE-11` 逐行校验 `SHA256SUMS.txt` 引用文件存在且 `sha256sum -c` 通过。
- [x] `M5-PACKAGE-12` 扫描构件中的 CDN、绝对工作区路径、token/password/private key 特征。
- [x] `M5-PACKAGE-13` binary archive 与 manifest 完全一致source archive 无缓存、trace、
build、dist、测试报告或日志。
### M5.8 声明、证据和最终审计
- [x] `M5-AUDIT-01` UI、README 和 scope 统一使用 `Web Blender Modeler V1`,浏览器范围为
Chromium-only。
- [x] `M5-AUDIT-02` UI 不把 `SERVER/EXCLUDED/BLOCKED` 能力呈现为可本地成功。
- [x] `M5-AUDIT-03` 确认 12 个 family 仍为 `parityStatus=BLOCKED`
- [x] `M5-AUDIT-04` 确认 12 个 V1 required slice 均为 `releaseStatus=READY`
- [x] `M5-AUDIT-05` 运行完整 `release:evidence`,用本轮命令输出和构件 hash 替换旧 records
当前生成时间 `2026-08-15T05:37:25.215Z`17 条 record总命令耗时 553,577 ms。
- [x] `M5-AUDIT-06` 检查 17 条 evidence record 均为 exit code 0、非空输出和非空构件 hash。
- [x] `M5-AUDIT-07` 检查 evidence 的 ledger SHA-256 与当前文件完全一致。
- [x] `M5-AUDIT-08` 运行 `test:release-evidence`,结果 `READY``missing=[]`
- [x] `M5-AUDIT-09` 运行 `test:status-consistency`,结果 12 parity blocked、0 release blocked。
- [x] `M5-AUDIT-10` 运行 `git diff --check`
- [x] `M5-AUDIT-11` 运行 `git status --short --ignored`;源码/证据为 tracked 工作项build、dist、
release、node_modules、测试结果和工具链 cache 均为预期 ignored 生成物。
- [x] `M5-AUDIT-12` 将实际命令、计数、耗时、失败重跑和最终 archive hash 写回本计划与
`PROJECT_STATUS_AND_NEXT_WORK.md`,不得把未运行项标完成。
M5 退出条件:所有必需命令退出码 0release gate `READY`;离线包可复现;当前构件、源码、
SBOM、状态和 evidence SHA-256 相互一致。
M5 最终 archive SHA-256binary
`f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549`source
`9a9c1b505c68a618de842be7c248f628b104b11b80e8a40d111f62540fcba327`。后续领取点只读取机器队列
最新 `nextTask`,本段不缓存任务名称。
## 12. V1 后原子里程碑
本节是 V1 RC 之后的领取顺序。每个里程碑控制在一个可独立回归的行为闭环;同一功能域不再
一次领取 reader、writer、UI、GPU 和发布门。前一里程碑未达到退出条件时,后一里程碑只能做
不会改变产品声明的测试准备。
### M6 可部署 RC
M6 固定按 `runtime -> HTTP -> cache/offline -> archive -> CI -> operations` 串行推进。每个
任务只验证一个行为;上一小里程碑未通过时,不把后一小里程碑的准备工作记为完成。
#### M6.1 single-thread 无隔离启动
- [x] `M6-01` 新建机器可读部署合同,固定 MIME、缓存、COOP、COEP、CORP 和 Range 响应头;
`node tools/web/check-deployment-contract.mjs`、typecheck、lint 和 production build 通过。
- [x] `M6-02` 复用 `CapabilityGateResult` 新增纯函数 `gateWasmThreadingCapability`;仅当
`crossOriginIsolated``SharedArrayBuffer``Worker` 同时可用时放行 pthread 引擎,
缺失项逐条返回稳定结构single-thread 初始化路径不调用该门。
- [x] `M6-03A` 增加仅供测试使用的无隔离 Vite profile默认 dev/preview 仍强制部署合同头。
- [x] `M6-03B` 在独立 Chromium context 断言文档响应没有 COOP/COEP/CORP
`crossOriginIsolated=false``SharedArrayBuffer` 不可用、`Worker` 可用。
- [x] `M6-03C` 断言当前 engine manifest 为 `memory.shared=false`,网络请求不包含 pthread
JS/WASM/worker 资产。
- [x] `M6-03D` 在上述环境等待 single-thread WebEngine 返回 `ready`,同时证明 pthread gate
`BLOCKED`,从行为上保证 single-thread 不受该门阻断。
- [x] `M6-03E` 使用真实 `basic_scene.blend` 完成 open 和一个 Main edit证明不是只加载 UI shell。
M6.1 退出条件:专项命令一次启动独立无隔离服务器,以上五项同进程通过,退出码为 0。
#### M6.2 pthread 选择与回退
- [x] `M6-04A` 扩展 engine manifest schema显式声明 single/pthread variant、JS、WASM、
pthread worker、shared memory 和每个文件 SHA-256v2 纯校验器固定 256 至 32768 pages、
variant 资源隔离和稳定错误路径21 个专项正负例断言通过。生产 manifest/安装路径保持 v1
无隔离 single-thread 专项回归通过。
- [x] `M6-04B` 安装脚本从两个独立构建目录复制 variant 资产到显式 single/pthread URL
生成绑定四个物理文件 SHA-256 的生产 schema v2 manifest同目录和缺失资产负例不会改写目标
Node 专项、manifest/local dependency、build 和无隔离 Chromium 回归通过。旧 single Worker 入口
只作为 `M6-05` fallback 接线前的完整 Main 兼容路径保留。
- [x] `M6-04C` 新增纯函数选择 engine variant输入仅为 manifest、用户策略和平台能力
frozen input 重复调用结果一致,未知策略稳定拒绝。
- [x] `M6-04D` `AUTO` 在完整 pthread gate `READY` 时选择 pthread隔离、SharedArrayBuffer 或
Worker 任一缺失时选择 single五组能力矩阵通过。
- [x] `M6-04E` `PTHREAD_REQUIRED` 在任一能力缺失时返回 `selectedVariant=null` 和原样 M6-02
结构,不向调用方暴露可请求的 pthread URL。
- [x] `M6-04F` 隔离 Chromium 实际加载 pthread JS/WASM校验 manifest hash、
`SharedArrayBuffer`、pool worker 二次 JS 请求和 native handle 创建/释放。
- [x] `M6-04G` 同一 manifest 在无隔离 Chromium 由 AUTO 实际加载 single JS/WASM确认非共享
内存、无 pthread 请求,并继续用完整 single Main 打开和编辑真实 `.blend`
- [x] `M6-05A` 为 pthread factory 增加仅测试可用的确定性初始化失败注入点。
- [x] `M6-05B` `AUTO` 下 pthread 初始化失败只回退一次 single不形成重试循环。
- [x] `M6-05C` fallback 前终止已创建的 pthread worker、timer 和 pending request。
- [x] `M6-05D` fallback 过程中未打开项目时只初始化;已有待打开项目时只执行一次 open。
- [x] `M6-05E` fallback 结果公开 selected/attempted/fallbackReasonUI 不显示 pthread READY。
M6.2 退出条件isolated pthread、unisolated single、forced failure fallback 三个专项测试均通过,
每种路径的资产请求、Worker 数和 open 次数可断言。
#### M6.3 HTTP MIME 与 byte range
- [x] `M6-06A` 从 deployment contract 生成测试用扩展名到 MIME 的唯一映射,不复制常量。
- [x] `M6-06B` 对 HTML、JS、CSS、WASM、JSON、PNG、WAV、BLEND、NVDB 分别执行 GET。
- [x] `M6-06C` 对 engine JS/WASM、pthread worker 和 fixture 分别执行 HEAD断言状态与
`Content-Type`,且 HEAD 无响应体。
- [x] `M6-06D` MIME 错误时测试必须报告 URL、expected、actual不能只返回布尔失败。
- [x] `M6-07A` 对 WASM、BLEND、NVDB 各执行一个闭区间 range断言 `206` 和精确字节。
- [x] `M6-07B` 分别验证 open-ended 与 suffix range若合同不支持则稳定返回 `416`
- [x] `M6-07C` malformed、倒序、越界和 multi-range 分别返回 `416`,不得返回截断的 `200`
- [x] `M6-07D` `206` 同时具有 `Accept-Ranges``Content-Range``Content-Length` 和强 ETag。
- [x] `M6-07E` 相同内容跨请求 ETag 稳定;内容变化后 ETag 必须变化。
- [x] `M6-07F` `If-Range` 匹配时返回对应 `206`,不匹配时返回完整 `200`
- [x] `M6-07G` 中断后从精确 offset 续传并校验合并后的 SHA-256不复用旧 revision 字节。
M6.3 退出条件:同一真实 HTTP server 同时通过 MIME 和 range 正负例矩阵。
#### M6.4 缓存、升级与离线运行
- [x] `M6-08A` HTML 和 engine manifest 固定 `no-cache`,每次启动都能重新验证版本。
- [x] `M6-08B` content-hashed JS/CSS/Worker 固定 immutable稳定 engine URL 必须重新验证 hash。
- [x] `M6-08C` manifest 为每个 variant 绑定同一 releaseId禁止跨 release 混用 JS/WASM/worker。
- [x] `M6-08D` 模拟发布切换:旧 HTML 只加载旧 manifest 全套或明确刷新,不加载混合版本。
- [x] `M6-08E` hash 不匹配时保持旧项目未打开并返回稳定错误,不进入 fallback 的错误 variant。
`npm --prefix web run test:deployment-cache` 从 production build 临时副本启动合同服务器:`/`
`/index.html`、manifest 和 4 个 stable engine 资源均为 `no-cache`;实际构建的入口 JS、CSS 和
3 个 Worker 共 5 项均具有 Vite 内容 hash 且为 immutable。HTML、manifest、stable engine 对相同
ETag 返回无 body `304`,换代后对旧 ETag 返回完整 `200` 和新 ETag临时换代不改写 `web/dist`
- [x] `M6-09A` 从 binary archive 解包后使用合同服务器启动,不读取工作区文件。
- [x] `M6-09B` 新浏览器 profile 冷启动时拦截所有非 loopback 请求,断言请求数为 0。
- [x] `M6-09C` 断开外网但保留本地合同服务器,完成 manifest、WASM、字体和基础 UI 冷启动。
- [x] `M6-10A` 在线创建 OPFS 项目、编辑并原子保存,记录 revision、blend hash 和 stable IDs。
- [x] `M6-10B` 重建 Worker 并切换到外网断开状态,从 OPFS 重开同一 revision。
- [x] `M6-10C` 离线重开后继续 Main edit、undo/redo 和保存revision 单调增加。
- [x] `M6-10D` 离线导出 GLB 并严格回读,且无非 loopback 请求。
M6.4 退出条件版本切换负例、archive 冷启动和 OPFS 离线闭环分别有独立命令与报告。
#### M6.5 归档独立复验
- [x] `M6-11A` 使用 `mktemp -d` 解包 binary archive禁止引用仓库 `web/dist`
- [x] `M6-11B` 校验 `SHA256SUMS.txt`、deployment contract、license、SBOM 和 binary manifest。
- [x] `M6-11C` 从临时目录启动合同服务器并运行 browser smoke退出后释放端口和临时 profile。
- [x] `M6-12A` 在另一个空临时目录解包 corresponding source archive。
- [x] `M6-12B` 校验 source offer 指向的版本、许可证、lockfile、CMake 与 Web 源码全部存在。
- [x] `M6-12C` 扫描 source archive拒绝 build、dist、release、cache、trace、绝对开发路径和凭据。
- [x] `M6-12D` 从 source archive 执行 lockfile install、typecheck、build并比对可重现清单。
M6.5 退出条件binary/source 两个归档均在独立目录复验,报告包含 archive 与关键文件 SHA-256。
#### M6.6 CI 分层与机器报告
- [x] `M6-13A` 确定仓库实际 CI provider、runner OS、Node/Chrome/Blender/Emscripten 版本矩阵。
- [x] `M6-13B` quick lane 从 lockfile 安装,执行 typecheck、lint、unit、status 和 evidence schema。
- [x] `M6-13C` quick lane 缓存只以 lockfile/toolchain key 命中,不缓存 release evidence。
- [x] `M6-14A` Chromium lane 执行 P0 user loop 和主线程/Offscreen 双视口 smoke。
- [x] `M6-14B` Chromium lane 执行 network、device-loss、OOM、quota 与 malicious-input fault smoke。
- [x] `M6-14C` Chromium lane 每个 browser server 使用独立动态端口,失败后也必须清理。
- [x] `M6-15A` release lane 顺序执行全 E2E、性能矩阵、VDB、acceptance 和离线复现。
- [x] `M6-15B` release lane 只在受保护 ref 或显式 dispatch 运行,不由普通 quick push 重复触发。
- [x] `M6-15C` 任一 release 子命令失败时证据状态为失败,不能上传旧 READY JSON。
- [x] `M6-16A` 三条 lane 均输出 schema-versioned JSON命令、exitCode、duration、environment。
- [x] `M6-16B` 报告绑定 commit、lockfile、ledger、engine、archive 和测试构件 SHA-256。
- [x] `M6-16C` 上传前运行 schema 与 hash checker报告和构件采用固定 retention。
2026-08-15 完整 release lane 的 19/19 子命令通过,总命令耗时
1,245,732 msfull E2E 126/126V1 acceptance 49/49。quick 6/6、Chromium 9/9 和
release 19/19 报告由 `check-ci-report.mjs` 联合校验,同时绑定 commit
`17ab961485fe2ea1574c5f0b7d3ef9608a2bf424`。失败注入返回 exit code 17 并仅写入
`FAILED` 报告。当前 binary/source archive SHA-256 分别为
`ca09540e8f5c0157bb85d62c98aeb3291ea195a29585bc8e05afb308747a8ff5`
`95b3a747425081a205b1966c004a08da4cb37aab7ba0a649ff076713f7010f74`
M6.6 退出条件fresh runner 上 quick/Chromium/release 三条 lane 通过,失败注入不会保留成功证据。
#### M6.7 运维与 RC 冻结
- [x] `M6-17A` 部署 runbook 从空目录安装 binary archive并验证 HTTPS/loopback 前置条件。
- [x] `M6-17B` 升级 runbook 覆盖新旧 manifest、cache、OPFS schema 和 Worker 版本切换。
- [x] `M6-17C` 回滚 runbook 保留项目数据,只回滚 app/engine releaseId并验证旧版可读策略。
- [x] `M6-17D` 故障诊断表覆盖 MIME、range、isolation、hash、quota、Worker 和 GPU 错误。
- [x] `M6-17E` 在一个全新临时目录逐条实跑部署、升级、回滚命令并记录输出。
- [x] `M6-18A` 固定 RC semver、commit、releaseId、binary/source/SBOM SHA-256。
- [x] `M6-18B` 变更说明只列已验证 V1 能力,并链接明确 excluded/server/blocked 清单。
- [x] `M6-18C` 已知限制包含 Chromium-only、single/pthread 选择、存储和 GPU 预算。
- [x] `M6-18D` 发布恢复说明包含项目备份、升级失败回滚和证据校验命令。
- [x] `M6-18E` 最终运行 quick、Chromium、release lane所有结果绑定同一 RC commit。
M6 退出条件:离线包和一个真实 HTTP 部署均通过同一 P0 流程single/pthread 选择可解释;
CI 可从 lockfile 和对应源码重现当前 binary/source hash。
### M7 核心项目体验硬化
- [x] `M7-01` 记录 import/open/save/save-as/export 五个用户动作的状态机。
- [x] `M7-02` 对重复 open、并发 save 和 close-during-save 建立互斥规则。
- [x] `M7-03` 大文件导入显示真实字节进度并支持取消。
- [x] `M7-04` 取消 import 后释放 input buffer、native handle 和 staging 文件。
- [x] `M7-05` save 中断时旧 revision 和旧 hash 保持不变。
- [x] `M7-06` dirty state 只由成功 Main transaction 改变。
- [x] `M7-07` undo 回到保存 revision 时 dirty state 清除。
- [x] `M7-08` Worker 崩溃时展示可恢复错误,不清空当前项目列表。
- [x] `M7-09` 重启 Worker 后恢复 selection、frame、workspace 和项目 revision。
- [x] `M7-10` recent projects 对缺失/损坏 OPFS 条目执行隔离和修复提示。
- [x] `M7-11` 存储预算面板报告项目、快照、LOD、媒体、VDB 分项字节。
- [x] `M7-12` 清理单个项目时只删除该项目 content-addressed 引用的孤儿资源。
- [x] `M7-13` 键盘焦点、菜单、modal、Escape 取消形成一致上下文规则。
- [x] `M7-14` 1440x900、1280x720、移动窄屏下无控制重叠和文本溢出。
- [x] `M7-15` 主线程与 Offscreen 视口的选择、相机和 gizmo 行为一致。
- [x] `M7-16` P0 用户流程增加无鼠标键盘路径和基础可访问性检查。
- [x] `M7-17` 用户可见错误只显示稳定短消息,详细诊断进入可导出报告。
- [x] `M7-18` 完成 30 分钟持续编辑、自动保存、重开 soak 测试。
### M8 VDB 自动分页最小闭环
- [x] `M8-01` 定义 GPU page-fault feedback buffer schema、容量和 overflow code。
- [x] `M8-02` shader 对未驻留 leaf 记录唯一 page ID不越界写 feedback。
- [x] `M8-03` CPU 读取 feedback 后排序、去重并绑定 render revision。
- [x] `M8-04` stale frame feedback 不触发 I/O。
- [x] `M8-05` page request 使用现有 manifest range/hash不建立第二套地址模型。
- [x] `M8-06` 同页并发请求合并,取消最后一个订阅者时中止 range。
- [x] `M8-07` page hash 错误不写 resident cache并返回稳定错误。
- [x] `M8-08` LRU 淘汰跳过当前 frame pin 的 page。
- [x] `M8-09` page upload 成功后只安排一次渐进重绘。
- [x] `M8-10` 重绘上限防止坏数据造成无限 render loop。
- [x] `M8-11` 主线程 WebGPU 验证缺页、加载、重绘到确定性像素。
- [x] `M8-12` Offscreen Worker 验证相同页序列和最终像素。
- [x] `M8-13` page fetch 中 network interruption 可从精确 byte offset 续传。
- [x] `M8-14` Worker restart 从 OPFS 恢复 manifest但不信任未复验 resident 状态。
- [x] `M8-15` device loss 重建 page table并按可见集合有界回放。
- [x] `M8-16` OOM 释放 page table/resident/feedback buffer 各一次。
- [x] `M8-17` 64 MiB sparse bundle 性能与取消门。
- [x] `M8-18` Volume Main、asset binding、双视口联合保存重开。
- [x] `M8-19` desktop/主线程/Offscreen 三视角 golden 和误差阈值。
- [x] `M8-20` 只有 M8 全部通过后更新 N-015 对应 sliceN-015 全域仍不自动 COMPLETE。
### M9 非 Mesh、Grease Pencil 与 Paint 增量
- [x] `M9-01` 外部字体导入先做路径、类型、大小和 hash 校验。
- [x] `M9-02` 字体进入 OPFS content-addressed asset 后才写 Main VFont。
- [x] `M9-03` 字体替换/撤销/保存/重开/缺失资产形成闭环。
- [x] `M9-04` Curve 完整 topology editor 先冻结 operator 白名单和预算。
- [x] `M9-05` 每个新增 Curve operator 单独完成 Main/undo/save/golden 后再开放 UI。
- [x] `M9-06` Grease Pencil marquee 只操作当前 drawing 的稳定 point/stroke ID。
- [x] `M9-07` Grease Pencil 2D canvas 与 3D viewport 使用同一 selection revision。
- [x] `M9-08` Grease Pencil layer/frame reorder 完成 undo/save/reopen。
- [x] `M9-09` Paint 建立真实深度可见性采样,不用 CPU proxy 冒充。
- [x] `M9-10` Paint stroke 分块提交且一次 pointer session 只产生一个 undo step。
- [x] `M9-11` Texture paint dirty tile 原子写入 packed/UDIM 资产。
- [x] `M9-12` Weight paint normalize/limit/mirror 与 Blender 5.2 desktop golden 对比;镜像仅对
复核后的局部坐标对称映射放行,限权后按 Blender 语义归一化。
- [x] `M9-13` PBVH 若不进入 WASM则相关笔刷保持明确 capability block。
- [x] `M9-14` 三个编辑域分别通过 Worker restart、OOM、GPU release 和小场景恢复。
### M10 Geometry Nodes、Shader、NLA 与 Simulation15/15
- [x] `M10-01` 从 Blender Main 读取图拓扑、socket 默认值、link 和稳定 node ID。
- [x] `M10-02` 为 Geometry Nodes 建立 allowlist不支持节点保留原数据并阻断求值。
- [x] `M10-03` 每个 allowlist 节点增加 desktop fixture、WASM 输出和误差阈值。
- [x] `M10-04` field/domain 转换显式预算,禁止按无限域展开 JSON。
- [x] `M10-05` Simulation Zone cache 绑定 graph/source/revision hash。
- [x] `M10-06` cache 取消、LRU、重启和损坏隔离通过后才允许播放。
- [x] `M10-07` Shader 图只编译声明的 Principled/Image/Normal/Math 子集。
- [x] `M10-08` shader compile key 包含 graph、texture、color space 和 renderer backend。
- [x] `M10-09` compile failure 不替换上一份可用 material pipeline。
- [x] `M10-10` 任意未支持 Shader 节点返回 stable capability block不静默降级。
- [x] `M10-11` NLA track/strip/action/time mapping 先完成只读精确求值。
- [x] `M10-12` NLA 单个编辑 operator 分别完成 Main transaction、undo 和保存重开。
- [x] `M10-13` Physics 本地 solver 按 family 探测;不支持项只消费 desktop/server bake。
- [x] `M10-14` 每类 cache 验证 source hash、frame range、字节预算和版本。
- [x] `M10-15` GN/Shader/NLA/Simulation 分别建立浏览器性能、OOM 和恶意图输入门。
### M11 Lighting、Render、Compositor 与 Sequencer14/14
- [x] `M11-01` Camera/Light/World/Scene color management 建立字段级 parity 表。
- [x] `M11-02` 每个支持字段通过 Main edit、undo、save/reopen 和 viewport 映射。
- [x] `M11-03` Three/WebGPU 灯光数量、shadow map 和纹理预算显式化。
- [x] `M11-04` Web 实时渲染结果与 Blender reference 采用可解释图像误差指标;固定 SRGB8/STRAIGHT RGBA、MAE/RMS/P95/坏像素/前景 IoU/alpha 覆盖率,主线程与 Offscreen 共享同一 256×256 Blender 5.2 Eevee fixture golden。
- [x] `M11-05` Cycles/复杂 Eevee/硬件后端固定为 server job不在浏览器伪造等价schema 1 将 bounded Eevee/WebGL2 与 server target 分离endpoint 未配置时稳定 fail-closed。
- [x] `M11-06` server render job 绑定 source `.blend` hash、Blender 5.2 build hash、规范化设置 hash 和实际输出 hash本地 loopback server 由 Blender 5.2 headless 执行 fixture rendersource/settings/build/output 篡改均 fail-closed。
- [x] `M11-07` Compositor allowlist 每新增一个 node 单独增加 CPU/WebGPU golden当前仅冻结
Constant Color、Exposure、Invert、Composite 的线性 sRGB Float32 有界链,真实 Blender 5.2
Main fixture 在 CPU 与 Chromium WebGPU compute 上逐节点 hash 等价,其余节点仍阻断。
- [x] `M11-08` unsupported compositor graph 保留原图并阻断执行CPU 与 cached 入口都在
求值/缓存命中前扫描完整图,未连接到输出的 Unsupported 节点也稳定阻断,真实 Main graph、
Blender type metadata 和 revision 保持不变。
- [x] `M11-09` Sequencer IMAGE/SOUND/MOVIE codec 由运行时 probe 决定,不按扩展名猜测;
receipt 绑定 strip family、MIME、字节长度和 source SHA-256Chromium 分别以 ImageBitmap、
固定采样率 Web Audio 和 HTMLMedia 实际解码 PNG/WAV/H.264 MP4。
- [x] `M11-10` long media proxy/cache 绑定源 hash 和 decode capabilityschema 1 将 source
family/MIME/bytes/SHA-256、M11-09 READY receipt、RGBA8 profile 和 source frame 共同绑定为
cache identitypayload 独立验 hashChromium 实际生成首帧 proxy 并通过 LRU/Storage Worker
重开门。
- [x] `M11-11` seek/scrub/decode 迟到结果全部受 revision gate 控制;三类请求共享单调
request revision结果必须回显 timeline/request identitytimeline 换代、旧请求和伪造结果
均在 publish/cache callback 前返回 `STALE/REVISION_CONFLICT`
- [x] `M11-12` 最终视频编码固定 server/export capability不误报本地完成schema 1 绑定
timeline/source revision、`.blend` SHA-256、帧率/范围、分辨率和 codec settings hash
endpoint 缺失时 fail-closed即使检测到 `VideoEncoder` 也保持 `localEncoding=BLOCKED`
- [x] `M11-13` audio context suspend/resume、设备缺失和静音恢复有专项测试schema 1
区分 context/output 状态,真实 Chromium `AudioContext` 通过用户手势、挂起、静音恢复、
二次挂起/恢复和 close缺设备与 resume failure 保持结构化静音阻断。
- [x] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复schema 1
固定三域 cancellation/budget code 和资源归零字段,真实 `.blend`、H.264 与 WebGL 输出通过
generation 2 重开、失败不提交、显式 clear/dispose 和恢复后 output SHA-256 门。
### M12 Asset、IO、Editor 与工作流
本节 M12-M15 仅保留短周期计划形成时的里程碑摘要不再作为原子任务定义。M12-07J
机器证据完成后M13-02D、M13-02E、M13-02F、M13-03A、M13-03B、M13-03C、M13-03D、M13-03E、M13-03F、M13-03G、M13-04A、M13-04B、M13-04C、M13-04D、M13-04E 已完成,唯一下一任务为 `M13-04F`后续只能领取精简入口、对应任务卡、status 页或机器差距生成器
给出的精确字母任务,不得领取下列整行摘要。
- [ ] `M12-01` asset catalog schema migration 有向前/向后兼容 fixture。
- [ ] `M12-02` append/link/override 分别定义 stable ID、所有权和失效语义。
- [ ] `M12-03` library source 只允许声明 origin路径穿越和符号链接逃逸被拒绝。
- [ ] `M12-04` archive 每个 entry 在解压前检查数量、压缩比、总字节和目标路径。
- [ ] `M12-05` GLB、OBJ、STL、PLY 每种格式建立独立导入/导出能力矩阵。
- [ ] `M12-06` 每种支持格式至少一个 desktop roundtrip 和一个 loss report。
- [ ] `M12-07` USD/Alembic 依赖固定 desktop runtime identity不由本机偶然能力决定 gate。
- [ ] `M12-08` View3D/Outliner/Properties 的 selection/context/revision 使用同一 store。
- [ ] `M12-09` UV/Node/Graph/Dope/NLA editor 每次只开放有 Main writer 的操作。
- [ ] `M12-10` operator search 从 capability registry 生成,不维护第二份成功列表。
- [ ] `M12-11` keymap resolution 按 editor/modal/tool 上下文确定性排序。
- [ ] `M12-12` workspace/layout 保存重开不修改 Blender 场景 revision。
- [ ] `M12-13` editor dispose 后无 DOM listener、Worker request 或 GPU resource 泄漏。
- [ ] `M12-14` 典型建模、动画、资产复用流程建立任务级 E2E不只做控件 smoke。
### M13 Scripting、安全与平台边界
- [ ] `M13-01` Python/Text autorun/add-on 默认拒绝保持产品级不可绕过。
- [ ] `M13-02` script manifest 只读解析限制文本、模块、路径和依赖数量。
- [ ] `M13-03` 若引入签名脚本,先定义签名者、权限、撤销和审计模型。
- [ ] `M13-04` 浏览器 sandbox 不提供 DOM、网络、OPFS、Worker 的隐式全权限。
- [ ] `M13-05` server Blender job 使用一次性目录、只读源、资源限制和网络策略。
- [ ] `M13-06` 所有 server result 验证 source/settings/build/output hash 后才绑定项目。
- [ ] `M13-07` CSP 禁止 inline script、eval 和未声明 origin。
- [ ] `M13-08` fixture 覆盖恶意 blend、图片、字体、媒体、archive、node graph 和 manifest。
- [ ] `M13-09` fuzz crash 不写入 release evidence修复后必须加入最小回归样本。
- [ ] `M13-10` capability report 明确 native window/CUDA/Metal/HIP/OptiX 不等价。
- [ ] `M13-11` 依赖审计区分生产、构建、测试依赖并设置严重级别门。
- [ ] `M13-12` SBOM、license、source offer 与每个 release archive hash 绑定。
### M14 跨浏览器与设备扩展
- [ ] `M14-01` 先冻结 Chromium RC不在同一变更中同时追三个浏览器差异。
- [x] `M14-02` Firefox capability probe永久跳过Chromium-only 铁律)。
- [x] `M14-03` Firefox P0 流程永久跳过Chromium-only 铁律)。
- [x] `M14-04` Firefox 主线程/Offscreen永久跳过Chromium-only 铁律)。
- [x] `M14-05` WebKit capability probe 和 P0 流程永久跳过Chromium-only 铁律)。
- [ ] `M14-06` WebKit 主线程视口先通过,再领取 Offscreen 路径。
- [ ] `M14-07` 浏览器不支持 WebGPU 时只隐藏明确依赖 WebGPU 的能力。
- [ ] `M14-08` 内存上限按浏览器/设备档位选择已声明预算,不自动扩大。
- [ ] `M14-09` 触控、笔、HiDPI、IME 和非 US 键盘分别建立输入 fixture。
- [ ] `M14-10` 每个新增浏览器加入 quick/P0/full 三层 CI 后才进入发布声明。
### M15 全域对标和下一发布
- [ ] `M15-01` 每季度从 Blender 5.2 operator/data-block 清单重新生成 parity 差距。
- [ ] `M15-02` 每个 family 的 `COMPLETE` 必须没有 summary-only、proxy-success 或隐式 loss。
- [ ] `M15-03` LOCAL_EXACT 必须有相同 fixture 的 desktop/WASM 语义或像素 golden。
- [ ] `M15-04` LOCAL_EQUIVALENT 只用于平台外壳,必须验证最终 Main/文件语义与 desktop 一致。
- [ ] `M15-05` SERVER_EXACT 必须验证真实 Blender 提交、取消、隔离、预算、hash 和结果绑定。
- [ ] `M15-06` BLOCKED/UNINVENTORIED 必须保持 UI fail-closed、文件保真和可诊断状态。
- [ ] `M15-07` 全部生成的 owner family 分别达到自身退出条件,禁止由当前 12-family 或 N-026
聚合状态反推完整 Blender core 已完成。
- [ ] `M15-08` 全域发布候选重新跑 clean build、acceptance、性能、fault 和安全矩阵。
- [ ] `M15-09` 在独立机器复验 binary/source archive 和所有 hash。
- [ ] `M15-10` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。
历史领取顺序为先完成 M5随后 M6、M7再在 M8-M11 中一次选择一个最小闭环。当前只领取
机器 `nextTask`M12 起使用完整对标计划的原子任务M15 是状态审计和差距生成,不是把剩余
功能一次性打包。

44
docs/EXECUTION_QUEUE.md Normal file
View File

@@ -0,0 +1,44 @@
# Web Blender 短周期执行入口
更新时间2026-08-20America/New_York
本页是唯一的当前任务指针不保存历史任务表、实现日志或长期规划。领取任务前只读取本页、当前任务上下文、parent manifest 和 parent status完整规则见 [`CONTEXT_BUDGET.md`](CONTEXT_BUDGET.md)。
## 当前指针
| 字段 | 值 |
| --- | --- |
| 里程碑 | M16 Main、Mesh、Modifier、Sculpt |
| 当前任务 | `M16-GAP-00096` |
| parent manifest | `tests/golden/M16-GAP-00095/manifest.json` |
| 任务卡 | [`tasks/M16-GAP-00096.md`](tasks/M16-GAP-00096.md) |
| 专项验收 | `npm --prefix web run test:generated-gap -- --task M16-GAP-00096` |
领取前执行:
```bash
npm --prefix web run test:task-context
npm --prefix web run test:context-governance
node tools/web/print-task-context.mjs
```
如果任务卡缺失,运行 `node tools/web/generate-task-card.mjs --task <task-id>`;如果索引过期,运行 `node tools/web/generate-task-index.mjs` 后再生成任务卡;不要打开或复制完整 `next-task-plan.json`
## 铁律
- 浏览器执行、CI、验收证据和发布声明永久仅限 Chromium禁止领取 Firefox/WebKit 任务。
- 当前任务必须等于 parent manifest 的 `nextTask`;不得从 Markdown 历史编号手工推导。
- 一个任务只能有一个主要行为变化或一个独立证据变化。实现、浏览器接线、负例和证据需要拆成明确子任务时,沿任务卡的门逐项完成。
- 完成必须有真实命令、退出码、构件 SHA-256、manifest 和 status协议、fixture 或测试文件存在不等于完成。
- 失败、环境缺失或 hash 漂移保持 `in_progress`/`blocked`,不得推进指针。
## 交接
```text
node tools/web/check-task-context.mjs --task <task-id> --write
```
完成后只写入 `tests/golden/<task>/task-context.json`、任务报告、manifest 和 `docs/status/<task>.md`。下一任务只读新 manifest 的 `nextTask`;长日志留在构件目录。
提交前运行 `node tools/web/check-context-governance.mjs``git diff --check`;门禁失败时保持原 task
不得手工推进 `nextTask`

View File

@@ -2,6 +2,8 @@
更新日期2026-08-09 更新日期2026-08-09
> 专项背景归档本文不进入默认上下文PBR 任务必须通过最小上下文工具按卡片追加读取。
## 1. 目标和声明边界 ## 1. 目标和声明边界
目标是在浏览器 3D Viewport 中提供与 Blender Principled 工作流一致、可保存、可重开的 目标是在浏览器 3D Viewport 中提供与 Blender Principled 工作流一致、可保存、可重开的

View File

@@ -1,6 +1,14 @@
# Web Blender 项目现状与后续连续任务 # Web Blender 项目现状与后续连续任务
更新时间2026-08-14 更新时间2026-08-19
> 背景状态快照不作为当前任务入口。当前任务、parent 和 `nextTask` 只认
> `docs/EXECUTION_QUEUE.md`、任务上下文和机器 manifest本文件中的“当前”仅指快照生成时点。
当前短周期任务、领取顺序和阶段退出条件统一维护在
`docs/EXECUTION_QUEUE.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为
V1 的逐项领取顺序V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准M12-M23 全功能
实施规则以 `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准。
## 1. 当前结论 ## 1. 当前结论
@@ -15,6 +23,32 @@ Blender C/C++ 子集编译为 WASMOPFS 保存大文件IndexedDB 保存小
SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM 和其他 SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM 和其他
运行时资源全部使用项目本地文件,不依赖 CDN。 运行时资源全部使用项目本地文件,不依赖 CDN。
`Web Blender Modeler V1` 的本地 RC 已收口ledger 的 50 条 acceptance 声明去重为 49 条
命令并全部通过126/126 Chromium full E2E 通过17 条 release record 为 `READY` 且 0 missing。
这不改变完整 Blender 5.2 对标结论N-015 至 N-026 仍为 12/12 `parityStatus=BLOCKED`
### 1.1 2026-08-15 重新盘点快照
| 状态轴 | 已完成 | 尚未完成 | 判断 |
| --- | --- | --- | --- |
| V1 release family | 12/12 `READY` | 0 | V1 已声明功能闭环完成 |
| Blender 5.2 全域 parity | 0/12 complete | 12/12 `BLOCKED` | 不是完整 Blender Web 移植 |
| slice 台账 | 195 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 |
| acceptance | 50 declarations / 49 unique passed | 0 failed | 本地 V1 RC 证据完整 |
| M6 可部署 RC | 71/71 原子任务 | 0 | 可部署 RC 已冻结 |
| M7 核心体验硬化 | 18/18 原子任务 | 0 | 已完成并进入持续回归 |
| M8 VDB 自动分页 | 20/20 原子任务 | 0 | 联合重开与 desktop/main/Offscreen 三轴 golden 通过 |
| M9 非 Mesh/GP/Paint | 14/14 原子任务 | 0 | `TOGGLE_CYCLIC` 已通过 Main/undo/save/reopen 与 Blender 5.2 goldenGP current-drawing marquee、2D/3D 共享 selection revision 及 layer/frame reorder 已通过 Main/undo/save/reopen 门Paint 主线程/Offscreen 真实 GPU depth、单 undo 分块 pointer session、normalize/limit/mirror 权重与 Blender 5.2 golden、packed/UDIM dirty tile 原子资产绑定,以及 46 项 PBVH brush 的 WASM 入口显式阻断已通过Curve、Grease Pencil、Paint 三域的 Worker restart、OOM、GPU release、小场景恢复已通过PBVH/桌面 brush 求值仍 BLOCKED |
| M10 GN/Shader/NLA/Simulation | 15/15 原子任务 | 0 | GN/Simulation cache、Shader、NLA 和 Physics 有界闭环全部完成M10-15 以四个隔离 Chromium Worker 分别通过性能、超预算/OOM-prevention、恶意输入和同会话小输入恢复门 |
| M11 Lighting/Render/Compositor/Media | 14/14 原子任务 | 0 | M11-01/02 已冻结字段 parity 并完成支持字段闭环M11-03 已让双 viewport 共用资源预算M11-04 已完成 Blender Eevee reference 图像指标M11-05/06 已完成最终渲染路由/provenanceM11-07/08 已完成有限 Compositor golden 与 Unsupported 全图阻断M11-09/10/11/12 已完成 codec/proxy/revision/export gateM11-13 已完成实时 AudioContext 恢复门M11-14 已完成 Render/Compositor/Media 三域取消、重启、预算、释放和恢复门 |
| M12 Asset/IO/Editors | M12-01A-I + M12-02A-H + M12-03A-N + M12-04A-J + M12-05A-F + M12-06A-G + M12-07A-J 共 64 项 | 后续 IO/Editor 仍未完成 | Archive 安全组、pinned Blender 5.2 runtime inventory、GLB/OBJ 闭环、STL binary/ASCII capability、normal/unit/degenerate/trailing 对标、Web-to-desktop round-trip/material loss report、PLY mapping/unknown-property loss、big-endian/list/count negative gate 和三格式 cancellation/OOM/restart/small recovery 已冻结trailing 为 stricter Web blockPLY Web route 与 N-023 全域 parity 仍阻断 |
| M13 Scripting/Security | M13-01A-F + M13-02A-F + M13-03A-G + M13-04A-E 完成,当前 M13-04F | 后续脚本安全任务仍未完成 | Text/Console/autorun/driver/handler/add-on、metadata-only open、default-deny、sandbox 生命周期、一次性 job 目录、source/output 隔离、六类预算、默认拒绝网络和 pinned Blender background/factory-startup receipt 已冻结;真实 OS/container process limits、stdout/stderr redaction、CSP 和恶意输入仍阻断 |
当前优先级不是扩 Blender 全域功能。single/pthread、真实 HTTP、缓存升级、离线闭环、
独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过M7 核心项目体验硬化
18/18 已完成并进入持续回归M9 已完成 14/14M10 已完成 15/15 并进入持续回归M11 已完成
14/14M12-01A-I、M12-02A-H、M12-03A-N、M12-04A-J、M12-05A-F、M12-06A-G 和 M12-07A-J 已完成M13-04A-E 也已完成,机器队列的当前唯一 `nextTask``M13-04F`
## 2. 已完成并有测试覆盖的能力 ## 2. 已完成并有测试覆盖的能力
| 能力 | 当前证据 | 状态 | | 能力 | 当前证据 | 状态 |
@@ -30,17 +64,22 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
| Packed image | PackedFile、双 tile UDIM、generated、linked library、损坏签名、路径沙箱和 OPFS SHA-256 去重/重发现 | 已完成当前资源矩阵 | | Packed image | PackedFile、双 tile UDIM、generated、linked library、损坏签名、路径沙箱和 OPFS SHA-256 去重/重发现 | 已完成当前资源矩阵 |
| GLB | 本地导出、严格解析,以及 Blender 5.2 回导比较网格/PBR/纹理/morph/skin/animation | 已完成当前 SceneIR 子集 | | GLB | 本地导出、严格解析,以及 Blender 5.2 回导比较网格/PBR/纹理/morph/skin/animation | 已完成当前 SceneIR 子集 |
| Modifier/undo | modifier enable 状态、命令 revision、undo/redo 和专项 smoke | 已完成基础命令链路 | | Modifier/undo | modifier enable 状态、命令 revision、undo/redo 和专项 smoke | 已完成基础命令链路 |
| Geometry Nodes | Main 图 reader、16 节点 schema 1 allowlist、11 个 Blender 5.2 desktop fixture、full-Main WASM/Chromium 保存重开求值 golden以及 7 域元素/字节/批次 materialization 预算 | 已完成 M10-04 有界闭包;超过 65,536 scalar 的 JSON field 明确阻断并要求 binary任意 field/domain 实际求值、图写回和 Simulation Zone 仍阻断 |
| Mesh Edit | 点/边/面选择Merge/Dissolve/Extrude/Inset/Bevel/Loop CutMain 保存重开 | 已完成当前操作集 | | Mesh Edit | 点/边/面选择Merge/Dissolve/Extrude/Inset/Bevel/Loop CutMain 保存重开 | 已完成当前操作集 |
| 材质与 UV | 材质槽/面分配、UV Map、planar/cube unwrap、限定 Principled/Image/Normal 图、图片打包 | 已完成当前节点集 | | 材质与 UV | 材质槽/面分配、UV Map、planar/cube unwrap、限定 Principled/Image/Normal 图、图片打包 | 已完成当前节点集 |
| 动画编辑 | TRS 关键帧、Timeline/Dope Sheet、插值、Action 选择和约束开关/影响 | 已完成当前操作集 | | 动画编辑 | TRS 关键帧、Timeline/Dope Sheet、插值、Action 选择和约束开关/影响 | 已完成当前操作集 |
| 对象层级 | Parent/Unparent、Collection、rename、Join/Separate、Apply Transform、Origin | 已完成当前操作集 | | 对象层级 | Parent/Unparent、Collection、rename、Join/Separate、Apply Transform、Origin | 已完成当前操作集 |
| SceneIR 扩展 | tangent/split normal/seam/sharp/vertex group/attribute/UVnative WBG1、资源 Delta 和 mesh byte-range patch | 已完成当前数据契约 | | SceneIR 扩展 | tangent/split normal/seam/sharp/vertex group/attribute/UVnative WBG1、资源 Delta 和 mesh byte-range patch | 已完成当前数据契约 |
| Sculpt 属性 | Mask/Face Set 的 SceneIR、WBG1 header v2、Delta、Main、undo 和保存重开 | 已完成属性子集;有界四笔刷 Main stroke 已开放PBVH/完整求值未开放 | | Sculpt 属性 | Mask/Face Set 的 SceneIR、WBG1 header v2、Delta、Main、undo 和保存重开 | 已完成属性子集;有界四笔刷 Main stroke 已开放PBVH/完整求值未开放 |
| Simulation cache | Blender 5.2/source blend/hash/frame manifest、OPFS 内容寻址、schema 6、重启逐帧复验 | 已完成缓存安全层Simulation evaluator 未开放 | | Simulation cache | Blender 5.2/source blend/hash/frame manifest、manifest schema 2 的 graph/source/revisionHash binding、OPFS 内容寻址、storage schema 7、Worker 生命周期播放准入、取消、确定性 LRU、重启全量复验和损坏 quarantine | 已完成缓存 identity/播放生命周期安全层Simulation Zone evaluator、GN modifier seek 接入仍未开放 |
| 发布性能/故障门 | Chromium 118 条 E2E、1M geometry、600 帧 OPFS cache、4K/8K texture、运行中断网、主线程 WebGL context loss | 已完成已声明门10M、长媒体、OOM 和 Offscreen device loss 仍 | | 发布性能/故障门 | Chromium E2E、1M/10M geometry、1M 帧长媒体、600 帧 OPFS cache、4K/8K texture、运行中断网、主线程 WebGL context loss、WASM/OPFS/GPU/NanoVDB 四类确定性 OOM 恢复 | V1 evidence gate 已齐;Offscreen device loss 仍属后续完整故障矩阵 |
| Shader Main | RGB/Value/Principled/Image Texture/Normal Map/Output 整图事务、资源门和保存重开 | 已完成六节点写回子集;任意图/编译仍阻断 | | 实时渲染 reference | Blender 5.2 Eevee 固定相机/黑体/World fixtureSRGB8 MAE、RMS、P95、坏像素比例、前景 IoU、alpha 覆盖率;主线程/Offscreen 同帧 | M11-04 有界 golden 已完成;完整 AgX/高级灯光、Cycles/复杂 Eevee、Volume 深度合成仍阻断 |
| NLA Main | 单对象 Action Clip 整栈写回、SceneIR 重开和 native frame 求值 | 已完成有限 Action Clip 子集 | | 最终渲染路由 | bounded Eevee WebGL2 可本地WebGPU 需 browser+bundle 双门Cycles、复杂 Eevee、Workbench 和桌面硬件后端只返回 `SERVER_JOB` | M11-05 路由合同已完成M11-06 server job 已绑定 source/settings/build/output hash真实远程队列、进度、取消和 denoise 仍属后续任务 |
| 非 Mesh 数据块 | Curve/Surface/Font/Metaball Main 权威写回与保存重开、真实 evaluated mesh、PointCloud/Curves/Hair 属性、WNM chunks、双 viewport、handle-local 连续 preview/单次 Main commit、desktop golden 与 7 对象 GLB/USD round-trip | N-015 整体 BLOCKEDVDB 资源 catalog、desktop OpenVDB->NanoVDB 与分块协议已完成server/WebGPU/保存重开/golden 未完成 | | Compositor CPU/WebGPU golden | Constant Color、Exposure、Invert、Composite 的单链由真实 Blender 5.2 Main fixture 驱动CPU 与 Chromium WebGPU compute 输出 Float32 hash 等价 | M11-07 有界 allowlist 已完成M11-08 已让 CPU/cached 在完整图预检时阻断并保留 Unsupported Main graph资源输入、分支、其他节点、完整色彩/HDR 与生产调度仍阻断 |
| Sequencer codec/proxy/revision/export/audio gate | IMAGE/SOUND/MOVIE receipt 绑定 family、MIME、bytes 和 source hashmovie RGBA8 proxy identity 再绑定 READY receipt、profile 和 source frameSEEK/SCRUB/DECODE 共享 revision gate最终导出只路由 server实时音频显式报告 context/output/mute 状态 | M11-09..13 已完成有界解码、首帧 proxy、迟到结果、最终编码路由和真实 Chromium AudioContext suspend/resume/mute/device 门;帧精确 seek、多帧生成、waveform、A/V sync、实际混音和真正 server encode job 仍阻断 |
| Shader Main/Web compiler | RGB/Value/Math/Principled/Image Texture/Normal Map/Output 整图事务、资源门、保存重开、有界 WebGL2 Three physical compile report、身份 compile key 和失败回滚 | 已完成 M10-09 当前切片;完整色彩管理/sampler、WebGPU 和任意图仍阻断 |
| NLA Main | 单对象 Action Clip 整栈写回、SceneIR 重开和 native frame 求值M10-11 对 scale/reverse/repeat 做 12 帧 goldenM10-12 `moveNLAStrip` 通过 Main/history/save/reopen | 已完成只读精确求值与单个移动 operator 子集create/remove/resize、多轨 blend、Transition/Meta/Sound 仍阻断 |
| 非 Mesh 数据块 | Curve/Surface/Font/Metaball Main 权威写回与保存重开、真实 evaluated mesh、PointCloud/Curves/Hair 属性、WNM chunks、双 viewport、handle-local 连续 preview/单次 Main commit、desktop golden 与 7 对象 GLB/USD round-trip | N-015 整体 BLOCKEDVDB 自动 demand paging、联合重开、64 MiB sparse 门和 desktop/main/Offscreen 三轴 density golden 已完成,深度合成与完整 Volume GLB/USD/材质仍阻断 |
## 3. 部分完成或仍有边界的能力 ## 3. 部分完成或仍有边界的能力
@@ -52,8 +91,8 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
| GLB round-trip | Web 严格解析与 Blender 5.2 importer 双门已覆盖网格、变换、PBR、PNG/sRGB、sampler、morph、双骨骼 skin 和动画帧 | 新增相机/灯光、插值和材质扩展时继续扩大独立摘要 | | GLB round-trip | Web 严格解析与 Blender 5.2 importer 双门已覆盖网格、变换、PBR、PNG/sRGB、sampler、morph、双骨骼 skin 和动画帧 | 新增相机/灯光、插值和材质扩展时继续扩大独立摘要 |
| `.blend` 保存 | 场景状态、modifier、Mesh Edit、材质/UV、动画/约束和对象层级命令均写 Main历史、原子保存、快照和日志重放可恢复 | skin/shape-key decimate 保持结构化阻断 | | `.blend` 保存 | 场景状态、modifier、Mesh Edit、材质/UV、动画/约束和对象层级命令均写 Main历史、原子保存、快照和日志重放可恢复 | skin/shape-key decimate 保持结构化阻断 |
| Blender UI 对标 | 默认区域、当前 Mesh Edit、raycast、多选、拖拽 gizmo、层级/材质/动画面板和核心快捷键已闭环 | Sculpt、Geometry Nodes 编辑器等未声明能力继续结构化阻断 | | Blender UI 对标 | 默认区域、当前 Mesh Edit、raycast、多选、拖拽 gizmo、层级/材质/动画面板和核心快捷键已闭环 | Sculpt、Geometry Nodes 编辑器等未声明能力继续结构化阻断 |
| 未声明能力扩展 | 四项协议/安全门均已细分Sculpt 属性/有界 Main stroke、Simulation cache、节点 Shader Main 和 Action Clip NLA Main 已有正例 | PBVH、GN lazy-function/Simulation 求值、其余 Shader Node、Web compiler、完整 NLA/UI/GLB 图映射仍 planned/结构化阻断 | | 未声明能力扩展 | 四项协议/安全门均已细分Sculpt 属性/有界 Main stroke、Simulation cache、节点 Shader Main/有界 Web compiler、compile key/rollback、未知节点稳定 capability block、Action Clip NLA Main/只读精确求值和 `moveNLAStrip` Main/history/save/reopen 已有正例 | PBVH、GN lazy-function/Simulation 求值、其余 Shader Node、完整色彩/sampler/WebGPU、完整 NLA/UI/GLB 图映射仍 planned/结构化阻断 |
| 非 Mesh 数据块 | 1M 点 reader budget、ID_MB/PT/VO/VF 注册、Main control-point/topology/body/element/Font style link commands、一维多 spline 与批量 handle/cyclic、二维 Surface topology transaction、undo/save/reopen、真实 evaluated mesh、WNM SHA chunks、handle raycast/handle-local 连续 preview/单次 commit、7 对象 GLB/USD、Chromium quota、VDB/NanoVDB 协议与 desktop converter | 新外部字体导入及 VDB server job、WebGPU renderer、材质、保存重开和 golden 仍 planned/阻断 | | 非 Mesh 数据块 | 1M 点 reader budget、ID_MB/PT/VO/VF 注册、Main control-point/topology/body/element/Font style link commands、一维多 spline 与批量 handle/cyclic、二维 Surface topology transaction、undo/save/reopen、真实 evaluated mesh、WNM SHA chunks、handle raycast/handle-local 连续 preview/单次 commit、7 对象 GLB/USD、Chromium quota、VDB desktop/server converter、自动 demand paging、联合重开和三轴 golden | 新外部字体导入,以及 VDB 深度合成、完整 Principled Volume/GLB/USD 映射和更大真实 bundle 联合故障矩阵仍 planned/阻断 |
| 大场景 | Worker 二进制解码、per-mesh/range transferable、linked Mesh 实例化、LOD/OPFS cache、frustum culling、能力门 OffscreenCanvas Worker、100k/1M 内存门 | 超出当前 WBG1 的网络式渐进流送仍属于后续性能扩展 | | 大场景 | Worker 二进制解码、per-mesh/range transferable、linked Mesh 实例化、LOD/OPFS cache、frustum culling、能力门 OffscreenCanvas Worker、100k/1M 内存门 | 超出当前 WBG1 的网络式渐进流送仍属于后续性能扩展 |
| Decimate 全对标 | Collapse 的当前网格集合已覆盖,错误路径结构化 | Un-Subdivide、Dissolve、所有 delimiter/权重/对称组合及大模型性能矩阵未完成 | | Decimate 全对标 | Collapse 的当前网格集合已覆盖,错误路径结构化 | Un-Subdivide、Dissolve、所有 delimiter/权重/对称组合及大模型性能矩阵未完成 |
@@ -172,8 +211,8 @@ Sculpt、Geometry Nodes/Simulation、Shader Node 图和 NLA 的详细任务分
`docs/UNDECLARED_CAPABILITIES_EXECUTION_PLAN.md`。当前四项均进入 `docs/UNDECLARED_CAPABILITIES_EXECUTION_PLAN.md`。当前四项均进入
`in_progress`:四项都已超过纯协议阶段,但只开放经过 native/存储闭环验证的有限切片: `in_progress`:四项都已超过纯协议阶段,但只开放经过 native/存储闭环验证的有限切片:
Sculpt Mask/Face Set 与有界 Main stroke、GN 外部资源门与 deterministic cache 存储、七节点 Sculpt Mask/Face Set 与有界 Main stroke、GN 外部资源门与 deterministic cache 存储、七节点
Shader Main 写回(含有限 Math常量 PBR 映射、单对象 Action Clip NLA Main/Depsgraph reverse/repeat 求值。详细完成项和 planned 项见专项计划; Shader Main 写回(含有限 Math常量 PBR 映射、M10-07 有界 Web compiler、M10-08 compile key、M10-09 pipeline rollback、单对象 Action Clip NLA Main/Depsgraph reverse/repeat 求值。详细完成项和 planned 项见专项计划;
这些正例不扩大到 PBVH Sculpt、任意 GN/Simulation、任意 Shader Node/Web compiler 或完整 NLA。 这些正例不扩大到 PBVH Sculpt、任意 GN/Simulation、任意 Shader Node、完整色彩/sampler/WebGPU 或完整 NLA。
Blender 5.2 全域功能矩阵、浏览器/服务端边界和 N-015 至 N-026 后续顺序见 Blender 5.2 全域功能矩阵、浏览器/服务端边界和 N-015 至 N-026 后续顺序见
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` `docs/BLENDER_5_2_WEB_FEATURE_PARITY.md`
@@ -184,10 +223,16 @@ save/reopen、真实 legacy evaluated mesh、PointCloud/Curves 属性和 WNM 二
主线程与 OffscreenCanvas 消费同一 chunksGLB 只接受真实 evaluated meshUSD 提供 主线程与 OffscreenCanvas 消费同一 chunksGLB 只接受真实 evaluated meshUSD 提供
`UsdGeomMesh``UsdGeomPoints``UsdGeomBasisCurves` 的机器可读映射/损失。 `UsdGeomMesh``UsdGeomPoints``UsdGeomBasisCurves` 的机器可读映射/损失。
VDB 已改为 desktop/server OpenVDB -> NanoVDB、浏览器分块读取 + WebGPU。真实资源 catalog、 VDB 已改为 desktop/server OpenVDB -> NanoVDB、浏览器分块读取 + WebGPU。真实资源 catalog、
desktop converter 和协议/range 基础件已接通,但 server job、renderer、材质、保存重开和 golden 仍阻断handle-local 专用 gizmo desktop/server converter、HTTP 断点续传、OPFS 原子绑定、Float32 WebGPU 双生产视口和 Main
已接通,新字体导入等剩余项按 `docs/status/N-015.md` Volume 属性、asset binding、双视口联合重开、自动 GPU page feedback、64 MiB sparse 门与
desktop/Chromium 发布 goldenmain/Offscreen 三轴 density已有真实证据深度合成、完整材质与 Volume GLB/USD 仍阻断。handle-local 专用 gizmo 已接通,新字体导入等剩余项按 `docs/status/N-015.md`
`docs/VDB_NANOVDB_WEBGPU_IMPLEMENTATION_PLAN.md` 推进。 `docs/VDB_NANOVDB_WEBGPU_IMPLEMENTATION_PLAN.md` 推进。
M9-14 已新增 `docs/status/M9-14.md``tests/golden/M9-14/manifest.json` 和统一
`editing-domain-recovery` runner。三个真实 fixture 均完成 Main writer 后保存/Worker 重开、
token-isolated OOM 清理、WebGL2 资源释放/重建与小场景像素恢复;该闭环不扩大 N-015/N-016/N-017
对完整 Blender topology、2D Grease Pencil 或 PBVH brush 的 parity 声明。
### PBR-001 至 PBR-012 模型物理渲染(核心切片 done_current_scope资产/高级渲染安全门 in_progress ### PBR-001 至 PBR-012 模型物理渲染(核心切片 done_current_scope资产/高级渲染安全门 in_progress
`physical-v1` 已完成扩展 Principled 参数的 Main 写回/保存重开、双后端 `physical-v1` 已完成扩展 Principled 参数的 Main 写回/保存重开、双后端
@@ -202,11 +247,44 @@ HDR/EXR、多 tile 采样、Offscreen context-loss、真实 WebGPU renderer、
### N-026 当前发布实况 ### N-026 当前发布实况
`docs/status/release-evidence.json` 已生成 13 条成功 command/output/artifact-hash 记录;其中 `docs/status/release-evidence.json` 已生成 17 条成功 command/output/artifact-hash 记录;其中
VDB 边界记录没有设置发布 true 字段,只证明 Phase 0 协议门。 VDB 专项记录覆盖真实 converter、server job、HTTP/OPFS 和有界 WebGPU 当前切片,独立 OOM
`test:release-evidence` 计算为 15 个缺失并保持 `BLOCKED`。其中 3 个是实际证据字段 记录覆盖 WASM/Main、OPFS staging、GPU 资源组和 NanoVDB resident/page-tableM8 后续专项已补齐
`performance.geometry10M``performance.longMedia``faults.oom`,其余 12 个来自仍为 自动分页、64 MiB sparse、联合重开和 density 三轴 golden但不改写已冻结 V1 release record。
`BLOCKED`N-016 至 N-026 family。浏览器范围仅为 Chromium。 发布账本现已拆分 `parityStatus``releaseStatus`N-015 至 N-026 的全域对标仍全部
`BLOCKED`,但 N-015 至 N-026 的 V1 有界切片均为 `releaseStatus=READY`,不会再因发布后能力
阻塞 V1。P0 用户闭环、10M geometry、long media 和 OOM 均有独立 command/output/artifact-hash
记录;当前为 0 个 V1 缺失release gate 为 `READY`。浏览器范围仅为 Chromium。
`docs/status/v1-acceptance-evidence.json` 逐条覆盖 12 个 family 的 50 条声明、49 个唯一命令,
结果为 49 passed、0 failed其 SHA-256
`d5fb0a7b2df92b076f8b05ccd68903d60e3352b9423b0c1404065f6cbcbdd45c` 已绑定到最终
`v1-user-loop` release record。desktop USD 使用经发布 SHA-256 校验的官方 Blender 5.2.0 LTS
runtime `USD=True`。当前 M6 离线 binary/source archive SHA-256 分别为
`ca09540e8f5c0157bb85d62c98aeb3291ea195a29585bc8e05afb308747a8ff5`
`95b3a747425081a205b1966c004a08da4cb37aab7ba0a649ff076713f7010f74`。release lane 19/19
子命令通过,总命令耗时 1,245,732 msquick/Chromium/release 报告联合校验同一 commit。
`M6-01` 已新增机器可读部署合同并让 Vite/离线包生成器复用该事实源。`M6-02` 已新增纯
pthread 能力门。`M6-03A-E` 已在没有 COOP/COEP/CORP 的独立 Chromium server 验证
`crossOriginIsolated=false``SharedArrayBuffer` 不可用且 pthread gate `BLOCKED` 时,
single-thread 引擎仍能加载真实 WASM、打开 `basic_scene.blend` 并执行 Main edit。`M6-04A`
已冻结 schema v2 和纯校验器single/pthread 各自声明 JS、WASM、pthread worker、SHA-256 与
16 MiB 至 2 GiB 页级内存边界。`M6-04B` 已从 `build_web-single``build_web-pthread`
安装显式 variant 资产,生产 manifest 已切换到 schema v2安装专项覆盖逐字节复制、四项 hash、
同目录和缺失资产不改写,且无隔离 single-thread 真实 Main 编辑回归通过。旧 single Worker 入口
暂时保留到选择器接线。`M6-04C-G` 已完成纯选择、AUTO/required 能力矩阵和双环境实机:
隔离 Chromium 加载 pthread shared memory 与 pool worker无隔离 Chromium 只加载显式 single
variant并继续完成真实 Main 编辑。`M6-05A-E` 已验证确定性 pthread 失败只回退一次、完整清理
且 pending project 只 open 一次。`M6-06A``M6-07G` 已由同一真实 HTTP server 一次通过 9 种
MIME、4 个 HEAD、WASM/BLEND/NVDB range、6 类 `416`、强 ETag、If-Range 和断流续传/换代拒绝。
`M6-08C-E` 已将生产双 variant manifest 绑定同一 `releaseId`:版本不一致只返回
`REFRESH_REQUIRED` 且 initialize/open 均为 0真实 Chromium 篡改 pthread WASM 后返回稳定
`ENGINE_VARIANT_INTEGRITY_FAILED`,不请求 single、不 fallback、不 open。`M6-08A/B` 已用实际
production build 验证 HTML/manifest/stable engine `no-cache`、5 个 hashed JS/CSS/Worker
`immutable`,以及未换代 `304`、换代后旧 ETag 得到完整 `200` 的重验证行为。M6 最终
quick 7/7、Chromium 9/9、release 25/25 与 RC manifest 联合通过M7 已完成 18/18
后续领取点只读取机器队列最新 `nextTask`,本文不缓存任务名称。
## 6. 当前验收命令 ## 6. 当前验收命令
@@ -219,6 +297,7 @@ node tools/web/run-web-engine-smoke.mjs
npm --prefix web run typecheck npm --prefix web run typecheck
npm --prefix web run lint npm --prefix web run lint
npm --prefix web test npm --prefix web test
npm --prefix web run test:engine-manifest-v2
npm --prefix web run test:golden npm --prefix web run test:golden
npm --prefix web run test:frame-evaluation npm --prefix web run test:frame-evaluation
npm --prefix web run test:pose-constraint-goldens npm --prefix web run test:pose-constraint-goldens
@@ -236,6 +315,8 @@ npm --prefix web run test:capability-gates
npm --prefix web run test:simulation-cache npm --prefix web run test:simulation-cache
npm --prefix web run test:release-performance npm --prefix web run test:release-performance
npm --prefix web run test:simulation-cache-performance npm --prefix web run test:simulation-cache-performance
npm --prefix web run test:render-reference
npm --prefix web run test:render-routing
npm --prefix web run test:network-interruption npm --prefix web run test:network-interruption
npm --prefix web run test:device-loss npm --prefix web run test:device-loss
npm --prefix web run test:texture-4k-performance npm --prefix web run test:texture-4k-performance
@@ -245,6 +326,15 @@ npm --prefix web run test:browser
npm --prefix web run release:offline npm --prefix web run release:offline
npm --prefix web run release:evidence npm --prefix web run release:evidence
npm --prefix web run test:release-evidence npm --prefix web run test:release-evidence
npm --prefix web run test:v1-acceptance-coverage
BLENDER_BIN=/path/to/usd-enabled/blender \
BLENDER_ARCHIVE_SHA256=<sha256> npm --prefix web run release:v1-acceptance
npm --prefix web run test:v1-acceptance-evidence
npm --prefix web run test:v1-acceptance-release-binding
node tools/web/check-deployment-contract.mjs
npm --prefix web run test:deployment-http
npm --prefix web run test:deployment-cache
WEB_TEST_PORT=5202 npm --prefix web exec -- playwright test --config web/playwright.single-thread.config.ts
WEB_TEST_PORT=5193 npm --prefix web run test:e2e # 5173 被占用时指定可用端口 WEB_TEST_PORT=5193 npm --prefix web run test:e2e # 5173 被占用时指定可用端口
WEB_TEST_PORT=5193 npm --prefix web run test:e2e -- -g "N-015|non-mesh" WEB_TEST_PORT=5193 npm --prefix web run test:e2e -- -g "N-015|non-mesh"
``` ```

50
docs/README.md Normal file
View File

@@ -0,0 +1,50 @@
# 文档导航
本目录按“产品契约、当前任务、实现事实、长期规划、验收证据”分层。领取任务时不要从仓库根目录
开始通读;只读取当前队列和机器生成的最小上下文包。预算和自动检查见
[`CONTEXT_BUDGET.md`](CONTEXT_BUDGET.md)。
完整拆分、预算、交接和提交规则见 [`CONTEXT_GOVERNANCE.md`](CONTEXT_GOVERNANCE.md)。
## 领取任务
1. [EXECUTION_QUEUE.md](EXECUTION_QUEUE.md):唯一的短周期入口和当前指针。
2. `node tools/web/print-task-context.mjs`:只输出当前任务需要的四份输入。
3. `tasks/<task>.md`:当前任务唯一的范围、输入和验收;缺卡时先修复生成流程,不要打开完整 plan。
4. parent `tests/golden/<parent>/manifest.json``status/<parent>.md`:只提供交接事实。
需要拆分实施或控制上下文时,先看 [CONTEXT_GOVERNANCE.md](CONTEXT_GOVERNANCE.md)
[TASK_BREAKDOWN.md](TASK_BREAKDOWN.md) 只提供阶段地图和归档示例,不覆盖 manifest/status 的机器事实。
任务卡使用 [TASK_CONTEXT_TEMPLATE.md](TASK_CONTEXT_TEMPLATE.md) 的固定结构;卡片只描述一个主要
行为或一个证据变化,不复制实现日志。全量 gap 计划由机器索引切片,`check-task-context` 会阻止
超预算、缺卡、过期索引或 parent 指针错误进入队列。
## 事实源
| 问题 | 文件 |
| --- | --- |
| V1 承诺、非目标、发布门 | [WEB_BLENDER_MODELER_V1_SCOPE.md](WEB_BLENDER_MODELER_V1_SCOPE.md) |
| 当前领取顺序 | [EXECUTION_QUEUE.md](EXECUTION_QUEUE.md) + 当前 manifest 的 `nextTask` |
| 当前实现和风险 | [PROJECT_STATUS_AND_NEXT_WORK.md](PROJECT_STATUS_AND_NEXT_WORK.md) |
| M12-M23 长期原子计划 | [BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md](BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md) |
| 机器状态和发布证据 | `status/parity-ledger.json``status/release-evidence.json` |
| Blender 全域覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md``BLENDER_5_2_FULL_PARITY_WBS.md` |
## 其他文档
- `web/`部署、CI、依赖、协议和已知限制等稳定合同。
- `status/`:每个已完成或阻断任务的证据记录,不作为下一任务的唯一上下文。
- `tasks/`:当前及近期任务卡;完成后保留,便于审计和回滚。
- 根目录路线图和 `后续工作.txt`:历史设计与决策背景,不参与当前任务领取。
## 最小上下文规则
默认只加载队列、当前任务卡、parent manifest 和 parent status总量上限为 3,500 tokens。
实现细节按任务卡的文件清单追加读取。不要为了确认一个局部输入、IO 或安全任务而加载整份
`PROJECT_STATUS_AND_NEXT_WORK.md` 或完整 parity 计划。
`next-task-plan.json`、parity map 和 gap audit 是生成器输入,不是任务上下文;任务工具只读取
带 hash 的单行 catalog 记录。
文档描述与 manifest/evidence 冲突时,以可复验的机器状态为准;不要通过修改 Markdown
checkbox 覆盖失败证据。

234
docs/TASK_BREAKDOWN.md Normal file
View File

@@ -0,0 +1,234 @@
# 项目任务分解与最小上下文指南
更新时间2026-08-19America/New_York
> 拆分参考(归档):本文件不进入默认任务上下文。当前任务和依赖只由 `print-task-context` 与
> parent manifest 提供;这里只在需要设计新的子任务门时读取对应小节。
本文件是“如何拆任务、如何领取任务、如何交接”的规划索引,不是实现状态事实源。当前状态仍以
`docs/EXECUTION_QUEUE.md`、当前任务的 `manifest.json`、对应 `docs/status/<task>.md` 和可复验命令为准。
本文件的目标是让一次任务只加载必要上下文,不要求阅读整份路线图或历史接续日志。
## 1. 30 秒入口
每轮只按下面顺序读取:
1. `docs/EXECUTION_QUEUE.md`:确认唯一当前 `nextTask`、Chromium-only 规则和专项命令。
2. `node tools/web/print-task-context.mjs`:取得已经裁剪的单任务 JSON不要读取 plan、gap audit 或 parity map。
3. `docs/tasks/<task>.md`:读取目标、输入、范围、验收和回滚。
4. `tests/golden/<parent>/manifest.json`:确认 parent hash、依赖、运行时和下一任务。
5. `docs/status/<parent>.md`:只读取上一项的证据摘要和已知风险。
只有遇到以下问题才继续读取:
| 问题 | 追加读取 |
| --- | --- |
| 不确定产品是否承诺 | `WEB_BLENDER_MODELER_V1_SCOPE.md` |
| 不确定实现事实或已有命令 | `PROJECT_STATUS_AND_NEXT_WORK.md` 的相关小节 |
| 不确定长期依赖或全域差距 | `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 的对应 M/F 小节 |
| 不确定 family 状态 | `status/parity-ledger.json``release-evidence.json` |
| 不确定协议字段 | 任务卡列出的 `web/protocol/*` 文件,不扫描整个 `web/` |
不要把 `README.md``后续工作.txt`、完整路线图和全部 status 日志作为每轮默认上下文。
根目录路线图与历史接续记录仅用于背景;它们不能覆盖机器 manifest 的指针。
## 2. 文档职责和冲突处理
| 层级 | 唯一职责 | 可以回答 | 不能回答 |
| --- | --- | --- | --- |
| 产品契约 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 承诺、非目标、发布门 | 当前领取哪一项 |
| 短周期入口 | `EXECUTION_QUEUE.md` | 当前任务、parent、专项命令 | 实现是否真的通过 |
| 任务卡 | `tasks/<task>.md` | 单项范围、最小输入、验收、回滚 | 历史实现日志 |
| 机器事实 | `tests/golden/*/manifest.json``status/*.json` | hash、依赖、运行时、状态轴 | 人类意图 |
| 完成证据 | `status/<task>.md` | 实际命令、退出码、报告、风险 | 下一任务的推导顺序 |
| 长期规划 | `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23/F 域规划 | 当前队列指针 |
| 覆盖参考 | `BLENDER_5_2_FULL_PARITY_WBS.md` | Blender 全域检查表 | 独立完成证明 |
冲突时使用以下优先级:
```text
可复验命令输出 > manifest/evidence > 当前任务卡 > 执行队列描述 > 长期规划/历史日志
```
如果 manifest、status 和命令互相矛盾,先标记 `blocked`,不要修改 checkbox 或手工推进
`nextTask`
## 3. 原子任务规则
一个任务只能有一个主要行为变化,或一个独立证据变化。把“实现、浏览器接线、发布证据”混在一项
会导致上下文过大,也会让测试文件存在被误判成生产能力完成。
每项任务都要能回答以下六个问题:
| 字段 | 最小内容 |
| --- | --- |
| 输入 | parent manifest、一个最小 fixture、生产入口 |
| 行为 | 一个可观察的状态/数据/错误变化 |
| 边界 | 至少一个非法、取消、超限或重复事件 |
| 产物 | 一个协议/实现改动 + 一个 focused test/checker |
| 验收 | 一条首选命令,必要时追加 typecheck/build |
| 交接 | report、manifest、status、下一任务、回滚点 |
### 3.1 建议的子任务门
复杂能力按以下门拆开,每个门都可独立复验:
| 门 | 交付内容 | 常见文件 |
| --- | --- | --- |
| C 契约 | schema、稳定 ID、预算、错误码、revision 规则 | `web/protocol/*` |
| P 生产路径 | Main/Worker/App/viewport 真正消费契约 | `web/app/src/*``web/engine/*` |
| N 负例 | 非法、重复、取消、迟到、超限不改已提交状态 | `web/tests/unit/*` |
| B 浏览器 | Chromium 真实用户路径和可见状态 | `tools/web/check-*.mjs``web/tests/e2e/*` |
| R 资源 | dispose、取消、Worker 重启、OPFS/ GPU 预算归零 | Worker、storage、viewport |
| E 证据 | 报告、SHA-256、manifest、status、回滚 | `tests/golden/*``docs/status/*` |
任务卡可以把 C/P/N/B/R/E 写成子任务,但只有所有适用门通过后,主任务才可变为 `done`
### 3.2 生成 gap 的细分协议
M16 及后续全域 gap 采用“一条 gap、一个任务、五扇证据门”的固定粒度。任务 ID、gap、owner 和
实现类别由机器 catalog 提供;任务卡只补充该条 gap 的真实 fixture、生产入口和边界不复制全量
计划字段。
| 门 | 唯一交付 | 失败时的状态 |
| --- | --- | --- |
| C | 字段/稳定 ID/错误边界契约 | `in_progress`,不得写入成功报告 |
| D | desktop fixture 和可重跑报告 | `blocked``in_progress`,不得接 WASM |
| W | WASM/Main 消费同一 fixture | `in_progress`,不得声称 parity |
| R | save/reopen 或 revision 稳定性 | `in_progress`,不得推进队列 |
| E | comparator、manifest、status、rollback | 只有 E 完成才允许 parent `nextTask` |
生成目录中的 `next-task-plan.json` 是盘点输入,不是执行上下文。`generate-task-index.mjs` 将其切成
带源 hash 的 `task-index.json` 和一行一个任务的 `task-catalog.jsonl`;执行工具按 offset 读取单条
记录并由任务卡恢复命令模板。索引失效、任务卡缺失、parent 指针不一致或单卡超过 8 KiB 时,
领取门直接失败。
新任务卡使用 `node tools/web/generate-task-card.mjs --task <task-id>` 按 catalog 单条记录生成;生成器拒绝未知 ID、没有 parent 的首项和已有卡覆盖,除非显式传入 `--force`。生成后必须重新运行上下文门禁。
## 4. 项目阶段分解
下面是导航级分解;具体领取仍由短周期队列决定。
| 阶段 | 主题 | 交付边界 | 当前使用方式 |
| --- | --- | --- | --- |
| M0 | 范围与状态模型 | V1 契约、双轴 ledger、P0 用户闭环 | 已完成,持续回归 |
| M1 | 工作区收口 | 静态门、VDB 基线、P0 一致性 | 已完成,持续回归 |
| M2 | 大几何 | 10M geometry、LOD、取消、释放、恢复 | 已完成,持续回归 |
| M3 | 长媒体 | 索引、seek、取消、缓存、重开 | 已完成,持续回归 |
| M4 | OOM/fault | WASM、OPFS、GPU、VDB 确定性恢复 | 已完成,持续回归 |
| M5-M6 | V1/可部署 RC | 离线包、SBOM、CI、部署、升级、回滚 | 已完成,持续回归 |
| M7 | 核心体验 | action/dirty/save/restart/recent projects/input 基础 | 已完成,持续回归 |
| M8 | VDB 自动分页 | range/OPFS/LRU/双视口/device loss | 已完成,持续回归 |
| M9 | 非 Mesh/GP/Paint | 有界 reader/writer、Main、保存重开、故障 | 已完成的 V1 slice全域仍可能 BLOCKED |
| M10 | GN/Shader/NLA/Simulation | allowlist、compile/cache、错误与恢复 | 已完成的 V1 slice完整 evaluator 仍排除 |
| M11 | Render/Compositor/Media | bounded local、server route、codec/revision/audio | 已完成的 V1 slice |
| M12 | Asset/IO/Editors | 资产库、GLB/OBJ/STL/PLY、编辑器上下文 | 按 manifest 继续领取,不能按总百分比判断 |
| M13 | Scripting/Security | metadata-only、default-deny、sandbox、server isolation、CSP | 每个安全门独立验收execution 默认禁用 |
| M14 | Chromium 设备与输入 | capability、预算、DPR、pointer、IME、keymap、modal、可访问性 | 已归档;后续只按 manifest 回归 |
| M15 | 全域审计 | Blender 5.2 inventory、operator/node/editor/format gap | 已完成计划/审计门;产出 6,900 条 M16-M22 原子 gap |
| M16-M22 | 原子 parity gap | 每条 gap 独立 fixture、desktop/WASM 对标、重开和 hash 证据 | 只按 parent manifest 顺序推进;当前任务不在本表缓存 |
状态轴必须分开V1 `releaseStatus=READY` 不等于 Blender 全域 `parityStatus=COMPLETE`
## 5. 历史示例M14-04F 细分(非当前任务)
### 5.1 归档时的已知上下文
- parent`M14-04E`,状态页已记录 keymap fixture 的成功证据。
- 当前任务:`M14-04F`,任务卡为 `docs/tasks/M14-04F.md`
- 协议:`web/protocol/input-modal.ts`
- 单测:`web/tests/unit/input-modal.test.mjs`
- Chromium 检查器:`tools/web/check-chromium-input-modal.mjs`
- 报告/manifest`tests/golden/M14-04F/`
- focused command`npm --prefix web run test:chromium-input-modal`
当前命令已经能证明协议单测通过,并能在 Chromium 页面派发 touch/pen 事件;完成主任务前还要
确认事件确实进入生产输入状态和 Main transaction而不是只在 checker 内构造事件并写入固定
保证值。
### 5.2 子任务清单
| 子任务 | 唯一目标 | 最小改动面 | 必须证明 |
| --- | --- | --- | --- |
| F-C | 冻结 `InputModalState` schema 和状态转移 | `web/protocol/input-modal.ts` | pointer ID 非法时稳定拒绝;状态转移确定 |
| F-T | 触控 modal 取消 | protocol + 生产 pointer cancel 入口 | cancel 后 `kind=NONE`、无 Main commit、活动 pointer 清零 |
| F-2T | 双指导航 session 去重 | protocol + navigation dispatch | 从 1 到 2 个 pointer 只增加一次 `navigationRevision`;重复 down 不增加 |
| F-P | 笔 stroke 单次提交 | protocol + pen pointerup/cancel 入口 | 第一个合法 up 最多一个 Main commitlate up/cancel 无二次提交 |
| F-W | 主线程/Offscreen 生产接线 | App、viewport、Worker 输入边界 | 两条生产视口消费同一状态规则cancel/late result 不污染 revision |
| F-B | Chromium 真实断言 | checker/e2e + 最小 fixture | 读取 DOM/诊断/Main revision 的真实结果,而不是只检查派发数量 |
| F-E | 证据和交接 | report、manifest、status | 命令退出 0、artifact hash 非空、风险/回滚清楚 |
### 5.3 完成门
主任务只有同时满足以下条件才可标记 `done`
1. `F-C``F-T``F-2T``F-P` 的 Node 单测通过。
2. `F-W` 在生产 App/viewport 中有实际 import 和事件消费路径。
3. `F-B` 对至少 touch cancel、双指 session、pen late-up/cancel 做真实正负例断言。
4. 取消或重复事件不产生迟到 Main commit且 revision/commit counter 可观测。
5. focused Chromium 命令、必要的 `typecheck`/`build``git diff --check` 通过。
6. `docs/status/M14-04F.md``tests/golden/M14-04F/manifest.json` 和代码 hash 一致。
如果只有协议和测试通过,状态应保持 `in_progress`,不得提前领取 M14-04G。
## 6. M14 后续任务草案(归档)
这些是领取前的拆分草案;正式任务仍须由 parent manifest 生成任务卡。
### M14-04G响应式布局无重叠/溢出
- 输入M14-04F manifest、App shell、viewport CSS、四档 Chromium viewport。
- 正例:`1440x900``1280x720``834x1112``390x844`
- 检查:`scrollWidth <= clientWidth`topbar/sidebar/viewport/timeline/status 不互相覆盖;文字不被裁切;触控目标仍可操作。
- 负例窄视口、长项目名、错误提示、打开进度、面板展开、DPR 2。
- 产物:布局 checker、4 档报告、截图或 bounding-box 摘要、manifest、status。
- 不做Firefox/WebKit完整响应式重设计新增 Blender 功能。
### M14-04H键盘无障碍与焦点恢复
- 输入M14-04G manifest、现有菜单/操作搜索/文件对话入口。
- 正例Tab 顺序、Escape 关闭 modal、Enter 提交、焦点回到触发器、按钮有 name/role。
- 负例modal 打开时快捷键穿透、焦点丢失、隐藏元素进入 tab 顺序、IME 期间提交 operator。
- 产物Chromium keyboard-only checker、焦点轨迹报告、必要的 ARIA/DOM 修复、manifest/status。
- 不做screen reader 的浏览器兼容性声明Firefox/WebKit 证据。
### M15-01A 至 M15-01F全域清单审计
1. 从 Blender 5.2 RNA 生成 data-block inventory。
2. 生成 operator、poll context 和 property inventory。
3. 生成 modifier、constraint、shader/GN/compositor node inventory。
4. 生成 sequencer、physics、import/export inventory。
5. 生成 editor/space/region/workspace/keymap inventory。
6. 将 inventory ID 映射到 `parity-ledger.json`,拒绝未分类新增项并固定总 hash。
每一项都只产出一个稳定 inventory 或映射证据;不要在 M15 直接实现功能。
## 7. 交接格式
完成任务后status 页只保留可审计摘要:
```text
status: done | blocked
task: <ID>
updated: <timezone>
scope: 一句话行为变化
evidence: 命令、退出码、关键结果
artifacts: report/manifest/代码 SHA-256
nextTask: 仅复制 manifest.nextTask
knownRisk: 仍未覆盖的边界
rollback: 删除本任务产物并恢复 parent 队列尾
```
不要把完整终端日志、实现过程或下一阶段设想复制进 status 页;长日志留在构件目录,任务卡只留
最小输入和验收入口。
## 8. 领取前检查表
- [ ] 当前 task 与 parent manifest 的 `nextTask` 一致。
- [ ] parent status 为 `done`,或明确写出允许并行的 `enablingTask`
- [ ] 任务卡只有一个主要行为。
- [ ] focused command 已存在,或任务明确包含创建该命令。
- [ ] 生产入口和测试入口分别列出,没有只写“相关代码”。
- [ ] 至少一个负例、取消或重复事件已列出。
- [ ] 任务状态不会误改 `parityStatus`/`releaseStatus` 另一条轴。
- [ ] 完成后能生成 report、manifest、status 和可回滚路径。

View File

@@ -0,0 +1,51 @@
# 任务上下文模板
每张任务卡只允许一个主要行为变化或一个证据变化。卡片上限 8 KiB最多 12 个输入路径和 8 条命令;实现细节放在代码和测试中,
运行结果放在 `docs/status/<task>.md`,不要在任务卡复制长日志。机器生成 gap 也必须有同样的短卡;
catalog 只供工具随机读取,不替代任务卡,也不应被执行者通读。
不要在任务卡引用 `next-task-plan.json`、完整路线图、全量 status 或 `test-results/`;需要背景时只
点名一个稳定契约的小节。
## 任务
- `task`: `<ID>`
- `parent`: `<parent ID>`
- `status`: `pending | in_progress | done | blocked`
## 目标
一句话描述唯一可观察变化,以及明确不改变的状态轴。
## 输入
- 上一个 manifest`tests/golden/<parent>/manifest.json`
- 最小 fixture`<fixture path or NOT_APPLICABLE>`
- 生产入口:`<single source/test/checker path>`;不要写“相关代码”或整目录。
## 范围
- 做:
- 不做:
## 验收
```text
<focused command>
```
至少列出一个负例、取消、重复或预算超限场景;不要把全量回归命令复制到卡片。
适用时补充 Node、Chromium、typecheck、lint每条命令必须真实运行并记录退出码。
## 产物和交接
- 报告:`tests/golden/<task>/...`
- 状态页:`docs/status/<task>.md`
- manifest`tests/golden/<task>/manifest.json`
- 下一任务:由 manifest 的 `nextTask` 决定
## 回滚
删除本任务新增的生产入口、测试、报告、manifest、package 命令和状态页,并将 parent manifest
恢复为队列尾;不得删除或改写 parent 的成功证据。

View File

@@ -2,6 +2,8 @@
更新时间2026-08-09 更新时间2026-08-09
> 长期规划(归档):本文不进入默认上下文;只按任务卡引用的章节读取。
本计划把当前未声明的 Sculpt、Geometry Nodes/Simulation、任意 Shader Node 和完整 本计划把当前未声明的 Sculpt、Geometry Nodes/Simulation、任意 Shader Node 和完整
NLA 拆成四个可独立验收的连续任务。每个任务都必须满足Blender Main 是唯一写入 NLA 拆成四个可独立验收的连续任务。每个任务都必须满足Blender Main 是唯一写入
权威SceneIR 只承载通过校验的数据Three.js 不执行 Blender 语义;不支持或不确定 权威SceneIR 只承载通过校验的数据Three.js 不执行 Blender 语义;不支持或不确定
@@ -236,21 +238,22 @@ N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、
| N-012-A1 | GraphIR schema、节点白名单、typed socket/link/domain 校验 | 已完成 | `geometry-nodes.ts`Worker cycle gate 已有 E2E | | N-012-A1 | GraphIR schema、节点白名单、typed socket/link/domain 校验 | 已完成 | `geometry-nodes.ts`Worker cycle gate 已有 E2E |
| N-012-A2 | 跨 group 的递归闭包、外部 object/collection/image ID 沙箱 | 已完成 | stable ID 类型、owner cycle、linked/missing/corrupt 资源门和 E2E 已覆盖 | | N-012-A2 | 跨 group 的递归闭包、外部 object/collection/image ID 沙箱 | 已完成 | stable ID 类型、owner cycle、linked/missing/corrupt 资源门和 E2E 已覆盖 |
| N-012-A3a | Simulation manifest/schema、Blender 版本、frame byte range | 已完成 | `simulation-cache.ts`;完整帧、连续范围、总/逐帧 SHA-256 | | N-012-A3a | Simulation manifest/schema、Blender 版本、frame byte range | 已完成 | `simulation-cache.ts`;完整帧、连续范围、总/逐帧 SHA-256 |
| N-012-A3b | committed blend 绑定、OPFS 内容寻址、schema 6 索引和 Worker 重启 | 已完成 | `simulation_manifest` + asset SHA-256`test:simulation-cache` | | N-012-A3b | committed blend 绑定、OPFS 内容寻址、schema 7 索引和 Worker 重启 | 已完成 | `simulation_manifest` + manifest schema 2 graph/source/revisionHash + asset SHA-256`test:simulation-cache-identity``test:simulation-cache` |
| N-012-A3b2 | cache 取消、播放准入、active playback 保护的确定性 LRU 和损坏隔离 | 已完成 | `simulation_quarantine`、Worker 生命周期 full verification、AbortSignal/cancelRequest、`test:simulation-cache-lifecycle`;不扩大 Simulation Zone evaluator |
| N-012-A3c | Blender desktop bake 生成、版本迁移、GN frame seek 消费 | planned | Simulation Zone 仍返回 `GEOMETRY_NODES_SIMULATION_UNAVAILABLE` | | N-012-A3c | Blender desktop bake 生成、版本迁移、GN frame seek 消费 | planned | Simulation Zone 仍返回 `GEOMETRY_NODES_SIMULATION_UNAVAILABLE` |
| N-012-B1 | 无时间 GN lazy-function/field Main evaluator | planned | 当前合法 GraphIR 仍返回 `CAPABILITY_MISSING` | | N-012-B1 | 无时间 GN lazy-function/field Main evaluator | 已完成有限切片 | M10-03 的自有有界 evaluator 覆盖 schema-1 allowlist完整 Blender lazy-function runtime 和任意 field/domain 仍阻断 |
| N-013-A1 | ShaderIR schema、单输出、typed socket/隐式转换、无环 | 已完成 | `shader-graph.ts`;未进入 writer 白名单的节点返回 `SHADER_NODE_UNSUPPORTED` | | N-013-A1 | ShaderIR schema、单输出、typed socket/隐式转换、无环 | 已完成 | `shader-graph.ts`;未进入 writer 白名单的节点返回 `SHADER_NODE_UNSUPPORTED` |
| N-013-A2 | material/image stable ID 与 linked/missing/corrupt 资源门 | 已完成当前子集 | packed image 可引用linked/missing/corrupt image 在 Worker 阻断 | | N-013-A2 | material/image stable ID 与 linked/missing/corrupt 资源门 | 已完成当前子集 | packed image 可引用linked/missing/corrupt image 在 Worker 阻断 |
| N-013-B1 | RGB/Value/Principled/Image Texture/Normal Map/Output 整图 Main transaction | 已完成当前子集 | native create/remove/link/default、history、save/reopen 回归 | | N-013-B1 | RGB/Value/Principled/Image Texture/Normal Map/Output 整图 Main transaction | 已完成当前子集 | native create/remove/link/default、history、save/reopen 回归 |
| N-013-B2a | Math 六运算 Main `custom1`、原生 socket identifier 和 operation reader | 已完成有限切片 | ADD/SUBTRACT/MULTIPLY/DIVIDE/MINIMUM/MAXIMUM其他运算阻断 | | N-013-B2a | Math 六运算 Main `custom1`、原生 socket identifier 和 operation reader | 已完成有限切片 | ADD/SUBTRACT/MULTIPLY/DIVIDE/MINIMUM/MAXIMUM其他运算阻断 |
| N-013-B2 | Mix/Mapping/TexCoord/Bump storage/property 写回 | planned | 未声明节点和 properties 仍 `SHADER_NODE_UNSUPPORTED` | | N-013-B2 | Mix/Mapping/TexCoord/Bump storage/property 写回 | planned | 未声明节点和 properties 仍 `SHADER_NODE_UNSUPPORTED` |
| N-013-C1a | RGB/Value 常量到 glTF PBR factor 的严格映射 | 已完成有限切片 | 缺值、越界、重复输入和未知链接返回 `SHADER_GRAPH_UNMAPPABLE`mapping E2E 覆盖 | | N-013-C1a | RGB/Value 常量到 glTF PBR factor 的严格映射 | 已完成有限切片 | 缺值、越界、重复输入和未知链接返回 `SHADER_GRAPH_UNMAPPABLE`mapping E2E 覆盖 |
| N-013-C1 | 受限 Web material compiler、graph hash、色彩空间/sampler | planned | C1a 不代表 Web shader compiler;任意图仍未启用 | | N-013-C1 | 受限 Web material compiler、graph hash、色彩空间/sampler | 已完成有限编译切片 | M10-07 发布 schema 1 compile report、M10-08 绑定 graph/texture/color-space/backend 的 compile key、M10-09 失败回滚、WebGL2 Three physical 共同消费和 fail-closed 预算;完整 sampler/WebGPU 仍 planned |
| N-014-A1 | Track/Strip schema、Action/range/blend/time-warp 校验 | 已完成 | `nla.ts`;缺失 Action 返回 `NLA_ACTION_MISSING` | | N-014-A1 | Track/Strip schema、Action/range/blend/time-warp 校验 | 已完成 | `nla.ts`;缺失 Action 返回 `NLA_ACTION_MISSING` |
| N-014-A2 | strip 排序/重叠、owner/path compatibility、循环引用校验 | 已完成当前子集 | 同轨重叠、duration mapping、blend range 和 unsupported strip type 均阻断 | | N-014-A2 | strip 排序/重叠、owner/path compatibility、循环引用校验 | 已完成当前子集 | 同轨重叠、duration mapping、blend range 和 unsupported strip type 均阻断 |
| N-014-B1 | Action Clip 整栈替换 Main transaction、reader、保存重开 | 已完成当前子集 | `setNLAStack`Track/Strip/Action 摘要重开一致 | | N-014-B1 | Action Clip 整栈替换 Main transaction、reader、保存重开 | 已完成当前子集 | `setNLAStack`Track/Strip/Action 摘要重开一致 |
| N-014-B2 | 细粒度 create/remove/move/resize/active 命令 | planned | UI 前先扩命令和 revision 回归 | | N-014-B2 | 细粒度 create/remove/move/resize/active 命令 | 已完成有限切片 | M10-12 `moveNLAStrip` 通过 revision gate、单次 Main `setNLAStack` transaction、undo/redo 和 save/reopencreate/remove/resize/active 仍 planned |
| N-014-C1 | 正 scale Action Clip native frame 求值 | 已完成当前子集 | authoritative Main bytes 进入 Blender depsgraphframe 5 矩阵回归通过 | | N-014-C1 | Action Clip track/action/time mapping 只读精确求值 | 已完成当前子集 | M10-11 desktop fixture 的 scale/reverse/repeat 2 个 Clip、12 帧 Main/Chromium 矩阵最大误差 0求值不改 NLA/Action identity |
| N-014-C2a | Action Clip reverse Main 标志、reader、Depsgraph、保存重开 | 已完成有限切片 | `NLASTRIP_FLAG_REVERSE`frame 5 反向矩阵回归 | | N-014-C2a | Action Clip reverse Main 标志、reader、Depsgraph、保存重开 | 已完成有限切片 | `NLASTRIP_FLAG_REVERSE`frame 5 反向矩阵回归 |
| N-014-C2b1 | Action Clip repeat 与跨周期 frame seek | 已完成有限切片 | `repeat=2`frame 5/14 native 矩阵一致 | | N-014-C2b1 | Action Clip repeat 与跨周期 frame seek | 已完成有限切片 | `repeat=2`frame 5/14 native 矩阵一致 |
| N-014-C2 | blend overlap、骨骼、约束顺序、transition/meta | planned | 未声明族继续 `NLA_*` 结构化阻断 | | N-014-C2 | blend overlap、骨骼、约束顺序、transition/meta | planned | 未声明族继续 `NLA_*` 结构化阻断 |
@@ -274,10 +277,10 @@ N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、
| GN/Simulation 切片 | 交付与验收 | 状态 | | GN/Simulation 切片 | 交付与验收 | 状态 |
| --- | --- | --- | | --- | --- | --- |
| N-012-B0 | wasm32 lazy-function/field runtime feature probe、超时和内存上限 | planned/阻断 | | N-012-B0 | wasm32 lazy-function/field runtime feature probe、超时和内存上限 | planned/阻断 |
| N-012-B1a | Group Input/Output + Transform Geometry 最小闭包,输出 topology/attributes/bounds | planned依赖 B0 | | N-012-B1a | Group Input/Output + Transform Geometry 最小闭包,输出 topology/attributes/bounds | 已完成有限切片 | M10-03 desktop/full-Main WASM golden不是完整 lazy-function runtime |
| N-012-B1b | Set Position、Math/Compare/Selection field 及 point/edge/face/corner 域转换 | planned,依赖 B1a | | N-012-B1b | Set Position、Math/Compare/Selection field 及 point/edge/face/corner 域转换 | 部分完成 | Set Position/Math/Compare 常量闭包和 M10-04 七域预算已完成Selection field 与真实跨域转换仍 planned |
| N-012-B1c | Join/Separate/Realize Instances 和 instance stable ID/transform | planned,依赖 B1b | | N-012-B1c | Join/Separate/Realize Instances 和 instance stable ID/transform | 已完成有限切片 | Join/Separate/Collection Realize golden 已通过;任意 instance 属性/ID 传播仍 planned |
| N-012-B1d | Store/Named Attribute 生命周期、anonymous attribute 泄漏和 deterministic graph hash | planned,依赖 B1c | | N-012-B1d | Store/Named Attribute 生命周期、anonymous attribute 泄漏和 deterministic graph hash | 部分完成 | constant point Float Store Named Attribute 和有界回执已通过Named/anonymous attribute 完整生命周期仍 planned |
| N-012-D1 | desktop bake 生成器、Blender 版本迁移和逐帧 cache 消费 | planned依赖现有 A3b | | N-012-D1 | desktop bake 生成器、Blender 版本迁移和逐帧 cache 消费 | planned依赖现有 A3b |
| N-012-D2 | Simulation Zone wasm frame step、状态所有权和 frame seek feature probe | planned/阻断,依赖 B1d/D1 | | N-012-D2 | Simulation Zone wasm frame step、状态所有权和 frame seek feature probe | planned/阻断,依赖 B1d/D1 |
| N-012-D3 | 浏览器 bake start/cancel/commit 原子事务;中断时不发布半成品 manifest | planned依赖 D2 | | N-012-D3 | 浏览器 bake start/cancel/commit 原子事务;中断时不发布半成品 manifest | planned依赖 D2 |
@@ -285,11 +288,11 @@ N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、
| Shader 切片 | 交付与验收 | 状态 | | Shader 切片 | 交付与验收 | 状态 |
| --- | --- | --- | | --- | --- | --- |
| N-013-B2a | Math 白名单逐运算 Main 写回和 operation readerGLB/Web 编译仍阻断 | 已完成有限切片 | | N-013-B2a | Math 白名单逐运算 Main 写回和 operation reader完整 GLB/Web 编译仍阻断 | 已完成有限切片M10-07 已消费六项常量 Math任意 field/动态输入仍阻断 |
| N-013-B2b | Mix 的 data type、factor clamp 和颜色/向量 socket 变体 Main 写回 | planned依赖 B2a | | N-013-B2b | Mix 的 data type、factor clamp 和颜色/向量 socket 变体 Main 写回 | planned依赖 B2a |
| N-013-B2c | Texture Coordinate -> Mapping -> Image TextureUV 名称/sampler/色彩空间闭环 | planned依赖 B2b | | N-013-B2c | Texture Coordinate -> Mapping -> Image TextureUV 名称/sampler/色彩空间闭环 | planned依赖 B2b |
| N-013-B2d | Normal Map/Bump 串联、强度/距离、切线前置条件和 desktop golden | planned依赖 B2c | | N-013-B2d | Normal Map/Bump 串联、强度/距离、切线前置条件和 desktop golden | planned依赖 B2c |
| N-013-C1b | 受限 Web compiler IR、graph hash cache、未知节点 fail-closed | planned依赖 B2d | | N-013-C1b | 受限 Web compiler IR、graph hash binding、未知节点 fail-closed | 已完成有限切片 | M10-07 只覆盖 RGB/Value/Math/Image/Normal/Principled/Output 与 WebGL2 Three physicalM10-08/M10-09 完成 graph/asset compile key 与失败回滚M10-10 完成任意未知节点稳定 capability block |
| N-013-C1c | sRGB/Non-Color、alpha、sampler、normal/tangent 和 WebGL/WebGPU 编译报告 | planned依赖 C1b | | N-013-C1c | sRGB/Non-Color、alpha、sampler、normal/tangent 和 WebGL/WebGPU 编译报告 | planned依赖 C1b |
| N-013-D1 | 可证明 PBR 闭包的纹理/常量完整映射;其余保持 `SHADER_GRAPH_UNMAPPABLE` | plannedC1a 已覆盖常量子集 | | N-013-D1 | 可证明 PBR 闭包的纹理/常量完整映射;其余保持 `SHADER_GRAPH_UNMAPPABLE` | plannedC1a 已覆盖常量子集 |
| N-013-D2 | 桌面 Blender GLB 再导入,比较 factor、节点摘要、图片、sampler 与渲染摘要 | planned依赖 D1 | | N-013-D2 | 桌面 Blender GLB 再导入,比较 factor、节点摘要、图片、sampler 与渲染摘要 | planned依赖 D1 |
@@ -315,4 +318,5 @@ npm --prefix web run typecheck
WEB_TEST_PORT=5200 npm --prefix web run test:capability-gates WEB_TEST_PORT=5200 npm --prefix web run test:capability-gates
npm --prefix web run test:simulation-cache npm --prefix web run test:simulation-cache
npm --prefix web run test:authoring-roundtrip npm --prefix web run test:authoring-roundtrip
npm --prefix web run test:nla-evaluation-golden
``` ```

View File

@@ -1,6 +1,8 @@
# VDB -> NanoVDB -> WebGPU 实施方案 # VDB -> NanoVDB -> WebGPU 实施方案
更新时间2026-08-13 更新时间2026-08-14
> 专项背景归档本文不进入默认上下文VDB 任务必须通过最小上下文工具按卡片追加读取。
## 1. 范围与完成定义 ## 1. 范围与完成定义
@@ -16,7 +18,7 @@ OpenVDB 读取 `.vdb`,转换为 NanoVDB浏览器只读取受校验的 `.nvd
- density/temperature/color/emission/velocity 的 Blender 材质语义; - density/temperature/color/emission/velocity 的 Blender 材质语义;
- `.blend` 保存、转换产物绑定、重开和资源丢失诊断; - `.blend` 保存、转换产物绑定、重开和资源丢失诊断;
- 与 Blender 5.2 桌面图像/数值 golden 一致; - 与 Blender 5.2 桌面图像/数值 golden 一致;
- 大体积、损坏输入、OOM、取消和 Chromium WebGPU 性能门。 - 大体积、损坏输入、取消和 Chromium WebGPU 性能门resident/page-table 确定性 OOM 门已完成
只有上述链路均有真实资源与自动测试证据时VDB 才能从 `BLOCKED` 转为可发布能力。 只有上述链路均有真实资源与自动测试证据时VDB 才能从 `BLOCKED` 转为可发布能力。
@@ -181,12 +183,15 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
点比较值与 active/inactive 状态。状态:`in_progress`Float32 CPU/WGSL 与 native sample 已逐点 点比较值与 active/inactive 状态。状态:`in_progress`Float32 CPU/WGSL 与 native sample 已逐点
一致Float16/Vec 仍保持阻断。 一致Float16/Vec 仍保持阻断。
- `VDB-032`:实现 GPU page allocator、storage buffer upload、indirection、resident LRU 和 - `VDB-032`:实现 GPU page allocator、storage buffer upload、indirection、resident LRU 和
dispose每页上传前已通过 chunk hash。`BLOCKED` dispose每页上传前已通过 chunk hash。状态:`done_current_scope`;初始空驻留集、显式逐页
hash 校验装入、页表、确定性 LRU、数据级页替换、dispose 和多材质 grid 共享总驻留预算均已
通过 Chromium WebGPU 门。着色器自动回报缺页并驱动渐进重绘不属于本任务,继续由生产视口门阻断。
- `VDB-033`:实现对象 bounds raycast、front-to-back ray marching、步长/early exit 和深度合成。 - `VDB-033`:实现对象 bounds raycast、front-to-back ray marching、步长/early exit 和深度合成。
状态:`in_progress`;有界正交 bounds、front-to-back integration、early exit 和确定性 96x96 状态:`in_progress`;有界正交 bounds、front-to-back integration、early exit 和确定性 96x96
Chromium 图像已完成,生产相机深度合成待接入。 Chromium 图像已完成,生产相机深度合成待接入。
- `VDB-034`:接入主线程/Offscreen renderer 的相同 volume scene deltadevice loss 后重建已 - `VDB-034`:接入主线程/Offscreen renderer 的相同 volume scene delta状态:`done_current_scope`
验证页面。`BLOCKED` 双生产视口已消费相同 hash-bound asset 并输出非空像素。生产 Offscreen WebGPU device-loss
自动重建和 demand-page 恢复继续由故障门阻断。
### Phase 4材质、Main 与导出 ### Phase 4材质、Main 与导出
@@ -196,9 +201,9 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
- `VDB-041`density/temperature/color/emission/anisotropy 的 Main writer、undo/redo 和原子事务。 - `VDB-041`density/temperature/color/emission/anisotropy 的 Main writer、undo/redo 和原子事务。
状态:`in_progress`Volume source/display/interpolation/step/velocity Main 写回、undo/redo、保存重开 状态:`in_progress`Volume source/display/interpolation/step/velocity Main 写回、undo/redo、保存重开
已完成Principled Volume 完整节点事务仍阻断。 已完成Principled Volume 完整节点事务仍阻断。
- `VDB-042`:保存 `.blend` 与 Web asset binding清空 Worker 后重开并重新流送。`BLOCKED` - `VDB-042`:保存 `.blend` 与 Web asset binding清空 Worker 后重开并重新流送。
状态:`in_progress`Main Volume 保存重开与 OPFS asset binding/Worker 重开分别通过,尚待生产视口 状态:`done_current_scope`Main VolumeOPFS asset bindingWorker 重建与主线程/Offscreen
联合重开闭环 两个生产视口已在同一 revision/hash 闭环联合重开。
- `VDB-043`GLB 明确报告 Volume 无核心映射USD 仅在 desktop USD/OpenVDB 路径真实可用时 - `VDB-043`GLB 明确报告 Volume 无核心映射USD 仅在 desktop USD/OpenVDB 路径真实可用时
写入 field asset不把 bounds proxy 当体积导出。`BLOCKED` 写入 field asset不把 bounds proxy 当体积导出。`BLOCKED`
@@ -206,14 +211,19 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
- `VDB-050`desktop OpenVDB -> NanoVDB grid count/name/type/transform/bounds/value sample golden。 - `VDB-050`desktop OpenVDB -> NanoVDB grid count/name/type/transform/bounds/value sample golden。
- `VDB-051`Blender 5.2 与 Chromium WebGPU 的至少 3 个视角像素 golden分别比较 alpha、 - `VDB-051`Blender 5.2 与 Chromium WebGPU 的至少 3 个视角像素 golden分别比较 alpha、
transmittance、color记录容差与色彩空间。 transmittance、color记录容差与色彩空间。状态:`done_current_scope`OpenVDB 13 native
density reference 与主线程/Offscreen WebGPU 的 X/Y/Z 三轴 RGBA8 逐通道比较均为零误差。
- `VDB-052`64 MiB/512 MiB/1 GiB sparse bundle 的首帧、渐进清晰、峰值 CPU/GPU 和取消门。 - `VDB-052`64 MiB/512 MiB/1 GiB sparse bundle 的首帧、渐进清晰、峰值 CPU/GPU 和取消门。
状态:`in_progress`64 MiB 实际 range、4 MiB 峰值工作集与中途取消已通过512 MiB/1 GiB
仍按设备和 CI 预算保持阻断。
- `VDB-053`:损坏 magic/version/tree offset/hash、zip bomb 等价超预算、NaN transform、设备丢失、 - `VDB-053`:损坏 magic/version/tree offset/hash、zip bomb 等价超预算、NaN transform、设备丢失、
网络中断和 OOM 门。 网络中断和 OOM 门。状态:`in_progress`resident/page-table 确定性 OOM、唯一释放和同设备
小 resident grid 恢复已完成,其余大 bundle 与联合故障矩阵仍阻断。
- `VDB-054`:保存重开、资源缺失、源变更重转、旧 schema migration、离线 OPFS 重开。 - `VDB-054`:保存重开、资源缺失、源变更重转、旧 schema migration、离线 OPFS 重开。
- `VDB-055`N-026 证据记录包含真实转换器、资源、Chromium WebGPU、desktop golden 和构件 hash。 - `VDB-055`N-026 证据记录包含真实转换器、资源、Chromium WebGPU、desktop golden 和构件 hash。
Phase 5 全部任务当前`BLOCKED`,不能由 Phase 0 的协议 fixture 代替 Phase 5 整体仍`BLOCKED`,不能由 Phase 0 的协议 fixture 代替`VDB-053` 的确定性 resident
OOM 子切片已有真实 Chromium WebGPU 资源证据,但不扩大为大 bundle 或完整 Phase 5 完成声明。
## 7. 验收命令与停止条件 ## 7. 验收命令与停止条件
@@ -234,7 +244,8 @@ npm --prefix web run test:vdb-faults
阶段停止条件: 阶段停止条件:
- 新增资源若无许可证/来源/hash不得进入 VDB catalog 或 golden。 - 新增资源若无许可证/来源/hash不得进入 VDB catalog 或 golden。
- desktop converter 已构建server job API 未实现时 `SERVER_CONVERSION` 保持 `BLOCKED` - desktop/server converter 已构建并有 hash 等价证据;未配置受控 endpoint 时运行时
`SERVER_CONVERSION` 仍保持 `BLOCKED`
- 无 CPU reference sample 对照WGSL traversal 不得进入 `READY` - 无 CPU reference sample 对照WGSL traversal 不得进入 `READY`
- 仅能显示 box/point proxy体渲染仍为 `BLOCKED` - 仅能显示 box/point proxy体渲染仍为 `BLOCKED`
- 未验证 material mapping、Main 保存重开或 desktop/Chromium goldenN-015 与 N-019 仍为 - 未验证 material mapping、Main 保存重开或 desktop/Chromium goldenN-015 与 N-019 仍为

View File

@@ -0,0 +1,68 @@
# Web Blender Modeler V1 发布范围
更新时间2026-08-14
## 1. 产品定义
V1 是复用 Blender 5.2 Main、文件格式和有界编辑能力的浏览器建模器不是桌面
Blender 的窗口、渲染器、Python 和插件体系的逐项移植。发布名称固定为
`Web Blender Modeler V1`
浏览器基线仅为 Chromium。主线程 WebGL 和 OffscreenCanvas Worker 是两个必须通过的
生产视口路径Firefox 和 WebKit 属于发布后的兼容性工作,不进入 V1 发布门。
## 2. V1 必须闭环的用户工作流
以下能力必须同时满足 Main 权威写回、undo/redo、原子保存、Worker 重启、重新打开和
GLB 导出检查:
1. 导入和保存 `.blend`
2. Scene、Object、Collection、Parent 和 Transform。
3. 有界 Mesh Object/Edit Mode 操作。
4. 材质槽、UV、Principled PBR 和受支持的贴图节点。
5. TRS 关键帧、Timeline、受支持的 Action/NLA 子集。
6. Armature/Pose/Constraint/Shape Key 的已声明读取和求值子集。
7. OPFS 项目、快照、资产和崩溃恢复。
8. GLB 的网格、PBR、贴图、morph、skin 和动画已声明子集。
## 3. 发布能力分类
| 分类 | 发布含义 | V1 规则 |
| --- | --- | --- |
| `LOCAL_EXACT` | 当前 fixture 与 Blender 5.2 语义一致 | 必须有 desktop/WASM golden |
| `LOCAL_BOUNDED` | 浏览器本地只支持白名单和显式预算 | 非白名单必须返回稳定错误码 |
| `SERVER` | 浏览器只提供校验、提交和结果绑定协议 | 服务端未配置时必须明确阻断 |
| `EXCLUDED` | V1 不提供且不计入本地实现承诺 | UI 不得伪造成功或静默丢数据 |
一个 family 可以整体为 `LOCAL_BOUNDED`,同时包含明确的 `SERVER``EXCLUDED`
切片。发布门检查已声明的 V1 正例和阻断行为,不把桌面 Blender 全量能力误记为完成。
## 4. 高级能力边界
| 功能域 | V1 分类 | V1 提供 | V1 不承诺 |
| --- | --- | --- | --- |
| Non-mesh/VDB | `LOCAL_BOUNDED` | Curve/Surface/Font/Metaball、PointCloud/Curves、受限 NanoVDB WebGPU | 任意 VDB 材质、无限 GPU 驻留 |
| Sculpt | `LOCAL_BOUNDED` | Mask/Face Set、四种有界 Main stroke | 完整 PBVH 和桌面全部笔刷 |
| Geometry Nodes/Simulation Zone | `LOCAL_BOUNDED` | 图、资源和确定性 cache 安全层 | 任意 lazy-function/field 本地求值 |
| Shader Nodes | `LOCAL_BOUNDED` | 已声明节点白名单和 PBR 映射 | 任意 Shader 图和通用 Web 编译器 |
| Grease Pencil/Paint | `LOCAL_BOUNDED` | 当前 Main writer、选择、预览和保存子集 | 完整 2D 编辑器、PBVH texture paint |
| Physics/Compositor/Sequencer | `LOCAL_BOUNDED` | reader、编辑、cache playback、有限 CPU executor | 完整 solver、节点全集和本地最终编码 |
| Tracking/Asset/Editors | `LOCAL_BOUNDED` | 当前 schema、Main reader/writer 和安全门 | Blender 全量 editor/operator/format |
| Cycles/复杂物理烘焙/最终视频编码 | `SERVER` | source hash、能力和结果绑定边界 | 浏览器本地等价执行 |
| Python/Text autorun/Add-ons | `EXCLUDED` | 清单读取、默认拒绝和审计 | 执行任意代码或自动安装插件 |
| 原生窗口/CUDA/Metal/HIP/OptiX | `EXCLUDED` | capability report | 浏览器平台等价 |
## 5. V1 发布门
发布候选必须具备以下自动证据:
1. P0 `import -> edit -> undo -> redo -> save -> StorageWorker/WebEngineWorker restart -> reopen -> GLB export`
2. Chromium 主线程和 OffscreenCanvas 视口非空像素。
3. 100k、1M 和 10M geometry 的有界性能/内存结果。
4. 4K/8K texture、长时间线/媒体能力门和 simulation cache。
5. OOM 注入、device loss、network interruption、quota、malformed input 和 zip bomb 恢复。
6. 离线资源、确定性二进制/源码包、许可证、SBOM 和 source offer。
7. `docs/status/parity-ledger.json`、release evidence 和状态文档完全一致。
任何测试只能证明其命名的有界切片;不得用协议测试代替真实 Blender Main、GPU、OPFS 或
desktop golden。

51
docs/status/M10-01.md Normal file
View File

@@ -0,0 +1,51 @@
# M10-01 Status
status: done
task: 从 Blender Main 读取 Geometry Nodes 图拓扑、socket 默认值、link 和稳定 node ID
updated: 2026-08-16 America/New_York
## Scope
The WebEngine authoritative Main reader publishes a bounded `GeometryNodeGraphIR`
snapshot for every `GeometryNodeTree`. It preserves unsupported nodes as metadata,
uses Blender node identifiers and socket identifiers rather than display names, and
does not evaluate Geometry Nodes. The existing `web/protocol/schema-version` remains
`1`: `geometryNodeGraphs` is an additive optional SceneIR field and no existing field
meaning or persisted storage schema changed.
## Evidence
- Fixture: `tests/files/web/modifier_geometry_nodes_scene.blend`.
- Fixture SHA-256: `f3820511f791769837d75be092934130c397cf46ee0acf79db28efca9a7948c9`.
- Desktop reference: `tests/golden/W-075/modifier_geometry_nodes_scene.json`.
- Desktop reference SHA-256: `93c21e5158daefce10f2d4df674e636bf3f6ed3dd43e1576c45a02a8592f5886`.
- Golden manifest: `tests/golden/M10-01/geometry-node-main-reader.json`.
- Protocol/unit check: `node --test web/tests/unit/geometry-nodes.test.mjs` (3/3).
- Native/WASM reader check: `node tools/web/check-geometry-node-main-reader.mjs`;
3 graphs, 10 nodes, 7 links, 9 defaults, stable IDs, graph hashes, desktop
comparison, save/reopen and unsupported Simulation-node preservation all passed.
- Chromium Worker check: `WEB_TEST_PORT=5440 npm --prefix web run test:geometry-node-main-reader`
(unit 3/3, reader 1/1, Chromium 1/1).
- `source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8`:
passed (`ninja: no work to do`).
- `npm --prefix web run typecheck`, `npm --prefix web run lint`,
`npm --prefix web run build`, and `git diff --check`: passed.
## Contract
- Per-graph budgets are fixed at 4,096 graphs, 4,096 nodes, 16,384 links,
65,536 node sockets, and 4,096 interface sockets.
- Graphs, nodes, links, and socket arrays are deterministically ordered before the
graph SHA-256 is emitted.
- Socket data types and finite defaults are read from Blender's native `bNodeSocket`
storage; ID-valued defaults are serialized as stable typed IDs.
- `__extend__` sockets are omitted because they are UI extension sockets, not graph
data. Missing/duplicate identifiers, invalid links, and budget overflow fail closed.
- Unsupported node types remain in the graph and are available to the later M10-02
allowlist gate; this task does not claim Geometry Nodes evaluation.
## Rollback
Remove the M10-01 reader, protocol field, golden, unit/E2E checks, package script and
this status file together. Existing SceneIR consumers continue to accept snapshots
without the optional `geometryNodeGraphs` field.

56
docs/status/M10-02.md Normal file
View File

@@ -0,0 +1,56 @@
# M10-02 Status
status: done
task: Geometry Nodes allowlist 与未支持节点无损阻断
updated: 2026-08-16 America/New_York
## Scope
`GEOMETRY_NODE_ALLOWLIST_SCHEMA` and `GEOMETRY_NODE_ALLOWLIST` freeze the only node
types that may proceed past the Geometry Nodes protocol gate. The gate validates the
complete graph before any Main writer call. A node outside the list returns
`GN_NODE_UNSUPPORTED`; a structurally valid allowlisted graph still returns
`CAPABILITY_MISSING` from the production Worker until its evaluator and desktop/WASM
golden are completed by M10-03. Neither failure mutates Main or replaces the graph
read by M10-01.
## Allowlist
Schema 1 contains 16 node types: Group Input/Output, Transform Geometry, Set
Position, Join/Separate Geometry, Realize Instances, Store Named Attribute, integer/
vector input, Value, Compare, Math, and Object/Collection/Image Info. External resource
nodes remain subject to stable-ID, missing-resource, linked-resource and owner-cycle
checks.
## Evidence
- Golden: `tests/golden/M10-02/geometry-node-allowlist.json`.
- Fixture: `tests/files/web/modifier_geometry_nodes_scene.blend`, SHA-256
`f3820511f791769837d75be092934130c397cf46ee0acf79db28efca9a7948c9`.
- `WEB_TEST_PORT=5441 npm --prefix web run test:geometry-node-allowlist`: unit 4/4
and Chromium 1/1 passed.
- The real Main graph `WebGeometryNodesSimulation` preserved
`GeometryNodeSimulationInput` and `GeometryNodeSimulationOutput`; an attempted
graph transaction returned `GN_NODE_UNSUPPORTED` and retained the graph JSON,
graph SHA-256 and Main revision.
- The allowlisted `WebGeometryNodes` graph returned `CAPABILITY_MISSING` while the
evaluator is still closed; it also retained graph identity and Main revision.
- M10-01 regression: `WEB_TEST_PORT=5442 npm --prefix web run
test:geometry-node-main-reader` passed (unit 4/4, reader 1/1, Chromium 1/1).
- Full unit suite: 131/131 passed.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`,
`npm --prefix web run build` (73 modules), and `git diff --check`: passed.
## Implementation Hashes
- Protocol: `23c2289ec2acbdce0beba47c0b6539bc3f14f5a71ce29927df86cf6c705d6ec3`.
- Unit: `e60933ed0f946bdaa96f5322ba8d80b26c8425c623dd6e87da33016a1592c299`.
- Chromium: `ff2c838b5c16d86eff13699586a0cd1121ee1a86d5ea52d7b89c01d35632bb77`.
- Golden: `471fec9c5382ab7522b6c18ab580a6926eaa59eb5a8b72d23968670b9b9ab37a`.
- Production Worker: `4a65547e6b1fea16a120b4e72eb37d4d9f7c7ced4342f535796535262ff65fcc`.
## Rollback
Remove the exported allowlist schema/list, M10-02 unit/E2E assertions, golden,
package script and this status file together. The older private allowlist behavior is
otherwise unchanged; no persisted schema migration is required.

73
docs/status/M10-03.md Normal file
View File

@@ -0,0 +1,73 @@
# M10-03 Status
status: done
task: Geometry Nodes allowlist 逐节点 Blender 5.2 desktop/WASM golden
updated: 2026-08-16 America/New_York
## Scope
The headless Web modifier evaluates the saved Blender Main graph only through the
16-node schema-1 allowlist. Eleven minimal Blender 5.2 fixtures make every allowlisted
node affect a measured output. The same fixture is evaluated by the full-Main WASM in
Node and by the production Chromium Worker before and after save/reopen.
This is a bounded constant-scalar, topology, attribute, and same-file resource closure.
It does not claim arbitrary Blender field/domain conversion, complete lazy-function
semantics, Geometry Nodes graph writing, linked resources, or Simulation Zones.
## Coverage
- Graph boundary: Group Input and Group Output.
- Geometry: Transform, Set Position, Join/Separate Geometry, Realize Instances, and
Store Named Attribute with a constant Float point-domain value.
- Scalar/vector: Integer, Vector, Value, Compare, and Math nodes on the bounded
operations accepted by the native evaluator.
- Same-file resources: Object Info, Collection Info with separate children, and Image
Info dimensions. Missing, recursive, linked, or otherwise unverified resources stay
behind the M10-02 resource gate.
## Evidence
- Fixture: `tests/files/web/geometry_node_allowlist_evaluator.blend`.
- Golden: `tests/golden/M10-03/geometry-node-evaluator.json`.
- Desktop generator: `tools/web/generate-geometry-node-evaluator-golden.py`; Blender
5.2.0 LTS emitted 11 cases, and a clean temporary regeneration matched every semantic
golden field after excluding output-path-dependent fixture path/hash fields.
- Node full-Main WASM: `node tools/web/check-geometry-node-evaluator-golden.mjs`;
16 nodes, 11 cases, maximum position error 0, RMS position error 0, and attributes
passed.
- Chromium: `WEB_TEST_PORT=5443 npm --prefix web run
test:geometry-node-evaluator-golden`; unit 4/4, Node golden 1/1, and Worker
open/evaluate/save/reopen 1/1 passed.
- Tolerances: maximum position `1e-5`, RMS position `1e-6`, Float point attribute
`1e-6`, and bounds `1e-5`.
- M10-01 regression on port 5444 and M10-02 regression on port 5445 passed.
- Full unit suite 131/131, typecheck, lint, production build (73 modules), local
dependency checks, and `git diff --check` passed.
## Implementation Hashes
- Native evaluator: `b896c5c52a611e7a309f80ca4bf8323ccded458584f65ab8f6841df24d70cc42`.
- Desktop generator: `f42ba309e4935a4534eabcb8ab1978abc08d863e2d18d80a70607f31dd3a3066`.
- WASM checker: `76a02d4aae70a597f0c26c12de3104d56e3f1cc9fded1d4824a05d7ddc59c1b5`.
- Fixture: `0cb3e33df570b436e32d783eef0a6c2daad04ca5bb4ccb4f0286b3d723ff7978`.
- Golden: `363ae47cfb50b8c95d0a55116e8663e41d27c7aa332cffe9b702bbe9b8cbfe1c`.
- Chromium spec: `83b1c96518eb64bef2c8830c778a84eb13d8da0ea62c2488471ed5fefea975c8`.
- Full-Main WASM: `432c5c9efb03b23a506c1b8f57f0d06ee683274bbdfe415f894f0d3c29db0b77`.
## Contract
- Evaluation is demand-driven from the active Group Output and is capped at 4,096
node-output evaluations. Unsupported nodes, sockets, operations, cycles, or resource
states return `GEOMETRY_NODES_EVALUATOR_UNSUPPORTED` without claiming evaluation.
- The evaluator uses Blender `GeometrySet`, geometry join/realize/transform helpers,
evaluated dependency graph resources, and Blender mesh attributes. Three.js does not
execute or approximate Geometry Nodes semantics.
- The fixture and golden inventory must cover every schema-1 allowlist entry before the
check starts; missing coverage fails before numeric comparison.
## Rollback
Remove the bounded evaluator expansion, M10-03 generator/fixture/golden/checker,
Chromium spec, package script, and this status file together. Revert the M10 count to
2/15 and restore the previous Transform/Set Position-only modifier evaluator.

75
docs/status/M10-04.md Normal file
View File

@@ -0,0 +1,75 @@
# M10-04 Status
status: done
task: Geometry Nodes field/domain materialization budgets
updated: 2026-08-16 America/New_York
## Scope
Field materialization schema 1 binds every request to a graph ID/hash, Main revision,
source and target domain, data type, transport, and an exact seven-domain cardinality
snapshot. The parser derives element, scalar, and byte counts rather than accepting
caller-provided counts. JSON never carries field values in this contract; the current
native Depsgraph path only publishes a small verified point-Float payload.
This task establishes budgets and transport boundaries. It does not claim actual
evaluation for arbitrary POINT/EDGE/FACE/CORNER/CURVE/INSTANCE/LAYER conversions,
complete Blender lazy-function semantics, graph writing, or Simulation Zones.
## Budgets
- Domain maxima: POINT 1,000,000; EDGE/FACE 2,000,000 each; CORNER 4,000,000;
CURVE/INSTANCE 100,000 each; LAYER 4,096.
- Batch maxima: 64 fields, 32 real domain conversions, 4,000,000 target elements,
and 64 MiB of materialized data.
- Per-field JSON maximum: 65,536 scalar values. Larger fields must stay binary or
return `GN_FIELD_JSON_BUDGET_EXCEEDED`.
- Supported bounded layouts are Boolean, Int, Float, Vector, and Color. Element,
component, and byte multiplication is checked before a receipt is returned.
## Native Contract
The full-Main Depsgraph reports exact POINT/EDGE/FACE/CORNER cardinality from the
evaluated Blender Mesh and zeroes the non-Mesh domains for the current fixture. The
observable `m10_value` point-Float attribute carries schema version, element/scalar
counts, materialized byte length, and transport. The TypeScript parser cross-checks
the receipt against mesh vertex count, domain cardinality, the Float32 byte layout,
and the actual attribute payload. Unknown receipt fields and hidden JSON arrays are
rejected.
## Evidence
- `WEB_TEST_PORT=5448 npm --prefix web run test:geometry-node-field-budget`:
unit 6/6 and real Chromium Worker 1/1 passed.
- The real `M10GN_StoreAttribute` evaluated mesh reported cardinality
POINT/EDGE/FACE/CORNER `8/12/6/24`, schema 1, eight Float scalars, 32 bytes, and
eight values equal to `0.375`.
- Chromium negative checks rejected POINT cardinality drift, byte-length drift, and
a hidden `values` array in the receipt.
- M10-01/02/03 regressions passed on ports 5449/5450/5451. M10-03 still covers all
16 allowlisted nodes across 11 desktop/full-Main WASM/Chromium cases with zero
maximum and RMS position error.
- Full unit suite 133/133, typecheck, lint, production build (73 modules), local
dependency checks, `git diff --check`, and the three-fixture 100-iteration
Depsgraph regression passed.
The first Chromium rerun on port 5447 was not accepted as evidence: the negative
check used a browser-relative dynamic import that resolved to a nonexistent
`/protocol/depsgraph.ts`. The check was moved to Playwright's Node-side static import,
then the complete task command was rerun on port 5448.
## Implementation Hashes
- Geometry Nodes protocol: `8d04291c29e9ce30e3f2f6d4141bc4101d49529bf7f7edf109d67d059efa58b1`.
- Depsgraph protocol: `df7e0e29422f5b605f20fd5ab939d170d35048020968d2f6aea86603d0633cd6`.
- Native Depsgraph: `5bcc17416fd4d45985bc1529288127b9eeed4a30b47836e687dc5321182c0b11`.
- Unit test: `1aa4df021731d7ba4c4191d56f88c663b223443f78c9fdc1b88c2a43423239c7`.
- Chromium spec: `95d8380d6a17a5b85df695a3cb850dffa8705f574ae0b9c97422bb802899eb8e`.
- Full-Main WASM: `1bb4a947da4332471f3d7bab8f3f628c83d1703244093a554a188668ddafafd7`.
## Rollback
Remove the field materialization schema/parser, native Depsgraph cardinality and
receipt fields, M10-04 unit/Chromium checks, package script, and this status file
together. Revert the M10 count to 3/15; do not leave the native attribute JSON path
without its receipt and cardinality validation.

68
docs/status/M10-05.md Normal file
View File

@@ -0,0 +1,68 @@
# M10-05 Status
status: done
task: Simulation cache graph/source/revision identity binding
updated: 2026-08-16 America/New_York
## Scope
Simulation cache manifest schema 2 binds every cache to the Geometry Nodes graph ID/hash,
the committed source `.blend` hash and revision, input hash, Blender 5.2 build, and the
declared frame range. A deterministic `revisionHash` is calculated from those fields and
the storage key is `sim2-` plus the complete 64-character digest. The payload hash and
per-frame hashes remain separate integrity checks.
This task closes identity and stale-source safety. It does not implement Simulation Zone
evaluation, browser bake, cancel, LRU, corruption quarantine, or playback capability.
## Protocol
- Schema 1 manifests and undeclared manifest/frame fields are rejected.
- `sourceRevision` is a bounded committed revision; frame values are bounded to +/-1,000,000.
- `revisionHash` uses a JSON-array canonical encoding with an explicit v2 domain marker,
avoiding ambiguous delimiter collisions.
- Graph/source/input/revision/frame-range drift returns
`SIMULATION_CACHE_REVISION_MISMATCH`; payload or source bytes still use their dedicated
hash mismatch codes.
## Storage Contract
`putSimulationCache`, full read, frame read, and list run through the per-project transaction
lock. Put checks revision and source hash before hashing/storing the payload. Full/frame reads
re-verify the manifest identity and current project; listing validates all manifests and filters
valid but stale revisions out of the current project view. Old rows are not silently reused.
## Evidence
- `WEB_TEST_PORT=5455 npm --prefix web run test:simulation-cache-identity`: unit 3/3 and
Chromium 1/1 passed. Revision 7 cache recovered after Worker restart; after save to revision
8, the old key was rejected and omitted from the list, while a new key was accepted.
- Negative checks covered forged graph hash, forged source hash, stale read/put, changed input,
changed source revision, changed frame range, legacy schema, undeclared fields, and frame
payload drift.
- `WEB_TEST_PORT=5456 npm --prefix web run test:simulation-cache`: identity and existing
Worker-restart smoke 2/2 passed.
- `WEB_TEST_PORT=5457 npm --prefix web run test:simulation-cache-performance`: 600/600 frames,
OPFS backend, playback completed, 600 published frames, 0 pending requests after terminate,
5,072 ms total, below the 30,000 ms gate.
- Full unit suite 136/136; typecheck, lint, production build (73 modules), local dependency
check, and `git diff --check` passed. The first full static pass had one unused import lint
failure after the read-path refactor; removing it and rerunning produced the results above.
## Implementation Hashes
- Simulation protocol: `974b006f1ad4cd1d3d294122fc777c89c3a7da04fdaa7aebedea12b545bc65e5`.
- Storage Worker: `a7b31eeb601e9e71c74eaceda95704f1d120653e9ade6bfbee6d262a42edaa74`.
- Unit test: `591582a4ce77b699a4462e30d587eb89034f29ec0a22a94c81f8ab7306f2524f`.
- Identity Chromium spec: `2446060a9c3955ef466adb479966bf424fd9e9c351b65cd8525e90a1a1032e4d`.
- Existing smoke: `0c8c001290c8c6b3482eeebc105b2f217c55b2eded0c62b4fee4595ec277a11d`.
- Performance spec: `f6c465b7afa012030dd8516d676e2663f4dd8298aa156defb905b3fa0229a84b`.
- Package: `f9e28fb23af28720f56f97b249cb636599ebd92955ab2ff6d234b821e6d5ea44`.
- Error registry: `4c6045b1cab8348f0acf180a1e618a0d691ddc5092822ca288f974904e5e13a5`.
## Rollback
Remove schema 2 identity fields and hash helper, restore the prefix-truncated key, revert
Storage Worker project-lock checks and stale filtering, remove the identity unit/E2E/package
entries and this status file, then restore the M10 count to 4/15. Do not retain schema 2
manifests with schema 1 readers.

84
docs/status/M10-06.md Normal file
View File

@@ -0,0 +1,84 @@
# M10-06 Status
status: done
task: Simulation cache cancellation, playback gate, LRU, restart and corruption isolation
updated: 2026-08-16 America/New_York
## Scope
Simulation cache playback is now a lifecycle-gated operation. A cache may be read by
frame only after the current Storage Worker has completed a full manifest, payload and
per-frame hash verification. This closes cache lifecycle safety; it does not implement
Simulation Zone evaluation, browser bake generation, or GN modifier seek semantics.
## Protocol and Client
- `SIMULATION_CACHE_NOT_READY`, `SIMULATION_CACHE_CANCELLED` and
`SIMULATION_CACHE_BUDGET_EXCEEDED` are stable error codes.
- `verifySimulationCacheCancellable` checks cancellation between total and per-frame
SHA-256 work. `StorageClient` accepts `AbortSignal`, removes an aborted request from
`pending`, and sends a `cancelRequest` to the Worker so late responses cannot publish.
- `planSimulationCacheLRU` sorts removable entries by `lastAccessAt`, `createdAt`, and
`cacheKey`; active playback keys are protected. If protected entries alone exceed the
requested budget, the result reports `budgetSatisfied: false` instead of evicting them.
## Storage Contract
- IndexedDB schema 7 adds `simulation_quarantine`; the M10-06 Chromium test creates a
temporary schema 6 database and verifies the real v6 to v7 migration and store.
- `prepareSimulationCachePlayback` performs full verification in the current Worker and
activates the key. `readSimulationCacheFrame` rejects unprepared keys, while `release`
removes readiness and LRU protection. A successful full read also establishes verified
readiness for compatibility with existing callers.
- `pruneSimulationCaches` removes deterministic LRU manifests and deletes their
unreferenced simulation assets. Active playback is automatically protected.
- Invalid manifests and missing, truncated, or hash-mismatched payloads are moved from
`simulation_manifest` to `simulation_quarantine`. Quarantine preserves the bad row for
diagnostics while valid cache listing and playback continue.
## Evidence
- `WEB_TEST_PORT=5464 npm --prefix web run test:simulation-cache-lifecycle`: unit 5/5 and
real Chromium 1/1. It covers v6 to v7 migration, post-restart `NOT_READY`, full
verification, StorageClient cancellation, BrowserTransform playback cancellation with
zero published frames and zero pending requests, active-cache LRU protection, release,
cancelled write cleanup, OPFS byte tamper, and quarantine listing.
- `WEB_TEST_PORT=5460 npm --prefix web run test:simulation-cache-identity`: unit 5/5 and
Chromium 1/1. Existing graph/source/revision stale filtering remains green.
- `WEB_TEST_PORT=5461 npm --prefix web run test:simulation-cache`: identity and existing
Worker-restart smoke 2/2.
- `WEB_TEST_PORT=5462 npm --prefix web run test:simulation-cache-performance`: 600/600
frames, OPFS backend, 6,639 ms total, 0 pending requests after terminate, and restart
recovery passed the 30,000 ms gate.
- Targeted schema/snapshot smoke 2/2 and recent-project recovery 4/4 passed after the
schema version update. Full unit suite passed 138/138; typecheck, lint, production
build (73 modules), local-dependency check, and `git diff --check` passed.
The first lifecycle run was not accepted as evidence because the test helper returned the
numeric legacy `DOMException.code` value `20` instead of the `AbortError` name. The helper
was corrected and the complete command was rerun on port 5459, then rerun again on port
5464 after adding playback cancellation and migration assertions.
## Implementation Hashes
- Simulation protocol: `3025db84bd4e0151b8894a457c51da630d628afb1760e2f0caeec003f0efce3f`.
- Storage protocol: `2711dbab0c52a566679b83d5e4d793ec0ffd3f991e744dce765b6bdc54d25c13`.
- Error registry: `7ebff554dd6346b539215a6545c67806bca2ead33953700b79c0910fd0cfb408`.
- Storage Worker: `e31d7dcf93c5dcefb7e6c4cfd7874c748f972849ee7d93adb6294f82aafcf236`.
- Storage client: `aaa3a03e3b91452e90a661566407c261c7a16b1671ed55ec948de7626d49bfa4`.
- Storage migration: `8de1fcbbda9d7a4e7496995306c4e9fc83224211a516cf7241496b6a145953b5`.
- OPFS helper: `2e8ef9d72194bf803a7e823fb2b7de30b9f10ae6dd4b031b81b97ff82eee9cc6`.
- Unit test: `4ece0f5051667b04fab78c81a847def7a0c77d9267bd9e4fa2bbe4b9308125bf`.
- Lifecycle Chromium spec: `861d370f72896987cef65d2e4d78ab3edcc1d4828d4a0fa0b5301b72a4a2932a`.
- Identity regression: `aa305871e8651018342b123112beab10a43dab3313e2c9765e450dc91c08b6e1`.
- Performance regression: `d7083c24789fc5877a7841bf611aee0ff471cc191621de1b30720d637635b44c`.
- Smoke/recovery version updates: `2d15e5bf54a1dcdec1686ee937a510a7521ad44b152c895289a152ea711fadf4` /
`a4d6279f05b6215f2e34cd5f1d76ebb6d5ed2cc765859c2ae0af975623f93b2c`.
- Package: `fe90c0bd741a7840f6184726737a1a93c606841b8408dd7fd6dec823a8352cf6`.
## Rollback
Remove the schema 7 migration and `simulation_quarantine` store, restore frame reads to
the schema 6 identity-only contract, remove cancellation/LRU/playback commands and the
M10-06 tests/status entry, then restore the M10 count to 5/15. Do not retain schema 7
databases with a schema 6 reader.

59
docs/status/M10-07.md Normal file
View File

@@ -0,0 +1,59 @@
# M10-07 Status
status: done
task: Bounded WebGL2/Three physical compiler for the declared Shader node subset
updated: 2026-08-16 America/New_York
## Scope
The browser material path now compiles only the declared RGB, Value, Math,
Image Texture, Normal Map, Principled, and Material Output closure. The compiler
produces a schema-versioned `WEBGL2_THREE_PHYSICAL` report before a Shader Main
transaction is sent to Blender. Unknown nodes, cycles, duplicate inputs, missing
or blocked images, oversized graphs, and invalid constants fail closed without
mutating the source graph.
This task does not claim arbitrary Shader Node support, WebGPU compilation,
complete color management/sampler parity, compile-key invalidation, or pipeline
rollback; those remain later M10 tasks.
## Protocol and Runtime
- `web/protocol/shader-compiler.ts` defines the M10-07 allowlist, fixed node/link/
depth/texture/identifier budgets, deterministic graph fingerprint fallback,
scalar Math evaluation, and bounded `ShaderCompileReport`.
- The Engine Worker compiles `setShaderGraph` before the native Main transaction,
rejects a blocked report, and returns the report with the committed delta.
- Main-thread and Offscreen viewport material creation share `createPBRMaterial`;
compiled constants and declared base-color/normal texture bindings are stored in
material `userData` for the texture adapter.
## Evidence
- `WEB_TEST_PORT=5521 npm --prefix web run test:shader-compile`: unit 5/5 and
Chromium 3/3 passed. The suite covers the real Main graph transaction, Math
result and topological order, Image/Normal bindings through the shared viewport
path, missing resources, cycles, duplicate inputs, oversized graphs, forged
fingerprints, and source graph immutability on blocked input.
- `tests/golden/M10-07/shader-compile.json` fixes the fixture digest, backend,
allowlist, six Math operations, and compiler budgets.
- The first attempt on the default port was rejected before browser startup because
an existing process owned `127.0.0.1:5173`; it is not counted as a feature failure.
## Implementation Hashes
- Compiler: `50b29c99d73c886a41bb8e10ccd2309068a1c09b23196655659b2f530d31f856`.
- Engine Worker: `376efe09de20150fc32803745da7e7e55bb41d178302e45b1fd90a2fe7778b2e`.
- PBR adapter: `f9c2e5ede635d11673f971c985288821af24ed81ecaadc95efefd958637672fd`.
- Texture adapter: `d5e379e25e0ab8294523690c4d4c79203b6f1610b4d6a608b33a7118f51efa57`.
- Unit test: `1e5d44b4f73f4ee4e4c9329f73628980df9337632dc48a3d50689f051a89b6e7`.
- Chromium spec: `c59b9f349bd6ce329ffc89f64eba3fec3e1e02a528978bdc1c3793d921faabc3`.
- Golden: `6bb8c07f014a0f2c1fd0e9ec16b08b99a524d8d3161bc556767acaf510c6c961`.
- Package: `ea0fef34497d51448d5478f4a761bef3c4ca816c408fd8a0807eaa0b093d0df8`.
## Rollback
Remove the compiler, Worker gate, shared material/texture adapter integration,
M10-07 tests, golden and this status file together. Restore the M10 count to
6/15 and leave Shader Main's existing capability gate and writeback contract in
place; do not retain a compiler report that is not enforced before Main mutation.

59
docs/status/M10-08.md Normal file
View File

@@ -0,0 +1,59 @@
# M10-08 Status
status: done
task: Shader compile key binding for graph, textures, color space and backend
updated: 2026-08-16 America/New_York
## Scope
Every bounded Shader compile report now carries a deterministic 64-character
`compileKey`. The key is SHA-256 over the task-local report schema, graph hash,
renderer backend, and sorted texture identities: image ID, usage, asset ID,
asset SHA-256, and resolved SRGB/LINEAR/NON_COLOR color space.
Texture identities are optional for legacy SceneIR rows; missing metadata is
represented explicitly as `null`, while usage-based color-space defaults remain
deterministic. An unknown renderer backend or malformed texture digest blocks
compilation. The global `web/protocol/schema-version` remains 1 because this is
an additive field in the task-local Shader report, whose schema is still 1.
## Runtime
- `ImageIR.colorSpace` is optional and validated; render asset requests use it
when present and otherwise retain the existing usage-derived color space.
- The Engine Worker passes snapshot image asset/hash/color-space identity into the
compiler. The PBR adapter accepts the same context for main-thread and Offscreen
material creation.
## Evidence
- `WEB_TEST_PORT=5523 npm --prefix web run test:shader-compile-key`: unit 7/7 and
Chromium 1/1 passed. The browser test proves key changes for texture SHA-256 and
color-space changes through the application PBR entry point.
- `WEB_TEST_PORT=5524 npm --prefix web run test:shader-compile`: M10-07 regression
unit 7/7 and Chromium 3/3 passed.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`,
`npm --prefix web run build`, `npm --prefix web run test:status-consistency`,
and `git diff --check` passed.
The first browser attempt on port 5522 used a repository-root dynamic import that
Vite does not expose and was rejected before exercising the compiler. The test was
moved through the application PBR entry point and rerun in full on port 5523.
## Implementation Hashes
- Compiler: `acd5f8deb2912f6823533df12baba8049c5e64d440c17a6dc3f90741f71774b2`.
- SceneIR: `a65ef3b4be5303b1b86b3ee6829c795870dcc2490ba58d3a66b8bed8172d6442`.
- Render assets: `840c09a6c2ec56cc75a9ca72bcbdde6957612a7c61b19ab83be47c571eca8b67`.
- Engine Worker: `668728f7e5b6edf9e1cee3e6152238d16ac19f43f030e94e77bfe24e7ff50489`.
- PBR adapter: `8a9cb52c8e7b1481096017cc659d93541c21724c1203d22755e37e1e3c365250`.
- Unit test: `9ae9bb4c778d9e31a607d0c7d678ecf3448ed4e5b57c88ab4ba767a2f0850cc7`.
- Chromium spec: `a09ebfa451e76c33f421bc8402fac4c77faedee88020431a505602f317b4eb65`.
- Golden: `187666d9dfb9d9cdd5bd6195cc2f7ee57bb78fa0d05d6020762c06a4ab015984`.
## Rollback
Remove `compileKey`, texture identity context, ImageIR color-space metadata,
M10-08 tests/golden and the Worker/adapter wiring together. Restore the M10
count to 7/15 and retain graphHash-only reports with the original usage-derived
render asset color-space behavior.

40
docs/status/M10-09.md Normal file
View File

@@ -0,0 +1,40 @@
# M10-09 Status
status: done
task: Preserve the previous usable material pipeline on compile failure
updated: 2026-08-16 America/New_York
## Scope
`PBRMaterialPipeline` makes material replacement transactional. A successfully
compiled candidate becomes current only after compilation; the previous material
is disposed exactly once after replacement. A blocked candidate is disposed and
the current material remains usable, with the failed report stored as
`shaderCompileFailure` for diagnostics. This does not claim arbitrary Shader Node
support or a WebGPU pipeline.
The Engine Worker independently rejects blocked `setShaderGraph` requests before
the Blender Main transaction, so a failed graph cannot publish a new SceneIR
snapshot in addition to the viewport-side rollback.
## Evidence
- `WEB_TEST_PORT=5525 npm --prefix web run test:shader-pipeline`: Chromium 1/1.
The test confirms the failed candidate reports `SHADER_NODE_UNSUPPORTED`, keeps
object identity and the previous compiled report, and that a later successful
compile replaces the material.
- `npm --prefix web run typecheck` and `npm --prefix web run lint` passed after the
pipeline helper was added.
- `tests/golden/M10-09/shader-pipeline.json` fixes the rollback invariants.
## Implementation Hashes
- PBR adapter: `8a9cb52c8e7b1481096017cc659d93541c21724c1203d22755e37e1e3c365250`.
- Chromium spec: `e6771f672e885250db199cbf97a0a3866084ab9bb3f2ad25d17f5c53edaa99af`.
- Golden: `5dbb916e512935a76dacde37242b123707897f54ab10d7b99e246f57afbc94ac`.
## Rollback
Remove `PBRMaterialPipeline`, its Chromium test/golden and this status file. Keep
the M10-08 compile key and Engine Worker pre-transaction gate, and restore the M10
count to 8/15.

39
docs/status/M10-10.md Normal file
View File

@@ -0,0 +1,39 @@
# M10-10 Status
status: done
task: Stable capability block for unsupported Shader nodes
updated: 2026-08-16 America/New_York
## Scope
The Shader capability query now accepts arbitrary node type strings and returns a
stable `PBR-012/ARBITRARY_SHADER` block for every type outside the declared bounded
compiler set. Unsupported node names are de-duplicated and sorted, so the same
graph receives the same error regardless of input order. The block uses
`SHADER_NODE_UNSUPPORTED`, remains recoverable, and does not silently report READY.
The existing ShaderGraph validator and Engine Worker gate continue to preserve the
source graph and reject blocked `setShaderGraph` commands before any Main mutation.
This task does not expand the compiler allowlist.
## Evidence
- `WEB_TEST_PORT=5526 npm --prefix web run test:shader-capability`: Chromium 1/1.
`VORONOI` and `CUSTOM_OSL` both return the expected stable block; reversed input
order produces the identical task/capability/status/code/message result.
- `npm --prefix web run typecheck` and `npm --prefix web run lint` passed.
- `tests/golden/M10-10/shader-capability-block.json` fixes the status, error code,
recoverability and graph-preservation policy.
## Implementation Hashes
- Render capability gate: `322929e9c7b627a9e79b8a6f11d5e9b0c7c05a5754f82d41c45c1134bbaaf385`.
- Chromium spec: `6337801f10b9115ba7b53a43152ae69a324b5baa61c8e455cc61505437228cbf`.
- Golden: `b7182bb57e10d0b302b4c7dc9bc43160721a2c9414b056c86173c3b5c2501739`.
- Package: `d6d4dcaf4f5c852f884ba2064da96a8c30519c661bf4a16c59e115742aa37e8a`.
## Rollback
Restore the enum-only `ARBITRARY_SHADER` request, remove deterministic sorting,
the M10-10 Chromium test/golden and this status file, and restore the M10 count to
9/15. Keep the existing M10-07 compiler fail-closed behavior.

44
docs/status/M10-11.md Normal file
View File

@@ -0,0 +1,44 @@
# M10-11 Status
status: done
task: NLA track/strip/action/time mapping read-only exact evaluation
updated: 2026-08-16 America/New_York
## Scope
A Blender 5.2 desktop-authored fixture contains one object, one NLA Track and two
Action Clip strips. The first maps Action frames 1..11 to scene frames 20..40 with
scale 2. The second maps the same Action range to frames 45..65 with reverse playback
and repeat 2. Both use `NOTHING` extrapolation so samples outside their ranges expose
boundary behavior rather than hold behavior.
The Main reader must preserve Track, Strip, Action and time-mapping fields. Blender's
native Depsgraph evaluates 12 boundary/interior/cycle frames; Node WASM and the
production Chromium Worker compare every world matrix with the desktop golden. The
test asserts that evaluation does not change NLA or Action JSON. It does not call
`setNLAStack` and does not claim the M10-12 operator transaction.
## Evidence
- Fixture: `tests/files/web/nla_time_mapping_scene.blend`, SHA-256
`b32f783debe213a1345ec4528e5ed27f0f4de00d7896b626df39756289119855`.
- Golden: `tests/golden/M10-11/nla-evaluation.json`; Blender 5.2.0 LTS, 1 Track,
2 Clip strips and 12 frames.
- `WEB_TEST_PORT=5527 npm --prefix web run test:nla-evaluation-golden`: Node/WASM
checker and Chromium 1/1 passed; maximum matrix error 0 and read-only identity passed.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and
`npm --prefix web run check:local-deps`: passed.
## Implementation Hashes
- Fixture generator: `a941ecb850ddd165ede117f6411615c6daed4d6bdaffaeac8228a308b7fbe745`.
- Golden generator: `941a63cc9edbaa0d8dd25025872e3fc44bf64df54d5a9232ad4128e6ac0cd5c5`.
- Node/WASM checker: `2ce9db0fcbceaf30e398e8feb5c002a60395ac34e61382d9414e980e51e6fdd5`.
- Golden: `2d148ac0cbc2ac9281d430da20dba8373ad1bb0de6ec5fa578adf36296455fbe`.
- Chromium spec: `c12f105d206a03dae5b73b076cc3b0a758d597cea7532b6720f4653568df0245`.
## Rollback
Remove the fixture/golden generators, fixture, M10-11 golden/checker/Chromium spec,
package command and this status file together. Restore the M10 count to 10/15. Keep
the existing N-014 bounded Main writer and reverse/repeat regression unchanged.

45
docs/status/M10-12.md Normal file
View File

@@ -0,0 +1,45 @@
# M10-12 Status
status: done
task: One NLA edit operator with Main transaction, undo/redo and save/reopen
updated: 2026-08-16 America/New_York
## Scope
The bounded `moveNLAStrip` command moves one existing Action Clip while preserving
its duration and time mapping. It requires the current `baseRevision`; stale commands
return `REVISION_CONFLICT` before Main mutation. The Worker derives a candidate stack
through the pure protocol helper, validates Action/path/range/overlap gates, and commits
it through one authoritative Main `setNLAStack` transaction. The native history entry
therefore covers the exact pre-edit `.blend` bytes.
This task intentionally opens only the move operator. Create/remove/resize/active,
multi-track blending, Transition/Meta/Sound, NLA UI and export remain blocked or planned.
## Evidence
- Pure contract: `web/protocol/nla.ts` and `web/tests/unit/nla.test.mjs`, 2/2 tests.
The source stack is not mutated; overlap, unknown identity and frame budget errors
are deterministic.
- `WEB_TEST_PORT=5528 npm --prefix web run test:nla-operator`: unit 2/2 and Chromium
1/1 passed. The browser path proves stale revision rejection, Main move 20..40 ->
5..25, monotonic revision/delta, undo to 20..40, redo to 5..25, save/reopen at
5..25, and native frame-10 evaluation at X=2.5.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check`:
passed.
## Rollback
Remove `moveNLAStrip` from `web/protocol/nla.ts` and `web/protocol/web-engine.ts`,
remove the Worker conversion, unit/E2E tests and package command, and delete this
status file. Restore the M10 count to 11/15. Keep M10-11 read-only evaluation and
the pre-existing whole-stack `setNLAStack` authoring path.
## Implementation Hashes
- `web/protocol/nla.ts`: `03d5abce5cb2f0a3a38e42eedbced7f4de6e2052d18e53a3dcde3b3ca1e6020d`
- `web/protocol/web-engine.ts`: `48f2290ead27aeda1f17b279f01ae278776b50b4cb4de065d94b0e1d44717dbb`
- `web/app/src/workers/web-engine.worker.ts`: `4ccad87c1619db8e39d2855ebca9918883484ce3e5dfc8a325ccee9a8220397c`
- `web/tests/unit/nla.test.mjs`: `d8bac2dd3e4ec1a1342de7c7401a51beb9879ea5ba2b34df7d615ae0449081a7`
- `web/tests/e2e/nla-operator.spec.ts`: `1658d52eb0b8cf329f73be552e9b2649ed6dc96b21edb48ade9dd4a725a37ae0`
- `web/package.json`: `e9d5c6e4f7132921faac8ecbee40215098497fe374d56309f2c76fab152c33d6`

48
docs/status/M10-13.md Normal file
View File

@@ -0,0 +1,48 @@
# M10-13 Status
status: done
task: Probe local Physics solvers per family and keep unsupported families bake-only
updated: 2026-08-16 America/New_York
## Scope
The Physics protocol now probes all seven declared families independently. A local
solver becomes `READY` only after its runtime export exists, initialization succeeds,
its thread requirement is met and its required memory fits the active WASM limit.
Missing exports, initialization errors, insufficient threads or memory, and malformed
probe results remain `BLOCKED` and route to `DESKTOP_SERVER_BAKE`.
The production inventory has no installed solver runtime adapter, so Rigid Body,
Soft Body, Cloth, Fluid, Dynamic Paint, Particle and Hair all remain bake-only.
The synthetic positive probe verifies the gate and does not declare an actual solver.
Desktop family decoding, cache playback, browser bake and server job submission remain
blocked for later tasks.
## Evidence
- `node --test web/tests/unit/physics-solver-probe.test.mjs`: 3/3. It covers the
default seven-family fallback, family isolation across all four gates, and invalid
environment/result fail-closed behavior.
- `WEB_TEST_PORT=5530 npm --prefix web run test:physics-solver-probe`: unit 3/3 and
Chromium Worker 1/1. The browser result matches the M10-13 golden and keeps every
failed family on `DESKTOP_SERVER_BAKE`.
- `WEB_TEST_PORT=5531 npm --prefix web run test:e2e -- --grep "N-018 physics"` and
`npm --prefix web run test:physics-main-reader`: passed without changing existing
metadata/cache gates or claiming a solver.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, production build
(74 modules), and `git diff --check`: passed.
## Implementation Hashes
- `web/protocol/physics-simulation.ts`: `adc57a12c7f57c6b1d5f99590d3c36cefa83636aca86c780c2520f3f41148981`
- `web/tests/unit/physics-solver-probe.test.mjs`: `e499d55770d2024c75803ce1acc1c234814c027279f2b40f42195fd480977653`
- `web/app/src/workers/physics-solver-probe-test.worker.ts`: `fbad383de186a659440da42dcad988310ca2e483db27e30d74342c8f57a0f49a`
- `web/tests/e2e/physics-solver-probe.spec.ts`: `1419b98d68da9b0ab0bf41b37843442741593655e2693159d84e069f1dec3f09`
- `tests/golden/M10-13/physics-solver-probe.json`: `bcc8f9094e2b0d8f41ef01fda0daa2a29693d188e602447d6d06358aaa751df4`
- `web/package.json`: `875162709315e2a8116c5d28023576a4b95fb076b79453de8b21acdbff2df9dc`
## Rollback
Remove the solver probe types/functions, unit/Worker/E2E/golden/package command and
this status file, then restore the M10 count to 12/15. Keep the existing N-018
metadata, Main reader, Simulation cache and BTF1 playback primitive unchanged.

47
docs/status/M10-14.md Normal file
View File

@@ -0,0 +1,47 @@
# M10-14 Status
status: done
task: Validate source hash, frame range, byte budget and version for every Physics cache family
updated: 2026-08-16 America/New_York
## Scope
Every Physics family cache now uses cache schema 1 and binds its family, desktop or
server bake origin, Blender 5.2 version, source blend/settings/input/cache SHA-256,
frame range, total byte length and per-frame byte offset/length/SHA-256. `COMPLETE`
caches require every frame; `PARTIAL` caches may omit frames but remain strictly
ordered and byte-contiguous. Per-frame and total cache budgets match the existing
bounded Simulation cache policy.
`verifyPhysicsCachePayload` hashes the actual current blend, full cache payload and
every frame before consumption. Family/source/version/range/budget drift fails closed.
This task validates cache identity and bytes; it does not implement a Blender family
decoder or change the existing cache playback capability block.
## Evidence
- `node --test web/tests/unit/physics-cache-family.test.mjs`: 3/3. All seven families
and both desktop/server sources pass; source/payload drift and family/schema/version/
range/budget/undeclared-field failures return stable codes.
- `WEB_TEST_PORT=5533 npm --prefix web run test:physics-cache-family`: unit 3/3 and
Chromium Worker 1/1, matching the M10-14 golden.
- M10-13 probe Chromium 1/1, Physics Main reader, M10-05 Simulation cache identity
unit 5/5 + Chromium 1/1, typecheck, lint, production build (74 modules), and
`git diff --check`: passed.
## Implementation Hashes
- `web/protocol/error.ts`: `6972a144bdc61c2a4270c88df2d86c75640bb5f24b5ac3b7f9a02d69d3c500fd`
- `web/protocol/physics-simulation.ts`: `b02b820503d79cd26b0323a4d2033e79d0eeb3032befd26fafe5eab55634584c`
- `web/app/src/workers/physics-simulation-test.worker.ts`: `453c6e9e683b9da0ef7ce4bd0e4e430b97102d80ddb9b14cc9d075319b37d818`
- `web/tests/unit/physics-cache-family.test.mjs`: `e1c6f07ec58ce4c4c987bd8a36f270497ae3a0f521d6ed27d7f199debeeaccb2`
- `web/app/src/workers/physics-cache-family-test.worker.ts`: `8af676506cc0ff1501816431d92d82ca15316b40095d617ecb664b9608e209e0`
- `web/tests/e2e/physics-cache-family.spec.ts`: `6a2788587f6692bf5bf19d76400810fa277a6985d1a6ecc1b8d1adb85c609249`
- `tests/golden/M10-14/physics-cache-family.json`: `b220d1fd73f307213078ce3cbd6212544f97ade84a45d54847f107cef69d302d`
- `web/package.json`: `9baeb319d8c740f67f3e7615942c78f920e7a56b0d654a5a8f47d68de7243eb4`
## Rollback
Restore the former Physics cache binding, remove the two new cache hash error codes,
unit/Worker/E2E/golden/package command and this status file, then restore M10 to
13/15. Keep M10-13 solver probing and the existing GN Simulation cache schema 2.

51
docs/status/M10-15.md Normal file
View File

@@ -0,0 +1,51 @@
# M10-15 Status
status: done
task: Browser performance, OOM-prevention and malicious-input gates for GN, Shader, NLA and Simulation
updated: 2026-08-16 America/New_York
## Scope
GN, Shader, NLA and Simulation now run in four isolated Chromium Worker sessions.
Each session executes a representative bounded workload, rejects an over-budget input
before evaluator/payload allocation, rejects a domain-specific malicious graph or
manifest, and then proves that a small valid input still succeeds in the same Worker.
NLA gains explicit track/strip/string budgets and exact declared-field checks.
Simulation cache total/frame/range overages now return the dedicated
`SIMULATION_CACHE_BUDGET_EXCEEDED` code. This is a deterministic OOM-prevention gate;
it does not claim that arbitrary browser heap exhaustion is recoverable inside a
domain evaluator.
## Evidence
- `WEB_TEST_PORT=5536 npm --prefix web run test:m10-domain-gates`: Node 23/23 and
Chromium 1/1. Measured Worker times were GN 54 ms (10 x 512 nodes), Shader 17 ms
(100 compiles), NLA 62 ms (20 x 256 strips), and Simulation 2 ms (64 frame hashes),
all below their 5/5/5/10 second gates.
- Stable OOM/malicious codes: GN `GN_GRAPH_BUDGET_EXCEEDED`/
`GN_DEPENDENCY_CYCLE`; Shader `SHADER_NODE_UNSUPPORTED`/`SHADER_GRAPH_CYCLE`;
NLA `NLA_BUDGET_EXCEEDED`/`NLA_INVALID_STACK`; Simulation
`SIMULATION_CACHE_BUDGET_EXCEEDED`/`SIMULATION_CACHE_INVALID`.
- Existing GN field budget (unit 6/6 + Chromium 1/1), Shader compiler (7/7 + 3/3),
NLA operator (4/4 + 1/1), and Simulation lifecycle (6/6 + 1/1) all passed.
- Typecheck, lint, production build (74 modules), status consistency and
`git diff --check`: passed.
## Implementation Hashes
- `web/protocol/error.ts`: `88119782307cb9a61e131f2d5e2e25127256c1e68a333fc7fc3206158b8a1019`
- `web/protocol/nla.ts`: `96ae7cf9ca3fadbaf98ca15a38937329f203b0bc4a9c39ef685987e7f55353dd`
- `web/protocol/simulation-cache.ts`: `a1302e42129420a96416e363cbc857d082d4646655685520c9e81f877ce3d9b8`
- `web/tests/unit/nla.test.mjs`: `e11aa1f395970613cee5d37a9eb21c3fac3422f0080c75c78eb8777effb77adc`
- `web/tests/unit/simulation-cache.test.mjs`: `2be82cdda991b47fbf8955cfbf2f92cb1fdc7b7c0debb0c8102d4f35e8fce8e3`
- `web/app/src/workers/m10-domain-gate-test.worker.ts`: `bc508e1c81ca6efefd67b5c572f93c27dd71bd602319ae887c50e27f8a309a31`
- `web/tests/e2e/m10-domain-browser-gates.spec.ts`: `2973a596df8613a9cfdb0bd4d300d95b71a8e818e615278a39fb777db6c5cf03`
- `tests/golden/M10-15/domain-browser-gates.json`: `580051434ec6673251bac755cc189939de1f887aad088497e16a7657f3a83963`
- `web/package.json`: `092931cebf9cb14814951703af26e2d0e4c5e0295c1ba4cb748bfe4ee49448c2`
## Rollback
Remove the NLA stack budgets/exact-field checks, restore the former Simulation cache
budget error mapping, remove the domain Worker/E2E/golden/package command and this
status file, then restore M10 to 14/15. Keep all M10-01 through M10-14 behavior.

42
docs/status/M11-01.md Normal file
View File

@@ -0,0 +1,42 @@
# M11-01 Status
status: done
task: Camera, Light, World and Scene color-management field parity table
updated: 2026-08-16 America/New_York
## Scope
`tests/golden/M11-01/lighting-field-parity.json` is the machine-readable field table
for all CameraIR, LightIR and WorldIR leaves plus Scene render engine and color
management. Each field independently records reader, writer, main viewport and
Offscreen viewport status as VERIFIED, PARTIAL, METADATA_ONLY, BLOCKED or
NOT_APPLICABLE, with an aggregate parity status.
The table contains 60 fields: 24 COMPLETE, 17 PARTIAL and 19 BLOCKED. It explicitly
keeps true Orthographic/Panoramic/Custom projection, camera DOF rendering, light
radius/area spread/sun angle, World environment rotation and visual Mist, display/
view/look/gamma/white-balance rendering and Scene writer behavior outside COMPLETE.
## Evidence
- `npm --prefix web run test:lighting-field-parity`: AST table checker and the real
Main lighting roundtrip passed. The checker proves exact type coverage, unique fields,
valid states, existing evidence paths and required blocked claims.
- The Main roundtrip covers Camera/DOF, Light exposure/temperature/shadow, World/Mist,
validation rejection, undo/redo, save/reopen and the white-balance integrity gate.
- `WEB_TEST_PORT=5541 npm --prefix web run test:e2e -- --grep "N-019"`: Chromium 2/2,
covering Scene-over-World exposure, shadow metadata and renderer-bound World/Scene
delta preservation.
- Typecheck, lint, production build (74 modules), status consistency and
`git diff --check`: passed.
## Implementation Hashes
- Parity table: `b7fee80b4f2469cf119902c939f9091e640a6f0e1307e97ba29f0afa8942c4ed`
- AST checker: `cf910a087f9af72e0782d62e67341f90fd4af1c6c8eddf54169ee6275e24bd9c`
- Package: `cd7174b04b19e61a280e03e6c61e2ffd20fd507c0716c820b8faa31ea2438ebf`
## Rollback
Remove the M11-01 table/checker/package command and this status file, restore M11 to
0/14, and keep the existing N-019 reader/writer/viewport implementation unchanged.

48
docs/status/M11-02.md Normal file
View File

@@ -0,0 +1,48 @@
# M11-02 Status
status: done
task: Supported Camera, Light and World fields through Main, history, persistence and viewport mapping
updated: 2026-08-16 America/New_York
## Scope
The bounded `setCameraProperties`, `setLightProperties` and `setWorldProperties`
commands now have one task-level acceptance path through Blender Main, monotonically
revisioned undo/redo, in-memory `.blend` save, a fresh Worker reopen and the shared
main-thread/Offscreen PBR mapping functions.
The golden covers Camera lens, both sensor dimensions and fit modes, shift, clip,
ortho scale and DOF fields; Light color, energy/exposure, temperature, shadow, spot
and area fields; and World color/exposure/Mist fields. Viewport assertions cover both
horizontal and vertical sensor fit, clip and film offset, temperature-adjusted linear
RGB, intensity, shadow, spot angle/blend, area dimensions and World background color.
Fields kept PARTIAL or BLOCKED by M11-01 remain outside visual parity claims.
## Evidence
- `WEB_TEST_PORT=5545 npm --prefix web run test:lighting-field-roundtrip`: native
Main roundtrip passed and Chromium 1/1 passed.
- Camera, Light and World edits each advanced revision; task-level Chromium coverage
undid and redid every domain, saved, opened the bytes in a fresh Worker and compared
the reopened values with float32-aware `1e-5` tolerance.
- The native rejection path kept an invalid `near >= far` edit out of Main. Existing
white-balance integrity protection remained blocked after Blender serialization
rather than publishing invalid Scene color-management values.
- `npm --prefix web test`: 156/156 passed. Typecheck, lint, production build
(74 modules) and `git diff --check` passed.
## Implementation Hashes
- Golden: `ac6d84c0099b12cf6c96df2dbbb4281e3308c36f0bda49c92d7548e58ccc286e`
- Chromium spec: `9f75c7b88c70ab42c6c418b0056dcffcab3b8851003258891902db9615fcfe67`
- Native roundtrip checker: `023e3e43bc37fb38c3063d33c20e59043308013874fce20bc3a49d508b850e6c`
- Web command protocol: `48f2290ead27aeda1f17b279f01ae278776b50b4cb4de065d94b0e1d44717dbb`
- Blender Main writer: `510b4017c540fc4d1ac135c1ce7251cc5d7143f31d8604e1d545de36716da9a2`
- Shared PBR adapter: `78352e46c62d0c8d9bf25408ea1d725a843a42068ea3eb304bb3d78f8d612e0b`
- Package: `8074d80265a1378fb3f284206c343ff1e388a5ea43e57946a1d4092076d72ea8`
## Rollback
Remove the M11-02 golden/spec/package command and this status file, restore M11 to
1/14, and keep the underlying N-019 bounded writers and viewport mappings at their
previous independently tested status.

55
docs/status/M11-03.md Normal file
View File

@@ -0,0 +1,55 @@
# M11-03 Status
status: done
task: Explicit Three WebGL2 and WebGPU light, shadow-map and texture budgets
updated: 2026-08-16 America/New_York
## Scope
Schema 1 `PBRRenderBudget` freezes separate `THREE_WEBGL2` and `THREE_WEBGPU`
product ceilings. WebGL2 reserves two built-in lights and one built-in shadow map
inside totals of 16 lights and four 1024-square shadow maps. The WebGPU contract uses
64 lights and eight 2048-square maps. Runtime device limits may only lower these
ceilings.
The shared planner deterministically retains light nodes in SceneIR order, retains a
bounded shadow-capable prefix and reports every dropped identity with stable
`GPU_LIGHT_BUDGET_EXCEEDED`/`GPU_SHADOW_BUDGET_EXCEEDED` issues. Main-thread and
Offscreen production viewports publish the same report on the canvas.
Texture planning covers asset count, maximum dimension, aggregate compressed payload
and decoded RGBA GPU bytes before hash/decode/allocation. An over-budget batch leaves
the previous texture set alive. This task defines the WebGPU budget contract but does
not claim that the still-unbundled Three WebGPU renderer is available.
## Evidence
- `WEB_TEST_PORT=5548 npm --prefix web run test:render-resource-budget`: unit 3/3 and
Chromium 3/3. Both production viewports requested 20 lights, rendered 14, dropped
six, rendered three of 14 requested scene shadow maps and blocked 11; with reserved
resources, the main Three scene contained exactly 16 lights and four shadow maps.
- A 257-asset batch was rejected before decode in both viewports. A separate browser
case first decoded a valid PNG, rejected a 257-asset follow-up (258 including the
retained asset), and proved the original texture object/hash remained installed.
- M11-02 field roundtrip remained 1/1. Real 4K (64 MiB decoded) and 8K (256 MiB
decoded) texture performance cases passed; packed raster main/Offscreen passed 2/2.
- Full unit was 159/159. Typecheck, lint, production build (75 modules), local runtime
dependency checks and `git diff --check` passed.
## Implementation Hashes
- Budget protocol: `d7784690358e38b71c6ad3ccc5baf82869d897714d5257f219bbd00dc7e7b0f7`
- PBR adapter: `934874434afa6778eaf783c78650ad221a428352741f3ff7173cb3c17a0c3c14`
- Texture store: `0f18f3bcf04c5c99175d4cac8bbf1bc028fc54d635701fb6a92791e06c335fb6`
- Main viewport: `dcb502ca892fd5431cbf1a13823aec5d83a2aa0a1b40487cb66fa630c6e461b3`
- Offscreen Worker: `c95bce10f0bd73591f002c21415bfbc6f709ecbe61e97a3b5b05bcec2b0e0bef`
- Unit: `4bde90788bbf9a94317d83bd398acd321bbcf4e53e26bf40165a889bacff03a2`
- Chromium spec: `90834dfff09e61a2d32732d3a8f0db4b9c71414ac4991412d93d322119ef2de7`
- Golden: `c312bf5ed8415ad51e5cccb3a2653f65cd9fee46488be2f860e6a5a8d5a7901d`
- Package: `c718983c3f3a552f4c68d9390376777e45a3f23e6999afd5cb4ddc786a12a278`
## Rollback
Remove the render-budget protocol, planner integration, canvas reports, unit/E2E/
golden/package command and this status file; restore the former fixed 1024 shadow
configuration and per-asset-only texture checks, then restore M11 to 2/14.

56
docs/status/M11-04.md Normal file
View File

@@ -0,0 +1,56 @@
# M11-04 Status
status: done
task: Explainable image-error metrics between the Web realtime viewport and a Blender 5.2 reference
updated: 2026-08-17 America/New_York
## Scope
Schema 1 `RenderImageComparisonIR` compares equal-size display-referred `SRGB8`/
`STRAIGHT` RGBA frames. The report records mean absolute error, root mean squared
error, P95 channel error, maximum channel error, bad-pixel ratio, reference-background
foreground intersection-over-union and alpha coverage delta. Each release check keeps
its measured value, threshold, direction and pass state; a mismatch returns the stable
`RENDER_REFERENCE_MISMATCH` code. Dimensions, byte lengths, threshold ranges and a
4,194,304-pixel bound are validated before comparison.
The reference is generated by Blender 5.2 Eevee from
`tests/files/web/m11_render_reference.blend` with a fixed camera, black rough material,
World color, one-sample TAA and zero dither. The source fixture, generator and PNG are
SHA-256 bound in `tests/golden/M11-04/manifest.json`. Re-render verification compares
decoded Blender pixels exactly because PNG container bytes may differ while pixels do
not.
## Evidence
- `WEB_TEST_PORT=5552 npm --prefix web run test:render-reference`: unit 3/3,
Blender reference re-render verification passed, Chromium 3/3. Main-thread and
Offscreen reports were identical: MAE `2.0433349609375`, RMS `5.448994264342599`,
P95 `4`, maximum channel error `110`, bad-pixel ratio `0.0047760009765625`,
foreground IoU `0.9853400565736072`, alpha coverage delta `0`.
- Thresholds are MAE `4`, RMS `12`, P95 `12`, bad-pixel ratio `0.02`, foreground
IoU `0.97` and alpha coverage delta `0`. A non-empty red block with the wrong
composition is rejected as `RENDER_REFERENCE_MISMATCH`.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, production build
(75 modules), `npm --prefix web run test:status-consistency` and `git diff --check`
passed.
## Implementation Hashes
- `web/protocol/render-image-comparison.ts`: `4345cc0d02b4482201de34292cbf5198009bf615e69c80cc00bc3e76116efec2`
- `web/app/src/three-adapter/render-image-comparison.ts`: `ad97fd594d0d964e05d4da2bffc84a64ef737fef700045c252b5222250f1ffc7`
- `web/protocol/error.ts`: `e8ea483466b16debaf803ff1640ade07a858a802781e3040d3547028ecb5d19f`
- `tools/web/generate-m11-render-reference.py`: `118c98c77d98922e72660898bfb65842e11743fc77f4124c9f5088a76cb6a94a`
- `tools/web/check-render-reference.mjs`: `ead3500f054e98c2db4cd6596c983586a622e929bc8a587e9fbd38d94a7df7d1`
- `web/tests/unit/render-image-comparison.test.mjs`: `f9800745760b85700e80abc5e389f44d7f7ea3b9e41e1002f4e5449bcb620f67`
- `web/tests/e2e/render-reference.spec.ts`: `349c7da0dfd60baec80139cced52079312bd5e892a42f000ba0b4f55c6a4a61d`
- `tests/files/web/m11_render_reference.blend`: `d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963`
- `tests/golden/M11-04/blender-eevee-reference.png`: `f5c22636a232bcbb0ed0f772418cb12fced18ff72f4999804197e42caa61480a`
- `tests/golden/M11-04/manifest.json`: `e9551f86f0b9704d7be35c03f07321bec5d2481ffee4208ae766156d9c9a0135`
- `web/package.json`: `38ba6a722a2e0bc2c91d6edbd2b7df5e3609342e3b7ef2ec9646b9c6ce0d25a6`
## Rollback
Remove the M11-04 protocol, reference fixture/golden, generator/checker, unit/E2E
specs and package command; restore M11 to 3/14 and remove the bounded reference slice
from N-019. Keep M11-01 through M11-03 resource and lighting behavior unchanged.

47
docs/status/M11-05.md Normal file
View File

@@ -0,0 +1,47 @@
# M11-05 Status
status: done
task: Server-only routing for Cycles, complex Eevee and hardware render backends
updated: 2026-08-17 America/New_York
## Scope
Schema 1 `RenderRoutingRequestIR` separates execution target from endpoint
availability. Only bounded Eevee on WebGL2, or WebGPU after both browser and bundled
renderer capability checks, may return `WEB_LOCAL_BOUNDED/READY`. Cycles, complex
Eevee, Workbench final rendering and CUDA/OptiX/HIP/Metal/oneAPI requests always
target `SERVER_JOB`; the default unconfigured endpoint returns
`SERVER_JOB_UNAVAILABLE` without a local approximation. Unknown, syntactically valid
SceneIR engine identities fail closed as `PLATFORM_CAPABILITY_UNAVAILABLE`.
Setting `serverRenderAvailable:true` can make a server route `READY`, but only proves
the routing decision. It does not submit a job or claim the source/build/settings/
output hash contract reserved for M11-06.
## Evidence
- `WEB_TEST_PORT=5556 npm --prefix web run test:render-routing`: unit 3/3 and
Chromium 1/1. The browser reads `BLENDER_EEVEE` from the real M11 reference `.blend`
through the production WebEngine Worker before checking the local/server matrix.
- Cycles, complex Eevee and OptiX all remained `SERVER_JOB/BLOCKED` with
`SERVER_JOB_UNAVAILABLE` under the production default. A synthetic configured
server context returned `SERVER_JOB/READY`, never a Web target.
- Unbundled WebGPU remained `WEB_LOCAL_BOUNDED/BLOCKED` with
`WEBGPU_RENDERER_UNAVAILABLE`; an unknown engine remained server-targeted and
blocked with `PLATFORM_CAPABILITY_UNAVAILABLE`.
- Typecheck, lint, production build (75 modules) and `git diff --check` passed.
## Implementation Hashes
- `web/protocol/render-routing.ts`: `2d41d48609635c810572aaae95979df2fe9e49bbd130c60d70f64042c5d0f097`
- `web/app/src/three-adapter/render-routing.ts`: `f360c05fa62aa830a486c5be4747488fcbb5ba9b68d82b4f7187e1267da46fa6`
- `web/tests/unit/render-routing.test.mjs`: `4ac2e7a6c2265f948d971f72effb4ff9ad1c51839ab6c06d496fc2b0768c4fa8`
- `web/tests/e2e/render-routing.spec.ts`: `e5572d9ad9426f1dcc1856fe426261910093fa2f5460117e4ffdcfc7b23b10a1`
- `tests/golden/M11-05/render-routing.json`: `53c5d3a2e51f251d14953594567081597be4250be5b1236832370e74805bd35d`
- `web/package.json`: `716a5331990618892bca97f48c7eef589a5af6c5131b8cf3ae52a1b9d923644c`
## Rollback
Remove the M11-05 routing protocol/wrapper, unit/E2E/golden/package command, restore
M11 to 4/14 and remove the N-019 server-routing completed slice. Keep M11-04 realtime
reference metrics and all earlier lighting/resource behavior unchanged.

48
docs/status/M11-06.md Normal file
View File

@@ -0,0 +1,48 @@
# M11-06 Status
status: done
task: server render job provenance binding
updated: 2026-08-17 America/New_York
## Scope
`web/protocol/server-render-job.ts` defines schema 1 for a server render request and
result. A request is accepted only when the uploaded `.blend` bytes match its declared
source SHA-256 and byte length, the Blender build is a declared 5.2 build with a
content hash, and canonical bounded render settings match `settingsSha256`. The
request itself is bound by `requestSha256`.
A successful result carries the same source/revision/build/settings identity and adds
output MIME, byte length, and output SHA-256, plus `resultSha256`. Consumers recalculate
the output hash before accepting pixels. The contract does not claim a remote queue,
progress, cancellation, denoise, or Freestyle implementation; those remain later
M11 tasks.
## Evidence
- `npm --prefix web run test:server-render-job`: unit 3/3 and a real loopback server
check passed.
- The check used Blender 5.2.0 headless to open
`tests/files/web/m11_render_reference.blend` and render a PNG from the submitted
source bytes. It verified source/build/settings/output hashes and rejected source,
build, settings, and output tampering.
- Result: `server-render-job-ok blender=5.2.0 source=d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963 settings=97bb832618bd27e8763722afa0d250ef74383fb355f0ff38763930f57ba1590b output=564b95cd501da2636085aeada88814c4a3b7af7b1f0188543a1863fe34dd091e bytes=3915 source-hash=1 build-hash=1 settings-hash=1 output-hash=1 tamper=4`.
- Full regression passed: typecheck, lint, Node `168/168`, status consistency
(`12` parity blocked, `0` release blocked), release evidence (`17` records,
`0` missing), production build (`75` modules), and `git diff --check`.
## Implementation Hashes
- `web/protocol/server-render-job.ts`: `e2b5b95032b0dbcb346aa35d1a10a3b55d6c006603f16b9e72c3de45763906c5`
- `web/protocol/error.ts`: `747ea52fd96b3ec7ede9ce72c15f56fdc454836ba5bf924cc60198096d4986de`
- `web/tests/unit/server-render-job.test.mjs`: `c84f86365006360f146601839d7e1394f3f7bc7fa71dcbf62d8d4e4321cdd16d`
- `tools/web/check-server-render-job.mjs`: `6dd49fbd1be202a88fa714b2fb81305018da6df5f9d73c95e048e3697742764e`
- `tools/web/render-server-job.py`: `48950b0884a52a40ae234f8bbe590a0cb89e58c4d2a2de40ea638f491d111aff`
- `tests/golden/M11-06/server-render-job.json`: `36f083f8da7585f850e748e6ac78fc57cd6e8d59bce03d77eee3acd2fa042ae7`
- `web/package.json`: `628191a76c7614393a74969bd000faa8a6302d887265d13a1ad49dcfc401c2fc`
## Rollback
Remove the server-render-job protocol, error codes, unit/check commands, and this status
record; restore M11 to 5/14 and remove the M11-06 completed slice. Keep M11-05 routing,
M11-04 reference metrics, and all earlier lighting/resource behavior unchanged.

36
docs/status/M11-07.md Normal file
View File

@@ -0,0 +1,36 @@
# M11-07 Status
status: done
task: compositor allowlist CPU/WebGPU node goldens
updated: 2026-08-17 America/New_York
## Scope
The schema 1 WebGPU compositor allowlist contains only `CONSTANT_COLOR`, `EXPOSURE`,
`INVERT`, and `COMPOSITE`. A Blender 5.2 fixture supplies an independent constant,
exposure, invert, and integrated-chain scene through the production Main/WASM reader.
The production CPU executor and a real Chromium WebGPU compute pipeline must produce
identical `LINEAR_SRGB` Float32 RGBA bytes for every case.
Resource inputs, branching graphs, Viewer, Transform, Alpha Over, Blur, Mix, and
undeclared Blender nodes remain outside the allowlist and fail before shader
compilation. This task does not claim the M11-08 graph-preservation pipeline, HDR/color
management parity, a production compositor scheduler, or server compositor execution.
## Evidence
- `WEB_TEST_PORT=5562 npm --prefix web run test:compositor-node-golden`: Node 3/3,
Blender 5.2 Main/WASM fixture check, and Chromium WebGPU 1/1 passed.
- The four CPU and WebGPU Float32 SHA-256 values match the committed golden exactly;
maximum absolute error is `0`.
- The first run without an isolated port passed Node/native checks but did not start
Chromium because port 5173 was already occupied. It was not counted as evidence;
the complete command was rerun on port 5562.
- Full regression passed: typecheck, lint, Node 171/171, production build, status
consistency, release evidence validation, and `git diff --check`.
## Rollback
Remove the WebGPU compositor plan/executor, M11-07 fixture/goldens/tests, and this
status record; restore M11 to 6/14 and remove the N-020 WebGPU golden completed slice.
Keep the existing bounded CPU compositor and Main graph reader unchanged.

37
docs/status/M11-08.md Normal file
View File

@@ -0,0 +1,37 @@
# M11-08 Status
status: done
task: preserve unsupported compositor graphs and block execution
updated: 2026-08-17 America/New_York
## Scope
The CPU compositor and cached execution entry points now preflight the complete parsed
graph before evaluation or cache lookup. Any `UNSUPPORTED` node blocks with
`COMPOSITOR_NODE_UNSUPPORTED`, including a disconnected node that does not contribute
to the Composite output. Unsupported Blender type names are sorted and deduplicated so
the failure is deterministic.
The gate is pure: it does not remove nodes, rewrite links, advance Main revision, run a
cancellation callback, allocate an output, or accept a previously cached result. The
existing Blender 5.2 fixture preserves its Glare node name, `UNSUPPORTED` IR type, and
`CompositorNodeGlare` source type before and after the failed attempt.
## Evidence
- `WEB_TEST_PORT=5565 npm --prefix web run test:compositor-unsupported-gate`: unit 2/2,
native Main reader, and Chromium 1/1 passed.
- `WEB_TEST_PORT=5566 npm --prefix web run test:e2e -- --grep "N-020"`: 2/2 passed;
the supported synthetic CPU graph still executes, while the real graph containing a
disconnected Glare node now fails closed.
- The first new unit run was 172/173 because an async rejection used `assert.throws`.
It was corrected to `await assert.rejects`; no implementation failure was counted as
evidence. The complete Node suite then passed 173/173.
- Full regression passed: typecheck, lint, production build, status consistency,
release evidence validation, and `git diff --check`.
## Rollback
Remove the complete-graph preflight from CPU/cached execution and the M11-08 tests and
golden, restore the old N-020 smoke expectation, restore M11 to 7/14, and remove the
N-020 completed slice. Keep M11-07 CPU/WebGPU goldens unchanged.

43
docs/status/M11-09.md Normal file
View File

@@ -0,0 +1,43 @@
# M11-09 Status
status: done
task: runtime codec probes for IMAGE, SOUND, and MOVIE strips
updated: 2026-08-17 America/New_York
## Scope
Schema 1 codec probe requests and receipts bind the Sequencer strip family, canonical
MIME type, source byte length, and source SHA-256. File names and source paths are not
accepted by the probe contract, so extensions cannot grant capability. A READY receipt
must also use the family-specific backend and carry validated decoded dimensions or
audio frame metadata.
The production browser probe hashes the supplied bytes and performs a real decode:
`createImageBitmap` for IMAGE, a fixed 48 kHz `OfflineAudioContext` for SOUND, and a
muted `HTMLMediaElement` load/seek/canvas readback for MOVIE. The committed movie is a
deterministic 16x16, two-frame H.264 MP4 generated locally by FFmpeg. This task does not
claim frame-accurate WebCodecs seeking, waveform/proxy generation, A/V synchronization,
arbitrary codec support, or final encoding.
## Evidence
- `WEB_TEST_PORT=5570 npm --prefix web run test:sequencer-codec-probe`: unit 3/3 and
Chromium 1/1 passed. PNG, WAV, and H.264 MP4 returned READY with their expected
runtime backend and decoded metadata.
- Deliberately wrong file extensions had no effect. Corrupt bytes, source identity
drift, PNG bytes declared as MOVIE, a forged backend, and a forged receipt all stayed
BLOCKED with `SEQUENCER_CODEC_UNSUPPORTED` at the gate.
- The first Chromium attempt failed before business execution because `/protocol` is
not exposed by Vite; the test now imports the existing app adapter. The second decoded
all media but exposed device-rate Web Audio resampling; the production probe now uses
a fixed 48 kHz OfflineAudioContext. The complete command was then rerun successfully.
- Sequencer Main reader, N-021 browser regression, and the one-million-frame long media
performance/restart test passed. Full regression passed: typecheck, lint, Node suite,
production build, status consistency, release evidence validation, and
`git diff --check`.
## Rollback
Remove the codec probe protocol/runtime/tests/movie fixture, restore the MIME-set gate,
restore M11 to 8/14, and remove the new N-021 completed slice. Keep the V1 long-media
IMAGE/SOUND index/cache behavior and all M11-08 compositor behavior unchanged.

57
docs/status/M11-10.md Normal file
View File

@@ -0,0 +1,57 @@
# M11-10 Status
status: done
task: bind long-media proxy/cache entries to source hash and runtime decode capability
updated: 2026-08-17 America/New_York
## Scope
Schema 1 proxy-cache manifests bind the complete movie source identity, the source-bound
M11-09 READY decode receipt, an explicit SRGB8/STRAIGHT RGBA8 profile, and the source
frame into one deterministic SHA-256 cache identity. The proxy payload has an independent
byte length and SHA-256. Source drift, decode-capability drift, identity tampering, and
payload tampering therefore fail with separate stable codes.
The production browser adapter verifies the movie bytes before decode, captures only the
initial decoded HTMLMedia frame, and stores verified copies in a 64 MiB-bounded LRU. The
Chromium gate persists the RGBA8 payload and its manifest through the content-addressed
Storage Worker path, creates a new Storage Worker, and verifies the reopened bytes against
the current runtime receipt. This task does not claim frame-accurate proxy generation,
seek/scrub publication, waveform generation, A/V synchronization, or final encoding.
## Evidence
- `WEB_TEST_PORT=5573 npm --prefix web run test:sequencer-media-cache`: unit 3/3 and
Chromium 1/1 passed. The real 16x16 H.264 fixture produced one 8x8/256-byte RGBA8
proxy; its cache identity is
`68a3af14865841e81f69bd75f2605461de4819fe025d158ac3723fa0cdf31525`.
- A one-frame cache budget admitted frame 0, returned a copied hit, then evicted it when
frame 1 was inserted. `clear()` released exactly 256 bytes and left zero entries.
- Source hash drift, READY receipt metadata drift, payload mutation, and bad source bytes
returned `SEQUENCER_CACHE_SOURCE_MISMATCH`,
`SEQUENCER_CACHE_CAPABILITY_MISMATCH`, or `SEQUENCER_CACHE_HASH_MISMATCH` as
appropriate. Blocked receipts, undeclared profile fields, oversized RGBA8 profiles, and
upscaled profiles were rejected before cache insertion.
- The first unit run was 2/3 because the test incorrectly treated exactly 64 MiB as over
budget; the corrected 64 MiB plus one-row case passed. The first Chromium run completed
all business operations but sampled `statsBeforeClear` after `clear()`; the sampling
order was fixed and the full command rerun on ports 5572 and 5573.
- Regression passed: M11-09 unit 3/3 plus Chromium 1/1, one-million-frame long-media 1/1,
Sequencer Main reader, full Node 179/179, typecheck, lint, and production build (75
modules).
## Artifact Hashes
- protocol: `1dd5ee8cade2642723bac6f0c8e34d09cf178d000beba6eb63626284ee35fd5e`
- browser adapter: `57070b1a42129afdfb8121ec11a48b0792b3de9d84f7e8a78017c7d6e12343aa`
- app protocol exports: `8ca82257c758580f5fe91a3a5cd21f8ae9e9ce10f9fa71fcd7cf1f30e0b6367b`
- unit: `f4cc1171dd6875bf079e242beed846409a1d1f53b9497882aa475bd2773b43c7`
- Chromium spec: `6d9d26e3639a74b173f77ec5d67e53e5ec42b88dd6e34bda54c8168b8251423e`
- golden: `39a71f8d97f0045da6ec913b6a0231ac3363f67a420eea5ab3fabaa8f1d47c33`
- package: `2c7fb9f8657155088e4a080443b8a0ba9c13aae9f46f1807ff3b8820640c44c4`
## Rollback
Remove the proxy-cache protocol/runtime/tests/golden and package command, restore M11 to
9/14, and remove the new N-021 completed slice. Keep M11-09 runtime codec probes and the
V1 long-media index/cache evidence unchanged.

52
docs/status/M11-11.md Normal file
View File

@@ -0,0 +1,52 @@
# M11-11 Status
status: done
task: revision-gate late seek, scrub, and decode results before publication
updated: 2026-08-17 America/New_York
## Scope
Schema 1 uses one request identity for SEEK, SCRUB, and DECODE. Every request binds a
timeline ID, timeline revision, monotonically increasing request revision, operation,
and frame. A completed result must echo that identity and add a bounded source frame and
payload SHA-256. Publication is allowed only when both the timeline revision and latest
request revision still match the controller state.
The production controller advances one shared request revision across all three operation
types. Replacing a timeline invalidates every pending request before a new request begins,
and same-timeline revisions must advance monotonically. The publish callback is invoked
only after the pure gate returns PUBLISH, so stale results cannot update the visible frame
or write a decoded payload into cache. This task does not claim frame-accurate WebCodecs
seeking or the M11-14 cancellation/restart resource lifecycle.
## Evidence
- `WEB_TEST_PORT=5576 npm --prefix web run test:sequencer-media-revision`: unit 4/4
and Chromium 1/1 passed on the first complete run.
- Chromium scheduled SEEK revision 1 behind SCRUB revision 2. SCRUB published first and
the late SEEK returned `STALE/REVISION_CONFLICT`. A real H.264 runtime decode started at
timeline revision 7, completed after replacement with revision 8, and was rejected
before publish/cache. The revision-8 DECODE published; a forged result identity stayed
stale.
- The machine sequence is frozen in
`tests/golden/M11-11/sequencer-media-revision.json`: only `SCRUB@2` and `DECODE@5`
publish, and only `DECODE@5` reaches the cache-write callback.
- Regression passed: M11-10 unit 3/3 plus Chromium 1/1, M11-09 unit 3/3 plus Chromium
1/1, one-million-frame long-media 1/1, full Node 183/183, typecheck, lint, and production
build (75 modules).
## Artifact Hashes
- protocol: `326f1525f50af3b2c4132ff58508ed6174ba3aa4a5075b2a6660e549c280826a`
- production controller: `29a44404bb28e46d67439eec0622a69ce4cd7264ce45da77a3f320c8687b7006`
- app protocol exports: `be2eca8482a60e437878b99f69d3178f47fee61618886d141760320e3a3ed1e1`
- unit: `2099f738120897dfbb8662741831774b5f9150d51924dfa7832907177de5e7`
- Chromium spec: `e47f105360ce0e2d4d561d3cf790ceec9f41dd6bac7361965914ada307db843c`
- golden: `672b4fde6e27f8b15cd51d839cacb5efb128c0d0f4c7f6aa4e184c713f9b45e5`
- package: `5d17b17d67ee587191552c3dfe226cd8f2e5a872791224d8e6c22aafa1177ebe`
## Rollback
Remove the media revision protocol/controller/tests/golden and package command, restore
M11 to 10/14, and remove the N-021 revision-gate slice. Keep M11-09 runtime probes,
M11-10 proxy-cache identity, and the V1 long-media generation behavior unchanged.

58
docs/status/M11-12.md Normal file
View File

@@ -0,0 +1,58 @@
# M11-12 Status
status: done
task: keep final video encoding on an explicit server-export capability route
updated: 2026-08-17 America/New_York
## Scope
Schema 1 final-export requests bind the Main timeline ID/revision, source `.blend`
SHA-256, bounded frame range and rational frame rate, output dimensions, and one declared
container/video/audio codec combination. The canonical encoding settings and the complete
source identity receive separate SHA-256 hashes, so source revision drift and settings
drift produce different request identities.
The only route is `SERVER_EXPORT`. A missing endpoint fails closed with
`SEQUENCER_EXPORT_SERVER_UNAVAILABLE`; a configured endpoint returns
`SERVER_EXPORT_REQUIRED`, not local success. Browser `VideoEncoder` availability is
recorded for diagnostics only and cannot change the route or the invariant
`localEncoding=BLOCKED`. This task does not claim a server encoder implementation,
upload/progress/cancellation, audio mixing, or encoded-output verification.
## Evidence
- `WEB_TEST_PORT=5579 npm --prefix web run test:sequencer-final-export`: unit 4/4 and
Chromium 1/1 passed on the first complete run.
- Chromium opened the real Blender 5.2 `sequencer_scene.blend` through WebEngine Main and
bound timeline `sequencer:scene:SequencerScene`, revision 1, frames 1..250, 24000/1001
FPS, and source SHA-256
`5f5212487bb6b5df62b5ca915c75133f1ca45678614712ea4c7d902d823cd90c`.
- The fixed settings hash is
`01e39d1fdd88c75aecad7bc48a2b756bbcd645929963528b42ceb180b51e5566`;
the source/timeline/settings request hash is
`20d3a9e333804e014a993a9892a557842293fc44902229dd6eb65661f0691cfc`.
- No-server, server-without-encoder, server-with-injected-encoder, and actual Chromium
runtime cases all retained the server route and blocked local encoding. Undeclared
fields, invalid codec combinations, over-budget ranges, and environment drift returned
`SEQUENCER_EXPORT_REQUEST_INVALID`.
- Regression passed: M11-09 unit 3/3 plus Chromium 1/1, M11-10 unit 3/3 plus Chromium
1/1, M11-11 unit 4/4 plus Chromium 1/1, one-million-frame long-media 1/1, Sequencer Main
reader, full Node 187/187, typecheck, lint, and production build (75 modules).
## Artifact Hashes
- protocol: `559516163ce9a9c60917033455c71a1ae6be2f34666371f12723a056ddfdb3dd`
- browser adapter: `f9f193eb71afa48a23b4eb618cd98b5c0064c59943b3652fb6a601b545ac175c`
- app protocol exports: `d30783528fabaaefe8835998e11d75da357d265ee06db67713e57e4b85b58d7d`
- error contract: `747894b216db59c753b24ce85eb946925e64340f63684311e5d9628c9a6a9c8e`
- unit: `6f5030852e89160fd898b2aff4dc9911963342bd6c2301a2c0e8a3d518ddbcbd`
- Chromium spec: `d8cba79c9a330c1e5c777c8f0ed08362b29f50702c31d8229fd65f4304bce363`
- golden: `c0061f82ef156e53ad4dfc6aedf7c6a8f53e616d8d9fa1435fd9491ba3d07e01`
- package: `83ee5bfa50a0e5b742048d4563ae815171232a121cf3df4a1e813f21313a6509`
## Rollback
Remove the final-export protocol/browser adapter/tests/golden and package command, remove
the two export error codes and SequencerTimeline exports, restore M11 to 11/14, and remove
the N-021 final-export completed slice. Keep M11-09/10/11 decode, proxy, and revision
gates and the existing `sequencerRuntimeCapabilities().localEncoding=BLOCKED` contract.

58
docs/status/M11-13.md Normal file
View File

@@ -0,0 +1,58 @@
# M11-13 Status
status: done
task: verify AudioContext suspend, resume, missing-device, and mute recovery
updated: 2026-08-17 America/New_York
## Scope
Schema 1 audio-session reports separate context state
(`UNAVAILABLE/SUSPENDED/RUNNING/CLOSED`) from output state
(`BLOCKED/SILENT/ENABLED`). Every initialize, resume, suspend, mute, device-recovery, and
close operation advances a monotonic revision. Reports bind the mute flag, effective gain,
and a stable issue code, and reject contradictory states such as enabled output from a
suspended or muted context.
The production `SequencerAudioSession` owns one `AudioContext` and master `GainNode`.
Mute schedules gain zero; unmute restores the configured nominal gain. Suspend and resume
must reach the corresponding real context state before output can be enabled. Missing API,
constructor failure, and resume/suspend failure remain silent with structured errors.
Closing first zeros and disconnects the gain node, closes the context, drops all references,
and reports `CLOSED/SILENT`. This task does not claim waveform generation, A/V sync,
multi-strip mixing, output-device selection, or encoded audio export.
## Evidence
- `WEB_TEST_PORT=5586 npm --prefix web run test:sequencer-audio-recovery`: unit 3/3 and
Chromium 1/1 passed on the first complete run.
- The Chromium case creates a real `AudioContext` inside a trusted click handler, then
verifies suspend, mute, resume-while-muted, unmute, second suspend/resume, and close.
Muted output remains `RUNNING/SILENT` at gain 0; unmute restores
`RUNNING/ENABLED` at gain 0.75; close reports `CLOSED/SILENT` at gain 0.
- A scope without `AudioContext` returns
`UNAVAILABLE/BLOCKED/SEQUENCER_AUDIO_DEVICE_UNAVAILABLE`. The deterministic unit path
then makes a constructor available and verifies `recoverDevice()` plus resume. Constructor
and resume failures stay silent; malformed state reports return
`SEQUENCER_AUDIO_CONTEXT_INVALID`.
- Regression passed: M11-09 unit 3/3 plus Chromium 1/1, M11-10 unit 3/3 plus Chromium
1/1, M11-11 unit 4/4 plus Chromium 1/1, M11-12 unit 4/4 plus Chromium 1/1,
one-million-frame long-media 1/1, Sequencer Main reader, full Node 190/190, typecheck,
lint, and production build (75 modules).
## Artifact Hashes
- protocol: `0b1b75c7630b599723914bbced9ee0a107bd8c0ac4382ced1f1c5114dac8fd01`
- production controller: `c09914d2581ee454e042ed6a9ef5b8c0f6f7b0688eadb1deff45163263742109`
- app protocol exports: `e630f647ddd3680a04e07ce7388cf03f54268d4e2d839aa5991cb9ceb6a53bda`
- error contract: `6589f712058a39a2177cb174cc355c01777a549d254c4b9eac2301a344c619f2`
- unit: `1a888d1b19a3d12091e6edd05287a5bcc19912415cf7f2ed73a028d12846a298`
- Chromium spec: `1b94ea07ab5fafb0b06fbd73742c77d6b69d745a546fc60def85857844afc719`
- golden: `584aea46687fd5cec8d89e8fac51314acc9a8d05af2a8d762e8d6a74483e0940`
- package: `8d87390f9b1b7382bf9c0ba189f7a9d95daa3ee15d1d422a8d856fa4eb6d3cca`
## Rollback
Remove the audio-session protocol/controller/tests/golden and package command, remove the
four audio error codes and SequencerTimeline exports, restore M11 to 12/14, and remove the
N-021 audio-recovery completed slice. Keep M11-09 offline audio decoding and M11-12 final
export routing unchanged.

64
docs/status/M11-14.md Normal file
View File

@@ -0,0 +1,64 @@
# M11-14 Status
status: done
task: close render, compositor, and media cancellation/restart/resource recovery
updated: 2026-08-17 America/New_York
## Scope
Schema 1 evidence fixes one report for each of `RENDER`, `COMPOSITOR`, and `MEDIA`.
Every report binds real source bytes, a domain-specific cancellation code, a domain-specific
budget code, generation 1/2 identity and output hashes, positive release receipts, zero
owned resources after release, and a non-empty recovered output. The suite parser rejects
undeclared fields, missing or duplicate domains, published cancellation results, restart
drift, budget mutation, and incomplete release.
The Chromium production-path case opens real Blender Main fixtures for Render and
Compositor and decodes a real H.264 fixture for Media. Render reads an actual WebGL RGBA8
frame, Compositor executes the real Main GraphIR into Float32 output, and Media generates
the initial RGBA8 proxy frame. This task closes only bounded lifecycle behavior; it does not
claim full Eevee/Cycles parity, arbitrary compositor nodes, frame-exact long-video decode,
or complete audio/video rendering.
## Evidence
- `WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery` passed:
protocol negatives 3/3, Blender 5.2 Eevee reference checker, Blender 5.2 Compositor
golden checker, and Chromium 1/1.
- Render used `m11_render_reference.blend`, rejected a pre-cancelled open with
`OPEN_CANCELLED`, preserved the active Main, restarted WebEngine at generation 2, and
reproduced the same scene identity and WebGL output SHA-256. Aggregate texture overflow
returned `GPU_TEXTURE_BUDGET_EXCEEDED` without replacing the retained texture; dispose
reported positive released bytes/resources followed by zero owned textures.
- Compositor used `m11_compositor_allowlist.blend`, returned `COMPOSITOR_CANCELLED` from the
periodic executor check, rejected width 8193 with `COMPOSITOR_BUDGET_EXCEEDED` before
cache mutation, reproduced GraphIR/Float32 output after Worker restart, and cleared the
LRU to zero.
- Media probed and decoded `sequencer-probe.mp4`, rejected a pre-cancelled proxy decode with
`SEQUENCER_CANCELLED`, rejected a cache one byte below the frame size with
`SEQUENCER_BUDGET_EXCEEDED`, cleared the first cache, then re-probed and re-decoded the
H.264 source before reproducing the source/decode/profile identity and RGBA8 payload
SHA-256 in a rebuilt session.
- Regression passed: full Node 193/193, typecheck, lint, production build (75 modules),
render resource budget unit 3/3 plus Chromium main/Offscreen 3/3, Compositor golden unit
3/3 plus Chromium 1/1, media proxy cache unit 3/3 plus Chromium 1/1, status consistency,
release evidence, and `git diff --check`.
## Artifact Hashes
- evidence protocol: `a1b0b36cf93173347d55a955ff2e02f3666f7c28ef7f06442b86f0ee2b272286`
- Chromium production suite: `ef9cd58ce3bbd522c72075e82d6423387b61039509996851d8d5ebf3a0d86789`
- media proxy cache: `7a1dbf8e838c47e2b99bc8932997e79d3c3cf2636b6285ace083800d833e2e64`
- GPU texture store: `a87adefa67b243cfd0cfcad0573a1b646f1e8be8abd0a187e1dc29cab2954d44`
- compositor cache: `3ce4819dadb2d6de7305c3676b2708689cd8be95e6fa7894651d6a02c4658b31`
- unit: `3cec4a33f28a76423a9678bcb61244cc463f5f2f5939c3f0b6865d34063e1f55`
- Chromium spec: `1578848eda7cb26cdf1584fa13f4cc2e252e782959d489dc4454e45c8d06595a`
- golden manifest: `87f17b0437d819e00c4a0902146d6549cf713482667ead627a45ba2b46050247`
- package: `0e731a5c826a999eef372f875c6c5ce9d697fd369a929025370a74cf18a93582`
## Rollback
Remove the recovery protocol, browser suite, unit/Chromium tests, M11-14 golden manifest,
and package command. Restore the media proxy generator signature, Compositor cache clear,
and GPU texture release receipt only if no later caller depends on them. Remove the three
family lifecycle slices, restore M11 to 13/14, and move the queue back from M12-01A.

59
docs/status/M12-01A.md Normal file
View File

@@ -0,0 +1,59 @@
# M12-01A Status
status: done
task: inventory Blender 5.2 AssetMetaData, tag, and catalog fields
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The machine inventory freezes the Blender 5.2 storage and public API surface needed before
designing a full-parity Web catalog schema. It covers every `AssetMetaData` DNA member,
the `AssetTag` storage and collection operations, actual `AssetMetaData` RNA properties,
Blend read/write participation, `AssetCatalog` semantic/runtime fields, catalog definition
file version/record order, path hierarchy rules, and the current Web v1 interface snapshot.
This task deliberately does not create the desktop catalog fixture, Web schema v2,
migrations, IndexedDB behavior, or a new verified parity slice. In particular, a source
inventory is not evidence that Web preserves or writes the inventoried fields.
## Findings
- `AssetMetaData` has 14 DNA members: 10 persisted semantic/UI members, custom
`IDProperty` metadata, one runtime type pointer, one derived tag count, and ABI padding.
The real Blender runtime exposes 10 RNA properties.
- `author`, `description`, `copyright`, and `license` are optional dynamic strings. Empty
values are valid Blender state; the existing Web v1 manifest currently requires non-empty
author/license and has no description/copyright fields.
- `catalog_id` is the authoritative RFC4122 identity. `catalog_simple_name` is a read-only
recovery copy and cannot replace the UUID-to-path mapping.
- Catalog hierarchy is implicit in a cleaned UTF-8 slash path. CDF v1 records
`UUID:path:simple_name`; no parent UUID is stored. Duplicate paths are legal and resolve
deterministically by first-loaded state and UUID.
- Existing Web v1 directly represents catalog ID, tags, author, and license, but misses seven
inventoried metadata semantics and models hierarchy as an explicit `parentId` graph.
## Evidence
- `npm --prefix web run test:asset-catalog-inventory` passed on the first complete run:
`dna=14 rna=10 tag=3 catalog=3 gaps=7 next=M12-01B`.
- The checker binds eight source SHA-256 values, extracts the RNA property declarations,
launches `Blender 5.2.0 LTS --factory-startup --background`, compares the actual runtime
property identifiers/types/read-only flags, verifies Blend IO markers and CDF v1 format,
and uses the TypeScript AST to freeze the current Web interfaces.
- The inventory manifest SHA-256 is
`194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936`;
the checker SHA-256 is
`38839f47dbe82ad7931e5e444e124e87b64a604d9358be6d71cce7769439e57b`.
## Next Task
`M12-01B`: generate a desktop Blender 5.2 catalog v1 fixture and canonical JSON from the
inventoried fields. Do not begin Web schema v2 until that desktop artifact is verified.
## Rollback
Remove the inventory manifest, checker, package command, and this status entry; restore
M12-01A to pending and move `nextTask` back from M12-01B. No parity ledger rollback is
needed because this enabling task did not change a family slice.

55
docs/status/M12-01B.md Normal file
View File

@@ -0,0 +1,55 @@
# M12-01B Status
status: done
task: generate a Blender 5.2 catalog v1 fixture and canonical desktop report
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The locked Blender 5.2 runtime generates a catalog definition v1 file and a `.blend`
containing Object, Material, and World assets. A separate Blender process reopens the
fixture and exports one canonical JSON report from the inventoried AssetMetaData fields.
The report covers catalog hierarchy, assigned and nil catalog UUIDs, optional empty text,
ordered tags, active tag, preferred import method, custom ID properties, and Blender's
automatically generated Object `dimensions` metadata.
This is an enabling desktop baseline. It does not define Web schema v2, migration behavior,
stable Web asset IDs, or catalog mutation parity, and it does not change a ledger slice.
The read-only `catalog_simple_name` remains empty because Blender's RNA catalog UUID setter
clears it outside an Asset Browser context; the fixture records that behavior rather than
inventing a recovery name.
## Evidence
- `npm --prefix web run test:asset-catalog-v1-fixture` passed from a fresh temporary
directory: `catalogs=3 assets=3 metadata=complete next=M12-01C`.
- The checker verified the locked Blender version and every artifact hash, regenerated the
CDF and `.blend`, opened the generated file in a second Blender process, and compared its
canonical report exactly with the checked-in report. It then reopened the checked-in
fixture in another Blender process and repeated the exact comparison.
- Catalog definition v1 contains three deterministic UUID/path/simple-name records, including
one nested path. The canonical report contains three assets across MATERIAL, OBJECT, and
WORLD, preserves tag order and custom metadata, and records an uncataloged nil UUID.
## Artifact Hashes
- generator: `4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307`
- canonical exporter: `3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41`
- checker: `c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b`
- desktop `.blend`: `8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f`
- catalog definition: `a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98`
- canonical report: `6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e`
- manifest: `1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd`
## Next Task
`M12-01C`: define Web catalog schema v2, stable catalog/asset identity, and bounded text and
collection limits from the verified desktop baseline.
## Rollback
Remove the M12-01B generator, exporter, checker, fixture, CDF, canonical report, manifest,
package command, and this status entry. Restore M12-01B to pending and `nextTask` to
M12-01B. No parity ledger rollback is required.

50
docs/status/M12-01C.md Normal file
View File

@@ -0,0 +1,50 @@
# M12-01C Status
status: done
task: define Web catalog schema v2, stable identities, and budgets
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Schema v2 represents Blender catalogs with canonical RFC4122 UUIDs and cleaned slash paths;
hierarchy is derived from `parentPath`, matching CDF semantics instead of retaining the Web v1
`parentId` graph. Asset identity binds Blender's `AssetWeakReference` fields
(`asset_library_identifier`, `relative_asset_identifier`) into a SHA-256 Web ID. A Blender
rename changes the relative identifier and therefore the stable ID, matching Blender's weak
reference behavior rather than promising an identity Blender does not provide.
All text budgets use UTF-8 bytes. Catalog/tag recovery strings keep Blender's 63-byte payload
limit, dynamic metadata permits empty strings, and catalog, asset, tag, custom-property,
custom-array, per-string, and aggregate-text counts are bounded. The positive schema fixture
is generated from the verified M12-01B desktop canonical report through production protocol
code. This task does not implement v1 migration, legacy-reader policy, IndexedDB migration, or
catalog mutation.
## Evidence
- `npm --prefix web run test:asset-catalog-v2` passed 4/4.
- The suite binds the desktop canonical report and Blender 5.2 weak-reference source, regenerates
schema v2 from the desktop report, parses the generated golden, and verifies deterministic
local/external identity hashes and exposed UTF-8/collection budgets.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
## Artifact Hashes
- protocol: `0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7`
- generator: `b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881`
- unit suite: `73038022124ddc7e683602b47a4804297b3a64ecd4819160c7ec086d2f6190ae`
- schema golden: `2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929`
- manifest: `612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9`
- Blender weak-reference source: `ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635`
## Next Task
`M12-01D`: define and verify schema v1 to v2 forward migration.
## Rollback
Remove the schema v2 protocol, generator, unit suite, M12-01C golden/manifest, package command,
and this status entry. Restore M12-01C to pending and `nextTask` to M12-01C. No parity ledger
rollback is required.

48
docs/status/M12-01D.md Normal file
View File

@@ -0,0 +1,48 @@
# M12-01D Status
status: done
task: define and verify schema v1 to v2 forward migration
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The forward migration first validates the current production schema v1 manifest and rejects
undeclared fields before conversion. Canonical UUIDs are preserved; legacy catalog IDs map
deterministically through RFC 4122 UUIDv5 with a fixed namespace. The v1 parent graph becomes
Blender cleaned slash paths, and assets receive SHA-256 IDs from Blender weak-reference identity.
Logical revision is preserved. Author/license/tag/source bindings, optional source paths,
preview receipts, and the complete library dependency graph survive migration. Metadata absent
from v1 receives explicit defaults recorded in the migration report. The report binds canonical
source/target hashes and every legacy-to-v2 catalog/asset identity mapping.
## Evidence
- `npm --prefix web run test:asset-catalog-migration` passed 3/3.
- A schema v1 fixture with three catalogs, two asset kinds, one preview, two source bindings,
and two dependent libraries migrated to the checked-in schema v2 golden and exact report.
- Repeating the migration on a cloned source produced byte-equivalent semantic objects and the
same source/target SHA-256 values. A canonical source UUID remained unchanged.
- M12-01C regression, typecheck, lint, and `git diff --check` passed.
## Artifact Hashes
- migration protocol: `6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226`
- generator: `954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60`
- unit suite: `6b927e7a8238e4e0a77cc0fdf783b099d8add22c5f875df2299012eb32730186`
- v1 fixture: `1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5`
- v2 fixture: `38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb`
- migration report: `57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a`
- manifest: `fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d`
## Next Task
`M12-01E`: define the read-only/block policy when a schema v1 reader encounters schema v2.
## Rollback
Remove the migration protocol, generator, unit suite, M12-01D fixtures/report/manifest, package
command, and this status entry. Restore M12-01D to pending and `nextTask` to M12-01D. No parity
ledger rollback is required.

45
docs/status/M12-01E.md Normal file
View File

@@ -0,0 +1,45 @@
# M12-01E Status
status: done
task: define schema v2 behavior for a schema v1 reader
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
A native schema v1 document remains `READY`. A valid schema v2 document exposed to a v1
reader yields a bounded `READ_ONLY` snapshot for metadata inspection, while catalog writes,
asset writes, and save all return `ASSET_SCHEMA_DOWNGRADE_BLOCKED`. Unknown future schema
versions return non-recoverable `PROTOCOL_MISMATCH` with no snapshot.
Every result binds a canonical source SHA-256. Inspection does not mutate or downgrade the
source document, and no write path can serialize the lossy read-only projection. This task
defines compatibility behavior only; it does not implement IndexedDB migration or catalog
mutation parity.
## Evidence
- `npm --prefix web run test:asset-catalog-legacy-reader` passed 4/4.
- Generated evidence covers v2 read-only metadata, all three write/save blocks, native v1
readiness, future-schema rejection, equal source hashes, and source object immutability.
- M12-01C/D regressions, typecheck, lint, and `git diff --check` passed.
## Artifact Hashes
- compatibility protocol: `5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349`
- error contract: `309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34`
- generator: `208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e`
- unit suite: `f6477222412044e6784b64c1be83dc9bd6050fe6a955ba5430a4ea963b20dbb4`
- compatibility report: `3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54`
- manifest: `cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b`
## Next Task
`M12-01F`: add duplicate ID, cyclic parent catalog, overlong text, and unknown-field negatives.
## Rollback
Remove the compatibility protocol, generator, unit suite, M12-01E report/manifest, error code,
package command, and this status entry. Restore M12-01E to pending and `nextTask` to M12-01E.
No parity ledger rollback is required.

40
docs/status/M12-01F.md Normal file
View File

@@ -0,0 +1,40 @@
# M12-01F Status
status: done
task: reject duplicate IDs, catalog cycles, oversized text, and unknown fields
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The negative matrix executes production schema v2 and migration parsers against duplicate
catalog/asset IDs, a legacy parent cycle, an inconsistent v2 `parentPath`, 64-byte UTF-8
simple-name/tag values, and unknown top-level/nested fields in both v1 and v2. The byte cases
use multi-byte text to prove limits are not JavaScript character counts.
Every failure returns a stable asset error code. The valid v2 fixture is parsed after each
negative case to prove the failed input did not poison parser state. This task adds negative
evidence only and does not change catalog parity status.
## Evidence
- `npm --prefix web run test:asset-catalog-negatives` passed 2/2 with 9/9 negative cases.
- Duplicate/cycle/parent/unknown cases returned `ASSET_MANIFEST_INVALID`; both 64-byte UTF-8
payloads returned `ASSET_BUDGET_EXCEEDED`.
- Typecheck, lint, and `git diff --check` passed.
## Artifact Hashes
- unit suite: `3a11605ac8666c8c4e1e2e85ccfd423115915820d0de6f92bdf72dff1f01ce41`
- negative cases: `74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874`
- manifest: `127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d`
## Next Task
`M12-01G`: prove an IndexedDB migration failure preserves the old transaction and index.
## Rollback
Remove the M12-01F negative suite, golden/manifest, package command, and this status entry.
Restore M12-01F to pending and `nextTask` to M12-01F. No parity ledger rollback is required.

47
docs/status/M12-01G.md Normal file
View File

@@ -0,0 +1,47 @@
# M12-01G Status
status: done
task: preserve the old catalog index when an IndexedDB migration fails
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The production IndexedDB migration prepares schema v2 with the existing schema v1 parser and
migrator before opening a write transaction. It then rechecks the source row and atomically
writes the v2 index and migration receipt while deleting the v1 row. A concurrent source change
returns `REVISION_CONFLICT`; a transaction failure returns `STORAGE_TRANSACTION`.
The browser suite injects failures after the v2 target write and after the v1 source delete.
After each abort it closes and reopens the database, proving the database version, complete v1
index, unrelated legacy migration row, absent v2 row, and absent receipt are byte-for-byte
equivalent to the baseline. A subsequent retry commits the v2 index and receipt together.
## Evidence
- `WEB_TEST_PORT=5593 npm --prefix web run test:asset-catalog-indexeddb-migration` passed the
M12-01D production migration unit suite 3/3, the M12-01G artifact binding unit 1/1, and
Chromium 1/1.
- Both `AFTER_TARGET_PUT` and `AFTER_SOURCE_DELETE` returned `STORAGE_TRANSACTION`; reopening
IndexedDB after either fault produced the exact pre-migration snapshot.
- The successful retry preserved revision 7 and the M12-01D source/target hashes, removed only
the v1 row, and retained the unrelated migration row.
- Typecheck and `git diff --check` passed.
## Artifact Hashes
- production IndexedDB migration: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`
- browser suite: `82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9`
- artifact unit: `a4366a16f28b91b3ff213e0232308eaecd392731f27972cfa49a11568eb0feca`
- manifest: `2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7`
## Next Task
`M12-01H`: prove catalog ordering and asset identity survive page and Worker restarts.
## Rollback
Remove the IndexedDB migration module, M12-01G unit/browser suites, manifest, package command,
and this status entry. Restore M12-01G to pending and `nextTask` to M12-01G. No parity ledger
rollback is required.

47
docs/status/M12-01H.md Normal file
View File

@@ -0,0 +1,47 @@
# M12-01H Status
status: done
task: preserve catalog order and asset identity across page and Worker restarts
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The production catalog index loader validates the stored schema v2 manifest, recomputes its
canonical SHA-256, and compares it with the committed migration receipt before publishing a
snapshot. The snapshot exposes persisted catalog order, asset order, stable asset IDs, Blender
weak-reference components, revision, and manifest identity.
A page migrates the M12-01D v1 fixture, reloads, and reads the same database again. Two separate
module Workers then open the database in sequence, with generation 1 terminated before
generation 2 starts. All four contexts must return the same complete snapshot.
## Evidence
- `WEB_TEST_PORT=5594 npm --prefix web run test:asset-catalog-restart` passed Node 5/5 and
Chromium 2/2, including the full M12-01G abort/reopen regression.
- Initial page, reloaded page, Worker generation 1, and Worker generation 2 returned exactly the
same 3 catalog IDs in order, 2 asset IDs in order, weak-reference components, revision 7, and
manifest SHA-256.
- A stored receipt whose target hash or revision drifts from the parsed v2 index is rejected as
`ASSET_MANIFEST_INVALID` before a snapshot is published.
- Typecheck, lint, and `git diff --check` passed.
## Artifact Hashes
- production IndexedDB reader: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`
- restart Worker: `e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e`
- browser suite: `7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b`
- artifact unit: `eb22b5cc852fa47d9bdd30afec2977facfca0dfa554e7ce738dc1bf8c4bf2122`
- manifest: `59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae`
## Next Task
`M12-01I`: bind the schema fixture, migration report, and hashes into M12-01 evidence.
## Rollback
Remove the restart snapshot reader, Worker/browser/unit suites, M12-01H manifest, package
command, and this status entry. Restore M12-01H to pending and `nextTask` to M12-01H. No parity
ledger rollback is required.

44
docs/status/M12-01I.md Normal file
View File

@@ -0,0 +1,44 @@
# M12-01I Status
status: done
task: bind the schema fixture, migration report, and hashes into M12-01 evidence
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The M12-01 aggregate evidence binds all eight predecessor manifests in exact queue order and
recursively verifies their declared source and artifact hashes. It separately binds the locked
Blender 5.2 desktop canonical fixture, Web schema v2 golden and protocol, schema v1/v2 migration
fixtures, production migration report, IndexedDB transaction implementation, and restart Worker.
The checker recomputes canonical source and target manifest SHA-256 values, matches them to the
migration report, verifies revision and preserved counts, and compares the persisted restart
catalog/asset order with the target fixture. This closes the schema/migration evidence section;
it does not claim catalog mutation or preview parity.
## Evidence
- `WEB_TEST_PORT=5595 npm --prefix web run test:asset-catalog-m12-evidence` passed the Blender
5.2 inventory and regenerated fixture checks, schema v2 4/4, migration 3/3, legacy-reader 4/4,
negatives 2/2, transaction/restart Node 5/5, Chromium 2/2, and final aggregate checker.
- The final checker reported `subtasks=8 catalogs=3 assets=2 next=M12-02A`.
- Source revision 7 and target revision 7 canonical hashes match the production migration report;
all A-H task links and recursive artifact hashes resolve without gaps.
- Typecheck, lint, status consistency, and `git diff --check` passed.
## Artifact Hashes
- M12-01 evidence: `2b99f64a207c2ffb966963be8a2150feef67d5bf78946939d3a49a6335b55bc3`
- aggregate checker: `7d0d9f5390648d4af79a8da6226627edb1bf99945bae1a7b1acfb2c5e2419a8b`
- package command: `a810fec6a9a24cad94cb01475f9529ac2f85da827d391a70ab7ddb57421003f8`
## Next Task
`M12-02A`: inventory Blender preview image dimensions, color space, and no-preview state.
## Rollback
Remove the M12-01 aggregate evidence, checker, package command, and this status entry. Restore
M12-01I to pending and `nextTask` to M12-01I. No parity ledger rollback is required.

43
docs/status/M12-02A.md Normal file
View File

@@ -0,0 +1,43 @@
# M12-02A Status
status: done
task: inventory Blender preview dimensions, color semantics, and no-preview state
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The machine inventory binds Blender 5.2 PreviewImage DNA, slot enum, BKE/RNA implementation,
Blend reader, asset preview generator, render-size constants, current Web v1/v2 preview protocols,
and two runtime fixtures. It records both ICON and PREVIEW slots, persisted dimensions/flags/pixel
arrays, runtime-only state, and absent-pointer behavior.
Preview pixels are packed 32-bit RGBA with four byte components and Blender's deferred load path
premultiplies alpha. PreviewImage has no persisted or RNA color-space field, and float access only
divides byte components by 255 without a color transform. The inventory therefore records color
space as unknown instead of assuming sRGB.
## Evidence
- `npm --prefix web run test:asset-preview-inventory` launched locked Blender 5.2 twice and
reported `slots=2 rna=9 absent=3 loaded=8x8 next=M12-02B`.
- The checked-in asset fixture reopened with Object, Material, and World `preview == null`.
- A real 8x8 PNG loaded through Blender's preview API produced an 8x8 PREVIEW slot, 32x32 ICON
slot, 64/256 image packed/float counts, and 1024 icon packed pixels.
- Typecheck, lint, status consistency, and `git diff --check` passed.
## Artifact Hashes
- inventory: `a4427426d94afdf1a2ccf5379d76d1797f8604a742a3f0d15bdca629272864bd`
- checker: `c9cc83d21bac5f34c6d68cb1225dffcd92a5ec76c158ddce909b620cbbcb245c`
- package command: `04f832a3c5773c8cc5294ecdcf175478eed9710496e48c63698d79043384e328`
## Next Task
`M12-02B`: define preview source/content SHA-256, MIME, dimensions, and generator identity.
## Rollback
Remove the M12-02A inventory, checker, package command, and this status entry. Restore M12-02A
to pending and `nextTask` to M12-02A. No parity ledger rollback is required.

48
docs/status/M12-02B.md Normal file
View File

@@ -0,0 +1,48 @@
# M12-02B Status
status: done
task: define preview source, content, dimensions, MIME, and generator identity
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The schema binds source bytes and encoded content as separate SHA-256/length/MIME identities.
Encoded content additionally binds width, height, RGBA8, explicit sRGB, and alpha mode. Generator
identity binds name, Blender version, executable hash, generator script hash, and canonical
settings hash. A domain-separated canonical hash covers the complete record.
The locked Blender 5.2 generator re-encodes a real 8x8 PNG with explicit PNG RGBA8, compression,
display, view-transform, exposure, and gamma settings. The source and content encodings have
different hashes and lengths while a second Blender process confirms decoded pixels are exact.
## Evidence
- `npm --prefix web run test:asset-preview-identity` passed 3/3 protocol tests and the locked
Blender generator checker.
- Two independent generation runs matched the checked-in 513-byte content SHA-256 exactly; the
261-byte source has a different SHA-256, and decoded pixel maximum error is zero.
- Source bytes, content bytes, generator settings, dimensions, identity hash, and unknown fields
each have fail-closed negative coverage.
- Typecheck and `git diff --check` passed.
## Artifact Hashes
- protocol: `2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438`
- generator: `753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce`
- checker: `4d8ece1a5440d202f4e79a48f1f26bc75b3fb11a02d821b8ebc5aee14b11e41a`
- unit suite: `5b7b3ccaf410d627365579fa82bf2b48fed9ef17144aad963cdbd4bb7fbd4b5a`
- identity golden: `1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b`
- content PNG: `ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7`
- manifest: `096f125461b963ee2c414f98f6bb00f02034a380a0cc9c43e0d84754324adb17`
## Next Task
`M12-02C`: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode.
## Rollback
Remove the preview identity protocol, generator/checker, unit suite, M12-02B goldens, package
command, error code, and this status entry. Restore M12-02B to pending and `nextTask` to M12-02B.
No parity ledger rollback is required.

46
docs/status/M12-02C.md Normal file
View File

@@ -0,0 +1,46 @@
# M12-02C Status
status: done
task: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The schema 1 pre-decode planner accepts only the M12-02B content identity and an exact
`ArrayBuffer` payload. It checks the 16 MiB encoded-byte limit before hashing, then binds the
content SHA-256, declared MIME to the PNG/WebP container header, header dimensions to the identity,
and dimensions to pixel and RGBA8 decoded-byte budgets. The final gate limits decoded-to-encoded
compression ratio to 100 before any browser image decoder or decoded allocation is invoked.
PNG dimensions come only from a valid signature and first IHDR header. WebP dimensions come only
from a size-bound RIFF/WEBP VP8X, VP8L, or VP8 header. Unsupported headers, content drift, and
dimension drift fail closed; a rejected input does not affect a following valid preview plan.
## Evidence
- `npm --prefix web run test:asset-preview-decode-budget` passed 5/5 M12-02B/C protocol tests.
- The positive 513-byte Blender-generated PNG plans 64 pixels and 256 decoded RGBA8 bytes without
calling a decoder.
- Eight negative cases cover payload hash/length drift, MIME mismatch, dimension mismatch,
over-width/height and decoded-byte budget, compression ratio, encoded-byte budget, and corrupt
container signature. The checked-in small preview passes immediately after each rejection.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
## Artifact Hashes
- decode protocol: `3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63`
- unit suite: `1a65df86d568ae8defd7cd9242f426ac900d92637eb3bed2cbb4a0fae2e86c20`
- package commands: `8563916637fc3cb29b53432ae9b80d1dcf6269f2f5c092b595e72c6b0ada55ab`
- manifest: `934b3ca29d905ad2f5c46f35acff4cf4b9ea291c423fb3aad4afe7e9f10e6244`
## Next Task
`M12-02D`: commit a catalog preview reference only after its content-addressed payload is durably
written to OPFS.
## Rollback
Remove the preview decode planner, unit suite, M12-02C manifest, package command, and this status
entry. Restore M12-02C to pending and `nextTask` to M12-02C. No parity ledger rollback is required.

49
docs/status/M12-02D.md Normal file
View File

@@ -0,0 +1,49 @@
# M12-02D Status
status: done
task: commit a catalog preview reference only after verified OPFS persistence
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The production coordinator first runs the M12-02C pre-decode gate, writes the content-addressed
preview to OPFS, reads it back, and repeats identity verification. Only then may one IndexedDB
transaction advance the catalog revision and write both the v2 preview reference and an M12-02D
head receipt. The receipt binds project, asset, base/current revision, base/current manifest hash,
preview identity, payload identity, and OPFS path.
The transaction rereads and compares the complete source catalog row before publication. A stale
revision or changed row returns `REVISION_CONFLICT`; OPFS and transaction faults return
`STORAGE_TRANSACTION`. An OPFS payload left by a failure is unreferenced, never presented as a
committed catalog preview.
## Evidence
- `WEB_TEST_PORT=5596 npm --prefix web run test:asset-preview-opfs-commit` passed the artifact unit
test 1/1 and the real Chromium OPFS/IndexedDB test 1/1.
- `BEFORE_OPFS_WRITE`, `AFTER_OPFS_WRITE`, and `AFTER_CATALOG_PUT` faults all preserved the complete
revision 7 catalog and left no commit receipt. Only the latter two left an unreferenced payload.
- Success persisted and reread the 513-byte payload before advancing the catalog to revision 8;
closing and reopening IndexedDB reproduced the same preview and receipt and rehashed OPFS bytes.
- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed.
## Artifact Hashes
- commit coordinator: `36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2`
- Chromium suite: `043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e`
- unit suite: `f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74`
- manifest: `34fe6c6ab097e58f7b648dd84cb919ecb407a5742e387c513bb3ed53db50eed6`
- package commands: `7363a8a03cb5f6b1d6bf40d8eabc90b9d48e91aeb4787b175deb4f242c676243`
## Next Task
`M12-02E`: deduplicate identical preview content without allowing different asset metadata to
overwrite the shared payload.
## Rollback
Remove the OPFS/catalog commit coordinator, Chromium and unit suites, M12-02D manifest, package
command, and this status entry. Restore M12-02D to pending and `nextTask` to M12-02D. No parity
ledger rollback is required.

45
docs/status/M12-02E.md Normal file
View File

@@ -0,0 +1,45 @@
# M12-02E Status
status: done
task: deduplicate identical preview content without merging asset metadata
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Two catalog assets with distinct Blender weak-reference identities and distinct catalog metadata
may bind the same verified preview content. The M12-02D coordinator keys OPFS storage only by the
content SHA-256, while catalog entries and preview identity hashes remain asset-specific. A second
commit of identical bytes reuses the existing payload after a full readback/hash check.
No catalog metadata is derived from or stored inside the shared payload record. Each transaction
updates only its targeted asset's preview field and advances the guarded catalog revision.
## Evidence
- `WEB_TEST_PORT=5597 npm --prefix web run test:asset-preview-dedup` passed the artifact unit test
1/1 and real Chromium OPFS/IndexedDB test 1/1.
- Two distinct asset/identity hashes committed at revisions 8 and 9; the second commit reported
`deduplicated=true` and the OPFS content directory contained exactly one final file.
- Both catalog entries referenced the same 513-byte/hash payload while their complete non-preview
metadata remained byte-for-byte equal to their different baseline records.
- Database reopen reproduced revision 9, the second asset head receipt, and the shared preview;
stored payload bytes rehashed to the declared content digest.
## Artifact Hashes
- Chromium suite: `4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40`
- unit suite: `e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835`
- manifest: `7d25f1d8335481795257c643c8589d1607a865634b27a7e5d145a57ce2a92a62`
- package commands: `5a83676e821bb768f6d3e282ea4728413cf84c5f3d57672fc664fac3883873e5`
## Next Task
`M12-02F`: quarantine corrupt preview content while keeping asset metadata readable.
## Rollback
Remove the deduplication Chromium/unit suites, M12-02E manifest, package command, and this status
entry. Restore M12-02E to pending and `nextTask` to M12-02E. The shared coordinator and parity
ledger do not require rollback.

49
docs/status/M12-02F.md Normal file
View File

@@ -0,0 +1,49 @@
# M12-02F Status
status: done
task: quarantine corrupt preview content while keeping asset metadata readable
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The preview inspector validates payload byte length and SHA-256 independently from the catalog.
Healthy bytes return `READY`. Missing or mismatched bytes return a structured
`ASSET_SOURCE_HASH_MISMATCH` quarantine receipt; present corrupt bytes are copied, verified, and
then removed from the content-addressed namespace before the receipt is published.
The catalog row and its preview provenance are not rewritten on corruption. Consumers receive the
complete parsed asset metadata together with `preview=null/data=null`, so a broken optional image
cannot make the asset name, type, catalog, license, tags, or source metadata unreadable. A matching
quarantine receipt is stable across database reopen.
## Evidence
- `WEB_TEST_PORT=5598 npm --prefix web run test:asset-preview-quarantine` passed the artifact unit
test 1/1 and real Chromium OPFS/IndexedDB test 1/1.
- The checked-in 513-byte PNG first returned `READY`; same-length byte tampering then returned
`QUARANTINED` with a different actual SHA-256 and stable error code.
- The corrupt file was absent from the SHA-256 namespace and present as a verified 513-byte file in
the quarantine directory. Database reopen returned the exact same receipt without reading it.
- Catalog revision 8, the target asset metadata/reference, and the unrelated asset all remained
exactly unchanged.
## Artifact Hashes
- quarantine protocol: `a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04`
- Chromium suite: `d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27`
- unit suite: `79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a`
- manifest: `d1a9feeb98c5332b1a14a14c5b813d75a29f172601590e1b5cde254c5059a7c5`
- package commands: `77b5cd3d3af68ac390d8fbdb9b2f64764fb4521a1d2bf22cd8a89e40d69424f6`
## Next Task
`M12-02G`: reclaim a preview payload only after its final project-scoped reference is removed,
without touching another project's copy.
## Rollback
Remove the quarantine protocol, Chromium/unit suites, M12-02F manifest, package command, and this
status entry. Restore M12-02F to pending and `nextTask` to M12-02F. Catalog and parity ledger data
do not require rollback.

47
docs/status/M12-02G.md Normal file
View File

@@ -0,0 +1,47 @@
# M12-02G Status
status: done
task: reclaim preview payload only after the final project-scoped reference is removed
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Preview reference removal is a guarded catalog mutation: it validates the v2 row, checks the exact
base revision, clears only the target asset's preview, advances revision, and commits a manifest
hash receipt. Payload reclamation happens only after that reference transaction has completed and
only when the committed manifest has zero remaining references to the content hash.
OPFS payload ownership remains project-scoped (`projects/<projectId>/assets/sha256/...`). Therefore
collection can target only the requested project's file and cannot delete an equal-hash payload
owned and referenced by another project. Non-preview asset metadata is preserved.
## Evidence
- `WEB_TEST_PORT=5599 npm --prefix web run test:asset-preview-reference-gc` passed the artifact unit
test 1/1 and real Chromium OPFS/IndexedDB test 1/1.
- Project A began with two references to one deduplicated payload. Removing the first at revision
9->10 returned `RETAINED`, one remaining reference, and kept the file.
- Removing the final reference at revision 10->11 returned `COLLECTED` and removed project A's
payload. Project B's equal-hash payload remained present and its preview returned READY/513 bytes.
- Reopening project A reproduced revision 11, two null preview references, and unchanged metadata.
## Artifact Hashes
- reference GC: `76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e`
- Chromium suite: `2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a`
- unit suite: `c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e`
- manifest: `3449f2ea9488f66ea5e3f5f2423413e8e290d8ffed3b3d941bdeee3f76addd0a`
- package commands: `7d1db0932fbe7405ffdbd44b770ce7bf9815b9dfb829c1906a06bfff635ae702`
## Next Task
`M12-02H`: compare desktop/browser preview pixels with explicit metrics and pass both main-thread
and Offscreen display paths.
## Rollback
Remove the reference-GC protocol, Chromium/unit suites, M12-02G manifest, package command, and this
status entry. Restore M12-02G to pending and `nextTask` to M12-02G. Catalog and parity ledger data
do not require rollback.

51
docs/status/M12-02H.md Normal file
View File

@@ -0,0 +1,51 @@
# M12-02H Status
status: done
task: compare desktop/browser preview pixels and pass main-thread and Offscreen display paths
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: true
## Scope
The display path validates the content identity, encoded SHA-256, image header, dimensions, decoded
byte budget, and compression ratio before invoking a decoder. It then decodes with disabled browser
color conversion and alpha premultiplication, draws with copy composition and no smoothing, and
publishes a receipt bound to the identity, content, backend, dimensions, RGBA8 hash, and alpha count.
Blender 5.2 deterministically regenerates the 8x8 `SRGB/RGBA8/STRAIGHT` reference. Main-thread
Canvas2D and a dedicated Worker OffscreenCanvas execute the same production display implementation
and match all 256 reference bytes. This closes only the bounded preview display slice; arbitrary
profiles, Asset Browser UI, and library operations remain outside this task.
## Evidence
- `WEB_TEST_PORT=5600 npm --prefix web run test:asset-preview-display` passed the evidence unit 1/1,
Blender 5.2 desktop regeneration/hash checker, and Chromium main-thread/Offscreen test 1/1.
- Desktop, main-thread, and Offscreen output SHA-256 was
`c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00`.
- Both browser paths reported 8x8, 256 RGBA8 bytes, 64 non-transparent pixels, and closed bitmap
ownership. Their MAE, RMS, P95, maximum channel error, bad-pixel ratio, and alpha coverage delta
were zero; foreground IoU was 1.
- A one-channel mutation returned `BLOCKED/RENDER_REFERENCE_MISMATCH` under the same metric contract.
## Artifact Hashes
- display runtime: `e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28`
- Offscreen Worker: `6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f`
- desktop checker: `d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54`
- desktop report: `cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0`
- Chromium suite: `340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520`
- unit suite: `f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873`
- manifest: `30e6e4b6f53e07768d2ae8d853fe08a5c0782f8608320a72e41db77f6d87f2c4`
- package commands: `2510242fbc0c2da787f558365733a4db6faf9f490e2594f69dc978920ed09f21`
## Next Task
`M12-03A`: inventory the data-block and dependency closures for append, link, and library override.
## Rollback
Remove the display runtime, Worker, desktop checker/report, unit/Chromium suites, M12-02H manifest,
package command, completed ledger slice, and this status entry. Restore M12-02H to pending and the
queue to M12-02H.

55
docs/status/M12-03A.md Normal file
View File

@@ -0,0 +1,55 @@
# M12-03A Status
status: done
task: inventory append, link, and library override data-blocks and dependency closures
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
The machine inventory separately records Append, Link, and Library Override entry points,
selectable data-block roots, non-root ID roles, direct/indirect/transitive/embedded dependency
classes, terminal ownership actions, and override reference/hierarchy/property-operation fields.
The exact closure is an actual Blender Main ID pointer graph walked by
`BKE_library_foreach_ID_link`, not a hard-coded Object-to-Mesh table. The inventory therefore
preserves cross-library and override-only dependency distinctions needed by later ownership and
invalidation contracts. It does not perform a library mutation or add a parity slice.
## Findings
- Blender 5.2 exposes 36 selectable library data-block collections. Screen and WorkSpace are
append-only; Library is source/transitive metadata, while embedded Key IDs follow their owner.
- Append can keep linked, reuse local, make local, or copy local after classifying direct,
indirect, and liboverride-only dependencies. Recursive cross-library localization is explicit.
- Link retains the source `Library`, direct `ID_TAG_EXTERN`, indirect `ID_TAG_INDIRECT`, and
transitive parent-library relationships without localization.
- Override retains a linked reference alongside a local override and hierarchy root. Its 17 RNA
fields cover reference/root flags, property paths, operations, and local/reference subitems.
- Current Web metadata represents only library source, dependency IDs, and read-only state; eight
per-ID ownership, override, and invalidation semantics remain absent and mutation stays blocked.
## Evidence
- `npm --prefix web run test:library-operation-inventory` passed with
`roots=36 appendOnly=2 operations=3 overrideFields=17 gaps=8 next=M12-03B`.
- The checker binds nine Blender/Web source files and one real nested-library fixture by SHA-256,
verifies link/append action and dependency-walk tokens, launches Blender 5.2, and compares the
actual append/link operator properties, library collections, nested source, and override RNA.
## Artifact Hashes
- inventory: `0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7`
- checker: `1581490d1923578293202d27a1c379eb2a8c5e13be418547d148cf09c6a783fd`
- package commands: `a7cf339b0cf6fab17ef63e9394b2e540af60327f7bf1f7fe95823d182e0d3e63`
## Next Task
`M12-03B`: define source library ID, owner, read-only state, and invalidation token for all three
operations.
## Rollback
Remove the library operation inventory, checker, package command, and this status entry. Restore
M12-03A to pending and the queue to M12-03A. No parity ledger rollback is required.

47
docs/status/M12-03B.md Normal file
View File

@@ -0,0 +1,47 @@
# M12-03B Status
status: done
task: define source library identity, owner, read-only state, and invalidation tokens
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Schema 1 derives `sourceLibraryId` from a canonical source locator and source `.blend` SHA-256.
Each operation binding then fixes one source data-block, its operation-specific owner and read-only
state, source generation/revision, dependency-closure SHA-256, and a hash-derived invalidation token.
APPEND owns a writable local Main ID. LINK remains owned by the matching source library and is
fully read-only. LIBRARY_OVERRIDE owns a writable local override while retaining a read-only linked
reference and hierarchy root. This is a pure protocol task and does not mutate Blender Main.
## Evidence
- `npm --prefix web run test:library-operation-identity` passed 6/6.
- Stable source identity changes when either locator or source bytes change.
- APPEND/LINK/LIBRARY_OVERRIDE produce distinct invalidation tokens for the same source root and
reject owner substitution, read-only drift, reference substitution, unknown fields, and forgery.
- Source library/hash, generation, revision, and dependency closure drift each return
`REVISION_CONFLICT`; the unchanged state revalidates the original binding exactly.
- The one initial test expectation was corrected to the actual fail-fast exact-schema code, then
the complete suite was rerun successfully.
## Artifact Hashes
- protocol: `5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451`
- golden bindings: `b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f`
- unit suite: `0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547`
- manifest: `f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b`
- package commands: `48cf41f0d0041bf4f497bc2a454cfa9469874eded1775cea50fe3249d4e0b59e`
## Next Task
`M12-03C`: create a desktop single-Object append fixture and record stable Object, Mesh, Material,
and Image mappings.
## Rollback
Remove the library operation identity protocol, golden, unit suite, manifest, package command, and
this status entry. Restore M12-03B to pending and the queue to M12-03B. No parity ledger rollback is
required.

50
docs/status/M12-03C.md Normal file
View File

@@ -0,0 +1,50 @@
# M12-03C Status
status: done
task: create a desktop single-Object append fixture and stable dependency mapping
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Blender 5.2 creates a source library containing one Object whose dependency closure is one Mesh,
one Material, and one packed Image. A clean desktop Main appends only the Object, saves the target,
reopens it, and exports the same canonical graph before and after reopen.
The fixture records stable source-to-local mappings, ownership, read-only state, dependency edges,
geometry/UV/material structure, and packed-image metadata and Float32 pixel SHA-256. Container hashes
remain session-bound Blender artifacts; the canonical semantic report must reproduce exactly.
## Evidence
- `npm --prefix web run test:library-append-desktop` passed.
- The generator appended one selected Object and resolved four stable local IDs with three dependency
edges.
- Object, Mesh, Material, and Image all have null library pointers, no library override, writable
`LOCAL_MAIN` ownership, and identical source/local stable names.
- Save/reopen preserved 4 vertices, 4 edges, 1 polygon, 4 loops, `UVMap`, the material slot, packed
2x2 sRGB RGBA image metadata, and Float32 pixel SHA-256
`6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`.
- A new temporary source/target pair reproduced the canonical report after normalizing only the
session-bound `.blend` container hashes.
## Artifact Hashes
- generator: `b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f`
- checker: `6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d`
- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606`
- target `.blend`: `5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c`
- desktop report: `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a`
- manifest: `cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed`
- package commands: `b68f2bce48fcd42e4cc1399b47ba33c35495248a6c0405c2280b5b73e3ce581a`
## Next Task
`M12-03D`: append the same closure into WASM Main with one transaction and local ownership.
## Rollback
Remove the desktop append generator/checker, source and target fixtures, golden report/manifest,
package command, and this status entry. Restore M12-03C to pending and the queue to M12-03C. No
parity ledger rollback is required.

52
docs/status/M12-03D.md Normal file
View File

@@ -0,0 +1,52 @@
# M12-03D Status
status: done
task: append one dependency closure through WASM Main in one transaction
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
The browser Worker validates a source-hash-bound APPEND binding and sends one Object closure to
the authoritative Blender Main. Blender's native link/append context recursively imports the
Object, Mesh, Material, and packed Image, verifies that every resulting ID is local and writable,
and publishes one SceneIR revision. The existing Main history transaction is used for rollback and
undo/redo; the source is never treated as a linked read-only snapshot.
## Evidence
- `EM_CACHE=/home/mes123456/workinf_Blender_Wasm/.emcache cmake --build build_web_blender6 --target web_engine -- -j2` passed;
the generated module exports `_web_engine_append_library_object`.
- `npm --prefix web run typecheck` passed.
- `WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` passed in Chromium.
- The test opens a clean `.blend`, appends the desktop fixture closure through `WebEngineClient`,
verifies `transactionCount=1`, four `LOCAL_MAIN` mappings, stable Object/Mesh/Material/Image
IDs, the material-to-image dependency, and a single revision increment.
- A stale base revision returns `REVISION_CONFLICT`; a second append colliding with the local IDs
returns `ASSET_MANIFEST_INVALID` without changing the revision. Undo removes the closure, redo
restores it, and a saved buffer reopens with the same local image and Object dependency.
- The Worker closure check accepts both the reader's `material.imageIds` representation and the
Blender node-level `imageId` representation; it does not infer ownership from a preview.
## Artifact hashes
- test: `7130453cbc81f22eb1bcf598ff3745672ac8177dccb11cef667f5b68b43e87c6`
- worker: `e3bb430e401144bf17cdcbd0de13cde0a4f904f1f9ff9d5b10a73d90f0d4c648`
- protocol: `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7`
- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606`
- clean target `.blend`: `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63`
- WASM JS: `9c831d0351ac9d0714cce3b1da748cba416ced3086e3c59595cf24ac4da459ed`
- WASM binary: `7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea`
- Main append API: `5a71260c542fa559f73c6b82e23e72d39bd2844f5ebe12c6edc809a49d79791b`
- Main append implementation: `d0c484ab62e6ec051c8c2f67c49d750a456578aaccc2aa4f461026307af0db95`
## Next task
`M12-03E`: append undo/redo/save/reopen and the desktop canonical report must agree.
## Rollback
Remove the M12-03D Worker/e2e test, package command, WASM append export and Main append entrypoint,
restore the material closure check, and move the machine queue back to `M12-03D`. No V1 release
ledger rollback is required.

54
docs/status/M12-03E.md Normal file
View File

@@ -0,0 +1,54 @@
# M12-03E Status
status: done
task: append undo/redo/save/reopen and desktop canonical comparison
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
The M12-03D WASM Main append path is compared with the M12-03C Blender 5.2 desktop canonical
report. One append transaction creates the Object/Mesh/Material/packed Image local closure and
advances exactly one SceneIR revision. Undo removes all four IDs, redo restores the canonical
graph, and a saved buffer reopens with the same graph and packed image pixels.
The image comparison normalizes Blender's bottom-up `Image.pixels` row order versus Canvas
top-down `ImageData`. Current SceneIR omits `Image.colorSpace`, so a missing field uses the desktop
canonical sRGB semantic default; an emitted colorspace must match it. All other graph, ID,
ownership, geometry, UV, material-slot, image metadata, and pixel hash fields remain exact. This
task does not claim Link, Library Override, or full N-023 IO parity.
## Evidence
- `node ../tools/web/check-library-main-append.mjs` passed the manifest, artifact hashes, canonical
desktop report, and required test markers.
- `WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` passed the checker and
Chromium 1/1. The browser test verified one transaction, four writable `LOCAL_MAIN` mappings,
one revision increment, complete Object -> Mesh -> Material -> Image closure, and exact
canonical graphs after append, redo, and save/reopen.
- A stale base revision returned `REVISION_CONFLICT`; a local ID collision returned
`ASSET_MANIFEST_INVALID` without changing the appended revision. Undo reported no Object, Mesh,
Material, or Image; redo restored the Object and canonical closure.
- The canonical packed image remained a 2x2 sRGB RGBA PNG with Float32 pixel SHA-256
`6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`.
## Artifact Hashes
- checker: `7fba72c5a38c0877f03682b51cbaaaf6d9a5f315f2beddfdef432bc54b2f1cd2`
- test: `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0`
- package commands: `36a29c5be9bd6163b06cf3511edf77d932388fa0b75c08bdcec397e19c0ef45f`
- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606`
- clean target `.blend`: `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63`
- desktop report: `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a`
- manifest: `beaa88ea0385225e712f9816072420bd8744c15da3229ddc352462abec407739`
## Next Task
`M12-03F`: desktop link fixture preserving source library and read-only ownership.
## Rollback
Remove the M12-03E checker, manifest, extended Chromium test, package command, and this status
entry. Restore M12-03D to pending and move the machine queue back to `M12-03D`. No parity ledger
rollback is required.

49
docs/status/M12-03F.md Normal file
View File

@@ -0,0 +1,49 @@
# M12-03F Status
status: done
task: create a desktop linked Object fixture preserving source-library ownership
updated: 2026-08-17 America/New_York
enablingTask: true
parityStateChange: false
## Scope
Blender 5.2 creates a source library containing one Object with a Mesh, Material, and packed Image
dependency closure. A clean target links only the selected Object with `bpy.data.libraries.load(link=True)`.
The Object, Mesh, Material, and Image retain the source library pointer, have no library override,
and map to read-only `SOURCE_LIBRARY` ownership. The target is saved, reopened in a new Main load,
and compared to the pre-save linked graph.
This task freezes the desktop LINK contract only. It does not implement a WASM link writer, linked
mutation, reload/relocate, missing-library placeholders, or full N-023 parity.
## Evidence
- `npm --prefix web run test:library-link-desktop` passed the manifest/artifact hash gate, Blender
5.2 generator rerun, and canonical report comparison.
- The fixture links one root and four dependency IDs. All three dependency edges, 4 vertices, 4
edges, 1 polygon, 4 loops, `UVMap`, material slot, packed 2x2 sRGB RGBA image, and Float32 pixel
SHA-256 `6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a` survive save/reopen.
- Every linked ID reports `library=m12_link_source.blend` and `isLibraryOverride=false`; all four
stable mappings are `SOURCE_LIBRARY/readOnly=true`. A temporary source/target regeneration matches
the canonical report after normalizing only session-bound blend container hashes.
## Artifact Hashes
- generator: `15a2e34c1c5b2a8ee63b10084dbb894e0f9677b9e1cf4adb7e2ddce6b4c965b1`
- checker: `6a4c024bea227d7bc14f793467b91766b006459fe4d7717cc75dfee12f49f894`
- source `.blend`: `fae97569e9d2e2066fc92749672f86cc42717cd9b97b012faeb9eb92015d7fd1`
- target `.blend`: `acdaf0f297deb08c84e1a8beba30972e3414ef62e60e3b103fe0661199c438a1`
- desktop report: `b278d4c254eff63d41c8cb3ea1d5e0984192137d45b1695ba0672e16f95b58ea`
- manifest: `8220a8f5d560d45a75a62cbed645b3febc1390fe37b07c3c48871fefc1a9b159`
- package commands: `336d8df1914aaaafaeccc181d4e73ed7058165f10e167efe539939c3ac1e0536`
## Next Task
`M12-03G`: linked data writers must fail closed with `LINKED_DATA_MUTATION_BLOCKED`.
## Rollback
Remove the M12-03F link generator/checker, source and target fixtures, desktop report/manifest,
package command, and this status entry. Restore M12-03E to pending and move the machine queue back
to `M12-03E`. No parity ledger rollback is required.

45
docs/status/M12-03G.md Normal file
View File

@@ -0,0 +1,45 @@
# M12-03G Status
status: done
task: block every linked data writer before Main mutation
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
The schema-1 linked mutation gate covers object transforms, mesh geometry, mesh material slots,
material properties, material image nodes, and packed image data. Every request must identify a
`SOURCE_LIBRARY` owner with `linkedLibrary=true` and `readOnly=true`. A current linked request is
blocked with `LINKED_DATA_MUTATION_BLOCKED` before any Main writer can run; stale revisions remain
`REVISION_CONFLICT`, and malformed or ownership-substituted requests fail closed.
This task does not claim a linked writer, reload, relocation, missing-library recovery, or full
N-023 Link parity. The desktop linked ownership contract is the M12-03F input to this gate.
## Evidence
- `npm --prefix web run test:library-linked-mutation` passed 3/3 unit tests.
- All six linked writer operations return `BLOCKED` with exactly
`LINKED_DATA_MUTATION_BLOCKED`, `recoverable=false`, and no Main mutation path. The test also
verifies the schema round-trip for each operation.
- A stale revision returns `REVISION_CONFLICT`; an unknown field, unsupported operation, or owner /
read-only substitution returns `TASK_VALIDATION_FAILED` or `LINKED_DATA_MUTATION_BLOCKED` before
any writer invocation.
## Artifact Hashes
- protocol: `f629e0e7e04dc1f5437b6e2ca62fbe35484fc238830fa47e8358bcab46b7e104`
- unit: `f19d47cefe89daf6123062e045ec717e6ffe60977e8a4b20c996dd62e49da211`
- manifest: `890891fda5c91b08d157927170fb33190ae0d8a49f8962b88c8554307c0c06a8`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03H`: library reload replaces only the matching linked generation snapshot.
## Rollback
Remove the linked mutation protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03F to pending and move the machine queue back to `M12-03F`. No parity ledger rollback
is required.

43
docs/status/M12-03H.md Normal file
View File

@@ -0,0 +1,43 @@
# M12-03H Status
status: done
task: library reload replaces only the matching linked generation snapshot
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 reload requests identify one `SOURCE_LIBRARY` and an expected generation/revision. The
replacement must keep the same library identity, advance exactly one generation, advance the source
revision, and retain a read-only source-library data-block closure. Only the unique matching snapshot
is replaced; other libraries and other generations remain unchanged.
Malformed requests, duplicate state identities, owner substitution, skipped generations, and stale
generation/revision requests fail closed before any Main mutation. A stale request returns
`REVISION_CONFLICT` and an unchanged state.
## Evidence
- `npm --prefix web run test:library-linked-reload` passed 4/4 unit tests.
- The matching-generation case replaces one snapshot atomically while preserving a future generation
and an unrelated library; the input state remains immutable.
- Stale, malformed, duplicate, owner-substituted, and non-adjacent reload cases are rejected without
publishing a partial state.
## Artifact Hashes
- protocol: `8be6f0b2abe36cd566ea7447d1b7de44c0e6a9a7351b863dfdd1372c1761060e`
- unit: `dff866291468cc01e775fe3b3b95c632f5c0742566f79b956a0193bfd8fd43d2`
- manifest: `dd96702dcb064779de3fc33ce1200d75615e7b75a3418a45a309e0e612fdb7df`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03I`: missing library preserves a placeholder and the original source reference.
## Rollback
Remove the linked reload protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03G to pending and move the machine queue back to `M12-03H`. No parity ledger rollback
is required.

42
docs/status/M12-03I.md Normal file
View File

@@ -0,0 +1,42 @@
# M12-03I Status
status: done
task: missing library preserves placeholder and original source reference
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 missing-library requests match a linked source library by source identity, generation, and
revision. Marking a matching reference missing changes only its status and adds a
`MISSING_LIBRARY` placeholder. The original locator, source SHA-256, generation, revision, and every
data-block ID remain attached to the reference; unrelated libraries remain unchanged.
Stale generation/revision and source hash drift return a stable conflict without deleting or replacing
the reference. Malformed fields, duplicate identities, and inconsistent placeholder data fail closed.
## Evidence
- `npm --prefix web run test:library-linked-missing` passed 4/4 unit tests.
- The matching case preserves original source metadata and data-block IDs while publishing only the
placeholder status transition.
- Stale, source-drift, undeclared-field, duplicate-identity, and invalid-placeholder cases publish no
partial state.
## Artifact Hashes
- protocol: `5833e8c943ef6bd866a93a0e1666c9521fa6d3e8358861df57b628874b679ec2`
- unit: `4ab6d6ff4845b4ca963399e7eea214ceb412871dc1fdef5bac1ed0333596da4f`
- manifest: `f5fa606161b93e9ee42e7ca8144d2a83379acf601c5045eb8cc76732f8dba431`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03J`: desktop override fixture records reference, local owner, and property override path.
## Rollback
Remove the missing-library protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03H to pending and move the machine queue back to `M12-03I`. No parity ledger rollback
is required.

45
docs/status/M12-03J.md Normal file
View File

@@ -0,0 +1,45 @@
# M12-03J Status
status: done
task: desktop override fixture records reference, local owner, and property override path
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Blender 5.2 creates a real linked Object and converts it through the library override API. The fixture
records the linked reference as `SOURCE_LIBRARY/readOnly=true`, the local hierarchy root as
`LOCAL_OVERRIDE/readOnly=false`, and one explicit custom-property override path. The source marker,
reference ID, local owner, hierarchy root, property operation and value remain stable after save/reopen.
This fixture does not open a general override writer; it freezes the reference/owner/property contract
needed by the next bounded writer task.
## Evidence
- `npm --prefix web run test:library-override-desktop` passed.
- Blender 5.2 source/target fixture regeneration matched the normalized desktop report.
- The reference retains `m12_override_source.blend`; the local object is `LOCAL_OVERRIDE`; the only
recorded property path is `["m12_override_value"]` with value `2.5` and one `REPLACE` operation.
- Save/reopen preserves the reference, hierarchy root, owner semantics, and property metadata.
## Artifact Hashes
- generator: `2cdbac04cac7240360a9d70919380fd90f9479e2cb41d8032fb51626ed4b4dd6`
- checker: `afebb3c9b8715b9d2e0c9b17f463f038bd23e8747074137bccbf1c09c4bfb5ba`
- source blend: `d7f8d78e7e91481bf46ecc9a6bcb01e900a6a397839dd31ab80a53def7675601`
- target blend: `b008a1608ff1e8f679e1e1281bf7be0360f1137e815dd890cbd93e1e98675495`
- report: `19cb13a3417b4b65a8497b622337c42c4697b3f3d48de26a1f70f2d4527ddde0`
- manifest: `01d0f66bb3819eea3e92727d1b5bffbec935d24eeecc28d874b999df78d73fbf`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03K`: expose exactly one verified override writer property.
## Rollback
Remove the override generator/checker, fixture, report, manifest, package command, and this status
entry. Restore M12-03I to pending and move the machine queue back to `M12-03J`. No parity ledger
rollback is required.

42
docs/status/M12-03K.md Normal file
View File

@@ -0,0 +1,42 @@
# M12-03K Status
status: done
task: expose exactly one verified override writer property
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 override writer accepts exactly `SET_M12_OVERRIDE_VALUE` for the property path
`["m12_override_value"]`. It requires `LOCAL_OVERRIDE`, `readOnly=false`,
`referenceReadOnly=true`, matching local/reference/hierarchy IDs, and the current revision. A valid
write updates only the verified value and advances the local revision by one.
Linked ownership, a second property path, identity drift, stale revision, malformed fields, and values
outside the bounded float range are blocked before any writer commit.
## Evidence
- `npm --prefix web run test:library-override-writer` passed 4/4 unit tests.
- The valid request applies one property and increments revision exactly once while preserving local
override ownership and reference read-only semantics.
- Stale, linked-owner, identity, alternate-operation, alternate-property, malformed, and out-of-range
requests fail closed.
## Artifact Hashes
- protocol: `dd181c7e9946b98334a5d1c686887afecfe3fc11d732beec246e40bf11a155aa`
- unit: `e41bd32eb4ce4d331a20ecb91f3325a27f461b9ae85e98940d5afd482ec21c4c`
- manifest: `9eeee93e4a806aa59b5c53a6485fe3a1b5e3972c1d2585cdc71b7cfc293afb70`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03L`: block override stale source/revision before Main commit.
## Rollback
Remove the override writer protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03J to pending and move the machine queue back to `M12-03K`. No parity ledger rollback
is required.

41
docs/status/M12-03L.md Normal file
View File

@@ -0,0 +1,41 @@
# M12-03L Status
status: done
task: block override stale source/revision before Main commit
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 override freshness binds the local override to its source library ID, source generation,
source revision, dependency-closure SHA-256, invalidation token, and local/reference/hierarchy IDs.
Only a fully matching `COMMIT_OVERRIDE` request reaches `READY`; no Main mutation is performed by
this gate.
Stale generation/revision, dependency closure, invalidation token, linked ownership, identity drift,
alternate operation, malformed token, and undeclared fields return stable blocking codes before Main.
## Evidence
- `npm --prefix web run test:library-override-freshness` passed 4/4 unit tests.
- The exact source generation/revision/closure/token binding returns `READY`.
- Stale source fields return `REVISION_CONFLICT`; local identity drift returns
`ASSET_SOURCE_HASH_MISMATCH`; linked ownership returns `LINKED_DATA_MUTATION_BLOCKED`.
## Artifact Hashes
- protocol: `2eff7ea7605b1579d7551336d87d4f30adb996b585596ff9eee67aea04ca7d22`
- unit: `d48344f31e1ba12e557ca30ece56643583efa633da556f5de3896a8e9175ef8f`
- manifest: `237f3f168e037b67b805ba8d9c9455250c889a5e90c45cbb17d2d20d9fbdcc50`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03M`: dependency cycle, ID collision, cross-library cycle, and duplicate reload negatives.
## Rollback
Remove the override freshness protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03K to pending and move the machine queue back to `M12-03L`. No parity ledger rollback
is required.

38
docs/status/M12-03M.md Normal file
View File

@@ -0,0 +1,38 @@
# M12-03M Status
status: done
task: dependency cycle, ID collision, cross-library cycle, and duplicate reload negatives
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 negative-case validation checks library dependency graphs, cross-library references,
data-block identity ownership, and reload generation identities. Acyclic graphs with unique IDs are
accepted; dependency/cross-library cycles, missing source libraries, duplicate data-block IDs, and
duplicate reload generations fail with stable codes before any writer or Main mutation.
## Evidence
- `npm --prefix web run test:library-negative-cases` passed 4/4 unit tests.
- Direct dependency cycles and cross-library cycles return `LIBRARY_DEPENDENCY_CYCLE`.
- Data-block collision returns `TASK_VALIDATION_FAILED`; duplicate reload returns `REVISION_CONFLICT`;
a missing source library returns `ASSET_SOURCE_HASH_MISMATCH`.
## Artifact Hashes
- protocol: `efc7cc810089eab6178fc5c2f2a45d641625a032ecfa5b2b4ef37694d0decc97`
- unit: `b7ae41b2c35b2fe1598bca4425dd434230bfe4bf342320c88214a060dcbb0a16`
- manifest: `693cbedfe8e29167283a753d119de5a9cfe5e20bc96aff7ba84d720ffde9148a`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-03N`: append/link/override each receive independent desktop/WASM/Chromium commands.
## Rollback
Remove the negative-case protocol, unit suite, manifest, package command, and this status entry.
Restore M12-03L to pending and move the machine queue back to `M12-03M`. No parity ledger rollback
is required.

42
docs/status/M12-03N.md Normal file
View File

@@ -0,0 +1,42 @@
# M12-03N Status
status: done
task: append/link/override each receive independent desktop/WASM/Chromium commands
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Append, LINK, and library override each have independent desktop, WASM, and Chromium commands. The
commands are bound to distinct fixtures/protocol tests/browser specs and use separate Chromium ports.
The command checker verifies all nine package entries and their lane-specific targets.
## Evidence
- `npm run test:library-operation-commands` passed with 9 independent commands: 3 operations x 3 lanes.
- Desktop: append fixture, LINK fixture, and Blender 5.2 override fixture all passed.
- WASM: append receipt protocol passed 2/2; LINK aggregate passed 11/11; override aggregate passed 8/8.
- Chromium: append passed 1/1; LINK gate passed 1/1 on port 5195; override writer passed 1/1 on port
5196. Append Chromium passed 1/1 on port 5194.
## Artifact Hashes
- command checker: `3564347393134d835fdf279b8b8f58558ee24fe0165c3b4527195d094a451e98`
- append WASM protocol: `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7`
- append WASM unit: `75fc2d626f7ca7e820e9cacf659a453886e15e790cde43348828c03bed752ec7`
- append Chromium: `101f67d35c2320da57459f08a59687d769234b910dbd9aaec5a668cee7e0f9f0`
- link Chromium: `814e3486522fb4f0ffdd59acd385a4fca9c5660cdb07e5a2acb1cadd70376bff`
- override Chromium: `1c12982b4eb4fb4b9a3c88907a842a1fc413b3a3a760a9f57b350f57060d007f`
- manifest: `e4cccc8edc277b5047c3f8006ea40b26b119d28589fe2f3bd9bfa4cc3575c85a`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04A`: library source schema accepts only declared HTTPS origins, project assets, or user-selected files.
## Rollback
Remove the independent command checker, lane-specific tests/wrappers, manifest, package commands, and this
status entry. Restore M12-03M to pending and move the machine queue back to `M12-03N`. No parity ledger
rollback is required.

40
docs/status/M12-04A.md Normal file
View File

@@ -0,0 +1,40 @@
# M12-04A Status
status: done
task: library source schema accepts declared HTTPS origin, project asset, or user-selected file
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Schema-1 source admission accepts exactly three declared kinds: HTTPS URLs whose credential-free origin
is present in the policy, project-relative asset paths normalized by the existing project-path gate,
and user-selected files carrying a stable selection ID, safe file name, bounded byte length, and
source SHA-256. Accepted sources receive a canonical locator.
Undeclared origins, credentials, unsafe project paths, empty origin policy, malformed file identity,
and undeclared fields fail closed before any library load.
## Evidence
- `npm --prefix web run test:library-source-origin` passed 4/4 unit tests.
- Declared HTTPS, project asset, and user-selected file cases return `READY` with canonical locators.
- Undeclared HTTPS, credential-bearing URLs, traversal paths, empty policies, malformed selection IDs,
and undeclared fields return stable blocking errors.
## Artifact Hashes
- protocol: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`
- unit: `89b207c9cb13b4fb055a2dfed0237c0f8a00b13a39cc4175a378f5ef2abdb7ae`
- manifest: `af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04B`: normalize POSIX/Windows separators, `.`, `..`, percent encoding, and Unicode names.
## Rollback
Remove the source-origin protocol, unit suite, manifest, package command, and this status entry. Restore
M12-03N to pending and move the machine queue back to `M12-04A`. No parity ledger rollback is required.

42
docs/status/M12-04B.md Normal file
View File

@@ -0,0 +1,42 @@
# M12-04B Status
status: done
task: normalize POSIX/Windows separators, dot segments, percent encoding, and Unicode names
updated: 2026-08-17 America/New_York
enablingTask: false
parityStateChange: false
## Scope
The shared project-asset path normalizer now emits one canonical `/`-separated path. It accepts
equivalent POSIX/Windows separators, removes `.` segments, resolves bounded `..` segments without
allowing project-root escape, decodes percent-encoded UTF-8 once, and applies Unicode NFC normalization.
Residual percent octets, malformed encoding, controls, absolute paths, URI schemes, and traversal beyond
the project remain fail-closed.
## Evidence
- `npm --prefix web run test:library-path-normalization` passed 4/4 unit tests.
- Separator and dot aliases converge to one idempotent canonical path.
- Percent-encoded separators/dot segments and decomposed Unicode names match the canonical project asset
locator used by the M12-04A source admission path.
- Re-decoding, malformed percent encoding, and project-root escape return stable path errors.
- `npm --prefix web run typecheck` passed after closing the prior library wrapper/parser type errors.
## Artifact Hashes
- parent manifest: `af80827d925ddd96d8541e446e0f70c956fd4c56e64ac984d187f5449df4cb0d`
- normalizer: `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf`
- source admission: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`
- unit: `abde64c60397541e92a83dd9e4a24e65faf340a8d046650604c101a21037c777`
- manifest: `8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04C`: reject absolute paths, UNC, drive paths, NUL, control characters, and origin escape.
## Rollback
Remove the path normalizer changes, unit suite, manifest, package command, and this status entry. Restore
M12-04A to pending and move the machine queue back to `M12-04B`. No parity ledger rollback is required.

45
docs/status/M12-04C.md Normal file
View File

@@ -0,0 +1,45 @@
# M12-04C Status
status: done
task: reject absolute paths, UNC/drive paths, controls, and origin escape
updated: 2026-08-18 America/New_York
enablingTask: false
parityStateChange: false
## Scope
The project-asset path gate rejects absolute POSIX paths, UNC and drive-shaped paths, NUL/control
characters, malformed or residual percent encoding, and URI/origin-shaped escapes before a locator is
accepted. HTTPS source policy entries are now strict credential-free origins: path, query, fragment,
encoded controls, and backslash smuggling are rejected instead of being silently reduced to
`URL.origin`. HTTPS resource paths retain the declared-origin check and reject unsafe decoded bytes.
## Evidence
- `npm --prefix web run test:library-path-security` passed 4/4 unit tests.
- Absolute, UNC, drive, raw/encoded NUL and control characters, and origin-style project paths return
stable `ASSET_PATH_OUTSIDE_PROJECT`/`ASSET_PATH_INVALID` errors.
- Raw and encoded backslashes, controls, malformed percent sequences, credentials, undeclared origins,
and policy origin smuggling return `IO_EXTERNAL_URI_BLOCKED`.
- Declared `https://assets.example.test/library/main.blend` remains accepted; policy declarations with
a path, query, fragment, encoded control, or duplicate canonical origin fail closed.
- `WEB_TEST_PORT=5323 npm --prefix web run test:asset-library` passed the N-023 Chromium asset/IO gate 1/1.
- `npm --prefix web run typecheck` passed.
## Artifact Hashes
- parent manifest: `8cd29c3f5281082584fc6aefe2ec385a2eedf8116e837a4db54c391391ff8f98`
- path normalizer: `6109d05251fc7355ac32d4c0d8298f85ea8b731767c76c394577c4e44652a6bf`
- source-origin protocol: `67a1808f225194a09a72987c5340c7494c5fa8d99dffde40664a11a779caedeb`
- unit: `ab302cacb24634a3225dda5cb8f5282cb80b3bd81ed5c8900ddf4ff18267b7e2`
- manifest: `a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04D`: resolve symlink/hardlink entries before writing and constrain them to the temporary root.
## Rollback
Remove the C path-security assertions, manifest, status entry, and strict origin validation. Restore
M12-04B to pending and move the machine queue back to `M12-04C`. No parity ledger rollback is required.

44
docs/status/M12-04D.md Normal file
View File

@@ -0,0 +1,44 @@
# M12-04D Status
status: done
task: resolve symlink/hardlink entries before writing and constrain them to the temporary root
updated: 2026-08-18 America/New_York
enablingTask: false
parityStateChange: false
## Scope
Archive entries are parsed before any write. Every member is canonicalized as a relative path under
the declared temporary-root identity. Symlink targets resolve relative to the link's parent; hardlink
targets resolve from the archive root and must end at a regular file. Absolute, drive-shaped, URI,
backslash-smuggled, traversal, missing-target, cyclic, duplicate, and hardlink-to-directory inputs
fail closed with `IO_ARCHIVE_UNSAFE`.
The resolver returns a write plan with the final in-root target and an explicit
`withinTemporaryRoot: true` proof for every member. It does not follow or write any link before the
entire manifest has passed validation.
## Evidence
- `npm --prefix web run test:library-link-safety` passed 5/5 unit tests.
- Parent-directory symlinks, archive-root hardlinks, and chained links resolve to canonical in-root
members.
- Absolute/UNC/drive/URI/traversal targets, missing members, cycles, duplicate paths, undeclared
fields, and hardlinks to directories are rejected before a write plan is returned.
## Artifact Hashes
- parent manifest: `a7f3a45eb7f68d022f38185a1c1409e1db1b27647fef587d66d2ffa52d78f98e`
- protocol: `d55a4ba762898aed22bdcc7aa6493c713ee94f6bb1bf58754fdc459d0ddf70d1`
- unit: `7739275be91222d7bfb61d2f5a1daf812c6860fe34d0aea27df8380a77322120`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04E`: archive first reads the central directory/manifest and does not extract payload first.
## Rollback
Remove the archive link-safety protocol, unit suite, manifest, package command, and this status entry.
Restore M12-04C to pending and move the machine queue back to `M12-04D`. No parity ledger rollback is
required.

38
docs/status/M12-04E.md Normal file
View File

@@ -0,0 +1,38 @@
# M12-04E Status
status: done
task: archive first reads the central directory or manifest before payload extraction
updated: 2026-08-18 America/New_York
enablingTask: false
parityStateChange: false
## Scope
ZIP requests produce a bounded `CENTRAL_DIRECTORY` first-read plan; TAR requests produce a bounded
`MANIFEST` first-read plan. The plan contains no payload ranges. A read trace is accepted only when
the first exact range is the declared metadata range and every later range is payload; payload-first,
wrong-range, duplicate-metadata, out-of-order, oversized, and out-of-archive reads fail closed with
`IO_ARCHIVE_UNSAFE`.
## Evidence
- `npm --prefix web run test:library-metadata-first` passed 4/4 unit tests.
- ZIP and TAR plans expose metadata-only first reads with an empty payload plan.
- Payload-first, wrong-range, duplicate metadata, out-of-archive, and metadata-budget cases are
rejected before extraction can be scheduled.
## Artifact Hashes
- parent manifest: `1c09abd1f4bd5908d22ab3b22e3e71d050f694af0b82a7f78a351d7a1bf1e664`
- protocol: `869586b041e134c7d1cb2ebd7e13b35218b337223d401697a179f71cd1420f5b`
- unit: `2da649722acee9cace8db6f337b4a93500a9c775a0b0f086bf38dd2b3e7f9e91`
- package commands: `cfb232baa494ff59f6c8d65514a08c86332ee9354350b2a0392b6f932a7ee42c`
## Next Task
`M12-04F`: enforce per-entry bytes, total bytes, entry count, directory depth, and filename length budgets.
## Rollback
Remove the metadata-first protocol, unit suite, manifest, package command, and this status entry. Restore
M12-04D to pending and move the machine queue back to `M12-04E`. No parity ledger rollback is required.

Some files were not shown because too many files have changed in this diff Show More