Advance M8-M11 parity workflows
This commit is contained in:
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,6 +1,8 @@
|
||||
# Local toolchains and caches
|
||||
.emcache/
|
||||
.emscripten-web
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*.log
|
||||
mylog.txt
|
||||
|
||||
|
||||
@@ -521,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 仍待完成 |
|
||||
|
||||
@@ -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) {
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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")
|
||||
{
|
||||
|
||||
@@ -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},
|
||||
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", read_pointer(*blend.sdna, element, "packedfile").value_or(0) != 0}});
|
||||
{"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);
|
||||
|
||||
@@ -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]]);
|
||||
|
||||
@@ -8,6 +8,8 @@
|
||||
#include <cstring>
|
||||
#include <limits>
|
||||
#include <new>
|
||||
#include <string_view>
|
||||
#include <tuple>
|
||||
#include <unordered_map>
|
||||
#include <unordered_set>
|
||||
|
||||
@@ -36,7 +38,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 +619,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,
|
||||
@@ -3451,6 +3666,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 +3897,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 +3924,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 +3948,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 +3967,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 +3991,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 +4345,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 +4544,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 +4558,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 +4565,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 +4656,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 ¤t = 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);
|
||||
}
|
||||
|
||||
@@ -346,6 +346,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 +367,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 +386,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 +423,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,
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Web Blender V1 当前执行计划
|
||||
|
||||
更新时间:2026-08-15
|
||||
更新时间:2026-08-17
|
||||
|
||||
## 1. 交付目标
|
||||
|
||||
@@ -124,12 +124,13 @@ Linux archive SHA-256 为 `96f6c181a30f4950607839dc84d42a354b250d8a0231b098b59b7
|
||||
|
||||
### 4.4 当前阻断与决策顺序
|
||||
|
||||
1. `P0`:V1 可部署 RC 已完成 M6 71/71;M7 已完成 7/18,下一领取点为 `M7-08` Worker 崩溃恢复。
|
||||
1. `P0`:V1 可部署 RC 已完成 M6 71/71;M7 核心体验硬化已完成 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 demand paging、PBVH、Geometry Nodes evaluator、
|
||||
deploy/upgrade/rollback 均已有可复验证据;M7 项目体验硬化已完成并进入持续回归。
|
||||
4. `P2`:完整 Blender 功能域继续受 VDB depth composition/完整 Volume IO、PBVH、Geometry Nodes evaluator、
|
||||
通用 Shader、完整编辑器/渲染/媒体和安全脚本边界阻断。
|
||||
|
||||
### 4.5 真实发布缺口
|
||||
@@ -151,18 +152,19 @@ loss、恶意 blend、zip bomb、离线包和 V1 用户闭环均已在最终 M5
|
||||
| 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 核心体验硬化 | 项目状态机、恢复、存储、输入和可访问性 | 进行中,7/18 | 30 分钟编辑/保存/重开 soak 通过 |
|
||||
| M8 VDB 自动分页 | GPU 缺页反馈、range/OPFS、LRU、双视口恢复 | 未开始 | 64 MiB sparse bundle 与三视角 golden 通过 |
|
||||
| M9 非 Mesh/GP/Paint | 字体、Curve、Grease Pencil、Paint 增量闭环 | 未开始 | 每个新增 writer 独立通过 Main/undo/save/golden |
|
||||
| M10 GN/Shader/NLA/Simulation | 白名单求值、cache、编译与阻断 | 未开始 | 四域分别通过 desktop/WASM/fault 门 |
|
||||
| M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 未开始 | 本地白名单与 server 边界均可审计 |
|
||||
| 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 | 灯光、渲染、合成、媒体执行边界 | 进行中,13/14 | 本地白名单与 server 边界均可审计 |
|
||||
| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 未开始 | 每个格式/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 至 M5 已完成并转入持续回归;当前严格领取 M6。里程碑内部允许先写纯校验任务,但不得在
|
||||
前一小里程碑失败时把后一小里程碑标为完成,M7 至 M15 不得绕过 M6 发布合同扩功能。
|
||||
M0 至 M7 已完成并转入持续回归;后续严格读取机器队列最新 `nextTask`。里程碑内部允许先写
|
||||
纯校验任务,但不得在前一小里程碑失败时把后一小里程碑标为完成,M8 至 M15 不得绕过 M6/M7
|
||||
发布与体验合同扩功能。
|
||||
|
||||
## 6. M0 范围、状态和 P0 闭环
|
||||
|
||||
@@ -555,7 +557,8 @@ SBOM、状态和 evidence SHA-256 相互一致。
|
||||
|
||||
M5 最终 archive SHA-256:binary
|
||||
`f2903a75026a0a28615aeb5f4834a3859af83022c82bc50e28437dcbed68e549`;source
|
||||
`9a9c1b505c68a618de842be7c248f628b104b11b80e8a40d111f62540fcba327`。当前下一任务:`M7-08`。
|
||||
`9a9c1b505c68a618de842be7c248f628b104b11b80e8a40d111f62540fcba327`。后续领取点只读取机器队列
|
||||
最新 `nextTask`,本段不缓存任务名称。
|
||||
|
||||
## 12. V1 后原子里程碑
|
||||
|
||||
@@ -716,91 +719,107 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。
|
||||
- [x] `M7-05` save 中断时旧 revision 和旧 hash 保持不变。
|
||||
- [x] `M7-06` dirty state 只由成功 Main transaction 改变。
|
||||
- [x] `M7-07` undo 回到保存 revision 时 dirty state 清除。
|
||||
- [ ] `M7-08` Worker 崩溃时展示可恢复错误,不清空当前项目列表。
|
||||
- [ ] `M7-09` 重启 Worker 后恢复 selection、frame、workspace 和项目 revision。
|
||||
- [ ] `M7-10` recent projects 对缺失/损坏 OPFS 条目执行隔离和修复提示。
|
||||
- [ ] `M7-11` 存储预算面板报告项目、快照、LOD、媒体、VDB 分项字节。
|
||||
- [ ] `M7-12` 清理单个项目时只删除该项目 content-addressed 引用的孤儿资源。
|
||||
- [ ] `M7-13` 键盘焦点、菜单、modal、Escape 取消形成一致上下文规则。
|
||||
- [ ] `M7-14` 1440x900、1280x720、移动窄屏下无控制重叠和文本溢出。
|
||||
- [ ] `M7-15` 主线程与 Offscreen 视口的选择、相机和 gizmo 行为一致。
|
||||
- [ ] `M7-16` P0 用户流程增加无鼠标键盘路径和基础可访问性检查。
|
||||
- [ ] `M7-17` 用户可见错误只显示稳定短消息,详细诊断进入可导出报告。
|
||||
- [ ] `M7-18` 完成 30 分钟持续编辑、自动保存、重开 soak 测试。
|
||||
- [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 自动分页最小闭环
|
||||
|
||||
- [ ] `M8-01` 定义 GPU page-fault feedback buffer schema、容量和 overflow code。
|
||||
- [ ] `M8-02` shader 对未驻留 leaf 记录唯一 page ID,不越界写 feedback。
|
||||
- [ ] `M8-03` CPU 读取 feedback 后排序、去重并绑定 render revision。
|
||||
- [ ] `M8-04` stale frame feedback 不触发 I/O。
|
||||
- [ ] `M8-05` page request 使用现有 manifest range/hash,不建立第二套地址模型。
|
||||
- [ ] `M8-06` 同页并发请求合并,取消最后一个订阅者时中止 range。
|
||||
- [ ] `M8-07` page hash 错误不写 resident cache,并返回稳定错误。
|
||||
- [ ] `M8-08` LRU 淘汰跳过当前 frame pin 的 page。
|
||||
- [ ] `M8-09` page upload 成功后只安排一次渐进重绘。
|
||||
- [ ] `M8-10` 重绘上限防止坏数据造成无限 render loop。
|
||||
- [ ] `M8-11` 主线程 WebGPU 验证缺页、加载、重绘到确定性像素。
|
||||
- [ ] `M8-12` Offscreen Worker 验证相同页序列和最终像素。
|
||||
- [ ] `M8-13` page fetch 中 network interruption 可从精确 byte offset 续传。
|
||||
- [ ] `M8-14` Worker restart 从 OPFS 恢复 manifest,但不信任未复验 resident 状态。
|
||||
- [ ] `M8-15` device loss 重建 page table,并按可见集合有界回放。
|
||||
- [ ] `M8-16` OOM 释放 page table/resident/feedback buffer 各一次。
|
||||
- [ ] `M8-17` 64 MiB sparse bundle 性能与取消门。
|
||||
- [ ] `M8-18` Volume Main、asset binding、双视口联合保存重开。
|
||||
- [ ] `M8-19` desktop/主线程/Offscreen 三视角 golden 和误差阈值。
|
||||
- [ ] `M8-20` 只有 M8 全部通过后更新 N-015 对应 slice,N-015 全域仍不自动 COMPLETE。
|
||||
- [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 对应 slice,N-015 全域仍不自动 COMPLETE。
|
||||
|
||||
### M9 非 Mesh、Grease Pencil 与 Paint 增量
|
||||
|
||||
- [ ] `M9-01` 外部字体导入先做路径、类型、大小和 hash 校验。
|
||||
- [ ] `M9-02` 字体进入 OPFS content-addressed asset 后才写 Main VFont。
|
||||
- [ ] `M9-03` 字体替换/撤销/保存/重开/缺失资产形成闭环。
|
||||
- [ ] `M9-04` Curve 完整 topology editor 先冻结 operator 白名单和预算。
|
||||
- [ ] `M9-05` 每个新增 Curve operator 单独完成 Main/undo/save/golden 后再开放 UI。
|
||||
- [ ] `M9-06` Grease Pencil marquee 只操作当前 drawing 的稳定 point/stroke ID。
|
||||
- [ ] `M9-07` Grease Pencil 2D canvas 与 3D viewport 使用同一 selection revision。
|
||||
- [ ] `M9-08` Grease Pencil layer/frame reorder 完成 undo/save/reopen。
|
||||
- [ ] `M9-09` Paint 建立真实深度可见性采样,不用 CPU proxy 冒充。
|
||||
- [ ] `M9-10` Paint stroke 分块提交且一次 pointer session 只产生一个 undo step。
|
||||
- [ ] `M9-11` Texture paint dirty tile 原子写入 packed/UDIM 资产。
|
||||
- [ ] `M9-12` Weight paint normalize/limit/mirror 与 Blender desktop golden 对比。
|
||||
- [ ] `M9-13` PBVH 若不进入 WASM,则相关笔刷保持明确 capability block。
|
||||
- [ ] `M9-14` 三个编辑域分别通过 Worker restart、OOM、GPU release 和小场景恢复。
|
||||
- [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 与 Simulation
|
||||
### M10 Geometry Nodes、Shader、NLA 与 Simulation(15/15)
|
||||
|
||||
- [ ] `M10-01` 从 Blender Main 读取图拓扑、socket 默认值、link 和稳定 node ID。
|
||||
- [ ] `M10-02` 为 Geometry Nodes 建立 allowlist,不支持节点保留原数据并阻断求值。
|
||||
- [ ] `M10-03` 每个 allowlist 节点增加 desktop fixture、WASM 输出和误差阈值。
|
||||
- [ ] `M10-04` field/domain 转换显式预算,禁止按无限域展开 JSON。
|
||||
- [ ] `M10-05` Simulation Zone cache 绑定 graph/source/revision hash。
|
||||
- [ ] `M10-06` cache 取消、LRU、重启和损坏隔离通过后才允许播放。
|
||||
- [ ] `M10-07` Shader 图只编译声明的 Principled/Image/Normal/Math 子集。
|
||||
- [ ] `M10-08` shader compile key 包含 graph、texture、color space 和 renderer backend。
|
||||
- [ ] `M10-09` compile failure 不替换上一份可用 material pipeline。
|
||||
- [ ] `M10-10` 任意未支持 Shader 节点返回 stable capability block,不静默降级。
|
||||
- [ ] `M10-11` NLA track/strip/action/time mapping 先完成只读精确求值。
|
||||
- [ ] `M10-12` NLA 单个编辑 operator 分别完成 Main transaction、undo 和保存重开。
|
||||
- [ ] `M10-13` Physics 本地 solver 按 family 探测;不支持项只消费 desktop/server bake。
|
||||
- [ ] `M10-14` 每类 cache 验证 source hash、frame range、字节预算和版本。
|
||||
- [ ] `M10-15` GN/Shader/NLA/Simulation 分别建立浏览器性能、OOM 和恶意图输入门。
|
||||
- [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 与 Sequencer
|
||||
### M11 Lighting、Render、Compositor 与 Sequencer(13/14)
|
||||
|
||||
- [ ] `M11-01` Camera/Light/World/Scene color management 建立字段级 parity 表。
|
||||
- [ ] `M11-02` 每个支持字段通过 Main edit、undo、save/reopen 和 viewport 映射。
|
||||
- [ ] `M11-03` Three/WebGPU 灯光数量、shadow map 和纹理预算显式化。
|
||||
- [ ] `M11-04` Web 实时渲染结果与 Blender reference 采用可解释图像误差指标。
|
||||
- [ ] `M11-05` Cycles/复杂 Eevee/硬件后端固定为 server job,不在浏览器伪造等价。
|
||||
- [ ] `M11-06` server render job 绑定 source hash、Blender build、设置和输出 hash。
|
||||
- [ ] `M11-07` Compositor allowlist 每新增一个 node 单独增加 CPU/WebGPU golden。
|
||||
- [ ] `M11-08` unsupported compositor graph 保留原图并阻断执行。
|
||||
- [ ] `M11-09` Sequencer IMAGE/SOUND/MOVIE codec 由运行时 probe 决定,不按扩展名猜测。
|
||||
- [ ] `M11-10` long media proxy/cache 绑定源 hash 和 decode capability。
|
||||
- [ ] `M11-11` seek/scrub/decode 迟到结果全部受 revision gate 控制。
|
||||
- [ ] `M11-12` 最终视频编码固定 server/export capability,不误报本地完成。
|
||||
- [ ] `M11-13` audio context suspend/resume、设备缺失和静音恢复有专项测试。
|
||||
- [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 render,source/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-256,Chromium 分别以 ImageBitmap、
|
||||
固定采样率 Web Audio 和 HTMLMedia 实际解码 PNG/WAV/H.264 MP4。
|
||||
- [x] `M11-10` long media proxy/cache 绑定源 hash 和 decode capability;schema 1 将 source
|
||||
family/MIME/bytes/SHA-256、M11-09 READY receipt、RGBA8 profile 和 source frame 共同绑定为
|
||||
cache identity,payload 独立验 hash,Chromium 实际生成首帧 proxy 并通过 LRU/Storage Worker
|
||||
重开门。
|
||||
- [x] `M11-11` seek/scrub/decode 迟到结果全部受 revision gate 控制;三类请求共享单调
|
||||
request revision,结果必须回显 timeline/request identity,timeline 换代、旧请求和伪造结果
|
||||
均在 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 保持结构化静音阻断。
|
||||
- [ ] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复。
|
||||
|
||||
### M12 Asset、IO、Editor 与工作流
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# Web Blender 项目现状与后续连续任务
|
||||
|
||||
更新时间:2026-08-15
|
||||
更新时间:2026-08-17
|
||||
|
||||
当前短周期任务、领取顺序和阶段退出条件统一维护在
|
||||
`docs/CURRENT_EXECUTION_PLAN.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为
|
||||
@@ -29,14 +29,19 @@ SQLite WASM 和 Bitbybit/OCCT 均不在当前依赖范围内。Three.js、WASM
|
||||
| --- | --- | --- | --- |
|
||||
| V1 release family | 12/12 `READY` | 0 | V1 已声明功能闭环完成 |
|
||||
| Blender 5.2 全域 parity | 0/12 complete | 12/12 `BLOCKED` | 不是完整 Blender Web 移植 |
|
||||
| slice 台账 | 177 completed | 60 blocked | blocked 项均留在 M8-M15 长期路线 |
|
||||
| slice 台账 | 191 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 |
|
||||
| acceptance | 50 declarations / 49 unique passed | 0 failed | 本地 V1 RC 证据完整 |
|
||||
| M6 可部署 RC | 71/71 原子任务 | 0 | 可部署 RC 已冻结 |
|
||||
| M7 核心体验硬化 | 7/18 原子任务 | 11 | 当前严格领取 M7 项目体验工作 |
|
||||
| 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 golden;GP 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 | 13/14 原子任务 | 1 | M11-01/02 已冻结字段 parity 并完成支持字段闭环;M11-03 已让双 viewport 共用资源预算;M11-04 已完成 Blender Eevee reference 图像指标;M11-05/06 已完成最终渲染路由/provenance;M11-07/08 已完成有限 Compositor golden 与 Unsupported 全图阻断;M11-09/10/11/12 已完成 codec/proxy/revision/export gate;M11-13 已完成实时 AudioContext 恢复门 |
|
||||
|
||||
当前优先级不是扩 Blender 全域功能。single/pthread、真实 HTTP、缓存升级、离线闭环、
|
||||
独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过;现在进入 M7 核心项目
|
||||
体验硬化。`M7-01` 至 `M7-07` 已通过,唯一下一任务为 `M7-08`。
|
||||
独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过;M7 核心项目体验硬化
|
||||
18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 当前
|
||||
13/14;后续领取点只读取机器队列最新 `nextTask`。
|
||||
|
||||
## 2. 已完成并有测试覆盖的能力
|
||||
|
||||
@@ -53,17 +58,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 Cut,Main 保存重开 | 已完成当前操作集 |
|
||||
| 材质与 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/UV,native 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 未开放 |
|
||||
| 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 仍属后续完整故障矩阵 |
|
||||
| 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 整体 BLOCKED;VDB 的 desktop/server converter、HTTP/OPFS、Float32 WebGPU 双生产视口、有限 grid 材质、显式 GPU resident LRU、确定性 resident OOM 和 Main 属性重开已完成,自动 demand paging、联合重开、大 bundle 和发布 golden 未完成 |
|
||||
| 实时渲染 reference | Blender 5.2 Eevee 固定相机/黑体/World fixture;SRGB8 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 hash;movie RGBA8 proxy identity 再绑定 READY receipt、profile 和 source frame;SEEK/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 帧 golden;M10-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 整体 BLOCKED;VDB 自动 demand paging、联合重开、64 MiB sparse 门和 desktop/main/Offscreen 三轴 density golden 已完成,深度合成与完整 Volume GLB/USD/材质仍阻断 |
|
||||
|
||||
## 3. 部分完成或仍有边界的能力
|
||||
|
||||
@@ -75,8 +85,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 desktop/server converter、HTTP/OPFS、有界 WebGPU 双视口、有限 grid 材质、显式 resident LRU 和 resident OOM 恢复 | 新外部字体导入,以及 VDB 自动 demand paging、联合保存重开、大 bundle 和发布 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/权重/对称组合及大模型性能矩阵未完成 |
|
||||
|
||||
@@ -195,8 +205,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`。
|
||||
|
||||
@@ -208,10 +218,15 @@ save/reopen、真实 legacy evaluated mesh、PointCloud/Curves 属性和 WNM 二
|
||||
`UsdGeomMesh`、`UsdGeomPoints` 和 `UsdGeomBasisCurves` 的机器可读映射/损失。
|
||||
VDB 已改为 desktop/server OpenVDB -> NanoVDB、浏览器分块读取 + WebGPU。真实资源 catalog、
|
||||
desktop/server converter、HTTP 断点续传、OPFS 原子绑定、Float32 WebGPU 双生产视口和 Main
|
||||
Volume 属性重开与 resident/page-table OOM 恢复已有真实证据;GPU demand paging、联合保存重开、完整材质、大 bundle 和
|
||||
desktop/Chromium 发布 golden 仍阻断。handle-local 专用 gizmo 已接通,新字体导入等剩余项按 `docs/status/N-015.md` 和
|
||||
Volume 属性、asset binding、双视口联合重开、自动 GPU page feedback、64 MiB sparse 门与
|
||||
desktop/Chromium 发布 golden(main/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 写回/保存重开、双后端
|
||||
@@ -228,8 +243,8 @@ HDR/EXR、多 tile 采样、Offscreen context-loss、真实 WebGPU renderer、
|
||||
|
||||
`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-table;这些记录不能替代
|
||||
demand paging、大 bundle 和发布 golden。
|
||||
记录覆盖 WASM/Main、OPFS staging、GPU 资源组和 NanoVDB resident/page-table;M8 后续专项已补齐
|
||||
自动分页、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
|
||||
@@ -262,8 +277,8 @@ MIME、4 个 HEAD、WASM/BLEND/NVDB range、6 类 `416`、强 ETag、If-Range
|
||||
`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 已完成 7/18,
|
||||
下一领取点是 `M7-08`。
|
||||
quick 7/7、Chromium 9/9、release 25/25 与 RC manifest 联合通过;M7 已完成 18/18,
|
||||
后续领取点只读取机器队列最新 `nextTask`,本文不缓存任务名称。
|
||||
|
||||
## 6. 当前验收命令
|
||||
|
||||
@@ -294,6 +309,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
|
||||
|
||||
@@ -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 depsgraph;frame 5 矩阵回归通过 |
|
||||
| N-014-B2 | 细粒度 create/remove/move/resize/active 命令 | 已完成有限切片 | M10-12 `moveNLAStrip` 通过 revision gate、单次 Main `setNLAStack` transaction、undo/redo 和 save/reopen;create/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 reader;GLB/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 Texture,UV 名称/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 physical;M10-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` | planned,C1a 已覆盖常量子集 |
|
||||
| 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
|
||||
```
|
||||
|
||||
@@ -199,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 Volume、OPFS asset binding、Worker 重建与主线程/Offscreen
|
||||
两个生产视口已在同一 revision/hash 闭环联合重开。
|
||||
- `VDB-043`:GLB 明确报告 Volume 无核心映射;USD 仅在 desktop USD/OpenVDB 路径真实可用时
|
||||
写入 field asset,不把 bounds proxy 当体积导出。`BLOCKED`。
|
||||
|
||||
@@ -209,8 +209,11 @@ 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 门。状态:`in_progress`;resident/page-table 确定性 OOM、唯一释放和同设备
|
||||
小 resident grid 恢复已完成,其余大 bundle 与联合故障矩阵仍阻断。
|
||||
|
||||
51
docs/status/M10-01.md
Normal file
51
docs/status/M10-01.md
Normal 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
56
docs/status/M10-02.md
Normal 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
73
docs/status/M10-03.md
Normal 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
75
docs/status/M10-04.md
Normal 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
68
docs/status/M10-05.md
Normal 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
84
docs/status/M10-06.md
Normal 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
59
docs/status/M10-07.md
Normal 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
59
docs/status/M10-08.md
Normal 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
40
docs/status/M10-09.md
Normal 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
39
docs/status/M10-10.md
Normal 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
44
docs/status/M10-11.md
Normal 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
45
docs/status/M10-12.md
Normal 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
48
docs/status/M10-13.md
Normal 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
47
docs/status/M10-14.md
Normal 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
51
docs/status/M10-15.md
Normal 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
42
docs/status/M11-01.md
Normal 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
48
docs/status/M11-02.md
Normal 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
55
docs/status/M11-03.md
Normal 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
56
docs/status/M11-04.md
Normal 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
47
docs/status/M11-05.md
Normal 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
48
docs/status/M11-06.md
Normal 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
36
docs/status/M11-07.md
Normal 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
37
docs/status/M11-08.md
Normal 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
43
docs/status/M11-09.md
Normal 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
57
docs/status/M11-10.md
Normal 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
52
docs/status/M11-11.md
Normal 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
58
docs/status/M11-12.md
Normal 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
58
docs/status/M11-13.md
Normal 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.
|
||||
57
docs/status/M9-14.md
Normal file
57
docs/status/M9-14.md
Normal 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.
|
||||
@@ -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 blend,Worker 重启后重新逐帧验证;
|
||||
缺失、版本不符和损坏均结构化阻断。
|
||||
M10-01 已把真实 Blender Main 图拓扑、socket 默认值、link 和稳定 node/socket ID 发布到
|
||||
SceneIR;M10-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 1:POINT/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,取消不发布
|
||||
manifest;LRU 按 `lastAccessAt/createdAt/cacheKey` 确定性淘汰并保护 active playback;Worker 重启
|
||||
重新验证,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`。
|
||||
|
||||
@@ -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` report;Native reader
|
||||
发布 graph SHA-256,Worker 在 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。
|
||||
|
||||
@@ -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`。
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
# N-015 非 Mesh 几何数据块
|
||||
|
||||
状态:`BLOCKED / in_progress`(VDB core 已覆盖 server、OPFS、Float32 WebGPU、显式 GPU 分页、
|
||||
有限 grid 材质与 Main 属性重开;自动 demand paging 缺页重绘、联合重开和发布 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,9 +14,10 @@ topology editor。
|
||||
Volume 采用“桌面/服务端 OpenVDB 转 NanoVDB,浏览器分块读取并以 WebGPU 渲染”的固定架构。
|
||||
浏览器不直接解码 OpenVDB;当前已完成真实资源 catalog、独立 OpenVDB 13 -> NanoVDB 32 desktop
|
||||
converter、受控 server job、转换/清单/分块协议、OPFS 绑定/重开、Float32 CPU/WGSL 树遍历、
|
||||
有界体积积分、主线程/Offscreen 生产视口、显式 hash 校验 GPU 分页/LRU、有限 grid 材质和
|
||||
Volume Main 属性保存重开。自动缺页重绘、联合重开和发布 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 预览 | 当前门 |
|
||||
| --- | --- | --- | --- |
|
||||
@@ -26,7 +27,7 @@ Volume Main 属性保存重开。自动缺页重绘、联合重开和发布 gold
|
||||
| Metaball | 元素类型、位置、半径、尺度 | bounded sphere proxy;depsgraph 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 可渲染真实 density/color/temperature/emission;双生产视口已接入 | `BLOCKED`(自动缺页/联合恢复/golden) |
|
||||
| Volume | path/grid metadata + VDB/NanoVDB 转换契约 | WebGPU 可渲染真实 density/color/temperature/emission;自动分页、联合重开与三轴 density golden 已接入 | `BLOCKED`(深度合成/完整材质与 IO/更大联合故障矩阵) |
|
||||
|
||||
## 已实现任务
|
||||
|
||||
@@ -104,6 +105,16 @@ Volume Main 属性保存重开。自动缺页重绘、联合重开和发布 gold
|
||||
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`。
|
||||
|
||||
## 后续分解
|
||||
|
||||
@@ -117,9 +128,9 @@ Volume Main 属性保存重开。自动缺页重绘、联合重开和发布 gold
|
||||
raycast、多 handle preview、handle-local gizmo 与单次 Main 写回已完成。
|
||||
4. N-015-D2:evaluated preview 与源控制笼分层显示、WebGPU 等价。
|
||||
5. N-015-B3/D2/E1(Volume):真实资源、desktop/server converter、可续传 HTTP range、OPFS、
|
||||
有界网络/Worker/quota/tamper/device-loss 故障门、Float32 WGSL core、有限 grid 材质、显式 GPU
|
||||
分页/LRU、resident/page-table OOM 恢复和 Main Volume 属性重开已完成;继续执行自动缺页重绘、联合恢复、GLB/USD loss、大 bundle 和
|
||||
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-E2:100k/1M WASM/GPU 内存、Worker restart 和 Chromium OPFS quota;Chromium
|
||||
64 KiB 真实 quota、失败后旧 revision 保持、Worker restart 恢复已通过。后续浏览器验收
|
||||
仅以 Chromium 为基线,不配置 Firefox/WebKit。
|
||||
@@ -131,7 +142,8 @@ Volume Main 属性保存重开。自动缺页重绘、联合重开和发布 gold
|
||||
- Metaball sphere proxy 不得用于导出或保存为求值曲面。
|
||||
- Volume/VDB 不得生成占位几何;协议 fixture 不得作为真实 decoder/renderer 证据。
|
||||
- 浏览器构建不得通过开启 `WITH_OPENVDB` 绕过 desktop/server 转换边界。
|
||||
- 自动缺页重绘、联合恢复、完整故障门和发布 golden 未齐前,VDB 与 N-015 保持 `BLOCKED`。
|
||||
- 场景深度合成、完整 Volume 材质/IO 和更大真实 bundle 联合故障矩阵未齐前,VDB 与 N-015 保持
|
||||
`BLOCKED`;M8 完成不能自动把 N-015 全域标为 `COMPLETE`。
|
||||
- Metaball sphere proxy 不得进入 GLB;GLB 只能消费 depsgraph evaluated mesh。
|
||||
- 任意新增 Main authoring 命令在 save/reopen 与 undo/redo 完成前不得开放 UI。
|
||||
|
||||
@@ -161,6 +173,10 @@ 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"
|
||||
```
|
||||
|
||||
@@ -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-D(editor 部分):editor context schema 绑定 data/layer/frame、onion 开关、stroke/
|
||||
point selection 和 revision,拒绝 stale/重复/超 1M selection;Properties 面板已连接真实 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-D(M9-06 marquee):Main 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-D(M9-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-D(M9-08 layer/frame reorder):纯 `grease-pencil-reorder` schema 将 layer 方向移动和
|
||||
drawing frame 移动绑定到稳定 data/layer/drawing ID 与 Main base revision;stale 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-C:material 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-E:desktop 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"
|
||||
```
|
||||
|
||||
@@ -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-10:WebEngine Worker 可序列化接收有界 color/weight chunks,一次 pointer
|
||||
session 仅在 commit 时对 Main 发一个合并命令;真实 Main 历史只产生一个 undo step。
|
||||
14. M9-11:Storage 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 budget;Worker 拒绝重复/越界/非法组合,native 在 Main 内先写入 patch,再做
|
||||
mirror、limit、normalize。镜像 map 使用 mesh 局部坐标,要求每个顶点都有 reciprocal
|
||||
counterpart,且大于 100k 顶点时结构化预算阻断。`rigged_shape_scene.blend` 的四步
|
||||
golden(initial/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-B:limit/clean、已验证拓扑映射上的 mirror、桌面 brush/falloff 对照。
|
||||
- N-017-C:Blender 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-B:clean/高级 Blender weight-paint operator、真实 PBVH brush/falloff 与更大拓扑的
|
||||
桌面对照;当前 bounded limit/normalize/verified mirror 已完成。
|
||||
- N-017-C:Blender packed/UDIM tile Main transaction、完整色彩转换和 `.blend` 内 image
|
||||
tile 保存仍阻断;浏览器 dirty tile 的 OPFS 原子 asset/binding 边界已完成。
|
||||
- N-017-D/E:armature 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。
|
||||
|
||||
@@ -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-D(M10-13 probe 门):七个 family 分别检查 runtime export、初始化、线程和内存;只有
|
||||
四门全过才允许 `LOCAL_SOLVER`。当前生产 inventory 没有 solver adapter,全部明确路由到
|
||||
`DESKTOP_SERVER_BAKE`。合成正例只验证门逻辑,不声明真实 WASM solver 或 cache decoder。
|
||||
12. N-018-C/D(M10-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-C:desktop 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-E:bake 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`。
|
||||
|
||||
@@ -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 图像指标闭环已落地;Scene 颜色管理
|
||||
writer、全域渲染等价和高级后端仍未实现)
|
||||
|
||||
## 已验证切片
|
||||
|
||||
@@ -24,6 +25,30 @@ Three exposure/shadow 映射已落地;Scene 颜色管理 writer 和渲染等
|
||||
7. N-019-B(色温部分):`useTemperature:true` 时,共享 PBR 适配器把 800–20000 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 planner;WebGL2
|
||||
总预算为 16 lights、4 x 1024 shadow maps(均包含内置保留槽),纹理同时限制数量、单边、
|
||||
aggregate payload 与 decoded RGBA bytes。WebGPU 只冻结 64/8 x 2048 及 device-limit 下调合同,
|
||||
未把未安装的 WebGPU renderer 标为可用。
|
||||
11. M11-04 有界实时 reference:Blender 5.2 Eevee 256×256 fixture 绑定 source/generator/image
|
||||
SHA-256;SRGB8/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 job:schema 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 并输出 PNG;source、settings、build
|
||||
和 output 篡改均在提交或消费前结构化拒绝。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
@@ -31,8 +56,11 @@ Three exposure/shadow 映射已落地;Scene 颜色管理 writer 和渲染等
|
||||
Blender/RNA 颜色管理 API 后才能开放。Camera writer 已完成。
|
||||
- N-019-B/C:AgX/Standard/Raw 的视觉等价、Area spread、Mist、DOF、
|
||||
transparent sorting、probe 和高级 shadow 参数。
|
||||
- N-019-D:Cycles/Freestyle/denoise 服务端 job 协议与结果 hash。
|
||||
- N-019-E:desktop/Chromium 像素 golden、设备丢失和 1M triangles。
|
||||
- N-019-D:M11-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 triangles;M11-04 仅关闭固定
|
||||
fixture 的有界 reference 指标门,不代表全场景或全色彩管理等价。
|
||||
- N-019-B/C/E(Volume):Float32 NanoVDB WGSL 树遍历、有界积分、有限
|
||||
temperature/color/emission grid 语义和显式 GPU resident LRU 已有真实 Chromium 数值/图像专项门;
|
||||
自动缺页重绘、生产视口深度合成和 desktop/Chromium 三视角 golden 仍阻断,不能据此声明发布级体渲染。
|
||||
@@ -41,5 +69,11 @@ Three exposure/shadow 映射已落地;Scene 颜色管理 writer 和渲染等
|
||||
|
||||
```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=5319 npm --prefix web run test:e2e -- --grep "N-019 Scene exposure"
|
||||
```
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# 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 +27,23 @@
|
||||
Output 的动态 `Socket_0` 规范化为 GraphIR `Image`。桌面生成 fixture 经 WASM/Worker 后直接由
|
||||
CPU executor 求值 Constant→Exposure→Invert→Composite,RGBA 结果通过;未连接的 Glare 仍保留
|
||||
Unsupported 并使完整图 capability gate 保持阻断。
|
||||
8. N-020-B/E(M11-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-D(M11-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 不能绕过该门。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
- N-020-A:Main graph 写回,以及 Transform、非默认 Invert、Alpha Over、Blur、Mix、Image/Render
|
||||
Layer 的逐节点参数与 resource/pass reader;基础真实图结构、常量色、Exposure、默认 Invert、
|
||||
Viewer/Composite 已完成。
|
||||
- N-020-B/C:WebGPU executor、tile scheduler、OPFS 持久 frame cache、增量 invalidation 和 GPU
|
||||
dispose;CPU 周期取消和内容寻址内存 LRU 已完成。
|
||||
- N-020-B/C:M11-07 只实现四节点、无资源、单输入链的有界 WebGPU compute;通用 WebGPU
|
||||
executor、tile scheduler、OPFS 持久 frame cache、增量 invalidation 和生产 GPU 调度仍阻断;
|
||||
CPU 周期取消和内容寻址内存 LRU 已完成。
|
||||
- N-020-D:服务端 Blender job、source hash 和结果提交。
|
||||
- N-020-E:desktop HDR/alpha/color-space golden、OOM/fault/device-loss。
|
||||
|
||||
@@ -43,4 +52,6 @@
|
||||
```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
|
||||
```
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
# N-021 Sequencer 与音频
|
||||
|
||||
状态:`BLOCKED`(strip schema、真实 Main reader、确定性时间编辑、长媒体有界索引/seek/cache
|
||||
和 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 @@
|
||||
映射,避免左右片段复用越界源帧。
|
||||
4. N-021-B/C(部分):`resolveSequencerFrame` 在切分边界解析当前顶层 strip、source frame
|
||||
与依赖;META 覆盖的子 strip 和 inactive dependency 不会泄漏到渲染帧。
|
||||
5. N-021-C/D(门):运行时只报告 WebCodecs/HTMLMedia 需要精确 probe;codec 必须
|
||||
出现在已验证 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。绝对路径、未知类型、
|
||||
@@ -32,16 +34,45 @@
|
||||
仍 resident;timeline/current frame/asset hashes 作为内容寻址 session manifest 写入 OPFS,终止
|
||||
StorageWorker 与 media Worker 后在 frame 765432 恢复。session SHA-256 固定为
|
||||
`d2e7e3c5ed9ae4fda6fe358cebb474f5774f1d2037dff3df9b6dfde9e0bebd2d`。
|
||||
11. 上述门只放行 fixture 的 IMAGE/SOUND 原始本地资源读取,不声称 WebCodecs MOVIE decode、
|
||||
waveform/proxy 生成或最终编码完成;`video/mp4` 和 local encoding 仍结构化阻断。
|
||||
11. 上述 V1 长媒体门只放行 fixture 的 IMAGE/SOUND 原始本地资源读取,不声称 WebCodecs MOVIE
|
||||
seek、waveform/proxy 生成或最终编码完成;local encoding 仍结构化阻断。
|
||||
12. M11-09 runtime codec probe:schema 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 cache:schema 1 identity 绑定 MOVIE source family/MIME/bytes/SHA-256、当前
|
||||
READY HTMLMedia receipt、SRGB8/STRAIGHT RGBA8 profile 和 source frame;payload 再独立绑定
|
||||
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 gate:SEEK/SCRUB/DECODE 使用同一 schema 1 request,绑定 timeline
|
||||
ID/revision、单调 request revision、operation 和 frame;completed 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 route:schema 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 recovery:schema 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.75,close 再归零并断开节点。API/构造器缺失与 resume
|
||||
failure 分别稳定返回 `SEQUENCER_AUDIO_DEVICE_UNAVAILABLE` 和
|
||||
`SEQUENCER_AUDIO_RESUME_FAILED`,不会误报输出启用。
|
||||
|
||||
## 仍然阻断
|
||||
|
||||
- N-021-A/B:Main strip 写回、非 CROSS transition/modifier 完整参数、undo/redo 和 save/reopen;
|
||||
基础真实 reader 与 CROSS/GAMMA_CROSS 帧描述已完成。
|
||||
- N-021-C:WebCodecs 精确 seek/decode、音频 waveform、proxy 生成、A/V sync、丢帧和
|
||||
损坏媒体处理。
|
||||
- N-021-D:浏览器不支持的 codec、混音与最终编码的服务端 Blender job。
|
||||
- N-021-C:M11-09/10/11/13 完成三类短 fixture 的初始实际解码门、MOVIE 初始帧 RGBA8 proxy
|
||||
cache identity、迟到结果 publish/cache gate 和实时 AudioContext 生命周期;WebCodecs 帧精确
|
||||
seek/decode、多帧 proxy、音频 waveform、A/V sync、实际混音、丢帧和长媒体损坏恢复仍阻断。
|
||||
- N-021-D:M11-12 已冻结最终编码的 server-export 路由与请求 identity;真正服务端 Blender
|
||||
encode job、进度/取消、混音和输出结果校验仍阻断。
|
||||
- N-021-E:V1 有界 Chromium/OPFS 长媒体门已完成;完整桌面/Chromium 解码/渲染 golden 仍阻断。
|
||||
|
||||
## 验收
|
||||
@@ -50,4 +81,9 @@
|
||||
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
|
||||
```
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"schemaVersion": 2,
|
||||
"updatedAt": "2026-08-14",
|
||||
"updatedAt": "2026-08-17",
|
||||
"source": "docs/BLENDER_5_2_WEB_FEATURE_PARITY.md",
|
||||
"parityStatusEnum": ["COMPLETE", "BLOCKED"],
|
||||
"releaseClassEnum": ["LOCAL_EXACT", "LOCAL_BOUNDED", "SERVER", "EXCLUDED"],
|
||||
@@ -13,10 +13,10 @@
|
||||
"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", "C3-volume-combined-asset-viewport-reopen", "E1-volume-glb-usd-loss-desktop-chromium-golden", "E2-vdb-large-stream-device-loss-oom"],
|
||||
"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", "D2-nanovdb-grid-material-mapping", "D2-nanovdb-explicit-resident-page-lru-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-automatic-page-fault-depth-composition", "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": []
|
||||
@@ -72,7 +72,7 @@
|
||||
"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"],
|
||||
"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"]
|
||||
@@ -86,7 +86,7 @@
|
||||
"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", "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"]
|
||||
@@ -100,7 +100,7 @@
|
||||
"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", "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"]
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{
|
||||
"schemaVersion": 4,
|
||||
"source": "docs/status/parity-ledger.json",
|
||||
"sourceSha256": "1e16518a25c6b6bed4ab7f605ea1ad5babe529f2842c858b20ae6daae30bd478",
|
||||
"generatedAt": "2026-08-15T05:37:25.215Z",
|
||||
"sourceSha256": "0a9b4c1026c42b5f77869f222b1531601ed229184a1d1737e74047c7b3406730",
|
||||
"generatedAt": "2026-08-17T08:22:00.415Z",
|
||||
"families": [
|
||||
{
|
||||
"id": "N-015",
|
||||
@@ -18,9 +18,7 @@
|
||||
"v1ExcludedSlices": [
|
||||
"C2-new-external-font-import",
|
||||
"D2-nanovdb-production-viewport-automatic-page-fault-depth-composition",
|
||||
"C3-volume-combined-asset-viewport-reopen",
|
||||
"E1-volume-glb-usd-loss-desktop-chromium-golden",
|
||||
"E2-vdb-large-stream-device-loss-oom"
|
||||
"E1-volume-glb-usd-loss-desktop-chromium-golden"
|
||||
],
|
||||
"roadmapStatus": "in_progress",
|
||||
"completedSlices": [
|
||||
@@ -61,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",
|
||||
@@ -72,6 +71,7 @@
|
||||
"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",
|
||||
@@ -79,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-automatic-page-fault-depth-composition",
|
||||
"C3-volume-combined-asset-viewport-reopen",
|
||||
"E1-volume-glb-usd-loss-desktop-chromium-golden",
|
||||
"E2-vdb-large-stream-device-loss-oom"
|
||||
"E1-volume-glb-usd-loss-desktop-chromium-golden"
|
||||
],
|
||||
"excludedSlices": [],
|
||||
"acceptance": [
|
||||
@@ -274,7 +274,10 @@
|
||||
"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"
|
||||
],
|
||||
"blockedSlices": [
|
||||
"A",
|
||||
@@ -317,6 +320,8 @@
|
||||
"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",
|
||||
"D-unsupported-node-preservation-gate"
|
||||
@@ -362,7 +367,12 @@
|
||||
"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",
|
||||
"D-final-export-server-routing-fail-closed"
|
||||
],
|
||||
"blockedSlices": [
|
||||
"A",
|
||||
|
||||
BIN
tests/files/web/geometry_node_allowlist_evaluator.blend
Normal file
BIN
tests/files/web/geometry_node_allowlist_evaluator.blend
Normal file
Binary file not shown.
BIN
tests/files/web/m11_compositor_allowlist.blend
Normal file
BIN
tests/files/web/m11_compositor_allowlist.blend
Normal file
Binary file not shown.
BIN
tests/files/web/m11_render_reference.blend
Normal file
BIN
tests/files/web/m11_render_reference.blend
Normal file
Binary file not shown.
@@ -19,6 +19,16 @@
|
||||
"objectNames": ["BasicArea", "BasicCamera", "BasicCube"],
|
||||
"frameRange": [1, 24]
|
||||
},
|
||||
{
|
||||
"id": "m11_render_reference",
|
||||
"path": "m11_render_reference.blend",
|
||||
"objects": 2,
|
||||
"meshes": 1,
|
||||
"materials": 1,
|
||||
"objectNames": ["M11 Reference Camera", "M11 Reference Cube"],
|
||||
"frameRange": [1, 250],
|
||||
"features": ["blender-eevee-reference", "webgl2-image-error-metrics", "main-offscreen-golden"]
|
||||
},
|
||||
{
|
||||
"id": "attribute_scene",
|
||||
"path": "attribute_scene.blend",
|
||||
@@ -38,6 +48,17 @@
|
||||
"frameRange": [1, 10],
|
||||
"animationTargets": ["object:AnimatedObject"]
|
||||
},
|
||||
{
|
||||
"id": "nla_time_mapping_scene",
|
||||
"path": "nla_time_mapping_scene.blend",
|
||||
"objects": 1,
|
||||
"meshes": 1,
|
||||
"materials": 0,
|
||||
"objectNames": ["M10_NLA_TimeMapping"],
|
||||
"frameRange": [1, 70],
|
||||
"animationTargets": ["object:M10_NLA_TimeMapping"],
|
||||
"features": ["nla-track-reader", "scaled-action-time", "reverse-repeat", "desktop-depsgraph-golden"]
|
||||
},
|
||||
{
|
||||
"id": "rigged_shape_scene",
|
||||
"path": "rigged_shape_scene.blend",
|
||||
@@ -157,6 +178,13 @@
|
||||
"meshes": 0,
|
||||
"features": ["compositor-main-reader", "exposure-invert-parameters", "socket-links", "unsupported-node-preservation"]
|
||||
},
|
||||
{
|
||||
"id": "m11_compositor_allowlist",
|
||||
"path": "m11_compositor_allowlist.blend",
|
||||
"objects": 0,
|
||||
"meshes": 0,
|
||||
"features": ["compositor-node-allowlist", "cpu-webgpu-golden", "constant-color", "exposure", "invert"]
|
||||
},
|
||||
{
|
||||
"id": "sequencer_scene",
|
||||
"path": "sequencer_scene.blend",
|
||||
|
||||
BIN
tests/files/web/media/sequencer-probe.mp4
Normal file
BIN
tests/files/web/media/sequencer-probe.mp4
Normal file
Binary file not shown.
BIN
tests/files/web/nla_time_mapping_scene.blend
Normal file
BIN
tests/files/web/nla_time_mapping_scene.blend
Normal file
Binary file not shown.
46
tests/golden/M10-01/geometry-node-main-reader.json
Normal file
46
tests/golden/M10-01/geometry-node-main-reader.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"fixture": "tests/files/web/modifier_geometry_nodes_scene.blend",
|
||||
"fixtureSha256": "f3820511f791769837d75be092934130c397cf46ee0acf79db28efca9a7948c9",
|
||||
"desktopGolden": "tests/golden/W-075/modifier_geometry_nodes_scene.json",
|
||||
"desktopGoldenSha256": "93c21e5158daefce10f2d4df674e636bf3f6ed3dd43e1576c45a02a8592f5886",
|
||||
"graphs": [
|
||||
{
|
||||
"name": "WebGeometryNodes",
|
||||
"nodeTypes": ["NodeGroupInput", "GeometryNodeTransform", "NodeGroupOutput"],
|
||||
"linkCount": 2,
|
||||
"defaults": {
|
||||
"GeometryNodeTransform:Mode": 0,
|
||||
"GeometryNodeTransform:Translation": [0.25, 0.5, 1.0],
|
||||
"GeometryNodeTransform:Rotation": [0.0, 0.0, 0.2617993950843811],
|
||||
"GeometryNodeTransform:Scale": [1.0, 1.0, 1.0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WebGeometryNodesSetPosition",
|
||||
"nodeTypes": ["NodeGroupInput", "GeometryNodeSetPosition", "NodeGroupOutput"],
|
||||
"linkCount": 2,
|
||||
"defaults": {
|
||||
"GeometryNodeSetPosition:Selection": true,
|
||||
"GeometryNodeSetPosition:Position": [0.0, 0.0, 0.0],
|
||||
"GeometryNodeSetPosition:Offset": [0.0, 0.0, 1.0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "WebGeometryNodesSimulation",
|
||||
"nodeTypes": ["GeometryNodeSimulationInput", "NodeGroupOutput", "GeometryNodeSimulationOutput", "NodeGroupInput"],
|
||||
"linkCount": 3,
|
||||
"defaults": {
|
||||
"GeometryNodeSimulationInput:Delta Time": 0.0,
|
||||
"GeometryNodeSimulationOutput:Skip": false
|
||||
}
|
||||
}
|
||||
],
|
||||
"budgets": {
|
||||
"maxGraphs": 4096,
|
||||
"maxNodesPerGraph": 4096,
|
||||
"maxLinksPerGraph": 16384,
|
||||
"maxSocketsPerGraph": 65536,
|
||||
"maxInterfaceSocketsPerGraph": 4096
|
||||
}
|
||||
}
|
||||
32
tests/golden/M10-02/geometry-node-allowlist.json
Normal file
32
tests/golden/M10-02/geometry-node-allowlist.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"fixture": "tests/files/web/modifier_geometry_nodes_scene.blend",
|
||||
"fixtureSha256": "f3820511f791769837d75be092934130c397cf46ee0acf79db28efca9a7948c9",
|
||||
"allowlistSchemaVersion": 1,
|
||||
"allowlist": [
|
||||
"NodeGroupInput",
|
||||
"NodeGroupOutput",
|
||||
"GeometryNodeTransform",
|
||||
"GeometryNodeSetPosition",
|
||||
"GeometryNodeJoinGeometry",
|
||||
"GeometryNodeSeparateGeometry",
|
||||
"GeometryNodeRealizeInstances",
|
||||
"GeometryNodeStoreNamedAttribute",
|
||||
"FunctionNodeInputInt",
|
||||
"FunctionNodeInputVector",
|
||||
"FunctionNodeCompare",
|
||||
"ShaderNodeValue",
|
||||
"ShaderNodeMath",
|
||||
"GeometryNodeObjectInfo",
|
||||
"GeometryNodeCollectionInfo",
|
||||
"GeometryNodeImageInfo"
|
||||
],
|
||||
"supportedGraph": "WebGeometryNodes",
|
||||
"unsupportedGraph": "WebGeometryNodesSimulation",
|
||||
"unsupportedNodeTypes": [
|
||||
"GeometryNodeSimulationInput",
|
||||
"GeometryNodeSimulationOutput"
|
||||
],
|
||||
"unsupportedErrorCode": "GN_NODE_UNSUPPORTED",
|
||||
"evaluatorUnavailableErrorCode": "CAPABILITY_MISSING"
|
||||
}
|
||||
1187
tests/golden/M10-03/geometry-node-evaluator.json
Normal file
1187
tests/golden/M10-03/geometry-node-evaluator.json
Normal file
File diff suppressed because it is too large
Load Diff
34
tests/golden/M10-07/shader-compile.json
Normal file
34
tests/golden/M10-07/shader-compile.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "M10-07",
|
||||
"fixture": "tests/files/web/basic_scene.blend",
|
||||
"fixtureSha256": "6fcc55bda74da8c95da96ba068b60339bf60ca44147ff8000fbb95d296db2a73",
|
||||
"backend": "WEBGL2_THREE_PHYSICAL",
|
||||
"allowlist": [
|
||||
"RGB",
|
||||
"VALUE",
|
||||
"MATH",
|
||||
"IMAGE_TEXTURE",
|
||||
"NORMAL_MAP",
|
||||
"PRINCIPLED",
|
||||
"OUTPUT"
|
||||
],
|
||||
"mathOperations": [
|
||||
"ADD",
|
||||
"SUBTRACT",
|
||||
"MULTIPLY",
|
||||
"DIVIDE",
|
||||
"MINIMUM",
|
||||
"MAXIMUM"
|
||||
],
|
||||
"budget": {
|
||||
"maxNodes": 128,
|
||||
"maxLinks": 512,
|
||||
"maxDepth": 64,
|
||||
"maxTextures": 16,
|
||||
"maxIdentifierBytes": 256,
|
||||
"maxNameBytes": 1024
|
||||
},
|
||||
"expectedMathResult": 0.42,
|
||||
"unsupportedNodeCode": "SHADER_NODE_UNSUPPORTED"
|
||||
}
|
||||
10
tests/golden/M10-08/shader-compile-key.json
Normal file
10
tests/golden/M10-08/shader-compile-key.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "M10-08",
|
||||
"backend": "WEBGL2_THREE_PHYSICAL",
|
||||
"textureColorSpaces": ["SRGB", "NON_COLOR", "LINEAR"],
|
||||
"keyInputs": ["graphHash", "rendererBackend", "imageId", "usage", "assetId", "sha256", "colorSpace"],
|
||||
"keyDigest": "sha256",
|
||||
"unknownBackendCode": "CAPABILITY_MISSING",
|
||||
"invalidTextureIdentityCode": "SHADER_INVALID_GRAPH"
|
||||
}
|
||||
8
tests/golden/M10-09/shader-pipeline.json
Normal file
8
tests/golden/M10-09/shader-pipeline.json
Normal file
@@ -0,0 +1,8 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "M10-09",
|
||||
"failureStatus": "BLOCKED",
|
||||
"preservedPipeline": true,
|
||||
"replacedOnSuccess": true,
|
||||
"failureDoesNotDisposePrevious": true
|
||||
}
|
||||
10
tests/golden/M10-10/shader-capability-block.json
Normal file
10
tests/golden/M10-10/shader-capability-block.json
Normal file
@@ -0,0 +1,10 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "M10-10",
|
||||
"capability": "ARBITRARY_SHADER",
|
||||
"status": "BLOCKED",
|
||||
"unsupportedNodeTypes": ["VORONOI", "CUSTOM_OSL"],
|
||||
"errorCode": "SHADER_NODE_UNSUPPORTED",
|
||||
"recoverable": true,
|
||||
"graphDataPolicy": "PRESERVE"
|
||||
}
|
||||
308
tests/golden/M10-11/nla-evaluation.json
Normal file
308
tests/golden/M10-11/nla-evaluation.json
Normal file
@@ -0,0 +1,308 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"fixture": "tests/files/web/nla_time_mapping_scene.blend",
|
||||
"fixtureSha256": "b32f783debe213a1345ec4528e5ed27f0f4de00d7896b626df39756289119855",
|
||||
"blenderVersion": "5.2.0 LTS",
|
||||
"object": "M10_NLA_TimeMapping",
|
||||
"mesh": "M10_NLA_TimeMappingMesh",
|
||||
"tracks": [
|
||||
{
|
||||
"name": "M10 Time Mapping",
|
||||
"muted": false,
|
||||
"solo": false,
|
||||
"strips": [
|
||||
{
|
||||
"id": "M10 Scaled Clip",
|
||||
"action": "M10_NLA_Scaled_X",
|
||||
"frameStart": 20.0,
|
||||
"frameEnd": 40.0,
|
||||
"actionFrameStart": 1.0,
|
||||
"actionFrameEnd": 11.0,
|
||||
"scale": 2.0,
|
||||
"repeat": 1.0,
|
||||
"blendIn": 0.0,
|
||||
"blendOut": 0.0,
|
||||
"influence": 1.0,
|
||||
"blendMode": "REPLACE",
|
||||
"extrapolation": "NOTHING",
|
||||
"muted": false,
|
||||
"reverse": false
|
||||
},
|
||||
{
|
||||
"id": "M10 Reverse Repeat Clip",
|
||||
"action": "M10_NLA_ReverseRepeat_Y",
|
||||
"frameStart": 45.0,
|
||||
"frameEnd": 65.0,
|
||||
"actionFrameStart": 1.0,
|
||||
"actionFrameEnd": 11.0,
|
||||
"scale": 1.0,
|
||||
"repeat": 2.0,
|
||||
"blendIn": 0.0,
|
||||
"blendOut": 0.0,
|
||||
"influence": 1.0,
|
||||
"blendMode": "REPLACE",
|
||||
"extrapolation": "NOTHING",
|
||||
"muted": false,
|
||||
"reverse": true
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"frames": [
|
||||
{
|
||||
"frame": 1,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 20,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 25,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
2.5,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 30,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
5.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 35,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
7.5,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 40,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
10.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 45,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
10.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 50,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
5.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 55,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
10.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 60,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
5.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 65,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
},
|
||||
{
|
||||
"frame": 70,
|
||||
"worldMatrix": [
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
0.0,
|
||||
1.0
|
||||
]
|
||||
}
|
||||
],
|
||||
"tolerance": {
|
||||
"maxMatrixError": 1e-05
|
||||
}
|
||||
}
|
||||
24
tests/golden/M10-13/physics-solver-probe.json
Normal file
24
tests/golden/M10-13/physics-solver-probe.json
Normal file
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"familyOrder": [
|
||||
"RIGID_BODY",
|
||||
"SOFT_BODY",
|
||||
"CLOTH",
|
||||
"FLUID",
|
||||
"DYNAMIC_PAINT",
|
||||
"PARTICLE",
|
||||
"HAIR"
|
||||
],
|
||||
"defaultProbe": "EXPORT_UNAVAILABLE",
|
||||
"probes": {
|
||||
"RIGID_BODY": "READY",
|
||||
"SOFT_BODY": "INITIALIZATION_FAILED",
|
||||
"CLOTH": "THREADS_UNAVAILABLE",
|
||||
"FLUID": "MEMORY_UNAVAILABLE",
|
||||
"DYNAMIC_PAINT": "INVALID_RESULT",
|
||||
"PARTICLE": "INITIALIZATION_FAILED",
|
||||
"HAIR": "EXPORT_UNAVAILABLE"
|
||||
},
|
||||
"localRoute": "LOCAL_SOLVER",
|
||||
"fallbackRoute": "DESKTOP_SERVER_BAKE",
|
||||
"fallbackErrorCode": "PHYSICS_SOLVER_UNAVAILABLE"
|
||||
}
|
||||
26
tests/golden/M10-14/physics-cache-family.json
Normal file
26
tests/golden/M10-14/physics-cache-family.json
Normal file
@@ -0,0 +1,26 @@
|
||||
{
|
||||
"families": [
|
||||
"RIGID_BODY",
|
||||
"SOFT_BODY",
|
||||
"CLOTH",
|
||||
"FLUID",
|
||||
"DYNAMIC_PAINT",
|
||||
"PARTICLE",
|
||||
"HAIR"
|
||||
],
|
||||
"sources": [
|
||||
"BLENDER_DESKTOP_BAKE",
|
||||
"BLENDER_SERVER_BAKE",
|
||||
"BLENDER_DESKTOP_BAKE",
|
||||
"BLENDER_SERVER_BAKE",
|
||||
"BLENDER_DESKTOP_BAKE",
|
||||
"BLENDER_SERVER_BAKE",
|
||||
"BLENDER_DESKTOP_BAKE"
|
||||
],
|
||||
"byteLength": 4,
|
||||
"frameCount": 2,
|
||||
"sourceMismatch": "PHYSICS_CACHE_SOURCE_MISMATCH",
|
||||
"payloadMismatch": "PHYSICS_CACHE_HASH_MISMATCH",
|
||||
"versionMismatch": "PROTOCOL_MISMATCH",
|
||||
"budgetExceeded": "PHYSICS_BUDGET_EXCEEDED"
|
||||
}
|
||||
33
tests/golden/M10-15/domain-browser-gates.json
Normal file
33
tests/golden/M10-15/domain-browser-gates.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"domains": {
|
||||
"GN": {
|
||||
"workUnits": 10,
|
||||
"maximumMs": 5000,
|
||||
"performanceStatus": "SUPPORTED",
|
||||
"oomCode": "GN_GRAPH_BUDGET_EXCEEDED",
|
||||
"maliciousCode": "GN_DEPENDENCY_CYCLE"
|
||||
},
|
||||
"SHADER": {
|
||||
"workUnits": 100,
|
||||
"maximumMs": 5000,
|
||||
"performanceStatus": "COMPILED",
|
||||
"oomCode": "SHADER_NODE_UNSUPPORTED",
|
||||
"maliciousCode": "SHADER_GRAPH_CYCLE"
|
||||
},
|
||||
"NLA": {
|
||||
"workUnits": 20,
|
||||
"maximumMs": 5000,
|
||||
"performanceStatus": "SUPPORTED",
|
||||
"oomCode": "NLA_BUDGET_EXCEEDED",
|
||||
"maliciousCode": "NLA_INVALID_STACK"
|
||||
},
|
||||
"SIMULATION": {
|
||||
"workUnits": 64,
|
||||
"maximumMs": 10000,
|
||||
"performanceStatus": "VERIFIED",
|
||||
"oomCode": "SIMULATION_CACHE_BUDGET_EXCEEDED",
|
||||
"maliciousCode": "SIMULATION_CACHE_INVALID"
|
||||
}
|
||||
}
|
||||
}
|
||||
128
tests/golden/M11-01/lighting-field-parity.json
Normal file
128
tests/golden/M11-01/lighting-field-parity.json
Normal file
@@ -0,0 +1,128 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-01",
|
||||
"blenderVersion": "5.2.0",
|
||||
"legend": {
|
||||
"VERIFIED": "Implemented and covered by the cited bounded path",
|
||||
"PARTIAL": "Implemented for a declared subset or approximate mapping",
|
||||
"METADATA_ONLY": "Exposed to the viewport but not visually evaluated",
|
||||
"BLOCKED": "Not implemented or not parity-safe",
|
||||
"NOT_APPLICABLE": "The stage does not mutate or visually consume this identity/status field"
|
||||
},
|
||||
"domains": [
|
||||
{
|
||||
"domain": "CAMERA",
|
||||
"sourceInterface": "CameraIR",
|
||||
"evidence": [
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/protocol/web-engine.ts",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"tools/web/check-lighting-roundtrip.mjs",
|
||||
"web/app/src/three-adapter/viewport.ts",
|
||||
"web/app/src/workers/viewport-render.worker.ts"
|
||||
],
|
||||
"fields": [
|
||||
{ "path": "id", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "name", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "projection", "reader": "VERIFIED", "writer": "PARTIAL", "viewportMain": "PARTIAL", "viewportOffscreen": "PARTIAL", "parity": "PARTIAL", "note": "Writer supports Perspective/Orthographic; viewport still uses PerspectiveCamera and blocks Panoramic/Custom parity." },
|
||||
{ "path": "lensMm", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "sensorWidthMm", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "sensorHeightMm", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "sensorFit", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "shift", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "PARTIAL", "viewportOffscreen": "PARTIAL", "parity": "PARTIAL", "note": "Horizontal film offset is mapped; vertical shift is not." },
|
||||
{ "path": "near", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "far", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "orthoScale", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "panoramaType", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "fisheyeFov", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.enabled", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.focusObjectId", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.focusDistance", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.apertureFStop", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.apertureBlades", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.apertureRotation", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "depthOfField.apertureRatio", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"domain": "LIGHT",
|
||||
"sourceInterface": "LightIR",
|
||||
"evidence": [
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/protocol/web-engine.ts",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"tools/web/check-lighting-roundtrip.mjs",
|
||||
"web/app/src/three-adapter/pbr.ts"
|
||||
],
|
||||
"fields": [
|
||||
{ "path": "id", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "name", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "lightType", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "PARTIAL" },
|
||||
{ "path": "color", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "energy", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "exposure", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "temperature", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "useTemperature", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "castsShadow", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "radius", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "spotAngle", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "spotBlend", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "areaShape", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "PARTIAL", "viewportOffscreen": "PARTIAL", "parity": "PARTIAL", "note": "All Area shapes currently map to RectAreaLight." },
|
||||
{ "path": "areaSize", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "areaSizeY", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "areaSpread", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "sunAngle", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"domain": "WORLD",
|
||||
"sourceInterface": "WorldIR",
|
||||
"evidence": [
|
||||
"web/protocol/scene-ir.ts",
|
||||
"web/protocol/web-engine.ts",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"tools/web/check-lighting-roundtrip.mjs",
|
||||
"web/app/src/three-adapter/viewport.ts",
|
||||
"web/app/src/three-adapter/texture-assets.ts"
|
||||
],
|
||||
"fields": [
|
||||
{ "path": "id", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "name", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" },
|
||||
{ "path": "color", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "COMPLETE" },
|
||||
{ "path": "exposure", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "PARTIAL", "viewportOffscreen": "PARTIAL", "parity": "PARTIAL", "note": "World exposure is used only when Scene exposure is absent." },
|
||||
{ "path": "environmentImageId", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "PARTIAL" },
|
||||
{ "path": "environmentStrength", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "PARTIAL" },
|
||||
{ "path": "environmentRotation", "reader": "BLOCKED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "backgroundVisible", "reader": "PARTIAL", "writer": "BLOCKED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "PARTIAL", "note": "Reader currently emits true rather than resolving the full node visibility graph." },
|
||||
{ "path": "mist.enabled", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "mist.type", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "mist.start", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "mist.depth", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "mist.intensity", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "mist.height", "reader": "VERIFIED", "writer": "VERIFIED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" }
|
||||
]
|
||||
},
|
||||
{
|
||||
"domain": "SCENE_COLOR_MANAGEMENT",
|
||||
"sourceInterface": "SceneIR",
|
||||
"evidence": [
|
||||
"web/protocol/scene-ir.ts",
|
||||
"blender-5.2.0/source/blender/web_engine/web_engine_blend_reader.cpp",
|
||||
"tools/web/check-lighting-roundtrip.mjs",
|
||||
"web/app/src/three-adapter/viewport.ts",
|
||||
"web/app/src/workers/viewport-render.worker.ts"
|
||||
],
|
||||
"fields": [
|
||||
{ "path": "renderEngine", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "colorManagement.displayDevice", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "colorManagement.viewTransform", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "colorManagement.look", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "METADATA_ONLY", "viewportOffscreen": "METADATA_ONLY", "parity": "PARTIAL" },
|
||||
{ "path": "colorManagement.exposure", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "VERIFIED", "viewportOffscreen": "VERIFIED", "parity": "PARTIAL" },
|
||||
{ "path": "colorManagement.gamma", "reader": "VERIFIED", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "colorManagement.temperature", "reader": "PARTIAL", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "colorManagement.tint", "reader": "PARTIAL", "writer": "BLOCKED", "viewportMain": "BLOCKED", "viewportOffscreen": "BLOCKED", "parity": "BLOCKED" },
|
||||
{ "path": "colorManagement.whiteBalanceStatus", "reader": "VERIFIED", "writer": "NOT_APPLICABLE", "viewportMain": "NOT_APPLICABLE", "viewportOffscreen": "NOT_APPLICABLE", "parity": "COMPLETE" }
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
46
tests/golden/M11-02/lighting-field-roundtrip.json
Normal file
46
tests/golden/M11-02/lighting-field-roundtrip.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"camera": {
|
||||
"lensMm": 35,
|
||||
"sensorWidthMm": 32,
|
||||
"sensorHeightMm": 18,
|
||||
"sensorFit": 2,
|
||||
"shift": [0.1, -0.2],
|
||||
"near": 0.2,
|
||||
"far": 500,
|
||||
"orthoScale": 8,
|
||||
"viewportFov": 28.841546,
|
||||
"viewportHorizontalFov": 49.134343,
|
||||
"viewportFilmGauge": 18,
|
||||
"viewportFilmOffset": 1.8
|
||||
},
|
||||
"light": {
|
||||
"color": [0.25, 0.5, 0.75],
|
||||
"energy": 400,
|
||||
"exposure": 1,
|
||||
"temperature": 5000,
|
||||
"useTemperature": true,
|
||||
"castsShadow": false,
|
||||
"radius": 0.3,
|
||||
"spotAngle": 1.1,
|
||||
"spotBlend": 0.25,
|
||||
"areaSize": 3,
|
||||
"areaSizeY": 2,
|
||||
"areaSpread": 2.4,
|
||||
"sunAngle": 0.1,
|
||||
"viewportIntensity": 80,
|
||||
"viewportColor": [0.25, 0.3910381443, 0.4834313443]
|
||||
},
|
||||
"world": {
|
||||
"color": [0.1, 0.2, 0.3],
|
||||
"exposure": 0.5,
|
||||
"mist": {
|
||||
"enabled": true,
|
||||
"type": "LINEAR",
|
||||
"start": 2,
|
||||
"depth": 50,
|
||||
"intensity": 0.2,
|
||||
"height": 3
|
||||
}
|
||||
},
|
||||
"tolerance": 0.00001
|
||||
}
|
||||
32
tests/golden/M11-03/render-resource-budget.json
Normal file
32
tests/golden/M11-03/render-resource-budget.json
Normal file
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"webgl2": {
|
||||
"maxLights": 16,
|
||||
"reservedLights": 2,
|
||||
"maxShadowMaps": 4,
|
||||
"reservedShadowMaps": 1,
|
||||
"shadowMapDimension": 1024,
|
||||
"maxTextureAssets": 256,
|
||||
"maxTexturePayloadBytes": 536870912,
|
||||
"maxTextureGPUBytes": 536870912
|
||||
},
|
||||
"webgpu": {
|
||||
"maxLights": 64,
|
||||
"reservedLights": 2,
|
||||
"maxShadowMaps": 8,
|
||||
"reservedShadowMaps": 1,
|
||||
"shadowMapDimension": 2048,
|
||||
"maxTextureAssets": 256,
|
||||
"maxTexturePayloadBytes": 536870912,
|
||||
"maxTextureGPUBytes": 1073741824
|
||||
},
|
||||
"overflow": {
|
||||
"requestedLights": 20,
|
||||
"renderedLights": 14,
|
||||
"droppedLights": 6,
|
||||
"requestedShadowMaps": 14,
|
||||
"renderedShadowMaps": 3,
|
||||
"blockedShadowMaps": 11,
|
||||
"codes": ["GPU_LIGHT_BUDGET_EXCEEDED", "GPU_SHADOW_BUDGET_EXCEEDED"]
|
||||
}
|
||||
}
|
||||
BIN
tests/golden/M11-04/blender-eevee-reference.png
Normal file
BIN
tests/golden/M11-04/blender-eevee-reference.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 3.8 KiB |
30
tests/golden/M11-04/manifest.json
Normal file
30
tests/golden/M11-04/manifest.json
Normal file
@@ -0,0 +1,30 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-04",
|
||||
"source": {
|
||||
"fixture": "tests/files/web/m11_render_reference.blend",
|
||||
"fixtureSha256": "d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963",
|
||||
"generator": "tools/web/generate-m11-render-reference.py",
|
||||
"generatorSha256": "118c98c77d98922e72660898bfb65842e11743fc77f4124c9f5088a76cb6a94a",
|
||||
"blenderVersion": "5.2.0 LTS"
|
||||
},
|
||||
"reference": {
|
||||
"file": "blender-eevee-reference.png",
|
||||
"sha256": "f5c22636a232bcbb0ed0f772418cb12fced18ff72f4999804197e42caa61480a",
|
||||
"width": 256,
|
||||
"height": 256,
|
||||
"colorSpace": "SRGB8",
|
||||
"alphaMode": "STRAIGHT",
|
||||
"renderEngine": "BLENDER_EEVEE"
|
||||
},
|
||||
"thresholds": {
|
||||
"maxMeanAbsoluteError": 4,
|
||||
"maxRootMeanSquaredError": 12,
|
||||
"maxP95ChannelError": 12,
|
||||
"maxBadPixelRatio": 0.02,
|
||||
"badPixelChannelError": 48,
|
||||
"foregroundDeltaFromReferenceBackground": 40,
|
||||
"minForegroundIntersectionOverUnion": 0.97,
|
||||
"maxAlphaCoverageDeltaRatio": 0
|
||||
}
|
||||
}
|
||||
48
tests/golden/M11-05/render-routing.json
Normal file
48
tests/golden/M11-05/render-routing.json
Normal file
@@ -0,0 +1,48 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"boundedEevee": {
|
||||
"target": "WEB_LOCAL_BOUNDED",
|
||||
"status": "READY",
|
||||
"capability": "WEB_REALTIME_BOUNDED",
|
||||
"reason": "BOUNDED_EEVEE"
|
||||
},
|
||||
"cycles": {
|
||||
"target": "SERVER_JOB",
|
||||
"withoutEndpoint": {
|
||||
"status": "BLOCKED",
|
||||
"code": "SERVER_JOB_UNAVAILABLE",
|
||||
"reason": "CYCLES_REQUIRES_SERVER"
|
||||
},
|
||||
"withEndpoint": {
|
||||
"status": "READY",
|
||||
"capability": "CYCLES_SERVER_RENDER"
|
||||
}
|
||||
},
|
||||
"complexEevee": {
|
||||
"target": "SERVER_JOB",
|
||||
"withoutEndpoint": {
|
||||
"status": "BLOCKED",
|
||||
"code": "SERVER_JOB_UNAVAILABLE",
|
||||
"reason": "COMPLEX_EEVEE_REQUIRES_SERVER"
|
||||
}
|
||||
},
|
||||
"hardware": {
|
||||
"target": "SERVER_JOB",
|
||||
"withoutEndpoint": {
|
||||
"status": "BLOCKED",
|
||||
"code": "SERVER_JOB_UNAVAILABLE",
|
||||
"reason": "HARDWARE_BACKEND_REQUIRES_SERVER"
|
||||
}
|
||||
},
|
||||
"webgpu": {
|
||||
"target": "WEB_LOCAL_BOUNDED",
|
||||
"status": "BLOCKED",
|
||||
"code": "WEBGPU_RENDERER_UNAVAILABLE"
|
||||
},
|
||||
"unknownEngine": {
|
||||
"target": "SERVER_JOB",
|
||||
"status": "BLOCKED",
|
||||
"capability": "UNSUPPORTED_RENDER_ENGINE",
|
||||
"code": "PLATFORM_CAPABILITY_UNAVAILABLE"
|
||||
}
|
||||
}
|
||||
18
tests/golden/M11-06/server-render-job.json
Normal file
18
tests/golden/M11-06/server-render-job.json
Normal file
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-06",
|
||||
"blenderVersion": "5.2.0",
|
||||
"sourceBlendSha256": "d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963",
|
||||
"settingsSha256": "97bb832618bd27e8763722afa0d250ef74383fb355f0ff38763930f57ba1590b",
|
||||
"output": {
|
||||
"mime": "image/png",
|
||||
"minimumByteLength": 1,
|
||||
"sha256Pattern": "^[a-f0-9]{64}$"
|
||||
},
|
||||
"tamperCodes": {
|
||||
"source": "SERVER_RENDER_SOURCE_HASH_MISMATCH",
|
||||
"build": "SERVER_RENDER_BUILD_MISMATCH",
|
||||
"settings": "SERVER_RENDER_SETTINGS_HASH_MISMATCH",
|
||||
"output": "SERVER_RENDER_OUTPUT_HASH_MISMATCH"
|
||||
}
|
||||
}
|
||||
46
tests/golden/M11-07/compositor-node-golden.json
Normal file
46
tests/golden/M11-07/compositor-node-golden.json
Normal file
@@ -0,0 +1,46 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-07",
|
||||
"fixture": {
|
||||
"path": "tests/files/web/m11_compositor_allowlist.blend",
|
||||
"sha256": "e844eba69002101c8cf4f376cdd844c4be78729091627d898634088707374b29",
|
||||
"generator": "tools/web/generate-m11-compositor-allowlist.py",
|
||||
"generatorSha256": "1389998aac849065311c85ef559a7d381274c0ebe1987afc3b6783c66a9a678e",
|
||||
"blenderVersion": "5.2.0"
|
||||
},
|
||||
"width": 2,
|
||||
"height": 2,
|
||||
"maxAbsoluteError": 0,
|
||||
"allowlist": ["CONSTANT_COLOR", "EXPOSURE", "INVERT", "COMPOSITE"],
|
||||
"cases": [
|
||||
{
|
||||
"scene": "M11 Constant",
|
||||
"newNode": "CONSTANT_COLOR",
|
||||
"nodeTypes": ["CONSTANT_COLOR", "COMPOSITE"],
|
||||
"pixel": [0.125, 0.25, 0.5, 0.75],
|
||||
"float32Sha256": "3664d5dd48f309ba6a45e84ad84bf931c7775de21b2f8eef255651743dcc7bf5"
|
||||
},
|
||||
{
|
||||
"scene": "M11 Exposure",
|
||||
"newNode": "EXPOSURE",
|
||||
"nodeTypes": ["CONSTANT_COLOR", "EXPOSURE", "COMPOSITE"],
|
||||
"pixel": [0.25, 0.5, 1, 0.75],
|
||||
"float32Sha256": "0c8caa21f76305cf3fc3a677f5c7af34811a0b29aaa035a5f55be15e2d89accd"
|
||||
},
|
||||
{
|
||||
"scene": "M11 Invert",
|
||||
"newNode": "INVERT",
|
||||
"nodeTypes": ["CONSTANT_COLOR", "INVERT", "COMPOSITE"],
|
||||
"pixel": [0.875, 0.75, 0.5, 0.75],
|
||||
"float32Sha256": "d96848cf025a567550b7de8a3fcfb304594bb3fbad5c8722fa39f55e23845c55"
|
||||
},
|
||||
{
|
||||
"scene": "M11 Chain",
|
||||
"newNode": "INTEGRATION",
|
||||
"nodeTypes": ["CONSTANT_COLOR", "EXPOSURE", "INVERT", "COMPOSITE"],
|
||||
"pixel": [0.75, 0.5, 0, 0.75],
|
||||
"float32Sha256": "20394b91c848df1aca00179b6785df2d5b38d44c333c9d9d1c926fe674f03a04"
|
||||
}
|
||||
],
|
||||
"blockedNodeTypes": ["ALPHA_OVER", "BLUR", "IMAGE", "MIX", "RENDER_LAYER", "TRANSFORM", "UNSUPPORTED", "VIEWER"]
|
||||
}
|
||||
19
tests/golden/M11-08/compositor-unsupported-gate.json
Normal file
19
tests/golden/M11-08/compositor-unsupported-gate.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-08",
|
||||
"fixture": {
|
||||
"path": "tests/files/web/compositor_scene.blend",
|
||||
"sha256": "348eeefc28a0fdaa2d55b3c7251a737e0a04d82f61dd1eba804662a8f03713b9",
|
||||
"generator": "tools/web/generate-compositor-fixture.py",
|
||||
"generatorSha256": "e6ca40c989fb4bf1a26a34aa55861c2979142817a4cbe4e2628f4d0318775dc0",
|
||||
"blenderVersion": "5.2.0"
|
||||
},
|
||||
"scene": "CompositorScene",
|
||||
"unsupportedNode": {
|
||||
"name": "PreservedUnsupportedGlare",
|
||||
"type": "UNSUPPORTED",
|
||||
"blenderType": "CompositorNodeGlare"
|
||||
},
|
||||
"expectedErrorCode": "COMPOSITOR_NODE_UNSUPPORTED",
|
||||
"expectedRevisionDelta": 0
|
||||
}
|
||||
41
tests/golden/M11-09/sequencer-codec-probe.json
Normal file
41
tests/golden/M11-09/sequencer-codec-probe.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-09",
|
||||
"assets": [
|
||||
{
|
||||
"stripType": "IMAGE",
|
||||
"mimeType": "image/png",
|
||||
"path": "tests/files/web/media/sequencer-frame.png",
|
||||
"sourcePath": "media/not-an-image.movie",
|
||||
"byteLength": 261,
|
||||
"sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed",
|
||||
"backend": "IMAGE_BITMAP",
|
||||
"decoded": { "width": 8, "height": 8 }
|
||||
},
|
||||
{
|
||||
"stripType": "SOUND",
|
||||
"mimeType": "audio/wav",
|
||||
"path": "tests/files/web/media/sequencer-silence.wav",
|
||||
"sourcePath": "media/not-a-sound.png",
|
||||
"byteLength": 16044,
|
||||
"sha256": "56d4af65701c26df20bd4021eda95b6e830348ce3a746086079fe89285548dc9",
|
||||
"backend": "WEB_AUDIO",
|
||||
"decoded": { "sampleRate": 48000, "channels": 1, "durationFrames": 48000 }
|
||||
},
|
||||
{
|
||||
"stripType": "MOVIE",
|
||||
"mimeType": "video/mp4",
|
||||
"path": "tests/files/web/media/sequencer-probe.mp4",
|
||||
"sourcePath": "media/not-a-movie.wav",
|
||||
"byteLength": 1484,
|
||||
"sha256": "f35a5a2765aef9d0fed7146108c699d0beb03f34414cb27e6b7a7d4871187b65",
|
||||
"backend": "HTML_MEDIA",
|
||||
"decoded": { "width": 16, "height": 16, "durationMicros": 1000000 }
|
||||
}
|
||||
],
|
||||
"movieGenerator": {
|
||||
"path": "tools/web/generate-sequencer-codec-fixture.sh",
|
||||
"sha256": "bab7b0b56a16b96fd87302894c1c35d72ea316542d0ffffe9610458e47875b7a"
|
||||
},
|
||||
"blockedCode": "SEQUENCER_CODEC_UNSUPPORTED"
|
||||
}
|
||||
14
tests/golden/M11-10/sequencer-media-cache.json
Normal file
14
tests/golden/M11-10/sequencer-media-cache.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-10",
|
||||
"sourceSha256": "f35a5a2765aef9d0fed7146108c699d0beb03f34414cb27e6b7a7d4871187b65",
|
||||
"profile": {
|
||||
"kind": "MOVIE_RGBA8_FRAME",
|
||||
"width": 8,
|
||||
"height": 8,
|
||||
"colorSpace": "SRGB8",
|
||||
"alphaMode": "STRAIGHT"
|
||||
},
|
||||
"identitySha256": "68a3af14865841e81f69bd75f2605461de4819fe025d158ac3723fa0cdf31525",
|
||||
"proxyByteLength": 256
|
||||
}
|
||||
14
tests/golden/M11-11/sequencer-media-revision.json
Normal file
14
tests/golden/M11-11/sequencer-media-revision.json
Normal file
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-11",
|
||||
"sourceSha256": "f35a5a2765aef9d0fed7146108c699d0beb03f34414cb27e6b7a7d4871187b65",
|
||||
"decisions": [
|
||||
["SCRUB", 2, "PUBLISH", null],
|
||||
["SEEK", 1, "STALE", "REVISION_CONFLICT"],
|
||||
["DECODE", 3, "STALE", "REVISION_CONFLICT"],
|
||||
["DECODE", 5, "PUBLISH", null],
|
||||
["SEEK", 6, "STALE", "REVISION_CONFLICT"]
|
||||
],
|
||||
"published": ["SCRUB@2", "DECODE@5"],
|
||||
"cacheWrites": ["DECODE@5"]
|
||||
}
|
||||
27
tests/golden/M11-12/sequencer-final-export.json
Normal file
27
tests/golden/M11-12/sequencer-final-export.json
Normal file
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-12",
|
||||
"sourceBlendSha256": "5f5212487bb6b5df62b5ca915c75133f1ca45678614712ea4c7d902d823cd90c",
|
||||
"settingsSha256": "01e39d1fdd88c75aecad7bc48a2b756bbcd645929963528b42ceb180b51e5566",
|
||||
"requestSha256": "20d3a9e333804e014a993a9892a557842293fc44902229dd6eb65661f0691cfc",
|
||||
"withoutServer": {
|
||||
"schemaVersion": 1,
|
||||
"requestSha256": "20d3a9e333804e014a993a9892a557842293fc44902229dd6eb65661f0691cfc",
|
||||
"settingsSha256": "01e39d1fdd88c75aecad7bc48a2b756bbcd645929963528b42ceb180b51e5566",
|
||||
"route": "SERVER_EXPORT",
|
||||
"status": "BLOCKED",
|
||||
"code": "SEQUENCER_EXPORT_SERVER_UNAVAILABLE",
|
||||
"localEncoding": "BLOCKED",
|
||||
"browserVideoEncoderDetected": false
|
||||
},
|
||||
"withServer": {
|
||||
"schemaVersion": 1,
|
||||
"requestSha256": "20d3a9e333804e014a993a9892a557842293fc44902229dd6eb65661f0691cfc",
|
||||
"settingsSha256": "01e39d1fdd88c75aecad7bc48a2b756bbcd645929963528b42ceb180b51e5566",
|
||||
"route": "SERVER_EXPORT",
|
||||
"status": "SERVER_EXPORT_REQUIRED",
|
||||
"code": null,
|
||||
"localEncoding": "BLOCKED",
|
||||
"browserVideoEncoderDetected": false
|
||||
}
|
||||
}
|
||||
16
tests/golden/M11-13/sequencer-audio-recovery.json
Normal file
16
tests/golden/M11-13/sequencer-audio-recovery.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M11-13",
|
||||
"outputGain": 0.75,
|
||||
"lifecycle": [
|
||||
["SUSPENDED", "SILENT", false, 0.75, null],
|
||||
["SUSPENDED", "SILENT", true, 0, null],
|
||||
["RUNNING", "SILENT", true, 0, null],
|
||||
["RUNNING", "ENABLED", false, 0.75, null],
|
||||
["SUSPENDED", "SILENT", false, 0.75, null],
|
||||
["RUNNING", "ENABLED", false, 0.75, null],
|
||||
["CLOSED", "SILENT", false, 0, null]
|
||||
],
|
||||
"missingDevice": ["UNAVAILABLE", "BLOCKED", "SEQUENCER_AUDIO_DEVICE_UNAVAILABLE"],
|
||||
"resumeFailure": ["SUSPENDED", "SILENT", "SEQUENCER_AUDIO_RESUME_FAILED"]
|
||||
}
|
||||
BIN
tests/golden/M8-19/generated-smoke-x.rgba
Normal file
BIN
tests/golden/M8-19/generated-smoke-x.rgba
Normal file
Binary file not shown.
BIN
tests/golden/M8-19/generated-smoke-y.rgba
Normal file
BIN
tests/golden/M8-19/generated-smoke-y.rgba
Normal file
Binary file not shown.
BIN
tests/golden/M8-19/generated-smoke-z.rgba
Normal file
BIN
tests/golden/M8-19/generated-smoke-z.rgba
Normal file
Binary file not shown.
75
tests/golden/M8-19/manifest.json
Normal file
75
tests/golden/M8-19/manifest.json
Normal file
@@ -0,0 +1,75 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"taskId": "M8-19",
|
||||
"source": {
|
||||
"resourceId": "generated-smoke-vdb",
|
||||
"sha256": "586f7cdf4b3329fcb7ef00fc57b12a268baafdbaac0a2b3d1bfa142a22ccda33",
|
||||
"grid": "density",
|
||||
"activeVoxelCount": 13997,
|
||||
"indexBounds": {
|
||||
"min": [
|
||||
-14,
|
||||
-14,
|
||||
-14
|
||||
],
|
||||
"max": [
|
||||
14,
|
||||
14,
|
||||
14
|
||||
]
|
||||
}
|
||||
},
|
||||
"native": {
|
||||
"openVDBVersion": "13.0.0",
|
||||
"generatorSourceSha256": "4d18306b9a498f8de73dccea9efd9f268fc24e07404c3e425c994b51434584eb"
|
||||
},
|
||||
"renderContract": {
|
||||
"shaderSemanticVersion": "volume-wgsl-v1",
|
||||
"width": 64,
|
||||
"height": 64,
|
||||
"format": "RGBA8_UNORM",
|
||||
"interpolation": "LINEAR",
|
||||
"densityScale": 1,
|
||||
"emissionScale": 0,
|
||||
"anisotropy": 0,
|
||||
"color": [
|
||||
0.72,
|
||||
0.78,
|
||||
0.86
|
||||
],
|
||||
"axes": [
|
||||
"X",
|
||||
"Y",
|
||||
"Z"
|
||||
],
|
||||
"thresholds": {
|
||||
"maxChannelError": 2,
|
||||
"meanAbsoluteError": 0.1,
|
||||
"rmsError": 0.5,
|
||||
"alphaCoverageDeltaRatio": 0.002
|
||||
}
|
||||
},
|
||||
"images": [
|
||||
{
|
||||
"axis": "X",
|
||||
"file": "generated-smoke-x.rgba",
|
||||
"byteLength": 16384,
|
||||
"sha256": "87d08a77a644f37ed59609ebdd18555ab1924f9a3b30d8d084db35e7776cfb9c",
|
||||
"alphaPixels": 3552
|
||||
},
|
||||
{
|
||||
"axis": "Y",
|
||||
"file": "generated-smoke-y.rgba",
|
||||
"byteLength": 16384,
|
||||
"sha256": "87d08a77a644f37ed59609ebdd18555ab1924f9a3b30d8d084db35e7776cfb9c",
|
||||
"alphaPixels": 3552
|
||||
},
|
||||
{
|
||||
"axis": "Z",
|
||||
"file": "generated-smoke-z.rgba",
|
||||
"byteLength": 16384,
|
||||
"sha256": "87d08a77a644f37ed59609ebdd18555ab1924f9a3b30d8d084db35e7776cfb9c",
|
||||
"alphaPixels": 3552
|
||||
}
|
||||
]
|
||||
}
|
||||
29
tests/golden/M9-05/curve-toggle-cyclic.json
Normal file
29
tests/golden/M9-05/curve-toggle-cyclic.json
Normal file
@@ -0,0 +1,29 @@
|
||||
{
|
||||
"afterCyclicU": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"beforeCyclicU": [
|
||||
false,
|
||||
false
|
||||
],
|
||||
"blenderVersion": "5.2.0",
|
||||
"curveName": "WebCurveData",
|
||||
"fixture": "tests/files/web/nonmesh_scene.blend",
|
||||
"fixtureSha256": "ae8ef85d606aa120ce6b7611fc03407fd80bb60311e94a6ef73f6384d0b6c8b4",
|
||||
"operator": "TOGGLE_CYCLIC",
|
||||
"reopenedCyclicU": [
|
||||
true,
|
||||
false
|
||||
],
|
||||
"schemaVersion": 1,
|
||||
"splineIndex": 0,
|
||||
"splinePointCounts": [
|
||||
4,
|
||||
3
|
||||
],
|
||||
"splineTypes": [
|
||||
"POLY",
|
||||
"BEZIER"
|
||||
]
|
||||
}
|
||||
59
tests/golden/M9-08/grease-pencil-reorder.json
Normal file
59
tests/golden/M9-08/grease-pencil-reorder.json
Normal file
@@ -0,0 +1,59 @@
|
||||
{
|
||||
"afterReorder": {
|
||||
"framesByLayer": {
|
||||
"Lines": [
|
||||
1
|
||||
],
|
||||
"Web Drafts": [
|
||||
12
|
||||
]
|
||||
},
|
||||
"layerOrder": [
|
||||
"Web Drafts",
|
||||
"Lines"
|
||||
]
|
||||
},
|
||||
"beforeReorder": {
|
||||
"framesByLayer": {
|
||||
"Lines": [
|
||||
1
|
||||
],
|
||||
"Web Drafts": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"layerOrder": [
|
||||
"Lines",
|
||||
"Web Drafts"
|
||||
]
|
||||
},
|
||||
"blenderVersion": "5.2.0",
|
||||
"fixture": "tests/files/web/modifier_grease_pencil_scene.blend",
|
||||
"fixtureSha256": "3a8525077807f9178dac3a5ba84b524e5f8e80874b15c54e2a6658ecb80315a3",
|
||||
"greasePencilName": "GreasePencilData",
|
||||
"reopened": {
|
||||
"framesByLayer": {
|
||||
"Lines": [
|
||||
1
|
||||
],
|
||||
"Web Drafts": [
|
||||
12
|
||||
]
|
||||
},
|
||||
"layerOrder": [
|
||||
"Web Drafts",
|
||||
"Lines"
|
||||
]
|
||||
},
|
||||
"schemaVersion": 1,
|
||||
"source": {
|
||||
"framesByLayer": {
|
||||
"Lines": [
|
||||
1
|
||||
]
|
||||
},
|
||||
"layerOrder": [
|
||||
"Lines"
|
||||
]
|
||||
}
|
||||
}
|
||||
193
tests/golden/M9-12/weight-paint.json
Normal file
193
tests/golden/M9-12/weight-paint.json
Normal file
@@ -0,0 +1,193 @@
|
||||
{
|
||||
"blenderVersion": "5.2.0 LTS",
|
||||
"fixture": "rigged_shape_scene.blend",
|
||||
"mesh": "RiggedShapeMesh",
|
||||
"object": "RiggedShapeObject",
|
||||
"operations": [
|
||||
{
|
||||
"name": "normalize",
|
||||
"vertex": 2
|
||||
},
|
||||
{
|
||||
"limit": 2,
|
||||
"name": "limit-normalize",
|
||||
"vertex": 3
|
||||
},
|
||||
{
|
||||
"axis": 0,
|
||||
"name": "mirror",
|
||||
"tolerance": 0.0001,
|
||||
"vertices": [
|
||||
0,
|
||||
1
|
||||
]
|
||||
}
|
||||
],
|
||||
"schemaVersion": 1,
|
||||
"steps": [
|
||||
{
|
||||
"groups": [
|
||||
"Root",
|
||||
"Tip",
|
||||
"WebPaintGroup"
|
||||
],
|
||||
"name": "initial",
|
||||
"vertices": [
|
||||
{
|
||||
"index": 0,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.75
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"weights": {
|
||||
"Root": 0.25,
|
||||
"Tip": 1.0
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"weights": {
|
||||
"Root": 0.25,
|
||||
"Tip": 1.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groups": [
|
||||
"Root",
|
||||
"Tip",
|
||||
"WebPaintGroup"
|
||||
],
|
||||
"name": "normalize",
|
||||
"vertices": [
|
||||
{
|
||||
"index": 0,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.75
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"weights": {
|
||||
"Root": 0.142857149,
|
||||
"Tip": 0.571428597,
|
||||
"WebPaintGroup": 0.285714298
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"weights": {
|
||||
"Root": 0.25,
|
||||
"Tip": 1.0
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groups": [
|
||||
"Root",
|
||||
"Tip",
|
||||
"WebPaintGroup"
|
||||
],
|
||||
"name": "limit-normalize",
|
||||
"vertices": [
|
||||
{
|
||||
"index": 0,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.75
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.25
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"weights": {
|
||||
"Root": 0.142857149,
|
||||
"Tip": 0.571428597,
|
||||
"WebPaintGroup": 0.285714298
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"weights": {
|
||||
"Tip": 0.714285731,
|
||||
"WebPaintGroup": 0.285714298
|
||||
}
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
"groups": [
|
||||
"Root",
|
||||
"Tip",
|
||||
"WebPaintGroup"
|
||||
],
|
||||
"name": "mirror",
|
||||
"vertices": [
|
||||
{
|
||||
"index": 0,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.899999976
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 1,
|
||||
"weights": {
|
||||
"Root": 1.0,
|
||||
"Tip": 0.25,
|
||||
"WebPaintGroup": 0.899999976
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 2,
|
||||
"weights": {
|
||||
"Root": 0.142857149,
|
||||
"Tip": 0.571428597,
|
||||
"WebPaintGroup": 0.285714298
|
||||
}
|
||||
},
|
||||
{
|
||||
"index": 3,
|
||||
"weights": {
|
||||
"Tip": 0.714285731,
|
||||
"WebPaintGroup": 0.285714298
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
"tolerance": 1e-06
|
||||
}
|
||||
31
tests/golden/M9-14/manifest.json
Normal file
31
tests/golden/M9-14/manifest.json
Normal file
@@ -0,0 +1,31 @@
|
||||
{
|
||||
"schemaVersion": 1,
|
||||
"task": "M9-14",
|
||||
"domains": ["CURVE", "GREASE_PENCIL", "PAINT"],
|
||||
"fixtures": {
|
||||
"CURVE": {
|
||||
"path": "tests/files/web/nonmesh_scene.blend",
|
||||
"sha256": "ae8ef85d606aa120ce6b7611fc03407fd80bb60311e94a6ef73f6384d0b6c8b4"
|
||||
},
|
||||
"GREASE_PENCIL": {
|
||||
"path": "tests/files/web/modifier_grease_pencil_scene.blend",
|
||||
"sha256": "3a8525077807f9178dac3a5ba84b524e5f8e80874b15c54e2a6658ecb80315a3"
|
||||
},
|
||||
"PAINT": {
|
||||
"path": "tests/files/web/attribute_scene.blend",
|
||||
"sha256": "12fa75bb79f8c38e660d3d2a8fc9cc16dd3df4fc9208b0e2ad94fb1f8aa68f71"
|
||||
}
|
||||
},
|
||||
"requiredStages": ["WORKER_RESTART", "OOM", "GPU_RELEASE", "SMALL_SCENE"],
|
||||
"invariants": {
|
||||
"workerRestart": "identity hash preserved; new engine worker has one live Main handle",
|
||||
"oom": "GPU_GEOMETRY_UPLOAD maps to GPU_GEOMETRY_BUDGET_EXCEEDED and leaves zero temporary resources",
|
||||
"gpuRelease": "one WebGL2 resource release and one reinitialization render non-zero pixels",
|
||||
"smallScene": "domain identity, data IDs and visible pixels are restored"
|
||||
},
|
||||
"verification": {
|
||||
"unit": "web/tests/unit/editing-domain-recovery.test.mjs",
|
||||
"browser": "web/tests/e2e/editing-domain-recovery.spec.ts",
|
||||
"command": "npm --prefix web run test:editing-domain-recovery"
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ target_include_directories(vdb_toolchain INTERFACE "${OPENVDB_INCLUDE_DIR}" "${T
|
||||
target_compile_definitions(vdb_toolchain INTERFACE NANOVDB_USE_OPENVDB)
|
||||
target_link_libraries(vdb_toolchain INTERFACE "${OPENVDB_LIBRARY}" "${TBB_LIBRARY}")
|
||||
|
||||
foreach(target vdb_fixture_generator vdb_to_nanovdb)
|
||||
foreach(target vdb_fixture_generator vdb_to_nanovdb vdb_volume_golden)
|
||||
add_executable(${target} "${target}.cc")
|
||||
target_link_libraries(${target} PRIVATE vdb_toolchain)
|
||||
set_target_properties(${target} PROPERTIES
|
||||
|
||||
86
tools/vdb/generate-volume-golden.mjs
Normal file
86
tools/vdb/generate-volume-golden.mjs
Normal file
@@ -0,0 +1,86 @@
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { spawnSync } from "node:child_process";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const resourceRoot = path.resolve(process.env.VDB_RESOURCE_ROOT ?? path.join(os.homedir(), "resource-library/blender-web-vdb"));
|
||||
const source = path.join(resourceRoot, "generated", "generated-smoke.vdb");
|
||||
const generator = path.join(root, "build_vdb_tools", "vdb_volume_golden");
|
||||
const committedRoot = path.join(root, "tests", "golden", "M8-19");
|
||||
const check = process.argv.includes("--check");
|
||||
const outputRoot = check ? fs.mkdtempSync(path.join(os.tmpdir(), "vdb-volume-golden-")) : committedRoot;
|
||||
const outputPrefix = path.join(outputRoot, "generated-smoke");
|
||||
const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex");
|
||||
|
||||
if (!fs.existsSync(generator)) throw new Error(`VDB_VOLUME_GOLDEN_TOOL_MISSING: ${generator}`);
|
||||
if (!fs.existsSync(source)) throw new Error(`VDB_VOLUME_GOLDEN_SOURCE_MISSING: ${source}`);
|
||||
fs.mkdirSync(outputRoot, { recursive: true });
|
||||
|
||||
try {
|
||||
const result = spawnSync(generator, [source, outputPrefix], { encoding: "utf8" });
|
||||
if (result.status !== 0) throw new Error(result.stderr || `vdb_volume_golden exited ${result.status}`);
|
||||
const native = JSON.parse(result.stdout.trim());
|
||||
const axes = ["X", "Y", "Z"];
|
||||
const images = axes.map((axis) => {
|
||||
const fileName = `generated-smoke-${axis.toLowerCase()}.rgba`;
|
||||
const file = path.join(outputRoot, fileName);
|
||||
const bytes = fs.readFileSync(file);
|
||||
let alphaPixels = 0;
|
||||
for (let index = 3; index < bytes.byteLength; index += 4) if (bytes[index] > 0) alphaPixels++;
|
||||
return { axis, file: fileName, byteLength: bytes.byteLength, sha256: sha256(file), alphaPixels };
|
||||
});
|
||||
const manifest = {
|
||||
schemaVersion: 1,
|
||||
taskId: "M8-19",
|
||||
source: {
|
||||
resourceId: "generated-smoke-vdb",
|
||||
sha256: sha256(source),
|
||||
grid: native.grid,
|
||||
activeVoxelCount: native.activeVoxelCount,
|
||||
indexBounds: native.indexBounds,
|
||||
},
|
||||
native: {
|
||||
openVDBVersion: native.openVDBVersion,
|
||||
generatorSourceSha256: sha256(path.join(root, "tools", "vdb", "vdb_volume_golden.cc")),
|
||||
},
|
||||
renderContract: {
|
||||
shaderSemanticVersion: "volume-wgsl-v1",
|
||||
width: native.width,
|
||||
height: native.height,
|
||||
format: "RGBA8_UNORM",
|
||||
interpolation: "LINEAR",
|
||||
densityScale: 1,
|
||||
emissionScale: 0,
|
||||
anisotropy: 0,
|
||||
color: [0.72, 0.78, 0.86],
|
||||
axes,
|
||||
thresholds: {
|
||||
maxChannelError: 2,
|
||||
meanAbsoluteError: 0.1,
|
||||
rmsError: 0.5,
|
||||
alphaCoverageDeltaRatio: 0.002,
|
||||
},
|
||||
},
|
||||
images,
|
||||
};
|
||||
const manifestText = `${JSON.stringify(manifest, null, 2)}\n`;
|
||||
const manifestFile = path.join(outputRoot, "manifest.json");
|
||||
fs.writeFileSync(manifestFile, manifestText);
|
||||
|
||||
if (check) {
|
||||
for (const image of images) {
|
||||
const expected = fs.readFileSync(path.join(committedRoot, image.file));
|
||||
const actual = fs.readFileSync(path.join(outputRoot, image.file));
|
||||
if (!expected.equals(actual)) throw new Error(`VDB_VOLUME_GOLDEN_MISMATCH: ${image.file}`);
|
||||
}
|
||||
const expectedManifest = fs.readFileSync(path.join(committedRoot, "manifest.json"), "utf8");
|
||||
if (expectedManifest !== manifestText) throw new Error("VDB_VOLUME_GOLDEN_MISMATCH: manifest.json");
|
||||
}
|
||||
process.stdout.write(`vdb-volume-golden-${check ? "check" : "generated"} axes=3 bytes=${images.reduce((total, image) => total + image.byteLength, 0)} sha256=${images.map((image) => image.sha256).join(",")}\n`);
|
||||
}
|
||||
finally {
|
||||
if (check) fs.rmSync(outputRoot, { recursive: true, force: true });
|
||||
}
|
||||
179
tools/vdb/vdb_volume_golden.cc
Normal file
179
tools/vdb/vdb_volume_golden.cc
Normal file
@@ -0,0 +1,179 @@
|
||||
#include <openvdb/openvdb.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
namespace fs = std::filesystem;
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kImageSize = 64;
|
||||
constexpr std::array<float, 3> kColor = {0.72f, 0.78f, 0.86f};
|
||||
|
||||
float sample_linear(const openvdb::FloatGrid::ConstAccessor &accessor,
|
||||
const std::array<float, 3> &position)
|
||||
{
|
||||
const std::array<int, 3> base = {
|
||||
static_cast<int>(std::floor(position[0])),
|
||||
static_cast<int>(std::floor(position[1])),
|
||||
static_cast<int>(std::floor(position[2])),
|
||||
};
|
||||
const std::array<float, 3> fraction = {
|
||||
position[0] - static_cast<float>(base[0]),
|
||||
position[1] - static_cast<float>(base[1]),
|
||||
position[2] - static_cast<float>(base[2]),
|
||||
};
|
||||
float value = 0.0f;
|
||||
for (int x = 0; x < 2; ++x) {
|
||||
for (int y = 0; y < 2; ++y) {
|
||||
for (int z = 0; z < 2; ++z) {
|
||||
const float weight = (x ? fraction[0] : 1.0f - fraction[0]) *
|
||||
(y ? fraction[1] : 1.0f - fraction[1]) *
|
||||
(z ? fraction[2] : 1.0f - fraction[2]);
|
||||
value += accessor.getValue(openvdb::Coord(base[0] + x, base[1] + y, base[2] + z)) *
|
||||
weight;
|
||||
}
|
||||
}
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
uint8_t pack_unorm(float value)
|
||||
{
|
||||
return static_cast<uint8_t>(std::lround(std::clamp(value, 0.0f, 1.0f) * 255.0f));
|
||||
}
|
||||
|
||||
std::vector<uint8_t> render_axis(const openvdb::FloatGrid &density,
|
||||
const openvdb::CoordBBox &bounds,
|
||||
int view_axis)
|
||||
{
|
||||
const auto accessor = density.getConstAccessor();
|
||||
const openvdb::Coord minimum = bounds.min();
|
||||
const openvdb::Coord maximum = bounds.max();
|
||||
const std::array<int, 3> plane_a = view_axis == 0 ? std::array<int, 3>{1, 2, 0} :
|
||||
view_axis == 1 ? std::array<int, 3>{0, 2, 1} :
|
||||
std::array<int, 3>{0, 1, 2};
|
||||
const int ray_axis = plane_a[2];
|
||||
const int ray_min = minimum[ray_axis];
|
||||
const int ray_max = maximum[ray_axis];
|
||||
const int ray_count = std::max(1, ray_max - ray_min + 1);
|
||||
const int stride = std::max(1, (ray_count + 255) / 256);
|
||||
const float voxel_size = std::max(0.01f, static_cast<float>(density.voxelSize()[ray_axis]));
|
||||
const float phase = 1.0f / 12.5663706f;
|
||||
const float source_scale = 0.5f + 8.0f * phase;
|
||||
std::vector<uint8_t> pixels(kImageSize * kImageSize * 4, 0);
|
||||
|
||||
for (int y = 0; y < kImageSize; ++y) {
|
||||
for (int x = 0; x < kImageSize; ++x) {
|
||||
const std::array<int, 2> plane_min = {minimum[plane_a[0]], minimum[plane_a[1]]};
|
||||
const std::array<int, 2> plane_max = {maximum[plane_a[0]], maximum[plane_a[1]]};
|
||||
const std::array<float, 2> extent = {
|
||||
static_cast<float>(plane_max[0] - plane_min[0] + 1),
|
||||
static_cast<float>(plane_max[1] - plane_min[1] + 1),
|
||||
};
|
||||
const std::array<float, 2> plane_position = {
|
||||
static_cast<float>(plane_min[0]) +
|
||||
((static_cast<float>(x) + 0.5f) / static_cast<float>(kImageSize)) * extent[0] - 0.5f,
|
||||
static_cast<float>(plane_min[1]) +
|
||||
((static_cast<float>(y) + 0.5f) / static_cast<float>(kImageSize)) * extent[1] - 0.5f,
|
||||
};
|
||||
|
||||
float transmittance = 1.0f;
|
||||
std::array<float, 3> radiance = {0.0f, 0.0f, 0.0f};
|
||||
for (int ray = ray_min; ray <= ray_max; ray += stride) {
|
||||
std::array<float, 3> position{};
|
||||
position[plane_a[0]] = plane_position[0];
|
||||
position[plane_a[1]] = plane_position[1];
|
||||
position[ray_axis] = static_cast<float>(ray) + 0.5f;
|
||||
const float sampled_density = std::max(0.0f, sample_linear(accessor, position));
|
||||
const float alpha = 1.0f - std::exp(-sampled_density * voxel_size * static_cast<float>(stride));
|
||||
for (int channel = 0; channel < 3; ++channel) {
|
||||
radiance[channel] += transmittance * alpha * kColor[channel] * source_scale;
|
||||
}
|
||||
transmittance *= 1.0f - alpha;
|
||||
if (transmittance < 0.005f) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const size_t offset = static_cast<size_t>(y * kImageSize + x) * 4;
|
||||
pixels[offset] = pack_unorm(radiance[0]);
|
||||
pixels[offset + 1] = pack_unorm(radiance[1]);
|
||||
pixels[offset + 2] = pack_unorm(radiance[2]);
|
||||
pixels[offset + 3] = pack_unorm(1.0f - transmittance);
|
||||
}
|
||||
}
|
||||
return pixels;
|
||||
}
|
||||
|
||||
void write_bytes(const fs::path &path, const std::vector<uint8_t> &bytes)
|
||||
{
|
||||
std::ofstream output(path, std::ios::binary | std::ios::trunc);
|
||||
if (!output) {
|
||||
throw std::runtime_error("failed to create golden image: " + path.string());
|
||||
}
|
||||
output.write(reinterpret_cast<const char *>(bytes.data()), static_cast<std::streamsize>(bytes.size()));
|
||||
if (!output) {
|
||||
throw std::runtime_error("failed to write golden image: " + path.string());
|
||||
}
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
int main(int argc, char **argv)
|
||||
{
|
||||
if (argc != 3) {
|
||||
std::cerr << "usage: vdb_volume_golden INPUT.vdb OUTPUT_PREFIX\n";
|
||||
return 2;
|
||||
}
|
||||
|
||||
try {
|
||||
openvdb::initialize();
|
||||
const fs::path input = fs::absolute(argv[1]);
|
||||
const fs::path output_prefix = fs::absolute(argv[2]);
|
||||
if (input.extension() != ".vdb") {
|
||||
throw std::runtime_error("input extension must be .vdb");
|
||||
}
|
||||
fs::create_directories(output_prefix.parent_path());
|
||||
|
||||
openvdb::io::File file(input.string());
|
||||
file.open(false);
|
||||
const openvdb::GridBase::Ptr base = file.readGrid("density");
|
||||
file.close();
|
||||
const openvdb::FloatGrid::Ptr density = openvdb::gridPtrCast<openvdb::FloatGrid>(base);
|
||||
if (!density || density->getGridClass() != openvdb::GRID_FOG_VOLUME) {
|
||||
throw std::runtime_error("density must be an OpenVDB FloatGrid fog volume");
|
||||
}
|
||||
const openvdb::CoordBBox bounds = density->evalActiveVoxelBoundingBox();
|
||||
if (bounds.empty()) {
|
||||
throw std::runtime_error("density grid has no active voxels");
|
||||
}
|
||||
|
||||
const std::array<const char *, 3> names = {"x", "y", "z"};
|
||||
for (int axis = 0; axis < 3; ++axis) {
|
||||
write_bytes(output_prefix.string() + "-" + names[axis] + ".rgba",
|
||||
render_axis(*density, bounds, axis));
|
||||
}
|
||||
std::cout << "{\"schemaVersion\":1,\"openVDBVersion\":\""
|
||||
<< openvdb::getLibraryVersionString() << "\",\"grid\":\"density\",\"width\":"
|
||||
<< kImageSize << ",\"height\":" << kImageSize << ",\"activeVoxelCount\":"
|
||||
<< density->activeVoxelCount() << ",\"indexBounds\":{\"min\":["
|
||||
<< bounds.min().x() << ',' << bounds.min().y() << ',' << bounds.min().z()
|
||||
<< "],\"max\":[" << bounds.max().x() << ',' << bounds.max().y() << ','
|
||||
<< bounds.max().z() << "]}}\n";
|
||||
openvdb::uninitialize();
|
||||
return 0;
|
||||
}
|
||||
catch (const std::exception &error) {
|
||||
std::cerr << "VDB_VOLUME_GOLDEN_FAILED: " << error.what() << '\n';
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
81
tools/web/check-compositor-node-golden.mjs
Normal file
81
tools/web/check-compositor-node-golden.mjs
Normal file
@@ -0,0 +1,81 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
import { createRequire } from "node:module";
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { fileURLToPath, pathToFileURL } from "node:url";
|
||||
import factory from "../../web/app/src/vendor/blender/web_engine.js";
|
||||
|
||||
const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-07/compositor-node-golden.json"), "utf8"));
|
||||
const fixturePath = path.join(root, golden.fixture.path);
|
||||
const generatorPath = path.join(root, golden.fixture.generator);
|
||||
const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender");
|
||||
const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
||||
assert.equal(sha256(fs.readFileSync(fixturePath)), golden.fixture.sha256);
|
||||
assert.equal(sha256(fs.readFileSync(generatorPath)), golden.fixture.generatorSha256);
|
||||
assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), new RegExp(`^Blender ${golden.fixture.blenderVersion.replaceAll(".", "\\.")}\\b`, "m"));
|
||||
|
||||
const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m11-compositor-golden-"));
|
||||
const requireFromWeb = createRequire(path.join(root, "web/package.json"));
|
||||
const ts = requireFromWeb("typescript");
|
||||
for (const [sourceName, outputName] of [["capability-gates.ts", "capability-gates.mjs"], ["compositor.ts", "compositor.mjs"]]) {
|
||||
const sourcePath = path.join(root, "web/protocol", sourceName);
|
||||
const transpiled = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), {
|
||||
compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 },
|
||||
fileName: sourcePath,
|
||||
reportDiagnostics: true,
|
||||
});
|
||||
assert.deepEqual(transpiled.diagnostics, []);
|
||||
const output = sourceName === "compositor.ts" ? transpiled.outputText.replaceAll('from "./capability-gates"', 'from "./capability-gates.mjs"') : transpiled.outputText;
|
||||
fs.writeFileSync(path.join(temporary, outputName), output);
|
||||
}
|
||||
const compositor = await import(pathToFileURL(path.join(temporary, "compositor.mjs")));
|
||||
const wasmBinary = fs.readFileSync(path.join(root, "web/app/src/vendor/blender/web_engine.wasm"));
|
||||
const fixture = fs.readFileSync(fixturePath);
|
||||
|
||||
function open(engine, handle, bytes) {
|
||||
const pointer = engine._malloc(bytes.byteLength);
|
||||
try {
|
||||
engine.HEAPU8.set(bytes, pointer);
|
||||
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
}
|
||||
finally { engine._free(pointer); }
|
||||
}
|
||||
|
||||
function snapshot(engine, handle) {
|
||||
const dataOut = engine._malloc(4);
|
||||
const lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(engine._web_engine_get_scene_snapshot(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2];
|
||||
const length = engine.HEAPU32[lengthOut >>> 2];
|
||||
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
|
||||
}
|
||||
finally { engine._free(dataOut); engine._free(lengthOut); }
|
||||
}
|
||||
|
||||
try {
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
try {
|
||||
open(engine, handle, fixture);
|
||||
const value = snapshot(engine, handle);
|
||||
for (const candidate of golden.cases) {
|
||||
const scene = value.scenes.find((item) => item.name === candidate.scene);
|
||||
assert.equal(scene?.compositorStatus, "AVAILABLE", `${candidate.scene} has no Main compositor graph`);
|
||||
assert.deepEqual(scene.compositorGraph.nodes.map((node) => node.type), candidate.nodeTypes);
|
||||
assert.deepEqual(compositor.compileCompositorWebGPUPlan(scene.compositorGraph).instructions.map((instruction) => instruction.type), candidate.nodeTypes);
|
||||
const cpu = compositor.executeCompositorGraph(scene.compositorGraph, new Map(), { width: golden.width, height: golden.height });
|
||||
assert.deepEqual(Array.from(cpu.composite.data.slice(0, 4)), candidate.pixel);
|
||||
assert.equal(sha256(new Uint8Array(cpu.composite.data.buffer)), candidate.float32Sha256);
|
||||
}
|
||||
}
|
||||
finally { engine._web_engine_destroy(handle); }
|
||||
process.stdout.write(`compositor-node-golden-ok fixture=${golden.fixture.sha256} scenes=${golden.cases.length} allowlist=${golden.allowlist.join(",")} cpu-hashes=4\n`);
|
||||
}
|
||||
finally {
|
||||
fs.rmSync(temporary, { recursive: true, force: true });
|
||||
}
|
||||
32
tools/web/check-editing-soak-report.mjs
Normal file
32
tools/web/check-editing-soak-report.mjs
Normal file
@@ -0,0 +1,32 @@
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const repoRoot = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../..");
|
||||
const reportPath = path.join(repoRoot, "release/soak-reports/editing.json");
|
||||
const report = JSON.parse(fs.readFileSync(reportPath, "utf8"));
|
||||
|
||||
assert.equal(report.schemaVersion, 1);
|
||||
assert.equal(report.status, "READY");
|
||||
assert.equal(report.profile, "FORMAL");
|
||||
assert.equal(report.requiredDurationMs, 1_800_000);
|
||||
assert.ok(report.configuredDurationMs >= report.requiredDurationMs);
|
||||
assert.ok(report.actualDurationMs >= report.requiredDurationMs);
|
||||
assert.ok(report.cycles > 100);
|
||||
assert.equal(report.autosaves, report.cycles);
|
||||
assert.ok(report.reopens >= 20);
|
||||
assert.ok(report.finalRevision >= report.initialRevision + report.cycles * 2);
|
||||
assert.match(report.finalSha256, /^[a-f0-9]{64}$/);
|
||||
assert.ok(report.finalBytes > 0);
|
||||
assert.ok(report.finalSnapshotCount <= report.limits.maxSnapshots);
|
||||
assert.equal(report.downloadCount, 1);
|
||||
assert.deepEqual(report.pageErrors, []);
|
||||
assert.equal(report.failure, null);
|
||||
assert.ok(report.observed.heapGrowthBytes <= report.limits.maxHeapGrowthBytes);
|
||||
assert.ok(report.observed.storageGrowthBytes <= report.limits.maxStorageGrowthBytes);
|
||||
assert.ok(report.resourceSamples.length >= report.reopens + 1);
|
||||
assert.equal(report.resourceSamples[0].label, "baseline");
|
||||
assert.equal(report.resourceSamples.at(-1).label, "final");
|
||||
|
||||
process.stdout.write(`editing-soak-report-ok durationMs=${report.actualDurationMs} cycles=${report.cycles} autosaves=${report.autosaves} reopens=${report.reopens} revision=${report.finalRevision} sha256=${report.finalSha256} heapGrowth=${report.observed.heapGrowthBytes} storageGrowth=${report.observed.storageGrowthBytes}\n`);
|
||||
140
tools/web/check-geometry-node-evaluator-golden.mjs
Normal file
140
tools/web/check-geometry-node-evaluator-golden.mjs
Normal file
@@ -0,0 +1,140 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import factory from "../../web/app/src/vendor/blender/web_engine.js";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
const golden = JSON.parse(fs.readFileSync(new URL("tests/golden/M10-03/geometry-node-evaluator.json", root), "utf8"));
|
||||
const fixture = fs.readFileSync(new URL(golden.fixture, root));
|
||||
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
|
||||
|
||||
assert.equal(golden.schemaVersion, 1);
|
||||
assert.equal(golden.blenderVersion, "5.2.0 LTS");
|
||||
assert.equal(crypto.createHash("sha256").update(fixture).digest("hex"), golden.fixtureSha256);
|
||||
assert.equal(golden.allowlist.length, 16);
|
||||
assert.deepEqual(Object.keys(golden.nodeCoverage).sort(), [...golden.allowlist].sort());
|
||||
for (const nodeType of golden.allowlist) {
|
||||
assert.ok(golden.nodeCoverage[nodeType].length > 0, `${nodeType} has no desktop fixture coverage`);
|
||||
}
|
||||
|
||||
function open(engine, handle, bytes) {
|
||||
const pointer = engine._malloc(bytes.byteLength);
|
||||
try {
|
||||
engine.HEAPU8.set(bytes, pointer);
|
||||
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
}
|
||||
finally {
|
||||
engine._free(pointer);
|
||||
}
|
||||
}
|
||||
|
||||
function output(engine, handle, fn) {
|
||||
const dataOut = engine._malloc(4);
|
||||
const lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(fn(handle, dataOut, lengthOut), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2];
|
||||
const length = engine.HEAPU32[lengthOut >>> 2];
|
||||
assert.ok(pointer > 0 && length > 0);
|
||||
return JSON.parse(new TextDecoder().decode(engine.HEAPU8.slice(pointer, pointer + length)));
|
||||
}
|
||||
finally {
|
||||
engine._free(dataOut);
|
||||
engine._free(lengthOut);
|
||||
}
|
||||
}
|
||||
|
||||
function errors(expected, actual) {
|
||||
assert.equal(actual.length, expected.length);
|
||||
return actual.map((value, index) => value - expected[index]);
|
||||
}
|
||||
|
||||
function compareFloatArray(expected, actual, maximum, rmsMaximum, label) {
|
||||
const delta = errors(expected, actual);
|
||||
const maxError = Math.max(0, ...delta.map((value) => Math.abs(value)));
|
||||
const rmsError = delta.length === 0 ? 0 : Math.sqrt(
|
||||
delta.reduce((sum, value) => sum + value * value, 0) / delta.length,
|
||||
);
|
||||
assert.ok(maxError <= maximum, `${label} max error ${maxError} exceeds ${maximum}`);
|
||||
assert.ok(rmsError <= rmsMaximum, `${label} RMS error ${rmsError} exceeds ${rmsMaximum}`);
|
||||
return { maxError, rmsError };
|
||||
}
|
||||
|
||||
function bounds(positions) {
|
||||
const result = { min: [Infinity, Infinity, Infinity], max: [-Infinity, -Infinity, -Infinity] };
|
||||
for (let index = 0; index < positions.length; index += 3) {
|
||||
for (let axis = 0; axis < 3; axis++) {
|
||||
result.min[axis] = Math.min(result.min[axis], positions[index + axis]);
|
||||
result.max[axis] = Math.max(result.max[axis], positions[index + axis]);
|
||||
}
|
||||
}
|
||||
if (positions.length === 0) return { min: [0, 0, 0], max: [0, 0, 0] };
|
||||
return result;
|
||||
}
|
||||
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
assert.ok(handle > 0);
|
||||
try {
|
||||
open(engine, handle, fixture);
|
||||
const snapshot = output(engine, handle, engine._web_engine_get_scene_snapshot);
|
||||
const report = output(engine, handle, engine._web_engine_evaluate_depsgraph);
|
||||
assert.equal(report.engine, "BlenderDepsgraph");
|
||||
assert.equal(report.status, "EVALUATED");
|
||||
|
||||
const graphs = new Map((snapshot.geometryNodeGraphs ?? []).map((graph) => [graph.name, graph]));
|
||||
const meshes = new Map(report.meshes.map((mesh) => [mesh.objectId, mesh]));
|
||||
let maximumPositionError = 0;
|
||||
let maximumRmsError = 0;
|
||||
for (const expectedCase of golden.cases) {
|
||||
const graph = graphs.get(expectedCase.graph);
|
||||
assert.ok(graph, `${expectedCase.name} graph is missing from the Main snapshot`);
|
||||
assert.deepEqual(graph.nodes.map((node) => node.type).sort(), [...expectedCase.nodeTypes].sort(),
|
||||
`${expectedCase.name} node inventory`);
|
||||
|
||||
const actual = meshes.get(`object:${expectedCase.name}`);
|
||||
assert.ok(actual, `${expectedCase.name} evaluated mesh is missing`);
|
||||
assert.equal(actual.vertexCount, expectedCase.mesh.vertexCount, `${expectedCase.name} vertex count`);
|
||||
assert.equal(actual.triangleCount, expectedCase.mesh.triangleCount, `${expectedCase.name} triangle count`);
|
||||
assert.deepEqual(actual.indices, expectedCase.mesh.indices, `${expectedCase.name} topology`);
|
||||
assert.equal(actual.modifiers.length, 1, `${expectedCase.name} modifier count`);
|
||||
assert.equal(actual.modifiers[0].status, "EVALUATED", `${expectedCase.name} modifier status`);
|
||||
|
||||
const positionError = compareFloatArray(
|
||||
expectedCase.mesh.positions,
|
||||
actual.positions,
|
||||
golden.tolerance.maxPositionError,
|
||||
golden.tolerance.rmsPositionError,
|
||||
`${expectedCase.name} positions`,
|
||||
);
|
||||
maximumPositionError = Math.max(maximumPositionError, positionError.maxError);
|
||||
maximumRmsError = Math.max(maximumRmsError, positionError.rmsError);
|
||||
|
||||
const actualBounds = bounds(actual.positions);
|
||||
compareFloatArray(expectedCase.mesh.bounds.min, actualBounds.min,
|
||||
golden.tolerance.boundsError, golden.tolerance.boundsError, `${expectedCase.name} minimum bounds`);
|
||||
compareFloatArray(expectedCase.mesh.bounds.max, actualBounds.max,
|
||||
golden.tolerance.boundsError, golden.tolerance.boundsError, `${expectedCase.name} maximum bounds`);
|
||||
|
||||
assert.deepEqual(Object.keys(actual.attributes ?? {}).sort(),
|
||||
Object.keys(expectedCase.mesh.attributes).sort(), `${expectedCase.name} attribute names`);
|
||||
for (const [name, expectedAttribute] of Object.entries(expectedCase.mesh.attributes)) {
|
||||
const actualAttribute = actual.attributes[name];
|
||||
assert.equal(actualAttribute.domain, expectedAttribute.domain, `${expectedCase.name}/${name} domain`);
|
||||
assert.equal(actualAttribute.dataType, expectedAttribute.dataType, `${expectedCase.name}/${name} data type`);
|
||||
compareFloatArray(expectedAttribute.values, actualAttribute.values,
|
||||
golden.tolerance.maxAttributeError, golden.tolerance.maxAttributeError,
|
||||
`${expectedCase.name}/${name} values`);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.write(
|
||||
`geometry-node-evaluator-ok nodes=${golden.allowlist.length} cases=${golden.cases.length} ` +
|
||||
`max-position-error=${maximumPositionError} max-rms-error=${maximumRmsError} attributes=passed\n`,
|
||||
);
|
||||
}
|
||||
finally {
|
||||
engine._web_engine_destroy(handle);
|
||||
}
|
||||
111
tools/web/check-geometry-node-main-reader.mjs
Normal file
111
tools/web/check-geometry-node-main-reader.mjs
Normal file
@@ -0,0 +1,111 @@
|
||||
import assert from "node:assert/strict";
|
||||
import crypto from "node:crypto";
|
||||
import fs from "node:fs";
|
||||
import factory from "../../web/app/src/vendor/blender/web_engine.js";
|
||||
|
||||
const root = new URL("../../", import.meta.url);
|
||||
const wasmBinary = fs.readFileSync(new URL("web/app/src/vendor/blender/web_engine.wasm", root));
|
||||
const expected = JSON.parse(fs.readFileSync(new URL("tests/golden/M10-01/geometry-node-main-reader.json", root), "utf8"));
|
||||
const fixture = fs.readFileSync(new URL(expected.fixture, root));
|
||||
const desktopGoldenBytes = fs.readFileSync(new URL(expected.desktopGolden, root));
|
||||
const desktopGolden = JSON.parse(desktopGoldenBytes.toString("utf8"));
|
||||
|
||||
assert.equal(crypto.createHash("sha256").update(fixture).digest("hex"), expected.fixtureSha256);
|
||||
assert.equal(crypto.createHash("sha256").update(desktopGoldenBytes).digest("hex"), expected.desktopGoldenSha256);
|
||||
|
||||
function open(engine, handle, bytes) {
|
||||
const pointer = engine._malloc(bytes.byteLength);
|
||||
try {
|
||||
engine.HEAPU8.set(bytes, pointer);
|
||||
assert.equal(engine._web_engine_open_blend(handle, pointer, bytes.byteLength), 0,
|
||||
engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
}
|
||||
finally { engine._free(pointer); }
|
||||
}
|
||||
|
||||
function output(engine, handle, fn, owned = false) {
|
||||
const dataOut = engine._malloc(4);
|
||||
const lengthOut = engine._malloc(4);
|
||||
try {
|
||||
assert.equal(fn(handle, dataOut, lengthOut), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
const pointer = engine.HEAPU32[dataOut >>> 2];
|
||||
const length = engine.HEAPU32[lengthOut >>> 2];
|
||||
const bytes = engine.HEAPU8.slice(pointer, pointer + length);
|
||||
if (owned) engine._web_engine_free_buffer(pointer);
|
||||
return bytes;
|
||||
}
|
||||
finally { engine._free(dataOut); engine._free(lengthOut); }
|
||||
}
|
||||
|
||||
function snapshot(engine, handle) {
|
||||
return JSON.parse(new TextDecoder().decode(output(engine, handle, engine._web_engine_get_scene_snapshot)));
|
||||
}
|
||||
|
||||
function socketDefault(graph, key) {
|
||||
const separator = key.indexOf(":");
|
||||
const nodeType = key.slice(0, separator);
|
||||
const socketName = key.slice(separator + 1);
|
||||
const node = graph.nodes.find((candidate) => candidate.type === nodeType);
|
||||
assert.ok(node, `missing node ${nodeType} in ${graph.name}`);
|
||||
const socket = node.sockets.find((candidate) => candidate.name === socketName);
|
||||
assert.ok(socket, `missing socket ${nodeType}:${socketName}`);
|
||||
return socket.defaultValue;
|
||||
}
|
||||
|
||||
function assertGraphs(scene) {
|
||||
const graphs = scene.geometryNodeGraphs;
|
||||
assert.equal(graphs?.length, expected.graphs.length);
|
||||
const desktopByName = new Map(desktopGolden.nodeGroups.map((graph) => [graph.name, graph]));
|
||||
const expectedByName = new Map(expected.graphs.map((graph) => [graph.name, graph]));
|
||||
for (const graph of graphs) {
|
||||
const manifest = expectedByName.get(graph.name);
|
||||
const desktop = desktopByName.get(graph.name);
|
||||
assert.ok(manifest && desktop, `unexpected graph ${graph.name}`);
|
||||
assert.equal(graph.schemaVersion, 1);
|
||||
assert.equal(graph.id, `node-group:${graph.name}`);
|
||||
assert.match(graph.graphHash, /^[0-9a-f]{64}$/);
|
||||
assert.deepEqual(graph.nodes.map((node) => node.type), manifest.nodeTypes);
|
||||
const nodeKey = (node) => `${node.name}\u0000${node.type}`;
|
||||
assert.deepEqual(
|
||||
[...graph.nodes].map((node) => ({ name: node.name, type: node.type })).sort((a, b) => nodeKey(a).localeCompare(nodeKey(b))),
|
||||
[...desktop.nodes].sort((a, b) => nodeKey(a).localeCompare(nodeKey(b))),
|
||||
);
|
||||
assert.equal(graph.links.length, manifest.linkCount);
|
||||
assert.equal(graph.links.length, desktop.links);
|
||||
assert.deepEqual(graph.interfaceInputs.map((socket) => [socket.direction, socket.dataType]), [["INPUT", "GEOMETRY"]]);
|
||||
assert.deepEqual(graph.interfaceOutputs.map((socket) => [socket.direction, socket.dataType]), [["OUTPUT", "GEOMETRY"]]);
|
||||
assert.equal(new Set(graph.nodes.map((node) => node.id)).size, graph.nodes.length);
|
||||
const nodeById = new Map(graph.nodes.map((node) => [node.id, node]));
|
||||
for (const node of graph.nodes) {
|
||||
assert.match(node.id, /^geometry-node:[1-9][0-9]*$/);
|
||||
assert.equal(node.type.includes("Undefined["), false);
|
||||
assert.equal(node.sockets.some((socket) => socket.id.endsWith(":__extend__")), false);
|
||||
assert.equal(new Set(node.sockets.map((socket) => socket.id)).size, node.sockets.length);
|
||||
}
|
||||
for (const link of graph.links) {
|
||||
const fromNode = nodeById.get(link.fromNodeId);
|
||||
const toNode = nodeById.get(link.toNodeId);
|
||||
assert.ok(fromNode?.sockets.some((socket) => socket.id === link.fromSocketId && socket.direction === "OUTPUT"));
|
||||
assert.ok(toNode?.sockets.some((socket) => socket.id === link.toSocketId && socket.direction === "INPUT"));
|
||||
}
|
||||
for (const [key, value] of Object.entries(manifest.defaults)) {
|
||||
assert.deepEqual(socketDefault(graph, key), value);
|
||||
}
|
||||
}
|
||||
return graphs;
|
||||
}
|
||||
|
||||
const engine = await factory({ wasmBinary: wasmBinary.slice() });
|
||||
const handle = engine._web_engine_create();
|
||||
open(engine, handle, fixture);
|
||||
const initialGraphs = assertGraphs(snapshot(engine, handle));
|
||||
const saved = output(engine, handle, engine._web_engine_save_blend, true);
|
||||
engine._web_engine_destroy(handle);
|
||||
|
||||
const reopened = engine._web_engine_create();
|
||||
open(engine, reopened, saved);
|
||||
const reopenedGraphs = assertGraphs(snapshot(engine, reopened));
|
||||
assert.deepEqual(reopenedGraphs, initialGraphs);
|
||||
engine._web_engine_destroy(reopened);
|
||||
|
||||
process.stdout.write("geometry-node-main-reader-ok graphs=3 nodes=10 links=7 defaults=9 stable-id=passed graph-hash=passed desktop-golden=passed save-reopen=passed simulation-preserved=passed\n");
|
||||
@@ -64,11 +64,19 @@ function assertFixture(scene) {
|
||||
assert.equal(data?.pointCount, 4);
|
||||
const layer = data.layers[0];
|
||||
const stroke = layer.frames[0].drawing.strokes[0];
|
||||
assert.equal(data.activeLayerId, layer.id);
|
||||
assert.equal(layer.name, "Lines");
|
||||
assert.equal(layer.visible, true);
|
||||
assert.equal(layer.locked, false);
|
||||
assert.equal(stroke.cyclic, false);
|
||||
assert.equal(stroke.materialIndex, 0);
|
||||
assert.equal(stroke.id, "grease-pencil-stroke:GreasePencilData:0:0");
|
||||
assert.deepEqual(stroke.points.map((point) => point.id), [
|
||||
"grease-pencil-point:GreasePencilData:0:0:0",
|
||||
"grease-pencil-point:GreasePencilData:0:0:1",
|
||||
"grease-pencil-point:GreasePencilData:0:0:2",
|
||||
"grease-pencil-point:GreasePencilData:0:0:3",
|
||||
]);
|
||||
assert.deepEqual(stroke.points[0].position, [-1.5, 0, 0]);
|
||||
close(stroke.points[0].radius, 0.05, "radius");
|
||||
close(stroke.points[0].opacity, 0.9, "opacity");
|
||||
@@ -85,9 +93,13 @@ command(engine, handle, { type: "createGreasePencilLayer", dataId, name: "Web Dr
|
||||
let edited = snapshot(engine, handle).greasePencils[0];
|
||||
assert.equal(edited.layerCount, 2);
|
||||
const layerId = edited.layers.find((layer) => layer.name === "Web Drafts").id;
|
||||
command(engine, handle, { type: "moveGreasePencilLayer", dataId, layerId, direction: "BOTTOM" });
|
||||
command(engine, handle, { type: "moveGreasePencilLayer", dataId, layerId, direction: "BOTTOM", baseRevision: edited.revision });
|
||||
edited = snapshot(engine, handle).greasePencils[0];
|
||||
assert.equal(edited.layers[0].id, layerId);
|
||||
assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].layers[1].id, layerId);
|
||||
assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].layers[0].id, layerId);
|
||||
command(engine, handle, { type: "insertGreasePencilFrame", dataId, layerId, frame: 10, duration: 4 });
|
||||
const webStrokes = [{
|
||||
cyclic: true,
|
||||
@@ -107,8 +119,18 @@ assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].drawi
|
||||
assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].strokeCount, 1);
|
||||
assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].pointCount, 7);
|
||||
command(engine, handle, { type: "removeGreasePencilFrame", dataId, layerId, frame: 10 });
|
||||
edited = snapshot(engine, handle).greasePencils[0];
|
||||
assert.equal(edited.pointCount, 7);
|
||||
const sourceFrame = edited.layers.find((layer) => layer.id === layerId).frames[0];
|
||||
command(engine, handle, { type: "moveGreasePencilFrame", dataId, layerId, frame: 10, targetFrame: 12, drawingId: sourceFrame.drawing.id, baseRevision: snapshot(engine, handle).revision });
|
||||
edited = snapshot(engine, handle).greasePencils[0];
|
||||
assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].frame, 12);
|
||||
assert.equal(edited.layers.find((layer) => layer.id === layerId).frames[0].drawing.id, sourceFrame.drawing.id);
|
||||
assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].layers.find((layer) => layer.id === layerId).frames[0].frame, 10);
|
||||
assert.equal(engine._web_engine_redo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].layers.find((layer) => layer.id === layerId).frames[0].frame, 12);
|
||||
command(engine, handle, { type: "removeGreasePencilFrame", dataId, layerId, frame: 12 });
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].frameCount, 1);
|
||||
assert.equal(engine._web_engine_undo(handle), 0, engine.UTF8ToString(engine._web_engine_last_error_message()));
|
||||
assert.equal(snapshot(engine, handle).greasePencils[0].frameCount, 2);
|
||||
@@ -130,8 +152,15 @@ assert.equal(reopenedData.frameCount, 2);
|
||||
assert.equal(reopenedData.strokeCount, 2);
|
||||
assert.equal(reopenedData.pointCount, 7);
|
||||
const reopenedStroke = reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].drawing.strokes[0];
|
||||
assert.deepEqual(reopenedData.layers.map((layer) => layer.name), ["Web Drafts", "Lines"]);
|
||||
assert.equal(reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].frame, 12);
|
||||
assert.equal(reopenedData.layers.find((layer) => layer.name === "Web Drafts").frames[0].drawing.id, sourceFrame.drawing.id);
|
||||
assert.equal(reopenedStroke.cyclic, true);
|
||||
assert.deepEqual(reopenedStroke.points[2].position, [2, 0, 0]);
|
||||
close(reopenedStroke.points[2].radius, 0.3, "reopened radius");
|
||||
assert.deepEqual(
|
||||
reopenedData.layers.find((layer) => layer.name === "Lines").frames[0].drawing.strokes[0].points.map((point) => point.id),
|
||||
original.layers[0].frames[0].drawing.strokes[0].points.map((point) => point.id),
|
||||
);
|
||||
engine._web_engine_destroy(reopened);
|
||||
process.stdout.write("grease-pencil-roundtrip-ok layer-frame-stroke=passed undo-redo=passed save-reopen=passed\n");
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user