Compare commits

...

6 Commits

Author SHA1 Message Date
mes123456
5a11045ca5 Advance Blender 5.2 web parity through M12-03D
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-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
557 changed files with 57339 additions and 1189 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
.emcache/
.emscripten-web
__pycache__/
*.py[cod]
*.log
mylog.txt

View File

@@ -29,6 +29,12 @@ npm --prefix web run build
npm --prefix web run test:browser
```
See `docs/PROJECT_STATUS_AND_NEXT_WORK.md` and
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` for the implemented scope and remaining
structured blockers.
See `docs/CURRENT_EXECUTION_PLAN.md` for the current task order,
`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

@@ -76,7 +76,8 @@ web/protocol/schema-version
### 3.2 功能完成
- Chrome、Firefox、Safari 最近稳定版至少各通过一次冒烟测试。
- V1 以 Chromium 为唯一发布基线,主线程 WebGL 和 OffscreenCanvas Worker 均须通过;
Firefox/WebKit 作为发布后兼容目标,不进入 V1 完成条件。
- 单线程构建可工作;多线程不可用时有明确降级信息。
- 引擎异常不会使 React 页面失去保存能力。
- 所有变更有 revision可撤销、可重做或明确标注不可撤销。
@@ -520,7 +521,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-039 | `in_progress` | basic_scene 已闭环;等待 W-033~W-038 的完整验收 |
| 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-043 | `in_progress` | 1.5 秒可取消 autosave 已接入 React dirty 状态并经 E2E 验证;命令数阈值、快照保留和大文件背压仍待完成 |
| W-044 | `in_progress` | operation_log schema、payload/inversePayload 写入和 StorageClient 接口已实现并经 E2E 验证replay、损坏隔离和 native command recovery 仍待完成 |
@@ -1416,7 +1417,8 @@ P2 按用户需求逐项启用,不得让单个高级模块阻塞 P0/P1 发布
前置:`W-130`~`W-134`
执行Chromium、Firefox、WebKit 测试 P0/P1 场景、存储、WebGL2、线程降级、导入导出和截图差异
执行:V1 在 Chromium 测试 P0/P1 场景、存储、WebGL2、线程降级、导入导出和截图差异
Firefox/WebKit 在发布后兼容阶段执行同一矩阵。
验收:支持矩阵中的每个组合有结果;不支持的组合有明确阻断页。

View File

@@ -510,7 +510,8 @@ manifest浏览器运行时禁止从 CDN、远程 ESM 或第三方脚本地址
## 12. 验收标准
以 Chromium、Firefox、Safari 最近稳定版各一台桌面设备作为参考环境,至少验证:
V1 以 Chromium 桌面设备作为发布参考环境,并同时覆盖主线程和 OffscreenCanvas Worker
Firefox、Safari/WebKit 留作发布后兼容矩阵。至少验证:
1. 空白项目、基础 `.blend` 和含贴图项目可以打开、编辑、保存、刷新恢复。
2. 保存失败、页面刷新、Worker 异常后不会破坏上一个完整快照。

View File

@@ -157,7 +157,7 @@ if(WITH_WEB)
# runtime only needs model/animation RNA needed by depsgraph evaluation.
# Trim after generation so the generator itself remains source-compatible.
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
"${CMAKE_CURRENT_BINARY_DIR}/rna_prototypes_gen.hh"
"${CMAKE_CURRENT_BINARY_DIR}/../RNA_prototypes.hh"

View File

@@ -5,12 +5,17 @@
/** \file
* \ingroup modifiers
*
* Native Web evaluator for the first bounded Geometry Nodes closure. It
* accepts a single Group Input -> Transform Geometry -> Group Output graph.
* Unsupported graphs remain visible and report an explicit modifier error.
* Native Web evaluator for the bounded Geometry Nodes allowlist. Unsupported
* graphs remain visible and report an explicit modifier error.
*/
#include <algorithm>
#include <cmath>
#include <cstdint>
#include <cstring>
#include <optional>
#include <string>
#include <variant>
#include "MEM_guardedalloc.h"
@@ -18,19 +23,32 @@
#include "BLI_math_rotation.hh"
#include "BLI_listbase.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_mesh.hh"
#include "BKE_modifier.hh"
#include "BKE_node.hh"
#include "BKE_node_legacy_types.hh"
#include "BKE_object.hh"
#include "BLO_read_write.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_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 "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)
{
NodesModifierData *nmd = reinterpret_cast<NodesModifierData *>(md);
if (nmd->node_group != nullptr) {
DEG_add_node_tree_output_relation(ctx->node, nmd->node_group, "Web Geometry Nodes Modifier");
if (nmd->node_group == nullptr) {
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)
{
for (const bNodeSocket &socket : node.inputs) {
if (STREQ(socket.identifier, identifier)) {
if (STREQ(socket.identifier, identifier) || STREQ(socket.name, identifier)) {
return &socket;
}
}
@@ -119,24 +168,12 @@ static const bNodeSocket *find_output(const bNode &node, const char *identifier)
return nullptr;
}
static bool has_link(const bNodeTree &tree,
const bNode &from_node,
const char *from_socket,
const bNode &to_node,
const char *to_socket)
static bool node_is(const bNode &node, const char *idname)
{
const auto matches_socket = [](const bNodeSocket &socket, const char *name) {
return STREQ(socket.identifier, name) || STREQ(socket.name, name);
};
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;
}
if (STREQ(node.idname, idname)) {
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)
@@ -151,6 +188,659 @@ static bool has_input_link(const bNodeTree &tree, const bNode &node, const char
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)
{
const NodesModifierData *nmd = reinterpret_cast<const NodesModifierData *>(md);
@@ -168,13 +858,8 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, 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;
int node_count = 0;
int link_count = 0;
for (const bNode &node : tree->nodes) {
node_count++;
if (ELEM(node.type_legacy,
@@ -188,25 +873,6 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
{
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) {
@@ -217,85 +883,20 @@ static Mesh *modify_mesh(ModifierData *md, const ModifierEvalContext *ctx, Mesh
return mesh;
}
const bNode *geometry_node = transform != nullptr ? transform : set_position;
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) {
if (node_count > WebGeometryNodeEvaluator::max_evaluations) {
BKE_modifier_set_error(ctx->object,
md,
"Web Geometry Nodes Transform supports Components mode only");
"WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: node budget exceeded");
return mesh;
}
const bNodeSocket *translation_socket = find_input(*transform, "Translation");
const bNodeSocket *rotation_socket = find_input(*transform, "Rotation");
const bNodeSocket *scale_socket = find_input(*transform, "Scale");
if (translation_socket == nullptr || rotation_socket == nullptr || scale_socket == nullptr ||
translation_socket->default_value == nullptr || rotation_socket->default_value == nullptr ||
scale_socket->default_value == nullptr)
{
BKE_modifier_set_error(ctx->object, md, "Web Geometry Nodes Transform sockets are incomplete");
WebGeometryNodeEvaluator evaluator(*tree, *ctx, *mesh);
Mesh *result = evaluator.evaluate();
if (result == nullptr) {
const std::string message = "WEB_GEOMETRY_NODES_EVALUATOR_UNSUPPORTED: " + evaluator.error();
BKE_modifier_set_error(ctx->object, md, "%s", message.c_str());
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;
}

View File

@@ -129,7 +129,7 @@ target_link_options(web_engine PRIVATE
"-sINITIAL_MEMORY=67108864"
"-sMAXIMUM_MEMORY=2147483648"
"-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']"
)
if(WEB_ENGINE_ENABLE_UNSAFE_DEPSGRAPH)

View File

@@ -829,6 +829,13 @@ bool refresh_scene_from_main(EngineState &engine,
return false;
}
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;
if (!web_engine_blend_main_physics_simulation_json(
engine.authoritative_main, physics_json, error))
@@ -1132,6 +1139,15 @@ EMSCRIPTEN_KEEPALIVE int web_engine_open_blend(const int handle,
return last_error_code;
}
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;
if (!web_engine_blend_main_physics_simulation_json(
authoritative_main, physics_json, main_error))
@@ -1543,9 +1559,10 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
type == "moveObjectToCollection" || type == "renameId" || type == "joinObjects" ||
type == "separateMeshFaces" || type == "applyObjectTransform" || type == "setObjectOrigin" ||
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 == "removeGreasePencilLayer" || type == "moveGreasePencilLayer" ||
type == "moveGreasePencilFrame" ||
type == "insertGreasePencilFrame" || type == "removeGreasePencilFrame" ||
type == "setGreasePencilStrokes" || type == "setVertexColors" ||
type == "setVertexWeights" || type == "setCameraProperties" || type == "setLightProperties" ||
@@ -1919,6 +1936,13 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(), characters,
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") {
const json links = command.value("links", json::object());
const std::array<const char *, 4> fields = {"regular", "bold", "italic", "boldItalic"};
@@ -1984,6 +2008,12 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
engine->authoritative_main, command.value("dataId", "").c_str(),
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") {
applied = web_engine_blend_main_insert_grease_pencil_frame(
engine->authoritative_main, command.value("dataId", "").c_str(),
@@ -2066,7 +2096,8 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle,
command.value("vertexGroup", "").c_str(),
command.value("indices", std::vector<uint32_t>()),
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")
{
@@ -2114,6 +2145,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)
{
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);
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_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_redo(int handle);
int web_engine_get_scene_snapshot(int handle, const uint8_t **data, uint32_t *length);

View File

@@ -3513,7 +3513,16 @@ json scene_ir_from_blend(const ParsedBlend &blend,
{"emissionStrength", std::clamp(emission_strength, 0.0f, 1000000.0f)} };
if (!normal_image_id.empty()) material["normalImageId"] = normal_image_id;
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 (!warnings.empty()) material["warnings"] = warnings;
materials.push_back(std::move(material));
@@ -3636,9 +3645,20 @@ json scene_ir_from_blend(const ParsedBlend &blend,
}
else if (record.type_name == "VFont") {
const std::string filepath = read_string(*blend.sdna, element, "filepath");
vfonts.push_back({{"id", record.id}, {"name", record.name}, {"sourcePath", filepath},
{"builtin", filepath == "<builtin>"},
{"packed", read_pointer(*blend.sdna, element, "packedfile").value_or(0) != 0}});
const std::optional<uint64_t> packed_file = read_pointer(*blend.sdna, element, "packedfile");
const std::vector<uint8_t> packed_bytes = packed_file ? packed_file_bytes(blend, *packed_file) :
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") {
const int64_t camera_type = read_integer(*blend.sdna, element, "type").value_or(0);

View File

@@ -80,6 +80,7 @@ using json = nlohmann::json;
using namespace blender;
std::once_flag blender_runtime_once;
constexpr int64_t geometry_node_max_json_scalar_values = 65'536;
void initialize_blender_runtime()
{
@@ -341,6 +342,11 @@ json evaluated_modifier_report(const Object &object, const Object *object_eval)
entry["suggestion"] =
"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:")) {
entry["errorCode"] = "MODIFIER_CONFIGURATION_UNSUPPORTED";
entry["suggestion"] =
@@ -399,7 +405,15 @@ json evaluated_mesh_report(const Depsgraph *graph, Object *object)
{"modifiers", evaluated_modifier_report(*object, object_eval)},
{"worldMatrix", 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 column = 0; column < 4; column++) {
/* 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.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 (int index = 0; index < 3; 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()
{
static bool initialized = false;
@@ -411,4 +418,5 @@ const ComputeContext *compute_context_for_viewer_path_elem(
namespace blender {
void WM_main_add_notifier(unsigned int /*type*/, void * /*reference*/) {}
void WM_msg_publish_rna(wmMsgBus * /*mbus*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/) {}
} // namespace blender

View File

@@ -8,6 +8,8 @@
#include <cstring>
#include <limits>
#include <new>
#include <string_view>
#include <tuple>
#include <unordered_map>
#include <unordered_set>
@@ -18,6 +20,7 @@
#include "BKE_anim_data.hh"
#include "BKE_attribute.hh"
#include "BKE_attribute.h"
#include "BKE_blendfile_link_append.hh"
#include "BKE_layer.hh"
#include "BKE_collection.hh"
#include "BKE_curve.hh"
@@ -36,7 +39,9 @@
#include "BKE_nla.hh"
#include "BKE_object.hh"
#include "BKE_object_deform.h"
#include "BKE_packedFile.hh"
#include "BKE_scene.hh"
#include "BKE_vfont.hh"
#include "BKE_volume.hh"
#include "BKE_fcurve.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
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;
}
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,
const char *curve_type,
const char *name,
@@ -3451,6 +3778,58 @@ bool web_engine_blend_main_set_font_advanced(
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,
const char *data_id,
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", "cyclic"}, {"domain", "STROKE"}, {"dataType", "BOOL"}, {"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) {
data["errorCode"] = "GREASE_PENCIL_BUDGET_EXCEEDED";
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;
json strokes = json::array();
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,
frame_number);
if (drawing != nullptr) {
@@ -3665,13 +4060,15 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
for (const int point_index : point_range) {
const float3 &position = positions[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]},
{"opacity", opacities[point_index]},
{"vertexColor", json::array({color.r, color.g, color.b, color.a})}});
}
strokes.push_back({{"id", "grease-pencil-stroke:" + id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index) + ":" +
strokes.push_back({{"id", "grease-pencil-stroke:" + drawing_suffix + ":" +
std::to_string(curve_index)},
{"cyclic", cyclic[curve_index]},
{"pointCount", point_range.size()},
@@ -3682,8 +4079,7 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
json frame_entry = {
{"frame", frame_number},
{"drawing",
{{"id", "grease-pencil-drawing:" + id_name(grease_pencil.id) + ":" +
std::to_string(frame.drawing_index)},
{{"id", drawing_id},
{"strokeCount", strokes.size()},
{"pointCount", drawing_point_count},
{"strokes", std::move(strokes)}}}};
@@ -3707,6 +4103,183 @@ bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
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,
std::string &physics_json,
std::string &error)
@@ -3884,6 +4457,50 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
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,
const char *data_id,
const char *layer_id,
@@ -4039,7 +4656,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices,
const std::vector<float> &values,
const bool normalize,
const uint32_t limit,
const bool mirror,
const int mirror_axis,
const float mirror_tolerance,
std::string &error)
{
Object *object = find_object(state != nullptr ? state->main : nullptr, object_id);
@@ -4050,10 +4670,6 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
if (error.empty()) error = "PAINT_SCHEMA_INVALID: Mesh object was not found";
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() ||
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); }) ||
@@ -4061,6 +4677,88 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
error = "PAINT_SCHEMA_INVALID: vertex weight patch is outside the bounded domain";
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);
if (group_index < 0) {
if (BKE_object_defgroup_add_name(object, vertex_group) == nullptr) {
@@ -4070,15 +4768,35 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
group_index = BKE_object_defgroup_name_index(object, vertex_group);
}
MutableSpan<MDeformVert> deform_verts = mesh->deform_verts_for_write();
for (size_t index = 0; index < indices.size(); index++) {
MDeformVert &deform_vert = deform_verts[indices[index]];
if (values[index] == 0.0f) {
std::vector<uint32_t> touched_vertices;
touched_vertices.reserve(updates.size());
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)) {
BKE_defvert_remove_group(&deform_vert, weight);
}
}
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);
}

View File

@@ -128,6 +128,16 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state,
const std::vector<float> &location,
std::string &object_id,
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,
const char *object_id,
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,
int active_text_box,
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,
const char *data_id,
const std::array<std::string, 4> &font_ids,
@@ -361,6 +377,9 @@ bool web_engine_blend_main_set_metaball_elements(WebBlendMainState *state,
bool web_engine_blend_main_grease_pencils_json(WebBlendMainState *state,
std::string &grease_pencils_json,
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,
std::string &physics_json,
std::string &error);
@@ -377,6 +396,13 @@ bool web_engine_blend_main_move_grease_pencil_layer(WebBlendMainState *state,
const char *layer_id,
const char *direction,
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,
const char *data_id,
const char *layer_id,
@@ -407,7 +433,10 @@ bool web_engine_blend_main_set_vertex_weights(WebBlendMainState *state,
const std::vector<uint32_t> &indices,
const std::vector<float> &values,
bool normalize,
uint32_t limit,
bool mirror,
int mirror_axis,
float mirror_tolerance,
std::string &error);
bool web_engine_blend_main_set_render_properties(WebBlendMainState *state,
const char *target_id,

View File

@@ -0,0 +1,527 @@
# Blender 5.2 全功能对标工作分解
更新时间2026-08-15
> 本文保留为 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/CURRENT_EXECUTION_PLAN.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-17M6-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 closure。当前唯一下一任务为
`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 `M12-03E`

View File

@@ -0,0 +1,615 @@
# Blender 5.2 完整 Web 对标实施计划
更新时间2026-08-17
本文是 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` |
| 当前唯一可领取任务 | `CURRENT_EXECUTION_PLAN.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 已完成,当前唯一 `nextTask``M12-03E`
## 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. Chromium、Firefox、WebKit 的支持声明分别由自己的 quick/P0/full 证据决定,不互相推断。
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-G enabling tasks 与 M12-02H preview display
parity slice 及 M12-03A-D library-operation inventory/identity/desktop fixture/WASM Main append
已完成,当前唯一下一任务为 M12-03E。
- 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 不在每个早期原子任务的默认完成门中M14 声明某浏览器支持后,该浏览器对
所有已验证 `parityId` 的适用 browser evidence 立即成为强制项,不能由 Chromium 结果代替。
### 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-03D 完成后,当前唯一 `nextTask``M12-03E`
## 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。
- [ ] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致。
- [ ] `M12-03F` desktop link fixture 保留 source library 和只读 ownership。
- [ ] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`
- [ ] `M12-03H` library reload 只替换匹配 generation 的 linked snapshot。
- [ ] `M12-03I` missing library 保留 placeholder 和原始 source不删除引用。
- [ ] `M12-03J` desktop override fixture 记录 reference、local owner 和 property override path。
- [ ] `M12-03K` 首个 override writer 只开放一个已验证属性。
- [ ] `M12-03L` override stale source/revision 在 Main commit 前阻断。
- [ ] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例。
- [ ] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令。
### M12.4 Origin、路径与 archive
- [ ] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件。
- [ ] `M12-04B` 规范化 POSIX/Windows separator、`.``..`、percent encoding 和 Unicode 名称。
- [ ] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸。
- [ ] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。
- [ ] `M12-04E` archive 先读取 central directory/manifest不先解压 payload。
- [ ] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。
- [ ] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。
- [ ] `M12-04H` 解压取消删除 staging不修改已提交项目。
- [ ] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复。
- [ ] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归。
### M12.5 格式 capability matrix
- [ ] `M12-05A` 从 Blender 5.2 build/runtime 生成 glTF/GLB、OBJ、STL、PLY、USD、Alembic 清单。
- [ ] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持。
- [ ] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行。
- [ ] `M12-05D` 能力由 runtime receipt 决定,不按扩展名推断。
- [ ] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash。
- [ ] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝。
### M12.6 GLB round-trip
- [ ] `M12-06A` desktop 生成 Mesh/PBR/UV/skin/animation GLB fixture 组。
- [ ] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations。
- [ ] `M12-06C` import 后保存 `.blend`、重开并比较 stable ID。
- [ ] `M12-06D` Web export 生成 machine loss report。
- [ ] `M12-06E` desktop Blender 再导入 Web GLB 并比较 canonical report。
- [ ] `M12-06F` sparse accessor、Draco/extension、外部 URI 和超预算负例。
- [ ] `M12-06G` import/export 取消、Worker restart 和 OPFS quota 恢复。
### M12.7 OBJ、STL、PLY round-trip
- [ ] `M12-07A` OBJ 单 Mesh 正例position/normal/UV/material group。
- [ ] `M12-07B` OBJ 多对象、负索引、MTL/texture origin 和坏 face 负例。
- [ ] `M12-07C` OBJ Web→desktop round-trip 与 loss report。
- [ ] `M12-07D` STL binary/ASCII capability 分开声明。
- [ ] `M12-07E` STL normal、unit、degenerate triangle 和 trailing bytes 对标。
- [ ] `M12-07F` STL Web→desktop round-trip 与材质缺失 loss report。
- [ ] `M12-07G` PLY ASCII/binary little-endian capability 分开声明。
- [ ] `M12-07H` PLY vertex/face/color/custom property 映射与未知 property loss report。
- [ ] `M12-07I` PLY big-endian/坏 list/超大 count 稳定阻断。
- [ ] `M12-07J` 三格式分别执行取消、OOM、重启和小文件恢复。
### 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 默认拒绝与只读盘点
- [ ] `M13-01A` 盘点 Text、Python Console、autorun、driver expression、handler 和 add-on 入口。
- [ ] `M13-01B` `.blend` 打开时只读取脚本 metadata不执行任意内容。
- [ ] `M13-01C` autorun、register、install、driver execution 默认返回稳定 policy code。
- [ ] `M13-01D` UI 不提供绕过协议直接 eval 的入口。
- [ ] `M13-01E` 保存/重开保留原 Text 数据块和未知脚本,不重写源码。
- [ ] `M13-01F` malicious text、driver、handler 和 embedded module fixture 进入负例。
### M13.2 Script manifest 与签名
- [ ] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission。
- [ ] `M13-02B` canonical serialization 固定签名输入。
- [ ] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy。
- [ ] `M13-02D` signature 只批准声明内容source hash 变化立即失效。
- [ ] `M13-02E` permission 默认最小化,未知 permission 阻断。
- [ ] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。
### M13.3 浏览器 sandbox
- [ ] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络。
- [ ] `M13-03B` CPU、wall time、memory、message 和 output byte budget 固定。
- [ ] `M13-03C` host call 使用显式 allowlist 和结构化参数。
- [ ] `M13-03D` sandbox crash/timeout 终止当前 job不污染 Main revision。
- [ ] `M13-03E` cancellation 后不得发布迟到 message 或 cache。
- [ ] `M13-03F` dispose 后 Worker、port、timer 和 buffer 归零。
- [ ] `M13-03G` 小脚本在同会话恢复,审计链保持连续。
### M13.4 Blender server job isolation
- [ ] `M13-04A` 每个 job 创建不可预测的一次性目录。
- [ ] `M13-04B` source 只读挂载output 写独立目录。
- [ ] `M13-04C` CPU、内存、进程、文件、时间和 output budget 由 OS/container 强制。
- [ ] `M13-04D` 默认无网络;声明 origin 使用单独 policy。
- [ ] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动。
- [ ] `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、供应链与恶意输入
- [ ] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。
- [ ] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。
- [ ] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。
- [ ] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。
- [ ] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。
- [ ] `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
- [ ] `M14-02A` Firefox quicktype/protocol/static asset 启动。
- [ ] `M14-02B` Firefox P0open/edit/undo/redo/save/reopen/export。
- [ ] `M14-02C` Firefox 主线程 WebGL2 像素和 context loss。
- [ ] `M14-02D` Firefox Offscreen 只在主线程通过后领取。
- [ ] `M14-02E` Firefox OPFS quota、Worker crash、OOM 和 network interruption。
- [ ] `M14-02F` Firefox full family 命令逐项 PASS/BLOCKED禁止静默 fallback。
- [ ] `M14-02G` Firefox quick/P0/full 进入 CI 后才更新支持声明。
### M14.3 WebKit
- [ ] `M14-03A` WebKit quick 启动和本地 asset 完整性。
- [ ] `M14-03B` WebKit P0 用户闭环。
- [ ] `M14-03C` WebKit 主线程 viewport。
- [ ] `M14-03D` WebKit Offscreen 独立 gate。
- [ ] `M14-03E` WebKit storage/fault/recovery。
- [ ] `M14-03F` WebKit full family matrix。
- [ ] `M14-03G` WebKit CI 和发布说明。
### M14.4 设备档位与输入
- [ ] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。
- [ ] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。
- [ ] `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. 先运行现有正例,确认不是在旧失败上继续扩展。
3. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。
4. 先完成协议和 fail-closed再接 Main writer再接浏览器 UI。
5. 失败结果记录到工作日志,但不写成成功 evidence。
6. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。
7. 更新 ledger 时只增加本轮真实完成的 slicefamily 全域状态保持 `BLOCKED` 直到 gap 为零。
8. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。

View File

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

View File

@@ -0,0 +1,894 @@
# Web Blender V1 当前执行计划
更新时间2026-08-17
## 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 元数据
```
本计划是当前唯一的短周期领取队列。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 产品契约、支持矩阵、非目标、发布标准 |
| `CURRENT_EXECUTION_PLAN.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-D 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 |
| M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 未开始 | 恶意输入矩阵和 release 安全门通过 |
| M14 跨浏览器/设备 | Firefox、WebKit、触控、笔、HiDPI、IME | 未开始 | 新浏览器进入 quick/P0/full CI 后才声明支持 |
| 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-03D
机器证据完成后,唯一下一任务为 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的
`M12-03E`;后续只能领取该文档或机器差距生成器给出的精确字母任务,不得领取下列整行摘要。
- [ ] `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不在同一变更中同时追三个浏览器差异。
- [ ] `M14-02` Firefox capability probe 覆盖 OPFS、WebGL2、WebGPU、Worker 和 isolation。
- [ ] `M14-03` Firefox P0 流程逐项记录 PASS/BLOCKED不启用静默 fallback。
- [ ] `M14-04` Firefox 主线程视口先通过,再领取 Offscreen 路径。
- [ ] `M14-05` WebKit capability probe 和 P0 流程采用相同规则。
- [ ] `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 是状态审计和差距生成,不是把剩余
功能一次性打包。

View File

@@ -1,6 +1,11 @@
# Web Blender 项目现状与后续连续任务
更新时间2026-08-14
更新时间2026-08-17
当前短周期任务、领取顺序和阶段退出条件统一维护在
`docs/CURRENT_EXECUTION_PLAN.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为
V1 的逐项领取顺序V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准M12-M23 全功能
实施规则以 `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准。
## 1. 当前结论
@@ -15,6 +20,32 @@ Blender C/C++ 子集编译为 WASMOPFS 保存大文件IndexedDB 保存小
SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM 和其他
运行时资源全部使用项目本地文件,不依赖 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-D 共 21 项 | Append undo/redo/save/reopen、Link/Override 与后续 IO/Editor 仍未完成 | WASM Main 已有单 transaction append slice但 N-023 全域 parity 仍阻断 |
当前优先级不是扩 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-D 已完成,机器队列的当前唯一 `nextTask` 为完整对标计划中的
`M12-03E`
## 2. 已完成并有测试覆盖的能力
| 能力 | 当前证据 | 状态 |
@@ -30,17 +61,22 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
| Packed image | PackedFile、双 tile UDIM、generated、linked library、损坏签名、路径沙箱和 OPFS SHA-256 去重/重发现 | 已完成当前资源矩阵 |
| GLB | 本地导出、严格解析,以及 Blender 5.2 回导比较网格/PBR/纹理/morph/skin/animation | 已完成当前 SceneIR 子集 |
| 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 保存重开 | 已完成当前操作集 |
| 材质与 UV | 材质槽/面分配、UV Map、planar/cube unwrap、限定 Principled/Image/Normal 图、图片打包 | 已完成当前节点集 |
| 动画编辑 | TRS 关键帧、Timeline/Dope Sheet、插值、Action 选择和约束开关/影响 | 已完成当前操作集 |
| 对象层级 | 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 | 已完成当前数据契约 |
| 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 未开放 |
| 发布性能/故障门 | Chromium 118 条 E2E、1M geometry、600 帧 OPFS cache、4K/8K texture、运行中断网、主线程 WebGL context loss | 已完成已声明门10M、长媒体、OOM 和 Offscreen device loss 仍 |
| Shader Main | RGB/Value/Principled/Image Texture/Normal Map/Output 整图事务、资源门和保存重开 | 已完成六节点写回子集;任意图/编译仍阻断 |
| NLA Main | 单对象 Action Clip 整栈写回、SceneIR 重开和 native frame 求值 | 已完成有限 Action Clip 子集 |
| 非 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 未完成 |
| 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 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 仍属后续完整故障矩阵 |
| 实时渲染 reference | Blender 5.2 Eevee 固定相机/黑体/World fixtureSRGB8 MAE、RMS、P95、坏像素比例、前景 IoU、alpha 覆盖率;主线程/Offscreen 同帧 | M11-04 有界 golden 已完成;完整 AgX/高级灯光、Cycles/复杂 Eevee、Volume 深度合成仍阻断 |
| 最终渲染路由 | bounded Eevee WebGL2 可本地WebGPU 需 browser+bundle 双门Cycles、复杂 Eevee、Workbench 和桌面硬件后端只返回 `SERVER_JOB` | M11-05 路由合同已完成M11-06 server job 已绑定 source/settings/build/output hash真实远程队列、进度、取消和 denoise 仍属后续任务 |
| 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. 部分完成或仍有边界的能力
@@ -52,8 +88,8 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
| GLB round-trip | Web 严格解析与 Blender 5.2 importer 双门已覆盖网格、变换、PBR、PNG/sRGB、sampler、morph、双骨骼 skin 和动画帧 | 新增相机/灯光、插值和材质扩展时继续扩大独立摘要 |
| `.blend` 保存 | 场景状态、modifier、Mesh Edit、材质/UV、动画/约束和对象层级命令均写 Main历史、原子保存、快照和日志重放可恢复 | skin/shape-key decimate 保持结构化阻断 |
| 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/结构化阻断 |
| 非 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/阻断 |
| 未声明能力扩展 | 四项协议/安全门均已细分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 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 的网络式渐进流送仍属于后续性能扩展 |
| Decimate 全对标 | Collapse 的当前网格集合已覆盖,错误路径结构化 | Un-Subdivide、Dissolve、所有 delimiter/权重/对称组合及大模型性能矩阵未完成 |
@@ -172,8 +208,8 @@ Sculpt、Geometry Nodes/Simulation、Shader Node 图和 NLA 的详细任务分
`docs/UNDECLARED_CAPABILITIES_EXECUTION_PLAN.md`。当前四项均进入
`in_progress`:四项都已超过纯协议阶段,但只开放经过 native/存储闭环验证的有限切片:
Sculpt Mask/Face Set 与有界 Main stroke、GN 外部资源门与 deterministic cache 存储、七节点
Shader Main 写回(含有限 Math常量 PBR 映射、单对象 Action Clip NLA Main/Depsgraph reverse/repeat 求值。详细完成项和 planned 项见专项计划;
这些正例不扩大到 PBVH Sculpt、任意 GN/Simulation、任意 Shader Node/Web compiler 或完整 NLA。
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、完整色彩/sampler/WebGPU 或完整 NLA。
Blender 5.2 全域功能矩阵、浏览器/服务端边界和 N-015 至 N-026 后续顺序见
`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md`
@@ -184,10 +220,16 @@ save/reopen、真实 legacy evaluated mesh、PointCloud/Curves 属性和 WNM 二
主线程与 OffscreenCanvas 消费同一 chunksGLB 只接受真实 evaluated meshUSD 提供
`UsdGeomMesh``UsdGeomPoints``UsdGeomBasisCurves` 的机器可读映射/损失。
VDB 已改为 desktop/server OpenVDB -> NanoVDB、浏览器分块读取 + WebGPU。真实资源 catalog、
desktop converter 和协议/range 基础件已接通,但 server job、renderer、材质、保存重开和 golden 仍阻断handle-local 专用 gizmo
已接通,新字体导入等剩余项按 `docs/status/N-015.md`
desktop/server converter、HTTP 断点续传、OPFS 原子绑定、Float32 WebGPU 双生产视口和 Main
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` 推进。
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
`physical-v1` 已完成扩展 Principled 参数的 Main 写回/保存重开、双后端
@@ -202,11 +244,44 @@ HDR/EXR、多 tile 采样、Offscreen context-loss、真实 WebGPU renderer、
### N-026 当前发布实况
`docs/status/release-evidence.json` 已生成 13 条成功 command/output/artifact-hash 记录;其中
VDB 边界记录没有设置发布 true 字段,只证明 Phase 0 协议门。
`test:release-evidence` 计算为 15 个缺失并保持 `BLOCKED`。其中 3 个是实际证据字段
`performance.geometry10M``performance.longMedia``faults.oom`,其余 12 个来自仍为
`BLOCKED`N-016 至 N-026 family。浏览器范围仅为 Chromium。
`docs/status/release-evidence.json` 已生成 17 条成功 command/output/artifact-hash 记录;其中
VDB 专项记录覆盖真实 converter、server job、HTTP/OPFS 和有界 WebGPU 当前切片,独立 OOM
记录覆盖 WASM/Main、OPFS staging、GPU 资源组和 NanoVDB resident/page-tableM8 后续专项已补齐
自动分页、64 MiB sparse、联合重开和 density 三轴 golden但不改写已冻结 V1 release record。
发布账本现已拆分 `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. 当前验收命令
@@ -219,6 +294,7 @@ node tools/web/run-web-engine-smoke.mjs
npm --prefix web run typecheck
npm --prefix web run lint
npm --prefix web test
npm --prefix web run test:engine-manifest-v2
npm --prefix web run test:golden
npm --prefix web run test:frame-evaluation
npm --prefix web run test:pose-constraint-goldens
@@ -236,6 +312,8 @@ npm --prefix web run test:capability-gates
npm --prefix web run test:simulation-cache
npm --prefix web run test:release-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:device-loss
npm --prefix web run test:texture-4k-performance
@@ -245,6 +323,15 @@ npm --prefix web run test:browser
npm --prefix web run release:offline
npm --prefix web run 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 -- -g "N-015|non-mesh"
```

View File

@@ -236,21 +236,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-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-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-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-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-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-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-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-B2 | 细粒度 create/remove/move/resize/active 命令 | planned | UI 前先扩命令和 revision 回归 |
| N-014-C1 | 正 scale Action Clip native frame 求值 | 已完成当前子集 | authoritative Main bytes 进入 Blender depsgraphframe 5 矩阵回归通过 |
| 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 | 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-C2b1 | Action Clip repeat 与跨周期 frame seek | 已完成有限切片 | `repeat=2`frame 5/14 native 矩阵一致 |
| N-014-C2 | blend overlap、骨骼、约束顺序、transition/meta | planned | 未声明族继续 `NLA_*` 结构化阻断 |
@@ -274,10 +275,10 @@ N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、
| GN/Simulation 切片 | 交付与验收 | 状态 |
| --- | --- | --- |
| 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-B1b | Set Position、Math/Compare/Selection field 及 point/edge/face/corner 域转换 | planned,依赖 B1a |
| N-012-B1c | Join/Separate/Realize Instances 和 instance stable ID/transform | planned,依赖 B1b |
| N-012-B1d | Store/Named Attribute 生命周期、anonymous attribute 泄漏和 deterministic graph hash | planned,依赖 B1c |
| 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 域转换 | 部分完成 | Set Position/Math/Compare 常量闭包和 M10-04 七域预算已完成Selection field 与真实跨域转换仍 planned |
| 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 | 部分完成 | constant point Float Store Named Attribute 和有界回执已通过Named/anonymous attribute 完整生命周期仍 planned |
| 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-D3 | 浏览器 bake start/cancel/commit 原子事务;中断时不发布半成品 manifest | planned依赖 D2 |
@@ -285,11 +286,11 @@ N-011 -> N-012 -> N-013 -> N-014 执行。任一任务的正例、阻断例、
| 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-B2c | Texture Coordinate -> Mapping -> Image TextureUV 名称/sampler/色彩空间闭环 | planned依赖 B2b |
| 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-D1 | 可证明 PBR 闭包的纹理/常量完整映射;其余保持 `SHADER_GRAPH_UNMAPPABLE` | plannedC1a 已覆盖常量子集 |
| N-013-D2 | 桌面 Blender GLB 再导入,比较 factor、节点摘要、图片、sampler 与渲染摘要 | planned依赖 D1 |
@@ -315,4 +316,5 @@ npm --prefix web run typecheck
WEB_TEST_PORT=5200 npm --prefix web run test:capability-gates
npm --prefix web run test:simulation-cache
npm --prefix web run test:authoring-roundtrip
npm --prefix web run test:nla-evaluation-golden
```

View File

@@ -1,6 +1,6 @@
# VDB -> NanoVDB -> WebGPU 实施方案
更新时间2026-08-13
更新时间2026-08-14
## 1. 范围与完成定义
@@ -16,7 +16,7 @@ OpenVDB 读取 `.vdb`,转换为 NanoVDB浏览器只读取受校验的 `.nvd
- density/temperature/color/emission/velocity 的 Blender 材质语义;
- `.blend` 保存、转换产物绑定、重开和资源丢失诊断;
- 与 Blender 5.2 桌面图像/数值 golden 一致;
- 大体积、损坏输入、OOM、取消和 Chromium WebGPU 性能门。
- 大体积、损坏输入、取消和 Chromium WebGPU 性能门resident/page-table 确定性 OOM 门已完成
只有上述链路均有真实资源与自动测试证据时VDB 才能从 `BLOCKED` 转为可发布能力。
@@ -181,12 +181,15 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
点比较值与 active/inactive 状态。状态:`in_progress`Float32 CPU/WGSL 与 native sample 已逐点
一致Float16/Vec 仍保持阻断。
- `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 和深度合成。
状态:`in_progress`;有界正交 bounds、front-to-back integration、early exit 和确定性 96x96
Chromium 图像已完成,生产相机深度合成待接入。
- `VDB-034`:接入主线程/Offscreen renderer 的相同 volume scene deltadevice loss 后重建已
验证页面。`BLOCKED`
- `VDB-034`:接入主线程/Offscreen renderer 的相同 volume scene delta状态:`done_current_scope`
双生产视口已消费相同 hash-bound asset 并输出非空像素。生产 Offscreen WebGPU device-loss
自动重建和 demand-page 恢复继续由故障门阻断。
### Phase 4材质、Main 与导出
@@ -196,9 +199,9 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
- `VDB-041`density/temperature/color/emission/anisotropy 的 Main writer、undo/redo 和原子事务。
状态:`in_progress`Volume source/display/interpolation/step/velocity Main 写回、undo/redo、保存重开
已完成Principled Volume 完整节点事务仍阻断。
- `VDB-042`:保存 `.blend` 与 Web asset binding清空 Worker 后重开并重新流送。`BLOCKED`
状态:`in_progress`Main Volume 保存重开与 OPFS asset binding/Worker 重开分别通过,尚待生产视口
联合重开闭环
- `VDB-042`:保存 `.blend` 与 Web asset binding清空 Worker 后重开并重新流送。
状态:`done_current_scope`Main VolumeOPFS asset bindingWorker 重建与主线程/Offscreen
两个生产视口已在同一 revision/hash 闭环联合重开。
- `VDB-043`GLB 明确报告 Volume 无核心映射USD 仅在 desktop USD/OpenVDB 路径真实可用时
写入 field asset不把 bounds proxy 当体积导出。`BLOCKED`
@@ -206,14 +209,19 @@ transmittance termination 和 NaN 防护。shader 不能通过越界 buffer read
- `VDB-050`desktop OpenVDB -> NanoVDB grid count/name/type/transform/bounds/value sample golden。
- `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 和取消门。
状态:`in_progress`64 MiB 实际 range、4 MiB 峰值工作集与中途取消已通过512 MiB/1 GiB
仍按设备和 CI 预算保持阻断。
- `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-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. 验收命令与停止条件
@@ -234,7 +242,8 @@ npm --prefix web run test:vdb-faults
阶段停止条件:
- 新增资源若无许可证/来源/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`
- 仅能显示 box/point proxy体渲染仍为 `BLOCKED`
- 未验证 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.

57
docs/status/M9-14.md Normal file
View File

@@ -0,0 +1,57 @@
# M9-14 Status
status: done
task: Curve、Grease Pencil、Paint 三编辑域故障恢复闭环
updated: 2026-08-16 America/New_York
## Scope
`web/protocol/editing-domain-recovery.ts` freezes one schema-v1 evidence shape for
the three domains. Each report must prove Main identity, Worker generation, OOM
fault mapping/cleanup, WebGL2 resource release/reinitialization, and a visible
small-scene recovery. Save/reopen may reset the logical Main revision; the
content identity SHA-256 remains the authoritative recovery check.
## Evidence
- Curve fixture: `tests/files/web/nonmesh_scene.blend`, SHA-256
`ae8ef85d606aa120ce6b7611fc03407fd80bb60311e94a6ef73f6384d0b6c8b4`.
- Grease Pencil fixture: `tests/files/web/modifier_grease_pencil_scene.blend`, SHA-256
`3a8525077807f9178dac3a5ba84b524e5f8e80874b15c54e2a6658ecb80315a3`.
- Paint fixture: `tests/files/web/attribute_scene.blend`, SHA-256
`12fa75bb79f8c38e660d3d2a8fc9cc16dd3df4fc9208b0e2ad94fb1f8aa68f71`.
- `WEB_TEST_PORT=5435 npm --prefix web run test:editing-domain-recovery`: unit 3/3 and
Chromium 1/1. All three reports returned `RECOVERED` for Worker restart, OOM,
GPU release and small-scene stages.
- Full unit suite: 127/127 tests passed.
- `npm --prefix web run typecheck`: passed.
- `npm --prefix web run lint`: passed.
- `npm --prefix web run build`: passed, Vite production bundle transformed 73 modules.
- `git diff --check`: passed.
## Implementation hashes
- Protocol: `42ab53dde3161ec31ca7de420046a2c06f58408b3ac4919c9904e02d875af258`.
- Browser runner: `2bb9805795fc2104e5443207a08951ac7684e67913064d6795c9d165b0c54c7e`.
- Unit test: `ca565149e7769b40f59e865e42d2dfb0f4ecb2a4655371004678c14536ec9ef6`.
- Chromium test: `7ab3421d1b44eb78d13525f67a26f1f6ed20816ae9b43c010c9ed3ebdf32c09a`.
- Golden manifest: `f548052f39f6259f30b0569528160512aad955e61e8e8559e0d553c30d5619f9`.
## Recovery boundaries
- Worker restart uses a fresh `WebEngineClient`/Worker and reopens the saved bytes;
stable domain identity is required, while revision reset on a newly serialized
`.blend` is accepted and recorded.
- OOM uses the token-isolated `GPU_GEOMETRY_UPLOAD` fault point. A tracked texture
lease is released before close; the matching error is
`GPU_GEOMETRY_BUDGET_EXCEEDED`, with zero live temporary resources.
- GPU release disposes geometry, material and renderer once, then creates a fresh
WebGL2 renderer and requires non-zero pixels for the same domain probe.
- Small-scene recovery requires the reopened data IDs, identity hash, object count
and visible pixels to match the committed baseline.
## Rollback
Remove the M9-14 protocol/runner/tests/golden and package script together. Existing
Curve, Grease Pencil and Paint writers remain unchanged; no persisted schema or
release metadata migration is required.

View File

@@ -7,14 +7,46 @@
本轮已交付版本化 `GeometryNodeGraphIR`、node/socket/link/domain 校验、支持节点白名单、
循环依赖和自递归 group 检测。Object/Collection/Image Info 资源现在按 SceneIR stable ID、
资源类型、owner cycle 和 linked/missing/corrupt 沙箱校验。合法图仍只通过协议门native
lazy-function evaluator 未启用时返回 `CAPABILITY_MISSING`
资源类型、owner cycle 和 linked/missing/corrupt 沙箱校验。保存于 Blender Main 的 allowlist
图已启用有界 native evaluator;任意图写回仍返回 `CAPABILITY_MISSING`
`SimulationCacheManifestIR` 已绑定 graph/source blend/input hash、Blender 5.2、完整帧范围、
连续 byte range、总 payload 和逐帧 SHA-256。StorageWorker schema 6 使用 OPFS 内容寻址资产
`simulation_manifest` 索引,写入前核对当前 committed blendWorker 重启后重新逐帧验证;
缺失、版本不符和损坏均结构化阻断。
M10-01 已把真实 Blender Main 图拓扑、socket 默认值、link 和稳定 node/socket ID 发布到
SceneIRM10-02 将 16 项 allowlist 固定为 schema 1。M10-03 用 11 个 Blender 5.2 desktop
fixture 覆盖全部 16 项节点,并在 Node full-Main WASM 与 Chromium Worker 打开、求值、保存、
重开路径逐项比较 topology、position、bounds 和 point float attribute。当前误差为 0门限为
position max `1e-5`/RMS `1e-6`、attribute `1e-6`、bounds `1e-5`。真实 Simulation Zone 节点
仍返回 `GN_NODE_UNSUPPORTED` 并保留原图、graph hash 和 Main revision。
仍未声明:浏览器生成 bake、WASM Simulation Zone 求值、cache frame seek 接入 GN modifier、
任意 lazy-function/field/instance graph。验收`npm --prefix web run test:simulation-cache`
`npm --prefix web run test:capability-gates`
M10-04 已冻结 field materialization schema 1POINT/EDGE/FACE/CORNER/CURVE/INSTANCE/LAYER
分别具有元素上限,单批最多 64 fields、32 次 domain conversion、4,000,000 个目标元素和
64 MiB单 field 超过 65,536 scalar 时禁止 JSON transport。Depsgraph 的真实 point Float
attribute 回执携带 schema、domain cardinality、元素/scalar/byte 数并与 payload 交叉校验;
基数漂移、字节漂移、隐藏 JSON 数组和未知 domain 均 fail-closed。
M10-05 将 `SimulationCacheManifestIR` 升为 schema 2加入 committed `sourceRevision` 和可由
graph ID/hash、source blend hash、source revision、input hash、Blender 版本及 frame range
复算的完整 `revisionHash`。cache key 不再截取 hash 前缀,而是 `sim2-` 加完整 revision hash
Storage Worker 的 put/read/frame/list 均在 project lock 内核对当前 committed revision/hash
revision 只保留在存储中、不进入当前列表且显式读取返回 `SIMULATION_CACHE_REVISION_MISMATCH`
manifest/frame 未声明字段、legacy schema、graph/source/input/range 漂移均 fail-closed。
`SimulationCacheManifestIR` 已绑定 graph/source blend/input/revision hash、Blender 5.2、完整帧范围、
连续 byte range、总 payload 和逐帧 SHA-256。StorageWorker schema 7 使用 OPFS 内容寻址资产、
`simulation_manifest` 索引和 `simulation_quarantine` 隔离 store写入前核对当前 committed blend。
每个 Worker 生命周期必须先完整复验 cache 才允许逐帧播放;播放/写入支持 AbortSignal取消不发布
manifestLRU 按 `lastAccessAt/createdAt/cacheKey` 确定性淘汰并保护 active playbackWorker 重启
重新验证manifest 或 payload 损坏会从可播放索引移入 quarantine列表仍返回其他有效 cache。
M10-15 为 GN 和 Simulation 分别增加隔离 Chromium Worker 门GN 以 10 次 512-node
parse/validate、超 node budget 和 dependency cycle 覆盖性能/OOM-prevention/恶意输入Simulation
以 64 帧逐帧 hash、16 GiB+1 manifest 和未声明字段覆盖同三类门。两域均在失败后由同 Worker
完成小输入恢复。
仍未声明:任意 field/domain 实际求值、完整 lazy-function runtime、图写回、浏览器生成 bake、
WASM Simulation Zone 求值、cache frame seek 接入 GN modifier 和任意实例图。验收:
`npm --prefix web run test:geometry-node-evaluator-golden`
`npm --prefix web run test:geometry-node-field-budget``npm --prefix web run
test:simulation-cache-identity``npm --prefix web run test:simulation-cache`
`npm --prefix web run test:simulation-cache-performance``npm --prefix web run
test:simulation-cache-lifecycle``npm --prefix web run test:capability-gates`
`npm --prefix web run test:m10-domain-gates`

View File

@@ -1,6 +1,6 @@
# N-013 Shader Node 图
状态:`in_progress七节点 Main + 常量 PBR 映射子集)`
状态:`in_progress七节点 Main + 有界 WebGL2 compiler 子集)`
详细分解、ShaderIR、typed socket 校验、Main 写回、Web 编译、GLB 映射和验收命令见
[`docs/UNDECLARED_CAPABILITIES_EXECUTION_PLAN.md`](../UNDECLARED_CAPABILITIES_EXECUTION_PLAN.md)。
@@ -16,6 +16,19 @@ operation 保存重开其他运算继续结构化阻断。RGB/Value 到 Princ
Roughness、Metallic 的有限常量链接现可确定性映射为 glTF PBR factor缺值、越界、
重复输入和未知链接继续返回 `SHADER_GRAPH_UNMAPPABLE`。Mix、Mapping、Texture
Coordinate、Bump 及任意节点仍返回 `SHADER_NODE_UNSUPPORTED`。node properties 仅开放
Math operation;受限 Web 编译、完整色彩管理、完整 GLB 图映射和桌面渲染 golden 仍为 planned。
验收:`npm --prefix web run test:authoring-roundtrip``npm --prefix web run test:capability-gates`
Math operation。M10-07 新增 schema 1 有界 compiler只把 RGB/Value 常量、六项 Math、Image
Texture、Normal Map、Principled 和 Output 编译为 `WEBGL2_THREE_PHYSICAL` reportNative reader
发布 graph SHA-256Worker 在 Main transaction 前编译并失败关闭,主线程和 Offscreen 复用同一
`createPBRMaterial` 入口。M10-08 为 report 增加 graph/texture/color-space/backend 绑定的
`compileKey`M10-09 由 `PBRMaterialPipeline` 保留上一份可用材质并记录失败报告。节点/链接/
深度/纹理/标识预算固定M10-10 将任意未知节点名称归一化排序后返回稳定
`PBR-012/ARBITRARY_SHADER` capability block未知节点不被删除或静默降级。
M10-15 在独立 Chromium Worker 中连续完成 100 次有界 compile、129-node 超预算阻断、恶意
cycle 阻断和同会话小图恢复performance/OOM-prevention/malicious-input 三门均使用现有 compiler
不以 mock 成功替代材质编译。
完整色彩管理、sampler/alpha/tangent、WebGPU、完整 GLB 图映射和桌面渲染 golden 仍为 planned。验收
`npm --prefix web run test:shader-compile``npm --prefix web run test:authoring-roundtrip`
`npm --prefix web run test:capability-gates``npm --prefix web run test:m10-domain-gates`
以及 Shader GLB mapping E2E。

View File

@@ -13,8 +13,21 @@ Blender `NLASTRIP_FLAG_REVERSE`reader 可恢复该标志;反向正例得到
`[0.555556, 1.111111, 1.666667]`
同一正例已扩为 `repeat=2`frame 5 与第二周期 frame 14 的 native Depsgraph 矩阵一致。
M10-11 另以桌面 Blender 5.2 生成只读 fixture同一 Track 包含 scale=2 的正向 Clip 和
reverse + repeat=2 的 Clip。桌面、Node WASM 与生产 Chromium Worker 在 12 个边界/周期帧上
逐矩阵比较,最大误差为 0每帧求值前后的 Track/Strip/Action JSON 完全一致。本切片不调用
`setNLAStack`,不提前关闭细粒度 operator 工作。
当前只支持单对象 Action Clip、正 scale、Replace/Add/Multiply/Combine 和有限外插。
Transition/Meta/Sound、Animated Time、Drivers、细粒度拖动/resize 命令、NLA UI、
M10-12 仅新增 `moveNLAStrip`,以 revision gate 和单次 Main transaction 保留时长移动现有 Clip
并已通过 undo/redo 与保存重开。Transition/Meta/Sound、Animated Time、Drivers、其余
create/remove/resize/active 命令、NLA UI、
骨骼/约束混合矩阵及 GLB 展开仍为 planned/结构化阻断。
验收:`npm --prefix web run test:authoring-roundtrip``npm --prefix web run test:capability-gates`
M10-15 冻结 4,096 tracks、每 track 16,384 strips、总 65,536 strips 及 ID/name/reason 字节预算,
拒绝未声明字段。隔离 Chromium Worker 对 20 次 256-strip stack、超 track budget、恶意字段和
同会话小 stack 恢复分别建立 performance/OOM-prevention/malicious-input 门。
验收:`npm --prefix web run test:nla-evaluation-golden``npm --prefix web run test:nla-operator`
`npm --prefix web run test:authoring-roundtrip`
`npm --prefix web run test:capability-gates``npm --prefix web run test:m10-domain-gates`

View File

@@ -1,9 +1,9 @@
# N-015 非 Mesh 几何数据块
状态:`BLOCKED / in_progress`VDB core 已覆盖 server、OPFS、Float32 WebGPU 与 Main 属性重开;
生产视口、高级材质、分页和发布 golden 仍阻断)
状态:`BLOCKED / in_progress`M8 自动 demand paging、联合重开、64 MiB sparse 门和三轴 density
golden 已完成;深度合成、完整 Volume 材质/GLB/USD 与 512 MiB/1 GiB 联合故障矩阵仍阻断)
更新时间2026-08-14
更新时间2026-08-16
## 当前声明
@@ -14,8 +14,10 @@ topology editor。
Volume 采用“桌面/服务端 OpenVDB 转 NanoVDB浏览器分块读取并以 WebGPU 渲染”的固定架构。
浏览器不直接解码 OpenVDB当前已完成真实资源 catalog、独立 OpenVDB 13 -> NanoVDB 32 desktop
converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开、Float32 CPU/WGSL 树遍历、
有界体积积分和 Volume Main 属性保存重开。主线程/Offscreen 生产视口、高级 grid 材质、GPU
分页和发布 golden 尚未完成,因此 N-015 整体保持 `BLOCKED`
有界体积积分、主线程/Offscreen 生产视口、显式 hash 校验 GPU 分页/LRU、有限 grid 材质、
Volume Main/asset binding 联合重开、自动缺页重绘、64 MiB sparse 门和 OpenVDB/WebGPU 三轴
density golden。深度合成、完整 Volume 材质/GLB/USD 与更大真实 bundle 联合故障矩阵尚未完成,
因此 N-015 整体保持 `BLOCKED`
| 数据族 | Reader | Web 预览 | 当前门 |
| --- | --- | --- | --- |
@@ -25,7 +27,7 @@ converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开
| Metaball | 元素类型、位置、半径、尺度 | bounded sphere proxydepsgraph implicit mesh | `READY` |
| PointCloud | position/radius/POINT 属性 | WNM 分块 Points | `READY`(空数据为 `summary-only` |
| Curves/Hair | position/radius/curve/POINT 属性 | WNM 分块 Points/curve line | `READY`(缺数据阻断) |
| Volume | path/grid metadata + VDB/NanoVDB 转换契约 | WebGPU core 可渲染真实 Float32 density生产视口未接入 | `BLOCKED` |
| Volume | path/grid metadata + VDB/NanoVDB 转换契约 | WebGPU 可渲染真实 density/color/temperature/emission自动分页、联合重开与三轴 density golden 已接入 | `BLOCKED`(深度合成/完整材质与 IO/更大联合故障矩阵) |
## 已实现任务
@@ -100,6 +102,19 @@ converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开
active 状态一致Chromium WebGPU 有界积分输出确定性 96x96 image hash。
19. Volume source/display/interpolation/step/velocity 属性已写入 Blender Main并通过 undo/redo、
save/reopen它不等于 Principled Volume 完整节点语义。
20. GPU grid 支持初始空驻留、经 manifest chunk hash 校验的显式单页加载、页表间接寻址、确定性
LRU 替换和设备重建后按需页重放;多材质 grid 按同一 manifest `maxResidentBytes` 分配页槽,
Chromium 以 GPU 数据读取验证被触碰页保留、旧页淘汰和恢复后内容一致。
21. GPU feedback buffer、revision gate、manifest range/hash page request、同页请求合并、frame pin
LRU、渐进重绘预算、网络精确 offset 续传、Worker restart 和 device-loss 可见页回放已形成自动
demand paging 最小闭环hash 失败、stale feedback、取消和 OOM 均不污染 resident/page table。
22. 64 MiB sparse HTTP bundle 以 4 MiB page 实际 range 读取,首 page、总耗时、峰值工作集和取消
延迟均有 Chromium 证据;取消后 coordinator/consumer 归零,未将逻辑 64 MiB fixture 写入仓库。
23. 同一保存 hash 下Volume Main 属性、OPFS project revision、NanoVDB binding 和 bundle/grid bytes
在 Worker 清空后联合重开;主线程和 Offscreen 两个生产视口都从恢复 asset 输出非空像素。
24. OpenVDB 13 native `density` reader 按 `volume-wgsl-v1` 有界积分生成 X/Y/Z 三轴 64x64 RGBA8
desktop/Chromium 发布 golden主线程与 Offscreen WebGPU 对 49,152 bytes 的最大通道误差、MAE、RMS 和 alpha 覆盖
差均为 0。门限仍固定为 2/0.1/0.5/0.002,漂移返回 `NANOVDB_GOLDEN_MISMATCH`
## 后续分解
@@ -113,9 +128,9 @@ converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开
raycast、多 handle preview、handle-local gizmo 与单次 Main 写回已完成。
4. N-015-D2evaluated preview 与源控制笼分层显示、WebGPU 等价。
5. N-015-B3/D2/E1Volume真实资源、desktop/server converter、可续传 HTTP range、OPFS、
有界网络/Worker/quota/tamper/device-loss 故障门、Float32 WGSL core 和 Main Volume 属性重开已完成;
继续执行生产 GPU paging、Principled Volume grid 语义、GLB/USD loss、大 bundle/OOM 和
desktop/Chromium golden未完成项保持阻断
有界网络/Worker/quota/tamper/device-loss 故障门、Float32 WGSL core、自动 GPU paging/LRU、
resident/page-table OOM、Main/binding/双视口联合重开、64 MiB sparse 和三轴 density golden 已完成;
继续执行场景深度合成、完整 Principled Volume/GLB/USD loss 与 512 MiB/1 GiB 联合故障矩阵
6. N-015-E2100k/1M WASM/GPU 内存、Worker restart 和 Chromium OPFS quotaChromium
64 KiB 真实 quota、失败后旧 revision 保持、Worker restart 恢复已通过。后续浏览器验收
仅以 Chromium 为基线,不配置 Firefox/WebKit。
@@ -127,7 +142,8 @@ converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开
- Metaball sphere proxy 不得用于导出或保存为求值曲面。
- Volume/VDB 不得生成占位几何;协议 fixture 不得作为真实 decoder/renderer 证据。
- 浏览器构建不得通过开启 `WITH_OPENVDB` 绕过 desktop/server 转换边界。
- 生产视口、GPU paging、完整材质、故障门和发布 golden 未齐前VDB 与 N-015 保持 `BLOCKED`
- 场景深度合成、完整 Volume 材质/IO 和更大真实 bundle 联合故障矩阵未齐前VDB 与 N-015 保持
`BLOCKED`M8 完成不能自动把 N-015 全域标为 `COMPLETE`
- Metaball sphere proxy 不得进入 GLBGLB 只能消费 depsgraph evaluated mesh。
- 任意新增 Main authoring 命令在 save/reopen 与 undo/redo 完成前不得开放 UI。
@@ -152,6 +168,15 @@ npm --prefix web run test:nonmesh-interaction
npm --prefix web run test:vdb
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-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:nanovdb-sparse-performance
npm --prefix web run test:nanovdb-volume-roundtrip
npm --prefix web run test:nanovdb-render-golden
node tools/vdb/generate-volume-golden.mjs --check
WEB_TEST_PORT=5349 npm --prefix web run test:e2e -- -g "real OPFS quota|reports quota exhaustion"
WEB_TEST_PORT=5201 npm --prefix web run test:e2e -- -g "N-015|non-mesh"
```

View File

@@ -1,7 +1,8 @@
# N-016 Grease Pencil
状态:`BLOCKED`(协议、有限 reader/Main transaction、当前帧及相邻帧 onion preview 已落地;
完整 2D canvas/Dope 编辑、modifier 语义和桌面 golden 仍阻断)
状态:`BLOCKED`(协议、有限 reader/Main transaction、当前帧及相邻帧 onion preview
current-drawing marquee 和有限 2D point canvas 已落地;完整 2D stroke/segment/Dope 编辑、
modifier 语义和桌面 golden 仍阻断)
## 已验证切片
@@ -22,8 +23,8 @@
drawing渲染真实 3D stroke/闭环Chromium 主线程与 OffscreenCanvas 均通过非空像素门。
6. N-016-Deditor 部分editor context schema 绑定 data/layer/frame、onion 开关、stroke/
point selection 和 revision拒绝 stale/重复/超 1M selectionProperties 面板已连接真实 Main
layer create/remove、当前 frame insert/remove 和整帧 clear transaction。完整 2D stroke/point
画布选择和 dope sheet integration 仍阻断。
layer create/remove、当前 frame insert/remove 和整帧 clear transaction。完整 2D stroke/segment
lasso 和 dope sheet integration 仍阻断。
7. N-016-D点编辑部分Properties 面板可在当前 layer/frame 选择实际 stroke/point执行
有界 X 轴平移;协议先校验 point identity、revision、有限坐标和 1M 点预算,再保留 radius、
opacity、vertex color、cyclic、material index 并合并为一个 `setGreasePencilStrokes` Main
@@ -40,11 +41,29 @@
11. N-016-D重启部分真实 Grease Pencil fixture 经首个 WebEngine Worker 点编辑和 `.blend`
保存后,终止 Worker 并由全新 Worker 重开data/layer/drawing identity、位置、radius、opacity
均重新解析验证。
12. N-016-DM9-06 marqueeMain reader 为 drawing、stroke 和 point 发布稳定 ID并发布 Blender
active layer旧文件未记录 active layer 时只读快照确定性选择首个实际 layer不修改 Main。
纯 marquee 合同绑定 Main revision、当前 data/layer/frame/drawing、归一化矩形和 1M candidate
预算,跨 drawing、重复/伪造 ID、stale revision 和超预算均稳定失败关闭。生产 UI 只在活动、
可见且未锁定 drawing 开放框选,主线程与 OffscreenCanvas 共用同一合同;真实 `.blend` 均选中
4 个稳定 point ID 和 1 个 stroke ID选择不递增 Main revision。
13. N-016-DM9-07 shared selection`grease-pencil-selection` schema 将当前 drawing、稳定
point ID、来源和独立 selection revision 绑定为唯一状态2D point canvas、主线程 3D viewport
和 OffscreenCanvas Worker 的 point/marquee 事件都携带 base selection revision迟到结果返回
`REVISION_CONFLICT`,不修改 Main。真实 fixture 从 2D 选择 1 点后由 3D 框选 4 点revision
`0 -> 1 -> 2`两条生产后端回执与画布高亮一致Main revision 保持不变。
14. N-016-DM9-08 layer/frame reorder`grease-pencil-reorder` schema 将 layer 方向移动和
drawing frame 移动绑定到稳定 data/layer/drawing ID 与 Main base revisionstale revision、
边界 no-op、伪造 drawing、已占用 target frame 和正负 1,000,000 之外帧号均在 Main 前失败关闭。
生产 Properties UI 的 layer 上下移动与 frame target 移动各只提交一次 Main transaction真实
fixture 经 undo/redo、OPFS save、close/recover 后保持 Blender 5.2 desktop golden 的 layer 顺序、
frame 12 和原 drawing identity。完整 group hierarchy、frame duplicate 与 Dope Sheet 拖拽仍阻断。
## 仍然阻断
- N-016-Cmaterial datablock 事务、onion fade/range 完整语义和完整 Grease Pencil modifier reader。
- N-016-D完整 2D canvas/marquee 和完整 timeline/dope 编辑3D current-frame 点 raycast、
- N-016-D完整 2D canvas 的 stroke/segment 选择、套索和完整 timeline/dope 编辑仍阻断;有限
2D point canvas 与 3D 共享 selection revision、3D current-drawing point marquee、点 raycast、
多点高亮、连续 preview、单次 gizmo commit、有界 drawing frame 导航和 worker restart 已完成。
- N-016-Edesktop drawing hash、Chromium 像素 golden、GLB/USD loss report 和 OPFS。
@@ -54,6 +73,9 @@
npm --prefix web run typecheck
npm --prefix web run lint
npm --prefix web run test:grease-pencil
WEB_TEST_PORT=5404 npm --prefix web run test:grease-pencil-marquee
WEB_TEST_PORT=5411 npm --prefix web run test:grease-pencil-selection
WEB_TEST_PORT=5416 npm --prefix web run test:grease-pencil-reorder
WEB_TEST_PORT=5205 npm --prefix web run test:grease-pencil-editor
WEB_TEST_PORT=5202 npm --prefix web run test:e2e -- --grep "N-016 Grease Pencil"
```

View File

@@ -1,7 +1,9 @@
# N-017 Paint 与权重
状态:`BLOCKED`stroke/patch schema、真实 Three raycast 命中、选择/遮罩门和 Main 顶点色/权重
transaction 已落地PBVH brush、texture paint 和 GPU/image 生命周期未实现)
状态:`BLOCKED`stroke/patch schema、真实 Three raycast 命中、GPU depth 可见性、选择/遮罩门、
单 undo 分块 pointer session、Main 顶点色/权重、浏览器 packed/UDIM 原子资产绑定和
PBVH WASM 入口显式能力门已落地;
PBVH brush、Blender Main image tile 写回、完整色彩转换和 GPU/image 生命周期仍未实现)
## 已验证切片
@@ -12,9 +14,11 @@ transaction 已落地PBVH brush、texture paint 和 GPU/image 生命周期未
tile 256 MiB非法 barycentric、pressure、color、weight 和非有限数会结构化拒绝。
4. N-017-B部分`setVertexColors` 在单用户 Mesh 上创建/转换 `POINT``CORNER`
`FLOAT_COLOR` 属性,并更新 active color`setVertexWeights` 在真实 Object/Mesh 上创建顶点组、
写入/移除 `MDeformWeight`,支持逐顶点 normalize
5. 顶点色和权重都已通过 Main undo/redo 与 `.blend` save/reopen`mirror:true` 在没有已验证
对称拓扑映射时返回 `CAPABILITY_MISSING`,不会伪造镜像结果。
写入/移除 `MDeformWeight`,支持逐顶点 normalize、最多 32 个 influence 的 limit以及
基于局部坐标 reciprocal map 的 mirror。
5. 顶点色和权重都已通过 Main undo/redo 与 `.blend` save/reopen镜像在没有已验证对称映射时
返回 `CAPABILITY_MISSING`不会伪造结果。limit/normalize/mirror 的组合已与 Blender 5.2
desktop golden 逐顶点比较。
6. `paintHitFromIntersection` 消费真实 Three `Raycaster` intersection按 indexed/non-indexed
triangle 解析顶点,输出对象/data stable ID、source face、局部 barycentric、世界法线、
插值 UV 与 pressure测试使用真实 BufferGeometry 射线,不注入伪命中。
@@ -24,7 +28,7 @@ transaction 已落地PBVH brush、texture paint 和 GPU/image 生命周期未
8. N-017-A/C协议边界有界 CPU brush 对候选顶点执行 smoothstep falloff、遮挡标记过滤和
front-face 法线门;它不宣称 PBVH 加速或桌面 brush 等价。`UdimTilePatchIR` 约束 1001-1999
tile、RGBA8、色彩空间、尺寸/256 MiB 预算、revision、base/result SHA-256并在内存中验证
原子 range patch;尚未绑定 Blender packed image/UDIM Main 写回与保存
原子 range patch。
9. N-017-A空间查询部分uniform-grid spatial index 在 1M vertex/1M cell 预算内按 brush AABB
粗筛候选再执行球半径、front-face、falloff 精筛depth-gated 查询必须显式提供已验证可见
vertex ID 集合,缺失或重复会拒绝。该结构减少全量扫描,但不是 Blender PBVH。
@@ -34,14 +38,35 @@ transaction 已落地PBVH brush、texture paint 和 GPU/image 生命周期未
11. Properties paint 面板可对当前真实 VERT selection 执行 selection-masked `Blend Color`
`Blend Weight`,每次仅提交一个 Main transaction。Blender 5.2 reader 从 Mesh
`vertex_group_names` 恢复顶点组摘要及 skin weight 名称;新增组经 undo/redo、保存重开后保持。
12. M9-09主线程和 Offscreen Worker 使用同一 WebGL2 RGBA depth pass/readback
以 Main revision 和稳定顶点索引返回真实可见集;遮挡 fixture 的两后端结果一致。
13. M9-10WebEngine Worker 可序列化接收有界 color/weight chunks一次 pointer
session 仅在 commit 时对 Main 发一个合并命令;真实 Main 历史只产生一个 undo step。
14. M9-11Storage Worker 将 packed/UDIM PNG 解码为 RGBA8复验 dirty range 的
base/result pixel hash新 PNG 经 OPFS content-addressed 临时写、回读验证后,
才以 IndexedDB 单事务切换 tile binding。中途故障保持旧 binding/旧资产可读,
packed 与 UDIM 均已通过 Worker restart 回读。
15. M9-12`WeightPaintOptionsIR` 固定 normalize、limit、mirror axis/tolerance 和 1M
patch budgetWorker 拒绝重复/越界/非法组合native 在 Main 内先写入 patch再做
mirror、limit、normalize。镜像 map 使用 mesh 局部坐标,要求每个顶点都有 reciprocal
counterpart且大于 100k 顶点时结构化预算阻断。`rigged_shape_scene.blend` 的四步
goldeninitial/normalize/limit-normalize/mirror与 WASM snapshot、save/reopen 逐项一致。
16. M9-13按 Blender 5.2 `DNA_brush_enums.h` 冻结 32 个 Sculpt、4 个 Vertex Color、
4 个 Weight 和 6 个 Texture 活跃 brush。生产 WebEngine Worker 探测
`_web_engine_apply_pbvh_stroke`;当前 stable/single/pthread 均无该 WASM 入口,因此 46 项
全部返回 `N-017/BLOCKED/PAINT_PBVH_UNAVAILABLE`。即使未来出现入口session context 和
逐 brush desktop/WASM golden 未就绪时仍分别失败关闭;查询不会修改 Main revision、handle
或 WASM allocated bytes畸形字段返回完整标准 `ErrorReport`
## 仍然阻断
- N-017-A真实 PBVH、由 viewport 深度缓冲生成可见 vertex 集合,以及 brush falloff 桌面
golden基础 Mesh/UV hit、uniform-grid 候选查询可见性硬门已完成。
- N-017-Blimit/clean、已验证拓扑映射上的 mirror、桌面 brush/falloff 对照
- N-017-CBlender packed/UDIM tile Main transaction、dirty tile、色彩转换和原子保存当前
只有内容哈希绑定的浏览器内存 patch 边界
- N-017-A真实 PBVH 与 brush falloff 桌面 golden基础 Mesh/UV hit、uniform-grid 候选
查询可见性硬门和双后端 GPU depth 可见集已完成。当前不存在 PBVH WASM writer完整
brush inventory 由 `PAINT_PBVH_UNAVAILABLE` 明确阻断,不会把 bounded helper 当作成功
- N-017-Bclean/高级 Blender weight-paint operator、真实 PBVH brush/falloff 与更大拓扑的
桌面对照;当前 bounded limit/normalize/verified mirror 已完成
- N-017-CBlender packed/UDIM tile Main transaction、完整色彩转换和 `.blend` 内 image
tile 保存仍阻断;浏览器 dirty tile 的 OPFS 原子 asset/binding 边界已完成。
- N-017-D/Earmature golden、seam bleed、face mask、GPU dispose、quota、坏图和桌面 UI 对照;
vertex selection 与数值 mask 门已完成,不等同于完整 Paint 面/纹理遮罩系统。
@@ -50,7 +75,9 @@ transaction 已落地PBVH brush、texture paint 和 GPU/image 生命周期未
```bash
WEB_TEST_PORT=5203 npm --prefix web run test:e2e -- --grep "N-017 paint"
npm --prefix web run test:paint-roundtrip
WEB_TEST_PORT=5422 npm --prefix web run test:texture-paint-asset
WEB_TEST_PORT=5427 npm --prefix web run test:paint-pbvh-capability
```
`test:paint-roundtrip` 覆盖 CORNER color、vertex group patch、normalize、mirror 错误门、
undo/redo 与 save/reopen。
`test:paint-roundtrip` 覆盖 CORNER color、vertex group patch、normalize、limit、verified
mirror、undo/redo 与 save/reopen`test:weight-paint-golden` 对比 Blender 5.2 desktop golden

View File

@@ -1,7 +1,7 @@
# N-018 Physics 与 Simulation
状态:`BLOCKED`family capability、settings/dependency/cache manifest、错误帧门浏览器自有
BTF1 播放会话已落地desktop bake playback、WASM solver 和 bake job 未实现)
状态:`BLOCKED`family capability、settings/dependency/cache manifest、错误帧门浏览器自有
BTF1 播放会话及逐 family solver probe 已落地desktop bake playback、WASM solver 和 bake job 未实现)
## 已验证切片
@@ -34,13 +34,19 @@ BTF1 播放会话已落地desktop bake playback、WASM solver 和 bake job
10. N-018-C持久缓存集成两帧真实 BTF1 经过 content-addressed Simulation cache 写入,终止
Storage Worker 后由新 Worker 精确 range 读取第二帧并应用到 SceneIR帧 hash、magic、offset
和最终 object translation 均验证。
11. N-018-DM10-13 probe 门):七个 family 分别检查 runtime export、初始化、线程和内存只有
四门全过才允许 `LOCAL_SOLVER`。当前生产 inventory 没有 solver adapter全部明确路由到
`DESKTOP_SERVER_BAKE`。合成正例只验证门逻辑,不声明真实 WASM solver 或 cache decoder。
12. N-018-C/DM10-14 cache 门):七类 desktop/server bake cache 均绑定 cache schema、family、
Blender 5.2、source/settings/input/cache hash、frame range、总/逐帧 byte range/hash消费前对
实际 source、payload 和逐帧 bytes 复算 SHA-256。decoder/playback capability 仍不开放。
## 仍然阻断
- N-018-A/B从真实 Main 提取完整 family settings、collection/effector/collision 依赖。
- N-018-Cdesktop bake payload 的逐 family decoder、正式 viewport UI 播放控制和 100 帧
golden浏览器自有 transform frame decoder/SceneIR 播放会话已完成但不替代 Blender bake。
- N-018-D逐 family WASM solver 初始化、线程、内存和确定性验证
- N-018-D逐 family 真实 WASM solver adapter、初始化实现和确定性 golden探测/路由门已完成
- N-018-Ebake start/cancel/commit、服务端 job、故障恢复、进度与 UI。
## 验收
@@ -49,8 +55,10 @@ BTF1 播放会话已落地desktop bake playback、WASM solver 和 bake job
WEB_TEST_PORT=5204 npm --prefix web run test:e2e -- --grep "N-018 physics"
npm --prefix web run test:simulation-cache
npm --prefix web run test:physics-main-reader
npm --prefix web run test:physics-solver-probe
npm --prefix web run test:physics-cache-family
```
本轮在 Chromium 隔离端口复验 content-addressed cache 的 Worker restart、目标帧 range read、
帧 SHA-256、BTF1 seek/play/cancel迟到结果抑制通过;没有 desktop bake family decoder 或
solver因此 desktop playback/solver/bake 状态不变,继续为 `BLOCKED`
帧 SHA-256、BTF1 seek/play/cancel迟到结果抑制和逐 family solver probe 通过;没有 desktop
bake family decoder 或真实 solver因此 desktop playback/solver/bake 状态不变,继续为 `BLOCKED`

View File

@@ -1,7 +1,8 @@
# N-019 灯光与渲染
状态:`BLOCKED`Camera/Light/World/Scene metadata、Camera/Light/World 有界 Main 写回
Three exposure/shadow 映射已落地Scene 颜色管理 writer 和渲染等价未实现)
状态:`BLOCKED`Camera/Light/World/Scene metadata、Camera/Light/World 有界 Main 写回
Three exposure/shadow 映射、一条有界 Blender reference 图像指标和 Render 故障生命周期闭环
已落地Scene 颜色管理 writer、全域渲染等价和高级后端仍未实现
## 已验证切片
@@ -24,6 +25,34 @@ Three exposure/shadow 映射已落地Scene 颜色管理 writer 和渲染等
7. N-019-B色温部分`useTemperature:true` 时,共享 PBR 适配器把 80020000 K 的有界
黑体近似归一到 6500 K 中性白并转换为线性 RGB再乘入 Light color关闭色温时原始颜色
保持不变。主线程与 Offscreen Worker 共用该实现,数值门和原生 temperature 保存重开通过。
8. M11-01 字段表CameraIR、LightIR、WorldIR 及 Scene render/color-management 共 60 个叶字段
已逐项标注 reader/writer/main viewport/Offscreen viewport 状态AST checker 保证零漏项、零重复。
当前 24 COMPLETE、17 PARTIAL、19 BLOCKED未把 metadata-only 或近似映射提升为视觉 parity。
9. M11-02 字段闭环:支持的 Camera/Light/World writer 已在同一 Chromium 任务中完成 Main edit、
revision 单调的 undo/redo、保存、全新 Worker 重开和共享 PBR 映射;水平/垂直 sensor fit、
temperature linear RGB、shadow、spot/area 参数及 World 背景均有字段级断言。
10. M11-03 资源预算:主线程与 Offscreen 共用 schema 1 light/shadow/texture plannerWebGL2
总预算为 16 lights、4 x 1024 shadow maps均包含内置保留槽纹理同时限制数量、单边、
aggregate payload 与 decoded RGBA bytes。WebGPU 只冻结 64/8 x 2048 及 device-limit 下调合同,
未把未安装的 WebGPU renderer 标为可用。
11. M11-04 有界实时 referenceBlender 5.2 Eevee 256×256 fixture 绑定 source/generator/image
SHA-256SRGB8/STRAIGHT RGBA 比较输出 MAE、RMS、P95 channel、坏像素比例、前景 IoU 和
alpha 覆盖率。主线程与 Offscreen 生产 viewport 均为同一报告MAE 2.0433、RMS 5.4490、
P95 4、坏像素 0.004776、IoU 0.98534);非空但错误构图负例稳定返回
`RENDER_REFERENCE_MISMATCH`
12. M11-05 最终渲染路由schema 1 只让 bounded Eevee 的 WebGL2以及 capability 全通过的
WebGPU进入 `WEB_LOCAL_BOUNDED`Cycles、complex Eevee、Workbench 与 CUDA/OptiX/HIP/
Metal/oneAPI 固定为 `SERVER_JOB`。默认 endpoint 未配置时返回 `SERVER_JOB_UNAVAILABLE`
未知 SceneIR render engine 返回 `PLATFORM_CAPABILITY_UNAVAILABLE`,均不产生本地等价成功。
13. M11-06 server render jobschema 1 请求绑定 source `.blend` SHA-256/byte length/revision、
Blender 5.2 build SHA-256、规范化 render settings SHA-256 和 request SHA-256成功结果
绑定同一 provenance 并公开 output MIME/byte length/output SHA-256/result SHA-256。真实
loopback server 由 Blender 5.2 headless 打开 fixture 并输出 PNGsource、settings、build
和 output 篡改均在提交或消费前结构化拒绝。
14. M11-14 Render fault lifecycle真实 Eevee `.blend` 在 Chromium WebEngine/Main 打开后生成
非空 WebGL RGBA8 输出;预取消 open 返回 `OPEN_CANCELLED` 且不替换当前 MainWorker
generation 2 重开保持 scene identity 和输出 SHA-256。纹理 aggregate budget 超限不替换
已加载资源,`GPUTextureStore.dispose()` 返回正数 byte/resource 回执并归零所有 owned texture。
## 仍然阻断
@@ -31,15 +60,25 @@ Three exposure/shadow 映射已落地Scene 颜色管理 writer 和渲染等
Blender/RNA 颜色管理 API 后才能开放。Camera writer 已完成。
- N-019-B/CAgX/Standard/Raw 的视觉等价、Area spread、Mist、DOF、
transparent sorting、probe 和高级 shadow 参数。
- N-019-DCycles/Freestyle/denoise 服务端 job 协议与结果 hash。
- N-019-Edesktop/Chromium 像素 golden、设备丢失和 1M triangles。
- N-019-B/C/EVolumeFloat32 NanoVDB WGSL 树遍历和有界 density ray integration 已有真实
Chromium 数值/图像专项门GPU page allocator、生产视口深度合成、temperature/color/emission
grid 语义和 desktop/Chromium 三视角 golden 仍阻断,不能据此声明发布级体渲染
- N-019-DM11-05 已冻结 Cycles/复杂 Eevee/硬件后端的 server-only 路由M11-06 已完成
有界 server-style submit 和 source/settings/build/output hash provenance真实远程队列、
进度、取消、Freestyle 和 denoise job 仍未完成。
- N-019-E完整 desktop/Chromium 像素 parity、设备丢失和 1M trianglesM11-04 仅关闭固定
fixture 的有界 reference 指标门,不代表全场景或全色彩管理等价
- N-019-B/C/EVolumeFloat32 NanoVDB WGSL 树遍历、有界积分、有限
temperature/color/emission grid 语义和显式 GPU resident LRU 已有真实 Chromium 数值/图像专项门;
自动缺页重绘、生产视口深度合成和 desktop/Chromium 三视角 golden 仍阻断,不能据此声明发布级体渲染。
## 验收
```bash
npm --prefix web run test:lighting-roundtrip
npm --prefix web run test:lighting-field-parity
WEB_TEST_PORT=5545 npm --prefix web run test:lighting-field-roundtrip
WEB_TEST_PORT=5548 npm --prefix web run test:render-resource-budget
WEB_TEST_PORT=5552 npm --prefix web run test:render-reference
WEB_TEST_PORT=5556 npm --prefix web run test:render-routing
WEB_TEST_PORT=5560 npm --prefix web run test:server-render-job
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
WEB_TEST_PORT=5319 npm --prefix web run test:e2e -- --grep "N-019 Scene exposure"
```

View File

@@ -1,7 +1,8 @@
# N-020 Compositor
状态:`BLOCKED`GraphIR、有界 CPU executor真实 Main graph 结构 reader 已落地;完整节点
参数映射、WebGPU、HDR golden 和服务端执行未实现)
状态:`BLOCKED`GraphIR、有界 CPU executor真实 Main graph reader、四节点 WebGPU golden、
Unsupported 全图执行门和故障生命周期闭环已落地;完整节点参数映射、通用 WebGPU、HDR golden
和服务端执行未实现)
## 已验证切片
@@ -27,14 +28,27 @@
Output 的动态 `Socket_0` 规范化为 GraphIR `Image`。桌面生成 fixture 经 WASM/Worker 后直接由
CPU executor 求值 Constant→Exposure→Invert→CompositeRGBA 结果通过;未连接的 Glare 仍保留
Unsupported 并使完整图 capability gate 保持阻断。
8. N-020-B/EM11-07 有界 WebGPU golden显式 allowlist 只包含 Constant Color、Exposure、
Invert、Composite。Blender 5.2 生成的四场景 fixture 经真实 WASM Main reader 后,生产 CPU
executor 与 Chromium WebGPU compute 对 2×2 `LINEAR_SRGB` Float32 RGBA 输出逐字节 SHA-256
相同CPU-only、资源输入、未声明节点、断链和错误 socket 在 shader 编译前结构化阻断。
9. N-020-DM11-08 全图执行门CPU 与 frame-cache 入口在求值、取消回调和 cache hit 前扫描
完整 graph即使 Unsupported 节点未连接到 Composite也返回
`COMPOSITOR_NODE_UNSUPPORTED`。真实 Blender 5.2 Main fixture 的 Glare node name/type/
`blenderType`、graph JSON 与 revision 在失败前后完全不变;预置 cache 不能绕过该门。
10. M11-14 Compositor fault lifecycle真实 Main graph 的 CPU executor 周期取消返回
`COMPOSITOR_CANCELLED`8193 像素超尺寸输入在分配/缓存前返回
`COMPOSITOR_BUDGET_EXCEEDED`。generation 2 重开保持 GraphIR identity 与 Float32 output
SHA-256LRU `clear()` 返回已释放字节并把 entries/bytes 归零。
## 仍然阻断
- N-020-AMain graph 写回,以及 Transform、非默认 Invert、Alpha Over、Blur、Mix、Image/Render
Layer 的逐节点参数与 resource/pass reader基础真实图结构、常量色、Exposure、默认 Invert、
Viewer/Composite 已完成。
- N-020-B/CWebGPU executor、tile scheduler、OPFS 持久 frame cache、增量 invalidation 和 GPU
disposeCPU 周期取消和内容寻址内存 LRU 已完成。
- N-020-B/CM11-07 只实现四节点、无资源、单输入链的有界 WebGPU compute通用 WebGPU
executor、tile scheduler、OPFS 持久 frame cache、增量 invalidation 和生产 GPU 调度仍阻断;
CPU 周期取消和内容寻址内存 LRU 已完成。
- N-020-D服务端 Blender job、source hash 和结果提交。
- N-020-Edesktop HDR/alpha/color-space golden、OOM/fault/device-loss。
@@ -43,4 +57,7 @@
```bash
WEB_TEST_PORT=5320 npm --prefix web run test:e2e -- --grep "N-020 CPU compositor"
npm --prefix web run test:compositor-main-reader
WEB_TEST_PORT=5562 npm --prefix web run test:compositor-node-golden
WEB_TEST_PORT=5565 npm --prefix web run test:compositor-unsupported-gate
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
```

View File

@@ -1,7 +1,9 @@
# N-021 Sequencer 与音频
状态:`BLOCKED`strip schema、真实 Main reader、确定性时间编辑和 codec 能力门已落地;
Main 写回、媒体解码/渲染、音频波形和服务端编码仍未实现)
状态:`BLOCKED`strip schema、真实 Main reader、确定性时间编辑、长媒体有界索引/seek/cache、
三类实际 runtime codec probe、source/decode-bound 首帧 proxy cache、三类迟到结果 revision
gate、最终编码 server-export 路由、实时 AudioContext 恢复门和媒体故障生命周期闭环已落地;
Main 写回、完整媒体解码/渲染、音频波形、A/V sync、实际混音和真正服务端编码 job 仍未实现)
## 已验证切片
@@ -14,8 +16,8 @@ Main 写回、媒体解码/渲染、音频波形和服务端编码仍未实现
映射,避免左右片段复用越界源帧。
4. N-021-B/C部分`resolveSequencerFrame` 在切分边界解析当前顶层 strip、source frame
与依赖META 覆盖的子 strip 和 inactive dependency 不会泄漏到渲染帧。
5. N-021-C/D运行时只报告 WebCodecs/HTMLMedia 需要精确 probecodec 必须
出现在已验证 MIME 集合中才可放行,本地编码固定为 `BLOCKED`
5. N-021-C/D运行时 inventory 只报告 WebCodecs/HTMLMedia 需要精确 probe旧的
MIME 集合不能作为 READY receipt,本地编码固定为 `BLOCKED`
6. N-021-A部分Blender 5.2 `Scene.ed/Editing.seqbase` 读取 Scene、Movie、Image、Sound、
Meta 和协议支持的 Effect显示范围由持久 `start/startofs/endofs/len` 计算,保留 channel、
mute/lock、相对媒体路径、24/1.001 FPS 和 effect input stable ID。绝对路径、未知类型、
@@ -25,19 +27,68 @@ Main 写回、媒体解码/渲染、音频波形和服务端编码仍未实现
muted 输入和非 cross effect 拒绝。
8. 桌面 Blender 5.2 fixture 经 WASM/Worker 后,在真实 CROSS 中间帧得到 factor 0.5;两个单张
Image strip 的 source range 为 `0..1`,显示 20 帧仍固定到 source frame 1不伪造成图像序列。
9. V1 long-media 门使用本地 261-byte PNG 与 16,044-byte WAV 建立 1,000,000 帧、10,001
strips 时间线1,024-frame bucket index 仅产生 11,914 references不按总帧数展开 JSON。
起点/中点/终点、64 次确定性随机 seek、连续 scrub 最新请求胜出和 index/seek/decode 取消均通过。
10. preview cache 使用 20 KiB 显式 byte budget/LRU随机 seek 发生真实淘汰且当前 audio/image key
仍 residenttimeline/current frame/asset hashes 作为内容寻址 session manifest 写入 OPFS终止
StorageWorker 与 media Worker 后在 frame 765432 恢复。session SHA-256 固定为
`d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d`
11. 上述 V1 长媒体门只放行 fixture 的 IMAGE/SOUND 原始本地资源读取,不声称 WebCodecs MOVIE
seek、waveform/proxy 生成或最终编码完成local encoding 仍结构化阻断。
12. M11-09 runtime codec probeschema 1 request/result 绑定 IMAGE/SOUND/MOVIE family、规范 MIME、
byte length 和 source SHA-256且不接收 source path/扩展名字段。Chromium 用 ImageBitmap
解码 8×8 PNG用固定 48 kHz OfflineAudioContext 解码 1 秒 WAV用 HTMLMedia 加载、seek
并绘制 16×16 两帧 H.264 MP4三类均 READY。错误扩展名不影响结果损坏 bytes、hash
drift、MIME/family 和 backend 伪造均返回 `SEQUENCER_CODEC_UNSUPPORTED`
13. M11-10 proxy cacheschema 1 identity 绑定 MOVIE source family/MIME/bytes/SHA-256、当前
READY HTMLMedia receipt、SRGB8/STRAIGHT RGBA8 profile 和 source framepayload 再独立绑定
byte length/SHA-256。Chromium 从真实 H.264 初始 decoded frame 生成 8x8/256-byte proxy
一帧预算 LRU 发生确定性淘汰content-addressed payload/manifest 经新 Storage Worker 重开
后复验。source、decode receipt、identity 或 payload 漂移均在消费前稳定阻断。
14. M11-11 media revision gateSEEK/SCRUB/DECODE 使用同一 schema 1 request绑定 timeline
ID/revision、单调 request revision、operation 和 framecompleted result 必须逐字段回显并绑定
source frame/payload hash。Chromium 中旧 SEEK 晚于新 SCRUB 返回、真实 H.264 DECODE 跨
timeline replacement 返回及伪造 result 全部在 publish/cache callback 前成为
`STALE/REVISION_CONFLICT`;只有当前 SCRUB 与当前 DECODE 发布。
15. M11-12 final export routeschema 1 将真实 Main timeline ID/revision、source `.blend`
SHA-256、帧范围/帧率、分辨率和 container/video/audio codec 绑定到 settings/request SHA-256。
无 endpoint 时返回 `SERVER_EXPORT/BLOCKED/SEQUENCER_EXPORT_SERVER_UNAVAILABLE`;有 endpoint
时只返回 `SERVER_EXPORT_REQUIRED`。Chromium 中注入和实际探测到的 `VideoEncoder` 都不能
`localEncoding``BLOCKED` 提升为本地完成。
16. M11-13 audio session recoveryschema 1 分开报告
`UNAVAILABLE/SUSPENDED/RUNNING/CLOSED` context 与 `BLOCKED/SILENT/ENABLED` output
revision 随 initialize/mute/resume/suspend/close 单调推进。Chromium 在真实用户手势内创建
`AudioContext`,通过 suspend、mute、muted resume、unmute、二次 suspend/resume 和 close
mute 将 gain 归零unmute 恢复 0.75close 再归零并断开节点。API/构造器缺失与 resume
failure 分别稳定返回 `SEQUENCER_AUDIO_DEVICE_UNAVAILABLE`
`SEQUENCER_AUDIO_RESUME_FAILED`,不会误报输出启用。
17. M11-14 Media fault lifecycle真实 H.264 先通过 HTMLMedia probe 并解出 8x8 RGBA8 frame
预取消 proxy decode 返回 `SEQUENCER_CANCELLED` 且不发布 payload。低于一帧的 cache budget
返回 `SEQUENCER_BUDGET_EXCEEDED` 且 entries/bytes 保持 0首会话 clear 返回正数释放字节,
重建会话后 source/decode/profile identity 和 payload SHA-256 保持不变。
## 仍然阻断
- N-021-A/BMain strip 写回、非 CROSS transition/modifier 完整参数、undo/redo 和 save/reopen
基础真实 reader 与 CROSS/GAMMA_CROSS 帧描述已完成。
- N-021-CWebCodecs 精确 seek/decode、音频 waveform、proxy 生成、A/V sync、丢帧和
损坏媒体处理。
- N-021-D浏览器不支持的 codec、混音与最终编码的服务端 Blender job
- N-021-E桌面/Chromium golden、OPFS 和大规模媒体性能门。
- N-021-CM11-09/10/11/13 完成三类短 fixture 的初始实际解码门、MOVIE 初始帧 RGBA8 proxy
cache identity、迟到结果 publish/cache gate 和实时 AudioContext 生命周期WebCodecs 帧精确
seek/decode、多帧 proxy、音频 waveform、A/V sync、实际混音、丢帧和长媒体损坏恢复仍阻断
- N-021-DM11-12 已冻结最终编码的 server-export 路由与请求 identity真正服务端 Blender
encode job、进度/取消、混音和输出结果校验仍阻断。
- N-021-EV1 有界 Chromium/OPFS 长媒体门已完成;完整桌面/Chromium 解码/渲染 golden 仍阻断。
## 验收
```bash
WEB_TEST_PORT=5321 npm --prefix web run test:e2e -- --grep "N-021 sequencer"
npm --prefix web run test:sequencer-main-reader
npm --prefix web run test:long-media-performance
WEB_TEST_PORT=5570 npm --prefix web run test:sequencer-codec-probe
WEB_TEST_PORT=5573 npm --prefix web run test:sequencer-media-cache
WEB_TEST_PORT=5576 npm --prefix web run test:sequencer-media-revision
WEB_TEST_PORT=5579 npm --prefix web run test:sequencer-final-export
WEB_TEST_PORT=5586 npm --prefix web run test:sequencer-audio-recovery
WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery
```

View File

@@ -3,6 +3,97 @@
状态:`BLOCKED`asset catalog、来源/许可证元数据、真实 Main library inventory、库依赖与
IO 安全门已落地Append/Link/Override Main、非 GLB 本地导入和跨桌面重导入未实现)
## 完整对标盘点基线
M12-01A 已从 Blender 5.2 DNA、RNA、Blend IO、AssetCatalog/CDF 源码及真实 RNA runtime 盘点
14 个 `AssetMetaData` DNA 字段、10 个公开 RNA 属性、3 个 `AssetTag` 存储字段和 3 个 catalog
持久语义字段。M12-01B 随后用锁定 Blender 5.2 生成 3 条 CDF v1 catalog、Object/Material/World
资产和 canonical JSON并由独立 Blender 新进程精确重开复验。M12-01C 以 Blender UUID/path
和 AssetWeakReference 定义 Web schema v2 stable identity并冻结 UTF-8 文本及条目预算。三项均为
`enablingTask`。M12-01D 又完成 v1→v2 无损前向迁移,保留 revision、source、preview 与 library
graph并输出确定性 ID mapping/hash report。四项只冻结完整对标输入与迁移不增加本节已验证
slice。M12-01E 又固定 v2 对 v1 reader 的 read-only/write-block/future-reject 策略。五项仍不改变
`parityStatus=BLOCKED`
M12-01F 已补齐 duplicate catalog/asset ID、legacy parent cycle、v2 parentPath drift、UTF-8
simple-name/tag budget 和 v1/v2 unknown-field 共 9 类稳定负例;该 enabling evidence 不改变 slice。
M12-01G 已让 v1→v2 IndexedDB index migration 在同一 readwrite transaction 中提交 target、
receipt 和 source delete写 target 后或删 source 后注入失败均 abort关闭并重开后旧 v1 index、
无关 migration 行和数据库版本精确不变。该 enabling evidence 仍不改变 slice。
M12-01H 又证明初始页面、reload 和两个顺序重建的独立 Worker 读取相同 catalog/asset 顺序、
AssetWeakReference stable identity、revision 和 manifest SHA-256receipt/hash drift 在发布前阻断。
该 enabling evidence 仍不改变 slice。
M12-01I 将 A-H 八项任务链、Blender 5.2 desktop fixture、Web v2 fixture、production migration
report、canonical 双 hash 和 IndexedDB fault/restart artifact 递归绑定为一个 READY evidence。
M12.1 schema/migration 已收口,但仍是 enabling evidence不把 catalog mutation/preview 计为完成。
M12-02A 已盘点 PreviewImage 的 ICON/PREVIEW 双槽、持久尺寸/rect/flag、RGBA8 packed pixel、
premultiplied alpha、runtime deferred/invalid 状态和真正的 null pointer。原生结构/RNA 没有颜色空间
字段;现有 Web v1/v2 也缺 slot/colorSpace/alpha/generator 语义。本项仍是 inventory evidence。
M12-02B 定义 source/content 各自的 byte length、MIME、SHA-256并让 content 绑定尺寸、RGBA8、
sRGB/alpha profilegenerator 绑定 Blender version/executable/script/settings。真实 Blender 两次重编码
确定性相同source/content 编码不同但解码像素 exact本项仍是 enabling identity evidence。
M12-02C 在任何图像 decoder 或 decoded allocation 前依次检查 identity、16 MiB encoded bytes、
content SHA-256、PNG/WebP header、identity-bound dimensions、16,777,216 pixels、64 MiB RGBA8
decoded bytes 和 100:1 compression ratio。8 类失败均 fail-closed随后小 preview 可立即恢复;
本项仍是 enabling budget evidence不把 preview storage/display 计为完成。
M12-02D 先把 content-addressed preview 写入 OPFS 并回读复验,再用一次 revision/full-row guarded
IndexedDB transaction 提交 catalog v2 preview reference 和 hash receipt。OPFS 前后及 catalog put
后的三处 fault 都保持旧 catalog重开后 revision、receipt 和 payload hash 一致。本项仍不把
dedupe、损坏隔离或 viewport display 计为完成。
M12-02E 又让两个不同 Blender weak-reference identity 和不同 metadata 的 asset 共享相同 content
SHA-256第二次写入只复验并复用 OPFS payload物理文件计数为 1两个 catalog entry 的完整
non-preview metadata 不变revision 9 重开一致。本项仍不把损坏隔离或 display 计为完成。
M12-02F 将同长度但 SHA-256 漂移的 bytes 从 content-addressed namespace 移入复验后的 quarantine
文件,并写入与 catalog revision/reference 绑定的 receipt。catalog 本身及另一 asset 均不变;重开后
仍可读取完整 metadata只把 preview/data 返回 null。本项仍不把 reference GC/display 计为完成。
M12-02G 在 catalog reference transaction 完成后按 committed manifest 计数:同项目仍有一个引用时
保留 payload最后一个引用删除后才回收。OPFS project namespace 保证另一项目的同 hash payload
和 READY preview 不受影响revision 11 重开 metadata 不变。本项仍不把 display 计为完成。
M12-02H 已把 Blender 5.2 确定性重编码的 `SRGB/RGBA8/STRAIGHT` preview 固定为逐字节 reference
主线程 `HTMLCanvasElement` 与独立 Worker `OffscreenCanvas` 共用生产 decode/display 实现。两条路径
均先通过 M12-02C 的 identity/header/预算门,再以禁用色彩转换和预乘的 `ImageBitmap` 解码;输出
256-byte RGBA8 hash 与 desktop reference 完全一致MAE/RMS/P95/max/bad-pixel/alpha 指标均为零,
前景 IoU 为 1。该项新增一个已验证 preview display parity slice但不扩大到任意尺寸、色彩空间、
asset browser UI 或 Append/Link/Override。
M12-03A 已分别冻结 Append、Link 与 Library Override 的数据块/闭包边界。真实 Blender runtime
公开 36 类 selectable root其中 Screen/WorkSpace 为 append-onlyLibrary 是来源/传递依赖元数据,
Key 等 embedded ID 随 owner 进入闭包。Append 由实际 ID pointer graph 区分 direct、indirect、
cross-library、override dependency 与 reusable localLink 保留 `ID.lib`、EXTERN/INDIRECT 和
`Library.runtime.parent`Override 另记录 linked reference、local owner、hierarchy root、property/
operation 与 system dependency。该 inventory 仍不创建 ownership contract 或 Main transaction。
M12-03B 已定义 schema 1 library operation identity。`sourceLibraryId` 同时绑定 canonical source
locator 与 `.blend` SHA-256APPEND 只能由 `LOCAL_MAIN` owner 持有且可写LINK 只能由匹配的
`SOURCE_LIBRARY` owner 持有且 root/reference 均只读LIBRARY_OVERRIDE 由 `LOCAL_OVERRIDE`
持有、local 可写但 linked reference 只读。invalidation token 还绑定 operation、root、owner、
source generation/revision 与 dependency-closure hash任一漂移稳定返回 `REVISION_CONFLICT`
M12-03C 已生成真实 Blender 5.2 desktop 单 Object append fixture。选中的 Object 自动带入 Mesh、
Material 和 packed Image 依赖,四个 ID 在 target Main 中均无 source library/override 指针并映射为
可写 `LOCAL_MAIN` owner。新 Blender load 路径重开 target 后3 条依赖边、4/4/1 geometry、UVMap、
material slot、2x2 sRGB RGBA image 和 Float32 pixel SHA-256 均与 source canonical report 一致。
本项只冻结 desktop 权威 fixture不提前声明 WASM Main append、undo/redo 或完整 append parity。
该合同没有提前执行 Append 或开放 writer。
M12-03D 已把同一 source-hash-bound closure 送入 WASM Worker 的 authoritative Blender Main。Native
link/append context 递归导入 Object、Mesh、Material 和 packed ImageMain commit 前核对 source
locator、root、closure 与 base revision四个 ID 均无 `ID.lib`/override、映射为可写 `LOCAL_MAIN`
并只产生一个新的 SceneIR revision。stale revision 与 local ID collision 在 Main mutation 前阻断,
失败路径回滚完整 history state。该项只证明单 transaction Main appendundo/redo/save/reopen 与
desktop canonical report 的联合一致性仍由 M12-03E 验证。
## 已验证切片
1. N-023-A版本化 manifest 覆盖 catalog、asset kind/tag、preview、author、license、
@@ -29,21 +120,40 @@ IO 安全门已落地Append/Link/Override Main、非 GLB 本地导入和跨
已完成;大 bundle 性能仍未实现。
9. VDB 独立资源库包含 generated/official/source/derived/report/manifest/license14 条记录逐项绑定
byte length 与 SHA-256官方 sphere 使用 CC-BY-4.0 和 Git LFS 权威 hash未放入仓库工作树。
10. N-023-Apreview displayBlender 5.2 desktop RGBA8 reference、主线程 Canvas2D 与
OffscreenCanvas 的像素 SHA-256 精确一致;错误像素返回 `RENDER_REFERENCE_MISMATCH`
## 仍然阻断
- N-023-BAppend/Link/Library Override、reload/relocate 和真实 Main transaction只读 library
inventory 已完成
- N-023-BAppend undo/redo/save/reopen、Link/Library Override、reload/relocate 和完整真实 Main
parityM12-03D 的单 transaction append 已有独立证据,但不解除本项阻断
- N-023-C/DGLTF/OBJ/PLY/STL、USD/Alembic import/export/save/reopen/desktop reimport。
- N-023-E真实 zip decoder/fuzz、OPFS quota/recovery、license/source offer 发布审计和大文件
流式性能archive 路径冲突、双向字节预算与确定性 range plan 已完成。
- N-023-EVDBGPU page resident LRU 64 MiB1 GiB bundle 中断/内存性能门。
- N-023-EVDBGPU page resident LRU 已完成;64 MiB1 GiB bundle 中断/内存性能门仍阻断
## 验收
```bash
WEB_TEST_PORT=5323 npm --prefix web run test:e2e -- --grep "N-023 asset"
npm --prefix web run test:library-main-reader
npm --prefix web run test:asset-catalog-inventory
npm --prefix web run test:asset-catalog-v1-fixture
npm --prefix web run test:asset-catalog-v2
npm --prefix web run test:asset-catalog-migration
npm --prefix web run test:asset-catalog-legacy-reader
npm --prefix web run test:asset-catalog-negatives
npm --prefix web run test:asset-preview-inventory
npm --prefix web run test:asset-preview-identity
npm --prefix web run test:asset-preview-decode-budget
npm --prefix web run test:asset-preview-opfs-commit
npm --prefix web run test:asset-preview-dedup
npm --prefix web run test:asset-preview-quarantine
npm --prefix web run test:asset-preview-reference-gc
npm --prefix web run test:asset-preview-display
npm --prefix web run test:library-operation-inventory
npm --prefix web run test:library-operation-identity
npm --prefix web run test:library-main-append
npm --prefix web run test:vdb
npm --prefix web run test:vdb-native
```

View File

@@ -1,29 +1,32 @@
# N-026 全域发布门
状态:`BLOCKED`machine-readable parity manifest、依赖/状态检查、命令/hash 证据绑定、
Chromium 完整套件、离线包/OPFS、SPDX SBOM、1M geometry、simulation cache 性能和
4K/8K texture、运行中断网主线程 WebGL device loss 证据已落地10M/长媒体、OOM 与上游能力仍未齐)
状态:`parity BLOCKED / V1 release READY`machine-readable parity/release manifest、依赖/状态
检查、命令/hash 证据绑定、Chromium 完整套件、离线包/OPFS、SPDX SBOM、1M/10M geometry、
simulation cache、1M 帧长媒体、4K/8K texture、运行中断网主线程 WebGL device loss、四类
确定性 OOM 和 P0 用户闭环证据已落地V1 evidence 为零缺失)
## 已验证切片
1. N-026-A版本化 schema 3 `ReleaseManifestIR` 为每个 family 记录 `LOCAL_EXACT`
`LOCAL_BOUNDED``SERVER``BLOCKED`、roadmap 状态、完成/阻断/明确排除切片、验收命令和
依赖;同一切片跨状态重复、缺失 family、非法非阻断状态或依赖环会拒绝。
1. N-026-A版本化 schema 4 `ReleaseManifestIR` 分别记录 Blender 全域 `parityStatus`V1
`releaseClass/releaseStatus`、required/excluded slices、roadmap 状态、验收命令和依赖;
required 未声明、READY 仍含未完成 required、V1 排除已完成切片、缺失 family 或依赖环会拒绝。
2. N-026-B/CChromium 主线程/OffscreenCanvas、offline/Worker restart/OPFS recovery、
1M/10M geometry、4K/8K texture、长媒体、simulation cache、OOM/device loss/网络
中断/损坏 blend/zip bomb 等证据字段必须逐项为 true 才能放行。
3. N-026-D/Elicense、SBOM、source offer、deterministic package 为发布必需证据;每个 true
字段必须绑定实际成功命令、耗时、输出与构件 SHA-256manifest family 排序可确定性序列化,
当前缺证据聚合为 `BLOCKED`,没有虚报发布通过。
当前 V1 缺证据为空并聚合为 `READY`;全域 parity 仍使用独立 `parityStatus=BLOCKED`,没有
把有界发布范围虚报为完整 Blender 对标。
4. 当前 Chromium smoke 已通过引擎启动/Main 编辑与内容寻址资产恢复,并通过真实 64 KiB
OPFS quota 下的失败保持旧 revision、Worker 重启恢复门。这只计为 partial evidence不等于
完整 suite 或其余 family 的桌面 golden。发布门 schema 3 仅接受 Chromium 浏览器证据,
完整 suite 或其余 family 的桌面 golden。发布门 schema 4 仅接受 Chromium 浏览器证据,
Firefox/WebKit 不在本项目当前测试配置内。
5. N-015 Curve/Surface/Font/Metaball 已通过 Blender 5.2 desktop geometry golden这四类及
PointCloud/Curves/Hair 共 7 对象已通过 GLB/USDA desktop round-trip。Volume/VDB 已重新纳入,
当前已有真实资源/license/hash、OpenVDB 13->NanoVDB 32 desktop 转换、manifest/range 和确定性
证据server job、OPFS 重开、Float32 WebGPU core 和 Main Volume 属性保存重开已有专项证据,
生产视口、GPU paging、GLB/USD loss 和 desktop/Chromium 发布 golden 仍计为阻断。
生产视口、有限 grid 材质和显式 GPU resident LRU 已接入自动缺页重绘、GLB/USD loss、联合重开和
desktop/Chromium 发布 golden 仍计为阻断;自动 demand paging 不计入已完成证据。
6. 当前发布包已通过本地 third-party notices、Blender/Three license 文件、无远程运行时依赖、
非空 SHA-256 manifest、SPDX 2.3 lockfile/vendored SBOM、对应源码提供和离线二进制/源码包
确定性复建100k/1M decimate、5 类损坏 blend 和 archive 高压缩比拒绝有实际命令记录。
@@ -45,15 +48,30 @@ Chromium 完整套件、离线包/OPFS、SPDX SBOM、1M geometry、simulation ca
至少一个唯一构件 SHA-256manifest `generatedAt` 必须是可往返的 canonical UTC 时间戳。
13. 8K texture 门先要求 WebGL `MAX_TEXTURE_SIZE >= 8192`,再对 8192x8192 PNG 执行同样的
SHA-256、解码、GPU 上传和实际采样,独立 Chromium 进程内耗时须小于 45 秒。
14. P0 用户闭环使用同一真实 `.blend` 完成对象/网格/材质 Main 编辑、undo/redo、OPFS 原子保存、
StorageWorker/WebEngineWorker 双重启、稳定 ID/几何 SHA 重开比较、主线程/Offscreen 视口和
GLB 语义/PBR 回读;不支持 Shader 仍返回 `SHADER_NODE_UNSUPPORTED`
15. 10M geometry 门以 40 个 250K triangle transferable chunk 连续覆盖 10,000,000 triangles
实际传输 180,492,000 bytesACK 背压使源端峰值工作集为 4,512,300 bytes。20K/2K 两级 LOD
使用既有 cache key/manifest 和 OPFS 重开路径,在完整流完成前进入主线程/Offscreen 双视口,
覆盖选择、相机、近远 LOD 切换、中途取消、renderer/Worker/cache 释放和小场景恢复。range
manifest 固定 SHA-256 为 `224738e0c3aa28ae42167bf8974455a3d0f940f59f19eb51c5768f435c66b24f`
16. long media 门覆盖 1,000,000 帧/10,001 strips 的有界 bucket index、起中末/64 个随机 seek、
最新 scrub 胜出、index/seek/decode 取消、20 KiB LRU 和活动 key 保留;本地 PNG/WAV 及会话
manifest 经内容寻址 OPFS 保存StorageWorker/media Worker 双重启后恢复当前帧。MP4 codec
与最终编码仍返回结构化阻断,不被该 V1 证据扩白。
17. OOM 门使用测试专用 token session 和 fail-after-bytes/count在真实 Chromium WebEngine、
OPFS、WebGL 与 WebGPU 路径分别注入 WASM open/edit/save、staging 中途写、GPU geometry/texture
原子资源组和 NanoVDB resident/page-table 失败;报告验证旧 revision/hash、undo/redo、临时资源
归零、释放字节、同会话继续执行与 Worker/持久存储重开,并由独立 record 绑定 `faults.oom`
## 仍然阻断
- 跨 family 桌面 golden、10M、长媒体和 OOM fault 仍无真实证据;
本项目当前只配置 Chromium上游 N-015 至 N-025
的阻断能力会传递到发布门。
- V1 evidence 当前为 17 条记录、0 个缺失release gate 为 `READY`。N-015 至 N-025 的全域
对标差距继续记录为 `parityStatus=BLOCKED`;本项目当前只配置 Chromium。
- VDB 发布证据已覆盖 server job、HTTP 续传/OPFS 分块恢复、quota/tamper/rollback、Float32 WebGPU
数值/专项像素、双生产视口、有限材质映射与 Volume Main 属性重开;仍缺 resident 分页、生产
Offscreen device-loss 重建、OOM/大 bundle、完整材质和三视角
数值/专项像素、显式 resident 分页/LRU、双生产视口、有限材质映射与 Volume Main 属性重开;仍缺自动缺页重绘、生产
Offscreen device-loss 重建、大 bundle、完整材质和三视角
desktop/Chromium golden。
- 完整 native dependency/license 审计仍需发行审核;当前 SBOM 覆盖 lockfile 和显式 notices
不声称替代 Blender 全部传递依赖的发布级法律审计。
@@ -63,5 +81,8 @@ Chromium 完整套件、离线包/OPFS、SPDX SBOM、1M geometry、simulation ca
```bash
WEB_TEST_PORT=5326 npm --prefix web run test:e2e -- --grep "N-026 release"
npm --prefix web run test:release-evidence
npm --prefix web run test:geometry-10m-performance
npm --prefix web run test:long-media-performance
npm --prefix web run test:oom-recovery
npm --prefix web run release:evidence
```

View File

@@ -0,0 +1,20 @@
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"$id": "https://blender-web.local/schemas/ci-lane-report-v1.json",
"title": "Web Blender CI lane report",
"type": "object",
"required": ["schemaVersion", "lane", "status", "generatedAt", "commit", "environment", "bindings", "records", "retentionDays"],
"properties": {
"schemaVersion": { "const": 1 },
"lane": { "enum": ["quick", "chromium", "release"] },
"status": { "enum": ["READY", "FAILED"] },
"generatedAt": { "type": "string", "format": "date-time" },
"commit": { "type": "string", "pattern": "^[a-f0-9]{40}$" },
"environment": { "type": "object" },
"bindings": { "type": "object" },
"records": { "type": "array", "minItems": 1 },
"retentionDays": { "enum": [7, 14, 30] }
},
"additionalProperties": false
}

View File

@@ -1,16 +1,22 @@
{
"schemaVersion": 1,
"updatedAt": "2026-08-14",
"schemaVersion": 2,
"updatedAt": "2026-08-17",
"source": "docs/BLENDER_5_2_WEB_FEATURE_PARITY.md",
"statusEnum": ["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "BLOCKED"],
"parityStatusEnum": ["COMPLETE", "BLOCKED"],
"releaseClassEnum": ["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "EXCLUDED"],
"releaseStatusEnum": ["READY", "BLOCKED"],
"families": [
{
"id": "N-015",
"name": "Non-mesh geometry",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["C3-roundtrip", "D2-nanovdb-bounded-webgpu-integration", "E2-vdb-network-worker-quota-tamper-rollback-faults"],
"v1ExcludedSlices": ["C2-new-external-font-import", "D2-nanovdb-production-viewport-automatic-page-fault-depth-composition", "E1-volume-glb-usd-loss-desktop-chromium-golden"],
"roadmapStatus": "in_progress",
"completedSlices": ["A1", "A2-malformed-binary-partial", "A2-integer-overflow-fixtures", "A2-multichunk-attribute-completeness", "B2", "B3-metadata", "B3-vdb-conversion-request-boundary", "B3-nanovdb-manifest-range-hash-protocol", "B3-nanovdb-serial-range-streamer", "B3-vdb-stage-capability-gates", "B3-vdb-real-resource-license-hash-catalog", "B3-desktop-openvdb13-nanovdb32-converter", "B3-native-conversion-determinism-malformed-input", "B3-vdb-server-job-isolation-cancel-timeout", "B3-vdb-desktop-server-hash-equality", "B3-nanovdb-http-retry-if-range-body-resume", "C1-topology-partial", "C1-handle-points-partial", "C1-handle-preview-raycast-partial", "C1-handle-identity-gizmo-main-roundtrip", "C1-multi-handle-selection-highlight-bounded-commit", "C1-gizmo-revision-delta-transaction-boundary", "C1-continuous-handle-gizmo-preview-single-commit", "C1-handle-local-origin-orientation-gizmo", "C1-rename-partial", "C1-create-delete-poly-partial", "C1-poly-bezier-nurbs-1d-conversion", "C1-multispline-create-delete-bulk-handle-cyclic-transaction", "C1-surface-2d-topology-transaction", "C2-font-geometry-partial", "C2-font-layout-partial", "C2-font-character-style-textbox-roundtrip", "C2-existing-packed-vfont-style-links", "C2-builtin-font-evaluation-exact", "C3-roundtrip", "C3-volume-main-properties-save-reopen", "C3-nanovdb-opfs-binding-worker-reopen", "D1-raycast-partial", "D1-offscreen-vert-edge-partial", "D1-selection-history-partial", "D1-cross-object-history-range-patch", "D1-handle-identity-axis-gizmo", "D2-partial", "D2-nanovdb-float32-cpu-wgsl-sampling", "D2-nanovdb-bounded-webgpu-integration", "D2-webgpu-device-loss-session-recovery", "E1-partial", "E1-desktop-geometry-golden", "E1-true-2d-surface-desktop-golden", "E1-glb-evaluated-nonmesh-roundtrip-partial", "E1-glb-curve-line-surface-mesh-evaluated", "E1-usda-four-object-desktop-roundtrip", "E1-pointcloud-curves-hair-glb-usd-loss-fixture", "E2-1M-chromium", "E2-chromium-worker-recovery", "E2-opfs-quota-chromium", "E2-vdb-network-worker-quota-tamper-rollback-faults"],
"blockedSlices": ["C2-new-external-font-import", "D2-nanovdb-production-viewport-paging-advanced-material", "C3-volume-combined-asset-viewport-reopen", "E1-volume-glb-usd-loss-desktop-chromium-golden", "E2-vdb-large-stream-device-loss-oom"],
"completedSlices": ["A1", "A2-malformed-binary-partial", "A2-integer-overflow-fixtures", "A2-multichunk-attribute-completeness", "B2", "B3-metadata", "B3-vdb-conversion-request-boundary", "B3-nanovdb-manifest-range-hash-protocol", "B3-nanovdb-serial-range-streamer", "B3-vdb-stage-capability-gates", "B3-vdb-real-resource-license-hash-catalog", "B3-desktop-openvdb13-nanovdb32-converter", "B3-native-conversion-determinism-malformed-input", "B3-vdb-server-job-isolation-cancel-timeout", "B3-vdb-desktop-server-hash-equality", "B3-nanovdb-http-retry-if-range-body-resume", "C1-topology-partial", "C1-handle-points-partial", "C1-handle-preview-raycast-partial", "C1-handle-identity-gizmo-main-roundtrip", "C1-multi-handle-selection-highlight-bounded-commit", "C1-gizmo-revision-delta-transaction-boundary", "C1-continuous-handle-gizmo-preview-single-commit", "C1-handle-local-origin-orientation-gizmo", "C1-rename-partial", "C1-create-delete-poly-partial", "C1-poly-bezier-nurbs-1d-conversion", "C1-multispline-create-delete-bulk-handle-cyclic-transaction", "C1-surface-2d-topology-transaction", "C2-font-geometry-partial", "C2-font-layout-partial", "C2-font-character-style-textbox-roundtrip", "C2-existing-packed-vfont-style-links", "C2-builtin-font-evaluation-exact", "C3-roundtrip", "C3-volume-main-properties-save-reopen", "C3-nanovdb-opfs-binding-worker-reopen", "C3-volume-combined-asset-viewport-reopen", "D1-raycast-partial", "D1-offscreen-vert-edge-partial", "D1-selection-history-partial", "D1-cross-object-history-range-patch", "D1-handle-identity-axis-gizmo", "D2-partial", "D2-nanovdb-float32-cpu-wgsl-sampling", "D2-nanovdb-bounded-webgpu-integration", "D2-webgpu-device-loss-session-recovery", "D2-nanovdb-grid-material-mapping", "D2-nanovdb-explicit-resident-page-lru-recovery", "D2-nanovdb-production-viewport-automatic-page-fault", "E1-partial", "E1-desktop-geometry-golden", "E1-true-2d-surface-desktop-golden", "E1-glb-evaluated-nonmesh-roundtrip-partial", "E1-glb-curve-line-surface-mesh-evaluated", "E1-usda-four-object-desktop-roundtrip", "E1-pointcloud-curves-hair-glb-usd-loss-fixture", "E1-volume-desktop-main-offscreen-pixel-golden", "E2-1M-chromium", "E2-chromium-worker-recovery", "E2-opfs-quota-chromium", "E2-vdb-network-worker-quota-tamper-rollback-faults", "E2-vdb-large-stream-device-loss-oom"],
"blockedSlices": ["C2-new-external-font-import", "D2-nanovdb-production-viewport-automatic-page-fault-depth-composition", "E1-volume-glb-usd-loss-desktop-chromium-golden"],
"excludedSlices": [],
"acceptance": ["web:test:nonmesh-roundtrip", "web:test:nonmesh-desktop-golden", "web:test:nonmesh-glb-blender-roundtrip", "web:test:nonmesh-usd-serialization", "web:test:nonmesh-usd-blender-roundtrip", "web:test:nonmesh-binary", "web:test:selection-history", "web:test:nonmesh-interaction", "web:test:vdb", "web:test:vdb-availability", "web:test:vdb-native", "web:test:vdb-server", "web:test:vdb-opfs", "web:test:vdb-webgpu", "web:test:vdb-viewport", "web:test:vdb-faults", "web:e2e:N-015|non-mesh", "web:e2e:real OPFS quota"],
"dependencies": []
@@ -18,7 +24,11 @@
{
"id": "N-016",
"name": "Grease Pencil",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["D-worker-restart-save-reopen"],
"v1ExcludedSlices": ["C-material-modifier-full-semantics", "D-full-2d-canvas-marquee-dope-editor", "E-desktop-browser-golden-export-opfs"],
"roadmapStatus": "planned",
"completedSlices": ["A-schema-budget", "A-main-reader", "B-layer-frame-stroke-transaction-partial", "C-point-radius-opacity-color-cyclic-material-partial", "C-bounded-previous-next-onion-preview", "D-current-frame-stroke-preview-main-offscreen-chromium", "D-editor-layer-frame-selection-context-partial", "D-editor-main-layer-frame-panel-partial", "D-single-point-revision-bound-main-translation", "D-viewport-point-raycast-multipoint-gizmo-transaction", "D-continuous-point-preview-single-main-commit", "D-bounded-drawing-frame-dope-navigation", "D-worker-restart-save-reopen"],
"blockedSlices": ["C-material-modifier-full-semantics", "D-full-2d-canvas-marquee-dope-editor", "E-desktop-browser-golden-export-opfs"],
@@ -28,7 +38,11 @@
{
"id": "N-017",
"name": "Paint and weights",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["B-blender52-vertex-group-summary-save-reopen", "C-udim-memory-range-patch-hash-boundary"],
"v1ExcludedSlices": ["A-real-pbvh-depth-occlusion-desktop-falloff", "B-clean-mirror-desktop-brush", "C-blender-packed-udim-dirty-atomic-save", "D-E-face-mask-gpu-quota-desktop-golden"],
"roadmapStatus": "planned",
"completedSlices": ["A-stroke-hit-weight-schema-budget-partial", "A-three-raycast-source-face-barycentric-uv", "A-bounded-cpu-candidate-falloff", "A-uniform-grid-candidate-depth-visibility-gate", "A-selection-mask-identity-gated-brush-patch", "B-main-vertex-color-partial", "B-main-vertex-weight-normalize-partial", "B-selected-vertex-color-weight-ui-transaction-partial", "B-selection-masked-color-weight-main-transaction", "B-blender52-vertex-group-summary-save-reopen", "C-udim-memory-range-patch-hash-boundary"],
"blockedSlices": ["A-real-pbvh-depth-occlusion-desktop-falloff", "B-clean-mirror-desktop-brush", "C-blender-packed-udim-dirty-atomic-save", "D-E-face-mask-gpu-quota-desktop-golden"],
@@ -38,7 +52,11 @@
{
"id": "N-018",
"name": "Physics and simulation",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["C-browser-transform-storage-restart-playback"],
"v1ExcludedSlices": ["A-B-full-family-settings-effectors-cache-binding", "C-desktop-bake-decoder-playback-100-frame-golden", "D-wasm-solvers", "E-bake-server-recovery-progress-ui"],
"roadmapStatus": "planned",
"completedSlices": ["A-family-capability-inventory", "A-authoritative-main-physics-reader-partial", "B-settings-dependency-cache-manifest-partial", "B-cloth-softbody-collision-settings-sha256-save-reopen", "C-exact-frame-selection-gate", "C-content-addressed-frame-range-read-hash-gate", "C-browser-transform-frame-binary-decoder", "C-browser-transform-sceneir-preview", "C-browser-transform-cancellable-playback-session", "C-browser-transform-storage-restart-playback"],
"blockedSlices": ["A-B-full-family-settings-effectors-cache-binding", "C-desktop-bake-decoder-playback-100-frame-golden", "D-wasm-solvers", "E-bake-server-recovery-progress-ui"],
@@ -48,9 +66,13 @@
{
"id": "N-019",
"name": "Lighting and render",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["B-world-scene-render-delta-roundtrip", "B-C-nanovdb-float32-wgsl-bounded-ray-integration"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E", "B-C-nanovdb-production-viewport-paging-advanced-material", "E-volume-desktop-chromium-pixel-golden"],
"roadmapStatus": "planned",
"completedSlices": ["A-camera-light-world-scene-reader-partial", "A-main-camera-dof-properties-partial", "A-main-light-world-properties-partial", "A-white-balance-integrity-gate", "B-three-exposure-shadow-mapping-partial", "B-bounded-kelvin-linear-light-color", "B-world-scene-render-delta-roundtrip", "B-volume-material-manifest-boundary", "B-C-nanovdb-float32-wgsl-bounded-ray-integration"],
"completedSlices": ["A-camera-light-world-scene-reader-partial", "A-main-camera-dof-properties-partial", "A-main-light-world-properties-partial", "A-white-balance-integrity-gate", "B-three-exposure-shadow-mapping-partial", "B-bounded-kelvin-linear-light-color", "B-world-scene-render-delta-roundtrip", "B-volume-material-manifest-boundary", "B-C-nanovdb-float32-wgsl-bounded-ray-integration", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", "E-bounded-realtime-blender-reference-image-metrics", "E-render-fault-lifecycle-recovery"],
"blockedSlices": ["A", "B", "C", "D", "E", "B-C-nanovdb-production-viewport-paging-advanced-material", "E-volume-desktop-chromium-pixel-golden"],
"acceptance": ["web:test:lighting-roundtrip", "web:e2e:N-019 Scene exposure"],
"dependencies": ["N-018"]
@@ -58,9 +80,13 @@
{
"id": "N-020",
"name": "Compositor",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["B-real-main-exposure-invert-cpu-chain", "D-unsupported-node-preservation-gate"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
"roadmapStatus": "planned",
"completedSlices": ["A-graph-resource-cycle-schema", "A-main-graph-structure-reader-partial", "A-main-exposure-default-invert-parameter-reader", "B-bounded-cpu-executor-partial", "B-real-main-exposure-invert-cpu-chain", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", "D-unsupported-node-preservation-gate"],
"completedSlices": ["A-graph-resource-cycle-schema", "A-main-graph-structure-reader-partial", "A-main-exposure-default-invert-parameter-reader", "B-bounded-cpu-executor-partial", "B-real-main-exposure-invert-cpu-chain", "B-webgpu-constant-exposure-invert-golden-partial", "B-D-unsupported-full-graph-execution-block", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", "C-E-compositor-fault-lifecycle-recovery", "D-unsupported-node-preservation-gate"],
"blockedSlices": ["A", "B", "C", "D", "E"],
"acceptance": ["web:test:compositor-main-reader", "web:e2e:N-020 CPU compositor"],
"dependencies": ["N-019"]
@@ -68,9 +94,13 @@
{
"id": "N-021",
"name": "Sequencer and audio",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
"roadmapStatus": "planned",
"completedSlices": ["A-strip-resource-schema", "A-main-strip-timeline-reader-partial", "B-deterministic-move-trim-split-partial", "B-source-frame-seek", "B-active-frame-dependency-resolution", "B-cross-transition-progress-input-resolution", "B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate"],
"completedSlices": ["A-strip-resource-schema", "A-main-strip-timeline-reader-partial", "B-deterministic-move-trim-split-partial", "B-source-frame-seek", "B-active-frame-dependency-resolution", "B-cross-transition-progress-input-resolution", "B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate", "C-runtime-image-sound-movie-byte-decode-probe", "C-source-decode-bound-movie-proxy-cache", "C-seek-scrub-decode-revision-gate", "C-audio-context-suspend-device-mute-recovery", "C-media-fault-lifecycle-recovery", "D-final-export-server-routing-fail-closed"],
"blockedSlices": ["A", "B", "C", "D", "E"],
"acceptance": ["web:test:sequencer-main-reader", "web:e2e:N-021 sequencer"],
"dependencies": ["N-020"]
@@ -78,7 +108,11 @@
{
"id": "N-022",
"name": "Tracking and masks",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["C-browser-probe-solve-gate", "D-real-main-locked-editable-raycast-marquee"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
"roadmapStatus": "planned",
"completedSlices": ["A-clip-track-plane-mask-schema", "A-main-mask-layer-spline-point-reader", "B-revision-marker-mask-edit-partial", "B-source-hash-binding-validation", "C-browser-probe-solve-gate", "D-mask-bezier-stable-identity-raycast", "D-mask-stable-identity-marquee-selection", "D-real-main-locked-editable-raycast-marquee"],
"blockedSlices": ["A", "B", "C", "D", "E"],
@@ -88,17 +122,25 @@
{
"id": "N-023",
"name": "Assets, libraries and IO",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["C-glb-export-usd-analysis-gates", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-opfs-tamper-rollback-quota-recovery"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"],
"roadmapStatus": "planned",
"completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery"],
"blockedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-gpu-resident-page-cache-large-bundle"],
"completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-preview-desktop-browser-main-offscreen-display", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery", "E-nanovdb-gpu-resident-page-lru"],
"blockedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"],
"acceptance": ["web:test:library-main-reader", "web:test:vdb-faults", "web:e2e:N-023 asset"],
"dependencies": ["N-022"]
},
{
"id": "N-024",
"name": "Editors and workflow",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["C-executable-context-filtered-operator-search", "D-context-scoped-keymap-resolution"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
"roadmapStatus": "planned",
"completedSlices": ["A-unified-area-region-context", "B-read-only-editor-manifest-partial", "B-main-workspace-area-region-reader", "C-selection-sync-revision-partial", "C-executable-context-filtered-operator-search", "D-keymap-layout-budget-gates", "D-normalized-key-chord-conflict-resolution", "D-context-scoped-keymap-resolution"],
"blockedSlices": ["A", "B", "C", "D", "E"],
@@ -108,7 +150,11 @@
{
"id": "N-025",
"name": "Scripting and platform",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "EXCLUDED",
"releaseStatus": "READY",
"v1RequiredSlices": ["A-default-deny-script-policy", "B-approved-key-still-sandbox-blocked-audit", "D-platform-capability-report"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E"],
"roadmapStatus": "planned",
"completedSlices": ["A-default-deny-script-policy", "A-main-text-source-inventory-reader", "B-signed-manifest-permission-budget", "B-approved-key-still-sandbox-blocked-audit", "B-request-decision-audit-receipt", "C-server-source-hash-job-gate", "D-platform-capability-report", "E-bounded-replay-resistant-audit-hash-chain"],
"blockedSlices": ["A", "B", "C", "D", "E"],
@@ -118,11 +164,15 @@
{
"id": "N-026",
"name": "Release gate",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_EXACT",
"releaseStatus": "READY",
"v1RequiredSlices": ["B-v1-p0-user-loop"],
"v1ExcludedSlices": ["A", "B", "C", "D", "E", "C-vdb-server-webgpu-save-reopen-golden-evidence"],
"roadmapStatus": "planned",
"completedSlices": ["A-machine-readable-parity-manifest", "A-dependency-status-validation", "A-command-hash-evidence-binding", "A-known-enabled-field-artifact-binding", "B-chromium-runtime-evidence-gate", "B-chromium-full-suite-evidence", "C-performance-fault-provenance-gate", "C-performance-1M-malicious-input-partial", "C-performance-4k-texture-upload-render", "C-performance-8k-texture-upload-render", "C-simulation-cache-opfs-playback-performance", "C-network-interruption-main-save-reopen", "C-main-thread-webgl-device-loss-recovery", "C-zip-bomb-ratio-rejection", "C-vdb-resource-converter-provenance-partial", "C-vdb-network-opfs-quota-tamper-device-loss-partial", "C-release-package-notices-partial", "C-spdx-2.3-lockfile-sbom", "D-deterministic-manifest-serialization", "D-offline-reproducible-source-archive-partial"],
"completedSlices": ["A-machine-readable-parity-manifest", "A-dependency-status-validation", "A-command-hash-evidence-binding", "A-known-enabled-field-artifact-binding", "B-chromium-runtime-evidence-gate", "B-chromium-full-suite-evidence", "B-v1-p0-user-loop", "C-performance-fault-provenance-gate", "C-performance-1M-malicious-input-partial", "C-performance-4k-texture-upload-render", "C-performance-8k-texture-upload-render", "C-simulation-cache-opfs-playback-performance", "C-network-interruption-main-save-reopen", "C-main-thread-webgl-device-loss-recovery", "C-zip-bomb-ratio-rejection", "C-vdb-resource-converter-provenance-partial", "C-vdb-network-opfs-quota-tamper-device-loss-partial", "C-release-package-notices-partial", "C-spdx-2.3-lockfile-sbom", "D-deterministic-manifest-serialization", "D-offline-reproducible-source-archive-partial"],
"blockedSlices": ["A", "B", "C", "D", "E", "C-vdb-server-webgpu-save-reopen-golden-evidence"],
"acceptance": ["web:e2e:N-026 release", "web:test:release-evidence", "web:test:browser", "web:test:release-package", "web:release:offline", "web:test:release-performance", "web:test:malicious-blends"],
"acceptance": ["web:e2e:N-026 release", "web:test:v1-user-loop", "web:test:release-evidence", "web:test:browser", "web:test:release-package", "web:release:offline", "web:test:release-performance", "web:test:malicious-blends"],
"dependencies": ["N-015", "N-016", "N-017", "N-018", "N-019", "N-020", "N-021", "N-022", "N-023", "N-024", "N-025"]
}
]

View File

@@ -1,13 +1,25 @@
{
"schemaVersion": 3,
"schemaVersion": 4,
"source": "docs/status/parity-ledger.json",
"sourceSha256": "fbd0b368e00d2dbeb7d63e248ae2cdb6228a20520b44a14722a21aa0693a6395",
"generatedAt": "2026-08-14T14:56:43.298Z",
"sourceSha256": "c8d73374c338f46c2016f6089d99efdd7b3331fc0100d64dad0f416fe66e3eb6",
"generatedAt": "2026-08-17T15:09:53.894Z",
"families": [
{
"id": "N-015",
"name": "Non-mesh geometry",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"C3-roundtrip",
"D2-nanovdb-bounded-webgpu-integration",
"E2-vdb-network-worker-quota-tamper-rollback-faults"
],
"v1ExcludedSlices": [
"C2-new-external-font-import",
"D2-nanovdb-production-viewport-automatic-page-fault-depth-composition",
"E1-volume-glb-usd-loss-desktop-chromium-golden"
],
"roadmapStatus": "in_progress",
"completedSlices": [
"A1",
@@ -47,6 +59,7 @@
"C3-roundtrip",
"C3-volume-main-properties-save-reopen",
"C3-nanovdb-opfs-binding-worker-reopen",
"C3-volume-combined-asset-viewport-reopen",
"D1-raycast-partial",
"D1-offscreen-vert-edge-partial",
"D1-selection-history-partial",
@@ -56,6 +69,9 @@
"D2-nanovdb-float32-cpu-wgsl-sampling",
"D2-nanovdb-bounded-webgpu-integration",
"D2-webgpu-device-loss-session-recovery",
"D2-nanovdb-grid-material-mapping",
"D2-nanovdb-explicit-resident-page-lru-recovery",
"D2-nanovdb-production-viewport-automatic-page-fault",
"E1-partial",
"E1-desktop-geometry-golden",
"E1-true-2d-surface-desktop-golden",
@@ -63,17 +79,17 @@
"E1-glb-curve-line-surface-mesh-evaluated",
"E1-usda-four-object-desktop-roundtrip",
"E1-pointcloud-curves-hair-glb-usd-loss-fixture",
"E1-volume-desktop-main-offscreen-pixel-golden",
"E2-1M-chromium",
"E2-chromium-worker-recovery",
"E2-opfs-quota-chromium",
"E2-vdb-network-worker-quota-tamper-rollback-faults"
"E2-vdb-network-worker-quota-tamper-rollback-faults",
"E2-vdb-large-stream-device-loss-oom"
],
"blockedSlices": [
"C2-new-external-font-import",
"D2-nanovdb-production-viewport-paging-advanced-material",
"C3-volume-combined-asset-viewport-reopen",
"E1-volume-glb-usd-loss-desktop-chromium-golden",
"E2-vdb-large-stream-device-loss-oom"
"D2-nanovdb-production-viewport-automatic-page-fault-depth-composition",
"E1-volume-glb-usd-loss-desktop-chromium-golden"
],
"excludedSlices": [],
"acceptance": [
@@ -101,7 +117,17 @@
{
"id": "N-016",
"name": "Grease Pencil",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"D-worker-restart-save-reopen"
],
"v1ExcludedSlices": [
"C-material-modifier-full-semantics",
"D-full-2d-canvas-marquee-dope-editor",
"E-desktop-browser-golden-export-opfs"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-schema-budget",
@@ -135,7 +161,19 @@
{
"id": "N-017",
"name": "Paint and weights",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"B-blender52-vertex-group-summary-save-reopen",
"C-udim-memory-range-patch-hash-boundary"
],
"v1ExcludedSlices": [
"A-real-pbvh-depth-occlusion-desktop-falloff",
"B-clean-mirror-desktop-brush",
"C-blender-packed-udim-dirty-atomic-save",
"D-E-face-mask-gpu-quota-desktop-golden"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-stroke-hit-weight-schema-budget-partial",
@@ -167,7 +205,18 @@
{
"id": "N-018",
"name": "Physics and simulation",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"C-browser-transform-storage-restart-playback"
],
"v1ExcludedSlices": [
"A-B-full-family-settings-effectors-cache-binding",
"C-desktop-bake-decoder-playback-100-frame-golden",
"D-wasm-solvers",
"E-bake-server-recovery-progress-ui"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-family-capability-inventory",
@@ -199,7 +248,22 @@
{
"id": "N-019",
"name": "Lighting and render",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"B-world-scene-render-delta-roundtrip",
"B-C-nanovdb-float32-wgsl-bounded-ray-integration"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E",
"B-C-nanovdb-production-viewport-paging-advanced-material",
"E-volume-desktop-chromium-pixel-golden"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-camera-light-world-scene-reader-partial",
@@ -210,7 +274,11 @@
"B-bounded-kelvin-linear-light-color",
"B-world-scene-render-delta-roundtrip",
"B-volume-material-manifest-boundary",
"B-C-nanovdb-float32-wgsl-bounded-ray-integration"
"B-C-nanovdb-float32-wgsl-bounded-ray-integration",
"D-final-render-server-routing-fail-closed",
"D-server-render-source-build-settings-output-hash-binding",
"E-bounded-realtime-blender-reference-image-metrics",
"E-render-fault-lifecycle-recovery"
],
"blockedSlices": [
"A",
@@ -232,7 +300,20 @@
{
"id": "N-020",
"name": "Compositor",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"B-real-main-exposure-invert-cpu-chain",
"D-unsupported-node-preservation-gate"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-graph-resource-cycle-schema",
@@ -240,8 +321,11 @@
"A-main-exposure-default-invert-parameter-reader",
"B-bounded-cpu-executor-partial",
"B-real-main-exposure-invert-cpu-chain",
"B-webgpu-constant-exposure-invert-golden-partial",
"B-D-unsupported-full-graph-execution-block",
"C-image-operation-budget-cancel-partial",
"C-content-addressed-frame-lru-cache",
"C-E-compositor-fault-lifecycle-recovery",
"D-unsupported-node-preservation-gate"
],
"blockedSlices": [
@@ -262,7 +346,20 @@
{
"id": "N-021",
"name": "Sequencer and audio",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"B-real-main-cross-still-frame-resolution",
"C-runtime-codec-probe-gate"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-strip-resource-schema",
@@ -272,7 +369,13 @@
"B-active-frame-dependency-resolution",
"B-cross-transition-progress-input-resolution",
"B-real-main-cross-still-frame-resolution",
"C-runtime-codec-probe-gate"
"C-runtime-codec-probe-gate",
"C-runtime-image-sound-movie-byte-decode-probe",
"C-source-decode-bound-movie-proxy-cache",
"C-seek-scrub-decode-revision-gate",
"C-audio-context-suspend-device-mute-recovery",
"C-media-fault-lifecycle-recovery",
"D-final-export-server-routing-fail-closed"
],
"blockedSlices": [
"A",
@@ -292,7 +395,20 @@
{
"id": "N-022",
"name": "Tracking and masks",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"C-browser-probe-solve-gate",
"D-real-main-locked-editable-raycast-marquee"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-clip-track-plane-mask-schema",
@@ -322,12 +438,28 @@
{
"id": "N-023",
"name": "Assets, libraries and IO",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"C-glb-export-usd-analysis-gates",
"E-archive-path-conflict-compressed-budget-range-plan",
"E-nanovdb-opfs-tamper-rollback-quota-recovery"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E",
"E-nanovdb-large-bundle-performance"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-catalog-asset-license-source-schema",
"A-content-addressed-opfs-capability",
"A-preview-content-signature-dimension-verification",
"A-preview-desktop-browser-main-offscreen-display",
"A-vdb-external-resource-license-sha-catalog",
"B-library-dependency-order-partial",
"B-main-library-inventory-reader",
@@ -337,7 +469,8 @@
"E-nanovdb-http206-range-hash-streaming-boundary",
"E-nanovdb-http-retry-if-range-body-resume",
"E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru",
"E-nanovdb-opfs-tamper-rollback-quota-recovery"
"E-nanovdb-opfs-tamper-rollback-quota-recovery",
"E-nanovdb-gpu-resident-page-lru"
],
"blockedSlices": [
"A",
@@ -345,7 +478,7 @@
"C",
"D",
"E",
"E-nanovdb-gpu-resident-page-cache-large-bundle"
"E-nanovdb-large-bundle-performance"
],
"acceptance": [
"web:test:library-main-reader",
@@ -359,7 +492,20 @@
{
"id": "N-024",
"name": "Editors and workflow",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_BOUNDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"C-executable-context-filtered-operator-search",
"D-context-scoped-keymap-resolution"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-unified-area-region-context",
@@ -389,7 +535,21 @@
{
"id": "N-025",
"name": "Scripting and platform",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "EXCLUDED",
"releaseStatus": "READY",
"v1RequiredSlices": [
"A-default-deny-script-policy",
"B-approved-key-still-sandbox-blocked-audit",
"D-platform-capability-report"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-default-deny-script-policy",
@@ -420,7 +580,20 @@
{
"id": "N-026",
"name": "Release gate",
"status": "BLOCKED",
"parityStatus": "BLOCKED",
"releaseClass": "LOCAL_EXACT",
"releaseStatus": "READY",
"v1RequiredSlices": [
"B-v1-p0-user-loop"
],
"v1ExcludedSlices": [
"A",
"B",
"C",
"D",
"E",
"C-vdb-server-webgpu-save-reopen-golden-evidence"
],
"roadmapStatus": "planned",
"completedSlices": [
"A-machine-readable-parity-manifest",
@@ -429,6 +602,7 @@
"A-known-enabled-field-artifact-binding",
"B-chromium-runtime-evidence-gate",
"B-chromium-full-suite-evidence",
"B-v1-p0-user-loop",
"C-performance-fault-provenance-gate",
"C-performance-1M-malicious-input-partial",
"C-performance-4k-texture-upload-render",
@@ -454,6 +628,7 @@
],
"acceptance": [
"web:e2e:N-026 release",
"web:test:v1-user-loop",
"web:test:release-evidence",
"web:test:browser",
"web:test:release-package",
@@ -487,14 +662,14 @@
},
"performance": {
"geometry1M": true,
"geometry10M": false,
"geometry10M": true,
"texture4K": true,
"texture8K": true,
"longMedia": false,
"longMedia": true,
"simulationCache": true
},
"faults": {
"oom": false,
"oom": true,
"deviceLoss": true,
"networkInterrupt": true,
"malformedBlend": true,
@@ -515,12 +690,12 @@
],
"command": "npm --prefix web run release:sbom",
"exitCode": 0,
"durationMs": 372,
"output": "> blender-web-editor@0.1.0 release:sbom\n> node ../tools/web/generate-sbom.mjs\n\nsbom-ok packages=150 sha256=8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30",
"durationMs": 343,
"output": "> blender-web-editor@0.1.0 release:sbom\n> node ../tools/web/generate-sbom.mjs\n\nsbom-ok packages=150 sha256=aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060",
"artifactSha256": [
"8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30",
"aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060",
"d5ec6e6ec9e1e0a50fe4455513a2938838609d3809224eaf26a2ebfac474f0ae",
"3c65ff96cc15c9072d2517555484ccfd0009a2814d96faadcc5bd7b2e3458503"
"e7b16da68885fe43f32ab7b3eeb2c037b4fd8bf54fe6d5e77049aa51e294b1fb"
]
},
{
@@ -528,14 +703,14 @@
"fields": [],
"command": "npm --prefix web run test:vdb-availability && npm --prefix web run test:vdb-native && npm --prefix web run test:vdb",
"exitCode": 0,
"durationMs": 5823,
"output": "> blender-web-editor@0.1.0 test:vdb-availability\n> node ../tools/web/check-vdb-availability.mjs\n\nvdb-availability-ok core=READY release=BLOCKED browser-openvdb=disabled desktop=ready server=ready opfs=ready webgpu-core=ready main-roundtrip=ready viewport=blocked advanced-material=blocked resources=9\n\n> blender-web-editor@0.1.0 test:vdb-native\n> node ../tools/web/check-vdb-native-pipeline.mjs\n\nvdb-native-pipeline-ok resources=14 converter=openvdb13-nanovdb32 conversion-deterministic=1 semantic-deterministic=1 official-deterministic=1 malformed=blocked browser-openvdb=off\n\n> blender-web-editor@0.1.0 test:vdb\n> playwright test --config playwright.config.ts -g \"VDB conversion boundary\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1156:1 validates the VDB conversion boundary and NanoVDB streaming contract (1.5s)\n\n 1 passed (3.3s)\n\n[WebServer] (node:1987436) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1987453) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 5619,
"output": "> blender-web-editor@0.1.0 test:vdb-availability\n> node ../tools/web/check-vdb-availability.mjs\n\nvdb-availability-ok core=READY release=BLOCKED browser-openvdb=disabled desktop=ready server=ready opfs=ready webgpu-core=ready main-roundtrip=ready viewport=blocked advanced-material=blocked resources=9\n\n> blender-web-editor@0.1.0 test:vdb-native\n> node ../tools/web/check-vdb-native-pipeline.mjs\n\nvdb-native-pipeline-ok resources=14 converter=openvdb13-nanovdb32 conversion-deterministic=1 semantic-deterministic=1 official-deterministic=1 malformed=blocked browser-openvdb=off\n\n> blender-web-editor@0.1.0 test:vdb\n> playwright test --config playwright.config.ts -g \"VDB conversion boundary\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1156:1 validates the VDB conversion boundary and NanoVDB streaming contract (1.5s)\n\n 1 passed (3.2s)\n\n[WebServer] (node:2314584) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2314596) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"885edcc156f87abdbddb0e70fde5566130acfc2770a0b6764d66d12c344973a6",
"7b2d576a613dff314f4b40177f10efa3c01e160b7aeb10bf95a69f6553f8f73b",
"a4c57345a4ea7302b9e2a7e0f8b0ed48f8f27c3ac70d41e02f6d92475a751298",
"179868fde557873b4c74c48e2f0d3199784efb6309ed00dd17310b494b975483",
"a13cf2747dacfbf4c0e6b50dd341db3adac32c7c3c9c07814349a90aa4436a3b"
"379dcc11dc7de0dfec4745763bddf3cc415a5f5dbe48500fefae2f7508095bb5"
]
},
{
@@ -548,8 +723,8 @@
],
"command": "npm --prefix web run test:e2e -- --workers=1 && npm --prefix web run test:browser",
"exitCode": 0,
"durationMs": 219085,
"output": " (1.4s)\n ✓ 95 tests/e2e/smoke.spec.ts:2607:1 reports GLB export blockers before any binary export (1.1s)\n ✓ 96 tests/e2e/smoke.spec.ts:2619:1 blocks Shader graphs that cannot be mapped to glTF PBR (1.0s)\n ✓ 97 tests/e2e/smoke.spec.ts:2646:1 maps bounded RGB and Value Shader constants to glTF PBR factors (1.0s)\n ✓ 98 tests/e2e/smoke.spec.ts:2679:1 exports a local SceneIR mesh as a standards-shaped GLB (1.0s)\n ✓ 99 tests/e2e/smoke.spec.ts:2712:1 keeps per-vertex UV and color attributes in GLB output (1.1s)\n ✓ 100 tests/e2e/smoke.spec.ts:2739:1 evaluates modifier dependency order and blocks unevaluated or cyclic stacks (1.1s)\n ✓ 101 tests/e2e/smoke.spec.ts:2754:1 embeds local textures and exports glTF skin and animation records (1.1s)\n ✓ 102 tests/e2e/smoke.spec.ts:2804:1 reports lightweight budget violations without altering usage (1.1s)\n ✓ 103 tests/e2e/smoke.spec.ts:2822:1 aggregates project, collection, object and LOD budgets without double counting LOD (1.0s)\n ✓ 104 tests/e2e/smoke.spec.ts:2848:1 round-trips LOD geometry through the local binary mesh cache container (1.0s)\n ✓ 105 tests/e2e/smoke.spec.ts:2872:1 blocks ImageIR paths outside the project asset sandbox (1.3s)\n ✓ 106 tests/e2e/smoke.spec.ts:2915:1 extracts Blender packed image bytes through the local asset request API (1.4s)\n ✓ 107 tests/e2e/smoke.spec.ts:2960:1 matches Blender Depsgraph deformation golden within the declared error budget (1.4s)\n ✓ 108 tests/e2e/smoke.spec.ts:2999:1 evaluates the full Blender Depsgraph or reports its safe capability gate (1.4s)\n ✓ 109 tests/e2e/smoke.spec.ts:3059:1 exports layered Action keyframes through SceneIR (1.3s)\n ✓ 110 tests/e2e/smoke.spec.ts:3100:1 patches changed mesh buffer ranges without replacing stable topology (1.1s)\n ✓ 111 tests/e2e/smoke.spec.ts:3124:1 renders through the capability-gated OffscreenCanvas worker (1.4s)\n ✓ 112 tests/e2e/smoke.spec.ts:3137:1 coalesces linked mesh objects into a raycastable instance group (1.9s)\n ✓ 113 tests/e2e/smoke.spec.ts:3145:1 returns structured gates for the undeclared capability protocols (1.2s)\n ✓ 114 tests/e2e/smoke.spec.ts:3204:1 exposes PBR-007 to PBR-012 renderer security gates (1.1s)\n ✓ 115 tests/e2e/smoke.spec.ts:3238:1 transfers packed raster assets into the PBR viewport with an explicit status (1.7s)\n ✓ 116 tests/e2e/smoke.spec.ts:3247:1 uses the same packed texture payload in the OffscreenCanvas renderer (1.8s)\n ✓ 117 tests/e2e/texture-4k-performance.spec.ts:3:1 decodes, uploads and renders a validated 4K texture in Chromium (1.8s)\n ✓ 118 tests/e2e/texture-8k-performance.spec.ts:3:1 decodes, uploads and renders a validated 8K texture in Chromium (4.2s)\n\n 118 passed (3.5m)\n\n> blender-web-editor@0.1.0 test:browser\n> playwright test --config playwright.release.config.ts\n\n\nRunning 3 tests using 1 worker\n\n ✓ 1 [chromium] tests/e2e/cross-browser.spec.ts:6:1 boots the offline engine, renders SceneIR and performs a Main edit (2.8s)\n ✓ 2 [chromium] tests/e2e/cross-browser.spec.ts:32:1 keeps content-addressed asset recovery available in Chromium (1.0s)\n ✓ 3 [chromium] tests/e2e/cross-browser.spec.ts:51:1 keeps the committed project after quota failure and Worker restart in Chromium (1.1s)\n\n 3 passed (6.6s)\n\n[WebServer] (node:1987737) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1987749) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1993877) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1993889) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 366958,
"output": "udgets without double counting LOD (1.0s)\n ✓ 107 tests/e2e/smoke.spec.ts:2874:1 round-trips LOD geometry through the local binary mesh cache container (993ms)\n ✓ 108 tests/e2e/smoke.spec.ts:2898:1 blocks ImageIR paths outside the project asset sandbox (1.3s)\n ✓ 109 tests/e2e/smoke.spec.ts:2941:1 extracts Blender packed image bytes through the local asset request API (1.3s)\n ✓ 110 tests/e2e/smoke.spec.ts:2986:1 matches Blender Depsgraph deformation golden within the declared error budget (1.4s)\n ✓ 111 tests/e2e/smoke.spec.ts:3025:1 evaluates the full Blender Depsgraph or reports its safe capability gate (1.3s)\n ✓ 112 tests/e2e/smoke.spec.ts:3085:1 exports layered Action keyframes through SceneIR (1.3s)\n ✓ 113 tests/e2e/smoke.spec.ts:3126:1 patches changed mesh buffer ranges without replacing stable topology (1.0s)\n ✓ 114 tests/e2e/smoke.spec.ts:3150:1 renders through the capability-gated OffscreenCanvas worker (1.3s)\n ✓ 115 tests/e2e/smoke.spec.ts:3163:1 coalesces linked mesh objects into a raycastable instance group (1.8s)\n ✓ 116 tests/e2e/smoke.spec.ts:3171:1 returns structured gates for the undeclared capability protocols (1.1s)\n ✓ 117 tests/e2e/smoke.spec.ts:3230:1 exposes PBR-007 to PBR-012 renderer security gates (1.0s)\n ✓ 118 tests/e2e/smoke.spec.ts:3264:1 transfers packed raster assets into the PBR viewport with an explicit status (1.6s)\n ✓ 119 tests/e2e/smoke.spec.ts:3273:1 uses the same packed texture payload in the OffscreenCanvas renderer (1.3s)\ntexture-4k-performance {\"width\":4096,\"height\":4096,\"status\":{\"loaded\":1,\"rejected\":0,\"bytes\":254670,\"errors\":[],\"errorCodes\":[]},\"byteLength\":254670,\"estimatedPeakGpuBytes\":67108864,\"released\":true,\"recoveryVisible\":true,\"colored\":4096,\"elapsedMs\":705}\n ✓ 120 tests/e2e/texture-4k-performance.spec.ts:3:1 decodes, uploads and renders a validated 4K texture in Chromium (1.8s)\ntexture-8k-performance {\"supported\":true,\"maxTextureSize\":8192,\"width\":8192,\"height\":8192,\"status\":{\"loaded\":1,\"rejected\":0,\"bytes\":972714,\"errors\":[],\"errorCodes\":[]},\"byteLength\":972714,\"estimatedPeakGpuBytes\":268435456,\"released\":true,\"recoveryVisible\":true,\"colored\":4096,\"elapsedMs\":2650}\n ✓ 121 tests/e2e/texture-8k-performance.spec.ts:3:1 decodes, uploads and renders a validated 8K texture in Chromium (4.0s)\n ✓ 122 tests/e2e/v1-user-loop.spec.ts:6:1 completes the V1 import-edit-save-restart-reopen-GLB loop (2.3s)\n\n Slow test file: tests/e2e/smoke.spec.ts (5.5m)\n Consider running tests from slow files in parallel. See: https://playwright.dev/docs/test-parallel\n 122 passed (6.0m)\n\n> blender-web-editor@0.1.0 test:browser\n> playwright test --config playwright.release.config.ts\n\n\nRunning 3 tests using 1 worker\n\n ✓ 1 [chromium] tests/e2e/cross-browser.spec.ts:6:1 boots the offline engine, renders SceneIR and performs a Main edit (2.7s)\n ✓ 2 [chromium] tests/e2e/cross-browser.spec.ts:32:1 keeps content-addressed asset recovery available in Chromium (1.1s)\n ✓ 3 [chromium] tests/e2e/cross-browser.spec.ts:51:1 keeps the committed project after quota failure and Worker restart in Chromium (1.1s)\n\n 3 passed (6.5s)\n\n[WebServer] (node:2315163) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2315175) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2315221) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2321103) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2321115) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8"
]
@@ -561,12 +736,45 @@
],
"command": "npm --prefix web run test:release-performance",
"exitCode": 0,
"durationMs": 93399,
"output": "> blender-web-editor@0.1.0 test:release-performance\n> node ../tools/web/check-release-performance.mjs\n\nrelease-performance-ok [{\"target\":100000,\"ratio\":0.9,\"outputTriangles\":89999,\"elapsedMs\":92308,\"heapBytes\":67108864},{\"target\":1000000,\"ratio\":1,\"outputTriangles\":1000000,\"elapsedMs\":540,\"heapBytes\":346554368}]\n\nHeap resize call from 67108864 to 80543744 took 0.22668800000974443 msecs. Success: true\nHeap resize call from 80543744 to 96665600 took 0.08722699999634642 msecs. Success: true\nHeap resize call from 96665600 to 115998720 took 0.04216300000553019 msecs. Success: true\nHeap resize call from 115998720 to 139198464 took 1.3969159999978729 msecs. Success: true\nHeap resize call from 139198464 to 167051264 took 1.700714999999036 msecs. Success: true\nHeap resize call from 167051264 to 200474624 took 1.4438179999997374 msecs. Success: true\nHeap resize call from 200474624 to 240582656 took 1.4305880000028992 msecs. Success: true\nHeap resize call from 240582656 to 288751616 took 1.5049569999973755 msecs. Success: true\nHeap resize call from 288751616 to 346554368 took 1.4472269999969285 msecs. Success: true",
"durationMs": 84015,
"output": "> blender-web-editor@0.1.0 test:release-performance\n> node ../tools/web/check-release-performance.mjs\n\nrelease-performance-ok [{\"target\":100000,\"ratio\":0.9,\"inputTriangles\":100000,\"outputTriangles\":89999,\"elapsedMs\":82975,\"peakHeapBytes\":67108864,\"releasedPointerCount\":10,\"recoveryAllocation\":true},{\"target\":1000000,\"ratio\":1,\"inputTriangles\":1000000,\"outputTriangles\":1000000,\"elapsedMs\":518,\"peakHeapBytes\":346554368,\"releasedPointerCount\":10,\"recoveryAllocation\":true}]\n\nHeap resize call from 67108864 to 80543744 took 0.18726400000741705 msecs. Success: true\nHeap resize call from 80543744 to 96665600 took 0.08590900000126567 msecs. Success: true\nHeap resize call from 96665600 to 115998720 took 0.04599199999938719 msecs. Success: true\nHeap resize call from 115998720 to 139198464 took 0.059525000004214235 msecs. Success: true\nHeap resize call from 139198464 to 167051264 took 1.4840919999987818 msecs. Success: true\nHeap resize call from 167051264 to 200474624 took 1.4312449999997625 msecs. Success: true\nHeap resize call from 200474624 to 240582656 took 1.4865769999887561 msecs. Success: true\nHeap resize call from 240582656 to 288751616 took 1.4735839999921154 msecs. Success: true\nHeap resize call from 288751616 to 346554368 took 1.4832890000107 msecs. Success: true",
"artifactSha256": [
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8"
]
},
{
"id": "geometry-10m",
"fields": [
"performance.geometry10M"
],
"command": "npm --prefix web run test:geometry-10m-performance",
"exitCode": 0,
"durationMs": 5565,
"output": "> blender-web-editor@0.1.0 test:geometry-10m-performance\n> playwright test --config playwright.config.ts tests/e2e/geometry-10m-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\ngeometry-10m-performance {\"triangleCount\":10000000,\"chunkCount\":40,\"transferredBytes\":180492000,\"peakWorkingSetBytes\":4512300,\"manifestSha256\":\"224738e0c3aa28ae42167bf8974455a3d0f940f59f19eb51c5768f435c66b24f\",\"firstInteractiveMs\":1002,\"chunksAtFirstInteractive\":18,\"estimatedGpuBytes\":1124880,\"wasmAllocatedBytes\":0,\"baselineHeapBytes\":40421007,\"peakHeapBytes\":128051538,\"afterHeapBytes\":99970221,\"cacheDeleted\":true,\"recoveryVisible\":2372,\"cancelLatencyMs\":51,\"elapsedMs\":1681}\n ✓ 1 tests/e2e/geometry-10m-performance.spec.ts:3:1 streams ten-million-triangle SceneIR ranges into a bounded interactive LOD (3.2s)\n\n 1 passed (4.7s)\n\n[WebServer] (node:2321602) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2321614) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2321660) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"6f2708499d59a5e0e90e4581359d6131428c22da0da1543bb4aa0fc38e5e6b29",
"04743733e385f33efbb144b0c6cd831a21840dad9b6e0715e8a73791d8a95e33",
"969bda967852622b78bd5c0a56b0c0832bf1e46b19fa09b6b26316f2c367fdaf",
"f7501e2b4ad6f8df11167197b0d10f77f16b9b385459937011ab486d6b145157"
]
},
{
"id": "long-media",
"fields": [
"performance.longMedia"
],
"command": "npm --prefix web run test:long-media-performance",
"exitCode": 0,
"durationMs": 4707,
"output": "> blender-web-editor@0.1.0 test:long-media-performance\n> playwright test --config playwright.config.ts tests/e2e/long-media-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\nlong-media-performance {\"frameCount\":1000000,\"stripCount\":10001,\"referenceCount\":11914,\"indexBuildMs\":207,\"coldSeekMs\":1,\"hotSeekMs\":0,\"randomSeekMs\":6,\"cacheBytes\":16305,\"evictions\":71,\"releasedCacheBytes\":16305,\"cacheBytesAfterDispose\":0,\"restartedReleasedCacheBytes\":16305,\"restartedCacheBytesAfterDispose\":0,\"workerRestartRecovered\":true,\"manifestSha256\":\"d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d\",\"elapsedMs\":861}\n ✓ 1 tests/e2e/long-media-performance.spec.ts:8:1 indexes, seeks, cancels and reopens a bounded one-million-frame media timeline (2.3s)\n\n 1 passed (3.9s)\n\n[WebServer] (node:2321890) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2321902) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2321948) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"3427a057e564c9e1b16d464d06e65cde90ae8557f4f51cb98ad43774658e8dca",
"70faaf13e2a55af6ff40951ef29a27f7b391ed6dd55310cb2283678cc9b8a993",
"42be8d81cb0fc170986fcf187666e8bd8b93fb5992d7e13d1d569ba1a30da1f3",
"295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed",
"56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9"
]
},
{
"id": "simulation-cache",
"fields": [
@@ -574,8 +782,8 @@
],
"command": "npm --prefix web run test:simulation-cache-performance",
"exitCode": 0,
"durationMs": 7549,
"output": "> blender-web-editor@0.1.0 test:simulation-cache-performance\n> playwright test --config playwright.config.ts tests/e2e/simulation-cache-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/simulation-cache-performance.spec.ts:3:1 meets the Chromium OPFS Simulation cache playback performance gate (5.1s)\n\n 1 passed (6.7s)\n\n[WebServer] (node:1994548) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1994560) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 7118,
"output": "> blender-web-editor@0.1.0 test:simulation-cache-performance\n> playwright test --config playwright.config.ts tests/e2e/simulation-cache-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\nsimulation-cache-performance {\"backend\":\"opfs\",\"frameCount\":600,\"byteLength\":52800,\"status\":\"COMPLETED\",\"appliedFrames\":600,\"lastFrame\":600,\"publishedFrames\":600,\"lastTranslation\":60,\"storeMs\":78,\"playbackMs\":3171,\"elapsedMs\":3249,\"peakCacheBytes\":52800,\"pendingRequestsAfterTerminate\":0,\"workerRestartRecovered\":true}\n ✓ 1 tests/e2e/simulation-cache-performance.spec.ts:3:1 meets the Chromium OPFS Simulation cache playback performance gate (4.7s)\n\n 1 passed (6.3s)\n\n[WebServer] (node:2322188) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2322200) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2322246) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"c8074425a4556c8d08c2382dc9fd7be8aa6078d11a45640c4b19bf6ef45aa590",
"331d5c6a59dc80a12cc7105abbd60d25a64a113809eae9ad3d4061d81ec5f59f",
@@ -589,7 +797,7 @@
],
"command": "npm --prefix web run test:malicious-blends",
"exitCode": 0,
"durationMs": 541,
"durationMs": 510,
"output": "> blender-web-editor@0.1.0 test:malicious-blends\n> node ../tools/web/check-malicious-blends.mjs\n\nmalicious-blends-ok rejected=5",
"artifactSha256": [
"6fcc55bda74da8c95da96ba068b60339bf60ca44147ff8000fbb95d296db2a73"
@@ -602,8 +810,8 @@
],
"command": "npm --prefix web run test:network-interruption",
"exitCode": 0,
"durationMs": 5632,
"output": "> blender-web-editor@0.1.0 test:network-interruption\n> playwright test --config playwright.config.ts tests/e2e/network-interruption.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/network-interruption.spec.ts:6:1 keeps Main edit and save-reopen available during a Chromium network interruption (3.1s)\n\n 1 passed (4.7s)\n\n[WebServer] (node:1994872) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1994884) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 5448,
"output": "> blender-web-editor@0.1.0 test:network-interruption\n> playwright test --config playwright.config.ts tests/e2e/network-interruption.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/network-interruption.spec.ts:6:1 keeps Main edit and save-reopen available during a Chromium network interruption (3.0s)\n\n 1 passed (4.6s)\n\n[WebServer] (node:2322506) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2322518) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"b1c03863f44026d44691042536ff1d1095617a35c8d767cc70372e5cbdc70db5",
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8"
@@ -616,13 +824,32 @@
],
"command": "npm --prefix web run test:device-loss",
"exitCode": 0,
"durationMs": 5138,
"output": "> blender-web-editor@0.1.0 test:device-loss\n> playwright test --config playwright.config.ts tests/e2e/device-loss.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/device-loss.spec.ts:6:1 recovers the main-thread Chromium viewport after a real WebGL context loss (2.6s)\n\n 1 passed (4.2s)\n\n[WebServer] (node:1995187) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1995199) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 4948,
"output": "> blender-web-editor@0.1.0 test:device-loss\n> playwright test --config playwright.config.ts tests/e2e/device-loss.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/device-loss.spec.ts:6:1 recovers the main-thread Chromium viewport after a real WebGL context loss (2.5s)\n\n 1 passed (4.1s)\n\n[WebServer] (node:2322781) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2322793) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"3b18c884f3baed68d4deb72feafbd7fe8b4af7935aaf02206ccc583ea9c527e2",
"3878ce0f4d25cfef35eefe90070df56e06e2d597cd1bdf3655aed707450590e9",
"76d781c0f8508ee68fde09b1bb0d3f7db963e3e2be7d10d60708af2a6a582c24"
]
},
{
"id": "oom-recovery",
"fields": [
"faults.oom"
],
"command": "npm --prefix web run test:oom-recovery",
"exitCode": 0,
"durationMs": 4897,
"output": "> blender-web-editor@0.1.0 test:oom-recovery\n> playwright test --config playwright.config.ts tests/e2e/oom-recovery.spec.ts\n\n\nRunning 1 test using 1 worker\n\noom-recovery [{\"scenario\":\"WASM_MAIN\",\"errors\":[\"WASM_OUT_OF_MEMORY@WASM_OPEN_INPUT\",\"WASM_OUT_OF_MEMORY@MAIN_EDIT_COMMAND\",\"WASM_OUT_OF_MEMORY@WASM_SAVE_RESULT\"],\"revision\":[2,2],\"hashStable\":true,\"releasedBytes\":205638,\"temporaryResourcesAfter\":0,\"recovered\":true},{\"scenario\":\"OPFS_STAGING\",\"errors\":[\"STORAGE_QUOTA@OPFS_STAGING_WRITE\"],\"revision\":[7,7],\"hashStable\":true,\"releasedBytes\":47804,\"temporaryResourcesAfter\":0,\"recovered\":true},{\"scenario\":\"GPU_RESOURCES\",\"errors\":[\"GPU_TEXTURE_BUDGET_EXCEEDED@GPU_TEXTURE_UPLOAD\"],\"revision\":[0,0],\"hashStable\":true,\"releasedBytes\":36,\"temporaryResourcesAfter\":0,\"recovered\":true},{\"scenario\":\"NANOVDB_RESIDENT\",\"errors\":[\"NANOVDB_GPU_BUDGET_EXCEEDED@NANOVDB_PAGE_TABLE\"],\"revision\":[0,0],\"hashStable\":true,\"releasedBytes\":65536,\"temporaryResourcesAfter\":0,\"recovered\":true}]\n ✓ 1 tests/e2e/oom-recovery.spec.ts:7:1 recovers deterministically from WASM, OPFS, GPU and NanoVDB allocation faults (2.5s)\n\n 1 passed (4.0s)\n\n[WebServer] (node:2323065) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2323077) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2323123) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"d5fe9184c693eb5b5d3ede8797cb09859669f43d9a82b55b9591cabb2a2c4a56",
"50fa383e7c7b3cf7a87eb652671dd9c180e30c86b99a8b129726d8af2f76a5a0",
"e6cec43ffa61b97cadf0926cd6468257cd47a38b05a121c4ea4a252fe257e585",
"3d0b7d7b98d1c9ffeda1b4c4ea43c4ced400e36814d5a819f7d26df2bd25d873",
"aac12afe4c92684dc74d546701d3f9e56f305f5ceb83fc4699bf8a06e6abed1b",
"ce2a44329068e6e9aafbb37259e8a47c1d9e306e563cae8b957d0b2f6b2b61cb",
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8"
]
},
{
"id": "texture-4k",
"fields": [
@@ -630,8 +857,8 @@
],
"command": "npm --prefix web run test:texture-4k-performance",
"exitCode": 0,
"durationMs": 4857,
"output": "> blender-web-editor@0.1.0 test:texture-4k-performance\n> playwright test --config playwright.config.ts tests/e2e/texture-4k-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/texture-4k-performance.spec.ts:3:1 decodes, uploads and renders a validated 4K texture in Chromium (2.3s)\n\n 1 passed (4.0s)\n\n[WebServer] (node:1995463) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1995475) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 4670,
"output": "> blender-web-editor@0.1.0 test:texture-4k-performance\n> playwright test --config playwright.config.ts tests/e2e/texture-4k-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\ntexture-4k-performance {\"width\":4096,\"height\":4096,\"status\":{\"loaded\":1,\"rejected\":0,\"bytes\":254670,\"errors\":[],\"errorCodes\":[]},\"byteLength\":254670,\"estimatedPeakGpuBytes\":67108864,\"released\":true,\"recoveryVisible\":true,\"colored\":4096,\"elapsedMs\":668}\n ✓ 1 tests/e2e/texture-4k-performance.spec.ts:3:1 decodes, uploads and renders a validated 4K texture in Chromium (2.3s)\n\n 1 passed (3.8s)\n\n[WebServer] (node:2323343) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2323355) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2323401) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"093588bcdf08f8b5a5aa8baceddbd7b499c43e5acda7f793b34ac54c79782490",
"75f2f33753a160f60acc56ad850bc149cef7fa2b54a42d5e75cf22402c9ac91d"
@@ -644,8 +871,8 @@
],
"command": "npm --prefix web run test:texture-8k-performance",
"exitCode": 0,
"durationMs": 7112,
"output": "> blender-web-editor@0.1.0 test:texture-8k-performance\n> playwright test --config playwright.config.ts tests/e2e/texture-8k-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/texture-8k-performance.spec.ts:3:1 decodes, uploads and renders a validated 8K texture in Chromium (4.6s)\n\n 1 passed (6.2s)\n\n[WebServer] (node:1995736) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1995748) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 6818,
"output": "> blender-web-editor@0.1.0 test:texture-8k-performance\n> playwright test --config playwright.config.ts tests/e2e/texture-8k-performance.spec.ts\n\n\nRunning 1 test using 1 worker\n\ntexture-8k-performance {\"supported\":true,\"maxTextureSize\":8192,\"width\":8192,\"height\":8192,\"status\":{\"loaded\":1,\"rejected\":0,\"bytes\":972714,\"errors\":[],\"errorCodes\":[]},\"byteLength\":972714,\"estimatedPeakGpuBytes\":268435456,\"released\":true,\"recoveryVisible\":true,\"colored\":4096,\"elapsedMs\":2579}\n ✓ 1 tests/e2e/texture-8k-performance.spec.ts:3:1 decodes, uploads and renders a validated 8K texture in Chromium (4.4s)\n\n 1 passed (6.0s)\n\n[WebServer] (node:2323628) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2323640) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n(node:2323686) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n(Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"093588bcdf08f8b5a5aa8baceddbd7b499c43e5acda7f793b34ac54c79782490",
"75f2f33753a160f60acc56ad850bc149cef7fa2b54a42d5e75cf22402c9ac91d"
@@ -658,8 +885,8 @@
],
"command": "WEB_TEST_PORT=5323 npm --prefix web run test:asset-library",
"exitCode": 0,
"durationMs": 4198,
"output": "> blender-web-editor@0.1.0 test:asset-library\n> playwright test --config playwright.config.ts -g \"N-023 asset\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:969:1 validates N-023 asset catalogs, library graphs, archive budgets and IO gates (1.5s)\n\n 1 passed (3.3s)\n\n[WebServer] (node:1996023) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:1996035) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"durationMs": 4136,
"output": "> blender-web-editor@0.1.0 test:asset-library\n> playwright test --config playwright.config.ts -g \"N-023 asset\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:969:1 validates N-023 asset catalogs, library graphs, archive budgets and IO gates (1.5s)\n\n 1 passed (3.3s)\n\n[WebServer] (node:2323896) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2323908) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4"
]
@@ -669,10 +896,10 @@
"fields": [],
"command": "npm --prefix web run test:release-package",
"exitCode": 0,
"durationMs": 5619,
"output": "> blender-web-editor@0.1.0 test:release-package\n> npm run build && npm run release:sbom && node ../tools/web/check-release-package.mjs\n\n\n> blender-web-editor@0.1.0 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 51 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.45 kB │ gzip: 0.29 kB\ndist/assets/storage.worker-hjSYzFAQ.js 36.77 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DXSzkSWB.js 592.78 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-Ce6I8dqP.js 951.79 kB │ gzip: 256.93 kB\n\n✓ built in 623ms\n\n> blender-web-editor@0.1.0 release:sbom\n> node ../tools/web/generate-sbom.mjs\n\nsbom-ok packages=150 sha256=8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30\nrelease-package-ok files=10 bytes=32067024\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.",
"durationMs": 5339,
"output": "> blender-web-editor@0.1.0 test:release-package\n> npm run build && npm run release:sbom && node ../tools/web/check-release-package.mjs\n\n\n> blender-web-editor@0.1.0 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 54 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.30 kB\ndist/assets/storage.worker-COUVNftM.js 36.81 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DZmZ4IRD.js 601.32 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-Cv6ZsBTp.js 973.17 kB │ gzip: 263.15 kB\n\n✓ built in 562ms\n\n> blender-web-editor@0.1.0 release:sbom\n> node ../tools/web/generate-sbom.mjs\n\nsbom-ok packages=150 sha256=aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060\nrelease-package-ok files=10 bytes=32096983\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.",
"artifactSha256": [
"8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30",
"aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060",
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8"
]
},
@@ -684,12 +911,25 @@
],
"command": "npm --prefix web run release:offline",
"exitCode": 0,
"durationMs": 41303,
"output": "> blender-web-editor@0.1.0 release:offline\n> npm run build && node ../tools/web/check-offline-reproducibility.mjs\n\n\n> blender-web-editor@0.1.0 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 51 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.45 kB │ gzip: 0.29 kB\ndist/assets/storage.worker-hjSYzFAQ.js 36.77 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DXSzkSWB.js 592.78 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-Ce6I8dqP.js 951.79 kB │ gzip: 256.93 kB\n\n✓ built in 723ms\nsbom-ok packages=150 sha256=8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30\noffline-release-ok binary=7575306 source=205785402 sha256=078f45513ab579800ab3688f7c5e35d31be0d9dc283db5cee1a68d77553bfb34\nsbom-ok packages=150 sha256=8b04215a993f62ba64e0adcb1ba36ac830191f8215bf8cf2681fdf7bbb63da30\noffline-release-ok binary=7575306 source=205785402 sha256=078f45513ab579800ab3688f7c5e35d31be0d9dc283db5cee1a68d77553bfb34\noffline-reproducibility-ok binary=078f45513ab579800ab3688f7c5e35d31be0d9dc283db5cee1a68d77553bfb34 source=ca97bef8cbe7943586e7af1df631775ce6ea041677c8bbb98da2fcc73baa9e1a\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.",
"durationMs": 36970,
"output": "> blender-web-editor@0.1.0 release:offline\n> npm run build && node ../tools/web/check-offline-reproducibility.mjs\n\n\n> blender-web-editor@0.1.0 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 54 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.30 kB\ndist/assets/storage.worker-COUVNftM.js 36.81 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DZmZ4IRD.js 601.32 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-Cv6ZsBTp.js 973.17 kB │ gzip: 263.15 kB\n\n✓ built in 581ms\nsbom-ok packages=150 sha256=aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060\noffline-release-ok binary=7585557 source=205827673 sha256=f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549\nsbom-ok packages=150 sha256=aa50a5b98ebb51c98c6eced2ec32dff96b8df6d90e10cc101ce9e8eb44695060\noffline-release-ok binary=7585557 source=205827673 sha256=f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549\noffline-reproducibility-ok binary=f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549 source=9a9c1b505c68a618de842be7c248f628b104b11b80e8a40d111f62540fcba327\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit.",
"artifactSha256": [
"078f45513ab579800ab3688f7c5e35d31be0d9dc283db5cee1a68d77553bfb34",
"ca97bef8cbe7943586e7af1df631775ce6ea041677c8bbb98da2fcc73baa9e1a",
"39c7f9be555f2727519a733237de22e66a33d33ac1b0aecfacd2ae695de10b28"
"f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549",
"9a9c1b505c68a618de842be7c248f628b104b11b80e8a40d111f62540fcba327",
"288b0bb969e670b458059adf8897f87bafc76a87a479c8aa0a305905accf2de9"
]
},
{
"id": "v1-user-loop",
"fields": [],
"command": "npm --prefix web run test:v1-user-loop && npm --prefix web run test:v1-acceptance-evidence",
"exitCode": 0,
"durationMs": 5516,
"output": "> blender-web-editor@0.1.0 test:v1-user-loop\n> playwright test --config playwright.config.ts tests/e2e/v1-user-loop.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/v1-user-loop.spec.ts:6:1 completes the V1 import-edit-save-restart-reopen-GLB loop (2.8s)\n\n 1 passed (4.4s)\n\n> blender-web-editor@0.1.0 test:v1-acceptance-evidence\n> node ../tools/web/check-v1-acceptance-evidence.mjs\n\nv1-acceptance-evidence-ok families=12 declarations=50 unique=49 sha256=d5fb0a7b2df92b076f8b05ccd68903d60e3352b9423b0c1404065f6cbcbdd45c\n\n[WebServer] (node:2396656) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)\n[WebServer] (node:2396668) Warning: The 'NO_COLOR' env is ignored due to the 'FORCE_COLOR' env being set.\n[WebServer] (Use `node --trace-warnings ...` to show where the warning was created)",
"artifactSha256": [
"3defbaa426195254fcaefaddabf9b128002a94def03aa255513c33eaf4ac0926",
"5d87a9ea57bd7a1888f16a5f4306e1c6d3a1bdfcf832c9485fe8518aac528fd8",
"d5fb0a7b2df92b076f8b05ccd68903d60e3352b9423b0c1404065f6cbcbdd45c"
]
}
]

View File

@@ -0,0 +1,572 @@
{
"schemaVersion": 1,
"source": "docs/status/parity-ledger.json",
"sourceSha256": "1e16518a25c6b6bed4ab7f605ea1ad5babe529f2842c858b20ae6daae30bd478",
"package": "web/package.json",
"packageSha256": "a7ee993997c8d62effcdd3bb994f16656424e6a5e976c3d3b4e2951bd4109710",
"toolSourceSha256": {
"tools/web/v1-acceptance-lib.mjs": "48005c8fc9d97955513e104ee1b8950cab2c3520c166eff4358e10bd7318f535",
"tools/web/check-v1-acceptance-coverage.mjs": "42b713f9b622851328266584e3a8ba2231e0393141ba4a8ae091a97337d9e03e",
"tools/web/run-v1-acceptance.mjs": "7e87893e42c6c848c2bc487340712ba4ec151e63e49b2bbc76e6a0c51d839da3",
"tools/web/check-v1-acceptance-evidence.mjs": "af1a3029a1148e8e5d346d66f3a58f7063b73fee8dd95e6e41ed0504ed56dfea"
},
"generatedAt": "2026-08-15T13:31:45.207Z",
"environment": {
"node": "v20.19.2",
"npm": "9.2.0",
"chromium": "Google Chrome 150.0.7871.128",
"blenderBin": "BLENDER_BIN",
"blenderExecutableSha256": "83e8261eace07a5337f71b52d156c1eece1a6ba913403cc6406182ae58bacf27",
"blenderArchiveSha256": "96f6c181a30f4950607839dc84d42a354b250d8a0231b098b59b7bc69c351c48",
"blender": "ACCEPTANCE_BLENDER=5.2.0 LTS;USD=True"
},
"summary": {
"families": 12,
"declarations": 50,
"unique": 49,
"passed": 49,
"failed": 0
},
"results": [
{
"token": "web:test:nonmesh-roundtrip",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-roundtrip",
"exitCode": 0,
"durationMs": 1996,
"outputSha256": "828a1171ff5444aba7bddaa0f2f47e1004881e0595b856b23b1e4eb46aa09c01",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-roundtrip\n> node ../tools/web/check-nonmesh-roundtrip.mjs\n\nnonmesh-roundtrip-ok types=CURVE,SURFACE,FONT,METABALL,VOLUME multispline-create-delete-bulk-handle-cyclic-surface-2d-topology-font-links-volume-properties=passed undo-redo=passed save-reopen=passed\n\nweb-depsgraph-stage=runtime-init-begin\nweb-depsgraph-stage=runtime-init-end\nweb-depsgraph-stage=blend-read-end\nweb-depsgraph-stage=view-layer-sync-end\nweb-depsgraph-data scene=0xaf0538 master=0xa18618 nodetree=0 world=0 collections=0\nweb-depsgraph-stage=global-main-swap-end\nweb-depsgraph-stage=graph-new-end\nweb-depsgraph-stage=graph-build-end\nweb-depsgraph-stage=frame-change-end\nweb-depsgraph-stage=graph-free-end\nweb-depsgraph-stage=global-main-restore-end\nweb-depsgraph-stage=blend-free-end"
},
{
"token": "web:test:nonmesh-desktop-golden",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-desktop-golden",
"exitCode": 0,
"durationMs": 920,
"outputSha256": "54cfd9cdec5a7885b22768bce6652410fc3709da1f4c943bcfa6655637435215",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-desktop-golden\n> node ../tools/web/check-nonmesh-desktop-golden.mjs\n\nnonmesh-desktop-golden-ok fixture=nonmesh_scene.blend geometries=4\n\nweb-depsgraph-stage=runtime-init-begin\nweb-depsgraph-stage=runtime-init-end\nweb-depsgraph-stage=blend-read-end\nweb-depsgraph-stage=view-layer-sync-end\nweb-depsgraph-data scene=0xa71220 master=0xacb470 nodetree=0 world=0 collections=0\nweb-depsgraph-stage=global-main-swap-end\nweb-depsgraph-stage=graph-new-end\nweb-depsgraph-stage=graph-build-end\nweb-depsgraph-stage=frame-change-end\nweb-depsgraph-stage=graph-free-end\nweb-depsgraph-stage=global-main-restore-end\nweb-depsgraph-stage=blend-free-end"
},
{
"token": "web:test:nonmesh-glb-blender-roundtrip",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-glb-blender-roundtrip",
"exitCode": 0,
"durationMs": 2200,
"outputSha256": "c9ba8f68095690f4235b19da1645416f912fc0a7716f11f75c2ee8a63f300319",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-glb-blender-roundtrip\n> node ../tools/web/check-nonmesh-glb-blender-roundtrip.mjs\n\nnonmesh-glb-blender-roundtrip-ok bytes=53172 objects=7 points=1 lines=3 triangles=3\n\nweb-depsgraph-stage=runtime-init-begin\nweb-depsgraph-stage=runtime-init-end\nweb-depsgraph-stage=blend-read-end\nweb-depsgraph-stage=view-layer-sync-end\nweb-depsgraph-data scene=0xa71260 master=0xacb460 nodetree=0 world=0 collections=0\nweb-depsgraph-stage=global-main-swap-end\nweb-depsgraph-stage=graph-new-end\nweb-depsgraph-stage=graph-build-end\nweb-depsgraph-stage=frame-change-end\nweb-depsgraph-stage=graph-free-end\nweb-depsgraph-stage=global-main-restore-end\nweb-depsgraph-stage=blend-free-end"
},
{
"token": "web:test:nonmesh-usd-serialization",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-usd-serialization",
"exitCode": 0,
"durationMs": 2532,
"outputSha256": "7a76fabed285b2fbd3b528fae02dd5696327b5068fd6909e504666d5022b80c2",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-usd-serialization\n> USD_DESKTOP_REQUIRED=0 node ../tools/web/check-nonmesh-usd-blender-roundtrip.mjs\n\nnonmesh-usd-blender-roundtrip-ok bytes=65405 objects=7 points=1 basisCurves=3 meshes=3\n\nweb-depsgraph-stage=runtime-init-begin\nweb-depsgraph-stage=runtime-init-end\nweb-depsgraph-stage=blend-read-end\nweb-depsgraph-stage=view-layer-sync-end\nweb-depsgraph-data scene=0xa71260 master=0xacb460 nodetree=0 world=0 collections=0\nweb-depsgraph-stage=global-main-swap-end\nweb-depsgraph-stage=graph-new-end\nweb-depsgraph-stage=graph-build-end\nweb-depsgraph-stage=frame-change-end\nweb-depsgraph-stage=graph-free-end\nweb-depsgraph-stage=global-main-restore-end\nweb-depsgraph-stage=blend-free-end"
},
{
"token": "web:test:nonmesh-usd-blender-roundtrip",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-usd-blender-roundtrip",
"exitCode": 0,
"durationMs": 2526,
"outputSha256": "0d4e89b5d022962e4effc43c53385b825c21217dd14850e36f6db7e20bd3511a",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-usd-blender-roundtrip\n> USD_DESKTOP_REQUIRED=1 node ../tools/web/check-nonmesh-usd-blender-roundtrip.mjs\n\nnonmesh-usd-blender-roundtrip-ok bytes=65405 objects=7 points=1 basisCurves=3 meshes=3\n\nweb-depsgraph-stage=runtime-init-begin\nweb-depsgraph-stage=runtime-init-end\nweb-depsgraph-stage=blend-read-end\nweb-depsgraph-stage=view-layer-sync-end\nweb-depsgraph-data scene=0xa71260 master=0xacb460 nodetree=0 world=0 collections=0\nweb-depsgraph-stage=global-main-swap-end\nweb-depsgraph-stage=graph-new-end\nweb-depsgraph-stage=graph-build-end\nweb-depsgraph-stage=frame-change-end\nweb-depsgraph-stage=graph-free-end\nweb-depsgraph-stage=global-main-restore-end\nweb-depsgraph-stage=blend-free-end"
},
{
"token": "web:test:nonmesh-binary",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-binary",
"exitCode": 0,
"durationMs": 4623,
"outputSha256": "53f1182423f3cdcb5d0ba00f5827ba1d5ed5f880151ef5d746ae3893020ed771",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-binary\n> playwright test --config playwright.config.ts -g \"one-million-point binary transfer gate\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:402:1 enforces the N-015 one-million-point binary transfer gate (1.6s)\n\n 1 passed (3.7s)"
},
{
"token": "web:test:selection-history",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:selection-history",
"exitCode": 0,
"durationMs": 4642,
"outputSha256": "9b8035591b1b4f7f665688d9190d7191d12a5cd6d204fcb271d46375817cb3a2",
"output": "> blender-web-editor@0.1.0-rc.1 test:selection-history\n> playwright test --config playwright.config.ts -g \"N-015 selection history\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1060:1 keeps N-015 selection history bounded and rejects stale raycast hits (1.7s)\n\n 1 passed (3.7s)"
},
{
"token": "web:test:nonmesh-interaction",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:nonmesh-interaction",
"exitCode": 0,
"durationMs": 4538,
"outputSha256": "0794b7d8acb9c24a8de963a9a76fbbf2919228cc637d79c2b101a8280698df63",
"output": "> blender-web-editor@0.1.0-rc.1 test:nonmesh-interaction\n> playwright test --config playwright.config.ts -g \"N-015 curve gizmo interaction\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1081:1 validates the N-015 curve gizmo interaction transaction boundary (1.8s)\n\n 1 passed (3.7s)"
},
{
"token": "web:test:vdb",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb",
"exitCode": 0,
"durationMs": 4103,
"outputSha256": "974bedb7c4fa970d79b6140d1527b39de4c99f063c9c848684d064efcdc8723b",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb\n> playwright test --config playwright.config.ts -g \"VDB conversion boundary\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1156:1 validates the VDB conversion boundary and NanoVDB streaming contract (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:vdb-availability",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-availability",
"exitCode": 0,
"durationMs": 370,
"outputSha256": "3d1fce4839232bd9c5c7dc42ea2e462be9ade0866d1296cfc5897afadd6b39fc",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-availability\n> node ../tools/web/check-vdb-availability.mjs\n\nvdb-availability-ok core=READY release=BLOCKED browser-openvdb=disabled desktop=ready server=ready opfs=ready webgpu-core=ready main-roundtrip=ready viewport=blocked advanced-material=blocked resources=9"
},
{
"token": "web:test:vdb-native",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-native",
"exitCode": 0,
"durationMs": 1341,
"outputSha256": "52abc08ddc600e3c00842cb2619b3c4b66b113e8bd83d96db910ac1b95d5d012",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-native\n> node ../tools/web/check-vdb-native-pipeline.mjs\n\nvdb-native-pipeline-ok resources=14 converter=openvdb13-nanovdb32 conversion-deterministic=1 semantic-deterministic=1 official-deterministic=1 malformed=blocked browser-openvdb=off"
},
{
"token": "web:test:vdb-server",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-server",
"exitCode": 0,
"durationMs": 691,
"outputSha256": "d958e3ed39b5660b5e8d66d7171b651e97f0e87c1c7b9769709478925a91907f",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-server\n> node ../tools/web/check-vdb-server-job.mjs\n\nvdb-server-job-ok job=vdb-b59105490496a2fee0dda646 bytes=2563944 idempotent=1 signed=1 isolated=1 cancelled=1 atomic=1 desktop-server-hash=equal"
},
{
"token": "web:test:vdb-opfs",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-opfs",
"exitCode": 0,
"durationMs": 5672,
"outputSha256": "976954270c006f573cd70022ab4cd5c500ff9d4ff895aabde4d1eb8cc1895430",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-opfs\n> playwright test --config playwright.config.ts -g \"hash-bound NanoVDB project through OPFS\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1206:1 commits and reopens a hash-bound NanoVDB project through OPFS (2.9s)\n\n 1 passed (4.8s)"
},
{
"token": "web:test:vdb-webgpu",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-webgpu",
"exitCode": 0,
"durationMs": 64026,
"outputSha256": "d7d13bf8104f9fef4a92836d37b839fc4ec24135ad6424e7d36eddcf7b45c070",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-webgpu\n> playwright test --config playwright.config.ts -g \"real NanoVDB Float32 tree with WebGPU\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1232:1 samples and integrates a real NanoVDB Float32 tree with WebGPU (1.0m)\n\n 1 passed (1.1m)"
},
{
"token": "web:test:vdb-viewport",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:vdb-viewport",
"exitCode": 0,
"durationMs": 122887,
"outputSha256": "215608ae74264a438f7e93caa25758eff946f429de284021b40d202ba78756a5",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-viewport\n> playwright test --config playwright.config.ts -g \"both production viewport backends\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1264:1 renders a real NanoVDB volume through both production viewport backends (2.0m)\n\n 1 passed (2.0m)"
},
{
"token": "web:test:vdb-faults",
"familyIds": [
"N-015",
"N-023"
],
"command": "npm --prefix web run test:vdb-faults",
"exitCode": 0,
"durationMs": 6038,
"outputSha256": "6ab0e4445b5c80adb967c1e573f12339ee9e5bae3b5585ff42218a266df6d28c",
"output": "> blender-web-editor@0.1.0-rc.1 test:vdb-faults\n> playwright test --config playwright.config.ts -g \"NanoVDB paging from network, Worker and WebGPU device faults\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1344:1 recovers NanoVDB paging from network, Worker and WebGPU device faults (3.4s)\n\n 1 passed (5.2s)"
},
{
"token": "web:e2e:N-015|non-mesh",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-015|non-mesh\"",
"exitCode": 0,
"durationMs": 20310,
"outputSha256": "4c6c6c6b742c24240b0ea458175e2054ef5cc0357c02f06310cea664fa7d57dc",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-015|non-mesh\n\n\nRunning 10 tests using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:295:1 reads bounded non-mesh data blocks and previews supported geometry (2.6s)\n ✓ 2 tests/e2e/smoke.spec.ts:341:1 reports structured N-015 gates for previewable and resource-backed data (1.3s)\n ✓ 3 tests/e2e/smoke.spec.ts:402:1 enforces the N-015 one-million-point binary transfer gate (1.1s)\n ✓ 4 tests/e2e/smoke.spec.ts:417:1 rejects malformed N-015 binary chunks, integer overflow and budget overflow (947ms)\n ✓ 5 tests/e2e/smoke.spec.ts:1060:1 keeps N-015 selection history bounded and rejects stale raycast hits (974ms)\n ✓ 6 tests/e2e/smoke.spec.ts:1081:1 validates the N-015 curve gizmo interaction transaction boundary (1.4s)\n ✓ 7 tests/e2e/smoke.spec.ts:1098:40 previews an N-015 Curve handle drag and commits Main once in main-thread Chromium (3.4s)\n ✓ 8 tests/e2e/smoke.spec.ts:1098:40 previews an N-015 Curve handle drag and commits Main once in OffscreenCanvas Chromium (2.5s)\n ✓ 9 tests/e2e/smoke.spec.ts:1435:1 returns real Blender evaluations for legacy non-mesh geometry (1.6s)\n ✓ 10 tests/e2e/smoke.spec.ts:1467:1 keeps N-015 non-mesh previews in the OffscreenCanvas renderer (1.6s)\n\n 10 passed (19.5s)"
},
{
"token": "web:e2e:real OPFS quota",
"familyIds": [
"N-015"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"real OPFS quota\"",
"exitCode": 0,
"durationMs": 4212,
"outputSha256": "9ca11848c3935b40798325d6e54d68ec0e7c0d8eb880bb16ecba04a7ca85d3cc",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g real OPFS quota\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1595:1 enforces a real OPFS quota without replacing the committed project (1.5s)\n\n 1 passed (3.4s)"
},
{
"token": "web:test:grease-pencil",
"familyIds": [
"N-016"
],
"command": "npm --prefix web run test:grease-pencil",
"exitCode": 0,
"durationMs": 776,
"outputSha256": "c5419926826128e17eed9837fd237b2467eee712143f7f3f1c7eecc243fe6775",
"output": "> blender-web-editor@0.1.0-rc.1 test:grease-pencil\n> node ../tools/web/check-grease-pencil-roundtrip.mjs\n\ngrease-pencil-roundtrip-ok layer-frame-stroke=passed undo-redo=passed save-reopen=passed"
},
{
"token": "web:test:grease-pencil-editor",
"familyIds": [
"N-016"
],
"command": "npm --prefix web run test:grease-pencil-editor",
"exitCode": 0,
"durationMs": 4194,
"outputSha256": "71cfe010c8140d96ece7bddc359805a6dbcb54213224e78d40db436b9aef6605",
"output": "> blender-web-editor@0.1.0-rc.1 test:grease-pencil-editor\n> playwright test --config playwright.config.ts -g \"N-016 Grease Pencil editor context\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:510:1 validates the N-016 Grease Pencil editor context transaction boundary (1.5s)\n\n 1 passed (3.3s)"
},
{
"token": "web:e2e:N-016 Grease Pencil",
"familyIds": [
"N-016"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-016 Grease Pencil\"",
"exitCode": 0,
"durationMs": 21136,
"outputSha256": "feeb4c14236ca3766be2bcefb3bd7567074bc91940935c2731cdcd3e4a8b8f03",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-016 Grease Pencil\n\n\nRunning 12 tests using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:437:1 enforces the N-016 Grease Pencil layer/frame/drawing/stroke/point schema budget (1.5s)\n ✓ 2 tests/e2e/smoke.spec.ts:453:1 renders a bounded previous/next N-016 Grease Pencil onion-skin preview (914ms)\n ✓ 3 tests/e2e/smoke.spec.ts:485:3 renders the N-016 Grease Pencil current-frame strokes in main-thread Chromium (1.4s)\n ✓ 4 tests/e2e/smoke.spec.ts:485:3 renders the N-016 Grease Pencil current-frame strokes in OffscreenCanvas Chromium (1.4s)\n ✓ 5 tests/e2e/smoke.spec.ts:510:1 validates the N-016 Grease Pencil editor context transaction boundary (974ms)\n ✓ 6 tests/e2e/smoke.spec.ts:526:1 raycasts and highlights N-016 Grease Pencil points with stable drawing identity (1.4s)\n ✓ 7 tests/e2e/smoke.spec.ts:541:40 previews N-016 Grease Pencil points and commits Main once in main-thread Chromium (2.0s)\n ✓ 8 tests/e2e/smoke.spec.ts:541:40 previews N-016 Grease Pencil points and commits Main once in OffscreenCanvas Chromium (1.7s)\n ✓ 9 tests/e2e/smoke.spec.ts:565:1 edits N-016 Grease Pencil layers and frames from the bounded editor panel (1.7s)\n ✓ 10 tests/e2e/smoke.spec.ts:580:1 navigates real N-016 Grease Pencil drawing frames in the bounded Dope Sheet (1.8s)\n ✓ 11 tests/e2e/smoke.spec.ts:595:1 moves an N-016 Grease Pencil point through one revision-bound Main transaction (1.6s)\n ✓ 12 tests/e2e/smoke.spec.ts:606:1 reopens an edited N-016 Grease Pencil drawing after WebEngine Worker restart (1.5s)\n\n 12 passed (20.1s)"
},
{
"token": "web:test:paint-roundtrip",
"familyIds": [
"N-017"
],
"command": "npm --prefix web run test:paint-roundtrip",
"exitCode": 0,
"durationMs": 785,
"outputSha256": "d43258df5975ef194ba7c45c3758c8cac3e2ef89256510e4c1fedbc27f2b5b25",
"output": "> blender-web-editor@0.1.0-rc.1 test:paint-roundtrip\n> node ../tools/web/check-paint-roundtrip.mjs\n\npaint-roundtrip-ok vertex-color=passed vertex-weight-normalize=passed mirror-gate=passed undo-redo=passed save-reopen=passed"
},
{
"token": "web:e2e:N-017 paint",
"familyIds": [
"N-017"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-017 paint\"",
"exitCode": 0,
"durationMs": 4154,
"outputSha256": "204d84caabd73ba9be657483115ed1c2f681acf34ece9500ed5c0d5861c8fb7a",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-017 paint\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:648:1 enforces the N-017 paint stroke, bounded brush and UDIM patch budgets (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:e2e:N-018 physics",
"familyIds": [
"N-018"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-018 physics\"",
"exitCode": 0,
"durationMs": 4129,
"outputSha256": "cfc04a1e75861cfd87fd3b58ed016a9d2a85c854866fcfc314e97fbfe591d2fa",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-018 physics\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:746:1 validates the N-018 physics capability and cache manifests without claiming solvers (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:simulation-cache",
"familyIds": [
"N-018"
],
"command": "npm --prefix web run test:simulation-cache",
"exitCode": 0,
"durationMs": 4312,
"outputSha256": "62ebe648848c8feb9f768825b87175e9c6ba374ee93fa43e9e089525e2d62a87",
"output": "> blender-web-editor@0.1.0-rc.1 test:simulation-cache\n> playwright test --config playwright.config.ts -g \"Simulation caches\"\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1631:1 persists and revalidates content-addressed Simulation caches across Worker restarts (1.6s)\n\n 1 passed (3.4s)"
},
{
"token": "web:test:physics-main-reader",
"familyIds": [
"N-018"
],
"command": "npm --prefix web run test:physics-main-reader",
"exitCode": 0,
"durationMs": 771,
"outputSha256": "db1e7ed03da623f091a436357d0fdc025fa358e13040aa98c8658b7b307c080c",
"output": "> blender-web-editor@0.1.0-rc.1 test:physics-main-reader\n> node ../tools/web/check-physics-main-reader.mjs\n\nphysics-main-reader-ok families=CLOTH,SOFT_BODY dependencies=COLLISION settings-sha256=passed save-reopen=passed playback=BLOCKED solver=BLOCKED"
},
{
"token": "web:test:lighting-roundtrip",
"familyIds": [
"N-019"
],
"command": "npm --prefix web run test:lighting-roundtrip",
"exitCode": 0,
"durationMs": 775,
"outputSha256": "c9ddac34f9376983f0c67309583d0cb4a946d94f87421ffe1c9d6c3b39dbaba0",
"output": "> blender-web-editor@0.1.0-rc.1 test:lighting-roundtrip\n> node ../tools/web/check-lighting-roundtrip.mjs\n\nlighting-roundtrip-ok camera-writer-dof=passed light-shadow-exposure=passed world-mist=passed color-management-reader=passed white-balance-gate=passed undo-redo=passed save-reopen=passed"
},
{
"token": "web:e2e:N-019 Scene exposure",
"familyIds": [
"N-019"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-019 Scene exposure\"",
"exitCode": 0,
"durationMs": 4815,
"outputSha256": "282a7bd253cb7748b37e69df903c4d6c0b3420222a0228b1aad4f2af863ae9da",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-019 Scene exposure\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:772:1 maps N-019 Scene exposure and light shadow metadata without using legacy World exposure (2.1s)\n\n 1 passed (3.9s)"
},
{
"token": "web:test:compositor-main-reader",
"familyIds": [
"N-020"
],
"command": "npm --prefix web run test:compositor-main-reader",
"exitCode": 0,
"durationMs": 772,
"outputSha256": "8a4e76fcac535266cf3d4ac3166a2465334ed066c30a6bad5821c10f488e1e3b",
"output": "> blender-web-editor@0.1.0-rc.1 test:compositor-main-reader\n> node ../tools/web/check-compositor-main-reader.mjs\n\ncompositor-main-reader-ok graph-structure=passed exposure-invert-parameters=passed socket-links=passed unsupported-preserved=passed"
},
{
"token": "web:e2e:N-020 CPU compositor",
"familyIds": [
"N-020"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-020 CPU compositor\"",
"exitCode": 0,
"durationMs": 4109,
"outputSha256": "dc95f8f62abd9d6662f2a919d6aa82ce8154e4f014d2fd2290db78aec5987381",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-020 CPU compositor\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:834:1 executes the bounded N-020 CPU compositor and preserves unsupported nodes as gates (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:sequencer-main-reader",
"familyIds": [
"N-021"
],
"command": "npm --prefix web run test:sequencer-main-reader",
"exitCode": 0,
"durationMs": 730,
"outputSha256": "3cfbd29640e5da4df0505732f2d6f963663cb67414d5ee97eebe8a7ffbbbeea8",
"output": "> blender-web-editor@0.1.0-rc.1 test:sequencer-main-reader\n> node ../tools/web/check-sequencer-main-reader.mjs\n\nsequencer-main-reader-ok media-paths=passed still-image-range=passed fps=passed effect-dependencies=passed"
},
{
"token": "web:e2e:N-021 sequencer",
"familyIds": [
"N-021"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-021 sequencer\"",
"exitCode": 0,
"durationMs": 4157,
"outputSha256": "4f9551827b9a72bc512d7ceba8ca569e4a8623ff767da93bc79c901451c94233",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-021 sequencer\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:879:1 validates N-021 sequencer strips, deterministic edits, sandbox paths and codec gates (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:mask-main-reader",
"familyIds": [
"N-022"
],
"command": "npm --prefix web run test:mask-main-reader",
"exitCode": 0,
"durationMs": 743,
"outputSha256": "5fab1e2b7cdae27caa97ebbaf2b9c0f72a84cb44897b856ecbfd4d5162712027",
"output": "> blender-web-editor@0.1.0-rc.1 test:mask-main-reader\n> node ../tools/web/check-mask-main-reader.mjs\n\nmask-main-reader-ok layers-splines=passed handles-feather=passed locked-editable-selection=passed"
},
{
"token": "web:e2e:N-022 tracking",
"familyIds": [
"N-022"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-022 tracking\"",
"exitCode": 0,
"durationMs": 4096,
"outputSha256": "67420ec6e4a28f27f2346a7e6db835d2ca8bd281cd789371458b3d17a5693e2d",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-022 tracking\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:922:1 validates N-022 tracking markers, masks, resource bindings and solve gates (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:library-main-reader",
"familyIds": [
"N-023"
],
"command": "npm --prefix web run test:library-main-reader",
"exitCode": 0,
"durationMs": 753,
"outputSha256": "30d5e1d42dd164832e6dea514753d5e72251927249c5019e87dd79b6c8eaf392",
"output": "> blender-web-editor@0.1.0-rc.1 test:library-main-reader\n> node ../tools/web/check-library-main-reader.mjs\n\nreports | Read library: 'resources/image_resource_library.blend', '//resources/image_resource_library.blend', parent '<direct>'\n | \u001b[2;37msource/blender/blenkernel/intern/report.cc:51 BKE_report_log\u001b[0m\nreports | \u001b[1;33mWARNING\u001b[0m Unable to open 'resources/image_resource_library.blend': No such file or directory\n | \u001b[2;37msource/blender/blenkernel/intern/report.cc:54 BKE_report_log\u001b[0m\nreports | Cannot find lib 'resources/image_resource_library.blend'\n | \u001b[2;37msource/blender/blenkernel/intern/report.cc:51 BKE_report_log\u001b[0m\nlibrary-main-reader-ok relative-path=passed readonly=passed dependency-inventory=passed"
},
{
"token": "web:e2e:N-023 asset",
"familyIds": [
"N-023"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-023 asset\"",
"exitCode": 0,
"durationMs": 4191,
"outputSha256": "eb7f5ba2385cdf3279dab8ede7a792d238a01f3155e8d0e08e69d4e5e606cf94",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-023 asset\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:969:1 validates N-023 asset catalogs, library graphs, archive budgets and IO gates (1.5s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:editor-main-reader",
"familyIds": [
"N-024"
],
"command": "npm --prefix web run test:editor-main-reader",
"exitCode": 0,
"durationMs": 750,
"outputSha256": "534c21e20c5abe806079a385cacc0136c73a12f95600a66d52deef019509b8ba",
"output": "> blender-web-editor@0.1.0-rc.1 test:editor-main-reader\n> node ../tools/web/check-editor-main-reader.mjs\n\neditor-main-reader-ok workspaces=9 areas=36 regions=167"
},
{
"token": "web:e2e:N-024 editor",
"familyIds": [
"N-024"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-024 editor\"",
"exitCode": 0,
"durationMs": 4193,
"outputSha256": "79f7ba8129518e6d2819cc2e4dde8348b108ad61b5eaeb93e751cb0568bd522a",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-024 editor\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:997:1 validates N-024 editor context, selection sync, layout budgets and workflow gates (1.5s)\n\n 1 passed (3.4s)"
},
{
"token": "web:test:script-main-reader",
"familyIds": [
"N-025"
],
"command": "npm --prefix web run test:script-main-reader",
"exitCode": 0,
"durationMs": 779,
"outputSha256": "e73ad4daab4992a2e86e5e91a81d5e830c7d4604122e021da8fac36f90b37316",
"output": "> blender-web-editor@0.1.0-rc.1 test:script-main-reader\n> node ../tools/web/check-script-main-reader.mjs\n\nscript-main-reader-ok full-source=passed sha256=passed autorun-default-deny=passed"
},
{
"token": "web:test:scripting-isolation",
"familyIds": [
"N-025"
],
"command": "npm --prefix web run test:scripting-isolation",
"exitCode": 0,
"durationMs": 894,
"outputSha256": "6469327e0bb1c241dc7b0c0d184ca45cbe258f56638a245663c215eaa2ab91bf",
"output": "> blender-web-editor@0.1.0-rc.1 test:scripting-isolation\n> node ../tools/web/check-scripting-isolation.mjs\n\nscripting-isolation-ok status=BLOCKED audit=DENY approved-key=sandbox-unavailable server=unavailable execution=disabled"
},
{
"token": "web:e2e:N-025 script",
"familyIds": [
"N-025"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-025 script\"",
"exitCode": 0,
"durationMs": 4125,
"outputSha256": "dc5463f44e788eed8372e05f0c9cfcaff9ffa31b0badb3f881c768f2dd6c6805",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-025 script\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1017:1 validates N-025 script policy, signatures, budgets and platform gates (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:e2e:N-026 release",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:e2e -- --workers=1 -g \"N-026 release\"",
"exitCode": 0,
"durationMs": 4146,
"outputSha256": "b9262c32aa2e913165d94cce4611a9422bcb3d66f5626317a7403c7972963e2a",
"output": "> blender-web-editor@0.1.0-rc.1 test:e2e\n> playwright test --config playwright.config.ts --workers=1 -g N-026 release\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/smoke.spec.ts:1040:1 keeps N-026 release manifest deterministic and blocks missing evidence (1.4s)\n\n 1 passed (3.3s)"
},
{
"token": "web:test:v1-user-loop",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:v1-user-loop",
"exitCode": 0,
"durationMs": 5555,
"outputSha256": "82cbcac6791c98fa4f0effa80ab5de4f170cfc65b0883c06bf447ea45008146a",
"output": "> blender-web-editor@0.1.0-rc.1 test:v1-user-loop\n> playwright test --config playwright.config.ts tests/e2e/v1-user-loop.spec.ts\n\n\nRunning 1 test using 1 worker\n\n ✓ 1 tests/e2e/v1-user-loop.spec.ts:6:1 completes the V1 import-edit-save-restart-reopen-GLB loop (3.0s)\n\n 1 passed (4.7s)"
},
{
"token": "web:test:release-evidence",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:release-evidence",
"exitCode": 0,
"durationMs": 1184,
"outputSha256": "fd4d31581227c482d4f7e165cbf83a313a667a6551a069c982c086d1b90fbaec",
"output": "> blender-web-editor@0.1.0-rc.1 test:release-evidence\n> node ../tools/web/check-release-evidence.mjs\n\nrelease-evidence-check-ok records=17 missing=0 status=READY"
},
{
"token": "web:test:browser",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:browser",
"exitCode": 0,
"durationMs": 9223,
"outputSha256": "c31e3eb770f85aff43c5da17b2cb819b5a4d4bb69dfc85a9cb2319a24f7bfc12",
"output": "> blender-web-editor@0.1.0-rc.1 test:browser\n> playwright test --config playwright.release.config.ts\n\n\nRunning 3 tests using 1 worker\n\n ✓ 1 [chromium] tests/e2e/cross-browser.spec.ts:6:1 boots the offline engine, renders SceneIR and performs a Main edit (3.4s)\n ✓ 2 [chromium] tests/e2e/cross-browser.spec.ts:32:1 keeps content-addressed asset recovery available in Chromium (1.6s)\n ✓ 3 [chromium] tests/e2e/cross-browser.spec.ts:51:1 keeps the committed project after quota failure and Worker restart in Chromium (1.3s)\n\n 3 passed (8.1s)"
},
{
"token": "web:test:release-package",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:release-package",
"exitCode": 0,
"durationMs": 7028,
"outputSha256": "d087f0a9f20a7027eccb94c7a9cc6b0b67924377510582a91755629debc8de0e",
"output": "> blender-web-editor@0.1.0-rc.1 test:release-package\n> npm run build && npm run release:sbom && node ../tools/web/check-release-package.mjs\n\n\n> blender-web-editor@0.1.0-rc.1 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 54 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.30 kB\ndist/assets/storage.worker-COUVNftM.js 36.81 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DZmZ4IRD.js 601.32 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-YnUA2xia.js 978.22 kB │ gzip: 264.63 kB\n\n✓ built in 877ms\n\n> blender-web-editor@0.1.0-rc.1 release:sbom\n> node ../tools/web/generate-sbom.mjs\n\nsbom-ok packages=150 sha256=7d0fb22790c44bada5f01e5ebc12f6050e1c0436a53f58dceb2e979c17bb593a\nrelease-package-ok files=14 bytes=32896131\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit."
},
{
"token": "web:release:offline",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run release:offline",
"exitCode": 0,
"durationMs": 45192,
"outputSha256": "0b59a36f3df2b4450493bfa7770e30b853cd97abdd761bef494e35ee738607e1",
"output": "> blender-web-editor@0.1.0-rc.1 release:offline\n> npm run build && node ../tools/web/check-offline-reproducibility.mjs\n\n\n> blender-web-editor@0.1.0-rc.1 build\n> tsc -p tsconfig.json && vite build --config app/vite.config.ts\n\nvite v8.2.0 building client environment for production...\n\u001b[2K\rtransforming...✓ 54 modules transformed.\nrendering chunks...\ncomputing gzip size...\ndist/index.html 0.46 kB │ gzip: 0.30 kB\ndist/assets/storage.worker-COUVNftM.js 36.81 kB\ndist/assets/web-engine.worker-CWkf48pG.js 245.76 kB\ndist/assets/viewport-render.worker-DZmZ4IRD.js 601.32 kB\ndist/assets/web_engine-CvVHxFlQ.wasm 15,048.04 kB │ gzip: 3,562.32 kB\ndist/assets/index-QI0bg0OR.css 11.41 kB │ gzip: 3.18 kB\ndist/assets/index-YnUA2xia.js 978.22 kB │ gzip: 264.63 kB\n\n✓ built in 880ms\nsbom-ok packages=150 sha256=7d0fb22790c44bada5f01e5ebc12f6050e1c0436a53f58dceb2e979c17bb593a\noffline-release-ok binary=7841801 source=206996277 sha256=8af6c782b8e4b31020219597b821160b7634439749c455fd31d9d919676e2e2e\nsbom-ok packages=150 sha256=7d0fb22790c44bada5f01e5ebc12f6050e1c0436a53f58dceb2e979c17bb593a\noffline-release-ok binary=7841801 source=206996277 sha256=8af6c782b8e4b31020219597b821160b7634439749c455fd31d9d919676e2e2e\noffline-reproducibility-ok binary=8af6c782b8e4b31020219597b821160b7634439749c455fd31d9d919676e2e2e source=ee09b3e43e715b3d5497573bbf63a449cbea146150d56c08c6506afd5fce2871\n\n[plugin rolldown:vite-resolve] Module \"module\" has been externalized for browser compatibility, imported by \"/home/mes123456/workinf_Blender_Wasm/web/app/src/vendor/blender/web_engine.js\". See https://vite.dev/guide/troubleshooting.html#module-externalized-for-browser-compatibility for more details.\n[plugin builtin:vite-reporter] \n(!) Some chunks are larger than 500 kB after minification. Consider:\n- Using dynamic import() to code-split the application\n- Use build.rolldownOptions.output.codeSplitting to improve chunking: https://rolldown.rs/reference/OutputOptions.codeSplitting\n- Adjust chunk size limit for this warning via build.chunkSizeWarningLimit."
},
{
"token": "web:test:release-performance",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:release-performance",
"exitCode": 0,
"durationMs": 89284,
"outputSha256": "e76cf7de997e507dbab0242f1cde2e6b291a9f99d63ef2ca2450332da16d9ea3",
"output": "> blender-web-editor@0.1.0-rc.1 test:release-performance\n> node ../tools/web/check-release-performance.mjs\n\nrelease-performance-ok [{\"target\":100000,\"ratio\":0.9,\"inputTriangles\":100000,\"outputTriangles\":89999,\"elapsedMs\":88241,\"peakHeapBytes\":67108864,\"releasedPointerCount\":10,\"recoveryAllocation\":true},{\"target\":1000000,\"ratio\":1,\"inputTriangles\":1000000,\"outputTriangles\":1000000,\"elapsedMs\":535,\"peakHeapBytes\":346554368,\"releasedPointerCount\":10,\"recoveryAllocation\":true}]\n\nHeap resize call from 67108864 to 80543744 took 0.2181620000046678 msecs. Success: true\nHeap resize call from 80543744 to 96665600 took 0.09495199999946635 msecs. Success: true\nHeap resize call from 96665600 to 115998720 took 0.04383199999574572 msecs. Success: true\nHeap resize call from 115998720 to 139198464 took 0.05545200000051409 msecs. Success: true\nHeap resize call from 139198464 to 167051264 took 1.4360670000023674 msecs. Success: true\nHeap resize call from 167051264 to 200474624 took 1.4820880000042962 msecs. Success: true\nHeap resize call from 200474624 to 240582656 took 1.6095040000072913 msecs. Success: true\nHeap resize call from 240582656 to 288751616 took 1.3340389999939362 msecs. Success: true\nHeap resize call from 288751616 to 346554368 took 1.456514999998035 msecs. Success: true"
},
{
"token": "web:test:malicious-blends",
"familyIds": [
"N-026"
],
"command": "npm --prefix web run test:malicious-blends",
"exitCode": 0,
"durationMs": 507,
"outputSha256": "acd3b170723a0b560590b327a3eea3a119df2c246e46c6d849a5ccb3e8766d74",
"output": "> blender-web-editor@0.1.0-rc.1 test:malicious-blends\n> node ../tools/web/check-malicious-blends.mjs\n\nmalicious-blends-ok rejected=5"
}
]
}

47
docs/web/CI.md Normal file
View File

@@ -0,0 +1,47 @@
# M6 CI contract
The repository remote is Gitea, so the authoritative workflow is
`.gitea/workflows/m6-ci.yml`. `main` is the protected release ref. Pull requests and ordinary
pushes run quick and Chromium lanes; the release lane runs only for a `main` push or an explicit
`workflow_dispatch`.
## Pinned runner matrix
| Component | Pinned value | Lane |
| --- | --- | --- |
| Runner | self-hosted Debian 13 x86_64, labels `blender-web` / `blender-web-release` | all |
| Node.js | 20.19.2 | all |
| npm | lockfile-compatible npm 9 | all |
| Playwright | 1.62.1 | Chromium, release |
| Chromium | Playwright Chromium 149.0.7827.55 or `CHROME_PATH` override | Chromium, release |
| Blender | official Blender 5.2.0 LTS with USD enabled, configured by `BLENDER_BIN` | release |
| Emscripten | 3.1.69 | release |
`BLENDER_ARCHIVE_SHA256` must identify the official Blender archive used by acceptance. VDB jobs
use `VDB_RESOURCE_ROOT`; when it is absent, tools resolve `resource-library/blender-web-vdb`
under the runner home directory.
## Lane contents
`npm --prefix web run ci:quick` installs from `package-lock.json`, then runs typecheck, lint, Node
tests, status consistency and release-evidence schema checks.
`npm --prefix web run ci:chromium` runs the P0 user loop (including main-thread and Offscreen
viewports), release browser smoke, network interruption, device loss, OOM, real OPFS quota,
malicious blend and archive gates. Every browser command receives a newly allocated loopback port;
Playwright owns and cleans its server process.
`npm --prefix web run ci:release` runs full E2E, the complete performance and VDB matrices, V1
acceptance, reproducible offline packaging, archive cold boot, and binary/source independent
verification.
Each lane writes `release/ci-reports/<lane>.json`, a sidecar SHA-256 and one immutable log per
command. Reports use `docs/status/ci-lane-report.schema.json` and bind the commit, lockfile, parity
ledger, engine manifest, SBOM, available archives and every command log. A failed subcommand writes
an atomic `FAILED` report before the lane exits nonzero; it cannot reuse an older READY report.
The npm cache key is derived from `web/package-lock.json`. The Emscripten cache additionally binds
version 3.1.69 and `tools/web/emscripten-env.sh`. Release evidence, reports and archives are never
cache inputs. Artifact retention is fixed at 7 days for quick, 14 days for Chromium and 30 days for
release; the report checker runs before upload.

212
docs/web/DEPLOYMENT.md Normal file
View File

@@ -0,0 +1,212 @@
# Web Blender Modeler V1 Deployment Contract
The normative machine-readable contract is `deployment-contract.json`. A server is compatible only
when every response, including errors and SPA fallbacks, preserves these headers:
| Header | Required value |
| --- | --- |
| `Cross-Origin-Opener-Policy` | `same-origin` |
| `Cross-Origin-Embedder-Policy` | `require-corp` |
| `Cross-Origin-Resource-Policy` | `same-origin` |
Production uses HTTPS. Loopback development may use `http://127.0.0.1`; `file://` is unsupported.
Runtime assets are same-origin. A reverse proxy must not strip the isolation, range, cache, MIME or
ETag headers.
## Empty-directory install runbook
The release delivery contains `blender-web-offline.tar.gz`,
`blender-web-corresponding-source.tar.gz` and `SHA256SUMS.txt`. Install as an unprivileged deployment
user on a Linux host with GNU `tar`, `sha256sum`, `awk`, `mktemp`, `ln` and `mv`. Start with an empty
install root; do not extract directly over a running release.
Set explicit paths. `BLENDER_WEB_INSTALL_ROOT` must be a new or dedicated directory, never `/`, a
home directory or a shared workspace.
```bash
BLENDER_WEB_DELIVERY=/absolute/path/to/delivery
BLENDER_WEB_INSTALL_ROOT=/srv/blender-web
BLENDER_WEB_ARCHIVE="$BLENDER_WEB_DELIVERY/blender-web-offline.tar.gz"
BLENDER_WEB_SUMS="$BLENDER_WEB_DELIVERY/SHA256SUMS.txt"
test -f "$BLENDER_WEB_ARCHIVE"
test -f "$BLENDER_WEB_SUMS"
mkdir -p "$BLENDER_WEB_INSTALL_ROOT/releases"
BLENDER_WEB_EXPECTED_SHA256="$(awk '$2 == "blender-web-offline.tar.gz" { print $1 }' "$BLENDER_WEB_SUMS")"
BLENDER_WEB_ACTUAL_SHA256="$(sha256sum "$BLENDER_WEB_ARCHIVE" | awk '{ print $1 }')"
test -n "$BLENDER_WEB_EXPECTED_SHA256"
test "$BLENDER_WEB_ACTUAL_SHA256" = "$BLENDER_WEB_EXPECTED_SHA256"
tar -tzf "$BLENDER_WEB_ARCHIVE"
BLENDER_WEB_STAGE="$(mktemp -d "$BLENDER_WEB_INSTALL_ROOT/.install.XXXXXX")"
tar --no-same-owner --no-same-permissions -xzf "$BLENDER_WEB_ARCHIVE" -C "$BLENDER_WEB_STAGE"
test -f "$BLENDER_WEB_STAGE/blender-web-offline/app/index.html"
test -f "$BLENDER_WEB_STAGE/blender-web-offline/deployment-contract.json"
BLENDER_WEB_RELEASE_DIR="$BLENDER_WEB_INSTALL_ROOT/releases/$BLENDER_WEB_ACTUAL_SHA256"
test ! -e "$BLENDER_WEB_RELEASE_DIR"
mv "$BLENDER_WEB_STAGE/blender-web-offline" "$BLENDER_WEB_RELEASE_DIR"
rmdir "$BLENDER_WEB_STAGE"
ln -s "releases/$BLENDER_WEB_ACTUAL_SHA256" "$BLENDER_WEB_INSTALL_ROOT/.current.next"
mv -Tf "$BLENDER_WEB_INSTALL_ROOT/.current.next" "$BLENDER_WEB_INSTALL_ROOT/current"
test -f "$BLENDER_WEB_INSTALL_ROOT/current/app/index.html"
```
Before extraction, operators must inspect the `tar -tzf` output and reject absolute paths, `..`
components, links and entries outside `blender-web-offline/`. The automated M6 gate performs these
checks before it extracts anything. The content SHA-256 is the immutable release directory name;
`current` is the only mutable pointer. The server document root is
`$BLENDER_WEB_INSTALL_ROOT/current/app`, while `deployment-contract.json` remains beside `app/`.
## Transport preflight
Choose and record one origin before starting the server:
- Production: `https://<public-host>[:port]`. TLS terminates at the compatible server or reverse
proxy. Plain HTTP on a public DNS name, LAN address, `0.0.0.0` or container hostname is rejected.
- Local verification only: `http://127.0.0.1:<port>`. Do not publish the loopback-only server by
changing its bind address.
- `file://`, origins with embedded credentials and deployments below a URL path prefix are
unsupported. The application and every runtime asset must share one origin.
An HTTPS reverse proxy must forward requests to the document root without rewriting asset paths and
must preserve every header in `responseHeaders.allResponses`. A loopback check is not evidence that a
public plain-HTTP deployment is supported.
After the server starts, replace `BLENDER_WEB_ORIGIN` with the recorded HTTPS or loopback origin and
run:
```bash
curl --fail --silent --show-error --head "$BLENDER_WEB_ORIGIN/"
curl --fail --silent --show-error --head "$BLENDER_WEB_ORIGIN/engine-manifest.json"
curl --fail --silent --show-error \
--header 'Range: bytes=0-15' \
"$BLENDER_WEB_ORIGIN/vendor/blender/single/web_engine.wasm" >/dev/null
```
The first two responses must contain the three isolation headers, the contract cache policy, a
strong ETag and the exact MIME type. The range request must return `206` with `Accept-Ranges`,
`Content-Range`, `Content-Length` and the same strong ETag validator. Open the recorded origin in a
new Chromium profile and require `window.isSecureContext === true`,
`window.crossOriginIsolated === true`, the manifest to be verified and the selected engine to be
ready before directing users to the release.
## Upgrade runbook
Keep the public origin unchanged. IndexedDB and OPFS are origin-bound; changing scheme, host or port
creates a different storage boundary and is not an upgrade. Do not clear site data, browser profiles,
OPFS, IndexedDB or the old release directory during an upgrade.
1. Announce a maintenance window. Require active edits and saves to finish, then have users close or
reload existing tabs. A document and all of its content-hashed Workers must come from one release.
2. Install the new archive into a new content SHA-256 release directory by repeating the validation
and staging steps above, but do not change `current` yet.
3. Compare `current/release-metadata.json` with the new `release-metadata.json`. Record the old and new
product versions, engine release IDs, IndexedDB schema versions, OPFS project manifest versions,
entry assets and Worker assets. Reject a lower storage schema during upgrade. An OPFS schema change
requires an explicitly documented migration and recovery test; V1 currently declares schema `1`.
4. Start the new directory on a separate loopback-only validation port. Run the header, MIME, range,
manifest hash and browser cold-start gates before changing public traffic.
5. Export or download a backup of each critical project before the first forward-only storage
migration. The current IndexedDB schema is `6`; `onupgradeneeded` applies migrations atomically when
the new StorageWorker starts. A failed transaction must leave the old database version intact.
6. Stop admitting new sessions, then create `.current.next` pointing to the complete new release and
atomically rename it over `current`. Never switch `app/`, `engine-manifest.json`, stable engine files
or Worker files separately.
7. Revalidate `/`, `/engine-manifest.json`, one engine range, `window.isSecureContext`,
`window.crossOriginIsolated`, the selected engine and Storage schema. Reopen a saved OPFS project,
compare its revision and blend SHA-256, perform one edit/save, and only then end maintenance.
HTML, `engine-manifest.json` and stable engine URLs remain `no-cache`; a release switch therefore
revalidates them. Content-hashed entry and Worker URLs remain immutable. A manifest `releaseId`
mismatch requires a full refresh and must not initialize an engine or open a project. If any preflight
or post-switch check fails, leave or restore `current` to the old complete release; do not delete the
new directory until diagnostics are captured, and do not delete the old directory until the rollback
window ends.
## Rollback runbook
Rollback changes only the `current` app/engine release pointer. It never deletes or rewrites browser
site data. Keep the public origin unchanged and retain both release directories plus the pre-upgrade
project exports throughout the rollback window.
1. Stop new sessions and require active saves to finish. Record the current project revision and blend
SHA-256, the active `current` target, and both releases' `release-metadata.json` files.
2. Compare the browser's already-opened IndexedDB schema and OPFS project manifest schema with the
target old release. A target with the same IndexedDB schema and the same OPFS schema is directly
readable. Re-run its loopback cold-start and project-reopen checks before switching.
3. If the browser database has migrated above the old release's IndexedDB schema, in-place rollback is
`BLOCKED`: opening the older database version would fail. Do not delete or recreate the database.
Deploy a forward-compatible repair release that retains the current storage schema, or restore the
pre-upgrade project export under a separate recovery origin/profile supported by the old release.
4. If the OPFS project manifest schema differs, in-place rollback is also `BLOCKED` unless that exact
reverse reader has its own tested migration. V1 does not claim reverse OPFS migrations.
5. For a compatible target, create `.current.next` pointing to the complete old content-hash directory
and atomically rename it over `current`. Never copy individual HTML, manifest, engine or Worker files.
6. Revalidate transport, cache, manifest and engine identity. Reopen the same project and require its
revision and blend SHA-256 to match the pre-rollback values before allowing one edit/save.
If post-switch validation fails, atomically restore `current` to the newer release. Capture diagnostics
before removing a failed target. Browser data, the newer release and project exports remain untouched.
## Failure diagnostics
The machine-readable source is `operations-diagnostics.json`. Capture the active release metadata,
origin, failing URL, response headers, stable error code and project revision before recovery.
| Domain | Primary signal | Confirm | Recovery and data boundary |
| --- | --- | --- | --- |
| MIME | `DEPLOYMENT_MIME_MISMATCH` or streaming/module refusal | HEAD the URL; compare `Content-Type` with the contract | Fix the MIME map and repeat HEAD. Do not clear project storage. |
| Range | `DEPLOYMENT_RANGE_INVALID`, `416`, failed resume | Request `bytes=0-15`; inspect `206`, range headers, ETag and If-Range | Fix byte ranges; discard only partial bytes whose ETag changed. Keep verified OPFS bytes. |
| Isolation | `PLATFORM_CAPABILITY_UNAVAILABLE`, no SharedArrayBuffer | Verify HTTPS or `127.0.0.1`, all three isolation headers and both browser flags | Restore headers/same-origin assets or use the gated single variant. Do not open through a blocked pthread path. |
| Hash | `ENGINE_VARIANT_INTEGRITY_FAILED` | Re-fetch no-cache manifest; hash JS/WASM/Worker and compare one `releaseId` | Restore one complete release and reload. No fallback or project open is allowed on mismatch. |
| Quota | `STORAGE_QUOTA` or `QuotaExceededError` | Record storage estimate and compare committed revision/SHA-256 | Export the project and remove only selected disposable caches. Never clear all site data. |
| Worker | `WORKER_TERMINATED` or Worker load failure | HEAD the hashed Worker; compare MIME/cache/path with release metadata | Reload one release, restart Worker and reopen the verified revision. Reject late results. |
| GPU | `GPU_DEVICE_LOST` or a GPU budget code | Record loss reason, limits/resident bytes and unchanged project revision | Release/recreate viewport resources or reduce budget. Never rewrite Main/OPFS for GPU recovery. |
Run `npm --prefix web run test:operations-diagnostics` to validate all seven entries against the
deployment contract and the packaged documentation.
## Fresh-directory rehearsal
Before freezing an RC, run `npm --prefix web run test:operations-rehearsal`. It creates one new
temporary root and executes delivery checksum/path validation, initial install, loopback HTTP
preflight, project seeding, complete-release upgrade, HTTP revalidation, compatible rollback and a
final HTTP/project-integrity check in that order. It never uses an existing install directory.
The machine report is `release/operations-reports/rehearsal.json`. Every record contains the runbook
command, exit code, duration and observed output. `READY` requires all records to pass, the same
project revision and SHA-256 before/after both switches, both release directories to remain present,
and cleanup of the temporary rehearsal root after the report is assembled.
## Cache policy
- `/` and `/index.html`: `Cache-Control: no-cache` so a deployment is revalidated.
- `/engine-manifest.json`: `Cache-Control: no-cache` so engine identity is revalidated.
- `/assets/*`: `Cache-Control: public, max-age=31536000, immutable`; Vite content hashes these names.
- `/vendor/blender/*`: `Cache-Control: no-cache`; these stable URLs are verified by the engine manifest.
- Other paths: `Cache-Control: no-cache` until a more specific content-addressed rule exists.
Responses carry a strong content SHA-256 ETag. A matching `If-None-Match` returns `304` with no body;
after HTML, manifest or a stable engine asset changes, the old validator returns the new complete response
with `200` and a different ETag. Immutable `/assets/*` names must include the Vite content hash and are
never reused for different bytes.
The server must use the MIME map in the JSON contract. In particular, WebAssembly is
`application/wasm`, JavaScript is `text/javascript; charset=utf-8`, and NanoVDB is
`application/x-nanovdb`.
## Range and validators
`.wasm`, `.blend` and `.nvdb` accept a single `bytes` range. A valid range returns `206` with
`Accept-Ranges`, `Content-Range`, `Content-Length` and a strong content SHA-256 ETag. An invalid or
unsatisfied range returns `416`. An `If-Range` mismatch ignores the range and returns the complete
resource with status `200`; partial bytes from a different revision must never be combined.
Run `node tools/web/check-deployment-contract.mjs` for the static contract gate and
`npm --prefix web run test:deployment-runbook` for the fresh-directory install and transport
preflight gate. Run `npm --prefix web run test:upgrade-runbook` for the two-release manifest, cache,
storage schema, Worker and atomic-switch gate, and `npm --prefix web run test:rollback-runbook` for
compatible and blocked-old-reader rollback paths. Production HTTP behavior is a separate M6
acceptance gate and must exercise an actual server before release.

View File

@@ -0,0 +1,68 @@
# Web Blender Modeler V1 Known Limitations
These limits apply to `0.1.0-rc.1`. A successful bounded gate does not imply complete Blender 5.2
parity or support beyond the declared operation, fixture, resource budget and browser configuration.
See [V1_SCOPE.md](V1_SCOPE.md) and [parity-ledger.json](parity-ledger.json) for the authoritative
capability boundary.
## Browser and deployment
- Chromium is the only V1 browser baseline. Firefox, WebKit, mobile browsers, touch, pen, IME and
non-Chromium accessibility compatibility are not release claims.
- Production requires HTTPS. `http://127.0.0.1` is allowed for local verification; public plain HTTP
and `file://` are unsupported. All app, Worker and engine assets must remain same-origin.
- The base viewport uses WebGL2. OffscreenCanvas is a second tested production path. WebGPU is used
only for the declared bounded NanoVDB path and is subject to adapter availability and limits.
## Single-thread and pthread selection
- `AUTO` selects pthread only when `crossOriginIsolated`, `SharedArrayBuffer` and Worker capability
gates are all ready. Otherwise it selects the single-thread variant and reports the pthread gate.
- A pthread initialization failure under `AUTO` is cleaned up before one single-thread attempt. A
resource request failure outside this path remains an error.
- A JS, WASM or pthread Worker SHA-256 mismatch is fatal
(`ENGINE_VARIANT_INTEGRITY_FAILED`): there is no integrity fallback and no project is opened.
- `PTHREAD_REQUIRED` blocks when its platform gate is not ready. `SINGLE_REQUIRED` never attempts
pthread. A mixed app/manifest `releaseId` returns `REFRESH_REQUIRED` before engine initialization or
project open; a full reload is required.
- Both variants declare 256 initial WebAssembly pages (16 MiB) and a 32,768-page ceiling (2 GiB).
This is a manifest ceiling, not a memory reservation or availability guarantee; Chromium, the OS or
the device can reject growth earlier.
## Project storage
- OPFS and IndexedDB are bound to the exact scheme, host and port. Moving between origins does not
migrate projects. Clearing site data or a browser profile deletes locally stored projects and caches.
- Chromium and the operating system control quota. V1 reserves no minimum capacity and cannot infer
that `navigator.storage.estimate()` free space will remain available through a save.
- Atomic save preserves the last committed revision on quota failure, but the new revision is not
durable until save succeeds. Export critical projects before upgrades and before deleting caches.
- IndexedDB schema migration is forward-only. OPFS project manifest schema has no general reverse
migration. An older app is blocked when it cannot read the current schema.
## CPU, WASM and GPU budgets
- The RC gates cover 100k, 1M and 10M geometry fixtures; they are not an unlimited scene-size claim.
Imports and operations can block earlier when their own vertex, element, transfer, time or memory
budgets are exceeded.
- A texture asset is limited by protocol to 64 MiB, 16,384 pixels per dimension and 256 requested
assets per scene. The actual WebGL `MAX_TEXTURE_SIZE`, address space and GPU memory may be lower.
The release gate exercised 4K and 8K textures, not every format or 16K residency combination.
- VDB input is bounded to 512 MiB, a NanoVDB bundle to 1 GiB, each chunk to 16 MiB and declared GPU
residency to at most 512 MiB. Actual `maxStorageBufferBindingSize` and `maxBufferSize` may be lower.
- NanoVDB pages are explicit and LRU-bounded. Automatic viewport page-fault feedback, depth-composed
production demand paging and an unbounded multi-grid material system are not included in V1.
- GPU device loss or budget failure releases and recreates viewport resources where supported; it does
not modify Blender Main or OPFS project bytes. A reduced scene or budget may still be required.
## Blender feature scope
Complete PBVH sculpt/paint, arbitrary Geometry Nodes and Shader evaluation, full physics solvers,
Cycles, final video encoding, Python/Text autorun, add-ons and native GPU backends are either bounded,
server-only or excluded. The exact family slices and stable blockers are in
[parity-ledger.json](parity-ledger.json); unsupported data must be preserved or explicitly blocked,
not reported as executed.
Use [DEPLOYMENT.md](DEPLOYMENT.md) for installation and rollback requirements, and
[operations-diagnostics.json](operations-diagnostics.json) for stable failure signals and data-safe
recovery boundaries.

60
docs/web/RELEASE_NOTES.md Normal file
View File

@@ -0,0 +1,60 @@
# Web Blender Modeler V1 0.1.0-rc.1 Release Notes
Release candidate identity: `web-blender-0.1.0-rc.1`, engine
`blender-wasm-0.1.0-rc.1`.
This candidate is a Chromium-only browser modeler built around a bounded Blender 5.2 Main/WebEngine
subset. It is not a browser port of the complete Blender desktop application. The normative product
boundary and capability definitions are in [V1_SCOPE.md](V1_SCOPE.md); the machine-readable family
status and exact required/blocked slices are in [parity-ledger.json](parity-ledger.json).
## Verified V1 capabilities
- Import `.blend`, edit the declared Object, Mesh and Principled material subset, undo, redo, save
atomically, restart the Storage and WebEngine Workers, reopen, and export a semantically checked GLB.
- Preserve stable object, mesh and material identities across save/reopen and verify both the
main-thread WebGL and OffscreenCanvas Worker production viewports with non-empty pixels.
- Select integrity-checked single-thread or pthread WebEngine artifacts from one versioned engine
manifest. The pthread variant is used only when the isolated platform prerequisites pass.
- Store projects, snapshots and bounded assets in origin-bound OPFS/IndexedDB, including tested
interruption, Worker restart, quota failure and recovery behavior.
- Exercise bounded 100k, 1M and 10M geometry, 4K/8K textures, 600-frame simulation cache and
one-million-frame media indexing gates.
- Exercise deterministic WASM, OPFS, GPU and NanoVDB out-of-memory recovery, WebGL device loss,
network interruption, malformed `.blend` input and malicious archive rejection.
- Deliver deterministic offline binary and corresponding-source archives with SHA-256 checksums,
SPDX SBOM, licenses, source offer, deployment contract and operations diagnostics.
- Reproduce quick, Chromium and release CI lanes with lockfile, source, engine, archive and per-command
log hash bindings.
## Capability boundaries
`LOCAL_EXACT` and `LOCAL_BOUNDED` mean only the fixtures, operations and budgets declared in
[V1_SCOPE.md](V1_SCOPE.md) and [parity-ledger.json](parity-ledger.json). Unsupported operations must
remain blocked or preserved; the release does not infer support from a readable Blender data block.
`SERVER` covers validated request/result boundaries for work such as Cycles rendering, complex physics
bakes and final video encoding. This offline candidate does not bundle or configure those services and
does not claim local-equivalent execution.
`EXCLUDED` includes arbitrary Python/Text autorun, add-on installation, native desktop windows and
native CUDA, Metal, HIP or OptiX backends. The UI must not report those operations as successful.
All 12 V1 family release slices are `READY`, while all 12 complete Blender 5.2 family parity states are
`BLOCKED`. The exact blocked and excluded slice names remain authoritative in
[parity-ledger.json](parity-ledger.json); this release note does not convert them to completed work.
## Delivery evidence
Verify `SHA256SUMS.txt`, then verify `RC_MANIFEST.json.sha256` and `RC_MANIFEST.json` before deployment.
The manifest binds the RC version, base commit, engine release ID, package lock, ledger, embedded release
metadata, SBOM, binary/source archives and operations reports.
Read [Known limitations](KNOWN_LIMITATIONS.md) before deployment, especially the browser, engine
variant, origin-bound storage, quota, WASM and GPU budget boundaries.
Use [Release recovery](RELEASE_RECOVERY.md) to verify delivery evidence, export project backups and
handle pre-switch failures, compatible rollback or blocked schema downgrade.
Deployment requirements and operator commands are in [DEPLOYMENT.md](DEPLOYMENT.md). Stable error
signals and recovery boundaries are in [operations-diagnostics.json](operations-diagnostics.json).

View File

@@ -0,0 +1,105 @@
# Web Blender Modeler V1 Release Recovery
Use this procedure for `0.1.0-rc.1` upgrades and failed deployments. The complete installation,
upgrade and rollback contract remains [DEPLOYMENT.md](DEPLOYMENT.md). Never clear Chromium site data,
delete a browser profile, overwrite a release directory or downgrade a storage schema as a recovery
shortcut.
## Verify the delivery
The delivery directory must contain `blender-web-offline.tar.gz`,
`blender-web-corresponding-source.tar.gz`, `SHA256SUMS.txt`, `RC_MANIFEST.json` and
`RC_MANIFEST.json.sha256`. Verify bytes before extracting:
```bash
cd /absolute/path/to/delivery
sha256sum --check SHA256SUMS.txt
sha256sum --check RC_MANIFEST.json.sha256
```
Both commands must report `OK`. Check that the manifest identity is
`web-blender-0.1.0-rc.1`, its commit and `engineReleaseId` match the release record, and its binary,
source and SBOM hashes match the delivered files. In a corresponding source checkout, independently
run:
```bash
npm --prefix web ci --ignore-scripts
npm --prefix web run test:rc-manifest
npm --prefix web run test:binary-archive
npm --prefix web run test:source-archive
node tools/web/check-ci-report.mjs release/ci-reports/quick.json \
release/ci-reports/chromium.json release/ci-reports/release.json
```
Do not deploy when any command fails or when a report is not `READY` and bound to the manifest commit,
lockfile, ledger, engine and archive hashes.
## Back up projects before upgrade
1. Record the exact public origin, including scheme, host and port. Upgrades must retain that origin
because IndexedDB and OPFS data are origin-bound.
2. For every critical project, finish active edits and use the top-bar **Save Project** action. Wait
for the committed/saved state and retain the downloaded `blender-web.blend` outside the browser
profile and outside the deployment install root.
3. Record the project's displayed SceneIR revision and calculate the external backup hash with
`sha256sum blender-web.blend`. Reopen the downloaded file with the current release and confirm the
expected objects before entering the maintenance window.
4. Record `readlink current`, the old binary archive SHA-256 and both old/new
`release-metadata.json` files. Keep the old content-hash release directory, new staged directory,
external project backups and browser profile through the rollback window.
A backup is not complete merely because OPFS still contains a project. The external `.blend`, revision,
SHA-256, origin and active release identity form the recovery record.
## Upgrade failure before the switch
If checksum, archive path, header, MIME, range, engine hash, Worker or cold-start preflight fails before
`current` changes, leave `current` on the old release. Remove only the incomplete staging directory
after diagnostics are captured. Do not change IndexedDB, OPFS, browser caches or the old release.
Repeat the delivery checks and the loopback preflight from [DEPLOYMENT.md](DEPLOYMENT.md). A failed
candidate remains uninstalled until its binary hash and all preflight evidence pass.
## Upgrade failure after the switch
Stop new sessions and require active saves to finish. Capture the active origin, `current` target,
both release metadata files, failing URL/headers, error code, project revision and project SHA-256.
Compare the browser's opened IndexedDB and OPFS schema versions with the old target:
- If the IndexedDB versions match and the OPFS project manifest versions match, validate the complete
old release on a loopback port. Atomically repoint `current` to that old content-hash directory using
the rollback commands in [DEPLOYMENT.md](DEPLOYMENT.md). Revalidate transport and engine identity,
reopen the same project, and require the recorded revision and blend SHA-256 before allowing an edit.
- If the old IndexedDB schema is lower, rollback is `BLOCKED` with
`INDEXEDDB_SCHEMA_DOWNGRADE_UNSUPPORTED`. Do not open, delete or recreate the database with the old app.
- If the OPFS schema differs without an exact tested reverse reader, rollback is `BLOCKED` with
`OPFS_REVERSE_MIGRATION_UNDECLARED`. V1 does not claim a general reverse OPFS migration.
For a blocked rollback, keep or restore the newer compatible app release. Deploy a forward-compatible
repair using the current storage schemas, or start the old release on a separate recovery origin/profile
and import an external pre-upgrade `.blend` backup. Never point the old app at newer incompatible site
data.
## Restore and prove project integrity
After a compatible rollback or repair:
1. Open the recorded origin in a new Chromium tab and confirm the expected release/engine identity.
2. Use **Recover Project** for unchanged compatible origin storage. If that fails or a separate recovery
origin is required, use **Open .blend** and select the external backup.
3. Confirm the recorded objects and revision, use **Save Project**, then hash the new downloaded
`blender-web.blend`. The hash must equal the recorded backup unless a deliberate post-recovery edit
was made.
4. Keep the failed release, logs, diagnostics and external backup until the recovery record contains the
final origin, release ID, revision and SHA-256.
The automated rehearsal command is:
```bash
npm --prefix web run test:operations-rehearsal
```
It verifies delivery, fresh install, HTTP preflight, project revision/SHA-256 preservation, upgrade,
compatible rollback and cleanup in one new temporary root. It does not replace real project exports or
authorize a schema downgrade.

View File

@@ -15,8 +15,8 @@ move from `待验证` only after a task adds a build or browser regression test.
| Blender `draw/gpu` viewport | 关闭首期 | Three.js is the browser renderer | W-013, W-050 |
| Python runtime and automatic `.blend` scripts | 关闭首期 | security and package-size boundary | W-117 |
| Cycles, CUDA/OptiX, Embree | 关闭首期 | server-side or native rendering only | roadmap non-goal |
| OpenVDB | 浏览器关闭;桌面/服务端转换目标 planned | 读取 `.vdb` 并输出受校验 NanoVDB;不得链接进浏览器 WASM | N-015 VDB-010~014 |
| NanoVDB | 浏览器仅消费格式 | 分块 range + WebGPU sparse volume当前协议已落地renderer 阻断 | N-015 VDB-003~034 |
| OpenVDB | 浏览器关闭;桌面/服务端转换目标保留 | 真实 OpenVDB 13 -> NanoVDB 32 desktop/server 转换;不得链接进浏览器 WASM | N-015 VDB-010~014native/server hash 等价门 |
| NanoVDB | 浏览器仅消费格式 | HTTP/OPFS 分块 + Float32 WebGPU sparse volume双生产视口已接入demand paging/material/golden 阻断 | N-015 VDB-003~034 |
| USD, FFmpeg | 关闭首期 | server-side or later format modules | W-021 |
| React + TypeScript | 保留 | application UI and state | W-010 |
| Three.js (vendored) | 保留 | WebGL2 viewport and scene adapter; runtime has no CDN dependency | W-013, W-050 |

View File

@@ -0,0 +1,67 @@
{
"schemaVersion": 1,
"product": "Web Blender Modeler V1",
"browserScope": "Chromium",
"transport": {
"production": "https",
"localDevelopment": "http://127.0.0.1",
"fileProtocolSupported": false,
"sameOriginRuntimeAssets": true
},
"methods": ["GET", "HEAD"],
"responseHeaders": {
"allResponses": {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
"Cross-Origin-Resource-Policy": "same-origin"
},
"routes": [
{
"id": "entry-document",
"patterns": ["/", "/index.html"],
"cacheControl": "no-cache"
},
{
"id": "runtime-manifest",
"patterns": ["/engine-manifest.json"],
"cacheControl": "no-cache"
},
{
"id": "content-hashed-assets",
"patterns": ["/assets/*"],
"cacheControl": "public, max-age=31536000, immutable"
},
{
"id": "stable-engine-assets",
"patterns": ["/vendor/blender/*"],
"cacheControl": "no-cache"
},
{
"id": "fallback",
"patterns": ["*"],
"cacheControl": "no-cache"
}
]
},
"mimeTypes": {
".html": "text/html; charset=utf-8",
".js": "text/javascript; charset=utf-8",
".css": "text/css; charset=utf-8",
".wasm": "application/wasm",
".json": "application/json; charset=utf-8",
".png": "image/png",
".wav": "audio/wav",
".blend": "application/octet-stream",
".nvdb": "application/x-nanovdb"
},
"rangeRequests": {
"extensions": [".wasm", ".blend", ".nvdb"],
"unit": "bytes",
"singleRangeOnly": true,
"etag": "strong-sha256",
"satisfiedStatus": 206,
"unsatisfiedStatus": 416,
"requiredSatisfiedHeaders": ["Accept-Ranges", "Content-Range", "Content-Length", "ETag"],
"ifRangeMismatch": "return-full-200"
}
}

View File

@@ -0,0 +1,61 @@
# Engine Manifest Schema v2
`M6-04A` 冻结双 variant 的数据合同和纯校验器。`M6-04B` 已将生产
`web/app/public/engine-manifest.json` 切换到 schema v2并从两个独立构建目录把资产安装到
`/vendor/blender/single/``/vendor/blender/pthread/`。纯策略选择和双环境实机加载已由
`M6-04C-G` 完成;旧 single Worker 入口继续作为 `M6-05` fallback 接线前的完整 Main 兼容路径。
## 顶层合同
- `schemaVersion` 固定为 `2``protocolVersion` 固定为 `1``engine` 固定为
`blender-wasm`
- `releaseId` 是整个 manifest 的原子发布身份single/pthread 及其 JS、WASM、worker 摘要
必须作为同一份 manifest 一起切换,调用方不得从另一 release 补资源。
- `variants` 必须恰好包含一个 `single` 和一个 `pthread`,不能有第三种或重复 variant。
- single 与 pthread 的 JS URL 必须不同WASM URL 也必须不同,避免通过通用路径隐含构建类型。
## 资源合同
每项资源都必须声明 `fileName`、同源根路径 `url` 和小写 64 位十六进制 SHA-256
| variant | 必需资源 | 禁止资源 |
| --- | --- | --- |
| `single` | `resources.js``resources.wasm` | `resources.pthreadWorker` |
| `pthread` | `resources.js``resources.wasm``resources.pthreadWorker` | 无 |
JS 与 pthread worker 文件名必须以 `.js` 结尾WASM 文件名必须以 `.wasm` 结尾URL 的最后一段
必须与 `fileName` 完全一致。资源 URL 不允许远程 origin、query、fragment、反斜线或路径穿越。
`pthreadWorker` 表示 worker 加载角色,不强制是第四个物理文件:当前 Emscripten pthread 输出会
让 worker 重新加载同一个 pthread ES module因此该角色可以与 `resources.js` 使用相同的
`fileName``url` 和 SHA-256同一 URL 不允许声明不同摘要。worker 不得引用 single 的 JS。
若后续工具链产生独立 worker 文件,则三个字段必须描述该文件。
## 内存合同
WASM page 固定为 65,536 bytes。每个 variant 都独立声明 `initialPages``maximumPages`
`shared`
- `initialPages` 范围为 256 至 32,768 pages即 16 MiB 至 2 GiB。
- `maximumPages` 不得小于 `initialPages`,且不得超过 32,768 pages2 GiB
- single 必须为 `shared=false`pthread 必须为 `shared=true`
权威 TypeScript 类型和无 I/O 校验器位于 `web/protocol/manifest.ts`;结构化正例位于
`tests/golden/M6-04A/engine-manifest-v2.json`。校验器只接收输入并返回深层新对象或抛出带稳定
`code`/`path``WebEngineManifestValidationError`,不读取文件、不请求网络,也不选择运行时
variant。
安装命令为 `bash tools/web/install-web-engine-assets.sh`。默认读取 `build_web-single`
`build_web-pthread`,先在临时目录生成并复核 manifest再安装四个物理文件。专项命令
`npm --prefix web run test:engine-variant-install` 同时覆盖正常复制、同一构建目录拒绝、缺失资产
拒绝和失败不改写目标 manifest。
纯选择器位于 `web/protocol/engine-variant.ts`,策略为 `AUTO``SINGLE_REQUIRED`
`PTHREAD_REQUIRED`。它不读取 DOM、网络、存储或 Worker调用方必须显式传入已校验 manifest
和三项线程能力。`PTHREAD_REQUIRED` 被阻断时不返回 variant。专项命令为
`npm --prefix web run test:engine-variant-selection``npm --prefix web run test:pthread-engine`
`npm --prefix web run test:single-thread-unisolated`
升级入口为 `bootstrapWebEngineRelease`。旧 HTML 绑定的 expected release 与重新验证后的 manifest
不一致时只返回 `REFRESH_REQUIRED`,不会选择或初始化 variant也不会打开待处理项目。资源
SHA-256 不一致统一返回 `ENGINE_VARIANT_INTEGRITY_FAILED`;该错误不可进入 pthread -> single
fallback。`npm --prefix web run test:engine-upgrade-safety` 同时覆盖纯策略和真实 Chromium 篡改响应。

View File

@@ -0,0 +1,69 @@
{
"schemaVersion": 1,
"product": "Web Blender Modeler V1",
"entries": [
{
"id": "OPS-MIME",
"domain": "mime",
"signals": ["DEPLOYMENT_MIME_MISMATCH", "WebAssembly.instantiateStreaming failed", "module script refused"],
"checks": ["HEAD the failing URL and record Content-Type", "compare the extension with deployment-contract.json mimeTypes"],
"expected": [".wasm application/wasm", ".js text/javascript; charset=utf-8", ".json application/json; charset=utf-8"],
"recovery": ["fix the server MIME map", "restart or reload proxy configuration", "repeat HEAD before refreshing Chromium"],
"dataSafety": "Do not clear OPFS or IndexedDB; MIME failures occur before project bytes need mutation."
},
{
"id": "OPS-RANGE",
"domain": "range",
"signals": ["DEPLOYMENT_RANGE_INVALID", "HTTP 416", "partial resource resume failed"],
"checks": ["GET the resource with Range: bytes=0-15", "record status, Accept-Ranges, Content-Range, Content-Length and ETag", "repeat with matching and mismatching If-Range"],
"expected": ["valid single range returns 206", "invalid or unsatisfied range returns 416", "If-Range mismatch returns full 200"],
"recovery": ["enable single byte ranges without compression rewriting", "preserve strong ETag validators", "discard partial bytes from a different ETag before retry"],
"dataSafety": "Never append bytes across different ETags; retain the last verified OPFS asset and project commit."
},
{
"id": "OPS-ISOLATION",
"domain": "isolation",
"signals": ["PLATFORM_CAPABILITY_UNAVAILABLE", "crossOriginIsolated false", "SharedArrayBuffer unavailable"],
"checks": ["confirm HTTPS or explicit http://127.0.0.1 loopback", "HEAD the document and error routes", "evaluate window.isSecureContext and window.crossOriginIsolated"],
"expected": ["Cross-Origin-Opener-Policy same-origin", "Cross-Origin-Embedder-Policy require-corp", "Cross-Origin-Resource-Policy same-origin"],
"recovery": ["restore the three headers on every response", "remove cross-origin runtime assets", "use the single variant only when the pthread capability gate is BLOCKED"],
"dataSafety": "Do not open a pending project through a blocked pthread path; correcting headers does not require storage deletion."
},
{
"id": "OPS-HASH",
"domain": "hash",
"signals": ["ENGINE_VARIANT_INTEGRITY_FAILED", "ENGINE_VARIANT_RESOURCE_HASH_MISMATCH", "manifest rejected"],
"checks": ["fetch engine-manifest.json with no-cache", "hash the exact JS, WASM and pthread Worker response bytes", "compare releaseId and every declared SHA-256"],
"expected": ["one releaseId binds both variants", "resource SHA-256 equals the manifest", "integrity failure performs no fallback and no project open"],
"recovery": ["restore one complete immutable release", "purge only the corrupt HTTP cache entry", "reload the document and revalidate the manifest"],
"dataSafety": "Keep the project unopened until all resource hashes pass; never repair integrity by editing project data."
},
{
"id": "OPS-QUOTA",
"domain": "quota",
"signals": ["STORAGE_QUOTA", "QuotaExceededError", "OPFS staging write failed"],
"checks": ["record navigator.storage.estimate usage and quota", "compare the committed project revision and SHA-256", "inspect staging and cache categories without deleting the committed blend"],
"expected": ["old committed revision remains readable", "failed staging is not promoted", "Worker restart reports the same committed SHA-256"],
"recovery": ["export the committed project", "remove explicitly selected disposable caches", "request persistent storage or free origin quota before retry"],
"dataSafety": "Never delete scene.blend, its verified manifest or all site data as a quota recovery shortcut."
},
{
"id": "OPS-WORKER",
"domain": "worker",
"signals": ["WORKER_TERMINATED", "Worker script load failed", "pending request aborted"],
"checks": ["HEAD the content-hashed Worker URL", "confirm JavaScript MIME and immutable cache", "compare Worker assets with release-metadata.json", "capture pending request and resource counters"],
"expected": ["all Workers belong to the current release", "terminated Worker publishes no late result", "restart rediscovers the committed project"],
"recovery": ["reload to one complete current release", "restart the affected Worker", "reopen the last verified project revision"],
"dataSafety": "Do not promote late Worker output or remove OPFS while recovering a Worker."
},
{
"id": "OPS-GPU",
"domain": "gpu",
"signals": ["GPU_DEVICE_LOST", "GPU_TEXTURE_BUDGET_EXCEEDED", "NANOVDB_GPU_BUDGET_EXCEEDED"],
"checks": ["record WebGL context loss or WebGPU device.lost reason", "record adapter limits and requested resident bytes", "confirm CPU, WASM and OPFS project revision did not change"],
"expected": ["GPU resources release once", "bounded recovery recreates the viewport", "persistent project SHA-256 remains stable"],
"recovery": ["reduce texture or NanoVDB resident budget", "recreate the viewport after device recovery", "reload the current release if bounded recovery fails"],
"dataSafety": "GPU recovery may discard viewport resources only; it must not rewrite Main or the committed project."
}
]
}

View File

@@ -3,7 +3,7 @@
"dataLicense": "CC0-1.0",
"SPDXID": "SPDXRef-DOCUMENT",
"name": "blender-web-editor-sbom",
"documentNamespace": "https://blender-web.local/spdx/0ce47f13cb0c6d4aa08df7f3df6d81f63db9574c3cdac8a2ee6abc0cc6f8e91e",
"documentNamespace": "https://blender-web.local/spdx/cf7beefe71131e5d51b45ffa79b7b906b21decc743fc7eafd7cf6791996dea37",
"creationInfo": {
"created": "1970-01-01T00:00:00Z",
"creators": [
@@ -2524,8 +2524,8 @@
{
"SPDXID": "SPDXRef-npm-node-modules-nanoid-2e287a9dc6ed",
"name": "nanoid",
"versionInfo": "3.3.17",
"downloadLocation": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.17.tgz",
"versionInfo": "3.3.18",
"downloadLocation": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.18.tgz",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
"licenseDeclared": "NOASSERTION",
@@ -2534,13 +2534,13 @@
{
"referenceCategory": "PACKAGE-MANAGER",
"referenceType": "purl",
"referenceLocator": "pkg:npm/nanoid@3.3.17"
"referenceLocator": "pkg:npm/nanoid@3.3.18"
}
],
"checksums": [
{
"algorithm": "SHA512",
"checksumValue": "c502dfd00dc738c9601ead27db8eff2d1b80398981edd5c9fc3bc0c46bec4818a3e395ed052990c9cbbe17c3836c7c27b3f5f21596a0c8bd7e27439f7f0d44d2"
"checksumValue": "0d38383096c631691f8ba55915d36ddbf71a31b432e0ebbe3a9fe1250bc611672755fa00d5003ec7344a033c3d8c3ebe19538e798aff98872e0d276e83a17afb"
}
]
},
@@ -3283,7 +3283,7 @@
{
"SPDXID": "SPDXRef-Package-blender-web-editor",
"name": "blender-web-editor",
"versionInfo": "0.1.0",
"versionInfo": "0.1.0-rc.1",
"downloadLocation": "NOASSERTION",
"filesAnalyzed": false,
"licenseConcluded": "NOASSERTION",
@@ -3292,7 +3292,7 @@
"checksums": [
{
"algorithm": "SHA256",
"checksumValue": "3c65ff96cc15c9072d2517555484ccfd0009a2814d96faadcc5bd7b2e3458503"
"checksumValue": "87af8e7d5eb36537541cf941699868a010c1fcea305daa3ce5385453e8fa4557"
}
]
},

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