diff --git a/README.md b/README.md index 718f09e5..84e7bf3a 100644 --- a/README.md +++ b/README.md @@ -31,8 +31,9 @@ npm --prefix web run test:browser See `docs/CURRENT_EXECUTION_PLAN.md` for the current task order, `docs/PROJECT_STATUS_AND_NEXT_WORK.md` for the implemented scope, and -`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` for the long-term Blender parity ledger. -The atomic full-product work breakdown is in +`docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` for the normative M12-M23 +full-parity execution plan. The domain overview and coverage cross-check are in +`docs/BLENDER_5_2_WEB_FEATURE_PARITY.md` and `docs/BLENDER_5_2_FULL_PARITY_WBS.md`. The release contract and explicit non-goals are defined in diff --git a/blender-5.2.0/source/blender/makesrna/intern/CMakeLists.txt b/blender-5.2.0/source/blender/makesrna/intern/CMakeLists.txt index a61f87e2..fd1e1295 100644 --- a/blender-5.2.0/source/blender/makesrna/intern/CMakeLists.txt +++ b/blender-5.2.0/source/blender/makesrna/intern/CMakeLists.txt @@ -157,7 +157,7 @@ if(WITH_WEB) # runtime only needs model/animation RNA needed by depsgraph evaluation. # Trim after generation so the generator itself remains source-compatible. list(FILTER GENSRC INCLUDE REGEX - "rna_(ID|action|animation|armature|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$") + "rna_(ID|action|animation|armature|blendfile_import|collection|constraint|curve|depsgraph|fcurve|grease_pencil|image|key|lattice|layer|main|material|mesh|object|packedfile|pose|scene|world)_gen\\.cc$") list(APPEND GENSRC "${CMAKE_CURRENT_BINARY_DIR}/rna_prototypes_gen.hh" "${CMAKE_CURRENT_BINARY_DIR}/../RNA_prototypes.hh" diff --git a/blender-5.2.0/source/blender/web_engine/CMakeLists.txt b/blender-5.2.0/source/blender/web_engine/CMakeLists.txt index 03050292..7d026bbf 100644 --- a/blender-5.2.0/source/blender/web_engine/CMakeLists.txt +++ b/blender-5.2.0/source/blender/web_engine/CMakeLists.txt @@ -129,7 +129,7 @@ target_link_options(web_engine PRIVATE "-sINITIAL_MEMORY=67108864" "-sMAXIMUM_MEMORY=2147483648" "-sSTACK_SIZE=8388608" - "-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']" + "-sEXPORTED_FUNCTIONS=['_malloc','_free','_web_engine_create','_web_engine_destroy','_web_engine_get_memory_stats','_web_engine_get_live_handles','_web_engine_get_allocated_bytes','_web_engine_open_blend','_web_engine_apply_command','_web_engine_append_library_object','_web_engine_undo','_web_engine_redo','_web_engine_get_scene_snapshot','_web_engine_get_scene_metadata','_web_engine_get_scene_geometry','_web_engine_get_scene_delta','_web_engine_get_packed_asset','_web_engine_evaluate_depsgraph','_web_engine_save_blend','_web_engine_free_buffer','_web_engine_last_error_code','_web_engine_last_error_message','_web_engine_decimate_apply']" "-sEXPORTED_RUNTIME_METHODS=['ccall','cwrap','UTF8ToString']" ) if(WEB_ENGINE_ENABLE_UNSAFE_DEPSGRAPH) diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_api.cpp b/blender-5.2.0/source/blender/web_engine/web_engine_api.cpp index c0427d59..35ddfe12 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_api.cpp +++ b/blender-5.2.0/source/blender/web_engine/web_engine_api.cpp @@ -2145,6 +2145,139 @@ EMSCRIPTEN_KEEPALIVE int web_engine_apply_command(const int handle, } } +EMSCRIPTEN_KEEPALIVE int web_engine_append_library_object(const int handle, + const uint8_t *source_data, + const uint32_t source_length, + const uint8_t *metadata, + const uint32_t metadata_length) +{ + EngineState *engine = find_engine(handle); + if (engine == nullptr) return last_error_code; + if (source_data == nullptr || source_length == 0 || + source_length > 64u * 1024u * 1024u || metadata == nullptr || metadata_length == 0) + { + set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_BUDGET_EXCEEDED: library append input is empty or exceeds 64 MiB"); + return last_error_code; + } + if (!engine->authoritative_commands || engine->authoritative_main == nullptr) { + set_error(WEB_ENGINE_NOT_IMPLEMENTED, + "COMMAND_REQUIRES_MAIN_AUTHORITY: library append requires an authoritative Main"); + return last_error_code; + } + try { + const json request = json::parse(reinterpret_cast(metadata), + reinterpret_cast(metadata) + metadata_length); + if (!request.is_object() || request.size() != 5 || request.value("schemaVersion", 0) != 1 || + !request.contains("baseRevision") || !request["baseRevision"].is_number_unsigned() || + !request.contains("sourceLocator") || !request["sourceLocator"].is_string() || + !request.contains("sourceDataBlockId") || !request["sourceDataBlockId"].is_string() || + !request.contains("expectedClosure") || !request["expectedClosure"].is_object()) + { + set_error(WEB_ENGINE_INVALID_ARGUMENT, + "ASSET_MANIFEST_INVALID: native library append metadata is malformed"); + return last_error_code; + } + for (const char *key : {"schemaVersion", "baseRevision", "sourceLocator", "sourceDataBlockId", "expectedClosure"}) { + if (!request.contains(key)) { + set_error(WEB_ENGINE_INVALID_ARGUMENT, + "ASSET_MANIFEST_INVALID: native library append metadata fields are not exact"); + return last_error_code; + } + } + const uint64_t base_revision = request["baseRevision"].get(); + if (base_revision != engine->revision) { + set_error(WEB_ENGINE_INVALID_ARGUMENT, + "REVISION_CONFLICT: library append base revision does not match the current SceneIR"); + return last_error_code; + } + const json &closure = request["expectedClosure"]; + if (closure.size() != 4) { + set_error(WEB_ENGINE_INVALID_ARGUMENT, "ASSET_MANIFEST_INVALID: append closure fields are not exact"); + return last_error_code; + } + auto closure_name = [&](const char *field, const char *prefix) -> std::string { + if (!closure.contains(field) || !closure[field].is_string()) return {}; + const std::string value = closure[field].get(); + const size_t prefix_length = strlen(prefix); + if (value.size() <= prefix_length || value.compare(0, prefix_length, prefix) != 0) return {}; + return value.substr(prefix_length); + }; + const std::string object_name = closure_name("object", "Object/"); + const std::string mesh_name = closure_name("mesh", "Mesh/"); + const std::string material_name = closure_name("material", "Material/"); + const std::string image_name = closure_name("image", "Image/"); + if (object_name.empty() || mesh_name.empty() || material_name.empty() || image_name.empty() || + request["sourceDataBlockId"].get() != "Object/" + object_name) + { + set_error(WEB_ENGINE_INVALID_ARGUMENT, + "ASSET_SOURCE_HASH_MISMATCH: native append root does not match the expected closure"); + return last_error_code; + } + + HistoryEntry previous_state; + std::string history_error; + if (!capture_history_entry(*engine, previous_state, history_error)) { + set_error(WEB_ENGINE_BLEND_WRITE_FAILED, history_error.c_str()); + return last_error_code; + } + const uint64_t previous_revision = engine->revision; + const std::string previous_snapshot = engine->scene_snapshot.empty() ? + std::string(empty_scene_snapshot_json()) : + engine->scene_snapshot; + const std::string previous_delta = engine->scene_delta; + const auto previous_packed_assets = engine->packed_assets; + auto rollback = [&]() { + std::string rollback_error; + if (restore_history_entry(*engine, previous_state, rollback_error)) { + engine->revision = previous_revision; + engine->scene_snapshot = previous_snapshot; + engine->scene_delta = previous_delta; + engine->packed_assets = previous_packed_assets; + engine->binary_revision = std::numeric_limits::max(); + } + }; + + std::string object_id; + std::string append_error; + if (!web_engine_blend_main_append_object(engine->authoritative_main, + source_data, + source_length, + request["sourceLocator"].get_ref().c_str(), + object_name.c_str(), + mesh_name.c_str(), + material_name.c_str(), + image_name.c_str(), + object_id, + append_error)) + { + rollback(); + set_error(append_error.rfind("BLEND_READ_FAILED:", 0) == 0 ? + WEB_ENGINE_BLEND_READ_FAILED : WEB_ENGINE_INVALID_ARGUMENT, + append_error.c_str()); + return last_error_code; + } + json snapshot; + if (!refresh_scene_from_main(*engine, snapshot, object_id, append_error)) { + rollback(); + set_error(WEB_ENGINE_BLEND_WRITE_FAILED, append_error.c_str()); + return last_error_code; + } + + engine->authoritative_dirty = true; + push_history(engine->undo_history, std::move(previous_state)); + engine->redo_history.clear(); + engine->scene_snapshot = snapshot.dump(); + engine->revision++; + engine->scene_delta = scene_delta_json(previous_snapshot, engine->scene_snapshot); + set_error(WEB_ENGINE_OK, "ok"); + return WEB_ENGINE_OK; + } + catch (const std::exception &exception) { + set_error(WEB_ENGINE_INVALID_ARGUMENT, exception.what()); + return last_error_code; + } +} + EMSCRIPTEN_KEEPALIVE int web_engine_undo(const int handle) { EngineState *engine = find_engine(handle); diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_api.h b/blender-5.2.0/source/blender/web_engine/web_engine_api.h index 8de7c03f..933d4d6b 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_api.h +++ b/blender-5.2.0/source/blender/web_engine/web_engine_api.h @@ -28,6 +28,11 @@ uint32_t web_engine_get_live_handles(void); uint32_t web_engine_get_allocated_bytes(void); int web_engine_open_blend(int handle, const uint8_t *data, uint32_t length); int web_engine_apply_command(int handle, const uint8_t *data, uint32_t length); +int web_engine_append_library_object(int handle, + const uint8_t *source_data, + uint32_t source_length, + const uint8_t *metadata, + uint32_t metadata_length); int web_engine_undo(int handle); int web_engine_redo(int handle); int web_engine_get_scene_snapshot(int handle, const uint8_t **data, uint32_t *length); diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_headless_fallback.cpp b/blender-5.2.0/source/blender/web_engine/web_engine_headless_fallback.cpp index bed00c2c..fb98a4be 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_headless_fallback.cpp +++ b/blender-5.2.0/source/blender/web_engine/web_engine_headless_fallback.cpp @@ -72,6 +72,13 @@ static void register_web_geometry_node_types() } +namespace seq { + +/* The browser engine does not load desktop sequencer playback state. */ +void doversion_250_sound_proxy_update(Main * /*bmain*/, Editing * /*editing*/) {} + +} // namespace seq + void WEB_headless_node_system_init() { static bool initialized = false; @@ -411,4 +418,5 @@ const ComputeContext *compute_context_for_viewer_path_elem( namespace blender { void WM_main_add_notifier(unsigned int /*type*/, void * /*reference*/) {} +void WM_msg_publish_rna(wmMsgBus * /*mbus*/, PointerRNA * /*ptr*/, PropertyRNA * /*prop*/) {} } // namespace blender diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp index 4d6c0f75..d0f70d15 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp +++ b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.cpp @@ -20,6 +20,7 @@ #include "BKE_anim_data.hh" #include "BKE_attribute.hh" #include "BKE_attribute.h" +#include "BKE_blendfile_link_append.hh" #include "BKE_layer.hh" #include "BKE_collection.hh" #include "BKE_curve.hh" @@ -1120,6 +1121,117 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state, return true; } +bool web_engine_blend_main_append_object(WebBlendMainState *state, + const uint8_t *source_data, + const uint32_t source_length, + const char *source_locator, + const char *object_name, + const char *mesh_name, + const char *material_name, + const char *image_name, + std::string &object_id, + std::string &error) +{ + if (state == nullptr || state->main == nullptr || state->scene == nullptr || + state->view_layer == nullptr || source_data == nullptr || source_length == 0 || + source_length > 64u * 1024u * 1024u || source_locator == nullptr || + source_locator[0] == '\0' || object_name == nullptr || mesh_name == nullptr || + material_name == nullptr || image_name == nullptr || object_name[0] == '\0' || + mesh_name[0] == '\0' || material_name[0] == '\0' || image_name[0] == '\0') + { + error = "ASSET_MANIFEST_INVALID: append requires a bounded source and complete Object closure"; + return false; + } + if (find_object(state->main, ("object:" + std::string(object_name)).c_str()) != nullptr || + find_material(state->main, ("material:" + std::string(material_name)).c_str()) != nullptr || + find_image(state->main, ("image:" + std::string(image_name)).c_str()) != nullptr) + { + error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local ID"; + return false; + } + for (const Mesh &mesh : state->main->meshes) { + if (id_name(mesh.id) == mesh_name) { + error = "ASSET_MANIFEST_INVALID: append closure collides with an existing local Mesh"; + return false; + } + } + + BlendHandle *blend_handle = BLO_blendhandle_from_memory( + source_data, int(source_length), nullptr); + if (blend_handle == nullptr) { + error = "BLEND_READ_FAILED: Blender rejected the append source buffer"; + return false; + } + + GlobalMainScope global_main_scope(state->main); + BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, true); + LibraryLink_Params parameters{}; + BLO_library_link_params_init_with_context(¶meters, + state->main, + BLO_LIBLINK_APPEND_RECURSIVE, + 0, + state->scene, + state->view_layer, + nullptr); + BlendfileLinkAppendContext *context = BKE_blendfile_link_append_context_new(¶meters); + if (context == nullptr) { + BLO_blendhandle_close(blend_handle); + BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false); + error = "BLEND_READ_FAILED: Blender could not create the append context"; + return false; + } + BKE_blendfile_link_append_context_library_add(context, source_locator, blend_handle); + BlendfileLinkAppendContextItem *item = BKE_blendfile_link_append_context_item_add( + context, object_name, ID_OB, nullptr); + BKE_blendfile_link_append_context_item_library_index_enable(context, item, 0); + BKE_blendfile_link_append_context_init_done(context); + BKE_blendfile_link(context, nullptr); + BKE_blendfile_append(context, nullptr); + BKE_blendfile_link_append_instantiate_loose(context, nullptr); + BKE_blendfile_link_append_context_finalize(context); + ID *new_id = BKE_blendfile_link_append_context_item_newid_get(context, item); + BKE_blendfile_link_append_context_free(context); + BKE_main_id_tag_all(state->main, ID_TAG_PRE_EXISTING, false); + + Object *object = new_id != nullptr && GS(new_id->name) == ID_OB ? + reinterpret_cast(new_id) : + nullptr; + Mesh *mesh = object != nullptr && object->type == OB_MESH && object->data != nullptr ? + reinterpret_cast(object->data) : + nullptr; + Material *material = mesh != nullptr && mesh->totcol == 1 && mesh->mat != nullptr ? + mesh->mat[0] : + nullptr; + Image *image = find_image(state->main, ("image:" + std::string(image_name)).c_str()); + bool material_uses_image = false; + if (material != nullptr && material->nodetree != nullptr && image != nullptr) { + for (const bNode &node : material->nodetree->nodes) { + if (node.id == &image->id) { + material_uses_image = true; + break; + } + } + } + if (object == nullptr || mesh == nullptr || material == nullptr || image == nullptr || + id_name(object->id) != object_name || id_name(mesh->id) != mesh_name || + id_name(material->id) != material_name || id_name(image->id) != image_name || + object->id.lib != nullptr || mesh->id.lib != nullptr || material->id.lib != nullptr || + image->id.lib != nullptr || object->id.override_library != nullptr || + mesh->id.override_library != nullptr || material->id.override_library != nullptr || + image->id.override_library != nullptr || !material_uses_image) + { + error = "ASSET_SOURCE_HASH_MISMATCH: appended Object closure is incomplete or not fully local"; + return false; + } + BKE_view_layer_synced_ensure(*state->main, state->scene, state->view_layer); + object->id.recalc |= ID_RECALC_TRANSFORM | ID_RECALC_GEOMETRY; + mesh->id.recalc |= ID_RECALC_GEOMETRY; + material->id.recalc |= ID_RECALC_SYNC_TO_EVAL; + image->id.recalc |= ID_RECALC_SOURCE; + object_id = "object:" + id_name(object->id); + return true; +} + bool web_engine_blend_main_create_curve(WebBlendMainState *state, const char *curve_type, const char *name, diff --git a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.h b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.h index a428e62e..02e9466b 100644 --- a/blender-5.2.0/source/blender/web_engine/web_engine_main_state.h +++ b/blender-5.2.0/source/blender/web_engine/web_engine_main_state.h @@ -128,6 +128,16 @@ bool web_engine_blend_main_create_primitive(WebBlendMainState *state, const std::vector &location, std::string &object_id, std::string &error); +bool web_engine_blend_main_append_object(WebBlendMainState *state, + const uint8_t *source_data, + uint32_t source_length, + const char *source_locator, + const char *object_name, + const char *mesh_name, + const char *material_name, + const char *image_name, + std::string &object_id, + std::string &error); bool web_engine_blend_main_duplicate_object(WebBlendMainState *state, const char *object_id, const std::vector &offset, diff --git a/docs/BLENDER_5_2_FULL_PARITY_WBS.md b/docs/BLENDER_5_2_FULL_PARITY_WBS.md index 687d2027..15b58001 100644 --- a/docs/BLENDER_5_2_FULL_PARITY_WBS.md +++ b/docs/BLENDER_5_2_FULL_PARITY_WBS.md @@ -2,19 +2,23 @@ 更新时间:2026-08-15 +> 本文保留为 Blender 全产品覆盖检查表和 F00-F24 taxonomy 参考。M12-M23 的执行分类、 +> 原子任务、证据合同与发布门以 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准; +> 本文的 F 任务不能直接作为当前 `nextTask`,也不能独立改变机器状态。 + ## 1. 目标与事实边界 -本文把“对标 Blender 全部功能”定义为:以仓库内 `blender-5.2.0` 的 RNA、operator、 -data-block、modifier、node、editor、文件格式和运行时后端为冻结基线,逐项给出 -`LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或 `EXCLUDED` 结论,并为结论保存可复验的正例、 -阻断例和数据保真证据。 +本文以仓库内 `blender-5.2.0` 的 RNA、operator、data-block、modifier、node、editor、 +文件格式和运行时后端为覆盖基线。本文原有 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED` 是 +V1 盘点术语;完整对标结论必须按新计划转换为 `LOCAL_EXACT/LOCAL_EQUIVALENT/SERVER_EXACT` +和独立 `parityStatus`,并保存可复验的正例、阻断例和数据保真证据。 当前 `Web Blender Modeler V1` 已达到其有界发布范围,但不等于完整 Blender。机器账本中 N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此以下任务默认均为未完成; 只有 `docs/status/parity-ledger.json` 和对应专项证据同时更新后才允许改变状态。 -短周期唯一领取顺序仍以 `docs/CURRENT_EXECUTION_PLAN.md` 为准。本文负责完整产品范围和细粒度 -工作包,不取代当前执行队列。 +短周期唯一领取顺序仍以 `docs/CURRENT_EXECUTION_PLAN.md` 为准。本文负责验证新计划没有遗漏 +产品域;与新计划冲突时按新计划解释,不取代当前执行队列或长期实施事实源。 ## 2. 原子任务完成定义 @@ -481,7 +485,8 @@ N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此 - [ ] `F23-09` 完成 user preferences schema、migration 和项目无关 persistence。 - [ ] `F23-10` 完成 locale 切换、数字/日期格式和翻译 fallback。 - [ ] `F23-11` 完成 theme、font scale、reduced motion 和高对比偏好。 -- [ ] `F23-12` native window/file watcher/CUDA/Metal/HIP/OptiX 明确 EXCLUDED。 +- [ ] `F23-12` native window/file watcher/CUDA/Metal/HIP/OptiX 实现细节明确不复制,其用户能力 + 按新计划映射到 platform-equivalent 或 server exact 路径。 - [ ] `F23-13` 每个新增浏览器加入 quick/P0/full 三层 CI。 - [ ] `F23-14` 1280x720、1440x900、4K 和窄屏无重叠截图门。 @@ -501,18 +506,22 @@ N-015 至 N-026 的 12 个 family 全部仍为 `parityStatus=BLOCKED`。因此 - [ ] `F24-12` CI 报告绑定 commit、lockfile、inventory、ledger、engine、archive hash。 - [ ] `F24-13` binary/source archive 在两个空临时目录独立复验。 - [ ] `F24-14` 完成 SBOM、license、source offer 和供应链审计。 -- [ ] `F24-15` 每个 parity ID 都有 LOCAL 正例、SERVER 闭环或 EXCLUDED 保真证据。 +- [ ] `F24-15` 每个 parity ID 都有 LOCAL_EXACT、LOCAL_EQUIVALENT 或 SERVER_EXACT 完整证据。 - [ ] `F24-16` inventory 未分类、隐式 loss、proxy-success 三项计数均为 0。 - [ ] `F24-17` 独立机器复验全部构件和关键证据 hash。 -- [ ] `F24-18` 发布说明逐项列出支持、受限、server 和 excluded 能力。 +- [ ] `F24-18` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。 里程碑 F24:不能由聚合 release gate 反推 family 完成。只有 inventory 的每个单项都达到自身 -退出条件,N-015 至 N-026 才能逐项从 `BLOCKED` 改为 `COMPLETE`。 +退出条件,全部生成的 owner family 才能逐项从 `BLOCKED` 改为 `COMPLETE`。 ## 29. 当前领取点 -截至 2026-08-15,M6 已完成 35/71:single/pthread 选择与回退、真实 HTTP MIME/range、 -releaseId/hash 升级负例和缓存合同均有专项证据。HTML、manifest、stable engine 未变化时均返回 -`304`,换代后均返回 `200` 且 ETag 变化;production build 的 1 个入口 JS、1 个 CSS、3 个 -Worker 均使用 8 位内容 hash 文件名并返回 `immutable`。当前唯一下一任务为 `M6-09A`:从 -binary archive 解包后使用合同服务器启动,不读取工作区文件。 +截至 2026-08-17,M6-M11 已按当前计划完成,M11 最终以 Render、Compositor、Media 三域故障 +生命周期专项门收口,M12-01A-I 已完成 catalog schema/migration;M12-02A-H 又完成 PreviewImage +inventory、identity、decode budget、OPFS commit、dedupe、quarantine、project-scoped reference GC +与 desktop/browser 双显示路径像素闭环。本文的 F00-F24 +条目只作为覆盖检查表,不能直接领取;M12-03A 又冻结 Append/Link/Override 的 36 类 root 与 +实际 ID pointer dependency closure;M12-03B 固定 source/owner/read-only/invalidation identity; +M12-03C 冻结 Blender 5.2 单 Object append 的 Object/Mesh/Material/Image local stable mapping; +M12-03D 已通过 WASM Main 单 transaction 创建同一 local dependency closure。当前唯一下一任务为 +`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 `M12-03E`。 diff --git a/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md b/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md new file mode 100644 index 00000000..28b07bba --- /dev/null +++ b/docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md @@ -0,0 +1,615 @@ +# Blender 5.2 完整 Web 对标实施计划 + +更新时间:2026-08-17 + +本文是 M12 及以后完整 Blender 5.2 Web 迁移的长期规范事实源,负责执行分类、原子任务、 +依赖、证据和完整发布门。它不覆盖 `WEB_BLENDER_MODELER_V1_SCOPE.md` 的 V1 产品契约,也不 +改写 V1 已冻结的 `LOCAL_EXACT/LOCAL_BOUNDED/SERVER/EXCLUDED` 发布分类。 + +文档职责固定如下: + +| 事实 | 权威来源 | +| --- | --- | +| V1 承诺、非目标和发布标准 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | +| 当前唯一可领取任务 | `CURRENT_EXECUTION_PLAN.md` 与机器 `nextTask` | +| M12-M23 完整对标规则和长期任务 | 本文 | +| 当前实现事实和 family/slice 状态 | `status/parity-ledger.json` 与对应 evidence | +| Blender 全域功能说明和覆盖参考 | `BLENDER_5_2_WEB_FEATURE_PARITY.md`、`BLENDER_5_2_FULL_PARITY_WBS.md` | + +描述与机器证据冲突时不得用文字把失败改成成功。M12-01A-I 已收口,M12-02A-G preview +inventory/identity/decode budget/OPFS commit/dedupe/quarantine/GC 已完成且未改变 parity 状态; +desktop/browser 主线程与 Offscreen preview 像素闭环也已完成;Append/Link/Override 数据块与 +依赖闭包 inventory、source/owner/read-only/invalidation 合同、desktop 单 Object append fixture +及 WASM Main 单 transaction append 已完成,当前唯一 `nextTask` 为 `M12-03E`。 + +## 1. 目标与边界 + +目标不是制作一个外观类似 Blender 的网页,而是让用户从浏览器完成 Blender 5.2 的可观察 +功能,并且每项声明都能用真实 Blender 5.2 执行结果复核。浏览器本地不适合承载的 Cycles、 +完整 Python、部分模拟和硬件后端,必须由隔离的 Blender 5.2 server job 执行;Three.js、 +WebGPU 或 JavaScript 近似结果不能冒充 Blender 结果。 + +“完整 Web Blender”只在以下条件同时满足后成立: + +1. Blender 5.2 的 data-block、operator、node、modifier、editor workflow、import/export 和 + render/compute family 全部进入机器清单。 +2. 每个用户可观察能力被标为 `LOCAL_EXACT`、`LOCAL_EQUIVALENT` 或 `SERVER_EXACT`,且有成功 + 证据;`BLOCKED`、`UNINVENTORIED` 或未声明项数量必须为零。 +3. `LOCAL_EXACT` 使用同一 fixture 比较 Blender desktop 与 WASM/Main 的结构、数值或像素。 +4. `LOCAL_EQUIVALENT` 只用于浏览器交互外壳;最终 Main、Depsgraph、保存文件和导出语义仍须 + 与 Blender 一致。 +5. `SERVER_EXACT` 必须真的启动锁定版本 Blender,验证提交、进度、取消、资源隔离、结果 hash + 和项目绑定,不允许只返回“应走服务端”。 +6. 完整项目通过保存、关闭、Worker/页面重启、重新打开和 Blender desktop 再打开;未知数据 + 不丢失,不支持的写操作不污染原文件。 +7. Chromium、Firefox、WebKit 的支持声明分别由自己的 quick/P0/full 证据决定,不互相推断。 + +Native window、CUDA/Metal/HIP/OptiX 实现细节不需要在浏览器复制,但它们提供的用户能力必须 +由 WebGPU、CPU 或 `SERVER_EXACT` 路径覆盖。若没有等价执行路径,就保持 `BLOCKED`,项目不得 +使用“完整对标”表述。 + +### 1.1 完整对标状态不是 V1 发布分类 + +全量盘点为每个 `parityId` 使用两个独立轴,禁止把 V1 family 的 `releaseClass` 直接升级为 +全量完成: + +| 轴 | 允许值 | 含义 | +| --- | --- | --- | +| `implementationClass` | `LOCAL_EXACT` | Blender 语义在浏览器 Main/WASM 本地执行并经同 fixture 对标 | +| `implementationClass` | `LOCAL_EQUIVALENT` | 仅浏览器平台或交互外壳不同,最终 Main、文件、导出和可见语义精确 | +| `implementationClass` | `SERVER_EXACT` | 锁定版本的真实 Blender server 执行权威求值,浏览器负责提交和消费 | +| `parityStatus` | `UNINVENTORIED` | Blender 基线项尚未进入机器清单 | +| `parityStatus` | `BLOCKED` | 已盘点但缺实现、证据或存在超阈值差异 | +| `parityStatus` | `IN_PROGRESS` | 唯一原子任务正在实现;仍按未完成计 | +| `parityStatus` | `VERIFIED` | 对应 class 的全部强制证据通过 | + +`LOCAL_EQUIVALENT` 不能用于 modifier、node、solver、render、import/export 或其他计算结果的 +近似实现。它只适用于 native window、文件选择器、菜单布局、输入映射等浏览器平台差异; +若操作后的 Main 或文件语义不同,状态只能是 `BLOCKED`。 + +### 1.2 本地与 Blender server 路由规则 + +执行 class 在写实现前确定,不能根据某次运行是否方便临时切换: + +| 能力特征 | 必选 class | 最低证据 | +| --- | --- | --- | +| Main/ID 读取写入、selection/context、undo/redo、增量编辑、保存恢复 | `LOCAL_EXACT` | desktop/WASM 同 fixture、单事务、保存重开、Chromium 用户路径 | +| native window、系统文件对话框、菜单/快捷键表现等平台外壳 | `LOCAL_EQUIVALENT` | desktop 最终状态、Web Main/文件最终状态、可访问交互和恢复一致 | +| WASM 已包含所需 Blender evaluator,且在声明预算内可确定性完成 | `LOCAL_EXACT` | desktop/WASM 语义或像素阈值、浏览器实际执行、故障恢复 | +| Cycles、复杂 Eevee、Blender 完整 Compositor、重模拟/烘焙、tracking solve | `SERVER_EXACT`,除非该项另有完整本地证据 | 真实隔离 Blender job、版本/输入/设置/输出绑定、取消和恢复 | +| 任意 Python/add-on、需原生库的 USD/Alembic/codec 或 OS/GPU 后端能力 | `SERVER_EXACT` | 权限策略、无隐式网络、资源限制、真实执行和结果复验 | +| Three.js/WebGPU 预览、summary、proxy、capability probe、仅路由结果 | 不能单独领取成功 class | 只能作为已分类能力的中间证据;权威结果缺失时保持 `BLOCKED` | + +同一 workflow 可以由多个 `parityId` 组成,例如本地编辑后提交服务端 Cycles 渲染;每个 ID +只能有一个权威执行 class。server 路径必须向用户明确上传内容、版本、预算、进度和取消状态; +未配置 server、用户不同意上传或 server receipt 失效时必须 fail-closed,不能回退到近似结果。 + +### 1.3 阻止“完整对标”声明的差异 + +以下任一项存在时,产品、README、发布说明和 UI 都不得使用“完整 Web Blender”或等价表述: + +- inventory 有未映射、重复映射、版本漂移或 `UNINVENTORIED` 条目; +- 任一 `parityId` 为 `BLOCKED/IN_PROGRESS`,或只有 summary/proxy/probe/route/mock 证据; +- 计算结果超过已批准的数值、拓扑、时序、音频或像素阈值; +- 不支持或未知数据在打开、编辑、保存、导出、append/link/override 中丢失或被重写; +- writer 缺 undo/redo、保存重开、desktop 再打开,或失败路径改变 Main/项目 revision; +- server 能力没有真实 Blender process、隔离、取消、预算、结果 hash 和项目绑定证据; +- 用户可进入没有 capability gate 的 UI,或本地近似被显示为 Blender 权威结果; +- 已声明浏览器、设备档位、输入方式或离线/server 模式缺自己的通过证据; +- clean build、依赖/SBOM/license、安全、性能、故障或独立机器复验未通过。 + +## 2. 当前基线 + +- 当前交付物是可审计的 Chromium `Web Blender Modeler V1`,不是完整 Blender。 +- M6 至 M11 已完成;M12-01A-I、M12-02A-G enabling tasks 与 M12-02H preview display + parity slice 及 M12-03A-D library-operation inventory/identity/desktop fixture/WASM Main append + 已完成,当前唯一下一任务为 M12-03E。 +- N-015 至 N-026 的 V1 `releaseStatus` 为 `READY`,但 12 个 family 的全域 + `parityStatus` 仍全部为 `BLOCKED`。 +- 这 12 个 family 是当前 post-V1 账本范围,不覆盖完整 Blender inventory;不能把它们未来的 + 聚合状态当作 Main、Mesh、Modifier 等全域完成证明。M15 必须生成完整 owner family 集合。 +- 当前完成项只证明已声明的有界切片;任何 summary、proxy、capability probe 或路由结果都不能 + 自动升级为 Blender 功能完成。 + +## 3. 真实 Blender 对标流水线 + +### 3.1 固定运行时 + +- [ ] `FULL-PARITY-RUNTIME-01` 固定官方 Blender 5.2.0 LTS archive SHA-256。 +- [ ] `FULL-PARITY-RUNTIME-02` 记录 `blender --version`、build hash、平台、Python、USD、 + Alembic、OpenVDB、Cycles device inventory。 +- [ ] `FULL-PARITY-RUNTIME-03` desktop runner 拒绝版本或 build hash 漂移。 +- [ ] `FULL-PARITY-RUNTIME-04` WASM manifest 绑定 Blender source commit、Emscripten、CMake + cache、JS/WASM SHA-256。 +- [ ] `FULL-PARITY-RUNTIME-05` server image 绑定 Blender binary、启动参数、依赖和 image digest。 +- [ ] `FULL-PARITY-RUNTIME-06` fixture generator 只能在临时目录写文件,运行后逐项 hash。 +- [ ] `FULL-PARITY-RUNTIME-07` 禁止从开发机未声明插件、偏好设置或环境变量读取能力。 +- [ ] `FULL-PARITY-RUNTIME-08` CI 在每次 runner 启动时执行 runtime preflight 并写机器报告。 + +### 3.2 每项能力的固定证据链 + +能力原子执行单元固定为一个 `parityId × 单一用户可观察行为 × implementationClass`,内部走完 +本节的完整证据链。纯 runtime、inventory、generator 或 schema 基建任务必须标记 +`enablingTask=true`,列出它解除的依赖,但自身不能把任何 `parityId` 改为 `VERIFIED`。 + +M12 的字母 ID 只有在单一性检查通过后才能进入 `nextTask`;任务文字中出现“每种/全部/逐项” +时,先按稳定 `parityId` 增加后缀并拆成多个任务。M16-M22 的“差距清零”项只是波次容器, +必须先由 M15 展开,不能直接勾选容器。 + +每个原子功能任务必须按以下顺序执行,任一步失败都不能领取下一功能: + +- [ ] `PARITY-TEMPLATE-01` 从 Blender RNA/operator/node 清单选取一个最小行为。 +- [ ] `PARITY-TEMPLATE-02` 写 capability 条目、输入 schema、预算、稳定错误码和保存策略。 +- [ ] `PARITY-TEMPLATE-03` 写 Blender 5.2 Python fixture generator,不手工伪造期望 JSON。 +- [ ] `PARITY-TEMPLATE-04` desktop Blender 执行操作并导出 canonical semantic report。 +- [ ] `PARITY-TEMPLATE-05` desktop 保存 `.blend`,新 Blender 进程重开并再次导出报告。 +- [ ] `PARITY-TEMPLATE-06` WASM/Main 对同一输入执行同一行为并导出同结构报告。 +- [ ] `PARITY-TEMPLATE-07` 比较 stable ID、Main revision、数据值、依赖和错误语义。 +- [ ] `PARITY-TEMPLATE-08` 对 writer 验证一次 transaction、undo、redo、save/reopen。 +- [ ] `PARITY-TEMPLATE-09` Chromium 主线程和 Offscreen 只消费权威结果并验证可见输出。 +- [ ] `PARITY-TEMPLATE-10` 加入 unsupported、malformed、stale revision 和预算超限负例。 +- [ ] `PARITY-TEMPLATE-11` 加入取消、Worker restart、资源释放和小任务恢复。 +- [ ] `PARITY-TEMPLATE-12` 将 command、duration、stdout 摘要和构件 SHA-256 写入 evidence。 +- [ ] `PARITY-TEMPLATE-13` 更新 family status、机器 ledger、实施记录和用户可见限制。 +- [ ] `PARITY-TEMPLATE-14` 独立运行 `typecheck`、lint、unit、专项 E2E 和状态一致性门。 + +### 3.3 原子任务与 evidence 机器合同 + +每个原子任务进入队列前必须含以下字段;字段不得留空,确实不适用时写 `NOT_APPLICABLE` 和 +可校验理由,不能直接删除: + +| 字段 | 必填内容 | +| --- | --- | +| identity | `taskId`、parent ID、`parityId`、`enablingTask`、Blender source anchor、owner、dependency IDs | +| classification | `implementationClass`、`parityStatus`、用户可观察行为、非目标 | +| fixture | generator command/hash、`.blend`/asset hash、输入预算、license/source | +| desktop | Blender command、runtime/build hash、canonical report/hash、保存后新进程重开报告 | +| wasm | build manifest、command、Main revision、semantic report/hash;server 项记录本地拒绝/提交门 | +| browser | 浏览器/OS/GPU、主线程/Offscreen 适用性、Chromium 用户动作和可见结果 | +| persistence | undo/redo、save/close/reopen、Worker/page restart、desktop reopen、unknown-data report | +| faults | malformed、unsupported、stale、cancel、timeout、OOM/quota、crash/restart、resource-zero | +| server | image/binary digest、policy、job/request/result ID、progress/cancel、resource receipt、output hash | +| comparison | comparator version、阈值来源、实际 max/RMS/P95/hash、pass/fail | +| provenance | commit、dirty flag、commands、exit code、duration、stdout/stderr 摘要、artifact hashes | + +evidence 只接受一次从干净输入开始的完整成功运行。重试前的失败保留为诊断,不得拼接不同 +commit、runtime 或 fixture 的局部成功。`parityStatus=VERIFIED` 必须由状态校验器重新计算, +不能由 Markdown checkbox 单独决定。 + +### 3.4 class 对应的强制执行矩阵 + +| 证据 | `LOCAL_EXACT` | `LOCAL_EQUIVALENT` | `SERVER_EXACT` | +| --- | --- | --- | --- | +| Blender desktop 同 fixture | 必须 | 必须,比较最终语义 | 必须,作为 job 结果基线 | +| WASM/Main 实际执行 | 必须 | 必须完成最终 Main/保存语义 | 必须完成读取、校验、提交和结果绑定;权威求值不冒充本地 | +| Chromium 实际用户路径 | 必须 | 必须 | 必须,含提交/进度/取消/取回 | +| 主线程与 Offscreen | 有可见 viewport 输出时都必须 | 有 viewport 输出时都必须 | 浏览器消费可见结果时按适用路径必须 | +| undo/redo/save/reopen | writer 必须 | 改 Main/项目时必须 | 本地准备和结果绑定改变项目时必须 | +| Worker/page restart | 必须 | 必须 | 必须,且不得重复提交或绑定迟到结果 | +| OOM/quota/cancel/crash 后小任务恢复 | 必须 | 必须 | 浏览器与 server 两侧都必须 | +| 真实 Blender server | 禁止作为本地成功替代 | 禁止作为等价外壳成功替代 | 必须 | + +Firefox/WebKit 不在每个早期原子任务的默认完成门中;M14 声明某浏览器支持后,该浏览器对 +所有已验证 `parityId` 的适用 browser evidence 立即成为强制项,不能由 Chromium 结果代替。 + +### 3.5 比较器 + +- [ ] `COMPARE-01` 整数、枚举、布尔、ID、拓扑计数和离散索引使用精确比较。 +- [ ] `COMPARE-02` Float32/Float64 分别记录 max、RMS、P95、NaN/Inf 和阈值来源。 +- [ ] `COMPARE-03` transform 同时比较 location/rotation/scale 和 world matrix,处理旋转表示等价。 +- [ ] `COMPARE-04` Mesh 比较 topology hash、position/normal/UV/color/weight 和 attribute domain。 +- [ ] `COMPARE-05` 动画逐关键帧和区间采样,比较 FCurve、pose matrix、constraint 和 evaluated mesh。 +- [ ] `COMPARE-06` 图结构比较 node type、socket、default、link、group boundary 和未知节点保留。 +- [ ] `COMPARE-07` 图像固定色彩空间/alpha,输出 MAE、RMS、P95、坏像素和前景 IoU。 +- [ ] `COMPARE-08` 音频先统一 sample rate/channel layout,再比较时长、峰值、RMS 和 sample drift。 +- [ ] `COMPARE-09` 视频比较帧时间戳、关键帧映射、RGBA frame hash、音视频同步和丢帧。 +- [ ] `COMPARE-10` simulation 比较 frame/source/settings/cache hash 与每帧 canonical state。 +- [ ] `COMPARE-11` import/export 输出 loss report,未知或不可逆语义禁止静默丢弃。 +- [ ] `COMPARE-12` UI workflow 比较最终 Main/selection/context,而不是像素位置模仿 desktop UI。 + +## 4. 里程碑依赖 + +```text +M11-14 跨域故障闭环 + -> M12 Asset / Library / IO / Editors + -> M13 Scripting / Security / Server isolation + -> M14 Firefox / WebKit / Device input + -> M15 全域自动盘点 + -> M16-M22 按机器差距逐 family 清零 + -> M23 完整 Web Blender 候选发布 +``` + +M12 至 M15 是当前确定队列。M16 至 M22 的具体领取顺序由 M15 生成的机器差距决定;文档中的 +顺序是依赖上限,不得用它跳过最新 `nextTask`。 + +## 5. M11-14 Render、Compositor、Media 故障收口 + +- [x] `M11-14A` schema 固定三域 cancellation code、budget code 和资源归零字段。 +- [x] `M11-14B` Render 使用真实 `.blend` 验证 open 取消不提交、Worker generation 2 重开、 + texture aggregate budget 不替换已加载资源、dispose 回执和非空 WebGL 恢复。 +- [x] `M11-14C` Compositor 使用真实 Main graph 验证周期取消、超尺寸分配前阻断、LRU clear、 + Worker 重开和 Float32 output hash 恢复。 +- [x] `M11-14D` Media 使用真实 H.264 验证 proxy decode 取消、cache budget、clear、会话重建和 + RGBA8 payload hash 恢复。 +- [x] `M11-14E` Blender 5.2 Eevee 与 Compositor desktop golden、协议负例和 Chromium 三域门 + 由一个专项命令串联。 + +本节已由 `npm --prefix web run test:render-compositor-media-recovery`、`M11-14` status、专项 +evidence 和 family ledger 联合收口。M12-03D 完成后,当前唯一 `nextTask` 为 `M12-03E`。 + +## 6. M12 Asset、Library、IO 与 Editor + +### M12.1 Catalog schema 与迁移 + +- [x] `M12-01A` 盘点 Blender 5.2 AssetMetaData、catalog、tag、author、license、description 字段; + machine inventory 绑定 8 个源码 hash 和真实 Blender 5.2 RNA,覆盖 14 DNA/10 RNA/3 catalog + 字段并记录 7 个 Web v1 缺口。本项为 enabling task,不改变 parity slice。 +- [x] `M12-01B` 生成 desktop catalog v1 fixture 和 canonical JSON;锁定 Blender 5.2 生成 + 3 条 CDF v1 catalog、3 类资产及完整 metadata,独立新进程重开后 canonical JSON 精确一致。 +- [x] `M12-01C` 定义 Web catalog schema、stable catalog/asset ID 和文本/条目预算;schema v2 + 使用 Blender UUID/path 与 AssetWeakReference identity,全部文本预算按 UTF-8 字节执行。 +- [x] `M12-01D` 定义 schema v1→v2 前向迁移;revision、source/preview/library graph 全部保留, + legacy catalog/asset ID 生成确定性 mapping 和 source/target hash report。 +- [x] `M12-01E` 定义新版数据由旧版读取时的只读/阻断策略;v2 对 v1 reader 只开放 + hash-bound metadata snapshot,catalog/asset/save 写入统一阻断,未来 schema 完全拒绝。 +- [x] `M12-01F` 重复 ID、循环父 catalog、超长文本和未知字段负例;9 类 v1/v2 负例均返回 + 稳定 code,失败后原始小正例继续通过。 +- [x] `M12-01G` IndexedDB migration 失败保持旧 transaction 和旧索引;两个真实 abort 点重开后 + v1 index、无关 migration 行、数据库版本均不变,成功重试才原子提交 v2 index 与 receipt。 +- [x] `M12-01H` 页面/Worker 重启后 catalog 顺序和 asset identity 不变;初始页面、reload、两个 + 顺序启动的独立 Worker 返回相同 catalog/asset order、weak-reference identity 和 manifest hash。 +- [x] `M12-01I` schema fixture、migration report 和 hash 写入 M12-01 evidence;聚合 checker + 递归核对 A-H 任务链、desktop/Web fixture、canonical 双 hash、runtime fault/restart artifact。 + +### M12.2 Preview 与 content-addressed asset + +- [x] `M12-02A` 盘点 Blender preview image 尺寸、颜色空间和无 preview 状态;锁定 DNA/RNA、 + ICON/PREVIEW 双槽、RGBA8 premultiplied alpha、无颜色空间字段及 3 个 null preview runtime 状态。 +- [x] `M12-02B` 定义 preview source/content SHA-256、MIME、width/height 和生成器 identity; + Blender 5.2 确定性重编码绑定 executable/script/settings,source/content 独立且像素 exact。 +- [x] `M12-02C` 解码前检查 MIME、像素、字节和压缩比预算;schema 1 在任何 decoder/ + decoded allocation 前完成 identity、16 MiB encoded bytes、SHA-256、PNG/WebP header、尺寸、 + 16,777,216 像素、64 MiB RGBA8 和 100:1 compression ratio 门,8 类负例后小正例仍可恢复。 +- [x] `M12-02D` preview 写 OPFS 后才提交 catalog 引用;production coordinator 先写入并回读复验 + content-addressed payload,再以一次 revision/full-row guarded IndexedDB transaction 提交 v2 + preview reference 与 hash receipt;三处 fault 均不发布引用,重开后 revision 8/payload 复验通过。 +- [x] `M12-02E` 相同内容去重,不同 metadata 不覆盖 payload;两个不同 asset/identity 在 revision + 8/9 绑定同一 content hash,第二次真实 OPFS write 去重且目录只有一份 payload,两条完整 + non-preview metadata 不变,重开后 head/reference/hash 一致。 +- [x] `M12-02F` 损坏 preview 隔离且 asset metadata 仍可读取;同长度 hash drift 从 content-addressed + namespace 移入已复验 quarantine 文件,catalog revision/reference/两条 metadata 均不变;页面重开 + 从 hash-bound receipt 返回 `QUARANTINED` 和完整 asset metadata,不再发布 bytes。 +- [x] `M12-02G` 删除引用只回收无其他项目引用的 payload;同项目首删在仍有一个 reference 时 + RETAINED,末删才 COLLECTED;OPFS project namespace 使另一项目的同 hash payload/reference + 保持 READY,revision 11 重开后两条 metadata 不变。 +- [x] `M12-02H` desktop/browser preview 指标与主线程/Offscreen 显示通过;Blender 5.2 + 确定性 RGBA8 reference 与两条 Canvas2D 路径逐字节一致,错误像素由同一指标门阻断。 + +### M12.3 Append、Link、Override 语义 + +- [x] `M12-03A` 分别盘点 append、link、library override 涉及的数据块和依赖闭包;冻结 36 类 + selectable root、2 类 append-only、direct/indirect/transitive/embedded/override dependency + 角色及 17 个 override RNA 字段。 +- [x] `M12-03B` 为三种操作定义 source library ID、owner、read-only 和 invalidation token; + locator/hash 绑定 source,operation/root/owner/generation/revision/closure 共同绑定 token。 +- [x] `M12-03C` desktop append 单 Object fixture,记录依赖 material/mesh/image stable mapping; + Blender 5.2 append 后 Object/Mesh/Material/Image 全部成为可写 local owner,保存重开后依赖图、 + 几何、UV、材质槽和 packed image Float32 像素 hash 保持一致。 +- [x] `M12-03D` WASM Main append 以一次 transaction 创建本地 owner 数据;Worker 先校验 source + hash、closure 和 revision,Blender Main 递归导入 Object/Mesh/Material/packed Image,四个 ID + 均验证为可写 local owner,并只推进一个 SceneIR revision。 +- [ ] `M12-03E` append undo/redo/save/reopen 与 desktop canonical report 一致。 +- [ ] `M12-03F` desktop link fixture 保留 source library 和只读 ownership。 +- [ ] `M12-03G` linked data writer 全部返回 `LINKED_DATA_MUTATION_BLOCKED`。 +- [ ] `M12-03H` library reload 只替换匹配 generation 的 linked snapshot。 +- [ ] `M12-03I` missing library 保留 placeholder 和原始 source,不删除引用。 +- [ ] `M12-03J` desktop override fixture 记录 reference、local owner 和 property override path。 +- [ ] `M12-03K` 首个 override writer 只开放一个已验证属性。 +- [ ] `M12-03L` override stale source/revision 在 Main commit 前阻断。 +- [ ] `M12-03M` dependency cycle、ID collision、跨库环和重复 reload 负例。 +- [ ] `M12-03N` append/link/override 各有独立 desktop/WASM/Chromium 命令。 + +### M12.4 Origin、路径与 archive + +- [ ] `M12-04A` library source schema 只接受声明的 HTTPS origin、项目 asset 或用户选择文件。 +- [ ] `M12-04B` 规范化 POSIX/Windows separator、`.`、`..`、percent encoding 和 Unicode 名称。 +- [ ] `M12-04C` 拒绝绝对路径、UNC、drive path、NUL、控制字符和 origin 逃逸。 +- [ ] `M12-04D` 符号链接/hardlink entry 在写入前解析并限制在临时根。 +- [ ] `M12-04E` archive 先读取 central directory/manifest,不先解压 payload。 +- [ ] `M12-04F` 单 entry 字节、总字节、entry 数、目录深度和文件名长度预算。 +- [ ] `M12-04G` 压缩比、重叠 range、重复路径、文件/目录前缀冲突负例。 +- [ ] `M12-04H` 解压取消删除 staging,不修改已提交项目。 +- [ ] `M12-04I` quota/OOM 后释放临时文件并允许小 archive 恢复。 +- [ ] `M12-04J` 恶意 ZIP/TAR fixture 进入长期安全回归。 + +### M12.5 格式 capability matrix + +- [ ] `M12-05A` 从 Blender 5.2 build/runtime 生成 glTF/GLB、OBJ、STL、PLY、USD、Alembic 清单。 +- [ ] `M12-05B` 每种格式分别声明 import/export、local/server、geometry/material/animation 支持。 +- [ ] `M12-05C` matrix 未声明组合在文件选择器和 operator search 中不可执行。 +- [ ] `M12-05D` 能力由 runtime receipt 决定,不按扩展名推断。 +- [ ] `M12-05E` 每个 receipt 绑定 source/settings/runtime hash。 +- [ ] `M12-05F` 伪造、过期或跨版本 receipt 在使用前拒绝。 + +### M12.6 GLB round-trip + +- [ ] `M12-06A` desktop 生成 Mesh/PBR/UV/skin/animation GLB fixture 组。 +- [ ] `M12-06B` Web import 比较 topology、attributes、materials、nodes 和 animations。 +- [ ] `M12-06C` import 后保存 `.blend`、重开并比较 stable ID。 +- [ ] `M12-06D` Web export 生成 machine loss report。 +- [ ] `M12-06E` desktop Blender 再导入 Web GLB 并比较 canonical report。 +- [ ] `M12-06F` sparse accessor、Draco/extension、外部 URI 和超预算负例。 +- [ ] `M12-06G` import/export 取消、Worker restart 和 OPFS quota 恢复。 + +### M12.7 OBJ、STL、PLY round-trip + +- [ ] `M12-07A` OBJ 单 Mesh 正例:position/normal/UV/material group。 +- [ ] `M12-07B` OBJ 多对象、负索引、MTL/texture origin 和坏 face 负例。 +- [ ] `M12-07C` OBJ Web→desktop round-trip 与 loss report。 +- [ ] `M12-07D` STL binary/ASCII capability 分开声明。 +- [ ] `M12-07E` STL normal、unit、degenerate triangle 和 trailing bytes 对标。 +- [ ] `M12-07F` STL Web→desktop round-trip 与材质缺失 loss report。 +- [ ] `M12-07G` PLY ASCII/binary little-endian capability 分开声明。 +- [ ] `M12-07H` PLY vertex/face/color/custom property 映射与未知 property loss report。 +- [ ] `M12-07I` PLY big-endian/坏 list/超大 count 稳定阻断。 +- [ ] `M12-07J` 三格式分别执行取消、OOM、重启和小文件恢复。 + +### M12.8 USD 与 Alembic + +- [ ] `M12-08A` 固定有 USD/Alembic 的 Blender 5.2 server runtime identity。 +- [ ] `M12-08B` 本地 runtime 缺能力时只能路由 `SERVER_EXACT`,不能静默降级 GLB。 +- [ ] `M12-08C` USD fixture 覆盖 hierarchy、xform、mesh、material、camera、animation。 +- [ ] `M12-08D` Alembic fixture 覆盖 topology sample、transform sample 和 frame range。 +- [ ] `M12-08E` server request 绑定 source/settings/build hash。 +- [ ] `M12-08F` server output 由新 Blender 进程重开并导出 canonical report。 +- [ ] `M12-08G` server cancel/timeout/OOM 不产生可绑定 output。 +- [ ] `M12-08H` 外部引用和不可映射语义进入 loss report。 + +### M12.9 统一 Editor context + +- [ ] `M12-09A` 冻结 Workspace/Area/Region/Editor/Mode/Tool context schema。 +- [ ] `M12-09B` View3D、Outliner、Properties 读取同一 selection revision。 +- [ ] `M12-09C` 任一 editor selection 更新只提交一次共享 store transaction。 +- [ ] `M12-09D` stale editor event 不覆盖较新 Main/selection revision。 +- [ ] `M12-09E` hidden/locked/library object 在三 editor 中保持相同可选规则。 +- [ ] `M12-09F` frame、active object、mode 和 tool 在 Worker restart 后恢复。 +- [ ] `M12-09G` editor dispose 移除 DOM listener、pending request、GPU resource 和 timer。 +- [ ] `M12-09H` workspace/layout 保存只改 UI revision,不改 Blender scene revision。 + +### M12.10 专用 Editor 逐项开放 + +- [ ] `M12-10A` UV Editor 先只读显示 Main UV 与 selection。 +- [ ] `M12-10B` UV 单个 writer 通过 desktop/Main/undo/save 后开放。 +- [ ] `M12-10C` Shader Node Editor 只显示完整图,unsupported node 不可编辑但不得丢失。 +- [ ] `M12-10D` Geometry Node Editor 只开放已有 Main writer 的 allowlist node。 +- [ ] `M12-10E` Graph Editor 显示 FCurve/key/handle,单 operator 单独验收。 +- [ ] `M12-10F` Dope Sheet 与 NLA 共享 frame/selection/action revision。 +- [ ] `M12-10G` Sequencer editor 只开放已完成 Main writer 的 strip operation。 +- [ ] `M12-10H` Compositor editor 保留 unsupported graph 并显示执行 gate。 +- [ ] `M12-10I` Spreadsheet 只读取有预算的 evaluated attributes。 +- [ ] `M12-10J` 每个 editor 各有 context、dispose、键盘和窄屏 E2E。 + +### M12.11 Operator、keymap 与 workflow + +- [ ] `M12-11A` operator search 直接由 capability registry 生成。 +- [ ] `M12-11B` registry、UI、协议和 Main writer 不维护第二份成功列表。 +- [ ] `M12-11C` operator poll 使用当前 area/region/mode/selection/tool context。 +- [ ] `M12-11D` keymap 按 editor→modal→tool→global 确定性解析。 +- [ ] `M12-11E` 冲突显示来源和胜出规则,不随机采用注册顺序。 +- [ ] `M12-11F` Escape、Enter、undo、redo 在所有 modal tool 保持一致。 +- [ ] `M12-11G` 建模 workflow:import→select→edit→undo/redo→save/reopen→export。 +- [ ] `M12-11H` 动画 workflow:key→curve→NLA→frame seek→save/reopen→export。 +- [ ] `M12-11I` asset workflow:catalog→append/link→override→reload→package。 +- [ ] `M12-11J` 三 workflow 各覆盖取消、Worker crash、quota 和诊断导出。 + +M12 退出条件:所有上述任务有独立报告;Asset/IO/Editor family 仍有未盘点项时不得宣称完整。 + +## 7. M13 Scripting、安全与 Server + +### M13.1 默认拒绝与只读盘点 + +- [ ] `M13-01A` 盘点 Text、Python Console、autorun、driver expression、handler 和 add-on 入口。 +- [ ] `M13-01B` `.blend` 打开时只读取脚本 metadata,不执行任意内容。 +- [ ] `M13-01C` autorun、register、install、driver execution 默认返回稳定 policy code。 +- [ ] `M13-01D` UI 不提供绕过协议直接 eval 的入口。 +- [ ] `M13-01E` 保存/重开保留原 Text 数据块和未知脚本,不重写源码。 +- [ ] `M13-01F` malicious text、driver、handler 和 embedded module fixture 进入负例。 + +### M13.2 Script manifest 与签名 + +- [ ] `M13-02A` manifest 限制文本数量、总字节、module、path、dependency 和 permission。 +- [ ] `M13-02B` canonical serialization 固定签名输入。 +- [ ] `M13-02C` 定义 signer identity、key rotation、revocation 和 timestamp policy。 +- [ ] `M13-02D` signature 只批准声明内容,source hash 变化立即失效。 +- [ ] `M13-02E` permission 默认最小化,未知 permission 阻断。 +- [ ] `M13-02F` replay、key confusion、过期、撤销和多签顺序负例。 + +### M13.3 浏览器 sandbox + +- [ ] `M13-03A` sandbox scope 不暴露 DOM、主 Worker、OPFS、IndexedDB 或网络。 +- [ ] `M13-03B` CPU、wall time、memory、message 和 output byte budget 固定。 +- [ ] `M13-03C` host call 使用显式 allowlist 和结构化参数。 +- [ ] `M13-03D` sandbox crash/timeout 终止当前 job,不污染 Main revision。 +- [ ] `M13-03E` cancellation 后不得发布迟到 message 或 cache。 +- [ ] `M13-03F` dispose 后 Worker、port、timer 和 buffer 归零。 +- [ ] `M13-03G` 小脚本在同会话恢复,审计链保持连续。 + +### M13.4 Blender server job isolation + +- [ ] `M13-04A` 每个 job 创建不可预测的一次性目录。 +- [ ] `M13-04B` source 只读挂载,output 写独立目录。 +- [ ] `M13-04C` CPU、内存、进程、文件、时间和 output budget 由 OS/container 强制。 +- [ ] `M13-04D` 默认无网络;声明 origin 使用单独 policy。 +- [ ] `M13-04E` Blender 只以 background/factory-startup 和固定 startup script 启动。 +- [ ] `M13-04F` stdout/stderr 截断并过滤凭据/绝对内部路径。 +- [ ] `M13-04G` cancel 终止 Blender process tree 并清理临时目录。 +- [ ] `M13-04H` timeout/OOM/exit signal 转换为稳定错误码。 +- [ ] `M13-04I` result 验证 source/settings/build/output hash 后才进入 OPFS。 +- [ ] `M13-04J` 同一 request 重试保持幂等,不绑定两个冲突结果。 + +### M13.5 CSP、供应链与恶意输入 + +- [ ] `M13-05A` CSP 禁止 inline script、eval、data script 和未声明 origin。 +- [ ] `M13-05B` Worker、WASM、font、image、media 的 CSP 分别验证。 +- [ ] `M13-05C` 生产依赖、构建依赖、测试依赖分别生成 inventory。 +- [ ] `M13-05D` severity 门和例外包含 owner、期限、理由和替代控制。 +- [ ] `M13-05E` SBOM、license、source offer 与 archive/commit hash 绑定。 +- [ ] `M13-05F` malicious blend/image/font/media/archive/node graph/manifest 全矩阵。 +- [ ] `M13-05G` fuzz crash 先保存最小样本,再修复,再进入长期回归。 +- [ ] `M13-05H` audit record 使用严格时间顺序、request ID 和防篡改 hash chain。 + +M13 退出条件:任意 Python/add-on 能力只有在本地 sandbox 或 server isolation 实跑后才可从 +`BLOCKED` 改为成功;仅签名验证通过不等于执行安全。 + +## 8. M14 跨浏览器与设备 + +### M14.1 Capability probe + +- [ ] `M14-01A` 冻结当前 Chromium release/engine/archive hash。 +- [ ] `M14-01B` Firefox probe:WASM、Worker、OPFS、IndexedDB、WebGL2、WebGPU、Offscreen、isolation。 +- [ ] `M14-01C` WebKit probe采用相同字段与错误码。 +- [ ] `M14-01D` probe 记录浏览器/OS/GPU adapter,不按 user-agent 猜测能力。 +- [ ] `M14-01E` 缺 WebGPU 只阻断依赖 WebGPU 的功能,不影响可验证 WebGL2/Main。 + +### M14.2 Firefox + +- [ ] `M14-02A` Firefox quick:type/protocol/static asset 启动。 +- [ ] `M14-02B` Firefox P0:open/edit/undo/redo/save/reopen/export。 +- [ ] `M14-02C` Firefox 主线程 WebGL2 像素和 context loss。 +- [ ] `M14-02D` Firefox Offscreen 只在主线程通过后领取。 +- [ ] `M14-02E` Firefox OPFS quota、Worker crash、OOM 和 network interruption。 +- [ ] `M14-02F` Firefox full family 命令逐项 PASS/BLOCKED,禁止静默 fallback。 +- [ ] `M14-02G` Firefox quick/P0/full 进入 CI 后才更新支持声明。 + +### M14.3 WebKit + +- [ ] `M14-03A` WebKit quick 启动和本地 asset 完整性。 +- [ ] `M14-03B` WebKit P0 用户闭环。 +- [ ] `M14-03C` WebKit 主线程 viewport。 +- [ ] `M14-03D` WebKit Offscreen 独立 gate。 +- [ ] `M14-03E` WebKit storage/fault/recovery。 +- [ ] `M14-03F` WebKit full family matrix。 +- [ ] `M14-03G` WebKit CI 和发布说明。 + +### M14.4 设备档位与输入 + +- [ ] `M14-04A` GPU/内存预算按声明设备档位选择,不自动扩容。 +- [ ] `M14-04B` DPR 1/1.5/2/3 下 canvas、raycast、gizmo 和截图一致。 +- [ ] `M14-04C` pointer mouse、touch、pen 分开记录 pressure/tilt/button/cancel。 +- [ ] `M14-04D` IME composition 不触发未完成 operator。 +- [ ] `M14-04E` US、非 US、dead key 和 modifier keymap fixture。 +- [ ] `M14-04F` 触控 modal cancel、双指导航和笔 stroke 只提交一次 Main transaction。 +- [ ] `M14-04G` 1440x900、1280x720、平板、手机无重叠/溢出。 +- [ ] `M14-04H` keyboard-only、screen reader name/role 和 focus restore。 + +## 9. M15 全域自动盘点 + +- [ ] `M15-01A` 从 Blender 5.2 source/RNA 生成 data-block type 清单。 +- [ ] `M15-01B` 生成 operator idname、poll context 和主要 property 清单。 +- [ ] `M15-01C` 生成 modifier、constraint、shader/GN/compositor node 清单。 +- [ ] `M15-01D` 生成 sequencer strip/effect、physics family 和 import/export 清单。 +- [ ] `M15-01E` 生成 editor/space/region/workspace/keymap 清单。 +- [ ] `M15-01F` 为 Main/Scene/Mesh/Depsgraph 和其余现有 V1 core slice 生成同级全量清单, + 不把“当前范围完成”映射为全量完成。 +- [ ] `M15-02A` 将每项映射到 owner family、implementation class、tests 和 evidence。 +- [ ] `M15-02B` 找出未映射、重复映射、只有 summary、只有 proxy 和只有 route 的条目。 +- [ ] `M15-02C` `LOCAL_EXACT` 缺 desktop/WASM 同 fixture 时降为 `BLOCKED`。 +- [ ] `M15-02D` `LOCAL_EQUIVALENT` 缺 Main/save 证据时降为 `BLOCKED`。 +- [ ] `M15-02E` `SERVER_EXACT` 缺真实 job/cancel/isolation/result binding 时降为 `BLOCKED`。 +- [ ] `M15-02F` unknown data 保存破坏原文件时对应 reader/writer family 降为 `BLOCKED`。 +- [ ] `M15-03A` 为每个 gap 生成一个最小 `nextTask`,不生成“完成整个 family”大任务。 +- [ ] `M15-03B` nextTask 包含 fixture、desktop command、Web command、比较器和退出条件。 +- [ ] `M15-03C` 依赖图无缺失、无环、每次只允许一个 active task。 +- [ ] `M15-03D` 机器统计分别输出 inventoried/completed/blocked/uninventoried。 +- [ ] `M15-03E` 所有生成的 owner family(包括当前 N-015 至 N-026)分别达到零 gap,禁止由 + 12-family 或 release 聚合 gate 反推未纳入当前账本的 core family 已完成。 + +## 10. M16-M22 全域差距清零波次 + +这些里程碑只定义 owner 和顺序。具体任务由 M15 生成,每个任务仍使用第 3 节的 14 步模板。 + +### M16 Main、Mesh、Modifier、Sculpt + +- [ ] `M16-01` `.blend` reader/writer unknown-data preservation 差距清零。 +- [ ] `M16-02` Object/Collection/parent/transform operator 差距清零。 +- [ ] `M16-03` Mesh create/delete/select/topology/attribute operator 差距清零。 +- [ ] `M16-04` UV/normal/data-transfer operator 差距清零。 +- [ ] `M16-05` Modifier family 与 Depsgraph 求值差距清零。 +- [ ] `M16-06` PBVH Sculpt brush/mask/face-set 差距清零。 + +### M17 Rigging、Animation、Constraint、NLA + +- [ ] `M17-01` Armature edit/pose/bone collection 差距清零。 +- [ ] `M17-02` Constraint family 与 dependency order 差距清零。 +- [ ] `M17-03` Shape key、driver 安全执行和 property animation 差距清零。 +- [ ] `M17-04` FCurve/key/handle/modifier/extrapolation 差距清零。 +- [ ] `M17-05` NLA track/strip/blend/transition/meta/time 差距清零。 +- [ ] `M17-06` animation export/import loss 差距清零。 + +### M18 Non-mesh、Grease Pencil、Paint + +- [ ] `M18-01` Curve/Surface/Text/Metaball 完整 data/operator 差距清零。 +- [ ] `M18-02` Curves/Hair/PointCloud/Volume data/operator 差距清零。 +- [ ] `M18-03` Grease Pencil layer/frame/drawing/material/modifier/editor 差距清零。 +- [ ] `M18-04` Vertex/Weight/Texture Paint 与 PBVH/UDIM 差距清零。 +- [ ] `M18-05` desktop/browser/export golden 差距清零。 + +### M19 Geometry Nodes、Shader、Simulation + +- [ ] `M19-01` Geometry Nodes node/socket/field/domain/group 清单差距清零。 +- [ ] `M19-02` Geometry Nodes external resource/instance/lazy-function 差距清零。 +- [ ] `M19-03` Shader node/texture/color/sampler/compiler 差距清零。 +- [ ] `M19-04` Rigid/Soft/Cloth/Fluid/Dynamic Paint/Particle/Hair simulation 差距清零。 +- [ ] `M19-05` local/server bake、cache、playback 和 fault 差距清零。 + +### M20 Render、Compositor、Sequencer、Tracking + +- [ ] `M20-01` Camera/Light/World/Color Management 完整字段差距清零。 +- [ ] `M20-02` Eevee/WebGPU/Cycles/server render 差距清零。 +- [ ] `M20-03` Compositor node/resource/HDR/tile/server 差距清零。 +- [ ] `M20-04` Sequencer strip/effect/modifier/proxy/mix/encode 差距清零。 +- [ ] `M20-05` Audio device/mix/sync/waveform 差距清零。 +- [ ] `M20-06` Tracking/Mask editor/solve/compositor binding 差距清零。 + +### M21 Asset、IO、Editors、Scripting + +- [ ] `M21-01` Asset/Library/Override 全清单差距清零。 +- [ ] `M21-02` Blender 启用的 import/export format 差距清零。 +- [ ] `M21-03` Editor/context/operator/keymap/workspace 差距清零。 +- [ ] `M21-04` Python/Text/Add-on/driver 的 local/server 安全能力差距清零。 +- [ ] `M21-05` help/i18n/accessibility/preferences 差距清零。 + +### M22 平台与跨浏览器 + +- [ ] `M22-01` Chromium 全矩阵零 gap。 +- [ ] `M22-02` Firefox 声明矩阵零 gap。 +- [ ] `M22-03` WebKit 声明矩阵零 gap。 +- [ ] `M22-04` touch/pen/HiDPI/IME/keymap 零 gap。 +- [ ] `M22-05` local/server platform capability 零未声明项。 + +## 11. M23 完整候选发布 + +- [ ] `M23-01` clean checkout 重建 WASM、Web app、server image 和所有 fixture。 +- [ ] `M23-02` 运行全部 desktop/WASM semantic golden。 +- [ ] `M23-03` 运行 Chromium/Firefox/WebKit quick、P0、full。 +- [ ] `M23-04` 运行 geometry/texture/volume/simulation/media 性能矩阵。 +- [ ] `M23-05` 运行 OOM/quota/network/device/Worker/server fault 矩阵。 +- [ ] `M23-06` 运行 malicious/fuzz/CSP/dependency/security 矩阵。 +- [ ] `M23-07` inventory 中每个 `parityId` 均为 `VERIFIED`,所有生成的 owner family 均为 + `COMPLETE`,blocked/in-progress/uninventoried 均为零。 +- [ ] `M23-08` binary/source/server archives 在独立机器复验。 +- [ ] `M23-09` SBOM、license、source offer、runtime 和 archive hash 联合校验。 +- [ ] `M23-10` 发布说明逐项列出 local/server 实现,不隐去架构差异。 +- [ ] `M23-11` 从空环境部署、升级、回滚并恢复真实项目。 +- [ ] `M23-12` 只有上述报告绑定同一 commit 后才允许“完整 Web Blender”声明。 + +## 12. 每轮执行规则 + +1. 从机器队列领取唯一 `nextTask`。 +2. 先运行现有正例,确认不是在旧失败上继续扩展。 +3. 写 fixture/golden 时实际启动锁定 Blender 5.2,不手工填写运行结果。 +4. 先完成协议和 fail-closed,再接 Main writer,再接浏览器 UI。 +5. 失败结果记录到工作日志,但不写成成功 evidence。 +6. 代码完成后运行专项;专项通过后运行全量 Node、typecheck、lint、build、status/evidence。 +7. 更新 ledger 时只增加本轮真实完成的 slice;family 全域状态保持 `BLOCKED` 直到 gap 为零。 +8. 每轮保留下一任务的可执行入口,不提前实现无依赖保证的后续功能。 diff --git a/docs/BLENDER_5_2_WEB_FEATURE_PARITY.md b/docs/BLENDER_5_2_WEB_FEATURE_PARITY.md index 2c665c55..72e562e9 100644 --- a/docs/BLENDER_5_2_WEB_FEATURE_PARITY.md +++ b/docs/BLENDER_5_2_WEB_FEATURE_PARITY.md @@ -3,7 +3,9 @@ 更新时间:2026-08-13 本文件以仓库内 `blender-5.2.0/source/blender/` 为基线,记录 Blender 功能域在 Web -项目中的迁移方式和唯一领取顺序。`completed_current_scope` 只表示声明子集通过验收, +项目中的迁移方式和当前差距。M12-M23 的唯一长期实施规则和原子任务位于 +`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md`;本文不再定义领取顺序。`completed_current_scope` +只表示声明子集通过验收, 不表示桌面 Blender 全量等价。任何未列入白名单的 operator、node、strip、physics 类型或数据块都必须返回机器可读阻断,不能由 Three.js 静默近似。 @@ -206,8 +208,8 @@ operator search;这不等价于 Blender 全量 operator registry、context men ## N-026 全域发布门 -1. 生成 machine-readable parity manifest:每个 Blender family 为 - `LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或 `BLOCKED`。 +1. 当前 V1 machine-readable manifest 保留 `LOCAL_EXACT`、`LOCAL_BOUNDED`、`SERVER` 或 + `BLOCKED`;完整对标 inventory 按新计划转换为独立 `implementationClass/parityStatus`。 2. 全部正例和阻断 fixture 在 Chromium 主线程/OffscreenCanvas、离线包和 Worker restart 下运行。 3. 100k/1M/10M 几何、4k/8k texture、100/1000 frame、长媒体和 simulation cache 基准。 4. `.blend`/image/media/script fuzz、OPFS quota、OOM、设备丢失、网络中断和恢复验证。 diff --git a/docs/CURRENT_EXECUTION_PLAN.md b/docs/CURRENT_EXECUTION_PLAN.md index 4b7b6523..65493484 100644 --- a/docs/CURRENT_EXECUTION_PLAN.md +++ b/docs/CURRENT_EXECUTION_PLAN.md @@ -18,9 +18,9 @@ React 工作区和编辑器 -> OPFS 大文件 + IndexedDB 元数据 ``` -本计划是当前唯一的短周期领取队列。长期 Blender 功能对标仍记录在 -`BLENDER_5_2_WEB_FEATURE_PARITY.md` 和 `status/parity-ledger.json`,但不以长期全域差距 -阻断已明确限定范围的 V1。 +本计划是当前唯一的短周期领取队列。M12-M23 的长期 Blender 全功能实施规范以 +`BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准;当前实现状态仍由 +`status/parity-ledger.json` 和对应 evidence 给出。长期全域差距不阻断已明确限定范围的 V1。 ## 2. 文档与事实源 @@ -29,7 +29,9 @@ React 工作区和编辑器 | `WEB_BLENDER_MODELER_V1_SCOPE.md` | V1 产品契约、支持矩阵、非目标、发布标准 | | `CURRENT_EXECUTION_PLAN.md` | 当前任务顺序、最小任务、依赖和退出条件 | | `PROJECT_STATUS_AND_NEXT_WORK.md` | 已实现能力、风险、验证命令总览 | -| `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 全域长期差距 | +| `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` | M12-M23 完整 Web 对标的长期规范事实源 | +| `BLENDER_5_2_WEB_FEATURE_PARITY.md` | Blender 5.2 功能域说明和当前差距参考 | +| `BLENDER_5_2_FULL_PARITY_WBS.md` | 完整产品覆盖检查表和历史 F 域映射参考 | | `status/parity-ledger.json` | 功能域和 V1 slice 的机器事实 | | `status/release-evidence.json` | 最近一次可审计发布证据快照 | @@ -156,8 +158,8 @@ loss、恶意 blend、zip bomb、离线包和 V1 用户闭环均已在最终 M5 | 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 或稳定阻断 | +| M11 Render/Compositor/Media | 灯光、渲染、合成、媒体执行边界 | 已完成,14/14 | 本地白名单、server 边界与三域故障恢复均可审计 | +| M12 Asset/IO/Editors | 资产、格式、编辑器和上下文工作流 | 进行中,M12-01A-I、M12-02A-H 与 M12-03A-D 完成 | 每个格式/editor 有独立 round-trip 或稳定阻断 | | M13 Scripting/Security | 脚本默认拒绝、服务端隔离、CSP、供应链 | 未开始 | 恶意输入矩阵和 release 安全门通过 | | M14 跨浏览器/设备 | Firefox、WebKit、触控、笔、HiDPI、IME | 未开始 | 新浏览器进入 quick/P0/full CI 后才声明支持 | | M15 全域审计 | Blender 5.2 全域差距和下一发布 | 未开始 | 逐 family 审计,不由聚合 release gate 反推完成 | @@ -790,7 +792,7 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。 - [x] `M10-14` 每类 cache 验证 source hash、frame range、字节预算和版本。 - [x] `M10-15` GN/Shader/NLA/Simulation 分别建立浏览器性能、OOM 和恶意图输入门。 -### M11 Lighting、Render、Compositor 与 Sequencer(13/14) +### M11 Lighting、Render、Compositor 与 Sequencer(14/14) - [x] `M11-01` Camera/Light/World/Scene color management 建立字段级 parity 表。 - [x] `M11-02` 每个支持字段通过 Main edit、undo、save/reopen 和 viewport 映射。 @@ -820,10 +822,16 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。 - [x] `M11-13` audio context suspend/resume、设备缺失和静音恢复有专项测试;schema 1 区分 context/output 状态,真实 Chromium `AudioContext` 通过用户手势、挂起、静音恢复、 二次挂起/恢复和 close,缺设备与 resume failure 保持结构化静音阻断。 -- [ ] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复。 +- [x] `M11-14` render/compositor/media 全部覆盖取消、重启、预算释放和恢复;schema 1 + 固定三域 cancellation/budget code 和资源归零字段,真实 `.blend`、H.264 与 WebGL 输出通过 + generation 2 重开、失败不提交、显式 clear/dispose 和恢复后 output SHA-256 门。 ### M12 Asset、IO、Editor 与工作流 +本节 M12-M15 仅保留短周期计划形成时的里程碑摘要,不再作为原子任务定义。M12-03D +机器证据完成后,唯一下一任务为 `BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 中的 +`M12-03E`;后续只能领取该文档或机器差距生成器给出的精确字母任务,不得领取下列整行摘要。 + - [ ] `M12-01` asset catalog schema migration 有向前/向后兼容 fixture。 - [ ] `M12-02` append/link/override 分别定义 stable ID、所有权和失效语义。 - [ ] `M12-03` library source 只允许声明 origin,路径穿越和符号链接逃逸被拒绝。 @@ -872,13 +880,15 @@ CI 可从 lockfile 和对应源码重现当前 binary/source hash。 - [ ] `M15-01` 每季度从 Blender 5.2 operator/data-block 清单重新生成 parity 差距。 - [ ] `M15-02` 每个 family 的 `COMPLETE` 必须没有 summary-only、proxy-success 或隐式 loss。 - [ ] `M15-03` LOCAL_EXACT 必须有相同 fixture 的 desktop/WASM 语义或像素 golden。 -- [ ] `M15-04` LOCAL_BOUNDED 必须同时验证白名单正例和非白名单稳定阻断。 -- [ ] `M15-05` SERVER 必须验证提交、取消、隔离、预算、hash 和结果绑定。 -- [ ] `M15-06` EXCLUDED 必须验证 UI 不开放、文件数据不丢失、保存不破坏原项目。 -- [ ] `M15-07` 12 个 family 分别达到自身退出条件,禁止由 N-026 聚合状态反推完成。 +- [ ] `M15-04` LOCAL_EQUIVALENT 只用于平台外壳,必须验证最终 Main/文件语义与 desktop 一致。 +- [ ] `M15-05` SERVER_EXACT 必须验证真实 Blender 提交、取消、隔离、预算、hash 和结果绑定。 +- [ ] `M15-06` BLOCKED/UNINVENTORIED 必须保持 UI fail-closed、文件保真和可诊断状态。 +- [ ] `M15-07` 全部生成的 owner family 分别达到自身退出条件,禁止由当前 12-family 或 N-026 + 聚合状态反推完整 Blender core 已完成。 - [ ] `M15-08` 全域发布候选重新跑 clean build、acceptance、性能、fault 和安全矩阵。 - [ ] `M15-09` 在独立机器复验 binary/source archive 和所有 hash。 -- [ ] `M15-10` 发布说明逐项列出支持、受限、server 和 excluded 能力。 +- [ ] `M15-10` 发布说明逐项列出 local exact、platform-equivalent 和 server exact 能力。 -实际领取顺序固定为:先完成 M5;随后 M6、M7;再按产品需求在 M8 至 M13 中一次选择一个 -最小闭环;M14 只在 Chromium RC 稳定后开始;M15 是状态审计,不是把剩余功能一次性打包。 +历史领取顺序为先完成 M5,随后 M6、M7,再在 M8-M11 中一次选择一个最小闭环。当前只领取 +机器 `nextTask`;M12 起使用完整对标计划的原子任务,M15 是状态审计和差距生成,不是把剩余 +功能一次性打包。 diff --git a/docs/PROJECT_STATUS_AND_NEXT_WORK.md b/docs/PROJECT_STATUS_AND_NEXT_WORK.md index 5b82481c..7e4c63d4 100644 --- a/docs/PROJECT_STATUS_AND_NEXT_WORK.md +++ b/docs/PROJECT_STATUS_AND_NEXT_WORK.md @@ -4,7 +4,8 @@ 当前短周期任务、领取顺序和阶段退出条件统一维护在 `docs/CURRENT_EXECUTION_PLAN.md`。本文件保留实现事实、长期能力台账和完整验收命令,不再作为 -V1 的逐项领取顺序;V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准。 +V1 的逐项领取顺序;V1 范围以 `docs/WEB_BLENDER_MODELER_V1_SCOPE.md` 为准,M12-M23 全功能 +实施规则以 `docs/BLENDER_5_2_FULL_WEB_PARITY_EXECUTION_PLAN.md` 为准。 ## 1. 当前结论 @@ -29,19 +30,21 @@ 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 台账 | 191 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 | +| slice 台账 | 195 completed | 58 blocked | blocked 项均留在 M9-M15 长期路线 | | acceptance | 50 declarations / 49 unique passed | 0 failed | 本地 V1 RC 证据完整 | | M6 可部署 RC | 71/71 原子任务 | 0 | 可部署 RC 已冻结 | | M7 核心体验硬化 | 18/18 原子任务 | 0 | 已完成并进入持续回归 | | M8 VDB 自动分页 | 20/20 原子任务 | 0 | 联合重开与 desktop/main/Offscreen 三轴 golden 通过 | | M9 非 Mesh/GP/Paint | 14/14 原子任务 | 0 | `TOGGLE_CYCLIC` 已通过 Main/undo/save/reopen 与 Blender 5.2 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 恢复门 | +| M11 Lighting/Render/Compositor/Media | 14/14 原子任务 | 0 | 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 恢复门;M11-14 已完成 Render/Compositor/Media 三域取消、重启、预算、释放和恢复门 | +| M12 Asset/IO/Editors | M12-01A-I + M12-02A-H + M12-03A-D 共 21 项 | Append undo/redo/save/reopen、Link/Override 与后续 IO/Editor 仍未完成 | WASM Main 已有单 transaction append slice,但 N-023 全域 parity 仍阻断 | 当前优先级不是扩 Blender 全域功能。single/pthread、真实 HTTP、缓存升级、离线闭环、 独立归档复验、运维 runbook、RC 文档和最终三条 CI lane 均已通过;M7 核心项目体验硬化 -18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 当前 -13/14;后续领取点只读取机器队列最新 `nextTask`。 +18/18 已完成并进入持续回归;M9 已完成 14/14;M10 已完成 15/15 并进入持续回归;M11 已完成 +14/14;M12-01A-I、M12-02A-H 和 M12-03A-D 已完成,机器队列的当前唯一 `nextTask` 为完整对标计划中的 +`M12-03E`。 ## 2. 已完成并有测试覆盖的能力 diff --git a/docs/status/M11-14.md b/docs/status/M11-14.md new file mode 100644 index 00000000..407e8a89 --- /dev/null +++ b/docs/status/M11-14.md @@ -0,0 +1,64 @@ +# M11-14 Status + +status: done +task: close render, compositor, and media cancellation/restart/resource recovery +updated: 2026-08-17 America/New_York + +## Scope + +Schema 1 evidence fixes one report for each of `RENDER`, `COMPOSITOR`, and `MEDIA`. +Every report binds real source bytes, a domain-specific cancellation code, a domain-specific +budget code, generation 1/2 identity and output hashes, positive release receipts, zero +owned resources after release, and a non-empty recovered output. The suite parser rejects +undeclared fields, missing or duplicate domains, published cancellation results, restart +drift, budget mutation, and incomplete release. + +The Chromium production-path case opens real Blender Main fixtures for Render and +Compositor and decodes a real H.264 fixture for Media. Render reads an actual WebGL RGBA8 +frame, Compositor executes the real Main GraphIR into Float32 output, and Media generates +the initial RGBA8 proxy frame. This task closes only bounded lifecycle behavior; it does not +claim full Eevee/Cycles parity, arbitrary compositor nodes, frame-exact long-video decode, +or complete audio/video rendering. + +## Evidence + +- `WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery` passed: + protocol negatives 3/3, Blender 5.2 Eevee reference checker, Blender 5.2 Compositor + golden checker, and Chromium 1/1. +- Render used `m11_render_reference.blend`, rejected a pre-cancelled open with + `OPEN_CANCELLED`, preserved the active Main, restarted WebEngine at generation 2, and + reproduced the same scene identity and WebGL output SHA-256. Aggregate texture overflow + returned `GPU_TEXTURE_BUDGET_EXCEEDED` without replacing the retained texture; dispose + reported positive released bytes/resources followed by zero owned textures. +- Compositor used `m11_compositor_allowlist.blend`, returned `COMPOSITOR_CANCELLED` from the + periodic executor check, rejected width 8193 with `COMPOSITOR_BUDGET_EXCEEDED` before + cache mutation, reproduced GraphIR/Float32 output after Worker restart, and cleared the + LRU to zero. +- Media probed and decoded `sequencer-probe.mp4`, rejected a pre-cancelled proxy decode with + `SEQUENCER_CANCELLED`, rejected a cache one byte below the frame size with + `SEQUENCER_BUDGET_EXCEEDED`, cleared the first cache, then re-probed and re-decoded the + H.264 source before reproducing the source/decode/profile identity and RGBA8 payload + SHA-256 in a rebuilt session. +- Regression passed: full Node 193/193, typecheck, lint, production build (75 modules), + render resource budget unit 3/3 plus Chromium main/Offscreen 3/3, Compositor golden unit + 3/3 plus Chromium 1/1, media proxy cache unit 3/3 plus Chromium 1/1, status consistency, + release evidence, and `git diff --check`. + +## Artifact Hashes + +- evidence protocol: `a1b0b36cf93173347d55a955ff2e02f3666f7c28ef7f06442b86f0ee2b272286` +- Chromium production suite: `ef9cd58ce3bbd522c72075e82d6423387b61039509996851d8d5ebf3a0d86789` +- media proxy cache: `7a1dbf8e838c47e2b99bc8932997e79d3c3cf2636b6285ace083800d833e2e64` +- GPU texture store: `a87adefa67b243cfd0cfcad0573a1b646f1e8be8abd0a187e1dc29cab2954d44` +- compositor cache: `3ce4819dadb2d6de7305c3676b2708689cd8be95e6fa7894651d6a02c4658b31` +- unit: `3cec4a33f28a76423a9678bcb61244cc463f5f2f5939c3f0b6865d34063e1f55` +- Chromium spec: `1578848eda7cb26cdf1584fa13f4cc2e252e782959d489dc4454e45c8d06595a` +- golden manifest: `87f17b0437d819e00c4a0902146d6549cf713482667ead627a45ba2b46050247` +- package: `0e731a5c826a999eef372f875c6c5ce9d697fd369a929025370a74cf18a93582` + +## Rollback + +Remove the recovery protocol, browser suite, unit/Chromium tests, M11-14 golden manifest, +and package command. Restore the media proxy generator signature, Compositor cache clear, +and GPU texture release receipt only if no later caller depends on them. Remove the three +family lifecycle slices, restore M11 to 13/14, and move the queue back from M12-01A. diff --git a/docs/status/M12-01A.md b/docs/status/M12-01A.md new file mode 100644 index 00000000..3cbc2427 --- /dev/null +++ b/docs/status/M12-01A.md @@ -0,0 +1,59 @@ +# M12-01A Status + +status: done +task: inventory Blender 5.2 AssetMetaData, tag, and catalog fields +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The machine inventory freezes the Blender 5.2 storage and public API surface needed before +designing a full-parity Web catalog schema. It covers every `AssetMetaData` DNA member, +the `AssetTag` storage and collection operations, actual `AssetMetaData` RNA properties, +Blend read/write participation, `AssetCatalog` semantic/runtime fields, catalog definition +file version/record order, path hierarchy rules, and the current Web v1 interface snapshot. + +This task deliberately does not create the desktop catalog fixture, Web schema v2, +migrations, IndexedDB behavior, or a new verified parity slice. In particular, a source +inventory is not evidence that Web preserves or writes the inventoried fields. + +## Findings + +- `AssetMetaData` has 14 DNA members: 10 persisted semantic/UI members, custom + `IDProperty` metadata, one runtime type pointer, one derived tag count, and ABI padding. + The real Blender runtime exposes 10 RNA properties. +- `author`, `description`, `copyright`, and `license` are optional dynamic strings. Empty + values are valid Blender state; the existing Web v1 manifest currently requires non-empty + author/license and has no description/copyright fields. +- `catalog_id` is the authoritative RFC4122 identity. `catalog_simple_name` is a read-only + recovery copy and cannot replace the UUID-to-path mapping. +- Catalog hierarchy is implicit in a cleaned UTF-8 slash path. CDF v1 records + `UUID:path:simple_name`; no parent UUID is stored. Duplicate paths are legal and resolve + deterministically by first-loaded state and UUID. +- Existing Web v1 directly represents catalog ID, tags, author, and license, but misses seven + inventoried metadata semantics and models hierarchy as an explicit `parentId` graph. + +## Evidence + +- `npm --prefix web run test:asset-catalog-inventory` passed on the first complete run: + `dna=14 rna=10 tag=3 catalog=3 gaps=7 next=M12-01B`. +- The checker binds eight source SHA-256 values, extracts the RNA property declarations, + launches `Blender 5.2.0 LTS --factory-startup --background`, compares the actual runtime + property identifiers/types/read-only flags, verifies Blend IO markers and CDF v1 format, + and uses the TypeScript AST to freeze the current Web interfaces. +- The inventory manifest SHA-256 is + `194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936`; + the checker SHA-256 is + `38839f47dbe82ad7931e5e444e124e87b64a604d9358be6d71cce7769439e57b`. + +## Next Task + +`M12-01B`: generate a desktop Blender 5.2 catalog v1 fixture and canonical JSON from the +inventoried fields. Do not begin Web schema v2 until that desktop artifact is verified. + +## Rollback + +Remove the inventory manifest, checker, package command, and this status entry; restore +M12-01A to pending and move `nextTask` back from M12-01B. No parity ledger rollback is +needed because this enabling task did not change a family slice. diff --git a/docs/status/M12-01B.md b/docs/status/M12-01B.md new file mode 100644 index 00000000..d1aa99b2 --- /dev/null +++ b/docs/status/M12-01B.md @@ -0,0 +1,55 @@ +# M12-01B Status + +status: done +task: generate a Blender 5.2 catalog v1 fixture and canonical desktop report +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The locked Blender 5.2 runtime generates a catalog definition v1 file and a `.blend` +containing Object, Material, and World assets. A separate Blender process reopens the +fixture and exports one canonical JSON report from the inventoried AssetMetaData fields. +The report covers catalog hierarchy, assigned and nil catalog UUIDs, optional empty text, +ordered tags, active tag, preferred import method, custom ID properties, and Blender's +automatically generated Object `dimensions` metadata. + +This is an enabling desktop baseline. It does not define Web schema v2, migration behavior, +stable Web asset IDs, or catalog mutation parity, and it does not change a ledger slice. +The read-only `catalog_simple_name` remains empty because Blender's RNA catalog UUID setter +clears it outside an Asset Browser context; the fixture records that behavior rather than +inventing a recovery name. + +## Evidence + +- `npm --prefix web run test:asset-catalog-v1-fixture` passed from a fresh temporary + directory: `catalogs=3 assets=3 metadata=complete next=M12-01C`. +- The checker verified the locked Blender version and every artifact hash, regenerated the + CDF and `.blend`, opened the generated file in a second Blender process, and compared its + canonical report exactly with the checked-in report. It then reopened the checked-in + fixture in another Blender process and repeated the exact comparison. +- Catalog definition v1 contains three deterministic UUID/path/simple-name records, including + one nested path. The canonical report contains three assets across MATERIAL, OBJECT, and + WORLD, preserves tag order and custom metadata, and records an uncataloged nil UUID. + +## Artifact Hashes + +- generator: `4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307` +- canonical exporter: `3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41` +- checker: `c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b` +- desktop `.blend`: `8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f` +- catalog definition: `a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98` +- canonical report: `6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e` +- manifest: `1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd` + +## Next Task + +`M12-01C`: define Web catalog schema v2, stable catalog/asset identity, and bounded text and +collection limits from the verified desktop baseline. + +## Rollback + +Remove the M12-01B generator, exporter, checker, fixture, CDF, canonical report, manifest, +package command, and this status entry. Restore M12-01B to pending and `nextTask` to +M12-01B. No parity ledger rollback is required. diff --git a/docs/status/M12-01C.md b/docs/status/M12-01C.md new file mode 100644 index 00000000..06bfbbce --- /dev/null +++ b/docs/status/M12-01C.md @@ -0,0 +1,50 @@ +# M12-01C Status + +status: done +task: define Web catalog schema v2, stable identities, and budgets +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Schema v2 represents Blender catalogs with canonical RFC4122 UUIDs and cleaned slash paths; +hierarchy is derived from `parentPath`, matching CDF semantics instead of retaining the Web v1 +`parentId` graph. Asset identity binds Blender's `AssetWeakReference` fields +(`asset_library_identifier`, `relative_asset_identifier`) into a SHA-256 Web ID. A Blender +rename changes the relative identifier and therefore the stable ID, matching Blender's weak +reference behavior rather than promising an identity Blender does not provide. + +All text budgets use UTF-8 bytes. Catalog/tag recovery strings keep Blender's 63-byte payload +limit, dynamic metadata permits empty strings, and catalog, asset, tag, custom-property, +custom-array, per-string, and aggregate-text counts are bounded. The positive schema fixture +is generated from the verified M12-01B desktop canonical report through production protocol +code. This task does not implement v1 migration, legacy-reader policy, IndexedDB migration, or +catalog mutation. + +## Evidence + +- `npm --prefix web run test:asset-catalog-v2` passed 4/4. +- The suite binds the desktop canonical report and Blender 5.2 weak-reference source, regenerates + schema v2 from the desktop report, parses the generated golden, and verifies deterministic + local/external identity hashes and exposed UTF-8/collection budgets. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7` +- generator: `b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881` +- unit suite: `73038022124ddc7e683602b47a4804297b3a64ecd4819160c7ec086d2f6190ae` +- schema golden: `2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929` +- manifest: `612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9` +- Blender weak-reference source: `ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635` + +## Next Task + +`M12-01D`: define and verify schema v1 to v2 forward migration. + +## Rollback + +Remove the schema v2 protocol, generator, unit suite, M12-01C golden/manifest, package command, +and this status entry. Restore M12-01C to pending and `nextTask` to M12-01C. No parity ledger +rollback is required. diff --git a/docs/status/M12-01D.md b/docs/status/M12-01D.md new file mode 100644 index 00000000..7ef97528 --- /dev/null +++ b/docs/status/M12-01D.md @@ -0,0 +1,48 @@ +# M12-01D Status + +status: done +task: define and verify schema v1 to v2 forward migration +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The forward migration first validates the current production schema v1 manifest and rejects +undeclared fields before conversion. Canonical UUIDs are preserved; legacy catalog IDs map +deterministically through RFC 4122 UUIDv5 with a fixed namespace. The v1 parent graph becomes +Blender cleaned slash paths, and assets receive SHA-256 IDs from Blender weak-reference identity. + +Logical revision is preserved. Author/license/tag/source bindings, optional source paths, +preview receipts, and the complete library dependency graph survive migration. Metadata absent +from v1 receives explicit defaults recorded in the migration report. The report binds canonical +source/target hashes and every legacy-to-v2 catalog/asset identity mapping. + +## Evidence + +- `npm --prefix web run test:asset-catalog-migration` passed 3/3. +- A schema v1 fixture with three catalogs, two asset kinds, one preview, two source bindings, + and two dependent libraries migrated to the checked-in schema v2 golden and exact report. +- Repeating the migration on a cloned source produced byte-equivalent semantic objects and the + same source/target SHA-256 values. A canonical source UUID remained unchanged. +- M12-01C regression, typecheck, lint, and `git diff --check` passed. + +## Artifact Hashes + +- migration protocol: `6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226` +- generator: `954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60` +- unit suite: `6b927e7a8238e4e0a77cc0fdf783b099d8add22c5f875df2299012eb32730186` +- v1 fixture: `1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5` +- v2 fixture: `38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb` +- migration report: `57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a` +- manifest: `fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d` + +## Next Task + +`M12-01E`: define the read-only/block policy when a schema v1 reader encounters schema v2. + +## Rollback + +Remove the migration protocol, generator, unit suite, M12-01D fixtures/report/manifest, package +command, and this status entry. Restore M12-01D to pending and `nextTask` to M12-01D. No parity +ledger rollback is required. diff --git a/docs/status/M12-01E.md b/docs/status/M12-01E.md new file mode 100644 index 00000000..a7c3e96f --- /dev/null +++ b/docs/status/M12-01E.md @@ -0,0 +1,45 @@ +# M12-01E Status + +status: done +task: define schema v2 behavior for a schema v1 reader +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +A native schema v1 document remains `READY`. A valid schema v2 document exposed to a v1 +reader yields a bounded `READ_ONLY` snapshot for metadata inspection, while catalog writes, +asset writes, and save all return `ASSET_SCHEMA_DOWNGRADE_BLOCKED`. Unknown future schema +versions return non-recoverable `PROTOCOL_MISMATCH` with no snapshot. + +Every result binds a canonical source SHA-256. Inspection does not mutate or downgrade the +source document, and no write path can serialize the lossy read-only projection. This task +defines compatibility behavior only; it does not implement IndexedDB migration or catalog +mutation parity. + +## Evidence + +- `npm --prefix web run test:asset-catalog-legacy-reader` passed 4/4. +- Generated evidence covers v2 read-only metadata, all three write/save blocks, native v1 + readiness, future-schema rejection, equal source hashes, and source object immutability. +- M12-01C/D regressions, typecheck, lint, and `git diff --check` passed. + +## Artifact Hashes + +- compatibility protocol: `5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349` +- error contract: `309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34` +- generator: `208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e` +- unit suite: `f6477222412044e6784b64c1be83dc9bd6050fe6a955ba5430a4ea963b20dbb4` +- compatibility report: `3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54` +- manifest: `cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b` + +## Next Task + +`M12-01F`: add duplicate ID, cyclic parent catalog, overlong text, and unknown-field negatives. + +## Rollback + +Remove the compatibility protocol, generator, unit suite, M12-01E report/manifest, error code, +package command, and this status entry. Restore M12-01E to pending and `nextTask` to M12-01E. +No parity ledger rollback is required. diff --git a/docs/status/M12-01F.md b/docs/status/M12-01F.md new file mode 100644 index 00000000..4b490827 --- /dev/null +++ b/docs/status/M12-01F.md @@ -0,0 +1,40 @@ +# M12-01F Status + +status: done +task: reject duplicate IDs, catalog cycles, oversized text, and unknown fields +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The negative matrix executes production schema v2 and migration parsers against duplicate +catalog/asset IDs, a legacy parent cycle, an inconsistent v2 `parentPath`, 64-byte UTF-8 +simple-name/tag values, and unknown top-level/nested fields in both v1 and v2. The byte cases +use multi-byte text to prove limits are not JavaScript character counts. + +Every failure returns a stable asset error code. The valid v2 fixture is parsed after each +negative case to prove the failed input did not poison parser state. This task adds negative +evidence only and does not change catalog parity status. + +## Evidence + +- `npm --prefix web run test:asset-catalog-negatives` passed 2/2 with 9/9 negative cases. +- Duplicate/cycle/parent/unknown cases returned `ASSET_MANIFEST_INVALID`; both 64-byte UTF-8 + payloads returned `ASSET_BUDGET_EXCEEDED`. +- Typecheck, lint, and `git diff --check` passed. + +## Artifact Hashes + +- unit suite: `3a11605ac8666c8c4e1e2e85ccfd423115915820d0de6f92bdf72dff1f01ce41` +- negative cases: `74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874` +- manifest: `127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d` + +## Next Task + +`M12-01G`: prove an IndexedDB migration failure preserves the old transaction and index. + +## Rollback + +Remove the M12-01F negative suite, golden/manifest, package command, and this status entry. +Restore M12-01F to pending and `nextTask` to M12-01F. No parity ledger rollback is required. diff --git a/docs/status/M12-01G.md b/docs/status/M12-01G.md new file mode 100644 index 00000000..ce709a29 --- /dev/null +++ b/docs/status/M12-01G.md @@ -0,0 +1,47 @@ +# M12-01G Status + +status: done +task: preserve the old catalog index when an IndexedDB migration fails +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The production IndexedDB migration prepares schema v2 with the existing schema v1 parser and +migrator before opening a write transaction. It then rechecks the source row and atomically +writes the v2 index and migration receipt while deleting the v1 row. A concurrent source change +returns `REVISION_CONFLICT`; a transaction failure returns `STORAGE_TRANSACTION`. + +The browser suite injects failures after the v2 target write and after the v1 source delete. +After each abort it closes and reopens the database, proving the database version, complete v1 +index, unrelated legacy migration row, absent v2 row, and absent receipt are byte-for-byte +equivalent to the baseline. A subsequent retry commits the v2 index and receipt together. + +## Evidence + +- `WEB_TEST_PORT=5593 npm --prefix web run test:asset-catalog-indexeddb-migration` passed the + M12-01D production migration unit suite 3/3, the M12-01G artifact binding unit 1/1, and + Chromium 1/1. +- Both `AFTER_TARGET_PUT` and `AFTER_SOURCE_DELETE` returned `STORAGE_TRANSACTION`; reopening + IndexedDB after either fault produced the exact pre-migration snapshot. +- The successful retry preserved revision 7 and the M12-01D source/target hashes, removed only + the v1 row, and retained the unrelated migration row. +- Typecheck and `git diff --check` passed. + +## Artifact Hashes + +- production IndexedDB migration: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57` +- browser suite: `82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9` +- artifact unit: `a4366a16f28b91b3ff213e0232308eaecd392731f27972cfa49a11568eb0feca` +- manifest: `2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7` + +## Next Task + +`M12-01H`: prove catalog ordering and asset identity survive page and Worker restarts. + +## Rollback + +Remove the IndexedDB migration module, M12-01G unit/browser suites, manifest, package command, +and this status entry. Restore M12-01G to pending and `nextTask` to M12-01G. No parity ledger +rollback is required. diff --git a/docs/status/M12-01H.md b/docs/status/M12-01H.md new file mode 100644 index 00000000..77443b14 --- /dev/null +++ b/docs/status/M12-01H.md @@ -0,0 +1,47 @@ +# M12-01H Status + +status: done +task: preserve catalog order and asset identity across page and Worker restarts +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The production catalog index loader validates the stored schema v2 manifest, recomputes its +canonical SHA-256, and compares it with the committed migration receipt before publishing a +snapshot. The snapshot exposes persisted catalog order, asset order, stable asset IDs, Blender +weak-reference components, revision, and manifest identity. + +A page migrates the M12-01D v1 fixture, reloads, and reads the same database again. Two separate +module Workers then open the database in sequence, with generation 1 terminated before +generation 2 starts. All four contexts must return the same complete snapshot. + +## Evidence + +- `WEB_TEST_PORT=5594 npm --prefix web run test:asset-catalog-restart` passed Node 5/5 and + Chromium 2/2, including the full M12-01G abort/reopen regression. +- Initial page, reloaded page, Worker generation 1, and Worker generation 2 returned exactly the + same 3 catalog IDs in order, 2 asset IDs in order, weak-reference components, revision 7, and + manifest SHA-256. +- A stored receipt whose target hash or revision drifts from the parsed v2 index is rejected as + `ASSET_MANIFEST_INVALID` before a snapshot is published. +- Typecheck, lint, and `git diff --check` passed. + +## Artifact Hashes + +- production IndexedDB reader: `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57` +- restart Worker: `e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e` +- browser suite: `7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b` +- artifact unit: `eb22b5cc852fa47d9bdd30afec2977facfca0dfa554e7ce738dc1bf8c4bf2122` +- manifest: `59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae` + +## Next Task + +`M12-01I`: bind the schema fixture, migration report, and hashes into M12-01 evidence. + +## Rollback + +Remove the restart snapshot reader, Worker/browser/unit suites, M12-01H manifest, package +command, and this status entry. Restore M12-01H to pending and `nextTask` to M12-01H. No parity +ledger rollback is required. diff --git a/docs/status/M12-01I.md b/docs/status/M12-01I.md new file mode 100644 index 00000000..52be4086 --- /dev/null +++ b/docs/status/M12-01I.md @@ -0,0 +1,44 @@ +# M12-01I Status + +status: done +task: bind the schema fixture, migration report, and hashes into M12-01 evidence +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The M12-01 aggregate evidence binds all eight predecessor manifests in exact queue order and +recursively verifies their declared source and artifact hashes. It separately binds the locked +Blender 5.2 desktop canonical fixture, Web schema v2 golden and protocol, schema v1/v2 migration +fixtures, production migration report, IndexedDB transaction implementation, and restart Worker. + +The checker recomputes canonical source and target manifest SHA-256 values, matches them to the +migration report, verifies revision and preserved counts, and compares the persisted restart +catalog/asset order with the target fixture. This closes the schema/migration evidence section; +it does not claim catalog mutation or preview parity. + +## Evidence + +- `WEB_TEST_PORT=5595 npm --prefix web run test:asset-catalog-m12-evidence` passed the Blender + 5.2 inventory and regenerated fixture checks, schema v2 4/4, migration 3/3, legacy-reader 4/4, + negatives 2/2, transaction/restart Node 5/5, Chromium 2/2, and final aggregate checker. +- The final checker reported `subtasks=8 catalogs=3 assets=2 next=M12-02A`. +- Source revision 7 and target revision 7 canonical hashes match the production migration report; + all A-H task links and recursive artifact hashes resolve without gaps. +- Typecheck, lint, status consistency, and `git diff --check` passed. + +## Artifact Hashes + +- M12-01 evidence: `2b99f64a207c2ffb966963be8a2150feef67d5bf78946939d3a49a6335b55bc3` +- aggregate checker: `7d0d9f5390648d4af79a8da6226627edb1bf99945bae1a7b1acfb2c5e2419a8b` +- package command: `a810fec6a9a24cad94cb01475f9529ac2f85da827d391a70ab7ddb57421003f8` + +## Next Task + +`M12-02A`: inventory Blender preview image dimensions, color space, and no-preview state. + +## Rollback + +Remove the M12-01 aggregate evidence, checker, package command, and this status entry. Restore +M12-01I to pending and `nextTask` to M12-01I. No parity ledger rollback is required. diff --git a/docs/status/M12-02A.md b/docs/status/M12-02A.md new file mode 100644 index 00000000..e3c88f02 --- /dev/null +++ b/docs/status/M12-02A.md @@ -0,0 +1,43 @@ +# M12-02A Status + +status: done +task: inventory Blender preview dimensions, color semantics, and no-preview state +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The machine inventory binds Blender 5.2 PreviewImage DNA, slot enum, BKE/RNA implementation, +Blend reader, asset preview generator, render-size constants, current Web v1/v2 preview protocols, +and two runtime fixtures. It records both ICON and PREVIEW slots, persisted dimensions/flags/pixel +arrays, runtime-only state, and absent-pointer behavior. + +Preview pixels are packed 32-bit RGBA with four byte components and Blender's deferred load path +premultiplies alpha. PreviewImage has no persisted or RNA color-space field, and float access only +divides byte components by 255 without a color transform. The inventory therefore records color +space as unknown instead of assuming sRGB. + +## Evidence + +- `npm --prefix web run test:asset-preview-inventory` launched locked Blender 5.2 twice and + reported `slots=2 rna=9 absent=3 loaded=8x8 next=M12-02B`. +- The checked-in asset fixture reopened with Object, Material, and World `preview == null`. +- A real 8x8 PNG loaded through Blender's preview API produced an 8x8 PREVIEW slot, 32x32 ICON + slot, 64/256 image packed/float counts, and 1024 icon packed pixels. +- Typecheck, lint, status consistency, and `git diff --check` passed. + +## Artifact Hashes + +- inventory: `a4427426d94afdf1a2ccf5379d76d1797f8604a742a3f0d15bdca629272864bd` +- checker: `c9cc83d21bac5f34c6d68cb1225dffcd92a5ec76c158ddce909b620cbbcb245c` +- package command: `04f832a3c5773c8cc5294ecdcf175478eed9710496e48c63698d79043384e328` + +## Next Task + +`M12-02B`: define preview source/content SHA-256, MIME, dimensions, and generator identity. + +## Rollback + +Remove the M12-02A inventory, checker, package command, and this status entry. Restore M12-02A +to pending and `nextTask` to M12-02A. No parity ledger rollback is required. diff --git a/docs/status/M12-02B.md b/docs/status/M12-02B.md new file mode 100644 index 00000000..07779b5e --- /dev/null +++ b/docs/status/M12-02B.md @@ -0,0 +1,48 @@ +# M12-02B Status + +status: done +task: define preview source, content, dimensions, MIME, and generator identity +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The schema binds source bytes and encoded content as separate SHA-256/length/MIME identities. +Encoded content additionally binds width, height, RGBA8, explicit sRGB, and alpha mode. Generator +identity binds name, Blender version, executable hash, generator script hash, and canonical +settings hash. A domain-separated canonical hash covers the complete record. + +The locked Blender 5.2 generator re-encodes a real 8x8 PNG with explicit PNG RGBA8, compression, +display, view-transform, exposure, and gamma settings. The source and content encodings have +different hashes and lengths while a second Blender process confirms decoded pixels are exact. + +## Evidence + +- `npm --prefix web run test:asset-preview-identity` passed 3/3 protocol tests and the locked + Blender generator checker. +- Two independent generation runs matched the checked-in 513-byte content SHA-256 exactly; the + 261-byte source has a different SHA-256, and decoded pixel maximum error is zero. +- Source bytes, content bytes, generator settings, dimensions, identity hash, and unknown fields + each have fail-closed negative coverage. +- Typecheck and `git diff --check` passed. + +## Artifact Hashes + +- protocol: `2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438` +- generator: `753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce` +- checker: `4d8ece1a5440d202f4e79a48f1f26bc75b3fb11a02d821b8ebc5aee14b11e41a` +- unit suite: `5b7b3ccaf410d627365579fa82bf2b48fed9ef17144aad963cdbd4bb7fbd4b5a` +- identity golden: `1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b` +- content PNG: `ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7` +- manifest: `096f125461b963ee2c414f98f6bb00f02034a380a0cc9c43e0d84754324adb17` + +## Next Task + +`M12-02C`: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode. + +## Rollback + +Remove the preview identity protocol, generator/checker, unit suite, M12-02B goldens, package +command, error code, and this status entry. Restore M12-02B to pending and `nextTask` to M12-02B. +No parity ledger rollback is required. diff --git a/docs/status/M12-02C.md b/docs/status/M12-02C.md new file mode 100644 index 00000000..1404f8c4 --- /dev/null +++ b/docs/status/M12-02C.md @@ -0,0 +1,46 @@ +# M12-02C Status + +status: done +task: enforce MIME, pixel, byte, and compression-ratio budgets before preview decode +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The schema 1 pre-decode planner accepts only the M12-02B content identity and an exact +`ArrayBuffer` payload. It checks the 16 MiB encoded-byte limit before hashing, then binds the +content SHA-256, declared MIME to the PNG/WebP container header, header dimensions to the identity, +and dimensions to pixel and RGBA8 decoded-byte budgets. The final gate limits decoded-to-encoded +compression ratio to 100 before any browser image decoder or decoded allocation is invoked. + +PNG dimensions come only from a valid signature and first IHDR header. WebP dimensions come only +from a size-bound RIFF/WEBP VP8X, VP8L, or VP8 header. Unsupported headers, content drift, and +dimension drift fail closed; a rejected input does not affect a following valid preview plan. + +## Evidence + +- `npm --prefix web run test:asset-preview-decode-budget` passed 5/5 M12-02B/C protocol tests. +- The positive 513-byte Blender-generated PNG plans 64 pixels and 256 decoded RGBA8 bytes without + calling a decoder. +- Eight negative cases cover payload hash/length drift, MIME mismatch, dimension mismatch, + over-width/height and decoded-byte budget, compression ratio, encoded-byte budget, and corrupt + container signature. The checked-in small preview passes immediately after each rejection. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed. + +## Artifact Hashes + +- decode protocol: `3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63` +- unit suite: `1a65df86d568ae8defd7cd9242f426ac900d92637eb3bed2cbb4a0fae2e86c20` +- package commands: `8563916637fc3cb29b53432ae9b80d1dcf6269f2f5c092b595e72c6b0ada55ab` +- manifest: `934b3ca29d905ad2f5c46f35acff4cf4b9ea291c423fb3aad4afe7e9f10e6244` + +## Next Task + +`M12-02D`: commit a catalog preview reference only after its content-addressed payload is durably +written to OPFS. + +## Rollback + +Remove the preview decode planner, unit suite, M12-02C manifest, package command, and this status +entry. Restore M12-02C to pending and `nextTask` to M12-02C. No parity ledger rollback is required. diff --git a/docs/status/M12-02D.md b/docs/status/M12-02D.md new file mode 100644 index 00000000..cb5ad932 --- /dev/null +++ b/docs/status/M12-02D.md @@ -0,0 +1,49 @@ +# M12-02D Status + +status: done +task: commit a catalog preview reference only after verified OPFS persistence +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The production coordinator first runs the M12-02C pre-decode gate, writes the content-addressed +preview to OPFS, reads it back, and repeats identity verification. Only then may one IndexedDB +transaction advance the catalog revision and write both the v2 preview reference and an M12-02D +head receipt. The receipt binds project, asset, base/current revision, base/current manifest hash, +preview identity, payload identity, and OPFS path. + +The transaction rereads and compares the complete source catalog row before publication. A stale +revision or changed row returns `REVISION_CONFLICT`; OPFS and transaction faults return +`STORAGE_TRANSACTION`. An OPFS payload left by a failure is unreferenced, never presented as a +committed catalog preview. + +## Evidence + +- `WEB_TEST_PORT=5596 npm --prefix web run test:asset-preview-opfs-commit` passed the artifact unit + test 1/1 and the real Chromium OPFS/IndexedDB test 1/1. +- `BEFORE_OPFS_WRITE`, `AFTER_OPFS_WRITE`, and `AFTER_CATALOG_PUT` faults all preserved the complete + revision 7 catalog and left no commit receipt. Only the latter two left an unreferenced payload. +- Success persisted and reread the 513-byte payload before advancing the catalog to revision 8; + closing and reopening IndexedDB reproduced the same preview and receipt and rehashed OPFS bytes. +- `npm --prefix web run typecheck`, `npm --prefix web run lint`, and `git diff --check` passed. + +## Artifact Hashes + +- commit coordinator: `36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2` +- Chromium suite: `043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e` +- unit suite: `f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74` +- manifest: `34fe6c6ab097e58f7b648dd84cb919ecb407a5742e387c513bb3ed53db50eed6` +- package commands: `7363a8a03cb5f6b1d6bf40d8eabc90b9d48e91aeb4787b175deb4f242c676243` + +## Next Task + +`M12-02E`: deduplicate identical preview content without allowing different asset metadata to +overwrite the shared payload. + +## Rollback + +Remove the OPFS/catalog commit coordinator, Chromium and unit suites, M12-02D manifest, package +command, and this status entry. Restore M12-02D to pending and `nextTask` to M12-02D. No parity +ledger rollback is required. diff --git a/docs/status/M12-02E.md b/docs/status/M12-02E.md new file mode 100644 index 00000000..27edd655 --- /dev/null +++ b/docs/status/M12-02E.md @@ -0,0 +1,45 @@ +# M12-02E Status + +status: done +task: deduplicate identical preview content without merging asset metadata +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Two catalog assets with distinct Blender weak-reference identities and distinct catalog metadata +may bind the same verified preview content. The M12-02D coordinator keys OPFS storage only by the +content SHA-256, while catalog entries and preview identity hashes remain asset-specific. A second +commit of identical bytes reuses the existing payload after a full readback/hash check. + +No catalog metadata is derived from or stored inside the shared payload record. Each transaction +updates only its targeted asset's preview field and advances the guarded catalog revision. + +## Evidence + +- `WEB_TEST_PORT=5597 npm --prefix web run test:asset-preview-dedup` passed the artifact unit test + 1/1 and real Chromium OPFS/IndexedDB test 1/1. +- Two distinct asset/identity hashes committed at revisions 8 and 9; the second commit reported + `deduplicated=true` and the OPFS content directory contained exactly one final file. +- Both catalog entries referenced the same 513-byte/hash payload while their complete non-preview + metadata remained byte-for-byte equal to their different baseline records. +- Database reopen reproduced revision 9, the second asset head receipt, and the shared preview; + stored payload bytes rehashed to the declared content digest. + +## Artifact Hashes + +- Chromium suite: `4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40` +- unit suite: `e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835` +- manifest: `7d25f1d8335481795257c643c8589d1607a865634b27a7e5d145a57ce2a92a62` +- package commands: `5a83676e821bb768f6d3e282ea4728413cf84c5f3d57672fc664fac3883873e5` + +## Next Task + +`M12-02F`: quarantine corrupt preview content while keeping asset metadata readable. + +## Rollback + +Remove the deduplication Chromium/unit suites, M12-02E manifest, package command, and this status +entry. Restore M12-02E to pending and `nextTask` to M12-02E. The shared coordinator and parity +ledger do not require rollback. diff --git a/docs/status/M12-02F.md b/docs/status/M12-02F.md new file mode 100644 index 00000000..9727335f --- /dev/null +++ b/docs/status/M12-02F.md @@ -0,0 +1,49 @@ +# M12-02F Status + +status: done +task: quarantine corrupt preview content while keeping asset metadata readable +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The preview inspector validates payload byte length and SHA-256 independently from the catalog. +Healthy bytes return `READY`. Missing or mismatched bytes return a structured +`ASSET_SOURCE_HASH_MISMATCH` quarantine receipt; present corrupt bytes are copied, verified, and +then removed from the content-addressed namespace before the receipt is published. + +The catalog row and its preview provenance are not rewritten on corruption. Consumers receive the +complete parsed asset metadata together with `preview=null/data=null`, so a broken optional image +cannot make the asset name, type, catalog, license, tags, or source metadata unreadable. A matching +quarantine receipt is stable across database reopen. + +## Evidence + +- `WEB_TEST_PORT=5598 npm --prefix web run test:asset-preview-quarantine` passed the artifact unit + test 1/1 and real Chromium OPFS/IndexedDB test 1/1. +- The checked-in 513-byte PNG first returned `READY`; same-length byte tampering then returned + `QUARANTINED` with a different actual SHA-256 and stable error code. +- The corrupt file was absent from the SHA-256 namespace and present as a verified 513-byte file in + the quarantine directory. Database reopen returned the exact same receipt without reading it. +- Catalog revision 8, the target asset metadata/reference, and the unrelated asset all remained + exactly unchanged. + +## Artifact Hashes + +- quarantine protocol: `a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04` +- Chromium suite: `d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27` +- unit suite: `79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a` +- manifest: `d1a9feeb98c5332b1a14a14c5b813d75a29f172601590e1b5cde254c5059a7c5` +- package commands: `77b5cd3d3af68ac390d8fbdb9b2f64764fb4521a1d2bf22cd8a89e40d69424f6` + +## Next Task + +`M12-02G`: reclaim a preview payload only after its final project-scoped reference is removed, +without touching another project's copy. + +## Rollback + +Remove the quarantine protocol, Chromium/unit suites, M12-02F manifest, package command, and this +status entry. Restore M12-02F to pending and `nextTask` to M12-02F. Catalog and parity ledger data +do not require rollback. diff --git a/docs/status/M12-02G.md b/docs/status/M12-02G.md new file mode 100644 index 00000000..c8c58da7 --- /dev/null +++ b/docs/status/M12-02G.md @@ -0,0 +1,47 @@ +# M12-02G Status + +status: done +task: reclaim preview payload only after the final project-scoped reference is removed +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Preview reference removal is a guarded catalog mutation: it validates the v2 row, checks the exact +base revision, clears only the target asset's preview, advances revision, and commits a manifest +hash receipt. Payload reclamation happens only after that reference transaction has completed and +only when the committed manifest has zero remaining references to the content hash. + +OPFS payload ownership remains project-scoped (`projects//assets/sha256/...`). Therefore +collection can target only the requested project's file and cannot delete an equal-hash payload +owned and referenced by another project. Non-preview asset metadata is preserved. + +## Evidence + +- `WEB_TEST_PORT=5599 npm --prefix web run test:asset-preview-reference-gc` passed the artifact unit + test 1/1 and real Chromium OPFS/IndexedDB test 1/1. +- Project A began with two references to one deduplicated payload. Removing the first at revision + 9->10 returned `RETAINED`, one remaining reference, and kept the file. +- Removing the final reference at revision 10->11 returned `COLLECTED` and removed project A's + payload. Project B's equal-hash payload remained present and its preview returned READY/513 bytes. +- Reopening project A reproduced revision 11, two null preview references, and unchanged metadata. + +## Artifact Hashes + +- reference GC: `76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e` +- Chromium suite: `2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a` +- unit suite: `c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e` +- manifest: `3449f2ea9488f66ea5e3f5f2423413e8e290d8ffed3b3d941bdeee3f76addd0a` +- package commands: `7d1db0932fbe7405ffdbd44b770ce7bf9815b9dfb829c1906a06bfff635ae702` + +## Next Task + +`M12-02H`: compare desktop/browser preview pixels with explicit metrics and pass both main-thread +and Offscreen display paths. + +## Rollback + +Remove the reference-GC protocol, Chromium/unit suites, M12-02G manifest, package command, and this +status entry. Restore M12-02G to pending and `nextTask` to M12-02G. Catalog and parity ledger data +do not require rollback. diff --git a/docs/status/M12-02H.md b/docs/status/M12-02H.md new file mode 100644 index 00000000..58593694 --- /dev/null +++ b/docs/status/M12-02H.md @@ -0,0 +1,51 @@ +# M12-02H Status + +status: done +task: compare desktop/browser preview pixels and pass main-thread and Offscreen display paths +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: true + +## Scope + +The display path validates the content identity, encoded SHA-256, image header, dimensions, decoded +byte budget, and compression ratio before invoking a decoder. It then decodes with disabled browser +color conversion and alpha premultiplication, draws with copy composition and no smoothing, and +publishes a receipt bound to the identity, content, backend, dimensions, RGBA8 hash, and alpha count. + +Blender 5.2 deterministically regenerates the 8x8 `SRGB/RGBA8/STRAIGHT` reference. Main-thread +Canvas2D and a dedicated Worker OffscreenCanvas execute the same production display implementation +and match all 256 reference bytes. This closes only the bounded preview display slice; arbitrary +profiles, Asset Browser UI, and library operations remain outside this task. + +## Evidence + +- `WEB_TEST_PORT=5600 npm --prefix web run test:asset-preview-display` passed the evidence unit 1/1, + Blender 5.2 desktop regeneration/hash checker, and Chromium main-thread/Offscreen test 1/1. +- Desktop, main-thread, and Offscreen output SHA-256 was + `c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00`. +- Both browser paths reported 8x8, 256 RGBA8 bytes, 64 non-transparent pixels, and closed bitmap + ownership. Their MAE, RMS, P95, maximum channel error, bad-pixel ratio, and alpha coverage delta + were zero; foreground IoU was 1. +- A one-channel mutation returned `BLOCKED/RENDER_REFERENCE_MISMATCH` under the same metric contract. + +## Artifact Hashes + +- display runtime: `e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28` +- Offscreen Worker: `6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f` +- desktop checker: `d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54` +- desktop report: `cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0` +- Chromium suite: `340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520` +- unit suite: `f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873` +- manifest: `30e6e4b6f53e07768d2ae8d853fe08a5c0782f8608320a72e41db77f6d87f2c4` +- package commands: `2510242fbc0c2da787f558365733a4db6faf9f490e2594f69dc978920ed09f21` + +## Next Task + +`M12-03A`: inventory the data-block and dependency closures for append, link, and library override. + +## Rollback + +Remove the display runtime, Worker, desktop checker/report, unit/Chromium suites, M12-02H manifest, +package command, completed ledger slice, and this status entry. Restore M12-02H to pending and the +queue to M12-02H. diff --git a/docs/status/M12-03A.md b/docs/status/M12-03A.md new file mode 100644 index 00000000..ed0d1948 --- /dev/null +++ b/docs/status/M12-03A.md @@ -0,0 +1,55 @@ +# M12-03A Status + +status: done +task: inventory append, link, and library override data-blocks and dependency closures +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +The machine inventory separately records Append, Link, and Library Override entry points, +selectable data-block roots, non-root ID roles, direct/indirect/transitive/embedded dependency +classes, terminal ownership actions, and override reference/hierarchy/property-operation fields. + +The exact closure is an actual Blender Main ID pointer graph walked by +`BKE_library_foreach_ID_link`, not a hard-coded Object-to-Mesh table. The inventory therefore +preserves cross-library and override-only dependency distinctions needed by later ownership and +invalidation contracts. It does not perform a library mutation or add a parity slice. + +## Findings + +- Blender 5.2 exposes 36 selectable library data-block collections. Screen and WorkSpace are + append-only; Library is source/transitive metadata, while embedded Key IDs follow their owner. +- Append can keep linked, reuse local, make local, or copy local after classifying direct, + indirect, and liboverride-only dependencies. Recursive cross-library localization is explicit. +- Link retains the source `Library`, direct `ID_TAG_EXTERN`, indirect `ID_TAG_INDIRECT`, and + transitive parent-library relationships without localization. +- Override retains a linked reference alongside a local override and hierarchy root. Its 17 RNA + fields cover reference/root flags, property paths, operations, and local/reference subitems. +- Current Web metadata represents only library source, dependency IDs, and read-only state; eight + per-ID ownership, override, and invalidation semantics remain absent and mutation stays blocked. + +## Evidence + +- `npm --prefix web run test:library-operation-inventory` passed with + `roots=36 appendOnly=2 operations=3 overrideFields=17 gaps=8 next=M12-03B`. +- The checker binds nine Blender/Web source files and one real nested-library fixture by SHA-256, + verifies link/append action and dependency-walk tokens, launches Blender 5.2, and compares the + actual append/link operator properties, library collections, nested source, and override RNA. + +## Artifact Hashes + +- inventory: `0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7` +- checker: `1581490d1923578293202d27a1c379eb2a8c5e13be418547d148cf09c6a783fd` +- package commands: `a7cf339b0cf6fab17ef63e9394b2e540af60327f7bf1f7fe95823d182e0d3e63` + +## Next Task + +`M12-03B`: define source library ID, owner, read-only state, and invalidation token for all three +operations. + +## Rollback + +Remove the library operation inventory, checker, package command, and this status entry. Restore +M12-03A to pending and the queue to M12-03A. No parity ledger rollback is required. diff --git a/docs/status/M12-03B.md b/docs/status/M12-03B.md new file mode 100644 index 00000000..4c0ceca1 --- /dev/null +++ b/docs/status/M12-03B.md @@ -0,0 +1,47 @@ +# M12-03B Status + +status: done +task: define source library identity, owner, read-only state, and invalidation tokens +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Schema 1 derives `sourceLibraryId` from a canonical source locator and source `.blend` SHA-256. +Each operation binding then fixes one source data-block, its operation-specific owner and read-only +state, source generation/revision, dependency-closure SHA-256, and a hash-derived invalidation token. + +APPEND owns a writable local Main ID. LINK remains owned by the matching source library and is +fully read-only. LIBRARY_OVERRIDE owns a writable local override while retaining a read-only linked +reference and hierarchy root. This is a pure protocol task and does not mutate Blender Main. + +## Evidence + +- `npm --prefix web run test:library-operation-identity` passed 6/6. +- Stable source identity changes when either locator or source bytes change. +- APPEND/LINK/LIBRARY_OVERRIDE produce distinct invalidation tokens for the same source root and + reject owner substitution, read-only drift, reference substitution, unknown fields, and forgery. +- Source library/hash, generation, revision, and dependency closure drift each return + `REVISION_CONFLICT`; the unchanged state revalidates the original binding exactly. +- The one initial test expectation was corrected to the actual fail-fast exact-schema code, then + the complete suite was rerun successfully. + +## Artifact Hashes + +- protocol: `5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451` +- golden bindings: `b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f` +- unit suite: `0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547` +- manifest: `f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b` +- package commands: `48cf41f0d0041bf4f497bc2a454cfa9469874eded1775cea50fe3249d4e0b59e` + +## Next Task + +`M12-03C`: create a desktop single-Object append fixture and record stable Object, Mesh, Material, +and Image mappings. + +## Rollback + +Remove the library operation identity protocol, golden, unit suite, manifest, package command, and +this status entry. Restore M12-03B to pending and the queue to M12-03B. No parity ledger rollback is +required. diff --git a/docs/status/M12-03C.md b/docs/status/M12-03C.md new file mode 100644 index 00000000..a7c2a938 --- /dev/null +++ b/docs/status/M12-03C.md @@ -0,0 +1,50 @@ +# M12-03C Status + +status: done +task: create a desktop single-Object append fixture and stable dependency mapping +updated: 2026-08-17 America/New_York +enablingTask: true +parityStateChange: false + +## Scope + +Blender 5.2 creates a source library containing one Object whose dependency closure is one Mesh, +one Material, and one packed Image. A clean desktop Main appends only the Object, saves the target, +reopens it, and exports the same canonical graph before and after reopen. + +The fixture records stable source-to-local mappings, ownership, read-only state, dependency edges, +geometry/UV/material structure, and packed-image metadata and Float32 pixel SHA-256. Container hashes +remain session-bound Blender artifacts; the canonical semantic report must reproduce exactly. + +## Evidence + +- `npm --prefix web run test:library-append-desktop` passed. +- The generator appended one selected Object and resolved four stable local IDs with three dependency + edges. +- Object, Mesh, Material, and Image all have null library pointers, no library override, writable + `LOCAL_MAIN` ownership, and identical source/local stable names. +- Save/reopen preserved 4 vertices, 4 edges, 1 polygon, 4 loops, `UVMap`, the material slot, packed + 2x2 sRGB RGBA image metadata, and Float32 pixel SHA-256 + `6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a`. +- A new temporary source/target pair reproduced the canonical report after normalizing only the + session-bound `.blend` container hashes. + +## Artifact Hashes + +- generator: `b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f` +- checker: `6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d` +- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` +- target `.blend`: `5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c` +- desktop report: `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a` +- manifest: `cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed` +- package commands: `b68f2bce48fcd42e4cc1399b47ba33c35495248a6c0405c2280b5b73e3ce581a` + +## Next Task + +`M12-03D`: append the same closure into WASM Main with one transaction and local ownership. + +## Rollback + +Remove the desktop append generator/checker, source and target fixtures, golden report/manifest, +package command, and this status entry. Restore M12-03C to pending and the queue to M12-03C. No +parity ledger rollback is required. diff --git a/docs/status/M12-03D.md b/docs/status/M12-03D.md new file mode 100644 index 00000000..0262b0b5 --- /dev/null +++ b/docs/status/M12-03D.md @@ -0,0 +1,52 @@ +# M12-03D Status + +status: done +task: append one dependency closure through WASM Main in one transaction +updated: 2026-08-17 America/New_York +enablingTask: false +parityStateChange: false + +## Scope + +The browser Worker validates a source-hash-bound APPEND binding and sends one Object closure to +the authoritative Blender Main. Blender's native link/append context recursively imports the +Object, Mesh, Material, and packed Image, verifies that every resulting ID is local and writable, +and publishes one SceneIR revision. The existing Main history transaction is used for rollback and +undo/redo; the source is never treated as a linked read-only snapshot. + +## Evidence + +- `EM_CACHE=/home/mes123456/workinf_Blender_Wasm/.emcache cmake --build build_web_blender6 --target web_engine -- -j2` passed; + the generated module exports `_web_engine_append_library_object`. +- `npm --prefix web run typecheck` passed. +- `WEB_TEST_PORT=5194 npm --prefix web run test:library-main-append` passed in Chromium. +- The test opens a clean `.blend`, appends the desktop fixture closure through `WebEngineClient`, + verifies `transactionCount=1`, four `LOCAL_MAIN` mappings, stable Object/Mesh/Material/Image + IDs, the material-to-image dependency, and a single revision increment. +- A stale base revision returns `REVISION_CONFLICT`; a second append colliding with the local IDs + returns `ASSET_MANIFEST_INVALID` without changing the revision. Undo removes the closure, redo + restores it, and a saved buffer reopens with the same local image and Object dependency. +- The Worker closure check accepts both the reader's `material.imageIds` representation and the + Blender node-level `imageId` representation; it does not infer ownership from a preview. + +## Artifact hashes + +- test: `7130453cbc81f22eb1bcf598ff3745672ac8177dccb11cef667f5b68b43e87c6` +- worker: `e3bb430e401144bf17cdcbd0de13cde0a4f904f1f9ff9d5b10a73d90f0d4c648` +- protocol: `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7` +- source `.blend`: `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` +- clean target `.blend`: `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63` +- WASM JS: `9c831d0351ac9d0714cce3b1da748cba416ced3086e3c59595cf24ac4da459ed` +- WASM binary: `7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea` +- Main append API: `5a71260c542fa559f73c6b82e23e72d39bd2844f5ebe12c6edc809a49d79791b` +- Main append implementation: `d0c484ab62e6ec051c8c2f67c49d750a456578aaccc2aa4f461026307af0db95` + +## Next task + +`M12-03E`: append undo/redo/save/reopen and the desktop canonical report must agree. + +## Rollback + +Remove the M12-03D Worker/e2e test, package command, WASM append export and Main append entrypoint, +restore the material closure check, and move the machine queue back to `M12-03D`. No V1 release +ledger rollback is required. diff --git a/docs/status/N-019.md b/docs/status/N-019.md index d9c6c585..5274e866 100644 --- a/docs/status/N-019.md +++ b/docs/status/N-019.md @@ -1,8 +1,8 @@ # N-019 灯光与渲染 状态:`BLOCKED`(Camera/Light/World/Scene metadata、Camera/Light/World 有界 Main 写回、 -Three exposure/shadow 映射和一条有界 Blender reference 图像指标闭环已落地;Scene 颜色管理 -writer、全域渲染等价和高级后端仍未实现) +Three exposure/shadow 映射、一条有界 Blender reference 图像指标和 Render 故障生命周期闭环 +已落地;Scene 颜色管理 writer、全域渲染等价和高级后端仍未实现) ## 已验证切片 @@ -49,6 +49,10 @@ writer、全域渲染等价和高级后端仍未实现) 绑定同一 provenance 并公开 output MIME/byte length/output SHA-256/result SHA-256。真实 loopback server 由 Blender 5.2 headless 打开 fixture 并输出 PNG;source、settings、build 和 output 篡改均在提交或消费前结构化拒绝。 +14. M11-14 Render fault lifecycle:真实 Eevee `.blend` 在 Chromium WebEngine/Main 打开后生成 + 非空 WebGL RGBA8 输出;预取消 open 返回 `OPEN_CANCELLED` 且不替换当前 Main,Worker + generation 2 重开保持 scene identity 和输出 SHA-256。纹理 aggregate budget 超限不替换 + 已加载资源,`GPUTextureStore.dispose()` 返回正数 byte/resource 回执并归零所有 owned texture。 ## 仍然阻断 @@ -75,5 +79,6 @@ WEB_TEST_PORT=5548 npm --prefix web run test:render-resource-budget WEB_TEST_PORT=5552 npm --prefix web run test:render-reference WEB_TEST_PORT=5556 npm --prefix web run test:render-routing WEB_TEST_PORT=5560 npm --prefix web run test:server-render-job +WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery WEB_TEST_PORT=5319 npm --prefix web run test:e2e -- --grep "N-019 Scene exposure" ``` diff --git a/docs/status/N-020.md b/docs/status/N-020.md index d672f2a2..34f6f036 100644 --- a/docs/status/N-020.md +++ b/docs/status/N-020.md @@ -1,7 +1,8 @@ # N-020 Compositor -状态:`BLOCKED`(GraphIR、有界 CPU executor、真实 Main graph reader、四节点 WebGPU golden -与 Unsupported 全图执行门已落地;完整节点参数映射、通用 WebGPU、HDR golden 和服务端执行未实现) +状态:`BLOCKED`(GraphIR、有界 CPU executor、真实 Main graph reader、四节点 WebGPU golden、 +Unsupported 全图执行门和故障生命周期闭环已落地;完整节点参数映射、通用 WebGPU、HDR golden +和服务端执行未实现) ## 已验证切片 @@ -35,6 +36,10 @@ 完整 graph;即使 Unsupported 节点未连接到 Composite,也返回 `COMPOSITOR_NODE_UNSUPPORTED`。真实 Blender 5.2 Main fixture 的 Glare node name/type/ `blenderType`、graph JSON 与 revision 在失败前后完全不变;预置 cache 不能绕过该门。 +10. M11-14 Compositor fault lifecycle:真实 Main graph 的 CPU executor 周期取消返回 + `COMPOSITOR_CANCELLED`,8193 像素超尺寸输入在分配/缓存前返回 + `COMPOSITOR_BUDGET_EXCEEDED`。generation 2 重开保持 GraphIR identity 与 Float32 output + SHA-256,LRU `clear()` 返回已释放字节并把 entries/bytes 归零。 ## 仍然阻断 @@ -54,4 +59,5 @@ WEB_TEST_PORT=5320 npm --prefix web run test:e2e -- --grep "N-020 CPU compositor npm --prefix web run test:compositor-main-reader WEB_TEST_PORT=5562 npm --prefix web run test:compositor-node-golden WEB_TEST_PORT=5565 npm --prefix web run test:compositor-unsupported-gate +WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery ``` diff --git a/docs/status/N-021.md b/docs/status/N-021.md index 9343ab4e..5f9e6231 100644 --- a/docs/status/N-021.md +++ b/docs/status/N-021.md @@ -2,8 +2,8 @@ 状态:`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 仍未实现) +gate、最终编码 server-export 路由、实时 AudioContext 恢复门和媒体故障生命周期闭环已落地; +Main 写回、完整媒体解码/渲染、音频波形、A/V sync、实际混音和真正服务端编码 job 仍未实现) ## 已验证切片 @@ -63,6 +63,10 @@ gate、最终编码 server-export 路由和实时 AudioContext 恢复门已落 mute 将 gain 归零,unmute 恢复 0.75,close 再归零并断开节点。API/构造器缺失与 resume failure 分别稳定返回 `SEQUENCER_AUDIO_DEVICE_UNAVAILABLE` 和 `SEQUENCER_AUDIO_RESUME_FAILED`,不会误报输出启用。 +17. M11-14 Media fault lifecycle:真实 H.264 先通过 HTMLMedia probe 并解出 8x8 RGBA8 frame; + 预取消 proxy decode 返回 `SEQUENCER_CANCELLED` 且不发布 payload。低于一帧的 cache budget + 返回 `SEQUENCER_BUDGET_EXCEEDED` 且 entries/bytes 保持 0;首会话 clear 返回正数释放字节, + 重建会话后 source/decode/profile identity 和 payload SHA-256 保持不变。 ## 仍然阻断 @@ -86,4 +90,5 @@ WEB_TEST_PORT=5573 npm --prefix web run test:sequencer-media-cache WEB_TEST_PORT=5576 npm --prefix web run test:sequencer-media-revision WEB_TEST_PORT=5579 npm --prefix web run test:sequencer-final-export WEB_TEST_PORT=5586 npm --prefix web run test:sequencer-audio-recovery +WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery ``` diff --git a/docs/status/N-023.md b/docs/status/N-023.md index 0d20a0c7..ab2ffedf 100644 --- a/docs/status/N-023.md +++ b/docs/status/N-023.md @@ -3,6 +3,97 @@ 状态:`BLOCKED`(asset catalog、来源/许可证元数据、真实 Main library inventory、库依赖与 IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨桌面重导入未实现) +## 完整对标盘点基线 + +M12-01A 已从 Blender 5.2 DNA、RNA、Blend IO、AssetCatalog/CDF 源码及真实 RNA runtime 盘点 +14 个 `AssetMetaData` DNA 字段、10 个公开 RNA 属性、3 个 `AssetTag` 存储字段和 3 个 catalog +持久语义字段。M12-01B 随后用锁定 Blender 5.2 生成 3 条 CDF v1 catalog、Object/Material/World +资产和 canonical JSON,并由独立 Blender 新进程精确重开复验。M12-01C 以 Blender UUID/path +和 AssetWeakReference 定义 Web schema v2 stable identity,并冻结 UTF-8 文本及条目预算。三项均为 +`enablingTask`。M12-01D 又完成 v1→v2 无损前向迁移,保留 revision、source、preview 与 library +graph,并输出确定性 ID mapping/hash report。四项只冻结完整对标输入与迁移,不增加本节已验证 +slice。M12-01E 又固定 v2 对 v1 reader 的 read-only/write-block/future-reject 策略。五项仍不改变 +`parityStatus=BLOCKED`。 + +M12-01F 已补齐 duplicate catalog/asset ID、legacy parent cycle、v2 parentPath drift、UTF-8 +simple-name/tag budget 和 v1/v2 unknown-field 共 9 类稳定负例;该 enabling evidence 不改变 slice。 + +M12-01G 已让 v1→v2 IndexedDB index migration 在同一 readwrite transaction 中提交 target、 +receipt 和 source delete;写 target 后或删 source 后注入失败均 abort,关闭并重开后旧 v1 index、 +无关 migration 行和数据库版本精确不变。该 enabling evidence 仍不改变 slice。 + +M12-01H 又证明初始页面、reload 和两个顺序重建的独立 Worker 读取相同 catalog/asset 顺序、 +AssetWeakReference stable identity、revision 和 manifest SHA-256;receipt/hash drift 在发布前阻断。 +该 enabling evidence 仍不改变 slice。 + +M12-01I 将 A-H 八项任务链、Blender 5.2 desktop fixture、Web v2 fixture、production migration +report、canonical 双 hash 和 IndexedDB fault/restart artifact 递归绑定为一个 READY evidence。 +M12.1 schema/migration 已收口,但仍是 enabling evidence,不把 catalog mutation/preview 计为完成。 + +M12-02A 已盘点 PreviewImage 的 ICON/PREVIEW 双槽、持久尺寸/rect/flag、RGBA8 packed pixel、 +premultiplied alpha、runtime deferred/invalid 状态和真正的 null pointer。原生结构/RNA 没有颜色空间 +字段;现有 Web v1/v2 也缺 slot/colorSpace/alpha/generator 语义。本项仍是 inventory evidence。 + +M12-02B 定义 source/content 各自的 byte length、MIME、SHA-256,并让 content 绑定尺寸、RGBA8、 +sRGB/alpha profile,generator 绑定 Blender version/executable/script/settings。真实 Blender 两次重编码 +确定性相同,source/content 编码不同但解码像素 exact;本项仍是 enabling identity evidence。 + +M12-02C 在任何图像 decoder 或 decoded allocation 前依次检查 identity、16 MiB encoded bytes、 +content SHA-256、PNG/WebP header、identity-bound dimensions、16,777,216 pixels、64 MiB RGBA8 +decoded bytes 和 100:1 compression ratio。8 类失败均 fail-closed,随后小 preview 可立即恢复; +本项仍是 enabling budget evidence,不把 preview storage/display 计为完成。 + +M12-02D 先把 content-addressed preview 写入 OPFS 并回读复验,再用一次 revision/full-row guarded +IndexedDB transaction 提交 catalog v2 preview reference 和 hash receipt。OPFS 前后及 catalog put +后的三处 fault 都保持旧 catalog;重开后 revision、receipt 和 payload hash 一致。本项仍不把 +dedupe、损坏隔离或 viewport display 计为完成。 + +M12-02E 又让两个不同 Blender weak-reference identity 和不同 metadata 的 asset 共享相同 content +SHA-256;第二次写入只复验并复用 OPFS payload,物理文件计数为 1,两个 catalog entry 的完整 +non-preview metadata 不变,revision 9 重开一致。本项仍不把损坏隔离或 display 计为完成。 + +M12-02F 将同长度但 SHA-256 漂移的 bytes 从 content-addressed namespace 移入复验后的 quarantine +文件,并写入与 catalog revision/reference 绑定的 receipt。catalog 本身及另一 asset 均不变;重开后 +仍可读取完整 metadata,只把 preview/data 返回 null。本项仍不把 reference GC/display 计为完成。 + +M12-02G 在 catalog reference transaction 完成后按 committed manifest 计数:同项目仍有一个引用时 +保留 payload,最后一个引用删除后才回收。OPFS project namespace 保证另一项目的同 hash payload +和 READY preview 不受影响;revision 11 重开 metadata 不变。本项仍不把 display 计为完成。 + +M12-02H 已把 Blender 5.2 确定性重编码的 `SRGB/RGBA8/STRAIGHT` preview 固定为逐字节 reference, +主线程 `HTMLCanvasElement` 与独立 Worker `OffscreenCanvas` 共用生产 decode/display 实现。两条路径 +均先通过 M12-02C 的 identity/header/预算门,再以禁用色彩转换和预乘的 `ImageBitmap` 解码;输出 +256-byte RGBA8 hash 与 desktop reference 完全一致,MAE/RMS/P95/max/bad-pixel/alpha 指标均为零, +前景 IoU 为 1。该项新增一个已验证 preview display parity slice,但不扩大到任意尺寸、色彩空间、 +asset browser UI 或 Append/Link/Override。 + +M12-03A 已分别冻结 Append、Link 与 Library Override 的数据块/闭包边界。真实 Blender runtime +公开 36 类 selectable root,其中 Screen/WorkSpace 为 append-only;Library 是来源/传递依赖元数据, +Key 等 embedded ID 随 owner 进入闭包。Append 由实际 ID pointer graph 区分 direct、indirect、 +cross-library、override dependency 与 reusable local;Link 保留 `ID.lib`、EXTERN/INDIRECT 和 +`Library.runtime.parent`;Override 另记录 linked reference、local owner、hierarchy root、property/ +operation 与 system dependency。该 inventory 仍不创建 ownership contract 或 Main transaction。 + +M12-03B 已定义 schema 1 library operation identity。`sourceLibraryId` 同时绑定 canonical source +locator 与 `.blend` SHA-256;APPEND 只能由 `LOCAL_MAIN` owner 持有且可写,LINK 只能由匹配的 +`SOURCE_LIBRARY` owner 持有且 root/reference 均只读,LIBRARY_OVERRIDE 由 `LOCAL_OVERRIDE` +持有、local 可写但 linked reference 只读。invalidation token 还绑定 operation、root、owner、 +source generation/revision 与 dependency-closure hash;任一漂移稳定返回 `REVISION_CONFLICT`。 + +M12-03C 已生成真实 Blender 5.2 desktop 单 Object append fixture。选中的 Object 自动带入 Mesh、 +Material 和 packed Image 依赖,四个 ID 在 target Main 中均无 source library/override 指针并映射为 +可写 `LOCAL_MAIN` owner。新 Blender load 路径重开 target 后,3 条依赖边、4/4/1 geometry、UVMap、 +material slot、2x2 sRGB RGBA image 和 Float32 pixel SHA-256 均与 source canonical report 一致。 +本项只冻结 desktop 权威 fixture,不提前声明 WASM Main append、undo/redo 或完整 append parity。 +该合同没有提前执行 Append 或开放 writer。 + +M12-03D 已把同一 source-hash-bound closure 送入 WASM Worker 的 authoritative Blender Main。Native +link/append context 递归导入 Object、Mesh、Material 和 packed Image,Main commit 前核对 source +locator、root、closure 与 base revision;四个 ID 均无 `ID.lib`/override、映射为可写 `LOCAL_MAIN`, +并只产生一个新的 SceneIR revision。stale revision 与 local ID collision 在 Main mutation 前阻断, +失败路径回滚完整 history state。该项只证明单 transaction Main append,undo/redo/save/reopen 与 +desktop canonical report 的联合一致性仍由 M12-03E 验证。 + ## 已验证切片 1. N-023-A:版本化 manifest 覆盖 catalog、asset kind/tag、preview、author、license、 @@ -29,11 +120,13 @@ IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨 已完成;大 bundle 性能仍未实现。 9. VDB 独立资源库包含 generated/official/source/derived/report/manifest/license,14 条记录逐项绑定 byte length 与 SHA-256;官方 sphere 使用 CC-BY-4.0 和 Git LFS 权威 hash,未放入仓库工作树。 +10. N-023-A(preview display):Blender 5.2 desktop RGBA8 reference、主线程 Canvas2D 与 + OffscreenCanvas 的像素 SHA-256 精确一致;错误像素返回 `RENDER_REFERENCE_MISMATCH`。 ## 仍然阻断 -- N-023-B:Append/Link/Library Override、reload/relocate 和真实 Main transaction;只读 library - inventory 已完成。 +- N-023-B:Append undo/redo/save/reopen、Link/Library Override、reload/relocate 和完整真实 Main + parity;M12-03D 的单 transaction append 已有独立证据,但不解除本项阻断。 - N-023-C/D:GLTF/OBJ/PLY/STL、USD/Alembic import/export/save/reopen/desktop reimport。 - N-023-E:真实 zip decoder/fuzz、OPFS quota/recovery、license/source offer 发布审计和大文件 流式性能;archive 路径冲突、双向字节预算与确定性 range plan 已完成。 @@ -44,6 +137,23 @@ IO 安全门已落地;Append/Link/Override Main、非 GLB 本地导入和跨 ```bash WEB_TEST_PORT=5323 npm --prefix web run test:e2e -- --grep "N-023 asset" npm --prefix web run test:library-main-reader +npm --prefix web run test:asset-catalog-inventory +npm --prefix web run test:asset-catalog-v1-fixture +npm --prefix web run test:asset-catalog-v2 +npm --prefix web run test:asset-catalog-migration +npm --prefix web run test:asset-catalog-legacy-reader +npm --prefix web run test:asset-catalog-negatives +npm --prefix web run test:asset-preview-inventory +npm --prefix web run test:asset-preview-identity +npm --prefix web run test:asset-preview-decode-budget +npm --prefix web run test:asset-preview-opfs-commit +npm --prefix web run test:asset-preview-dedup +npm --prefix web run test:asset-preview-quarantine +npm --prefix web run test:asset-preview-reference-gc +npm --prefix web run test:asset-preview-display +npm --prefix web run test:library-operation-inventory +npm --prefix web run test:library-operation-identity +npm --prefix web run test:library-main-append npm --prefix web run test:vdb npm --prefix web run test:vdb-native ``` diff --git a/docs/status/parity-ledger.json b/docs/status/parity-ledger.json index 83806ded..384be61c 100644 --- a/docs/status/parity-ledger.json +++ b/docs/status/parity-ledger.json @@ -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", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", "E-bounded-realtime-blender-reference-image-metrics"], + "completedSlices": ["A-camera-light-world-scene-reader-partial", "A-main-camera-dof-properties-partial", "A-main-light-world-properties-partial", "A-white-balance-integrity-gate", "B-three-exposure-shadow-mapping-partial", "B-bounded-kelvin-linear-light-color", "B-world-scene-render-delta-roundtrip", "B-volume-material-manifest-boundary", "B-C-nanovdb-float32-wgsl-bounded-ray-integration", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", "E-bounded-realtime-blender-reference-image-metrics", "E-render-fault-lifecycle-recovery"], "blockedSlices": ["A", "B", "C", "D", "E", "B-C-nanovdb-production-viewport-paging-advanced-material", "E-volume-desktop-chromium-pixel-golden"], "acceptance": ["web:test:lighting-roundtrip", "web:e2e:N-019 Scene exposure"], "dependencies": ["N-018"] @@ -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", "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"], + "completedSlices": ["A-graph-resource-cycle-schema", "A-main-graph-structure-reader-partial", "A-main-exposure-default-invert-parameter-reader", "B-bounded-cpu-executor-partial", "B-real-main-exposure-invert-cpu-chain", "B-webgpu-constant-exposure-invert-golden-partial", "B-D-unsupported-full-graph-execution-block", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", "C-E-compositor-fault-lifecycle-recovery", "D-unsupported-node-preservation-gate"], "blockedSlices": ["A", "B", "C", "D", "E"], "acceptance": ["web:test:compositor-main-reader", "web:e2e:N-020 CPU compositor"], "dependencies": ["N-019"] @@ -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", "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"], + "completedSlices": ["A-strip-resource-schema", "A-main-strip-timeline-reader-partial", "B-deterministic-move-trim-split-partial", "B-source-frame-seek", "B-active-frame-dependency-resolution", "B-cross-transition-progress-input-resolution", "B-real-main-cross-still-frame-resolution", "C-runtime-codec-probe-gate", "C-runtime-image-sound-movie-byte-decode-probe", "C-source-decode-bound-movie-proxy-cache", "C-seek-scrub-decode-revision-gate", "C-audio-context-suspend-device-mute-recovery", "C-media-fault-lifecycle-recovery", "D-final-export-server-routing-fail-closed"], "blockedSlices": ["A", "B", "C", "D", "E"], "acceptance": ["web:test:sequencer-main-reader", "web:e2e:N-021 sequencer"], "dependencies": ["N-020"] @@ -128,7 +128,7 @@ "v1RequiredSlices": ["C-glb-export-usd-analysis-gates", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-opfs-tamper-rollback-quota-recovery"], "v1ExcludedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"], "roadmapStatus": "planned", - "completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery", "E-nanovdb-gpu-resident-page-lru"], + "completedSlices": ["A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", "A-preview-desktop-browser-main-offscreen-display", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", "C-glb-export-usd-analysis-gates", "E-archive-path-ratio-budget", "E-archive-path-conflict-compressed-budget-range-plan", "E-nanovdb-http206-range-hash-streaming-boundary", "E-nanovdb-http-retry-if-range-body-resume", "E-nanovdb-opfs-atomic-binding-worker-reopen-bundle-lru", "E-nanovdb-opfs-tamper-rollback-quota-recovery", "E-nanovdb-gpu-resident-page-lru"], "blockedSlices": ["A", "B", "C", "D", "E", "E-nanovdb-large-bundle-performance"], "acceptance": ["web:test:library-main-reader", "web:test:vdb-faults", "web:e2e:N-023 asset"], "dependencies": ["N-022"] diff --git a/docs/status/release-evidence.json b/docs/status/release-evidence.json index 76365e45..7de2f7c2 100644 --- a/docs/status/release-evidence.json +++ b/docs/status/release-evidence.json @@ -1,8 +1,8 @@ { "schemaVersion": 4, "source": "docs/status/parity-ledger.json", - "sourceSha256": "0a9b4c1026c42b5f77869f222b1531601ed229184a1d1737e74047c7b3406730", - "generatedAt": "2026-08-17T08:22:00.415Z", + "sourceSha256": "c8d73374c338f46c2016f6089d99efdd7b3331fc0100d64dad0f416fe66e3eb6", + "generatedAt": "2026-08-17T15:09:53.894Z", "families": [ { "id": "N-015", @@ -277,7 +277,8 @@ "B-C-nanovdb-float32-wgsl-bounded-ray-integration", "D-final-render-server-routing-fail-closed", "D-server-render-source-build-settings-output-hash-binding", - "E-bounded-realtime-blender-reference-image-metrics" + "E-bounded-realtime-blender-reference-image-metrics", + "E-render-fault-lifecycle-recovery" ], "blockedSlices": [ "A", @@ -324,6 +325,7 @@ "B-D-unsupported-full-graph-execution-block", "C-image-operation-budget-cancel-partial", "C-content-addressed-frame-lru-cache", + "C-E-compositor-fault-lifecycle-recovery", "D-unsupported-node-preservation-gate" ], "blockedSlices": [ @@ -372,6 +374,7 @@ "C-source-decode-bound-movie-proxy-cache", "C-seek-scrub-decode-revision-gate", "C-audio-context-suspend-device-mute-recovery", + "C-media-fault-lifecycle-recovery", "D-final-export-server-routing-fail-closed" ], "blockedSlices": [ @@ -456,6 +459,7 @@ "A-catalog-asset-license-source-schema", "A-content-addressed-opfs-capability", "A-preview-content-signature-dimension-verification", + "A-preview-desktop-browser-main-offscreen-display", "A-vdb-external-resource-license-sha-catalog", "B-library-dependency-order-partial", "B-main-library-inventory-reader", diff --git a/tests/files/web/m12_asset_catalog_v1/blender_assets.cats.txt b/tests/files/web/m12_asset_catalog_v1/blender_assets.cats.txt new file mode 100644 index 00000000..25884cc8 --- /dev/null +++ b/tests/files/web/m12_asset_catalog_v1/blender_assets.cats.txt @@ -0,0 +1,9 @@ +# This is an Asset Catalog Definition file for Blender. +# +# Generated by M12-01B from a locked Blender 5.2 runtime. + +VERSION 1 + +11111111-1111-4111-8111-111111111111:Characters:Characters +22222222-2222-4222-8222-222222222222:Characters/Heroes:Heroes +33333333-3333-4333-8333-333333333333:Materials/Metal:Metal diff --git a/tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend b/tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend new file mode 100644 index 00000000..2773a7c0 Binary files /dev/null and b/tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend differ diff --git a/tests/files/web/m12_library_append_v1/m12_append_source.blend b/tests/files/web/m12_library_append_v1/m12_append_source.blend new file mode 100644 index 00000000..1e72c178 Binary files /dev/null and b/tests/files/web/m12_library_append_v1/m12_append_source.blend differ diff --git a/tests/files/web/m12_library_append_v1/m12_append_target.blend b/tests/files/web/m12_library_append_v1/m12_append_target.blend new file mode 100644 index 00000000..ac027cb5 Binary files /dev/null and b/tests/files/web/m12_library_append_v1/m12_append_target.blend differ diff --git a/tests/golden/M11-14/render-compositor-media-recovery.json b/tests/golden/M11-14/render-compositor-media-recovery.json new file mode 100644 index 00000000..2b9c3171 --- /dev/null +++ b/tests/golden/M11-14/render-compositor-media-recovery.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": 1, + "task": "M11-14", + "blenderVersion": "5.2.0", + "domains": [ + { + "domain": "RENDER", + "fixture": "tests/files/web/m11_render_reference.blend", + "sha256": "d2bea55fe4de0b00e73a9241b4d1b0c802c2eb0e6159c0cd48e007b97b9d3963", + "desktopGolden": "tests/golden/M11-04/manifest.json", + "cancellationCode": "OPEN_CANCELLED", + "budgetCode": "GPU_TEXTURE_BUDGET_EXCEEDED" + }, + { + "domain": "COMPOSITOR", + "fixture": "tests/files/web/m11_compositor_allowlist.blend", + "sha256": "e844eba69002101c8cf4f376cdd844c4be78729091627d898634088707374b29", + "desktopGolden": "tests/golden/M11-07/compositor-node-golden.json", + "cancellationCode": "COMPOSITOR_CANCELLED", + "budgetCode": "COMPOSITOR_BUDGET_EXCEEDED" + }, + { + "domain": "MEDIA", + "fixture": "tests/files/web/media/sequencer-probe.mp4", + "sha256": "f35a5a2765aef9d0fed7146108c699d0beb03f34414cb27e6b7a7d4871187b65", + "desktopGolden": "tests/golden/M11-09/sequencer-codec-probe.json", + "cancellationCode": "SEQUENCER_CANCELLED", + "budgetCode": "SEQUENCER_BUDGET_EXCEEDED" + } + ], + "textureFixture": { + "path": "tests/files/web/resources/udim_1001.png", + "sha256": "0c66d053412de9e99f1fb83fa8f199d061df874ad1b6bda86c76cf3f499382cf" + }, + "requiredStages": ["CANCELLATION", "RESTART", "BUDGET", "RELEASE", "RECOVERY"], + "invariants": { + "cancellation": "zero result publication and zero temporary resources", + "restart": "generation 1 and 2 preserve domain identity and output SHA-256", + "budget": "stable domain-specific block before committed state mutation", + "release": "positive resource and byte release followed by zero owned resources", + "recovery": "a bounded real fixture produces non-empty output after restart" + }, + "verification": { + "unit": "web/tests/unit/render-compositor-media-recovery.test.mjs", + "browser": "web/tests/e2e/render-compositor-media-recovery.spec.ts", + "command": "npm --prefix web run test:render-compositor-media-recovery" + } +} diff --git a/tests/golden/M12-01/evidence.json b/tests/golden/M12-01/evidence.json new file mode 100644 index 00000000..d63f7c78 --- /dev/null +++ b/tests/golden/M12-01/evidence.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "task": "M12-01I", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "status": "READY", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "subtasks": [ + { "task": "M12-01A", "path": "tests/golden/M12-01A/asset-catalog-field-inventory.json", "sha256": "194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936" }, + { "task": "M12-01B", "path": "tests/golden/M12-01B/manifest.json", "sha256": "1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd" }, + { "task": "M12-01C", "path": "tests/golden/M12-01C/manifest.json", "sha256": "612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9" }, + { "task": "M12-01D", "path": "tests/golden/M12-01D/manifest.json", "sha256": "fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d" }, + { "task": "M12-01E", "path": "tests/golden/M12-01E/manifest.json", "sha256": "cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b" }, + { "task": "M12-01F", "path": "tests/golden/M12-01F/manifest.json", "sha256": "127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d" }, + { "task": "M12-01G", "path": "tests/golden/M12-01G/manifest.json", "sha256": "2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7" }, + { "task": "M12-01H", "path": "tests/golden/M12-01H/manifest.json", "sha256": "59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae" } + ], + "schema": { + "desktopCanonical": { "path": "tests/golden/M12-01B/canonical.json", "sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e" }, + "webV2Golden": { "path": "tests/golden/M12-01C/asset-catalog-v2.json", "sha256": "2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929" }, + "productionProtocol": { "path": "web/protocol/asset-catalog-v2.ts", "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" } + }, + "migration": { + "sourceFixture": { "path": "tests/golden/M12-01D/catalog-v1.json", "sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5" }, + "targetFixture": { "path": "tests/golden/M12-01D/catalog-v2.json", "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" }, + "report": { "path": "tests/golden/M12-01D/migration-report.json", "sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a" }, + "productionProtocol": { "path": "web/protocol/asset-catalog-migration.ts", "sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226" }, + "sourceManifestSha256": "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881", + "targetManifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "revision": 7 + }, + "runtime": { + "indexedDB": { "path": "web/app/src/storage/asset-catalog-indexeddb.ts", "sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57" }, + "restartWorker": { "path": "web/app/src/workers/asset-catalog-restart-test.worker.ts", "sha256": "e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e" }, + "catalogCount": 3, + "assetCount": 2, + "faultPoints": ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"], + "restartContexts": ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"] + }, + "claims": [ + "BLENDER_5_2_FIELD_INVENTORY_BOUND", + "DESKTOP_FIXTURE_REOPENED", + "SCHEMA_V2_UTF8_BUDGETED", + "V1_TO_V2_MIGRATION_HASH_BOUND", + "LEGACY_READER_FAIL_CLOSED", + "NEGATIVE_MATRIX_RECOVERS", + "INDEXEDDB_ABORT_PRESERVES_V1", + "PAGE_WORKER_RESTART_IDENTITY_STABLE" + ], + "nextTask": "M12-02A" +} diff --git a/tests/golden/M12-01A/asset-catalog-field-inventory.json b/tests/golden/M12-01A/asset-catalog-field-inventory.json new file mode 100644 index 00000000..a440d517 --- /dev/null +++ b/tests/golden/M12-01A/asset-catalog-field-inventory.json @@ -0,0 +1,264 @@ +{ + "schemaVersion": 1, + "task": "M12-01A", + "enablingTask": true, + "parityStateChange": false, + "blenderVersion": "5.2.0", + "sources": [ + { + "role": "DNA_ASSET_STORAGE", + "path": "blender-5.2.0/source/blender/makesdna/DNA_asset_types.h", + "sha256": "de97f44c71d46049b0effdf0fed6aba3f0df72d6d4ba89b88f61e7624d3b9a49" + }, + { + "role": "RNA_ASSET_API", + "path": "blender-5.2.0/source/blender/makesrna/intern/rna_asset.cc", + "sha256": "92c9cd5ce718d5b250a02eaefbd1c356a962e2f69b235d23fc97447a9a567a22" + }, + { + "role": "ASSET_BLEND_IO", + "path": "blender-5.2.0/source/blender/blenkernel/intern/asset.cc", + "sha256": "eb66d1bfb3efb39cd6c6a6be9e6ff23f2be10a1c427e5b998a2dbcd1ec5b9adc" + }, + { + "role": "CATALOG_MODEL", + "path": "blender-5.2.0/source/blender/asset_system/AS_asset_catalog.hh", + "sha256": "6fd93dc78d0634b9c4d0ee03a23d3233be591204aea7957d1b9768b7c3508110" + }, + { + "role": "CATALOG_PATH", + "path": "blender-5.2.0/source/blender/asset_system/AS_asset_catalog_path.hh", + "sha256": "9579b74fac1cb3208c96c8aa56b51c9e44d96e3416b718d77270d5f8269bc7b9" + }, + { + "role": "CATALOG_FILE_API", + "path": "blender-5.2.0/source/blender/asset_system/intern/asset_catalog_definition_file.hh", + "sha256": "880f5ae42fa6c532d46b70d5264f3ac7f8e8abab8c6cf189a4e8aebdd9dd04b6" + }, + { + "role": "CATALOG_FILE_FORMAT", + "path": "blender-5.2.0/source/blender/asset_system/intern/asset_catalog_definition_file.cc", + "sha256": "4316b683125df87e07e4f1116b44f58eb725b654ac9941118a148c08ee916a18" + }, + { + "role": "CURRENT_WEB_V1_SNAPSHOT", + "path": "web/protocol/asset-library-io.ts", + "sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4" + } + ], + "assetTag": { + "fields": [ + { + "name": "next", + "cType": "AssetTag*", + "sourceToken": "struct AssetTag *next", + "persistence": "LIST_LINKAGE", + "semantic": "NON_USER_LINKAGE", + "rnaProperty": null + }, + { + "name": "prev", + "cType": "AssetTag*", + "sourceToken": "*prev = nullptr", + "persistence": "LIST_LINKAGE", + "semantic": "NON_USER_LINKAGE", + "rnaProperty": null + }, + { + "name": "name", + "cType": "char[64]", + "sourceToken": "char name[/*MAX_NAME*/ 64]", + "persistence": "BLEND", + "semantic": "USER_VALUE", + "rnaProperty": "name", + "maximumStorageBytesIncludingNull": 64 + } + ], + "collectionOperations": ["new", "remove"], + "newSupportsSkipIfExists": true, + "orderIsPersistent": true + }, + "assetMetaData": { + "dnaFields": [ + { + "name": "local_type_info", + "cType": "AssetTypeInfo*", + "sourceToken": "struct AssetTypeInfo *local_type_info", + "persistence": "RUNTIME_ONLY", + "semantic": "CALLBACK_TYPE", + "rnaProperty": null + }, + { + "name": "properties", + "cType": "IDProperty*", + "sourceToken": "struct IDProperty *properties", + "persistence": "BLEND_IDPROPERTY", + "semantic": "CUSTOM_METADATA_NO_ID_POINTERS", + "rnaProperty": "CUSTOM_ID_PROPERTIES" + }, + { + "name": "catalog_id", + "cType": "bUUID", + "sourceToken": "struct bUUID catalog_id", + "persistence": "BLEND", + "semantic": "AUTHORITATIVE_CATALOG_ID", + "rnaProperty": "catalog_id" + }, + { + "name": "catalog_simple_name", + "cType": "char[64]", + "sourceToken": "char catalog_simple_name[/*MAX_NAME*/ 64]", + "persistence": "BLEND", + "semantic": "RECOVERY_ONLY_NOT_AUTHORITY", + "rnaProperty": "catalog_simple_name" + }, + { + "name": "author", + "cType": "char*", + "sourceToken": "char *author = nullptr", + "persistence": "BLEND_DYNAMIC_STRING", + "semantic": "OPTIONAL_DISPLAY_TEXT", + "rnaProperty": "author" + }, + { + "name": "description", + "cType": "char*", + "sourceToken": "char *description = nullptr", + "persistence": "BLEND_DYNAMIC_STRING", + "semantic": "OPTIONAL_DISPLAY_TEXT", + "rnaProperty": "description" + }, + { + "name": "copyright", + "cType": "char*", + "sourceToken": "char *copyright = nullptr", + "persistence": "BLEND_DYNAMIC_STRING", + "semantic": "OPTIONAL_LEGAL_TEXT", + "rnaProperty": "copyright" + }, + { + "name": "license", + "cType": "char*", + "sourceToken": "char *license = nullptr", + "persistence": "BLEND_DYNAMIC_STRING", + "semantic": "OPTIONAL_LEGAL_TEXT", + "rnaProperty": "license" + }, + { + "name": "tags", + "cType": "ListBaseT", + "sourceToken": "ListBaseT tags", + "persistence": "BLEND_ORDERED_COLLECTION", + "semantic": "FILTER_TOKENS", + "rnaProperty": "tags" + }, + { + "name": "active_tag", + "cType": "short", + "sourceToken": "short active_tag = 0", + "persistence": "BLEND", + "semantic": "EDITOR_SELECTION_INDEX", + "rnaProperty": "active_tag" + }, + { + "name": "tot_tags", + "cType": "short", + "sourceToken": "short tot_tags = 0", + "persistence": "BLEND_DERIVED_COUNT", + "semantic": "MUST_EQUAL_TAG_LIST_COUNT", + "rnaProperty": null + }, + { + "name": "flag", + "cType": "AssetMetaDataFlag", + "sourceToken": "AssetMetaDataFlag flag = {}", + "persistence": "BLEND", + "semantic": "USE_OWN_IMPORT_METHOD_BIT", + "rnaProperty": "use_preferred_import_method" + }, + { + "name": "preferred_import_method", + "cType": "eAssetImportMethod", + "sourceToken": "eAssetImportMethod preferred_import_method", + "persistence": "BLEND", + "semantic": "CONDITIONAL_IMPORT_POLICY", + "rnaProperty": "preferred_import_method" + }, + { + "name": "_pad", + "cType": "char[4]", + "sourceToken": "char _pad[4] = {}", + "persistence": "ABI_PADDING", + "semantic": "NON_USER_PADDING", + "rnaProperty": null + } + ], + "rnaProperties": [ + { "identifier": "author", "type": "STRING", "isReadonly": false, "isRuntime": false }, + { "identifier": "description", "type": "STRING", "isReadonly": false, "isRuntime": false }, + { "identifier": "copyright", "type": "STRING", "isReadonly": false, "isRuntime": false }, + { "identifier": "license", "type": "STRING", "isReadonly": false, "isRuntime": false }, + { "identifier": "tags", "type": "COLLECTION", "isReadonly": true, "isRuntime": false }, + { "identifier": "active_tag", "type": "INT", "isReadonly": false, "isRuntime": false }, + { "identifier": "catalog_id", "type": "STRING", "isReadonly": false, "isRuntime": false }, + { "identifier": "catalog_simple_name", "type": "STRING", "isReadonly": true, "isRuntime": false }, + { "identifier": "use_preferred_import_method", "type": "BOOLEAN", "isReadonly": false, "isRuntime": false }, + { "identifier": "preferred_import_method", "type": "ENUM", "isReadonly": false, "isRuntime": false } + ], + "preferredImportMethods": ["LINK", "APPEND", "APPEND_REUSE", "PACK"], + "emptyAuthorDescriptionCopyrightLicenseAreValid": true, + "catalogIdFormat": "RFC4122_UUID_OR_NIL", + "customPropertiesMayNotReferenceDataBlocks": true + }, + "catalog": { + "semanticFields": [ + { "name": "catalog_id", "cType": "CatalogID", "sourceToken": "const CatalogID catalog_id", "persistence": "CATALOG_DEFINITION_FILE" }, + { "name": "path", "cType": "AssetCatalogPath", "sourceToken": "AssetCatalogPath path", "persistence": "CATALOG_DEFINITION_FILE" }, + { "name": "simple_name", "cType": "std::string", "sourceToken": "std::string simple_name", "persistence": "CATALOG_DEFINITION_FILE_AND_ASSET_RECOVERY_COPY" } + ], + "runtimeFlags": [ + { "name": "is_deleted", "sourceToken": "bool is_deleted = false" }, + { "name": "is_first_loaded", "sourceToken": "bool is_first_loaded = false" }, + { "name": "has_unsaved_changes", "sourceToken": "bool has_unsaved_changes = false" } + ], + "definitionFile": { + "defaultFilename": "blender_assets.cats.txt", + "supportedVersion": 1, + "versionMarker": "VERSION ", + "recordFields": ["catalog_id", "path", "simple_name"], + "delimiter": ":", + "writeOrder": ["path", "is_first_loaded", "catalog_id"], + "emptyAndCommentLinesIgnored": true, + "firstSemanticLineMustBeVersion": true + }, + "path": { + "encoding": "UTF8_BYTES", + "separator": "/", + "hierarchy": "IMPLICIT_PATH_COMPONENTS", + "leadingSlash": false, + "componentColonAllowed": false, + "cleanup": ["TRIM_COMPONENTS", "REMOVE_EMPTY_COMPONENTS", "REPLACE_INVALID_CHARACTERS"] + }, + "identityRules": { + "assetReferenceAuthority": "catalog_id", + "simpleNameAuthority": false, + "parentIdStored": false, + "duplicatePathsAllowed": true, + "duplicatePathSelectionOrder": ["is_first_loaded", "catalog_id"] + } + }, + "currentWebSnapshot": { + "assetCatalogIRFields": ["id", "name", "parentId"], + "assetEntryIRFields": ["id", "name", "kind", "catalogId", "tags", "author", "license", "sourceSha256", "sourcePath", "preview"], + "directlyRepresentedMetadata": ["catalog_id", "tags", "author", "license"], + "missingMetadata": ["properties", "description", "copyright", "catalog_simple_name", "active_tag", "use_preferred_import_method", "preferred_import_method"], + "semanticDrift": [ + "Blender author and license are optional; Web v1 requires both to be non-empty", + "Blender catalog hierarchy is an implicit cleaned path; Web v1 stores an explicit parentId graph", + "Blender catalog simple_name is recovery metadata; Web v1 name is the only catalog label", + "Blender tag storage is 64 bytes including the terminator; Web v1 bounds JavaScript string length to 64 code units", + "Blender custom ID properties are persisted but Web v1 has no preservation field" + ] + }, + "nextTask": "M12-01B" +} diff --git a/tests/golden/M12-01B/canonical.json b/tests/golden/M12-01B/canonical.json new file mode 100644 index 00000000..fb416560 --- /dev/null +++ b/tests/golden/M12-01B/canonical.json @@ -0,0 +1,116 @@ +{ + "schemaVersion": 1, + "task": "M12-01B", + "enablingTask": true, + "parityStateChange": false, + "blenderVersion": "5.2.0", + "catalogDefinition": { + "fileName": "blender_assets.cats.txt", + "version": 1, + "records": [ + { + "catalogId": "11111111-1111-4111-8111-111111111111", + "path": "Characters", + "simpleName": "Characters", + "parentPath": null + }, + { + "catalogId": "22222222-2222-4222-8222-222222222222", + "path": "Characters/Heroes", + "simpleName": "Heroes", + "parentPath": "Characters" + }, + { + "catalogId": "33333333-3333-4333-8333-333333333333", + "path": "Materials/Metal", + "simpleName": "Metal", + "parentPath": "Materials" + } + ] + }, + "assets": [ + { + "idType": "MATERIAL", + "name": "M12 Brushed Metal", + "catalogId": "33333333-3333-4333-8333-333333333333", + "catalogSimpleName": "", + "author": "", + "description": "", + "copyright": "", + "license": "", + "tags": [ + "metal" + ], + "activeTag": 0, + "usePreferredImportMethod": false, + "preferredImportMethod": "APPEND", + "customProperties": [ + { + "name": "roughness", + "type": "FLOAT", + "value": 0.35 + } + ] + }, + { + "idType": "OBJECT", + "name": "M12 Hero", + "catalogId": "22222222-2222-4222-8222-222222222222", + "catalogSimpleName": "", + "author": "M12 Artist", + "description": "Desktop catalog v1 object fixture", + "copyright": "Copyright 2026 M12 Fixture Authors", + "license": "CC0-1.0", + "tags": [ + "character", + "hero", + "rig-ready" + ], + "activeTag": 1, + "usePreferredImportMethod": true, + "preferredImportMethod": "APPEND", + "customProperties": [ + { + "name": "approved", + "type": "BOOL", + "value": true + }, + { + "name": "dimensions", + "type": "IDPROPERTYARRAY", + "value": [ + 2.0, + 2.0, + 0.0 + ] + }, + { + "name": "rating", + "type": "INT", + "value": 5 + }, + { + "name": "source", + "type": "STR", + "value": "M12-01B" + } + ] + }, + { + "idType": "WORLD", + "name": "M12 Uncataloged World", + "catalogId": "00000000-0000-0000-0000-000000000000", + "catalogSimpleName": "", + "author": "M12 Artist", + "description": "Asset without a catalog assignment", + "copyright": "", + "license": "CC0-1.0", + "tags": [], + "activeTag": 0, + "usePreferredImportMethod": true, + "preferredImportMethod": "LINK", + "customProperties": [] + } + ], + "nextTask": "M12-01C" +} diff --git a/tests/golden/M12-01B/manifest.json b/tests/golden/M12-01B/manifest.json new file mode 100644 index 00000000..49298943 --- /dev/null +++ b/tests/golden/M12-01B/manifest.json @@ -0,0 +1,60 @@ +{ + "schemaVersion": 1, + "task": "M12-01B", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "blenderVersion": "5.2.0", + "artifacts": { + "generator": { + "path": "tools/web/generate-asset-catalog-v1.py", + "sha256": "4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307" + }, + "exporter": { + "path": "tools/web/export-asset-catalog-v1.py", + "sha256": "3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41" + }, + "checker": { + "path": "tools/web/check-asset-catalog-v1-fixture.mjs", + "sha256": "c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b" + }, + "fixture": { + "path": "tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend", + "sha256": "8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f" + }, + "catalogDefinition": { + "path": "tests/files/web/m12_asset_catalog_v1/blender_assets.cats.txt", + "sha256": "a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98" + }, + "canonicalReport": { + "path": "tests/golden/M12-01B/canonical.json", + "sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e" + } + }, + "coverage": { + "catalogRecords": 3, + "assetRecords": 3, + "assetIdTypes": ["MATERIAL", "OBJECT", "WORLD"], + "metadata": [ + "properties", + "catalog_id", + "catalog_simple_name", + "author", + "description", + "copyright", + "license", + "tags", + "active_tag", + "use_preferred_import_method", + "preferred_import_method" + ] + }, + "nonGoals": [ + "Web catalog schema v2", + "Web migration behavior", + "Catalog or asset mutation parity" + ], + "nextTask": "M12-01C" +} diff --git a/tests/golden/M12-01C/asset-catalog-v2.json b/tests/golden/M12-01C/asset-catalog-v2.json new file mode 100644 index 00000000..c861bc67 --- /dev/null +++ b/tests/golden/M12-01C/asset-catalog-v2.json @@ -0,0 +1,127 @@ +{ + "schemaVersion": 2, + "revision": 1, + "catalogs": [ + { + "catalogId": "11111111-1111-4111-8111-111111111111", + "path": "Characters", + "simpleName": "Characters", + "parentPath": null + }, + { + "catalogId": "22222222-2222-4222-8222-222222222222", + "path": "Characters/Heroes", + "simpleName": "Heroes", + "parentPath": "Characters" + }, + { + "catalogId": "33333333-3333-4333-8333-333333333333", + "path": "Materials/Metal", + "simpleName": "Metal", + "parentPath": "Materials" + } + ], + "assets": [ + { + "assetId": "asset:0ecbd393de19b0cc4fe26956ba658e782aaad4a859b4a9647161f1405ddadfd5", + "assetLibraryIdentifier": null, + "relativeAssetIdentifier": "Material/M12 Brushed Metal", + "idType": "MATERIAL", + "name": "M12 Brushed Metal", + "catalogId": "33333333-3333-4333-8333-333333333333", + "catalogSimpleName": "", + "author": "", + "description": "", + "copyright": "", + "license": "", + "tags": [ + "metal" + ], + "activeTag": 0, + "usePreferredImportMethod": false, + "preferredImportMethod": "APPEND", + "customProperties": [ + { + "name": "roughness", + "type": "FLOAT", + "value": 0.35 + } + ], + "sourceSha256": null, + "sourcePath": null, + "preview": null + }, + { + "assetId": "asset:4e9e5a3755581b0f3b604748fc5c64f4f51f14b5c2efd485a7093edeae4dbabd", + "assetLibraryIdentifier": null, + "relativeAssetIdentifier": "Object/M12 Hero", + "idType": "OBJECT", + "name": "M12 Hero", + "catalogId": "22222222-2222-4222-8222-222222222222", + "catalogSimpleName": "", + "author": "M12 Artist", + "description": "Desktop catalog v1 object fixture", + "copyright": "Copyright 2026 M12 Fixture Authors", + "license": "CC0-1.0", + "tags": [ + "character", + "hero", + "rig-ready" + ], + "activeTag": 1, + "usePreferredImportMethod": true, + "preferredImportMethod": "APPEND", + "customProperties": [ + { + "name": "approved", + "type": "BOOL", + "value": true + }, + { + "name": "dimensions", + "type": "FLOAT_ARRAY", + "value": [ + 2, + 2, + 0 + ] + }, + { + "name": "rating", + "type": "INT", + "value": 5 + }, + { + "name": "source", + "type": "STRING", + "value": "M12-01B" + } + ], + "sourceSha256": null, + "sourcePath": null, + "preview": null + }, + { + "assetId": "asset:7ef9ef809269e187e988effca992a2249e6340bd65bba2da2c4b2545c8a7bd8c", + "assetLibraryIdentifier": null, + "relativeAssetIdentifier": "World/M12 Uncataloged World", + "idType": "WORLD", + "name": "M12 Uncataloged World", + "catalogId": null, + "catalogSimpleName": "", + "author": "M12 Artist", + "description": "Asset without a catalog assignment", + "copyright": "", + "license": "CC0-1.0", + "tags": [], + "activeTag": 0, + "usePreferredImportMethod": true, + "preferredImportMethod": "LINK", + "customProperties": [], + "sourceSha256": null, + "sourcePath": null, + "preview": null + } + ], + "libraries": [] +} diff --git a/tests/golden/M12-01C/manifest.json b/tests/golden/M12-01C/manifest.json new file mode 100644 index 00000000..ac104654 --- /dev/null +++ b/tests/golden/M12-01C/manifest.json @@ -0,0 +1,45 @@ +{ + "schemaVersion": 1, + "task": "M12-01C", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "blenderVersion": "5.2.0", + "identityContract": { + "catalog": "CANONICAL_RFC4122_UUID", + "asset": "SHA256_OF_BLENDER_ASSET_WEAK_REFERENCE_V1", + "renameBehavior": "RELATIVE_IDENTIFIER_AND_ASSET_ID_CHANGE_TO_MATCH_BLENDER", + "catalogHierarchy": "CLEANED_SLASH_PATH" + }, + "artifacts": { + "protocol": { + "path": "web/protocol/asset-catalog-v2.ts", + "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" + }, + "generator": { + "path": "tools/web/generate-asset-catalog-v2.mjs", + "sha256": "b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881" + }, + "desktopCanonical": { + "path": "tests/golden/M12-01B/canonical.json", + "sha256": "6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e" + }, + "schemaGolden": { + "path": "tests/golden/M12-01C/asset-catalog-v2.json", + "sha256": "2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929" + }, + "blenderWeakReferenceSource": { + "path": "blender-5.2.0/source/blender/asset_system/intern/asset_representation.cc", + "sha256": "ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635" + } + }, + "nonGoals": [ + "Schema v1 to v2 migration", + "Schema v2 legacy-reader policy", + "IndexedDB migration", + "Catalog mutation parity" + ], + "nextTask": "M12-01D" +} diff --git a/tests/golden/M12-01D/catalog-v1.json b/tests/golden/M12-01D/catalog-v1.json new file mode 100644 index 00000000..00bd2ae7 --- /dev/null +++ b/tests/golden/M12-01D/catalog-v1.json @@ -0,0 +1,58 @@ +{ + "schemaVersion": 1, + "revision": 7, + "catalogs": [ + { "id": "catalog:root", "name": "Characters", "parentId": null }, + { "id": "catalog:heroes", "name": "Heroes", "parentId": "catalog:root" }, + { "id": "44444444-4444-4444-8444-444444444444", "name": "Animation", "parentId": null } + ], + "assets": [ + { + "id": "legacy:hero", + "name": "Legacy Hero", + "kind": "OBJECT", + "catalogId": "catalog:heroes", + "tags": ["character", "hero"], + "author": "M12 Artist", + "license": "CC0-1.0", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sourcePath": "assets/legacy-hero.blend", + "preview": { + "assetId": "legacy:hero", + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "mimeType": "image/png", + "width": 64, + "height": 64, + "byteLength": 4096 + } + }, + { + "id": "legacy:walk", + "name": "Legacy Walk", + "kind": "ACTION", + "catalogId": "44444444-4444-4444-8444-444444444444", + "tags": ["walk"], + "author": "M12 Artist", + "license": "CC-BY-4.0", + "sourceSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc" + } + ], + "libraries": [ + { + "id": "library:characters", + "name": "Characters", + "sourcePath": "libraries/characters.blend", + "sourceSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "dependencyIds": ["library:materials"], + "readOnly": true + }, + { + "id": "library:materials", + "name": "Materials", + "sourcePath": "libraries/materials.blend", + "sourceSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "dependencyIds": [], + "readOnly": true + } + ] +} diff --git a/tests/golden/M12-01D/catalog-v2.json b/tests/golden/M12-01D/catalog-v2.json new file mode 100644 index 00000000..6e8c690e --- /dev/null +++ b/tests/golden/M12-01D/catalog-v2.json @@ -0,0 +1,99 @@ +{ + "schemaVersion": 2, + "revision": 7, + "catalogs": [ + { + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation", + "simpleName": "Animation", + "parentPath": null + }, + { + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters", + "simpleName": "Characters", + "parentPath": null + }, + { + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes", + "simpleName": "Heroes", + "parentPath": "Characters" + } + ], + "assets": [ + { + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "assetLibraryIdentifier": null, + "relativeAssetIdentifier": "Action/Legacy Walk", + "idType": "ACTION", + "name": "Legacy Walk", + "catalogId": "44444444-4444-4444-8444-444444444444", + "catalogSimpleName": "Animation", + "author": "M12 Artist", + "description": "", + "copyright": "", + "license": "CC-BY-4.0", + "tags": [ + "walk" + ], + "activeTag": 0, + "usePreferredImportMethod": false, + "preferredImportMethod": "APPEND", + "customProperties": [], + "sourceSha256": "cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc", + "sourcePath": null, + "preview": null + }, + { + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "assetLibraryIdentifier": null, + "relativeAssetIdentifier": "Object/Legacy Hero", + "idType": "OBJECT", + "name": "Legacy Hero", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "catalogSimpleName": "Heroes", + "author": "M12 Artist", + "description": "", + "copyright": "", + "license": "CC0-1.0", + "tags": [ + "character", + "hero" + ], + "activeTag": 0, + "usePreferredImportMethod": false, + "preferredImportMethod": "APPEND", + "customProperties": [], + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", + "sourcePath": "assets/legacy-hero.blend", + "preview": { + "sha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "mimeType": "image/png", + "width": 64, + "height": 64, + "byteLength": 4096 + } + } + ], + "libraries": [ + { + "libraryId": "library:characters", + "name": "Characters", + "sourcePath": "libraries/characters.blend", + "sourceSha256": "dddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddddd", + "dependencyIds": [ + "library:materials" + ], + "readOnly": true + }, + { + "libraryId": "library:materials", + "name": "Materials", + "sourcePath": "libraries/materials.blend", + "sourceSha256": "eeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeeee", + "dependencyIds": [], + "readOnly": true + } + ] +} diff --git a/tests/golden/M12-01D/manifest.json b/tests/golden/M12-01D/manifest.json new file mode 100644 index 00000000..c39ab4c6 --- /dev/null +++ b/tests/golden/M12-01D/manifest.json @@ -0,0 +1,44 @@ +{ + "schemaVersion": 1, + "task": "M12-01D", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "migration": { + "sourceSchemaVersion": 1, + "targetSchemaVersion": 2, + "catalogIdMapping": "RFC4122_UUIDV5_FIXED_NAMESPACE", + "assetIdMapping": "BLENDER_ASSET_WEAK_REFERENCE_V1_SHA256", + "revisionPolicy": "PRESERVE", + "unknownFieldPolicy": "BLOCK_BEFORE_MIGRATION" + }, + "artifacts": { + "migrationProtocol": { + "path": "web/protocol/asset-catalog-migration.ts", + "sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226" + }, + "targetProtocol": { + "path": "web/protocol/asset-catalog-v2.ts", + "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" + }, + "generator": { + "path": "tools/web/generate-asset-catalog-migration.mjs", + "sha256": "954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60" + }, + "sourceFixture": { + "path": "tests/golden/M12-01D/catalog-v1.json", + "sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5" + }, + "targetFixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + }, + "migrationReport": { + "path": "tests/golden/M12-01D/migration-report.json", + "sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a" + } + }, + "nextTask": "M12-01E" +} diff --git a/tests/golden/M12-01D/migration-report.json b/tests/golden/M12-01D/migration-report.json new file mode 100644 index 00000000..776ef48d --- /dev/null +++ b/tests/golden/M12-01D/migration-report.json @@ -0,0 +1,53 @@ +{ + "schemaVersion": 1, + "task": "M12-01D", + "status": "MIGRATED", + "sourceSchemaVersion": 1, + "targetSchemaVersion": 2, + "sourceRevision": 7, + "targetRevision": 7, + "sourceManifestSha256": "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881", + "targetManifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "catalogMappings": [ + { + "legacyId": "44444444-4444-4444-8444-444444444444", + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation" + }, + { + "legacyId": "catalog:heroes", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes" + }, + { + "legacyId": "catalog:root", + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters" + } + ], + "assetMappings": [ + { + "legacyId": "legacy:hero", + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "relativeAssetIdentifier": "Object/Legacy Hero" + }, + { + "legacyId": "legacy:walk", + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "relativeAssetIdentifier": "Action/Legacy Walk" + } + ], + "defaultsApplied": { + "dynamicMetadataFields": 4, + "preferredImportMethods": 2, + "customPropertyCollections": 2 + }, + "preserved": { + "catalogs": 3, + "assets": 2, + "libraries": 2, + "previews": 1, + "sourceBindings": 2 + }, + "nextTask": "M12-01E" +} diff --git a/tests/golden/M12-01E/compatibility-report.json b/tests/golden/M12-01E/compatibility-report.json new file mode 100644 index 00000000..5a2d45f9 --- /dev/null +++ b/tests/golden/M12-01E/compatibility-report.json @@ -0,0 +1,255 @@ +{ + "read": { + "schemaVersion": 1, + "task": "M12-01E", + "readerSchemaVersion": 1, + "documentSchemaVersion": 2, + "operation": "READ", + "status": "READ_ONLY", + "code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + "recoverable": true, + "sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "snapshot": { + "revision": 7, + "catalogs": [ + { + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation", + "simpleName": "Animation" + }, + { + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters", + "simpleName": "Characters" + }, + { + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes", + "simpleName": "Heroes" + } + ], + "assets": [ + { + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "relativeAssetIdentifier": "Action/Legacy Walk", + "idType": "ACTION", + "name": "Legacy Walk", + "catalogId": "44444444-4444-4444-8444-444444444444" + }, + { + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "relativeAssetIdentifier": "Object/Legacy Hero", + "idType": "OBJECT", + "name": "Legacy Hero", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab" + } + ], + "libraries": [ + { + "libraryId": "library:characters", + "name": "Characters", + "readOnly": true + }, + { + "libraryId": "library:materials", + "name": "Materials", + "readOnly": true + } + ] + }, + "nextTask": "M12-01F" + }, + "catalogWrite": { + "schemaVersion": 1, + "task": "M12-01E", + "readerSchemaVersion": 1, + "documentSchemaVersion": 2, + "operation": "CATALOG_WRITE", + "status": "BLOCKED", + "code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + "recoverable": true, + "sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "snapshot": { + "revision": 7, + "catalogs": [ + { + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation", + "simpleName": "Animation" + }, + { + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters", + "simpleName": "Characters" + }, + { + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes", + "simpleName": "Heroes" + } + ], + "assets": [ + { + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "relativeAssetIdentifier": "Action/Legacy Walk", + "idType": "ACTION", + "name": "Legacy Walk", + "catalogId": "44444444-4444-4444-8444-444444444444" + }, + { + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "relativeAssetIdentifier": "Object/Legacy Hero", + "idType": "OBJECT", + "name": "Legacy Hero", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab" + } + ], + "libraries": [ + { + "libraryId": "library:characters", + "name": "Characters", + "readOnly": true + }, + { + "libraryId": "library:materials", + "name": "Materials", + "readOnly": true + } + ] + }, + "nextTask": "M12-01F" + }, + "assetWrite": { + "schemaVersion": 1, + "task": "M12-01E", + "readerSchemaVersion": 1, + "documentSchemaVersion": 2, + "operation": "ASSET_WRITE", + "status": "BLOCKED", + "code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + "recoverable": true, + "sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "snapshot": { + "revision": 7, + "catalogs": [ + { + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation", + "simpleName": "Animation" + }, + { + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters", + "simpleName": "Characters" + }, + { + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes", + "simpleName": "Heroes" + } + ], + "assets": [ + { + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "relativeAssetIdentifier": "Action/Legacy Walk", + "idType": "ACTION", + "name": "Legacy Walk", + "catalogId": "44444444-4444-4444-8444-444444444444" + }, + { + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "relativeAssetIdentifier": "Object/Legacy Hero", + "idType": "OBJECT", + "name": "Legacy Hero", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab" + } + ], + "libraries": [ + { + "libraryId": "library:characters", + "name": "Characters", + "readOnly": true + }, + { + "libraryId": "library:materials", + "name": "Materials", + "readOnly": true + } + ] + }, + "nextTask": "M12-01F" + }, + "save": { + "schemaVersion": 1, + "task": "M12-01E", + "readerSchemaVersion": 1, + "documentSchemaVersion": 2, + "operation": "SAVE", + "status": "BLOCKED", + "code": "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + "recoverable": true, + "sourceSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "snapshot": { + "revision": 7, + "catalogs": [ + { + "catalogId": "44444444-4444-4444-8444-444444444444", + "path": "Animation", + "simpleName": "Animation" + }, + { + "catalogId": "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "path": "Characters", + "simpleName": "Characters" + }, + { + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", + "path": "Characters/Heroes", + "simpleName": "Heroes" + } + ], + "assets": [ + { + "assetId": "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "relativeAssetIdentifier": "Action/Legacy Walk", + "idType": "ACTION", + "name": "Legacy Walk", + "catalogId": "44444444-4444-4444-8444-444444444444" + }, + { + "assetId": "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + "relativeAssetIdentifier": "Object/Legacy Hero", + "idType": "OBJECT", + "name": "Legacy Hero", + "catalogId": "b4bb3608-9267-5aba-bb9e-9e97b006d5ab" + } + ], + "libraries": [ + { + "libraryId": "library:characters", + "name": "Characters", + "readOnly": true + }, + { + "libraryId": "library:materials", + "name": "Materials", + "readOnly": true + } + ] + }, + "nextTask": "M12-01F" + }, + "future": { + "schemaVersion": 1, + "task": "M12-01E", + "readerSchemaVersion": 1, + "documentSchemaVersion": 3, + "operation": "READ", + "status": "BLOCKED", + "code": "PROTOCOL_MISMATCH", + "recoverable": false, + "sourceSha256": "d6b7646f6ede911358f954a0ccae376703f1c0e8854a9953a4a77a968b600b80", + "snapshot": null, + "nextTask": "M12-01F" + } +} diff --git a/tests/golden/M12-01E/manifest.json b/tests/golden/M12-01E/manifest.json new file mode 100644 index 00000000..92276172 --- /dev/null +++ b/tests/golden/M12-01E/manifest.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "task": "M12-01E", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "policy": { + "nativeV1": "READY", + "validV2Read": "READ_ONLY", + "validV2Write": "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + "futureSchema": "PROTOCOL_MISMATCH", + "sourceMutation": "FORBIDDEN" + }, + "artifacts": { + "protocol": { + "path": "web/protocol/asset-catalog-compatibility.ts", + "sha256": "5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349" + }, + "errorContract": { + "path": "web/protocol/error.ts", + "sha256": "309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34" + }, + "generator": { + "path": "tools/web/generate-asset-catalog-compatibility.mjs", + "sha256": "208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e" + }, + "v2Fixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + }, + "compatibilityReport": { + "path": "tests/golden/M12-01E/compatibility-report.json", + "sha256": "3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54" + } + }, + "nextTask": "M12-01F" +} diff --git a/tests/golden/M12-01F/manifest.json b/tests/golden/M12-01F/manifest.json new file mode 100644 index 00000000..ea522e17 --- /dev/null +++ b/tests/golden/M12-01F/manifest.json @@ -0,0 +1,33 @@ +{ + "schemaVersion": 1, + "task": "M12-01F", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "negativeCaseCount": 9, + "artifacts": { + "schemaProtocol": { + "path": "web/protocol/asset-catalog-v2.ts", + "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" + }, + "migrationProtocol": { + "path": "web/protocol/asset-catalog-migration.ts", + "sha256": "6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226" + }, + "v1Fixture": { + "path": "tests/golden/M12-01D/catalog-v1.json", + "sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5" + }, + "v2Fixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + }, + "negativeCases": { + "path": "tests/golden/M12-01F/negative-cases.json", + "sha256": "74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874" + } + }, + "nextTask": "M12-01G" +} diff --git a/tests/golden/M12-01F/negative-cases.json b/tests/golden/M12-01F/negative-cases.json new file mode 100644 index 00000000..2ce85b02 --- /dev/null +++ b/tests/golden/M12-01F/negative-cases.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M12-01F", + "cases": [ + { "id": "DUPLICATE_CATALOG_ID", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "DUPLICATE_ASSET_ID", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "LEGACY_PARENT_CYCLE", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "PARENT_PATH_MISMATCH", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "OVERLONG_SIMPLE_NAME_UTF8", "code": "ASSET_BUDGET_EXCEEDED" }, + { "id": "OVERLONG_TAG_UTF8", "code": "ASSET_BUDGET_EXCEEDED" }, + { "id": "V2_UNKNOWN_TOP_LEVEL", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "V2_UNKNOWN_ASSET_FIELD", "code": "ASSET_MANIFEST_INVALID" }, + { "id": "V1_UNKNOWN_ASSET_FIELD", "code": "ASSET_MANIFEST_INVALID" } + ], + "recovery": "VALID_V2_AFTER_EACH_NEGATIVE", + "nextTask": "M12-01G" +} diff --git a/tests/golden/M12-01G/manifest.json b/tests/golden/M12-01G/manifest.json new file mode 100644 index 00000000..58972072 --- /dev/null +++ b/tests/golden/M12-01G/manifest.json @@ -0,0 +1,40 @@ +{ + "schemaVersion": 1, + "task": "M12-01G", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "transaction": { + "sourceRow": "asset-catalog:index:v1", + "targetRow": "asset-catalog:index:v2", + "receiptRow": "asset-catalog:migration:v1-to-v2", + "faultPoints": ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"], + "failureCode": "STORAGE_TRANSACTION", + "rollbackPolicy": "PRESERVE_SOURCE_TARGET_RECEIPT_AND_UNRELATED_ROWS" + }, + "artifacts": { + "indexedDBMigration": { + "path": "web/app/src/storage/asset-catalog-indexeddb.ts", + "sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-catalog-indexeddb-migration.spec.ts", + "sha256": "82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9" + }, + "sourceFixture": { + "path": "tests/golden/M12-01D/catalog-v1.json", + "sha256": "1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5" + }, + "targetFixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + }, + "migrationReport": { + "path": "tests/golden/M12-01D/migration-report.json", + "sha256": "57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a" + } + }, + "nextTask": "M12-01H" +} diff --git a/tests/golden/M12-01H/manifest.json b/tests/golden/M12-01H/manifest.json new file mode 100644 index 00000000..dddb46a2 --- /dev/null +++ b/tests/golden/M12-01H/manifest.json @@ -0,0 +1,43 @@ +{ + "schemaVersion": 1, + "task": "M12-01H", + "parentTask": "M12-01", + "parityId": "N-023-ASSET-CATALOG-SCHEMA", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "restart": { + "contexts": ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"], + "catalogCount": 3, + "assetCount": 2, + "manifestSha256": "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + "catalogOrder": [ + "44444444-4444-4444-8444-444444444444", + "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", + "b4bb3608-9267-5aba-bb9e-9e97b006d5ab" + ], + "assetOrder": [ + "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326" + ] + }, + "artifacts": { + "indexedDBReader": { + "path": "web/app/src/storage/asset-catalog-indexeddb.ts", + "sha256": "283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57" + }, + "restartWorker": { + "path": "web/app/src/workers/asset-catalog-restart-test.worker.ts", + "sha256": "e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-catalog-restart.spec.ts", + "sha256": "7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b" + }, + "targetFixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + } + }, + "nextTask": "M12-01I" +} diff --git a/tests/golden/M12-02A/asset-preview-inventory.json b/tests/golden/M12-02A/asset-preview-inventory.json new file mode 100644 index 00000000..75cb70bf --- /dev/null +++ b/tests/golden/M12-02A/asset-preview-inventory.json @@ -0,0 +1,98 @@ +{ + "schemaVersion": 1, + "task": "M12-02A", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "blenderVersion": "5.2.0", + "sources": [ + { "role": "PREVIEW_DNA", "path": "blender-5.2.0/source/blender/makesdna/DNA_ID.h", "sha256": "27d0eac02df737f2d2298eeef45e66eae0d76da66542ccdf32400b3e9bd904a9" }, + { "role": "PREVIEW_SLOT_ENUM", "path": "blender-5.2.0/source/blender/makesdna/DNA_ID_enums.h", "sha256": "9b97042ca408924c2efa92b49bc44a4f2f949b11768fb89b2c1b71c9c796cd61" }, + { "role": "PREVIEW_API", "path": "blender-5.2.0/source/blender/blenkernel/BKE_preview_image.hh", "sha256": "25656ad3ef44bb6d00b6e6f9748d26fd04a358d7bb9f876da4194c05d1e5a9a1" }, + { "role": "PREVIEW_IMPLEMENTATION", "path": "blender-5.2.0/source/blender/blenkernel/intern/preview_image.cc", "sha256": "886ae47b7f4950b7b3dc558ac1973c0b114a265d837aa130e4479dd8b438db86" }, + { "role": "PREVIEW_RNA", "path": "blender-5.2.0/source/blender/makesrna/intern/rna_ID.cc", "sha256": "d97b3346a3d36e5198989087cbb6a24f43d9d6d535a6f795392a47f77ccdfc2c" }, + { "role": "ASSET_PREVIEW_GENERATOR", "path": "blender-5.2.0/source/blender/editors/asset/intern/asset_ops.cc", "sha256": "94ae9ce907d1060b3f4dc7536ddd66590efb8e1680bf7d3dc72ce1ce53523732" }, + { "role": "PREVIEW_BLEND_READER", "path": "blender-5.2.0/source/blender/blenloader/intern/readblenentry.cc", "sha256": "dd10bdbdd5f42a09722411e8b99ce34749186d256e91413b7cb5bdef422b8cde" }, + { "role": "PREVIEW_SIZE_CONSTANTS", "path": "blender-5.2.0/source/blender/imbuf/IMB_thumbs.hh", "sha256": "c7d5ddf7c3fbbdd731eac7f0761cada3e8e0df024d8d2c7b48d777e517761be1" }, + { "role": "ICON_SIZE_CONSTANTS", "path": "blender-5.2.0/source/blender/blenkernel/BKE_icons.hh", "sha256": "c16c52503a2fe2e5df9c2925b04d6ce64468cf2c5d98fd85e0e47423cfc9d2aa" }, + { "role": "CURRENT_WEB_V1", "path": "web/protocol/asset-library-io.ts", "sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4" }, + { "role": "CURRENT_WEB_V2", "path": "web/protocol/asset-catalog-v2.ts", "sha256": "0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7" } + ], + "fixtures": [ + { "role": "LOADED_RGBA8_PNG", "path": "tests/files/web/media/sequencer-frame.png", "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" }, + { "role": "ASSET_NO_PREVIEW_BLEND", "path": "tests/files/web/m12_asset_catalog_v1/m12_asset_catalog_v1.blend", "sha256": "8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f" } + ], + "storage": { + "slotCount": 2, + "slots": [ + { "index": 0, "name": "ICON", "defaultRenderMaxHeight": 32 }, + { "index": 1, "name": "PREVIEW", "defaultRenderMaxHeight": 128, "customAssetMaxHeight": 256 } + ], + "fields": [ + { "name": "w", "cType": "unsigned int[2]", "persistence": "BLEND", "semantic": "PIXEL_WIDTH" }, + { "name": "h", "cType": "unsigned int[2]", "persistence": "BLEND", "semantic": "PIXEL_HEIGHT" }, + { "name": "flag", "cType": "short[2]", "persistence": "BLEND_WITH_RUNTIME_BITS_CLEARED", "semantic": "CHANGED_USER_EDITED_RENDERING" }, + { "name": "changed_timestamp", "cType": "short[2]", "persistence": "BLEND", "semantic": "CHANGE_COUNTER" }, + { "name": "rect", "cType": "unsigned int*[2]", "persistence": "BLEND_ARRAY", "semantic": "ONE_PACKED_RGBA32_VALUE_PER_PIXEL" }, + { "name": "runtime", "cType": "PreviewImageRuntime*", "persistence": "RUNTIME_ONLY", "semantic": "GPU_DEFERRED_LOADING_AND_USER_COUNT" } + ], + "pixelFormat": "RGBA8_PACKED_UINT32", + "byteComponents": 4, + "alphaMode": "PREMULTIPLIED", + "colorSpace": null, + "colorSpaceFinding": "NO_COLOR_SPACE_FIELD_OR_RNA_PROPERTY; BYTE_BUFFER_INTERPRETATION_DEPENDS_ON_PRODUCER_PIPELINE" + }, + "rna": { + "idProperty": { "identifier": "preview", "type": "POINTER", "readOnly": true, "nullWhenAbsent": true }, + "imagePreviewProperties": [ + { "identifier": "is_image_custom", "type": "BOOLEAN", "readOnly": false, "arrayLength": 0 }, + { "identifier": "image_size", "type": "INT", "readOnly": false, "arrayLength": 2 }, + { "identifier": "image_pixels", "type": "INT", "readOnly": false, "arrayLength": 0 }, + { "identifier": "image_pixels_float", "type": "FLOAT", "readOnly": false, "arrayLength": 0 }, + { "identifier": "is_icon_custom", "type": "BOOLEAN", "readOnly": false, "arrayLength": 0 }, + { "identifier": "icon_size", "type": "INT", "readOnly": false, "arrayLength": 2 }, + { "identifier": "icon_pixels", "type": "INT", "readOnly": false, "arrayLength": 0 }, + { "identifier": "icon_pixels_float", "type": "FLOAT", "readOnly": false, "arrayLength": 0 }, + { "identifier": "icon_id", "type": "INT", "readOnly": true, "arrayLength": 0 } + ], + "lengthRules": { + "packedPixels": "width*height", + "floatComponents": "width*height*4", + "floatConversion": "BYTE_COMPONENT_DIVIDED_BY_255_WITHOUT_COLOR_TRANSFORM" + } + }, + "runtimeStates": { + "noPreview": { + "state": "ABSENT_POINTER", + "assetCount": 3, + "assetTypes": ["Object", "Material", "World"], + "previewValues": [null, null, null] + }, + "loadedPreview": { + "sourceSize": [8, 8], + "imageSize": [8, 8], + "iconSize": [32, 32], + "imagePackedPixelCount": 64, + "imageFloatComponentCount": 256, + "iconPackedPixelCount": 1024, + "imageCustom": false, + "iconCustom": false + }, + "emptyBufferRule": "NULL_RECT_OR_ZERO_WIDTH_OR_ZERO_HEIGHT_IS_NO_IMAGE_FOR_THAT_SLOT", + "invalidDeferredRule": "RUNTIME_TAG_ONLY_NOT_A_PERSISTED_COLOR_OR_CONTENT_IDENTITY" + }, + "currentWeb": { + "v1Fields": ["assetId", "sha256", "mimeType", "width", "height", "byteLength"], + "v2Fields": ["sha256", "mimeType", "width", "height", "byteLength"], + "noPreviewRepresentation": { "v1": "OPTIONAL_PROPERTY_ABSENT", "v2": "NULL" }, + "missingSemantics": ["slot", "colorSpace", "alphaMode", "sourceSha256", "generatorIdentity", "deferredOrInvalidState"] + }, + "nonGoals": [ + "Preview content identity schema", + "Decode budgets", + "OPFS commit ordering", + "Browser display parity" + ], + "nextTask": "M12-02B" +} diff --git a/tests/golden/M12-02B/identity.json b/tests/golden/M12-02B/identity.json new file mode 100644 index 00000000..8314e87b --- /dev/null +++ b/tests/golden/M12-02B/identity.json @@ -0,0 +1,28 @@ +{ + "schemaVersion": 1, + "assetId": "asset:4e9e5a3755581b0f3b604748fc5c64f4f51f14b5c2efd485a7093edeae4dbabd", + "slot": "PREVIEW", + "source": { + "mimeType": "image/png", + "byteLength": 261, + "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" + }, + "content": { + "mimeType": "image/png", + "byteLength": 513, + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7", + "width": 8, + "height": 8, + "pixelFormat": "RGBA8", + "colorSpace": "SRGB", + "alphaMode": "STRAIGHT" + }, + "generator": { + "name": "BLENDER_ASSET_PREVIEW_IDENTITY", + "version": "5.2.0", + "executableSha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82", + "scriptSha256": "753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce", + "settingsSha256": "07ebc55d4b6cbb293badf0640874846912df3d23549997cf6f87f4c325280ca0" + }, + "identitySha256": "3139b1df11615fd8c7caaf920a0ba2c93e8921d999374876a2c65d889d256e5f" +} diff --git a/tests/golden/M12-02B/manifest.json b/tests/golden/M12-02B/manifest.json new file mode 100644 index 00000000..c8c762f9 --- /dev/null +++ b/tests/golden/M12-02B/manifest.json @@ -0,0 +1,31 @@ +{ + "schemaVersion": 1, + "task": "M12-02B", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "generatorSettings": { + "schemaVersion": 1, + "format": "PNG", + "colorMode": "RGBA", + "colorDepth": 8, + "compression": 0, + "displayDevice": "sRGB", + "viewTransform": "Standard", + "look": "None", + "exposure": 0, + "gamma": 1 + }, + "generatorSettingsSha256": "07ebc55d4b6cbb293badf0640874846912df3d23549997cf6f87f4c325280ca0", + "artifacts": { + "protocol": { "path": "web/protocol/asset-preview.ts", "sha256": "2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438" }, + "errorContract": { "path": "web/protocol/error.ts", "sha256": "6aec7b8a9d6903d83ae67718db3ae775d709cabb207cf2a02968d4ff7c5f26c0" }, + "generator": { "path": "tools/web/generate-asset-preview-identity.py", "sha256": "753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce" }, + "blender": { "path": "build_blender_5.2.0/bin/blender", "sha256": "d4483926610484ef9c2ad9241aae1469f934d955ebe791f1920e263e0ba85b82" }, + "source": { "path": "tests/files/web/media/sequencer-frame.png", "sha256": "295b083db2299ab904947eb39c17173de70c211882b0d855537d8f3cc27698ed" }, + "content": { "path": "tests/golden/M12-02B/preview.png", "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" } + }, + "nextTask": "M12-02C" +} diff --git a/tests/golden/M12-02B/preview.png b/tests/golden/M12-02B/preview.png new file mode 100644 index 00000000..b0f0480d Binary files /dev/null and b/tests/golden/M12-02B/preview.png differ diff --git a/tests/golden/M12-02C/manifest.json b/tests/golden/M12-02C/manifest.json new file mode 100644 index 00000000..afda54b7 --- /dev/null +++ b/tests/golden/M12-02C/manifest.json @@ -0,0 +1,34 @@ +{ + "schemaVersion": 1, + "task": "M12-02C", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "budget": { + "maxEncodedBytes": 16777216, + "maxWidth": 4096, + "maxHeight": 4096, + "maxPixels": 16777216, + "maxDecodedBytes": 67108864, + "maxCompressionRatio": 100 + }, + "preDecodeOrder": [ + "IDENTITY_PARSE", + "ENCODED_BYTE_BUDGET", + "CONTENT_SHA256", + "MIME_AND_HEADER", + "DIMENSION_BINDING", + "PIXEL_AND_DECODED_BYTE_BUDGET", + "COMPRESSION_RATIO" + ], + "negativeCaseCount": 8, + "artifacts": { + "decodeProtocol": { "path": "web/protocol/asset-preview-decode.ts", "sha256": "3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63" }, + "identityProtocol": { "path": "web/protocol/asset-preview.ts", "sha256": "2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438" }, + "identityGolden": { "path": "tests/golden/M12-02B/identity.json", "sha256": "1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b" }, + "contentPNG": { "path": "tests/golden/M12-02B/preview.png", "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" } + }, + "nextTask": "M12-02D" +} diff --git a/tests/golden/M12-02D/manifest.json b/tests/golden/M12-02D/manifest.json new file mode 100644 index 00000000..d31a9c0b --- /dev/null +++ b/tests/golden/M12-02D/manifest.json @@ -0,0 +1,48 @@ +{ + "schemaVersion": 1, + "task": "M12-02D", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "commitOrder": [ + "PRE_DECODE_GATE", + "OPFS_WRITE", + "OPFS_READBACK", + "CATALOG_TRANSACTION" + ], + "faultPoints": [ + "BEFORE_OPFS_WRITE", + "AFTER_OPFS_WRITE", + "AFTER_CATALOG_PUT" + ], + "failureCode": "STORAGE_TRANSACTION", + "artifacts": { + "commitCoordinator": { + "path": "web/app/src/storage/asset-preview-opfs-commit.ts", + "sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-preview-opfs-commit.spec.ts", + "sha256": "043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e" + }, + "unitSuite": { + "path": "web/tests/unit/asset-preview-opfs-commit.test.mjs", + "sha256": "f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74" + }, + "decodeProtocol": { + "path": "web/protocol/asset-preview-decode.ts", + "sha256": "3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63" + }, + "identityGolden": { + "path": "tests/golden/M12-02B/identity.json", + "sha256": "1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b" + }, + "contentPNG": { + "path": "tests/golden/M12-02B/preview.png", + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" + } + }, + "nextTask": "M12-02E" +} diff --git a/tests/golden/M12-02E/manifest.json b/tests/golden/M12-02E/manifest.json new file mode 100644 index 00000000..9b04224c --- /dev/null +++ b/tests/golden/M12-02E/manifest.json @@ -0,0 +1,41 @@ +{ + "schemaVersion": 1, + "task": "M12-02E", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "claims": [ + "ONE_CONTENT_HASH_ONE_OPFS_PAYLOAD", + "DISTINCT_ASSET_IDENTITY_PRESERVED", + "DISTINCT_ASSET_METADATA_PRESERVED", + "REOPENED_HEAD_VERIFIED" + ], + "assetCount": 2, + "physicalPayloadCount": 1, + "contentSha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7", + "artifacts": { + "commitCoordinator": { + "path": "web/app/src/storage/asset-preview-opfs-commit.ts", + "sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-preview-dedup.spec.ts", + "sha256": "4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40" + }, + "unitSuite": { + "path": "web/tests/unit/asset-preview-dedup.test.mjs", + "sha256": "e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835" + }, + "catalogFixture": { + "path": "tests/golden/M12-01D/catalog-v2.json", + "sha256": "38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb" + }, + "contentPNG": { + "path": "tests/golden/M12-02B/preview.png", + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" + } + }, + "nextTask": "M12-02F" +} diff --git a/tests/golden/M12-02F/manifest.json b/tests/golden/M12-02F/manifest.json new file mode 100644 index 00000000..c3a83d88 --- /dev/null +++ b/tests/golden/M12-02F/manifest.json @@ -0,0 +1,35 @@ +{ + "schemaVersion": 1, + "task": "M12-02F", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "states": ["READY", "QUARANTINED", "REOPENED_QUARANTINED"], + "failureCode": "ASSET_SOURCE_HASH_MISMATCH", + "catalogMutationOnCorruption": false, + "artifacts": { + "quarantineProtocol": { + "path": "web/app/src/storage/asset-preview-quarantine.ts", + "sha256": "a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-preview-quarantine.spec.ts", + "sha256": "d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27" + }, + "unitSuite": { + "path": "web/tests/unit/asset-preview-quarantine.test.mjs", + "sha256": "79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a" + }, + "commitCoordinator": { + "path": "web/app/src/storage/asset-preview-opfs-commit.ts", + "sha256": "36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2" + }, + "contentPNG": { + "path": "tests/golden/M12-02B/preview.png", + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" + } + }, + "nextTask": "M12-02G" +} diff --git a/tests/golden/M12-02G/manifest.json b/tests/golden/M12-02G/manifest.json new file mode 100644 index 00000000..1eb0ed36 --- /dev/null +++ b/tests/golden/M12-02G/manifest.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "task": "M12-02G", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": true, + "parityStateChange": false, + "implementationClass": "NOT_APPLICABLE", + "sequence": [ + "REMOVE_FIRST_REFERENCE", + "RETAIN_PAYLOAD", + "REMOVE_FINAL_REFERENCE", + "COLLECT_PAYLOAD" + ], + "otherProjectPayload": "PRESERVED_READY", + "artifacts": { + "referenceGC": { + "path": "web/app/src/storage/asset-preview-reference-gc.ts", + "sha256": "76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-preview-reference-gc.spec.ts", + "sha256": "2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a" + }, + "unitSuite": { + "path": "web/tests/unit/asset-preview-reference-gc.test.mjs", + "sha256": "c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e" + }, + "opfsStorage": { + "path": "web/app/src/storage/opfs-files.ts", + "sha256": "2e8ef9d72194bf803a7e823fb2b7de30b9f10ae6dd4b031b81b97ff82eee9cc6" + }, + "contentPNG": { + "path": "tests/golden/M12-02B/preview.png", + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" + } + }, + "nextTask": "M12-02H" +} diff --git a/tests/golden/M12-02H/desktop-report.json b/tests/golden/M12-02H/desktop-report.json new file mode 100644 index 00000000..aa2c1aec --- /dev/null +++ b/tests/golden/M12-02H/desktop-report.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "task": "M12-02H", + "generator": "BLENDER_ASSET_PREVIEW_IDENTITY", + "blenderVersion": "5.2.0", + "width": 8, + "height": 8, + "pixelCount": 64, + "pixelFormat": "RGBA8", + "colorSpace": "SRGB", + "alphaMode": "STRAIGHT", + "uniquePixelCount": 1, + "referencePixel": [51, 102, 204, 255], + "rgbaSha256": "c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00", + "sourceContentMaxFloatError": 0, + "contentSha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" +} diff --git a/tests/golden/M12-02H/manifest.json b/tests/golden/M12-02H/manifest.json new file mode 100644 index 00000000..2221634f --- /dev/null +++ b/tests/golden/M12-02H/manifest.json @@ -0,0 +1,64 @@ +{ + "schemaVersion": 1, + "task": "M12-02H", + "parentTask": "M12-02", + "parityId": "N-023-ASSET-PREVIEW", + "enablingTask": false, + "parityStateChange": true, + "implementationClass": "LOCAL_EXACT", + "completedSlice": "A-preview-desktop-browser-main-offscreen-display", + "backends": [ + "BLENDER_5_2_DESKTOP", + "MAIN_THREAD_CANVAS_2D", + "OFFSCREEN_CANVAS_2D" + ], + "reference": { + "width": 8, + "height": 8, + "pixelFormat": "RGBA8", + "colorSpace": "SRGB", + "alphaMode": "STRAIGHT", + "rgbaSha256": "c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00" + }, + "thresholds": { + "maxMeanAbsoluteError": 0, + "maxRootMeanSquaredError": 0, + "maxP95ChannelError": 0, + "maxBadPixelRatio": 0, + "badPixelChannelError": 0, + "foregroundDeltaFromReferenceBackground": 1, + "minForegroundIntersectionOverUnion": 1, + "maxAlphaCoverageDeltaRatio": 0 + }, + "artifacts": { + "displayRuntime": { + "path": "web/app/src/assets/AssetPreviewDisplay.ts", + "sha256": "e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28" + }, + "displayWorker": { + "path": "web/app/src/workers/asset-preview-display-test.worker.ts", + "sha256": "6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f" + }, + "desktopChecker": { + "path": "tools/web/check-asset-preview-display.mjs", + "sha256": "d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54" + }, + "desktopReport": { + "path": "tests/golden/M12-02H/desktop-report.json", + "sha256": "cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0" + }, + "browserSuite": { + "path": "web/tests/e2e/asset-preview-display.spec.ts", + "sha256": "340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520" + }, + "unitSuite": { + "path": "web/tests/unit/asset-preview-display.test.mjs", + "sha256": "f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873" + }, + "contentPNG": { + "path": "tests/golden/M12-02B/preview.png", + "sha256": "ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7" + } + }, + "nextTask": "M12-03A" +} diff --git a/tests/golden/M12-03A/library-operation-inventory.json b/tests/golden/M12-03A/library-operation-inventory.json new file mode 100644 index 00000000..d6767e7b --- /dev/null +++ b/tests/golden/M12-03A/library-operation-inventory.json @@ -0,0 +1,255 @@ +{ + "schemaVersion": 1, + "task": "M12-03A", + "enablingTask": true, + "parityStateChange": false, + "blenderVersion": "5.2.0", + "sources": [ + { + "role": "LINK_APPEND_CONTEXT_API", + "path": "blender-5.2.0/source/blender/blenkernel/BKE_blendfile_link_append.hh", + "sha256": "3676283eca608a12fa6485c438374dfbe07a787efe22fd7719aadb8ccb8c81f9" + }, + { + "role": "LINK_APPEND_CLOSURE", + "path": "blender-5.2.0/source/blender/blenkernel/intern/blendfile_link_append.cc", + "sha256": "b6e7f839a934eabd708c5465fdcf00e138e1f84837ae6690198b14a5069b6d23" + }, + { + "role": "LINK_APPEND_OPERATORS", + "path": "blender-5.2.0/source/blender/windowmanager/intern/wm_files_link.cc", + "sha256": "d67d555f8534e6794ba7e520d95faef5a0a83017415517c8821690fa4accfb8d" + }, + { + "role": "PYTHON_LIBRARY_LOAD", + "path": "blender-5.2.0/source/blender/python/intern/bpy_library_load.cc", + "sha256": "41b80637140582c3c75fabe234a8c33e9749c75cfc5c8f9c748401c18b253cab" + }, + { + "role": "BLEND_LIBRARY_READER", + "path": "blender-5.2.0/source/blender/blenloader/BLO_readfile.hh", + "sha256": "24e65071b94e7fbc754e12563b9698f2d92a5aab06b89763950d2d4f7d66c623" + }, + { + "role": "OVERRIDE_STORAGE", + "path": "blender-5.2.0/source/blender/makesdna/DNA_ID.h", + "sha256": "27d0eac02df737f2d2298eeef45e66eae0d76da66542ccdf32400b3e9bd904a9" + }, + { + "role": "OVERRIDE_API", + "path": "blender-5.2.0/source/blender/blenkernel/BKE_lib_override.hh", + "sha256": "78545dad30c66a61e1f9754ba8017422b8d7fd3a4b41e522299e27f951762fde" + }, + { + "role": "OVERRIDE_CLOSURE", + "path": "blender-5.2.0/source/blender/blenkernel/intern/lib_override.cc", + "sha256": "34ed9f0edd6549c77e73dc958ec189717bb6eb7dda3531109161546b8c532808" + }, + { + "role": "CURRENT_WEB_LIBRARY_GATE", + "path": "web/protocol/asset-library-io.ts", + "sha256": "5c62539db3833a2113007dfbdfd12ca06704d86e0cf0e57ad97f7fbf6acda7f4" + } + ], + "runtimeFixture": { + "path": "tests/files/web/image_resource_matrix.blend", + "sha256": "21e16821b62dad84539560bfa53ac755f097dbcd33ca2d614c07123461cdd74e", + "nestedLibraries": [ + { + "filepath": "//resources/image_resource_library.blend", + "isArchive": false + } + ] + }, + "dataBlockSurface": { + "discoverableCollections": [ + "actions", + "annotations", + "armatures", + "brushes", + "cache_files", + "cameras", + "collections", + "curves", + "fonts", + "grease_pencils", + "hair_curves", + "images", + "lattices", + "lightprobes", + "lights", + "linestyles", + "masks", + "materials", + "meshes", + "metaballs", + "movieclips", + "node_groups", + "objects", + "paint_curves", + "palettes", + "particles", + "pointclouds", + "scenes", + "screens", + "sounds", + "speakers", + "texts", + "textures", + "volumes", + "workspaces", + "worlds" + ], + "onlyAppendableCollections": ["screens", "workspaces"], + "nonRootRoles": [ + {"collection": "libraries", "role": "SOURCE_AND_TRANSITIVE_LIBRARY_METADATA"}, + {"collection": "shape_keys", "role": "EMBEDDED_OWNER_DEPENDENCY"}, + {"collection": "window_managers", "role": "CURRENT_FILE_RUNTIME_ONLY"}, + {"collection": "all_ids", "role": "AGGREGATE_QUERY_NOT_LIBRARY_SECTION"} + ] + }, + "operatorSurface": { + "appendProperties": [ + "filepath", "directory", "filename", "files", "check_existing", "filter_blender", + "filter_backup", "filter_image", "filter_movie", "filter_python", "filter_font", + "filter_sound", "filter_text", "filter_archive", "filter_btx", "filter_alembic", + "filter_usd", "filter_obj", "filter_volume", "filter_folder", "filter_blenlib", + "filemode", "display_type", "sort_method", "link", "do_reuse_local_id", + "clear_asset_data", "autoselect", "active_collection", "instance_collections", + "instance_object_data", "set_fake", "use_recursive" + ], + "linkProperties": [ + "filepath", "directory", "filename", "files", "check_existing", "filter_blender", + "filter_backup", "filter_image", "filter_movie", "filter_python", "filter_font", + "filter_sound", "filter_text", "filter_archive", "filter_btx", "filter_alembic", + "filter_usd", "filter_obj", "filter_volume", "filter_folder", "filter_blenlib", + "filemode", "relative_path", "display_type", "sort_method", "link", + "do_reuse_local_id", "clear_asset_data", "autoselect", "active_collection", + "instance_collections", "instance_object_data" + ], + "pythonLoadArguments": [ + "filepath", "link", "pack", "relative", "set_fake", "recursive", "reuse_local_id", + "assets_only", "clear_asset_data", "create_liboverrides", "reuse_liboverrides", + "create_liboverrides_runtime" + ] + }, + "operations": [ + { + "operation": "APPEND", + "entryPoints": ["WM_OT_append", "bpy.data.libraries.load(link=False)"], + "rootCollections": "ALL_DISCOVERABLE", + "dataBlockRoles": [ + "SOURCE_LIBRARY", + "DIRECT_SELECTED_ROOT", + "INDIRECT_ID_DEPENDENCY", + "TRANSITIVE_LIBRARY_DEPENDENCY", + "EMBEDDED_OWNER_DEPENDENCY", + "LIBOVERRIDE_DEPENDENCY", + "REUSABLE_LOCAL_ID", + "INSTANTIATED_COLLECTION_OBJECT_OR_OBJECT_DATA" + ], + "closure": { + "walker": "BKE_library_foreach_ID_link", + "walkFlags": ["IDWALK_NOP"], + "ignoredAsIndependentEdges": [ + "IDWALK_CB_EMBEDDED", + "IDWALK_CB_EMBEDDED_NOT_OWNING", + "IDWALK_CB_INTERNAL", + "IDWALK_CB_LOOPBACK" + ], + "selectedRootTag": "DIRECT", + "discoveredDependencyTag": "INDIRECT", + "overrideDependencyTags": ["LIBOVERRIDE_DEPENDENCY", "LIBOVERRIDE_DEPENDENCY_ONLY"], + "terminalActions": ["KEEP_LINKED", "REUSE_LOCAL", "MAKE_LOCAL", "COPY_LOCAL"], + "recursiveCrossLibraryLocalization": "CONTROLLED_BY_USE_RECURSIVE" + } + }, + { + "operation": "LINK", + "entryPoints": ["WM_OT_link", "bpy.data.libraries.load(link=True)"], + "rootCollections": "DISCOVERABLE_EXCEPT_ONLY_APPENDABLE", + "dataBlockRoles": [ + "SOURCE_LIBRARY", + "DIRECT_LINKED_ROOT", + "INDIRECT_LINKED_ID_DEPENDENCY", + "TRANSITIVE_LIBRARY_DEPENDENCY", + "EMBEDDED_OWNER_DEPENDENCY", + "INSTANTIATED_COLLECTION_OBJECT_OR_OBJECT_DATA" + ], + "closure": { + "readerStages": ["BLO_library_link_begin", "BLO_library_link_named_part", "BLO_library_link_end"], + "selectedRootTag": "ID_TAG_EXTERN", + "discoveredDependencyTag": "ID_TAG_INDIRECT", + "sourceOwnership": "ID.lib", + "transitiveLibraryOwnership": "Library.runtime.parent", + "localization": "NONE" + } + }, + { + "operation": "LIBRARY_OVERRIDE", + "entryPoints": ["bpy.data.libraries.load(link=True, create_liboverrides=True)", "BKE_blendfile_override"], + "rootCollections": "LINK_ROOT_SURFACE_SUBJECT_TO_OVERRIDE_HIERARCHY", + "dataBlockRoles": [ + "LINKED_REFERENCE_ROOT", + "LINKED_REFERENCE_DEPENDENCY", + "LOCAL_OVERRIDE_ID", + "LOCAL_HIERARCHY_ROOT", + "SYSTEM_OVERRIDE_DEPENDENCY", + "OVERRIDE_PROPERTY", + "OVERRIDE_PROPERTY_OPERATION", + "REFERENCE_AND_LOCAL_SUBITEM" + ], + "closure": { + "referencePointer": "IDOverrideLibrary.reference", + "hierarchyRootPointer": "IDOverrideLibrary.hierarchy_root", + "propertyPath": "IDOverrideLibraryProperty.rna_path", + "operationCollection": "IDOverrideLibraryProperty.operations", + "creation": "BKE_lib_override_library_create", + "resync": "BKE_lib_override_library_resync", + "operationDiff": "BKE_lib_override_library_operations_create", + "dependencyRule": "REFERENCE_POINTER_GRAPH_WITH_LOCAL_OVERRIDE_REMAP", + "linkedReferencePreserved": true + } + } + ], + "overrideRNA": { + "IDOverrideLibrary": [ + {"identifier": "reference", "type": "POINTER", "isReadonly": true}, + {"identifier": "hierarchy_root", "type": "POINTER", "isReadonly": true}, + {"identifier": "is_in_hierarchy", "type": "BOOLEAN", "isReadonly": false}, + {"identifier": "is_system_override", "type": "BOOLEAN", "isReadonly": false}, + {"identifier": "properties", "type": "COLLECTION", "isReadonly": true} + ], + "IDOverrideLibraryProperty": [ + {"identifier": "rna_path", "type": "STRING", "isReadonly": true}, + {"identifier": "operations", "type": "COLLECTION", "isReadonly": true} + ], + "IDOverrideLibraryPropertyOperation": [ + {"identifier": "operation", "type": "ENUM", "isReadonly": true}, + {"identifier": "flag", "type": "ENUM", "isReadonly": true}, + {"identifier": "subitem_reference_name", "type": "STRING", "isReadonly": true}, + {"identifier": "subitem_local_name", "type": "STRING", "isReadonly": true}, + {"identifier": "subitem_reference_id", "type": "POINTER", "isReadonly": true}, + {"identifier": "subitem_local_id", "type": "POINTER", "isReadonly": true}, + {"identifier": "subitem_reference_index", "type": "INT", "isReadonly": true}, + {"identifier": "subitem_local_index", "type": "INT", "isReadonly": true}, + {"identifier": "label", "type": "STRING", "isReadonly": true}, + {"identifier": "tooltip", "type": "STRING", "isReadonly": true} + ] + }, + "currentWebGap": { + "represented": ["LIBRARY_SOURCE", "LIBRARY_DEPENDENCY_IDS", "READ_ONLY_FLAG"], + "missing": [ + "PER_ID_SOURCE_LIBRARY", + "DIRECT_INDIRECT_ID_CLASSIFICATION", + "APPEND_LOCAL_OWNERSHIP", + "APPEND_REUSE_WEAK_REFERENCE", + "OVERRIDE_REFERENCE", + "OVERRIDE_HIERARCHY_ROOT", + "OVERRIDE_PROPERTY_OPERATIONS", + "INVALIDATION_TOKEN" + ], + "mutationGate": "LIBRARY_MUTATION_UNAVAILABLE" + }, + "nextTask": "M12-03B" +} diff --git a/tests/golden/M12-03B/library-operation-bindings.json b/tests/golden/M12-03B/library-operation-bindings.json new file mode 100644 index 00000000..4dfbab71 --- /dev/null +++ b/tests/golden/M12-03B/library-operation-bindings.json @@ -0,0 +1,80 @@ +{ + "schemaVersion": 1, + "task": "M12-03B", + "source": { + "schemaVersion": 1, + "sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94", + "sourceLocator": "project-assets/libraries/m12-library-source.blend", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "bindings": [ + { + "schemaVersion": 1, + "operation": "APPEND", + "source": { + "schemaVersion": 1, + "sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94", + "sourceLocator": "project-assets/libraries/m12-library-source.blend", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "sourceDataBlockId": "Object/M12Asset", + "owner": { + "kind": "LOCAL_MAIN", + "projectId": "project:m12", + "localDataBlockId": "Object/M12Asset" + }, + "readOnly": false, + "referenceReadOnly": false, + "sourceGeneration": 3, + "sourceRevision": 7, + "dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "invalidationToken": "libtoken:c650807d1afb1c64d1e9deba45120b2bff5336dfdff52ce61c5d3645be923e6a" + }, + { + "schemaVersion": 1, + "operation": "LINK", + "source": { + "schemaVersion": 1, + "sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94", + "sourceLocator": "project-assets/libraries/m12-library-source.blend", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "sourceDataBlockId": "Object/M12Asset", + "owner": { + "kind": "SOURCE_LIBRARY", + "sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94", + "sourceDataBlockId": "Object/M12Asset" + }, + "readOnly": true, + "referenceReadOnly": true, + "sourceGeneration": 3, + "sourceRevision": 7, + "dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "invalidationToken": "libtoken:028c1336cb8f27596daa8754654be461ab10af66caa35f706eac3f4b9480cfa4" + }, + { + "schemaVersion": 1, + "operation": "LIBRARY_OVERRIDE", + "source": { + "schemaVersion": 1, + "sourceLibraryId": "library:f35430c19c85351788053ceb44ba4dc01ec0a190a985197e60826d9d52c55a94", + "sourceLocator": "project-assets/libraries/m12-library-source.blend", + "sourceSha256": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa" + }, + "sourceDataBlockId": "Object/M12Asset", + "owner": { + "kind": "LOCAL_OVERRIDE", + "projectId": "project:m12", + "localDataBlockId": "Object/M12AssetOverride", + "referenceSourceDataBlockId": "Object/M12Asset", + "hierarchyRootDataBlockId": "Collection/M12Root" + }, + "readOnly": false, + "referenceReadOnly": true, + "sourceGeneration": 3, + "sourceRevision": 7, + "dependencyClosureSha256": "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", + "invalidationToken": "libtoken:a308528792e6056af25bebb3a349ac7e1ff5c4e40189d2cb7aad453a8a8299fe" + } + ] +} diff --git a/tests/golden/M12-03B/manifest.json b/tests/golden/M12-03B/manifest.json new file mode 100644 index 00000000..918741c7 --- /dev/null +++ b/tests/golden/M12-03B/manifest.json @@ -0,0 +1,42 @@ +{ + "schemaVersion": 1, + "task": "M12-03B", + "parentTask": "M12-03", + "enablingTask": true, + "parityStateChange": false, + "operations": ["APPEND", "LINK", "LIBRARY_OVERRIDE"], + "ownershipSemantics": { + "APPEND": {"owner": "LOCAL_MAIN", "readOnly": false, "referenceReadOnly": false}, + "LINK": {"owner": "SOURCE_LIBRARY", "readOnly": true, "referenceReadOnly": true}, + "LIBRARY_OVERRIDE": {"owner": "LOCAL_OVERRIDE", "readOnly": false, "referenceReadOnly": true} + }, + "invalidationFields": [ + "operation", + "sourceLibraryId", + "sourceSha256", + "sourceDataBlockId", + "owner", + "sourceGeneration", + "sourceRevision", + "dependencyClosureSha256" + ], + "artifacts": { + "parentInventory": { + "path": "tests/golden/M12-03A/library-operation-inventory.json", + "sha256": "0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7" + }, + "protocol": { + "path": "web/protocol/library-operation-identity.ts", + "sha256": "5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451" + }, + "golden": { + "path": "tests/golden/M12-03B/library-operation-bindings.json", + "sha256": "b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f" + }, + "unitSuite": { + "path": "web/tests/unit/library-operation-identity.test.mjs", + "sha256": "0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547" + } + }, + "nextTask": "M12-03C" +} diff --git a/tests/golden/M12-03C/desktop-append-report.json b/tests/golden/M12-03C/desktop-append-report.json new file mode 100644 index 00000000..3cd554b5 --- /dev/null +++ b/tests/golden/M12-03C/desktop-append-report.json @@ -0,0 +1,202 @@ +{ + "appendedGraph": { + "edges": [ + { + "from": "Object/M12 Append Object", + "relation": "OBJECT_DATA", + "to": "Mesh/M12 Append Mesh" + }, + { + "from": "Mesh/M12 Append Mesh", + "relation": "MATERIAL_SLOT[0]", + "to": "Material/M12 Append Material" + }, + { + "from": "Material/M12 Append Material", + "relation": "NODE_IMAGE[M12 Append Image Node]", + "to": "Image/M12 Append Image" + } + ], + "geometry": { + "edges": 4, + "loops": 4, + "materialSlots": [ + "M12 Append Material" + ], + "polygons": 1, + "uvLayers": [ + "UVMap" + ], + "vertices": 4 + }, + "ids": { + "IMAGE": { + "idType": "IMAGE", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Image", + "nameFull": "M12 Append Image" + }, + "MATERIAL": { + "idType": "MATERIAL", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Material", + "nameFull": "M12 Append Material" + }, + "MESH": { + "idType": "MESH", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Mesh", + "nameFull": "M12 Append Mesh" + }, + "OBJECT": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Object", + "nameFull": "M12 Append Object" + } + }, + "image": { + "channels": 4, + "colorspace": "sRGB", + "packed": true, + "pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a", + "size": [ + 2, + 2 + ] + }, + "root": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Object", + "nameFull": "M12 Append Object" + }, + "sourceMarker": "M12-03C" + }, + "blenderVersion": "5.2.0", + "nextTask": "M12-03D", + "operation": "APPEND", + "schemaVersion": 1, + "selectedRoots": [ + "Object/M12 Append Object" + ], + "source": { + "file": "m12_append_source.blend", + "sha256": "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606" + }, + "sourceGraph": { + "edges": [ + { + "from": "Object/M12 Append Object", + "relation": "OBJECT_DATA", + "to": "Mesh/M12 Append Mesh" + }, + { + "from": "Mesh/M12 Append Mesh", + "relation": "MATERIAL_SLOT[0]", + "to": "Material/M12 Append Material" + }, + { + "from": "Material/M12 Append Material", + "relation": "NODE_IMAGE[M12 Append Image Node]", + "to": "Image/M12 Append Image" + } + ], + "geometry": { + "edges": 4, + "loops": 4, + "materialSlots": [ + "M12 Append Material" + ], + "polygons": 1, + "uvLayers": [ + "UVMap" + ], + "vertices": 4 + }, + "ids": { + "IMAGE": { + "idType": "IMAGE", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Image", + "nameFull": "M12 Append Image" + }, + "MATERIAL": { + "idType": "MATERIAL", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Material", + "nameFull": "M12 Append Material" + }, + "MESH": { + "idType": "MESH", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Mesh", + "nameFull": "M12 Append Mesh" + }, + "OBJECT": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Object", + "nameFull": "M12 Append Object" + } + }, + "image": { + "channels": 4, + "colorspace": "sRGB", + "packed": true, + "pixelFloat32Sha256": "6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a", + "size": [ + 2, + 2 + ] + }, + "root": { + "idType": "OBJECT", + "isLibraryOverride": false, + "library": null, + "name": "M12 Append Object", + "nameFull": "M12 Append Object" + }, + "sourceMarker": "M12-03C" + }, + "stableMapping": [ + { + "local": "Object/M12 Append Object", + "owner": "LOCAL_MAIN", + "readOnly": false, + "source": "Object/M12 Append Object" + }, + { + "local": "Mesh/M12 Append Mesh", + "owner": "LOCAL_MAIN", + "readOnly": false, + "source": "Mesh/M12 Append Mesh" + }, + { + "local": "Material/M12 Append Material", + "owner": "LOCAL_MAIN", + "readOnly": false, + "source": "Material/M12 Append Material" + }, + { + "local": "Image/M12 Append Image", + "owner": "LOCAL_MAIN", + "readOnly": false, + "source": "Image/M12 Append Image" + } + ], + "target": { + "file": "m12_append_target.blend", + "sha256": "5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c" + }, + "task": "M12-03C" +} diff --git a/tests/golden/M12-03C/manifest.json b/tests/golden/M12-03C/manifest.json new file mode 100644 index 00000000..fb50f2eb --- /dev/null +++ b/tests/golden/M12-03C/manifest.json @@ -0,0 +1,39 @@ +{ + "schemaVersion": 1, + "task": "M12-03C", + "parentTask": "M12-03", + "enablingTask": true, + "parityStateChange": false, + "runtime": "BLENDER_5_2_DESKTOP", + "operation": "APPEND", + "selectedRoots": ["Object/M12 Append Object"], + "dependencyTypes": ["OBJECT", "MESH", "MATERIAL", "IMAGE"], + "containerDeterminism": "SESSION_BOUND_SEMANTIC_REPORT_EXACT", + "artifacts": { + "parentContract": { + "path": "tests/golden/M12-03B/manifest.json", + "sha256": "f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b" + }, + "generator": { + "path": "tools/web/generate-library-append-fixture.py", + "sha256": "b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f" + }, + "checker": { + "path": "tools/web/check-library-append-fixture.mjs", + "sha256": "6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d" + }, + "sourceBlend": { + "path": "tests/files/web/m12_library_append_v1/m12_append_source.blend", + "sha256": "5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606" + }, + "targetBlend": { + "path": "tests/files/web/m12_library_append_v1/m12_append_target.blend", + "sha256": "5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c" + }, + "desktopReport": { + "path": "tests/golden/M12-03C/desktop-append-report.json", + "sha256": "b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a" + } + }, + "nextTask": "M12-03D" +} diff --git a/tools/web/check-asset-catalog-field-inventory.mjs b/tools/web/check-asset-catalog-field-inventory.mjs new file mode 100644 index 00000000..a1033fab --- /dev/null +++ b/tools/web/check-asset-catalog-field-inventory.mjs @@ -0,0 +1,136 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-01A/asset-catalog-field-inventory.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const sourceByRole = new Map(); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + +assert.equal(manifest.schemaVersion, 1); +assert.equal(manifest.task, "M12-01A"); +assert.equal(manifest.enablingTask, true); +assert.equal(manifest.parityStateChange, false); +assert.equal(manifest.blenderVersion, "5.2.0"); +assert.equal(manifest.nextTask, "M12-01B"); + +const expectedRoles = [ + "DNA_ASSET_STORAGE", + "RNA_ASSET_API", + "ASSET_BLEND_IO", + "CATALOG_MODEL", + "CATALOG_PATH", + "CATALOG_FILE_API", + "CATALOG_FILE_FORMAT", + "CURRENT_WEB_V1_SNAPSHOT", +]; +assert.deepEqual(manifest.sources.map((source) => source.role), expectedRoles); +for (const source of manifest.sources) { + const absolutePath = path.join(root, source.path); + const bytes = fs.readFileSync(absolutePath); + assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`); + sourceByRole.set(source.role, bytes.toString("utf8")); +} + +const expectedTagFields = ["next", "prev", "name"]; +const expectedDNAFields = [ + "local_type_info", + "properties", + "catalog_id", + "catalog_simple_name", + "author", + "description", + "copyright", + "license", + "tags", + "active_tag", + "tot_tags", + "flag", + "preferred_import_method", + "_pad", +]; +assert.deepEqual(manifest.assetTag.fields.map((field) => field.name), expectedTagFields); +assert.deepEqual(manifest.assetMetaData.dnaFields.map((field) => field.name), expectedDNAFields); +assert.equal(new Set(expectedDNAFields).size, manifest.assetMetaData.dnaFields.length); + +const dna = sourceByRole.get("DNA_ASSET_STORAGE"); +for (const field of [...manifest.assetTag.fields, ...manifest.assetMetaData.dnaFields]) { + assert.ok(dna.includes(field.sourceToken), `DNA token for ${field.name} is missing`); +} +assert.equal(manifest.assetTag.fields.find((field) => field.name === "name").maximumStorageBytesIncludingNull, 64); +assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "properties").semantic, "CUSTOM_METADATA_NO_ID_POINTERS"); +assert.equal(manifest.assetMetaData.dnaFields.find((field) => field.name === "catalog_simple_name").semantic, "RECOVERY_ONLY_NOT_AUTHORITY"); + +const rna = sourceByRole.get("RNA_ASSET_API"); +const assetDataDefinition = rna.slice(rna.indexOf("static void rna_def_asset_data"), rna.indexOf("static void rna_def_asset_representation")); +const sourceRNAProperties = [...assetDataDefinition.matchAll(/RNA_def_property\(srna, "([^"]+)"/g)].map((match) => match[1]); +assert.deepEqual(sourceRNAProperties, manifest.assetMetaData.rnaProperties.map((property) => property.identifier)); +assert.match(rna, /RNA_def_property_string_maxlength\(prop, MAX_NAME\)/); +assert.match(rna, /RNA_def_function\(srna, "new", "rna_AssetMetaData_tag_new"\)/); +assert.match(rna, /RNA_def_function\(srna, "remove", "rna_AssetMetaData_tag_remove"\)/); + +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); +const python = [ + "import bpy,json", + "props=[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly,'isRuntime':p.is_runtime} for p in bpy.types.AssetMetaData.bl_rna.properties if p.identifier != 'rna_type']", + "print('M12_RNA='+json.dumps(props,separators=(',',':')))", +].join(";"); +const runtimeOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", python], { encoding: "utf8" }); +const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_RNA=")); +assert.ok(runtimeLine, "Blender AssetMetaData RNA report is missing"); +assert.deepEqual(JSON.parse(runtimeLine.slice("M12_RNA=".length)), manifest.assetMetaData.rnaProperties); + +const blendIO = sourceByRole.get("ASSET_BLEND_IO"); +for (const value of ["properties", "author", "description", "copyright", "license", "tags"]) { + assert.match(blendIO, new RegExp(`asset_data->${value}`), `${value} is not bound to Blend IO`); +} +assert.match(blendIO, /asset_data->tags\.count\(\) == asset_data->tot_tags/); + +const catalog = sourceByRole.get("CATALOG_MODEL"); +for (const field of [...manifest.catalog.semanticFields, ...manifest.catalog.runtimeFlags]) { + assert.ok(catalog.includes(field.sourceToken), `catalog token for ${field.name} is missing`); +} +assert.deepEqual(manifest.catalog.definitionFile.recordFields, ["catalog_id", "path", "simple_name"]); +assert.deepEqual(manifest.catalog.definitionFile.writeOrder, ["path", "is_first_loaded", "catalog_id"]); +assert.deepEqual(manifest.catalog.identityRules.duplicatePathSelectionOrder, ["is_first_loaded", "catalog_id"]); + +const catalogFile = sourceByRole.get("CATALOG_FILE_FORMAT"); +assert.match(catalogFile, /SUPPORTED_VERSION = 1/); +assert.match(catalogFile, /VERSION_MARKER = "VERSION "/); +assert.match(catalogFile, /catalog->catalog_id << ":" << catalog->path << ":" << catalog->simple_name/); +const catalogPath = sourceByRole.get("CATALOG_PATH"); +for (const statement of [ + "Only slashes are used as path component separators", + "Paths are stored as byte sequences, and assumed to be UTF8", + "Empty components (caused by double slashes or leading/trailing slashes) are removed", +]) assert.ok(catalogPath.includes(statement)); + +const webPath = path.join(root, manifest.sources.find((source) => source.role === "CURRENT_WEB_V1_SNAPSHOT").path); +const webSource = ts.createSourceFile(webPath, fs.readFileSync(webPath, "utf8"), ts.ScriptTarget.Latest, true); +function interfaceFields(name) { + const declaration = webSource.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name); + assert.ok(declaration, `missing Web interface ${name}`); + return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(webSource)); +} +assert.deepEqual(interfaceFields("AssetCatalogIR"), manifest.currentWebSnapshot.assetCatalogIRFields); +assert.deepEqual(interfaceFields("AssetEntryIR"), manifest.currentWebSnapshot.assetEntryIRFields); +assert.deepEqual(manifest.currentWebSnapshot.missingMetadata, [ + "properties", + "description", + "copyright", + "catalog_simple_name", + "active_tag", + "use_preferred_import_method", + "preferred_import_method", +]); +assert.equal(manifest.currentWebSnapshot.semanticDrift.length, 5); + +process.stdout.write( + `asset-catalog-field-inventory-ok dna=${expectedDNAFields.length} rna=${sourceRNAProperties.length} tag=${expectedTagFields.length} catalog=${manifest.catalog.semanticFields.length} gaps=${manifest.currentWebSnapshot.missingMetadata.length} next=${manifest.nextTask}\n`, +); diff --git a/tools/web/check-asset-catalog-m12-evidence.mjs b/tools/web/check-asset-catalog-m12-evidence.mjs new file mode 100644 index 00000000..269b84d8 --- /dev/null +++ b/tools/web/check-asset-catalog-m12-evidence.mjs @@ -0,0 +1,67 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const read = (file) => JSON.parse(fs.readFileSync(path.join(root, file), "utf8")); +const sha256File = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); +const record = (value) => typeof value === "object" && value !== null && !Array.isArray(value); +const stableJSON = (value) => { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + return JSON.stringify(value); +}; +const sha256Value = (value) => crypto.createHash("sha256").update(stableJSON(value)).digest("hex"); +const checkArtifact = (artifact) => assert.equal(sha256File(artifact.path), artifact.sha256, artifact.path); + +const evidence = read("tests/golden/M12-01/evidence.json"); +assert.equal(evidence.task, "M12-01I"); +assert.equal(evidence.status, "READY"); +assert.equal(evidence.enablingTask, true); +assert.equal(evidence.parityStateChange, false); +assert.equal(evidence.nextTask, "M12-02A"); +assert.equal(evidence.subtasks.length, 8); + +const taskNames = evidence.subtasks.map((entry) => entry.task); +assert.deepEqual(taskNames, ["M12-01A", "M12-01B", "M12-01C", "M12-01D", "M12-01E", "M12-01F", "M12-01G", "M12-01H"]); +evidence.subtasks.forEach((entry, index) => { + checkArtifact(entry); + const manifest = read(entry.path); + assert.equal(manifest.task, entry.task); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, index === evidence.subtasks.length - 1 ? "M12-01I" : evidence.subtasks[index + 1].task); + if (Array.isArray(manifest.sources)) manifest.sources.forEach(checkArtifact); + if (record(manifest.artifacts)) Object.values(manifest.artifacts).forEach(checkArtifact); +}); + +Object.values(evidence.schema).forEach(checkArtifact); +for (const key of ["sourceFixture", "targetFixture", "report", "productionProtocol"]) checkArtifact(evidence.migration[key]); +for (const key of ["indexedDB", "restartWorker"]) checkArtifact(evidence.runtime[key]); + +const source = read(evidence.migration.sourceFixture.path); +const target = read(evidence.migration.targetFixture.path); +const report = read(evidence.migration.report.path); +assert.equal(source.schemaVersion, 1); +assert.equal(target.schemaVersion, 2); +assert.equal(source.revision, evidence.migration.revision); +assert.equal(target.revision, evidence.migration.revision); +assert.equal(sha256Value(source), evidence.migration.sourceManifestSha256); +assert.equal(sha256Value(target), evidence.migration.targetManifestSha256); +assert.equal(report.sourceManifestSha256, evidence.migration.sourceManifestSha256); +assert.equal(report.targetManifestSha256, evidence.migration.targetManifestSha256); +assert.equal(report.sourceRevision, evidence.migration.revision); +assert.equal(report.targetRevision, evidence.migration.revision); +assert.equal(report.preserved.catalogs, evidence.runtime.catalogCount); +assert.equal(report.preserved.assets, evidence.runtime.assetCount); + +const restart = read("tests/golden/M12-01H/manifest.json").restart; +assert.deepEqual(restart.catalogOrder, target.catalogs.map((catalog) => catalog.catalogId)); +assert.deepEqual(restart.assetOrder, target.assets.map((asset) => asset.assetId)); +assert.equal(restart.manifestSha256, evidence.migration.targetManifestSha256); +assert.deepEqual(restart.contexts, evidence.runtime.restartContexts); +assert.deepEqual(read("tests/golden/M12-01G/manifest.json").transaction.faultPoints, evidence.runtime.faultPoints); + +process.stdout.write(`asset-catalog-m12-evidence-ok subtasks=${evidence.subtasks.length} catalogs=${evidence.runtime.catalogCount} assets=${evidence.runtime.assetCount} next=${evidence.nextTask}\n`); diff --git a/tools/web/check-asset-catalog-v1-fixture.mjs b/tools/web/check-asset-catalog-v1-fixture.mjs new file mode 100644 index 00000000..2d6101fc --- /dev/null +++ b/tools/web/check-asset-catalog-v1-fixture.mjs @@ -0,0 +1,80 @@ +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 { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifestPath = path.join(root, "tests/golden/M12-01B/manifest.json"); +const manifest = JSON.parse(fs.readFileSync(manifestPath, "utf8")); +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const artifact = (name) => path.join(root, manifest.artifacts[name].path); + +assert.equal(manifest.schemaVersion, 1); +assert.equal(manifest.task, "M12-01B"); +assert.equal(manifest.enablingTask, true); +assert.equal(manifest.parityStateChange, false); +assert.equal(manifest.nextTask, "M12-01C"); +assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); +for (const name of ["generator", "exporter", "checker", "fixture", "catalogDefinition", "canonicalReport"]) { + assert.equal(sha256(artifact(name)), manifest.artifacts[name].sha256, `${name} hash drifted`); +} + +const expected = JSON.parse(fs.readFileSync(artifact("canonicalReport"), "utf8")); +assert.equal(expected.schemaVersion, 1); +assert.equal(expected.blenderVersion, "5.2.0"); +assert.equal(expected.catalogDefinition.version, 1); +assert.equal(expected.catalogDefinition.records.length, 3); +assert.deepEqual(expected.catalogDefinition.records.map((item) => item.path), ["Characters", "Characters/Heroes", "Materials/Metal"]); +assert.deepEqual(expected.catalogDefinition.records.map((item) => item.parentPath), [null, "Characters", "Materials"]); +assert.equal(expected.assets.length, 3); +assert.deepEqual(expected.assets.map((item) => `${item.idType}:${item.name}`), [ + "MATERIAL:M12 Brushed Metal", + "OBJECT:M12 Hero", + "WORLD:M12 Uncataloged World", +]); +const hero = expected.assets.find((item) => item.name === "M12 Hero"); +assert.deepEqual(hero.tags, ["character", "hero", "rig-ready"]); +assert.equal(hero.activeTag, 1); +assert.equal(hero.catalogId, "22222222-2222-4222-8222-222222222222"); +assert.equal(hero.catalogSimpleName, ""); +assert.equal(hero.usePreferredImportMethod, true); +assert.equal(hero.preferredImportMethod, "APPEND"); +assert.deepEqual(hero.customProperties.map((item) => item.name), ["approved", "dimensions", "rating", "source"]); +assert.deepEqual(hero.customProperties.find((item) => item.name === "dimensions").value, [2, 2, 0]); +const material = expected.assets.find((item) => item.name === "M12 Brushed Metal"); +assert.equal(material.author, ""); +assert.equal(material.license, ""); +const world = expected.assets.find((item) => item.name === "M12 Uncataloged World"); +assert.equal(world.catalogId, "00000000-0000-0000-0000-000000000000"); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-asset-catalog-v1-")); +try { + execFileSync(blender, [ + "--background", "--factory-startup", "--python", artifact("generator"), "--", temporary, + ], { cwd: root, stdio: "pipe" }); + const generatedFixture = path.join(temporary, "m12_asset_catalog_v1.blend"); + const generatedCatalog = path.join(temporary, "blender_assets.cats.txt"); + const generatedReport = path.join(temporary, "canonical.json"); + assert.ok(fs.statSync(generatedFixture).size > 0); + assert.equal(fs.readFileSync(generatedCatalog, "utf8"), fs.readFileSync(artifact("catalogDefinition"), "utf8")); + execFileSync(blender, [ + "--background", generatedFixture, "--python", artifact("exporter"), "--", generatedCatalog, generatedReport, + ], { cwd: root, stdio: "pipe" }); + assert.deepEqual(JSON.parse(fs.readFileSync(generatedReport, "utf8")), expected); + + const reopenedReport = path.join(temporary, "checked-in-canonical.json"); + execFileSync(blender, [ + "--background", artifact("fixture"), "--python", artifact("exporter"), "--", + artifact("catalogDefinition"), reopenedReport, + ], { cwd: root, stdio: "pipe" }); + assert.deepEqual(JSON.parse(fs.readFileSync(reopenedReport, "utf8")), expected); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`asset-catalog-v1-fixture-ok catalogs=3 assets=3 metadata=complete next=${manifest.nextTask}\n`); diff --git a/tools/web/check-asset-preview-display.mjs b/tools/web/check-asset-preview-display.mjs new file mode 100644 index 00000000..a64c5349 --- /dev/null +++ b/tools/web/check-asset-preview-display.mjs @@ -0,0 +1,48 @@ +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 { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8")); +const content = path.join(root, "tests/golden/M12-02B/preview.png"); +const generator = path.join(root, "tools/web/generate-asset-preview-identity.py"); +const source = path.join(root, "tests/files/web/media/sequencer-frame.png"); +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"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-display-")); + +try { + const regenerated = path.join(temporary, "preview.png"); + const raw = path.join(temporary, "preview.rgba8"); + const output = execFileSync(blender, ["--background", "--factory-startup", "--python", generator, "--", source, regenerated], { + cwd: root, + encoding: "utf8", + }); + assert.match(output, /asset-preview-generated width=8 height=8/); + assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); + assert.equal(sha256(fs.readFileSync(regenerated)), report.contentSha256); + execFileSync("magick", [regenerated, "-depth", "8", `RGBA:${raw}`], { cwd: root, stdio: "pipe" }); + const pixels = fs.readFileSync(raw); + assert.equal(pixels.byteLength, report.pixelCount * 4); + assert.equal(sha256(pixels), report.rgbaSha256); + const unique = new Set(); + for (let offset = 0; offset < pixels.byteLength; offset += 4) { + const pixel = Array.from(pixels.subarray(offset, offset + 4)); + unique.add(pixel.join(",")); + assert.deepEqual(pixel, report.referencePixel); + } + assert.equal(unique.size, report.uniquePixelCount); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); + } +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`asset-preview-display-desktop-ok size=${report.width}x${report.height} rgba=${report.rgbaSha256} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-asset-preview-identity.mjs b/tools/web/check-asset-preview-identity.mjs new file mode 100644 index 00000000..52d16200 --- /dev/null +++ b/tools/web/check-asset-preview-identity.mjs @@ -0,0 +1,50 @@ +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 { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/manifest.json"), "utf8")); +const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(file)).digest("hex"); +const blender = process.env.BLENDER_BIN ?? path.join(root, manifest.artifacts.blender.path); +const source = path.join(root, manifest.artifacts.source.path); +const generator = path.join(root, manifest.artifacts.generator.path); +const golden = path.join(root, manifest.artifacts.content.path); + +assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); +assert.equal(sha256(blender), identity.generator.executableSha256); +assert.equal(sha256(generator), identity.generator.scriptSha256); +assert.equal(sha256(source), identity.source.sha256); +assert.equal(fs.statSync(source).size, identity.source.byteLength); +assert.equal(sha256(golden), identity.content.sha256); +assert.equal(fs.statSync(golden).size, identity.content.byteLength); + +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-asset-preview-identity-")); +try { + const first = path.join(temporary, "first.png"); + const second = path.join(temporary, "second.png"); + for (const output of [first, second]) { + execFileSync(blender, ["--background", "--factory-startup", "--python", generator, "--", source, output], { cwd: root, stdio: "pipe" }); + assert.equal(sha256(output), identity.content.sha256); + assert.equal(fs.statSync(output).size, identity.content.byteLength); + } + const probe = [ + "import bpy,json", + `a=bpy.data.images.load(${JSON.stringify(source)},check_existing=False)`, + `b=bpy.data.images.load(${JSON.stringify(first)},check_existing=False)`, + "print('M12_PREVIEW_COMPARE='+json.dumps({'source':list(a.size),'content':list(b.size),'max':max(abs(x-y) for x,y in zip(a.pixels[:],b.pixels[:]))},separators=(',',':')))", + ].join(";"); + const output = execFileSync(blender, ["--background", "--factory-startup", "--python-expr", probe], { cwd: root, encoding: "utf8" }); + const line = output.split(/\r?\n/).find((item) => item.startsWith("M12_PREVIEW_COMPARE=")); + assert.ok(line); + assert.deepEqual(JSON.parse(line.slice("M12_PREVIEW_COMPARE=".length)), { source: [8, 8], content: [8, 8], max: 0 }); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write(`asset-preview-identity-ok source=${identity.source.sha256} content=${identity.content.sha256} size=${identity.content.width}x${identity.content.height} next=${manifest.nextTask}\n`); diff --git a/tools/web/check-asset-preview-inventory.mjs b/tools/web/check-asset-preview-inventory.mjs new file mode 100644 index 00000000..4d34260f --- /dev/null +++ b/tools/web/check-asset-preview-inventory.mjs @@ -0,0 +1,98 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02A/asset-preview-inventory.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +assert.equal(inventory.task, "M12-02A"); +assert.equal(inventory.enablingTask, true); +assert.equal(inventory.parityStateChange, false); +assert.equal(inventory.blenderVersion, "5.2.0"); +assert.equal(inventory.nextTask, "M12-02B"); +for (const item of [...inventory.sources, ...inventory.fixtures]) assert.equal(sha256(item.path), item.sha256, item.path); + +const source = (role) => fs.readFileSync(path.join(root, inventory.sources.find((item) => item.role === role).path), "utf8"); +const dna = source("PREVIEW_DNA"); +for (const token of ["unsigned int w[2]", "unsigned int h[2]", "short flag[2]", "short changed_timestamp[2]", "unsigned int *rect[2]", "PreviewImageRuntime *runtime"]) { + assert.ok(dna.includes(token), `PreviewImage DNA token is missing: ${token}`); +} +const slots = source("PREVIEW_SLOT_ENUM"); +assert.match(slots, /ICON_SIZE_ICON = 0/); +assert.match(slots, /ICON_SIZE_PREVIEW = 1/); +assert.equal(inventory.storage.slotCount, 2); + +const implementation = source("PREVIEW_IMPLEMENTATION"); +assert.match(implementation, /PreviewImage assumes pre-multiplied alpha/); +assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[0\] \* prv_copy\.h\[0\]/); +assert.match(implementation, /writer->write_uint32_array\(prv_copy\.w\[1\] \* prv_copy\.h\[1\]/); +assert.match(implementation, /prv->flag\[i\] &= ~PRV_RENDERING/); +assert.equal(inventory.storage.colorSpace, null); +assert.ok(!inventory.storage.fields.some((field) => /color/i.test(field.name))); + +const rnaSource = source("PREVIEW_RNA"); +assert.match(rnaSource, /Image pixels, as bytes \(always 32-bit RGBA\)/); +assert.match(rnaSource, /length\[0\] = prv_img->w\[size\] \* prv_img->h\[size\] \* 4/); +assert.match(rnaSource, /values\[i\] = data\[i\] \* \(1\.0f \/ 255\.0f\)/); +assert.doesNotMatch(rnaSource.slice(rnaSource.indexOf("static void rna_def_image_preview"), rnaSource.indexOf("static void rna_def_image_user")), /color.?space/i); + +const parseInterface = (file, name) => { + const absolute = path.join(root, file); + const parsed = ts.createSourceFile(absolute, fs.readFileSync(absolute, "utf8"), ts.ScriptTarget.Latest, true); + const declaration = parsed.statements.find((statement) => ts.isInterfaceDeclaration(statement) && statement.name.text === name); + assert.ok(declaration, `${name} is missing`); + return declaration.members.filter(ts.isPropertySignature).map((member) => member.name.getText(parsed)); +}; +assert.deepEqual(parseInterface("web/protocol/asset-library-io.ts", "AssetPreviewIR"), inventory.currentWeb.v1Fields); +assert.deepEqual(parseInterface("web/protocol/asset-catalog-v2.ts", "AssetCatalogV2PreviewIR"), inventory.currentWeb.v2Fields); + +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); +const rnaProbe = [ + "import bpy,json", + "props=[{'identifier':p.identifier,'type':p.type,'readOnly':p.is_readonly,'arrayLength':p.array_length} for p in bpy.types.ImagePreview.bl_rna.properties if p.identifier != 'rna_type']", + "idp=bpy.types.ID.bl_rna.properties['preview']", + "print('M12_PREVIEW_RNA='+json.dumps({'idProperty':{'identifier':idp.identifier,'type':idp.type,'readOnly':idp.is_readonly,'nullWhenAbsent':True},'imagePreviewProperties':props},separators=(',',':')))", +].join(";"); +const rnaOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", rnaProbe], { encoding: "utf8" }); +const rnaLine = rnaOutput.split(/\r?\n/).find((line) => line.startsWith("M12_PREVIEW_RNA=")); +assert.ok(rnaLine); +assert.deepEqual(JSON.parse(rnaLine.slice("M12_PREVIEW_RNA=".length)), { + idProperty: inventory.rna.idProperty, + imagePreviewProperties: inventory.rna.imagePreviewProperties, +}); + +const noPreviewScript = [ + "import bpy,json", + "items=[]", + "groups=(bpy.data.objects,bpy.data.materials,bpy.data.worlds)", + "[items.append({'type':item.bl_rna.identifier,'preview':None if item.preview is None else list(item.preview.image_size)}) for group in groups for item in group if item.asset_data]", + "print('M12_NO_PREVIEW='+json.dumps(items,separators=(',',':')))", +].join(";"); +const noPreviewOutput = execFileSync(blender, ["--factory-startup", "--background", path.join(root, inventory.fixtures[1].path), "--python-expr", noPreviewScript], { encoding: "utf8" }); +const noPreviewLine = noPreviewOutput.split(/\r?\n/).find((line) => line.startsWith("M12_NO_PREVIEW=")); +assert.ok(noPreviewLine); +const noPreview = JSON.parse(noPreviewLine.slice("M12_NO_PREVIEW=".length)); +assert.deepEqual(noPreview.map((item) => item.type), inventory.runtimeStates.noPreview.assetTypes); +assert.deepEqual(noPreview.map((item) => item.preview), inventory.runtimeStates.noPreview.previewValues); + +const png = path.join(root, inventory.fixtures[0].path); +const loadedScript = [ + "import bpy,json,bpy.utils.previews", + "collection=bpy.utils.previews.new()", + `preview=collection.load('m12-preview',${JSON.stringify(png)},'IMAGE',True)`, + "result={'sourceSize':[8,8],'imageSize':list(preview.image_size),'iconSize':list(preview.icon_size),'imagePackedPixelCount':len(preview.image_pixels),'imageFloatComponentCount':len(preview.image_pixels_float),'iconPackedPixelCount':len(preview.icon_pixels),'imageCustom':preview.is_image_custom,'iconCustom':preview.is_icon_custom}", + "print('M12_LOADED_PREVIEW='+json.dumps(result,separators=(',',':')))", + "bpy.utils.previews.remove(collection)", +].join(";"); +const loadedOutput = execFileSync(blender, ["--factory-startup", "--background", "--python-expr", loadedScript], { encoding: "utf8" }); +const loadedLine = loadedOutput.split(/\r?\n/).find((line) => line.startsWith("M12_LOADED_PREVIEW=")); +assert.ok(loadedLine); +assert.deepEqual(JSON.parse(loadedLine.slice("M12_LOADED_PREVIEW=".length)), inventory.runtimeStates.loadedPreview); + +process.stdout.write(`asset-preview-inventory-ok slots=${inventory.storage.slotCount} rna=${inventory.rna.imagePreviewProperties.length} absent=${noPreview.length} loaded=${inventory.runtimeStates.loadedPreview.imageSize.join("x")} next=${inventory.nextTask}\n`); diff --git a/tools/web/check-library-append-fixture.mjs b/tools/web/check-library-append-fixture.mjs new file mode 100644 index 00000000..80e32416 --- /dev/null +++ b/tools/web/check-library-append-fixture.mjs @@ -0,0 +1,100 @@ +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 { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const reportPath = path.join(root, "tests/golden/M12-03C/desktop-append-report.json"); +const report = JSON.parse(fs.readFileSync(reportPath, "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03C/manifest.json"), "utf8")); +const fixtureRoot = path.join(root, "tests/files/web/m12_library_append_v1"); +const generator = path.join(root, "tools/web/generate-library-append-fixture.py"); +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(manifest.task, "M12-03C"); +assert.equal(manifest.nextTask, "M12-03D"); +for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); +} +assert.equal(report.schemaVersion, 1); +assert.equal(report.task, "M12-03C"); +assert.equal(report.operation, "APPEND"); +assert.equal(report.blenderVersion, "5.2.0"); +assert.equal(report.nextTask, "M12-03D"); +assert.deepEqual(report.selectedRoots, ["Object/M12 Append Object"]); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.source.file))), report.source.sha256); +assert.equal(sha256(fs.readFileSync(path.join(fixtureRoot, report.target.file))), report.target.sha256); +assert.deepEqual(report.sourceGraph, report.appendedGraph); +assert.deepEqual(report.appendedGraph.edges, [ + { from: "Object/M12 Append Object", relation: "OBJECT_DATA", to: "Mesh/M12 Append Mesh" }, + { from: "Mesh/M12 Append Mesh", relation: "MATERIAL_SLOT[0]", to: "Material/M12 Append Material" }, + { from: "Material/M12 Append Material", relation: "NODE_IMAGE[M12 Append Image Node]", to: "Image/M12 Append Image" }, +]); +assert.deepEqual(Object.keys(report.appendedGraph.ids).sort(), ["IMAGE", "MATERIAL", "MESH", "OBJECT"]); +for (const value of Object.values(report.appendedGraph.ids)) { + assert.equal(value.library, null); + assert.equal(value.isLibraryOverride, false); +} +assert.deepEqual(report.appendedGraph.geometry, { + edges: 4, + loops: 4, + materialSlots: ["M12 Append Material"], + polygons: 1, + uvLayers: ["UVMap"], + vertices: 4, +}); +assert.deepEqual(report.appendedGraph.image.size, [2, 2]); +assert.equal(report.appendedGraph.image.channels, 4); +assert.equal(report.appendedGraph.image.colorspace, "sRGB"); +assert.equal(report.appendedGraph.image.packed, true); +assert.match(report.appendedGraph.image.pixelFloat32Sha256, /^[a-f0-9]{64}$/); +assert.deepEqual(report.stableMapping.map((item) => [item.owner, item.readOnly]), [ + ["LOCAL_MAIN", false], + ["LOCAL_MAIN", false], + ["LOCAL_MAIN", false], + ["LOCAL_MAIN", false], +]); +assert.deepEqual(report.stableMapping.map((item) => item.source), [ + "Object/M12 Append Object", + "Mesh/M12 Append Mesh", + "Material/M12 Append Material", + "Image/M12 Append Image", +]); +assert.deepEqual(report.stableMapping.map((item) => item.local), report.stableMapping.map((item) => item.source)); + +const normalizeContainerHashes = (value) => ({ + ...value, + source: { ...value.source, sha256: "" }, + target: { ...value.target, sha256: "" }, +}); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "m12-library-append-")); +try { + const generatedFixtureRoot = path.join(temporary, "files"); + const generatedReportPath = path.join(temporary, "report.json"); + const output = execFileSync(blender, [ + "--background", + "--factory-startup", + "--python", + generator, + "--", + generatedFixtureRoot, + generatedReportPath, + ], { cwd: root, encoding: "utf8" }); + assert.match(output, /library-append-fixture-ok roots=1 mapping=4/); + assert.match(output, /next=M12-03D/); + const generated = JSON.parse(fs.readFileSync(generatedReportPath, "utf8")); + assert.deepEqual(normalizeContainerHashes(generated), normalizeContainerHashes(report)); + assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.source.file))), generated.source.sha256); + assert.equal(sha256(fs.readFileSync(path.join(generatedFixtureRoot, generated.target.file))), generated.target.sha256); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} + +process.stdout.write( + `library-append-fixture-check-ok roots=${report.selectedRoots.length} mapping=${report.stableMapping.length} edges=${report.appendedGraph.edges.length} pixels=${report.appendedGraph.image.pixelFloat32Sha256} next=${report.nextTask}\n`, +); diff --git a/tools/web/check-library-operation-inventory.mjs b/tools/web/check-library-operation-inventory.mjs new file mode 100644 index 00000000..85b84b21 --- /dev/null +++ b/tools/web/check-library-operation-inventory.mjs @@ -0,0 +1,153 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { execFileSync } from "node:child_process"; +import { fileURLToPath } from "node:url"; + +const root = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../.."); +const inventory = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03A/library-operation-inventory.json"), "utf8")); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); +const sources = new Map(); + +assert.equal(inventory.schemaVersion, 1); +assert.equal(inventory.task, "M12-03A"); +assert.equal(inventory.enablingTask, true); +assert.equal(inventory.parityStateChange, false); +assert.equal(inventory.blenderVersion, "5.2.0"); +assert.equal(inventory.nextTask, "M12-03B"); +assert.deepEqual(inventory.sources.map((source) => source.role), [ + "LINK_APPEND_CONTEXT_API", + "LINK_APPEND_CLOSURE", + "LINK_APPEND_OPERATORS", + "PYTHON_LIBRARY_LOAD", + "BLEND_LIBRARY_READER", + "OVERRIDE_STORAGE", + "OVERRIDE_API", + "OVERRIDE_CLOSURE", + "CURRENT_WEB_LIBRARY_GATE", +]); +for (const source of inventory.sources) { + const bytes = fs.readFileSync(path.join(root, source.path)); + assert.equal(sha256(bytes), source.sha256, `${source.role} source hash drifted`); + sources.set(source.role, bytes.toString("utf8")); +} +assert.equal(sha256(fs.readFileSync(path.join(root, inventory.runtimeFixture.path))), inventory.runtimeFixture.sha256); + +assert.equal(inventory.dataBlockSurface.discoverableCollections.length, 36); +assert.equal(new Set(inventory.dataBlockSurface.discoverableCollections).size, 36); +assert.deepEqual(inventory.dataBlockSurface.onlyAppendableCollections, ["screens", "workspaces"]); +assert.deepEqual(inventory.operations.map((operation) => operation.operation), ["APPEND", "LINK", "LIBRARY_OVERRIDE"]); +for (const operation of inventory.operations) { + assert.ok(operation.dataBlockRoles.length >= 6, `${operation.operation} has an incomplete role inventory`); + assert.ok(operation.closure && Object.keys(operation.closure).length >= 5, `${operation.operation} has an incomplete closure inventory`); +} + +const context = sources.get("LINK_APPEND_CONTEXT_API"); +for (const token of [ + "LINK_APPEND_ACT_KEEP_LINKED", + "LINK_APPEND_ACT_REUSE_LOCAL", + "LINK_APPEND_ACT_MAKE_LOCAL", + "LINK_APPEND_ACT_COPY_LOCAL", + "LINK_APPEND_TAG_INDIRECT", + "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY", + "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY", + "ID *new_id", + "Library *source_library", + "ID *liboverride_id", + "ID *reusable_local_id", + "enum class ProcessStage", +]) assert.ok(context.includes(token), `link/append context token is missing: ${token}`); + +const closure = sources.get("LINK_APPEND_CLOSURE"); +for (const token of [ + "BKE_library_foreach_ID_link", + "IDWALK_CB_EMBEDDED", + "IDWALK_CB_EMBEDDED_NOT_OWNING", + "IDWALK_CB_INTERNAL", + "IDWALK_CB_LOOPBACK", + "LINK_APPEND_TAG_LIBOVERRIDE_DEPENDENCY_ONLY", + "new_id->lib->runtime->parent", +]) assert.ok(closure.includes(token), `dependency closure token is missing: ${token}`); +assert.match(closure, /BKE_blendfile_append\(/); +assert.match(closure, /BKE_blendfile_override\(/); + +const operators = sources.get("LINK_APPEND_OPERATORS"); +for (const token of [ + "BKE_idtype_idcode_is_linkable(idcode)", + "BKE_idtype_idcode_is_only_appendable(idcode)", + "BLO_LIBLINK_APPEND_RECURSIVE", + "BLO_LIBLINK_APPEND_LOCAL_ID_REUSE", + "void WM_OT_link", + "void WM_OT_append", +]) assert.ok(operators.includes(token), `operator inventory token is missing: ${token}`); + +const pythonLoad = sources.get("PYTHON_LIBRARY_LOAD"); +for (const argument of inventory.operatorSurface.pythonLoadArguments) { + assert.ok(pythonLoad.includes(`\"${argument}\"`), `Python load argument is missing: ${argument}`); +} +for (const token of [ + "if (!is_library && !BKE_idtype_idcode_is_linkable(code))", + "if (!BKE_idtype_idcode_is_linkable(idcode) || (idcode == ID_WS && !do_append))", + "create_liboverrides", + "BKE_blendfile_link(lapp_context", + "BKE_blendfile_append(lapp_context", + "BKE_blendfile_override(lapp_context", +]) assert.ok(pythonLoad.includes(token), `Python library closure token is missing: ${token}`); + +const reader = sources.get("BLEND_LIBRARY_READER"); +for (const token of ["BLO_library_link_begin", "BLO_library_link_named_part", "BLO_library_link_end"]) { + assert.ok(reader.includes(token), `reader stage is missing: ${token}`); +} + +const overrideStorage = sources.get("OVERRIDE_STORAGE"); +for (const token of [ + "struct IDOverrideLibraryPropertyOperation", + "struct IDOverrideLibraryProperty", + "struct IDOverrideLibrary", + "ID *reference", + "ID *hierarchy_root", + "ListBaseT properties", +]) assert.ok(overrideStorage.includes(token), `override storage token is missing: ${token}`); +const overrideApi = sources.get("OVERRIDE_API"); +for (const token of [ + "BKE_lib_override_library_create(", + "BKE_lib_override_library_resync(", + "BKE_lib_override_library_operations_create(", +]) assert.ok(overrideApi.includes(token), `override API token is missing: ${token}`); +assert.match(sources.get("OVERRIDE_CLOSURE"), /BKE_library_foreach_ID_link/); + +const web = sources.get("CURRENT_WEB_LIBRARY_GATE"); +assert.match(web, /gateLibraryMutation\(operation:/); +assert.match(web, /LIBRARY_MUTATION_UNAVAILABLE/); +for (const gap of inventory.currentWebGap.missing) assert.ok(!inventory.currentWebGap.represented.includes(gap)); + +const blender = process.env.BLENDER_BIN ?? path.join(root, "build_blender_5.2.0/bin/blender"); +assert.match(execFileSync(blender, ["--version"], { encoding: "utf8" }), /^Blender 5\.2\.0 LTS/m); +const script = [ + "import bpy,json", + `fixture=${JSON.stringify(path.join(root, inventory.runtimeFixture.path))}`, + "result={}", + "result['appendProperties']=[p.identifier for p in bpy.ops.wm.append.get_rna_type().properties if p.identifier != 'rna_type']", + "result['linkProperties']=[p.identifier for p in bpy.ops.wm.link.get_rna_type().properties if p.identifier != 'rna_type']", + "ctx=bpy.data.libraries.load(fixture,link=True)", + "source,target=ctx.__enter__()", + "result['collections']=sorted([name for name in dir(source) if not name.startswith('_') and name not in {'libraries','version'}])", + "result['libraries']=[{'filepath':item.filepath,'isArchive':item.is_archive} for item in source.libraries]", + "ctx.__exit__(None,None,None)", + "result['overrideRNA']={name:[{'identifier':p.identifier,'type':p.type,'isReadonly':p.is_readonly} for p in getattr(bpy.types,name).bl_rna.properties if p.identifier != 'rna_type'] for name in ['IDOverrideLibrary','IDOverrideLibraryProperty','IDOverrideLibraryPropertyOperation']}", + "print('M12_03A='+json.dumps(result,separators=(',',':'),sort_keys=True))", +].join(";"); +const runtimeOutput = execFileSync(blender, ["--background", "--factory-startup", "--python-expr", script], { cwd: root, encoding: "utf8" }); +const runtimeLine = runtimeOutput.split(/\r?\n/).find((line) => line.startsWith("M12_03A=")); +assert.ok(runtimeLine, "Blender link/append/override runtime inventory is missing"); +const runtime = JSON.parse(runtimeLine.slice("M12_03A=".length)); +assert.deepEqual(runtime.appendProperties, inventory.operatorSurface.appendProperties); +assert.deepEqual(runtime.linkProperties, inventory.operatorSurface.linkProperties); +assert.deepEqual(runtime.collections, inventory.dataBlockSurface.discoverableCollections); +assert.deepEqual(runtime.libraries, inventory.runtimeFixture.nestedLibraries); +assert.deepEqual(runtime.overrideRNA, inventory.overrideRNA); + +process.stdout.write( + `library-operation-inventory-ok roots=${runtime.collections.length} appendOnly=${inventory.dataBlockSurface.onlyAppendableCollections.length} operations=${inventory.operations.length} overrideFields=${Object.values(runtime.overrideRNA).reduce((sum, values) => sum + values.length, 0)} gaps=${inventory.currentWebGap.missing.length} next=${inventory.nextTask}\n`, +); diff --git a/tools/web/export-asset-catalog-v1.py b/tools/web/export-asset-catalog-v1.py new file mode 100644 index 00000000..4708ac19 --- /dev/null +++ b/tools/web/export-asset-catalog-v1.py @@ -0,0 +1,105 @@ +import json +import pathlib +import sys + +import bpy + + +NIL_UUID = "00000000-0000-0000-0000-000000000000" + + +def catalog_records(path): + version = None + records = [] + for line_number, raw_line in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): + line = raw_line.strip() + if not line or line.startswith("#"): + continue + if version is None: + if not line.startswith("VERSION "): + raise RuntimeError(f"catalog definition line {line_number} has no version marker") + version = int(line.removeprefix("VERSION ")) + continue + catalog_id, catalog_path, simple_name = line.split(":", 2) + records.append({ + "catalogId": catalog_id, + "path": catalog_path, + "simpleName": simple_name.strip(), + "parentPath": catalog_path.rpartition("/")[0] or None, + }) + return version, records + + +def custom_property_value(value): + if isinstance(value, (bool, int, float, str)): + return value + if hasattr(value, "to_list"): + return value.to_list() + if isinstance(value, (list, tuple)): + return list(value) + raise RuntimeError(f"unsupported asset custom property type {type(value).__name__}") + + +def asset_record(id_type, asset): + metadata = asset.asset_data + custom_properties = [ + { + "name": name, + "type": type(metadata[name]).__name__.upper(), + "value": custom_property_value(metadata[name]), + } + for name in sorted(metadata.keys()) + ] + return { + "idType": id_type, + "name": asset.name, + "catalogId": metadata.catalog_id or NIL_UUID, + "catalogSimpleName": metadata.catalog_simple_name, + "author": metadata.author, + "description": metadata.description, + "copyright": metadata.copyright, + "license": metadata.license, + "tags": [tag.name for tag in metadata.tags], + "activeTag": metadata.active_tag, + "usePreferredImportMethod": metadata.use_preferred_import_method, + "preferredImportMethod": metadata.preferred_import_method, + "customProperties": custom_properties, + } + + +def main(catalog_file, output_file): + catalog_path = pathlib.Path(catalog_file).resolve() + output_path = pathlib.Path(output_file).resolve() + version, catalogs = catalog_records(catalog_path) + assets = [] + for id_type, collection in ( + ("MATERIAL", bpy.data.materials), + ("OBJECT", bpy.data.objects), + ("WORLD", bpy.data.worlds), + ): + assets.extend(asset_record(id_type, asset) for asset in collection if asset.asset_data is not None) + assets.sort(key=lambda item: (item["idType"], item["name"])) + report = { + "schemaVersion": 1, + "task": "M12-01B", + "enablingTask": True, + "parityStateChange": False, + "blenderVersion": ".".join(str(value) for value in bpy.app.version), + "catalogDefinition": { + "fileName": catalog_path.name, + "version": version, + "records": catalogs, + }, + "assets": assets, + "nextTask": "M12-01C", + } + output_path.parent.mkdir(parents=True, exist_ok=True) + output_path.write_text(json.dumps(report, indent=2, ensure_ascii=True) + "\n", encoding="utf-8", newline="\n") + print(f"m12-asset-catalog-v1-canonical catalogs={len(catalogs)} assets={len(assets)} output={output_path}") + + +if __name__ == "__main__": + arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(arguments) != 2: + raise SystemExit("usage: blender --background FIXTURE --python export-asset-catalog-v1.py -- CATALOG_FILE OUTPUT_JSON") + main(*arguments) diff --git a/tools/web/generate-asset-catalog-compatibility.mjs b/tools/web/generate-asset-catalog-compatibility.mjs new file mode 100644 index 00000000..7201ab51 --- /dev/null +++ b/tools/web/generate-asset-catalog-compatibility.mjs @@ -0,0 +1,39 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const [sourceArg, outputArg] = process.argv.slice(2); +if (!sourceArg || !outputArg) throw new Error("usage: node generate-asset-catalog-compatibility.mjs V2_JSON OUTPUT_JSON"); +const root = path.resolve(import.meta.dirname, "../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-compatibility-generator-")); +const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-compatibility.ts"]; +try { + for (const sourceName of sources) { + const sourcePath = path.join(root, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"')); + } + const compatibility = await import(pathToFileURL(path.join(temporary, "asset-catalog-compatibility.mjs"))); + const value = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8")); + const output = { + read: await compatibility.inspectAssetCatalogForLegacyReader(value, "READ"), + catalogWrite: await compatibility.inspectAssetCatalogForLegacyReader(value, "CATALOG_WRITE"), + assetWrite: await compatibility.inspectAssetCatalogForLegacyReader(value, "ASSET_WRITE"), + save: await compatibility.inspectAssetCatalogForLegacyReader(value, "SAVE"), + future: await compatibility.inspectAssetCatalogForLegacyReader({ schemaVersion: 3 }, "READ"), + }; + fs.mkdirSync(path.dirname(path.resolve(outputArg)), { recursive: true }); + fs.writeFileSync(path.resolve(outputArg), `${JSON.stringify(output, null, 2)}\n`); + process.stdout.write("asset-catalog-compatibility-generated read=READ_ONLY writes=3/BLOCKED future=BLOCKED\n"); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/generate-asset-catalog-migration.mjs b/tools/web/generate-asset-catalog-migration.mjs new file mode 100644 index 00000000..bb0f1bd6 --- /dev/null +++ b/tools/web/generate-asset-catalog-migration.mjs @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const [sourceArg, targetArg, reportArg] = process.argv.slice(2); +if (!sourceArg || !targetArg || !reportArg) throw new Error("usage: node generate-asset-catalog-migration.mjs V1_JSON V2_JSON REPORT_JSON"); +const root = path.resolve(import.meta.dirname, "../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-migration-generator-")); +const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"]; +try { + for (const sourceName of sources) { + const sourcePath = path.join(root, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + const output = result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"'); + fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), output); + } + const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs"))); + const value = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8")); + const result = await migration.migrateAssetCatalogV1ToV2(value); + fs.mkdirSync(path.dirname(path.resolve(targetArg)), { recursive: true }); + fs.writeFileSync(path.resolve(targetArg), `${JSON.stringify(result.manifest, null, 2)}\n`); + fs.writeFileSync(path.resolve(reportArg), `${JSON.stringify(result.report, null, 2)}\n`); + process.stdout.write(`asset-catalog-migration-generated catalogs=${result.manifest.catalogs.length} assets=${result.manifest.assets.length} libraries=${result.manifest.libraries.length}\n`); +} +finally { + fs.rmSync(temporary, { recursive: true, force: true }); +} diff --git a/tools/web/generate-asset-catalog-v1.py b/tools/web/generate-asset-catalog-v1.py new file mode 100644 index 00000000..e74282f5 --- /dev/null +++ b/tools/web/generate-asset-catalog-v1.py @@ -0,0 +1,111 @@ +import pathlib +import sys + +import bpy + + +CATALOGS = ( + ("11111111-1111-4111-8111-111111111111", "Characters", "Characters"), + ("22222222-2222-4222-8222-222222222222", "Characters/Heroes", "Heroes"), + ("33333333-3333-4333-8333-333333333333", "Materials/Metal", "Metal"), +) + + +def set_metadata(asset, *, catalog_id, author, description, copyright_text, license_text, + tags, active_tag, preferred_import_method, properties): + metadata = asset.asset_data + metadata.catalog_id = catalog_id + metadata.author = author + metadata.description = description + metadata.copyright = copyright_text + metadata.license = license_text + for tag in tags: + metadata.tags.new(tag, skip_if_exists=False) + metadata.active_tag = active_tag + metadata.use_preferred_import_method = preferred_import_method is not None + if preferred_import_method is not None: + metadata.preferred_import_method = preferred_import_method + for name, value in properties.items(): + metadata[name] = value + + +def write_catalog_definition(path): + lines = [ + "# This is an Asset Catalog Definition file for Blender.", + "#", + "# Generated by M12-01B from a locked Blender 5.2 runtime.", + "", + "VERSION 1", + "", + ] + lines.extend(f"{catalog_id}:{catalog_path}:{simple_name}" for catalog_id, catalog_path, simple_name in CATALOGS) + path.write_text("\n".join(lines) + "\n", encoding="utf-8", newline="\n") + + +def main(output_directory): + output = pathlib.Path(output_directory).resolve() + output.mkdir(parents=True, exist_ok=True) + bpy.ops.wm.read_factory_settings(use_empty=True) + + mesh = bpy.data.meshes.new("M12 Hero Mesh") + mesh.from_pydata(((-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (0.0, 1.0, 0.0)), (), ((0, 1, 2),)) + hero = bpy.data.objects.new("M12 Hero", mesh) + bpy.context.scene.collection.objects.link(hero) + hero.asset_mark() + set_metadata( + hero, + catalog_id=CATALOGS[1][0], + author="M12 Artist", + description="Desktop catalog v1 object fixture", + copyright_text="Copyright 2026 M12 Fixture Authors", + license_text="CC0-1.0", + tags=("character", "hero", "rig-ready"), + active_tag=1, + preferred_import_method="APPEND", + properties={"approved": True, "rating": 5, "source": "M12-01B"}, + ) + + material = bpy.data.materials.new("M12 Brushed Metal") + material.diffuse_color = (0.25, 0.3, 0.35, 1.0) + material.asset_mark() + set_metadata( + material, + catalog_id=CATALOGS[2][0], + author="", + description="", + copyright_text="", + license_text="", + tags=("metal",), + active_tag=0, + preferred_import_method=None, + properties={"roughness": 0.35}, + ) + + world = bpy.data.worlds.new("M12 Uncataloged World") + world.color = (0.02, 0.03, 0.04) + world.asset_mark() + set_metadata( + world, + catalog_id="", + author="M12 Artist", + description="Asset without a catalog assignment", + copyright_text="", + license_text="CC0-1.0", + tags=(), + active_tag=0, + preferred_import_method="LINK", + properties={}, + ) + + catalog_path = output / "blender_assets.cats.txt" + fixture_path = output / "m12_asset_catalog_v1.blend" + write_catalog_definition(catalog_path) + bpy.ops.wm.save_as_mainfile(filepath=str(fixture_path), compress=True) + print(f"m12-asset-catalog-v1 fixture={fixture_path} catalogs={len(CATALOGS)} assets=3") + + +if __name__ == "__main__": + arguments = sys.argv[sys.argv.index("--") + 1:] if "--" in sys.argv else [] + if len(arguments) != 1: + raise SystemExit("usage: blender --background --factory-startup --python generate-asset-catalog-v1.py -- OUTPUT_DIRECTORY") + main(arguments[0]) diff --git a/tools/web/generate-asset-catalog-v2.mjs b/tools/web/generate-asset-catalog-v2.mjs new file mode 100644 index 00000000..b18ebef0 --- /dev/null +++ b/tools/web/generate-asset-catalog-v2.mjs @@ -0,0 +1,29 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import { pathToFileURL } from "node:url"; +import ts from "../../web/node_modules/typescript/lib/typescript.js"; + +const [sourceArg, outputArg] = process.argv.slice(2); +if (!sourceArg || !outputArg) throw new Error("usage: node generate-asset-catalog-v2.mjs SOURCE_CANONICAL OUTPUT_JSON"); +const root = path.resolve(import.meta.dirname, "../.."); +const sourcePath = path.join(root, "web/protocol/asset-catalog-v2.ts"); +const temporaryPath = path.resolve(outputArg, "../asset-catalog-v2.generated.mjs"); +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, []); +fs.mkdirSync(path.dirname(outputArg), { recursive: true }); +fs.writeFileSync(temporaryPath, transpiled.outputText); +try { + const protocol = await import(`${pathToFileURL(temporaryPath)}?v=${Date.now()}`); + const desktop = JSON.parse(fs.readFileSync(path.resolve(sourceArg), "utf8")); + const manifest = await protocol.createAssetCatalogManifestV2FromDesktop(desktop, 1); + fs.writeFileSync(path.resolve(outputArg), `${JSON.stringify(manifest, null, 2)}\n`); + process.stdout.write(`asset-catalog-v2-generated catalogs=${manifest.catalogs.length} assets=${manifest.assets.length}\n`); +} +finally { + fs.rmSync(temporaryPath, { force: true }); +} diff --git a/tools/web/generate-asset-preview-identity.py b/tools/web/generate-asset-preview-identity.py new file mode 100644 index 00000000..9bd637a9 --- /dev/null +++ b/tools/web/generate-asset-preview-identity.py @@ -0,0 +1,33 @@ +import pathlib +import sys + +import bpy + + +def main() -> None: + if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2: + raise SystemExit("usage: blender --background --factory-startup --python generate-asset-preview-identity.py -- INPUT OUTPUT") + source_arg, output_arg = sys.argv[sys.argv.index("--") + 1 :] + source = pathlib.Path(source_arg).resolve() + output = pathlib.Path(output_arg).resolve() + output.parent.mkdir(parents=True, exist_ok=True) + + image = bpy.data.images.load(str(source), check_existing=False) + width, height = image.size[:] + scene = bpy.context.scene + scene.render.image_settings.file_format = "PNG" + scene.render.image_settings.color_mode = "RGBA" + scene.render.image_settings.color_depth = "8" + scene.render.image_settings.compression = 0 + scene.display_settings.display_device = "sRGB" + scene.view_settings.view_transform = "Standard" + scene.view_settings.look = "None" + scene.view_settings.exposure = 0 + scene.view_settings.gamma = 1 + image.save_render(str(output), scene=scene) + if not output.is_file() or output.stat().st_size == 0: + raise RuntimeError("asset preview output was not written") + print(f"asset-preview-generated width={width} height={height} output={output}") + + +main() diff --git a/tools/web/generate-library-append-fixture.py b/tools/web/generate-library-append-fixture.py new file mode 100644 index 00000000..818f8dd4 --- /dev/null +++ b/tools/web/generate-library-append-fixture.py @@ -0,0 +1,181 @@ +import hashlib +import json +import pathlib +import struct +import sys + +import bpy + + +ROOT_OBJECT = "M12 Append Object" +MESH_NAME = "M12 Append Mesh" +MATERIAL_NAME = "M12 Append Material" +IMAGE_NAME = "M12 Append Image" +IMAGE_NODE_NAME = "M12 Append Image Node" + + +def sha256_file(path: pathlib.Path) -> str: + return hashlib.sha256(path.read_bytes()).hexdigest() + + +def pixel_sha256(image: bpy.types.Image) -> str: + values = list(image.pixels) + return hashlib.sha256(struct.pack(f"<{len(values)}f", *values)).hexdigest() + + +def reset() -> None: + bpy.ops.wm.read_factory_settings(use_empty=True) + + +def create_source(path: pathlib.Path) -> None: + reset() + image = bpy.data.images.new(IMAGE_NAME, width=2, height=2, alpha=True, float_buffer=False) + image.colorspace_settings.name = "sRGB" + image.pixels = [ + 1.0, 0.0, 0.0, 1.0, + 0.0, 1.0, 0.0, 1.0, + 0.0, 0.0, 1.0, 1.0, + 1.0, 1.0, 1.0, 0.5, + ] + image.pack() + + material = bpy.data.materials.new(MATERIAL_NAME) + material.use_nodes = True + node_tree = material.node_tree + principled = node_tree.nodes.get("Principled BSDF") + image_node = node_tree.nodes.new("ShaderNodeTexImage") + image_node.name = IMAGE_NODE_NAME + image_node.label = IMAGE_NODE_NAME + image_node.image = image + image_node.interpolation = "Closest" + image_node.extension = "REPEAT" + node_tree.links.new(image_node.outputs["Color"], principled.inputs["Base Color"]) + + mesh = bpy.data.meshes.new(MESH_NAME) + mesh.from_pydata( + [(-1.0, -1.0, 0.0), (1.0, -1.0, 0.0), (1.0, 1.0, 0.0), (-1.0, 1.0, 0.0)], + [], + [(0, 1, 2, 3)], + ) + mesh.materials.append(material) + uv_layer = mesh.uv_layers.new(name="UVMap") + for loop, uv in zip(uv_layer.data, [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)]): + loop.uv = uv + mesh.update() + + obj = bpy.data.objects.new(ROOT_OBJECT, mesh) + obj["m12_source_marker"] = "M12-03C" + bpy.context.scene.collection.objects.link(obj) + bpy.ops.wm.save_as_mainfile(filepath=str(path), check_existing=False) + + +def data_block(id_type: str, value: object) -> dict: + return { + "idType": id_type, + "name": value.name, + "nameFull": value.name_full, + "library": None if value.library is None else value.library.filepath, + "isLibraryOverride": value.override_library is not None, + } + + +def inspect_local_graph() -> dict: + obj = bpy.data.objects[ROOT_OBJECT] + mesh = obj.data + material = mesh.materials[0] + image_node = material.node_tree.nodes[IMAGE_NODE_NAME] + image = image_node.image + ids = { + "OBJECT": data_block("OBJECT", obj), + "MESH": data_block("MESH", mesh), + "MATERIAL": data_block("MATERIAL", material), + "IMAGE": data_block("IMAGE", image), + } + return { + "root": ids["OBJECT"], + "ids": ids, + "edges": [ + {"from": "Object/M12 Append Object", "relation": "OBJECT_DATA", "to": "Mesh/M12 Append Mesh"}, + {"from": "Mesh/M12 Append Mesh", "relation": "MATERIAL_SLOT[0]", "to": "Material/M12 Append Material"}, + {"from": "Material/M12 Append Material", "relation": f"NODE_IMAGE[{IMAGE_NODE_NAME}]", "to": "Image/M12 Append Image"}, + ], + "geometry": { + "vertices": len(mesh.vertices), + "edges": len(mesh.edges), + "polygons": len(mesh.polygons), + "loops": len(mesh.loops), + "uvLayers": [layer.name for layer in mesh.uv_layers], + "materialSlots": [item.name for item in mesh.materials], + }, + "image": { + "size": list(image.size), + "channels": image.channels, + "colorspace": image.colorspace_settings.name, + "packed": image.packed_file is not None, + "pixelFloat32Sha256": pixel_sha256(image), + }, + "sourceMarker": obj["m12_source_marker"], + } + + +def append_object(source: pathlib.Path, target: pathlib.Path) -> dict: + reset() + with bpy.data.libraries.load(str(source), link=False) as (data_from, data_to): + if ROOT_OBJECT not in data_from.objects: + raise RuntimeError("source root object is missing") + data_to.objects = [ROOT_OBJECT] + if len(data_to.objects) != 1 or data_to.objects[0] is None: + raise RuntimeError("desktop append did not return one object") + bpy.context.scene.collection.objects.link(data_to.objects[0]) + before_save = inspect_local_graph() + if any(item["library"] is not None or item["isLibraryOverride"] for item in before_save["ids"].values()): + raise RuntimeError("appended dependency closure is not fully local") + bpy.ops.wm.save_as_mainfile(filepath=str(target), check_existing=False) + bpy.ops.wm.open_mainfile(filepath=str(target), load_ui=False) + reopened = inspect_local_graph() + if reopened != before_save: + raise RuntimeError("appended dependency mapping drifted after save/reopen") + return reopened + + +def main() -> None: + if "--" not in sys.argv or len(sys.argv[sys.argv.index("--") + 1 :]) != 2: + raise SystemExit("usage: blender --background --factory-startup --python generate-library-append-fixture.py -- OUTPUT_DIR REPORT") + output_arg, report_arg = sys.argv[sys.argv.index("--") + 1 :] + output_dir = pathlib.Path(output_arg).resolve() + report_path = pathlib.Path(report_arg).resolve() + output_dir.mkdir(parents=True, exist_ok=True) + report_path.parent.mkdir(parents=True, exist_ok=True) + source = output_dir / "m12_append_source.blend" + target = output_dir / "m12_append_target.blend" + + create_source(source) + source_graph = inspect_local_graph() + appended_graph = append_object(source, target) + report = { + "schemaVersion": 1, + "task": "M12-03C", + "operation": "APPEND", + "blenderVersion": "5.2.0", + "source": {"file": source.name, "sha256": sha256_file(source)}, + "target": {"file": target.name, "sha256": sha256_file(target)}, + "selectedRoots": ["Object/M12 Append Object"], + "sourceGraph": source_graph, + "appendedGraph": appended_graph, + "stableMapping": [ + {"source": "Object/M12 Append Object", "local": "Object/M12 Append Object", "owner": "LOCAL_MAIN", "readOnly": False}, + {"source": "Mesh/M12 Append Mesh", "local": "Mesh/M12 Append Mesh", "owner": "LOCAL_MAIN", "readOnly": False}, + {"source": "Material/M12 Append Material", "local": "Material/M12 Append Material", "owner": "LOCAL_MAIN", "readOnly": False}, + {"source": "Image/M12 Append Image", "local": "Image/M12 Append Image", "owner": "LOCAL_MAIN", "readOnly": False}, + ], + "nextTask": "M12-03D", + } + report_path.write_text(json.dumps(report, indent=2, sort_keys=True) + "\n", encoding="utf-8") + print( + f"library-append-fixture-ok roots={len(report['selectedRoots'])} " + f"mapping={len(report['stableMapping'])} source={report['source']['sha256']} " + f"target={report['target']['sha256']} next={report['nextTask']}" + ) + + +main() diff --git a/web/app/public/vendor/blender/web_engine.js b/web/app/public/vendor/blender/web_engine.js index 8616e241..c644158c 100644 --- a/web/app/public/vendor/blender/web_engine.js +++ b/web/app/public/vendor/blender/web_engine.js @@ -6,7 +6,7 @@ var Module = (() => { async function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});["_malloc","_free","_web_engine_create","_web_engine_destroy","_web_engine_get_memory_stats","_web_engine_get_live_handles","_web_engine_get_allocated_bytes","_web_engine_open_blend","_web_engine_apply_command","_web_engine_undo","_web_engine_redo","_web_engine_get_scene_snapshot","_web_engine_get_scene_metadata","_web_engine_get_scene_geometry","_web_engine_get_scene_delta","_web_engine_get_packed_asset","_web_engine_evaluate_depsgraph","_web_engine_save_blend","_web_engine_free_buffer","_web_engine_last_error_code","_web_engine_last_error_message","_web_engine_decimate_apply","_memory","___indirect_function_table","___set_stack_limits","onRuntimeInitialized"].forEach(prop=>{if(!Object.getOwnPropertyDescriptor(readyPromise,prop)){Object.defineProperty(readyPromise,prop,{get:()=>abort("You are getting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js"),set:()=>abort("You are setting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js")})}});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");let dirname=import.meta.url;if(dirname.startsWith("data:")){dirname="/"}var require=createRequire(dirname)}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){if(typeof process=="undefined"||!process.release||process.release.name!=="node")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");var nodeVersion=process.versions.node;var numericVersion=nodeVersion.split(".").slice(0,3);numericVersion=numericVersion[0]*1e4+numericVersion[1]*100+numericVersion[2].split("-")[0]*1;if(numericVersion<16e4){throw new Error("This emscripten-generated code requires node v16.0.0 (detected v"+nodeVersion+")")}var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);assert(ret.buffer);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_SHELL){if(typeof process=="object"&&typeof require==="function"||typeof window=="object"||typeof importScripts=="function")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)")}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}if(!(typeof window=="object"||typeof importScripts=="function"))throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{throw new Error("environment detection error")}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];legacyModuleProp("arguments","arguments_");if(Module["thisProgram"])thisProgram=Module["thisProgram"];legacyModuleProp("thisProgram","thisProgram");assert(typeof Module["memoryInitializerPrefixURL"]=="undefined","Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["pthreadMainPrefixURL"]=="undefined","Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["cdInitializerPrefixURL"]=="undefined","Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["filePackagePrefixURL"]=="undefined","Module.filePackagePrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["read"]=="undefined","Module.read option was removed");assert(typeof Module["readAsync"]=="undefined","Module.readAsync option was removed (modify readAsync in JS)");assert(typeof Module["readBinary"]=="undefined","Module.readBinary option was removed (modify readBinary in JS)");assert(typeof Module["setWindowTitle"]=="undefined","Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)");assert(typeof Module["TOTAL_MEMORY"]=="undefined","Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");legacyModuleProp("asm","wasmExports");legacyModuleProp("readAsync","readAsync");legacyModuleProp("readBinary","readBinary");legacyModuleProp("setWindowTitle","setWindowTitle");assert(!ENVIRONMENT_IS_SHELL,"shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.");var wasmBinary=Module["wasmBinary"];legacyModuleProp("wasmBinary","wasmBinary");if(typeof WebAssembly!="object"){err("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}assert(!Module["STACK_SIZE"],"STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");assert(!Module["wasmMemory"],"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(!Module["INITIAL_MEMORY"],"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically");function writeStackCookie(){var max=_emscripten_stack_get_end();assert((max&3)==0);if(max==0){max+=4}HEAPU32[max>>2]=34821223;checkInt32(34821223);HEAPU32[max+4>>2]=2310721022;checkInt32(2310721022);HEAPU32[0>>2]=1668509029;checkInt32(1668509029)}function checkStackCookie(){if(ABORT)return;var max=_emscripten_stack_get_end();if(max==0){max+=4}var cookie1=HEAPU32[max>>2];var cookie2=HEAPU32[max+4>>2];if(cookie1!=34821223||cookie2!=2310721022){abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`)}if(HEAPU32[0>>2]!=1668509029){abort("Runtime error: The application has corrupted its heap memory area (address zero)!")}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();setStackLimits();if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){checkStackCookie();var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}assert(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;var runDependencyTracking={};function getUniqueRunDependency(id){var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(!runDependencyTracking[id]);runDependencyTracking[id]=1;if(runDependencyWatcher===null&&typeof setInterval!="undefined"){runDependencyWatcher=setInterval(()=>{if(ABORT){clearInterval(runDependencyWatcher);runDependencyWatcher=null;return}var shown=false;for(var dep in runDependencyTracking){if(!shown){shown=true;err("still waiting on run dependencies:")}err(`dependency: ${dep}`)}if(shown){err("(end of list)")}},1e4)}}else{err("warning: run dependency added without ID")}}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(runDependencyTracking[id]);delete runDependencyTracking[id]}else{err("warning: run dependency removed without ID")}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function createExportWrapper(name,nargs){return(...args)=>{assert(runtimeInitialized,`native function \`${name}\` called before runtime initialization`);var f=wasmExports[name];assert(f,`exported native function \`${name}\` not found`);assert(args.length<=nargs,`native function \`${name}\` called with ${args.length} args but expects ${nargs}`);return f(...args)}}function findWasmBinary(){if(Module["locateFile"]){var f="web_engine.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("web_engine.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);if(isFileURI(wasmBinaryFile)){err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`)}abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];assert(wasmMemory,"memory not found in wasm exports");updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];assert(wasmTable,"table not found in wasm exports");addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");var trueModule=Module;function receiveInstantiationResult(result){assert(Module===trueModule,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");trueModule=null;receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;(()=>{var h16=new Int16Array(1);var h8=new Int8Array(h16.buffer);h16[0]=25459;if(h8[0]!==115||h8[1]!==99)throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"})();if(Module["ENVIRONMENT"]){throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)")}function legacyModuleProp(prop,newName,incoming=true){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,get(){let extra=incoming?" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)":"";abort(`\`Module.${prop}\` has been replaced by \`${newName}\``+extra)}})}}function ignoredModuleProp(prop){if(Object.getOwnPropertyDescriptor(Module,prop)){abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`)}}function isExportedByForceFilesystem(name){return name==="FS_createPath"||name==="FS_createDataFile"||name==="FS_createPreloadedFile"||name==="FS_unlink"||name==="addRunDependency"||name==="FS_createLazyFile"||name==="FS_createDevice"||name==="removeRunDependency"}function hookGlobalSymbolAccess(sym,func){if(typeof globalThis!="undefined"&&!Object.getOwnPropertyDescriptor(globalThis,sym)){Object.defineProperty(globalThis,sym,{configurable:true,get(){func();return undefined}})}}function missingGlobal(sym,msg){hookGlobalSymbolAccess(sym,()=>{warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`)})}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){hookGlobalSymbolAccess(sym,()=>{var msg=`\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;var librarySymbol=sym;if(!librarySymbol.startsWith("_")){librarySymbol="$"+sym}msg+=` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}warnOnce(msg)});unexportedRuntimeSymbol(sym)}function unexportedRuntimeSymbol(sym){if(!Object.getOwnPropertyDescriptor(Module,sym)){Object.defineProperty(Module,sym,{configurable:true,get(){var msg=`'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}abort(msg)}})}}var MAX_UINT8=2**8-1;var MAX_UINT16=2**16-1;var MAX_UINT32=2**32-1;var MAX_UINT53=2**53-1;var MAX_UINT64=2**64-1;var MIN_INT8=-(2**(8-1));var MIN_INT16=-(2**(16-1));var MIN_INT32=-(2**(32-1));var MIN_INT53=-(2**(53-1));var MIN_INT64=-(2**(64-1));function checkInt(value,bits,min,max){assert(Number.isInteger(Number(value)),`attempt to write non-integer (${value}) into integer heap`);assert(value<=max,`value (${value}) too large to write as ${bits}-bit value`);assert(value>=min,`value (${value}) too small to write as ${bits}-bit value`)}var checkInt8=value=>checkInt(value,8,MIN_INT8,MAX_UINT8);var checkInt16=value=>checkInt(value,16,MIN_INT16,MAX_UINT16);var checkInt32=value=>checkInt(value,32,MIN_INT32,MAX_UINT32);var checkInt64=value=>checkInt(value,64,MIN_INT64,MAX_UINT64);function dbg(...args){console.warn(...args)}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var ptrToString=ptr=>{assert(typeof ptr==="number");ptr>>>=0;return"0x"+ptr.toString(16).padStart(8,"0")};var setStackLimits=()=>{var stackLow=_emscripten_stack_get_base();var stackHigh=_emscripten_stack_get_end();___set_stack_limits(stackLow,stackHigh)};var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text="warning: "+text;err(text)}};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}assert(wasmTable.get(funcPtr)==func,"JavaScript-side Wasm function table mirror is out of date!");return func};var ___call_sighandler=(fp,sig)=>getWasmTableEntry(fp)(sig);var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);assert(exceptionCaught.length>0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught;checkInt8(caught)}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown;checkInt8(rethrown)}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___handle_stack_overflow=requested=>{var base=_emscripten_stack_get_base();var end=_emscripten_stack_get_end();abort(`stack overflow (Attempt to set SP to ${ptrToString(requested)}`+`, with stack limits [${ptrToString(end)} - ${ptrToString(base)}`+"]). If you require more stack space build with -sSTACK_SIZE=")};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{assert(typeof str==="string",`stringToUTF8Array expects a string (got ${typeof str})`);if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;if(u>1114111)warnOnce("Invalid Unicode code point "+ptrToString(u)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size)};var alignMemory=(size,alignment)=>{assert(alignment,"alignment argument is required");return Math.ceil(size/alignment)*alignment};var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);assert(size>=0);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var strError=errno=>UTF8ToString(_strerror(errno));var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class extends Error{constructor(errno){super(runtimeInitialized?strError(errno):"");this.name="ErrnoError";this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){assert(typeof parent=="object");var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){assert(fd>=-1);stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){assert(FS.syncFSRequests>0);FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){if(typeof type=="string"){throw type}var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);assert(idx!==-1);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;i=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate(stream,offset,length){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){assert(offset>=0);if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1);assert(stdin.fd===0,`invalid handle for stdin (${stdin.fd})`);assert(stdout.fd===1,`invalid handle for stdout (${stdout.fd})`);assert(stderr.fd===2,`invalid handle for stderr (${stderr.fd})`)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){assert(!FS.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;_fflush(0);for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);assert(size>=0);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node},absolutePath(){abort("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder(){abort("FS.createFolder has been removed; use FS.mkdir instead")},createLink(){abort("FS.createLink has been removed; use FS.symlink instead")},joinPath(){abort("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc(){abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath(){abort("FS.standardizePath has been removed; use PATH.normalize instead")}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;checkInt32(stat.dev);HEAP32[buf+4>>2]=stat.mode;checkInt32(stat.mode);HEAPU32[buf+8>>2]=stat.nlink;checkInt32(stat.nlink);HEAP32[buf+12>>2]=stat.uid;checkInt32(stat.uid);HEAP32[buf+16>>2]=stat.gid;checkInt32(stat.gid);HEAP32[buf+20>>2]=stat.rdev;checkInt32(stat.rdev);tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];checkInt64(stat.size);HEAP32[buf+32>>2]=4096;checkInt32(4096);HEAP32[buf+36>>2]=stat.blocks;checkInt32(stat.blocks);var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];checkInt64(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;checkInt32(atime%1e3*1e3*1e3);tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];checkInt64(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;checkInt32(mtime%1e3*1e3*1e3);tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];checkInt64(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;checkInt32(ctime%1e3*1e3*1e3);tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];checkInt64(stat.ino);return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);assert(flags===0||flags==512);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){assert(SYSCALLS.varargs!=undefined);var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;checkInt16(2);return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>{assert(typeof maxBytesToWrite=="number","stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)};function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.getStreamFromFD(fd);stream.getdents||=FS.readdir(stream.path);var struct_size=280;var pos=0;var off=FS.llseek(stream,0,1);var idx=Math.floor(off/struct_size);while(idx>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];checkInt64(id);tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];checkInt64((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;checkInt16(280);HEAP8[dirp+pos+18]=type;checkInt8(type);stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;checkInt32(termios.c_iflag||0);HEAP32[argp+4>>2]=termios.c_oflag||0;checkInt32(termios.c_oflag||0);HEAP32[argp+8>>2]=termios.c_cflag||0;checkInt32(termios.c_cflag||0);HEAP32[argp+12>>2]=termios.c_lflag||0;checkInt32(termios.c_lflag||0);for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0;checkInt8(termios.c_cc[i]||0)}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;checkInt32(0);return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];checkInt16(winsize[0]);HEAP16[argp+2>>1]=winsize[1];checkInt16(winsize[1])}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;assert(!flags,`unknown flags in __syscall_newfstatat: ${flags}`);path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("native code called abort()")};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var __emscripten_throw_longjmp=()=>{throw Infinity};var convertI32PairToI53Checked=(lo,hi)=>{assert(lo==lo>>>0||lo==(lo|0));assert(hi===(hi|0));return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;checkInt32(res.allocated);HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;checkInt32(stdTimezoneOffset*60);HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);checkInt32(Number(winterOffset!=summerOffset));var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);assert(winterName);assert(summerName);assert(lengthBytesUTF8(winterName)<=16,`timezone name truncated to fit in TZNAME_MAX (${winterName})`);assert(lengthBytesUTF8(summerName)<=16,`timezone name truncated to fit in TZNAME_MAX (${summerName})`);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_get_now=()=>performance.now();var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){err(`growMemory: Attempted to grow heap from ${b.byteLength} bytes to ${size} bytes, but got error: ${e}`)}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;assert(requestedSize>oldSize);var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`);return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var t0=_emscripten_get_now();var replacement=growMemory(newSize);var t1=_emscripten_get_now();dbg(`Heap resize call from ${oldSize} to ${newSize} took ${t1-t0} msecs. Success: ${!!replacement}`);if(replacement){return true}}err(`Failed to grow the heap from ${oldSize} bytes to ${newSize} bytes, not enough memory!`);return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;checkInt32(ptr);stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;checkInt32(strings.length);var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;checkInt32(bufSize);return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;checkInt8(type);HEAP16[pbuf+2>>1]=flags;checkInt16(flags);tempI64=[rightsBase>>>0,(tempDouble=rightsBase,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+8>>2]=tempI64[0],HEAP32[pbuf+12>>2]=tempI64[1];checkInt64(rightsBase);tempI64=[rightsInheriting>>>0,(tempDouble=rightsInheriting,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+16>>2]=tempI64[0],HEAP32[pbuf+20>>2]=tempI64[1];checkInt64(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];checkInt64(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var _llvm_eh_typeid_for=type=>type;var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var getCFunc=ident=>{var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func};var writeArrayToMemory=(array,buffer)=>{assert(array.length>=0,"writeArrayToMemory array must have a length (should be an array or typed array)");HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;assert(returnType!=="array",'Return type should not be "array".');if(args){for(var i=0;i(...args)=>ccall(ident,returnType,argTypes,args,opts);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var wasmImports={__assert_fail:___assert_fail,__call_sighandler:___call_sighandler,__cxa_begin_catch:___cxa_begin_catch,__cxa_end_catch:___cxa_end_catch,__cxa_find_matching_catch_2:___cxa_find_matching_catch_2,__cxa_find_matching_catch_3:___cxa_find_matching_catch_3,__cxa_rethrow:___cxa_rethrow,__cxa_throw:___cxa_throw,__handle_stack_overflow:___handle_stack_overflow,__resumeException:___resumeException,__syscall_faccessat:___syscall_faccessat,__syscall_fcntl64:___syscall_fcntl64,__syscall_fstat64:___syscall_fstat64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_rmdir:___syscall_rmdir,__syscall_stat64:___syscall_stat64,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,_emscripten_memcpy_js:__emscripten_memcpy_js,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_emscripten_throw_longjmp:__emscripten_throw_longjmp,_mmap_js:__mmap_js,_munmap_js:__munmap_js,_tzset_js:__tzset_js,emscripten_date_now:_emscripten_date_now,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,getentropy:_getentropy,invoke_d,invoke_diii,invoke_fi,invoke_fif,invoke_fifff,invoke_fii,invoke_fiii,invoke_fiiii,invoke_fiiiiii,invoke_fiiiiiii,invoke_fij,invoke_i,invoke_idiii,invoke_ii,invoke_iif,invoke_iifi,invoke_iifii,invoke_iifiiiii,invoke_iii,invoke_iiid,invoke_iiidii,invoke_iiif,invoke_iiiffi,invoke_iiii,invoke_iiiidi,invoke_iiiiffifffffffi,invoke_iiiifiii,invoke_iiiii,invoke_iiiiif,invoke_iiiiifi,invoke_iiiiii,invoke_iiiiiifi,invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii,invoke_iiiiiii,invoke_iiiiiiifii,invoke_iiiiiiii,invoke_iiiiiiiii,invoke_iiiiiiiiiffi,invoke_iiiiiiiiii,invoke_iiiiiiiiiifi,invoke_iiiiiiiiiii,invoke_iiiiiiiiiiii,invoke_iiiiiiiiiiiif,invoke_iiiiiiiiiiiiiii,invoke_iiiiiiij,invoke_iiiiiij,invoke_iiiiij,invoke_iiiij,invoke_iiij,invoke_iiiji,invoke_iiijii,invoke_iiijiii,invoke_iij,invoke_iijiii,invoke_iijj,invoke_iijji,invoke_iijjiii,invoke_ij,invoke_ijjiii,invoke_j,invoke_ji,invoke_jii,invoke_jiii,invoke_jiji,invoke_v,invoke_vdiii,invoke_vi,invoke_vid,invoke_vif,invoke_vififiif,invoke_vifii,invoke_vifiiifiiiiiiiiiiiiifiiii,invoke_vii,invoke_viid,invoke_viif,invoke_viiff,invoke_viifi,invoke_viifiii,invoke_viii,invoke_viiid,invoke_viiif,invoke_viiiffii,invoke_viiiffiiii,invoke_viiifi,invoke_viiii,invoke_viiiif,invoke_viiiii,invoke_viiiiif,invoke_viiiiii,invoke_viiiiiid,invoke_viiiiiif,invoke_viiiiiifi,invoke_viiiiiifif,invoke_viiiiiifii,invoke_viiiiiii,invoke_viiiiiiidiiii,invoke_viiiiiiii,invoke_viiiiiiiif,invoke_viiiiiiiii,invoke_viiiiiiiiii,invoke_viiiiiiiiiidii,invoke_viiiiiiiiiii,invoke_viiiiiiiiiiii,invoke_viiiiiiiiiiiiii,invoke_viiiiiiiiiiiiiiii,invoke_viiiiij,invoke_viiiiiji,invoke_viiiiji,invoke_viiiijiiifi,invoke_viiij,invoke_viiiji,invoke_viiijii,invoke_viiijjji,invoke_viij,invoke_viiji,invoke_viijii,invoke_viijj,invoke_vij,invoke_viji,invoke_vijif,invoke_vijii,invoke_vijj,invoke_vjjiii,llvm_eh_typeid_for:_llvm_eh_typeid_for,proc_exit:_proc_exit};var wasmExports=createWasm();var ___wasm_call_ctors=createExportWrapper("__wasm_call_ctors",0);var _web_engine_create=Module["_web_engine_create"]=createExportWrapper("web_engine_create",0);var _web_engine_get_memory_stats=Module["_web_engine_get_memory_stats"]=createExportWrapper("web_engine_get_memory_stats",1);var _web_engine_destroy=Module["_web_engine_destroy"]=createExportWrapper("web_engine_destroy",1);var _fflush=createExportWrapper("fflush",1);var _web_engine_get_live_handles=Module["_web_engine_get_live_handles"]=createExportWrapper("web_engine_get_live_handles",0);var _web_engine_get_allocated_bytes=Module["_web_engine_get_allocated_bytes"]=createExportWrapper("web_engine_get_allocated_bytes",0);var _web_engine_open_blend=Module["_web_engine_open_blend"]=createExportWrapper("web_engine_open_blend",3);var _web_engine_apply_command=Module["_web_engine_apply_command"]=createExportWrapper("web_engine_apply_command",3);var _web_engine_decimate_apply=Module["_web_engine_decimate_apply"]=createExportWrapper("web_engine_decimate_apply",35);var _web_engine_undo=Module["_web_engine_undo"]=createExportWrapper("web_engine_undo",1);var _web_engine_redo=Module["_web_engine_redo"]=createExportWrapper("web_engine_redo",1);var _web_engine_get_scene_snapshot=Module["_web_engine_get_scene_snapshot"]=createExportWrapper("web_engine_get_scene_snapshot",3);var _web_engine_get_scene_metadata=Module["_web_engine_get_scene_metadata"]=createExportWrapper("web_engine_get_scene_metadata",3);var _web_engine_get_scene_geometry=Module["_web_engine_get_scene_geometry"]=createExportWrapper("web_engine_get_scene_geometry",3);var _web_engine_get_scene_delta=Module["_web_engine_get_scene_delta"]=createExportWrapper("web_engine_get_scene_delta",3);var _web_engine_get_packed_asset=Module["_web_engine_get_packed_asset"]=createExportWrapper("web_engine_get_packed_asset",5);var _web_engine_evaluate_depsgraph=Module["_web_engine_evaluate_depsgraph"]=createExportWrapper("web_engine_evaluate_depsgraph",3);var _web_engine_save_blend=Module["_web_engine_save_blend"]=createExportWrapper("web_engine_save_blend",3);var _malloc=Module["_malloc"]=createExportWrapper("malloc",1);var _web_engine_free_buffer=Module["_web_engine_free_buffer"]=createExportWrapper("web_engine_free_buffer",1);var _free=Module["_free"]=createExportWrapper("free",1);var _web_engine_last_error_code=Module["_web_engine_last_error_code"]=createExportWrapper("web_engine_last_error_code",0);var _web_engine_last_error_message=Module["_web_engine_last_error_message"]=createExportWrapper("web_engine_last_error_message",0);var _strerror=createExportWrapper("strerror",1);var _emscripten_builtin_memalign=createExportWrapper("emscripten_builtin_memalign",2);var _setThrew=createExportWrapper("setThrew",2);var __emscripten_tempret_set=createExportWrapper("_emscripten_tempret_set",1);var __emscripten_tempret_get=createExportWrapper("_emscripten_tempret_get",0);var _emscripten_stack_init=()=>(_emscripten_stack_init=wasmExports["emscripten_stack_init"])();var _emscripten_stack_get_free=()=>(_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"])();var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"])();var _emscripten_stack_get_end=()=>(_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();var ___cxa_decrement_exception_refcount=createExportWrapper("__cxa_decrement_exception_refcount",1);var ___cxa_increment_exception_refcount=createExportWrapper("__cxa_increment_exception_refcount",1);var ___cxa_can_catch=createExportWrapper("__cxa_can_catch",3);var ___cxa_get_exception_ptr=createExportWrapper("__cxa_get_exception_ptr",1);var ___set_stack_limits=Module["___set_stack_limits"]=createExportWrapper("__set_stack_limits",2);var dynCall_viij=Module["dynCall_viij"]=createExportWrapper("dynCall_viij",5);var dynCall_viiji=Module["dynCall_viiji"]=createExportWrapper("dynCall_viiji",6);var dynCall_viijii=Module["dynCall_viijii"]=createExportWrapper("dynCall_viijii",7);var dynCall_vij=Module["dynCall_vij"]=createExportWrapper("dynCall_vij",4);var dynCall_viiiijiiifi=Module["dynCall_viiiijiiifi"]=createExportWrapper("dynCall_viiiijiiifi",12);var dynCall_viiijjji=Module["dynCall_viiijjji"]=createExportWrapper("dynCall_viiijjji",11);var dynCall_viiiiji=Module["dynCall_viiiiji"]=createExportWrapper("dynCall_viiiiji",8);var dynCall_viiiji=Module["dynCall_viiiji"]=createExportWrapper("dynCall_viiiji",7);var dynCall_ij=Module["dynCall_ij"]=createExportWrapper("dynCall_ij",3);var dynCall_viji=Module["dynCall_viji"]=createExportWrapper("dynCall_viji",5);var dynCall_iij=Module["dynCall_iij"]=createExportWrapper("dynCall_iij",4);var dynCall_fij=Module["dynCall_fij"]=createExportWrapper("dynCall_fij",4);var dynCall_vijf=Module["dynCall_vijf"]=createExportWrapper("dynCall_vijf",5);var dynCall_jiii=Module["dynCall_jiii"]=createExportWrapper("dynCall_jiii",4);var dynCall_viiij=Module["dynCall_viiij"]=createExportWrapper("dynCall_viiij",6);var dynCall_iiijii=Module["dynCall_iiijii"]=createExportWrapper("dynCall_iiijii",7);var dynCall_iiijiii=Module["dynCall_iiijiii"]=createExportWrapper("dynCall_iiijiii",8);var dynCall_iiij=Module["dynCall_iiij"]=createExportWrapper("dynCall_iiij",5);var dynCall_vjjiii=Module["dynCall_vjjiii"]=createExportWrapper("dynCall_vjjiii",8);var dynCall_ijjiii=Module["dynCall_ijjiii"]=createExportWrapper("dynCall_ijjiii",8);var dynCall_iijiii=Module["dynCall_iijiii"]=createExportWrapper("dynCall_iijiii",7);var dynCall_iijjiii=Module["dynCall_iijjiii"]=createExportWrapper("dynCall_iijjiii",9);var dynCall_iiiji=Module["dynCall_iiiji"]=createExportWrapper("dynCall_iiiji",6);var dynCall_jii=Module["dynCall_jii"]=createExportWrapper("dynCall_jii",3);var dynCall_iijj=Module["dynCall_iijj"]=createExportWrapper("dynCall_iijj",6);var dynCall_ji=Module["dynCall_ji"]=createExportWrapper("dynCall_ji",2);var dynCall_vijii=Module["dynCall_vijii"]=createExportWrapper("dynCall_vijii",6);var dynCall_vijif=Module["dynCall_vijif"]=createExportWrapper("dynCall_vijif",6);var dynCall_viiiiij=Module["dynCall_viiiiij"]=createExportWrapper("dynCall_viiiiij",8);var dynCall_viiiiiji=Module["dynCall_viiiiiji"]=createExportWrapper("dynCall_viiiiiji",9);var dynCall_iiiij=Module["dynCall_iiiij"]=createExportWrapper("dynCall_iiiij",6);var dynCall_vijj=Module["dynCall_vijj"]=createExportWrapper("dynCall_vijj",6);var dynCall_iijji=Module["dynCall_iijji"]=createExportWrapper("dynCall_iijji",7);var dynCall_j=Module["dynCall_j"]=createExportWrapper("dynCall_j",1);var dynCall_iiiiiiij=Module["dynCall_iiiiiiij"]=createExportWrapper("dynCall_iiiiiiij",9);var dynCall_viijj=Module["dynCall_viijj"]=createExportWrapper("dynCall_viijj",7);var dynCall_iiiiij=Module["dynCall_iiiiij"]=createExportWrapper("dynCall_iiiiij",7);var dynCall_viiijii=Module["dynCall_viiijii"]=createExportWrapper("dynCall_viiijii",8);var dynCall_jij=Module["dynCall_jij"]=createExportWrapper("dynCall_jij",4);var dynCall_vijji=Module["dynCall_vijji"]=createExportWrapper("dynCall_vijji",7);var dynCall_iiiiiij=Module["dynCall_iiiiiij"]=createExportWrapper("dynCall_iiiiiij",8);var dynCall_jiji=Module["dynCall_jiji"]=createExportWrapper("dynCall_jiji",5);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=createExportWrapper("dynCall_iiiiijj",9);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=createExportWrapper("dynCall_iiiiiijj",10);function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiffifffffffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vififiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifi(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vdiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_idiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiifiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_d(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fifff(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifiiifiiiiiiiiiiiiifiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiid(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiidiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiidii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viid(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiid(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiid(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_fij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ij(index,a1,a2){var sp=stackSave();try{return dynCall_ij(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_jiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{dynCall_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{dynCall_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiijiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_vjjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ijjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_ijjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijif(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijj(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return dynCall_ji(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiij(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiiiij(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiijii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return dynCall_jii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiji(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_jiji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var missingLibrarySymbols=["writeI53ToI64","writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","readI53FromI64","readI53FromU64","convertI32PairToI53","convertU32PairToI53","getTempRet0","exitJS","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","jstoi_q","listenOnce","autoResumeAudioContext","dynCallLegacy","getDynCaller","dynCall","handleException","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asmjsMangle","HandleAllocator","getNativeTypeSize","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","UTF16ToString","stringToUTF16","lengthBytesUTF16","UTF32ToString","stringToUTF32","lengthBytesUTF32","stringToNewUTF8","registerKeyEventCallback","maybeCStringToJsString","findEventTarget","getBoundingClientRect","fillMouseEventData","registerMouseEventCallback","registerWheelEventCallback","registerUiEventCallback","registerFocusEventCallback","fillDeviceOrientationEventData","registerDeviceOrientationEventCallback","fillDeviceMotionEventData","registerDeviceMotionEventCallback","screenOrientation","fillOrientationChangeEventData","registerOrientationChangeEventCallback","fillFullscreenChangeEventData","registerFullscreenChangeEventCallback","JSEvents_requestFullscreen","JSEvents_resizeCanvasForFullscreen","registerRestoreOldStyle","hideEverythingExceptGivenElement","restoreHiddenElements","setLetterbox","softFullscreenResizeWebGLRenderTarget","doRequestFullscreen","fillPointerlockChangeEventData","registerPointerlockChangeEventCallback","registerPointerlockErrorEventCallback","requestPointerLock","fillVisibilityChangeEventData","registerVisibilityChangeEventCallback","registerTouchEventCallback","fillGamepadEventData","registerGamepadEventCallback","registerBeforeUnloadEventCallback","fillBatteryEventData","battery","registerBatteryEventCallback","setCanvasElementSize","getCanvasElementSize","jsStackTrace","getCallstack","convertPCtoSourceLocation","checkWasiClock","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","createDyncallWrapper","safeSetTimeout","setImmediateWrapped","clearImmediateWrapped","polyfillSetImmediate","registerPostMainLoop","registerPreMainLoop","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","safeRequestAnimationFrame","isLeapYear","ydayFromDate","arraySum","addDays","getSocketFromFD","getSocketAddress","FS_unlink","FS_mkdirTree","_setNetworkCallback","heapObjectForWebGLType","toTypedArrayIndex","webgl_enable_ANGLE_instanced_arrays","webgl_enable_OES_vertex_array_object","webgl_enable_WEBGL_draw_buffers","webgl_enable_WEBGL_multi_draw","webgl_enable_EXT_polygon_offset_clamp","webgl_enable_EXT_clip_control","webgl_enable_WEBGL_polygon_mode","emscriptenWebGLGet","computeUnpackAlignedImageSize","colorChannelsInGlTextureFormat","emscriptenWebGLGetTexPixelData","emscriptenWebGLGetUniform","webglGetUniformLocation","webglPrepareUniformLocationsBeforeFirstUse","webglGetLeftBracePos","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","setErrNo","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addOnPreRun","addOnInit","addOnPreMain","addOnExit","addOnPostRun","addRunDependency","removeRunDependency","out","err","callMain","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","convertI32PairToI53Checked","stackSave","stackRestore","stackAlloc","setTempRet0","ptrToString","zeroMemory","getHeapMax","growMemory","ENV","setStackLimits","ERRNO_CODES","strError","DNS","Protocols","Sockets","initRandomFill","randomFill","timers","warnOnce","readEmAsmArgsArray","jstoi_s","getExecutableName","keepRuntimeAlive","asyncLoad","alignMemory","mmapAlloc","wasmTable","noExitRuntime","getCFunc","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","stringToAscii","UTF16Decoder","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","doReadv","doWritev","promiseMap","uncaughtExceptionCount","exceptionLast","exceptionCaught","ExceptionInfo","findMatchingCatch","Browser","getPreloadedImageData__data","wget","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_createPath","FS_createDevice","FS_readFile","FS","FS_createDataFile","FS_createLazyFile","MEMFS","TTY","PIPEFS","SOCKFS","tempFixedLengthArray","miniTempWebGLFloatBuffers","miniTempWebGLIntBuffers","GL","AL","GLUT","EGL","GLEW","IDBStore","SDL","SDL_gfx","allocateUTF8","allocateUTF8OnStack","print","printErr"];unexportedSymbols.forEach(unexportedRuntimeSymbol);var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function stackCheckInit(){_emscripten_stack_init();writeStackCookie()}function run(){if(runDependencies>0){return}stackCheckInit();if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();assert(!Module["_main"],'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]');postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}checkStackCookie()}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise;for(const prop of Object.keys(Module)){if(!(prop in moduleArg)){Object.defineProperty(moduleArg,prop,{configurable:true,get(){abort(`Access to module property ('${prop}') is no longer possible via the module constructor argument; Instead, use the result of the module constructor.`)}})}} +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});["_malloc","_free","_web_engine_create","_web_engine_destroy","_web_engine_get_memory_stats","_web_engine_get_live_handles","_web_engine_get_allocated_bytes","_web_engine_open_blend","_web_engine_apply_command","_web_engine_append_library_object","_web_engine_undo","_web_engine_redo","_web_engine_get_scene_snapshot","_web_engine_get_scene_metadata","_web_engine_get_scene_geometry","_web_engine_get_scene_delta","_web_engine_get_packed_asset","_web_engine_evaluate_depsgraph","_web_engine_save_blend","_web_engine_free_buffer","_web_engine_last_error_code","_web_engine_last_error_message","_web_engine_decimate_apply","_memory","___indirect_function_table","___set_stack_limits","onRuntimeInitialized"].forEach(prop=>{if(!Object.getOwnPropertyDescriptor(readyPromise,prop)){Object.defineProperty(readyPromise,prop,{get:()=>abort("You are getting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js"),set:()=>abort("You are setting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js")})}});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");let dirname=import.meta.url;if(dirname.startsWith("data:")){dirname="/"}var require=createRequire(dirname)}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){if(typeof process=="undefined"||!process.release||process.release.name!=="node")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");var nodeVersion=process.versions.node;var numericVersion=nodeVersion.split(".").slice(0,3);numericVersion=numericVersion[0]*1e4+numericVersion[1]*100+numericVersion[2].split("-")[0]*1;if(numericVersion<16e4){throw new Error("This emscripten-generated code requires node v16.0.0 (detected v"+nodeVersion+")")}var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);assert(ret.buffer);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_SHELL){if(typeof process=="object"&&typeof require==="function"||typeof window=="object"||typeof importScripts=="function")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)")}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}if(!(typeof window=="object"||typeof importScripts=="function"))throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{throw new Error("environment detection error")}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];legacyModuleProp("arguments","arguments_");if(Module["thisProgram"])thisProgram=Module["thisProgram"];legacyModuleProp("thisProgram","thisProgram");assert(typeof Module["memoryInitializerPrefixURL"]=="undefined","Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["pthreadMainPrefixURL"]=="undefined","Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["cdInitializerPrefixURL"]=="undefined","Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["filePackagePrefixURL"]=="undefined","Module.filePackagePrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["read"]=="undefined","Module.read option was removed");assert(typeof Module["readAsync"]=="undefined","Module.readAsync option was removed (modify readAsync in JS)");assert(typeof Module["readBinary"]=="undefined","Module.readBinary option was removed (modify readBinary in JS)");assert(typeof Module["setWindowTitle"]=="undefined","Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)");assert(typeof Module["TOTAL_MEMORY"]=="undefined","Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");legacyModuleProp("asm","wasmExports");legacyModuleProp("readAsync","readAsync");legacyModuleProp("readBinary","readBinary");legacyModuleProp("setWindowTitle","setWindowTitle");assert(!ENVIRONMENT_IS_SHELL,"shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.");var wasmBinary=Module["wasmBinary"];legacyModuleProp("wasmBinary","wasmBinary");if(typeof WebAssembly!="object"){err("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}assert(!Module["STACK_SIZE"],"STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");assert(!Module["wasmMemory"],"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(!Module["INITIAL_MEMORY"],"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically");function writeStackCookie(){var max=_emscripten_stack_get_end();assert((max&3)==0);if(max==0){max+=4}HEAPU32[max>>2]=34821223;checkInt32(34821223);HEAPU32[max+4>>2]=2310721022;checkInt32(2310721022);HEAPU32[0>>2]=1668509029;checkInt32(1668509029)}function checkStackCookie(){if(ABORT)return;var max=_emscripten_stack_get_end();if(max==0){max+=4}var cookie1=HEAPU32[max>>2];var cookie2=HEAPU32[max+4>>2];if(cookie1!=34821223||cookie2!=2310721022){abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`)}if(HEAPU32[0>>2]!=1668509029){abort("Runtime error: The application has corrupted its heap memory area (address zero)!")}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();setStackLimits();if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){checkStackCookie();var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}assert(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;var runDependencyTracking={};function getUniqueRunDependency(id){var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(!runDependencyTracking[id]);runDependencyTracking[id]=1;if(runDependencyWatcher===null&&typeof setInterval!="undefined"){runDependencyWatcher=setInterval(()=>{if(ABORT){clearInterval(runDependencyWatcher);runDependencyWatcher=null;return}var shown=false;for(var dep in runDependencyTracking){if(!shown){shown=true;err("still waiting on run dependencies:")}err(`dependency: ${dep}`)}if(shown){err("(end of list)")}},1e4)}}else{err("warning: run dependency added without ID")}}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(runDependencyTracking[id]);delete runDependencyTracking[id]}else{err("warning: run dependency removed without ID")}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function createExportWrapper(name,nargs){return(...args)=>{assert(runtimeInitialized,`native function \`${name}\` called before runtime initialization`);var f=wasmExports[name];assert(f,`exported native function \`${name}\` not found`);assert(args.length<=nargs,`native function \`${name}\` called with ${args.length} args but expects ${nargs}`);return f(...args)}}function findWasmBinary(){if(Module["locateFile"]){var f="web_engine.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("web_engine.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);if(isFileURI(wasmBinaryFile)){err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`)}abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];assert(wasmMemory,"memory not found in wasm exports");updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];assert(wasmTable,"table not found in wasm exports");addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");var trueModule=Module;function receiveInstantiationResult(result){assert(Module===trueModule,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");trueModule=null;receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;(()=>{var h16=new Int16Array(1);var h8=new Int8Array(h16.buffer);h16[0]=25459;if(h8[0]!==115||h8[1]!==99)throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"})();if(Module["ENVIRONMENT"]){throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)")}function legacyModuleProp(prop,newName,incoming=true){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,get(){let extra=incoming?" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)":"";abort(`\`Module.${prop}\` has been replaced by \`${newName}\``+extra)}})}}function ignoredModuleProp(prop){if(Object.getOwnPropertyDescriptor(Module,prop)){abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`)}}function isExportedByForceFilesystem(name){return name==="FS_createPath"||name==="FS_createDataFile"||name==="FS_createPreloadedFile"||name==="FS_unlink"||name==="addRunDependency"||name==="FS_createLazyFile"||name==="FS_createDevice"||name==="removeRunDependency"}function hookGlobalSymbolAccess(sym,func){if(typeof globalThis!="undefined"&&!Object.getOwnPropertyDescriptor(globalThis,sym)){Object.defineProperty(globalThis,sym,{configurable:true,get(){func();return undefined}})}}function missingGlobal(sym,msg){hookGlobalSymbolAccess(sym,()=>{warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`)})}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){hookGlobalSymbolAccess(sym,()=>{var msg=`\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;var librarySymbol=sym;if(!librarySymbol.startsWith("_")){librarySymbol="$"+sym}msg+=` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}warnOnce(msg)});unexportedRuntimeSymbol(sym)}function unexportedRuntimeSymbol(sym){if(!Object.getOwnPropertyDescriptor(Module,sym)){Object.defineProperty(Module,sym,{configurable:true,get(){var msg=`'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}abort(msg)}})}}var MAX_UINT8=2**8-1;var MAX_UINT16=2**16-1;var MAX_UINT32=2**32-1;var MAX_UINT53=2**53-1;var MAX_UINT64=2**64-1;var MIN_INT8=-(2**(8-1));var MIN_INT16=-(2**(16-1));var MIN_INT32=-(2**(32-1));var MIN_INT53=-(2**(53-1));var MIN_INT64=-(2**(64-1));function checkInt(value,bits,min,max){assert(Number.isInteger(Number(value)),`attempt to write non-integer (${value}) into integer heap`);assert(value<=max,`value (${value}) too large to write as ${bits}-bit value`);assert(value>=min,`value (${value}) too small to write as ${bits}-bit value`)}var checkInt8=value=>checkInt(value,8,MIN_INT8,MAX_UINT8);var checkInt16=value=>checkInt(value,16,MIN_INT16,MAX_UINT16);var checkInt32=value=>checkInt(value,32,MIN_INT32,MAX_UINT32);var checkInt64=value=>checkInt(value,64,MIN_INT64,MAX_UINT64);function dbg(...args){console.warn(...args)}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var ptrToString=ptr=>{assert(typeof ptr==="number");ptr>>>=0;return"0x"+ptr.toString(16).padStart(8,"0")};var setStackLimits=()=>{var stackLow=_emscripten_stack_get_base();var stackHigh=_emscripten_stack_get_end();___set_stack_limits(stackLow,stackHigh)};var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text="warning: "+text;err(text)}};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}assert(wasmTable.get(funcPtr)==func,"JavaScript-side Wasm function table mirror is out of date!");return func};var ___call_sighandler=(fp,sig)=>getWasmTableEntry(fp)(sig);var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);assert(exceptionCaught.length>0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught;checkInt8(caught)}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown;checkInt8(rethrown)}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___handle_stack_overflow=requested=>{var base=_emscripten_stack_get_base();var end=_emscripten_stack_get_end();abort(`stack overflow (Attempt to set SP to ${ptrToString(requested)}`+`, with stack limits [${ptrToString(end)} - ${ptrToString(base)}`+"]). If you require more stack space build with -sSTACK_SIZE=")};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{assert(typeof str==="string",`stringToUTF8Array expects a string (got ${typeof str})`);if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;if(u>1114111)warnOnce("Invalid Unicode code point "+ptrToString(u)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size)};var alignMemory=(size,alignment)=>{assert(alignment,"alignment argument is required");return Math.ceil(size/alignment)*alignment};var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);assert(size>=0);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var strError=errno=>UTF8ToString(_strerror(errno));var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class extends Error{constructor(errno){super(runtimeInitialized?strError(errno):"");this.name="ErrnoError";this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){assert(typeof parent=="object");var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){assert(fd>=-1);stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){assert(FS.syncFSRequests>0);FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){if(typeof type=="string"){throw type}var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);assert(idx!==-1);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;i=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate(stream,offset,length){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){assert(offset>=0);if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1);assert(stdin.fd===0,`invalid handle for stdin (${stdin.fd})`);assert(stdout.fd===1,`invalid handle for stdout (${stdout.fd})`);assert(stderr.fd===2,`invalid handle for stderr (${stderr.fd})`)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){assert(!FS.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;_fflush(0);for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);assert(size>=0);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node},absolutePath(){abort("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder(){abort("FS.createFolder has been removed; use FS.mkdir instead")},createLink(){abort("FS.createLink has been removed; use FS.symlink instead")},joinPath(){abort("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc(){abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath(){abort("FS.standardizePath has been removed; use PATH.normalize instead")}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;checkInt32(stat.dev);HEAP32[buf+4>>2]=stat.mode;checkInt32(stat.mode);HEAPU32[buf+8>>2]=stat.nlink;checkInt32(stat.nlink);HEAP32[buf+12>>2]=stat.uid;checkInt32(stat.uid);HEAP32[buf+16>>2]=stat.gid;checkInt32(stat.gid);HEAP32[buf+20>>2]=stat.rdev;checkInt32(stat.rdev);tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];checkInt64(stat.size);HEAP32[buf+32>>2]=4096;checkInt32(4096);HEAP32[buf+36>>2]=stat.blocks;checkInt32(stat.blocks);var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];checkInt64(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;checkInt32(atime%1e3*1e3*1e3);tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];checkInt64(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;checkInt32(mtime%1e3*1e3*1e3);tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];checkInt64(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;checkInt32(ctime%1e3*1e3*1e3);tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];checkInt64(stat.ino);return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);assert(flags===0||flags==512);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){assert(SYSCALLS.varargs!=undefined);var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;checkInt16(2);return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>{assert(typeof maxBytesToWrite=="number","stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)};function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.getStreamFromFD(fd);stream.getdents||=FS.readdir(stream.path);var struct_size=280;var pos=0;var off=FS.llseek(stream,0,1);var idx=Math.floor(off/struct_size);while(idx>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];checkInt64(id);tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];checkInt64((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;checkInt16(280);HEAP8[dirp+pos+18]=type;checkInt8(type);stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;checkInt32(termios.c_iflag||0);HEAP32[argp+4>>2]=termios.c_oflag||0;checkInt32(termios.c_oflag||0);HEAP32[argp+8>>2]=termios.c_cflag||0;checkInt32(termios.c_cflag||0);HEAP32[argp+12>>2]=termios.c_lflag||0;checkInt32(termios.c_lflag||0);for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0;checkInt8(termios.c_cc[i]||0)}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;checkInt32(0);return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];checkInt16(winsize[0]);HEAP16[argp+2>>1]=winsize[1];checkInt16(winsize[1])}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;assert(!flags,`unknown flags in __syscall_newfstatat: ${flags}`);path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("native code called abort()")};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var __emscripten_throw_longjmp=()=>{throw Infinity};var convertI32PairToI53Checked=(lo,hi)=>{assert(lo==lo>>>0||lo==(lo|0));assert(hi===(hi|0));return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;checkInt32(res.allocated);HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;checkInt32(stdTimezoneOffset*60);HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);checkInt32(Number(winterOffset!=summerOffset));var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);assert(winterName);assert(summerName);assert(lengthBytesUTF8(winterName)<=16,`timezone name truncated to fit in TZNAME_MAX (${winterName})`);assert(lengthBytesUTF8(summerName)<=16,`timezone name truncated to fit in TZNAME_MAX (${summerName})`);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_get_now=()=>performance.now();var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){err(`growMemory: Attempted to grow heap from ${b.byteLength} bytes to ${size} bytes, but got error: ${e}`)}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;assert(requestedSize>oldSize);var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`);return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var t0=_emscripten_get_now();var replacement=growMemory(newSize);var t1=_emscripten_get_now();dbg(`Heap resize call from ${oldSize} to ${newSize} took ${t1-t0} msecs. Success: ${!!replacement}`);if(replacement){return true}}err(`Failed to grow the heap from ${oldSize} bytes to ${newSize} bytes, not enough memory!`);return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;checkInt32(ptr);stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;checkInt32(strings.length);var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;checkInt32(bufSize);return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;checkInt8(type);HEAP16[pbuf+2>>1]=flags;checkInt16(flags);tempI64=[rightsBase>>>0,(tempDouble=rightsBase,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+8>>2]=tempI64[0],HEAP32[pbuf+12>>2]=tempI64[1];checkInt64(rightsBase);tempI64=[rightsInheriting>>>0,(tempDouble=rightsInheriting,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+16>>2]=tempI64[0],HEAP32[pbuf+20>>2]=tempI64[1];checkInt64(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];checkInt64(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var _llvm_eh_typeid_for=type=>type;var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var getCFunc=ident=>{var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func};var writeArrayToMemory=(array,buffer)=>{assert(array.length>=0,"writeArrayToMemory array must have a length (should be an array or typed array)");HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;assert(returnType!=="array",'Return type should not be "array".');if(args){for(var i=0;i(...args)=>ccall(ident,returnType,argTypes,args,opts);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var wasmImports={__assert_fail:___assert_fail,__call_sighandler:___call_sighandler,__cxa_begin_catch:___cxa_begin_catch,__cxa_end_catch:___cxa_end_catch,__cxa_find_matching_catch_2:___cxa_find_matching_catch_2,__cxa_find_matching_catch_3:___cxa_find_matching_catch_3,__cxa_rethrow:___cxa_rethrow,__cxa_throw:___cxa_throw,__handle_stack_overflow:___handle_stack_overflow,__resumeException:___resumeException,__syscall_faccessat:___syscall_faccessat,__syscall_fcntl64:___syscall_fcntl64,__syscall_fstat64:___syscall_fstat64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_rmdir:___syscall_rmdir,__syscall_stat64:___syscall_stat64,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,_emscripten_memcpy_js:__emscripten_memcpy_js,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_emscripten_throw_longjmp:__emscripten_throw_longjmp,_mmap_js:__mmap_js,_munmap_js:__munmap_js,_tzset_js:__tzset_js,emscripten_date_now:_emscripten_date_now,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,getentropy:_getentropy,invoke_d,invoke_diii,invoke_fi,invoke_fif,invoke_fifff,invoke_fii,invoke_fiii,invoke_fiiii,invoke_fiiiiii,invoke_fiiiiiii,invoke_fij,invoke_i,invoke_idiii,invoke_ii,invoke_iif,invoke_iifi,invoke_iifii,invoke_iifiiiii,invoke_iii,invoke_iiid,invoke_iiidii,invoke_iiif,invoke_iiiffi,invoke_iiii,invoke_iiiidi,invoke_iiiiffifffffffi,invoke_iiiifiii,invoke_iiiii,invoke_iiiiif,invoke_iiiiifi,invoke_iiiiii,invoke_iiiiiifi,invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii,invoke_iiiiiii,invoke_iiiiiiifii,invoke_iiiiiiii,invoke_iiiiiiiii,invoke_iiiiiiiiiffi,invoke_iiiiiiiiii,invoke_iiiiiiiiiifi,invoke_iiiiiiiiiii,invoke_iiiiiiiiiiii,invoke_iiiiiiiiiiiif,invoke_iiiiiiiiiiiiiii,invoke_iiiiiiij,invoke_iiiiiij,invoke_iiiiij,invoke_iiiij,invoke_iiij,invoke_iiiji,invoke_iiijii,invoke_iiijiii,invoke_iij,invoke_iijiii,invoke_iijj,invoke_iijji,invoke_iijjiii,invoke_ij,invoke_ijjiii,invoke_j,invoke_ji,invoke_jii,invoke_jiii,invoke_jiji,invoke_v,invoke_vdiii,invoke_vi,invoke_vid,invoke_vif,invoke_vififiif,invoke_vifii,invoke_vifiiifiiiiiiiiiiiiifiiii,invoke_vii,invoke_viid,invoke_viif,invoke_viiff,invoke_viifi,invoke_viifiii,invoke_viii,invoke_viiid,invoke_viiif,invoke_viiiffii,invoke_viiiffiiii,invoke_viiifi,invoke_viiii,invoke_viiiif,invoke_viiiii,invoke_viiiiif,invoke_viiiiii,invoke_viiiiiid,invoke_viiiiiif,invoke_viiiiiifi,invoke_viiiiiifif,invoke_viiiiiifii,invoke_viiiiiii,invoke_viiiiiiidiiii,invoke_viiiiiiii,invoke_viiiiiiiif,invoke_viiiiiiiii,invoke_viiiiiiiiii,invoke_viiiiiiiiiidii,invoke_viiiiiiiiiii,invoke_viiiiiiiiiiii,invoke_viiiiiiiiiiiiii,invoke_viiiiiiiiiiiiiiii,invoke_viiiiij,invoke_viiiiiji,invoke_viiiiji,invoke_viiiijiiifi,invoke_viiij,invoke_viiiji,invoke_viiijii,invoke_viiijjji,invoke_viij,invoke_viiji,invoke_viijii,invoke_viijj,invoke_vij,invoke_viji,invoke_vijif,invoke_vijii,invoke_vijj,invoke_vjjiii,llvm_eh_typeid_for:_llvm_eh_typeid_for,proc_exit:_proc_exit};var wasmExports=createWasm();var ___wasm_call_ctors=createExportWrapper("__wasm_call_ctors",0);var _web_engine_create=Module["_web_engine_create"]=createExportWrapper("web_engine_create",0);var _web_engine_get_memory_stats=Module["_web_engine_get_memory_stats"]=createExportWrapper("web_engine_get_memory_stats",1);var _web_engine_destroy=Module["_web_engine_destroy"]=createExportWrapper("web_engine_destroy",1);var _fflush=createExportWrapper("fflush",1);var _web_engine_get_live_handles=Module["_web_engine_get_live_handles"]=createExportWrapper("web_engine_get_live_handles",0);var _web_engine_get_allocated_bytes=Module["_web_engine_get_allocated_bytes"]=createExportWrapper("web_engine_get_allocated_bytes",0);var _web_engine_open_blend=Module["_web_engine_open_blend"]=createExportWrapper("web_engine_open_blend",3);var _web_engine_apply_command=Module["_web_engine_apply_command"]=createExportWrapper("web_engine_apply_command",3);var _web_engine_decimate_apply=Module["_web_engine_decimate_apply"]=createExportWrapper("web_engine_decimate_apply",35);var _web_engine_append_library_object=Module["_web_engine_append_library_object"]=createExportWrapper("web_engine_append_library_object",5);var _web_engine_undo=Module["_web_engine_undo"]=createExportWrapper("web_engine_undo",1);var _web_engine_redo=Module["_web_engine_redo"]=createExportWrapper("web_engine_redo",1);var _web_engine_get_scene_snapshot=Module["_web_engine_get_scene_snapshot"]=createExportWrapper("web_engine_get_scene_snapshot",3);var _web_engine_get_scene_metadata=Module["_web_engine_get_scene_metadata"]=createExportWrapper("web_engine_get_scene_metadata",3);var _web_engine_get_scene_geometry=Module["_web_engine_get_scene_geometry"]=createExportWrapper("web_engine_get_scene_geometry",3);var _web_engine_get_scene_delta=Module["_web_engine_get_scene_delta"]=createExportWrapper("web_engine_get_scene_delta",3);var _web_engine_get_packed_asset=Module["_web_engine_get_packed_asset"]=createExportWrapper("web_engine_get_packed_asset",5);var _web_engine_evaluate_depsgraph=Module["_web_engine_evaluate_depsgraph"]=createExportWrapper("web_engine_evaluate_depsgraph",3);var _web_engine_save_blend=Module["_web_engine_save_blend"]=createExportWrapper("web_engine_save_blend",3);var _malloc=Module["_malloc"]=createExportWrapper("malloc",1);var _web_engine_free_buffer=Module["_web_engine_free_buffer"]=createExportWrapper("web_engine_free_buffer",1);var _free=Module["_free"]=createExportWrapper("free",1);var _web_engine_last_error_code=Module["_web_engine_last_error_code"]=createExportWrapper("web_engine_last_error_code",0);var _web_engine_last_error_message=Module["_web_engine_last_error_message"]=createExportWrapper("web_engine_last_error_message",0);var _strerror=createExportWrapper("strerror",1);var _emscripten_builtin_memalign=createExportWrapper("emscripten_builtin_memalign",2);var _setThrew=createExportWrapper("setThrew",2);var __emscripten_tempret_set=createExportWrapper("_emscripten_tempret_set",1);var __emscripten_tempret_get=createExportWrapper("_emscripten_tempret_get",0);var _emscripten_stack_init=()=>(_emscripten_stack_init=wasmExports["emscripten_stack_init"])();var _emscripten_stack_get_free=()=>(_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"])();var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"])();var _emscripten_stack_get_end=()=>(_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();var ___cxa_decrement_exception_refcount=createExportWrapper("__cxa_decrement_exception_refcount",1);var ___cxa_increment_exception_refcount=createExportWrapper("__cxa_increment_exception_refcount",1);var ___cxa_can_catch=createExportWrapper("__cxa_can_catch",3);var ___cxa_get_exception_ptr=createExportWrapper("__cxa_get_exception_ptr",1);var ___set_stack_limits=Module["___set_stack_limits"]=createExportWrapper("__set_stack_limits",2);var dynCall_viij=Module["dynCall_viij"]=createExportWrapper("dynCall_viij",5);var dynCall_viiji=Module["dynCall_viiji"]=createExportWrapper("dynCall_viiji",6);var dynCall_viijii=Module["dynCall_viijii"]=createExportWrapper("dynCall_viijii",7);var dynCall_vij=Module["dynCall_vij"]=createExportWrapper("dynCall_vij",4);var dynCall_viiiijiiifi=Module["dynCall_viiiijiiifi"]=createExportWrapper("dynCall_viiiijiiifi",12);var dynCall_viiijjji=Module["dynCall_viiijjji"]=createExportWrapper("dynCall_viiijjji",11);var dynCall_viiiiji=Module["dynCall_viiiiji"]=createExportWrapper("dynCall_viiiiji",8);var dynCall_viiiji=Module["dynCall_viiiji"]=createExportWrapper("dynCall_viiiji",7);var dynCall_ij=Module["dynCall_ij"]=createExportWrapper("dynCall_ij",3);var dynCall_viji=Module["dynCall_viji"]=createExportWrapper("dynCall_viji",5);var dynCall_iij=Module["dynCall_iij"]=createExportWrapper("dynCall_iij",4);var dynCall_fij=Module["dynCall_fij"]=createExportWrapper("dynCall_fij",4);var dynCall_vijf=Module["dynCall_vijf"]=createExportWrapper("dynCall_vijf",5);var dynCall_jiii=Module["dynCall_jiii"]=createExportWrapper("dynCall_jiii",4);var dynCall_viiij=Module["dynCall_viiij"]=createExportWrapper("dynCall_viiij",6);var dynCall_iiijii=Module["dynCall_iiijii"]=createExportWrapper("dynCall_iiijii",7);var dynCall_iiijiii=Module["dynCall_iiijiii"]=createExportWrapper("dynCall_iiijiii",8);var dynCall_iiij=Module["dynCall_iiij"]=createExportWrapper("dynCall_iiij",5);var dynCall_vjjiii=Module["dynCall_vjjiii"]=createExportWrapper("dynCall_vjjiii",8);var dynCall_ijjiii=Module["dynCall_ijjiii"]=createExportWrapper("dynCall_ijjiii",8);var dynCall_iijiii=Module["dynCall_iijiii"]=createExportWrapper("dynCall_iijiii",7);var dynCall_iijjiii=Module["dynCall_iijjiii"]=createExportWrapper("dynCall_iijjiii",9);var dynCall_iiiji=Module["dynCall_iiiji"]=createExportWrapper("dynCall_iiiji",6);var dynCall_jii=Module["dynCall_jii"]=createExportWrapper("dynCall_jii",3);var dynCall_iijj=Module["dynCall_iijj"]=createExportWrapper("dynCall_iijj",6);var dynCall_vijj=Module["dynCall_vijj"]=createExportWrapper("dynCall_vijj",6);var dynCall_ji=Module["dynCall_ji"]=createExportWrapper("dynCall_ji",2);var dynCall_vijii=Module["dynCall_vijii"]=createExportWrapper("dynCall_vijii",6);var dynCall_vijif=Module["dynCall_vijif"]=createExportWrapper("dynCall_vijif",6);var dynCall_viiiiij=Module["dynCall_viiiiij"]=createExportWrapper("dynCall_viiiiij",8);var dynCall_viiiiiji=Module["dynCall_viiiiiji"]=createExportWrapper("dynCall_viiiiiji",9);var dynCall_iiiij=Module["dynCall_iiiij"]=createExportWrapper("dynCall_iiiij",6);var dynCall_iijji=Module["dynCall_iijji"]=createExportWrapper("dynCall_iijji",7);var dynCall_j=Module["dynCall_j"]=createExportWrapper("dynCall_j",1);var dynCall_iiiiiiij=Module["dynCall_iiiiiiij"]=createExportWrapper("dynCall_iiiiiiij",9);var dynCall_viijj=Module["dynCall_viijj"]=createExportWrapper("dynCall_viijj",7);var dynCall_iiiiij=Module["dynCall_iiiiij"]=createExportWrapper("dynCall_iiiiij",7);var dynCall_viiijii=Module["dynCall_viiijii"]=createExportWrapper("dynCall_viiijii",8);var dynCall_jij=Module["dynCall_jij"]=createExportWrapper("dynCall_jij",4);var dynCall_vijji=Module["dynCall_vijji"]=createExportWrapper("dynCall_vijji",7);var dynCall_iiiiiij=Module["dynCall_iiiiiij"]=createExportWrapper("dynCall_iiiiiij",8);var dynCall_jiji=Module["dynCall_jiji"]=createExportWrapper("dynCall_jiji",5);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=createExportWrapper("dynCall_iiiiijj",9);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=createExportWrapper("dynCall_iiiiiijj",10);function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiffifffffffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vififiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifi(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vdiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_idiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiifiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_d(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fifff(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifiiifiiiiiiiiiiiiifiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiid(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiidiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiidii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viid(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiid(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiid(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_fij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ij(index,a1,a2){var sp=stackSave();try{return dynCall_ij(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_jiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{dynCall_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{dynCall_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiijiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_vjjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ijjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_ijjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijif(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijj(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return dynCall_ji(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiij(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiiiij(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiijii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return dynCall_jii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiji(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_jiji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var missingLibrarySymbols=["writeI53ToI64","writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","readI53FromI64","readI53FromU64","convertI32PairToI53","convertU32PairToI53","getTempRet0","exitJS","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","jstoi_q","listenOnce","autoResumeAudioContext","dynCallLegacy","getDynCaller","dynCall","handleException","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asmjsMangle","HandleAllocator","getNativeTypeSize","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","UTF16ToString","stringToUTF16","lengthBytesUTF16","UTF32ToString","stringToUTF32","lengthBytesUTF32","stringToNewUTF8","registerKeyEventCallback","maybeCStringToJsString","findEventTarget","getBoundingClientRect","fillMouseEventData","registerMouseEventCallback","registerWheelEventCallback","registerUiEventCallback","registerFocusEventCallback","fillDeviceOrientationEventData","registerDeviceOrientationEventCallback","fillDeviceMotionEventData","registerDeviceMotionEventCallback","screenOrientation","fillOrientationChangeEventData","registerOrientationChangeEventCallback","fillFullscreenChangeEventData","registerFullscreenChangeEventCallback","JSEvents_requestFullscreen","JSEvents_resizeCanvasForFullscreen","registerRestoreOldStyle","hideEverythingExceptGivenElement","restoreHiddenElements","setLetterbox","softFullscreenResizeWebGLRenderTarget","doRequestFullscreen","fillPointerlockChangeEventData","registerPointerlockChangeEventCallback","registerPointerlockErrorEventCallback","requestPointerLock","fillVisibilityChangeEventData","registerVisibilityChangeEventCallback","registerTouchEventCallback","fillGamepadEventData","registerGamepadEventCallback","registerBeforeUnloadEventCallback","fillBatteryEventData","battery","registerBatteryEventCallback","setCanvasElementSize","getCanvasElementSize","jsStackTrace","getCallstack","convertPCtoSourceLocation","checkWasiClock","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","createDyncallWrapper","safeSetTimeout","setImmediateWrapped","clearImmediateWrapped","polyfillSetImmediate","registerPostMainLoop","registerPreMainLoop","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","safeRequestAnimationFrame","isLeapYear","ydayFromDate","arraySum","addDays","getSocketFromFD","getSocketAddress","FS_unlink","FS_mkdirTree","_setNetworkCallback","heapObjectForWebGLType","toTypedArrayIndex","webgl_enable_ANGLE_instanced_arrays","webgl_enable_OES_vertex_array_object","webgl_enable_WEBGL_draw_buffers","webgl_enable_WEBGL_multi_draw","webgl_enable_EXT_polygon_offset_clamp","webgl_enable_EXT_clip_control","webgl_enable_WEBGL_polygon_mode","emscriptenWebGLGet","computeUnpackAlignedImageSize","colorChannelsInGlTextureFormat","emscriptenWebGLGetTexPixelData","emscriptenWebGLGetUniform","webglGetUniformLocation","webglPrepareUniformLocationsBeforeFirstUse","webglGetLeftBracePos","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","setErrNo","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addOnPreRun","addOnInit","addOnPreMain","addOnExit","addOnPostRun","addRunDependency","removeRunDependency","out","err","callMain","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","convertI32PairToI53Checked","stackSave","stackRestore","stackAlloc","setTempRet0","ptrToString","zeroMemory","getHeapMax","growMemory","ENV","setStackLimits","ERRNO_CODES","strError","DNS","Protocols","Sockets","initRandomFill","randomFill","timers","warnOnce","readEmAsmArgsArray","jstoi_s","getExecutableName","keepRuntimeAlive","asyncLoad","alignMemory","mmapAlloc","wasmTable","noExitRuntime","getCFunc","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","stringToAscii","UTF16Decoder","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","doReadv","doWritev","promiseMap","uncaughtExceptionCount","exceptionLast","exceptionCaught","ExceptionInfo","findMatchingCatch","Browser","getPreloadedImageData__data","wget","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_createPath","FS_createDevice","FS_readFile","FS","FS_createDataFile","FS_createLazyFile","MEMFS","TTY","PIPEFS","SOCKFS","tempFixedLengthArray","miniTempWebGLFloatBuffers","miniTempWebGLIntBuffers","GL","AL","GLUT","EGL","GLEW","IDBStore","SDL","SDL_gfx","allocateUTF8","allocateUTF8OnStack","print","printErr"];unexportedSymbols.forEach(unexportedRuntimeSymbol);var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function stackCheckInit(){_emscripten_stack_init();writeStackCookie()}function run(){if(runDependencies>0){return}stackCheckInit();if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();assert(!Module["_main"],'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]');postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}checkStackCookie()}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise;for(const prop of Object.keys(Module)){if(!(prop in moduleArg)){Object.defineProperty(moduleArg,prop,{configurable:true,get(){abort(`Access to module property ('${prop}') is no longer possible via the module constructor argument; Instead, use the result of the module constructor.`)}})}} return moduleRtn; diff --git a/web/app/public/vendor/blender/web_engine.wasm b/web/app/public/vendor/blender/web_engine.wasm index 4568056d..a213cd82 100644 Binary files a/web/app/public/vendor/blender/web_engine.wasm and b/web/app/public/vendor/blender/web_engine.wasm differ diff --git a/web/app/src/assets/AssetPreviewDisplay.ts b/web/app/src/assets/AssetPreviewDisplay.ts new file mode 100644 index 00000000..5bd3290c --- /dev/null +++ b/web/app/src/assets/AssetPreviewDisplay.ts @@ -0,0 +1,87 @@ +import { planAssetPreviewDecode } from "../../../protocol/asset-preview-decode"; +import type { AssetPreviewIdentityIR } from "../../../protocol/asset-preview"; + +export type AssetPreviewDisplayBackend = "MAIN_THREAD_CANVAS_2D" | "OFFSCREEN_CANVAS_2D"; + +export interface AssetPreviewDisplayReceiptIR { + schemaVersion: 1; + status: "READY"; + backend: AssetPreviewDisplayBackend; + identitySha256: string; + contentSha256: string; + width: number; + height: number; + pixelByteLength: number; + pixelSha256: string; + nonTransparentPixels: number; + bitmapClosed: true; +} + +export interface AssetPreviewDisplayResultIR { + receipt: AssetPreviewDisplayReceiptIR; + pixels: Uint8Array; +} + +type PreviewCanvas = HTMLCanvasElement | OffscreenCanvas; + +async function sha256Bytes(value: Uint8Array): Promise { + const digest = await crypto.subtle.digest("SHA-256", Uint8Array.from(value).buffer); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function displayAssetPreview( + canvas: PreviewCanvas, + backend: AssetPreviewDisplayBackend, + identityValue: unknown, + encodedBytes: ArrayBuffer, +): Promise { + const plan = await planAssetPreviewDecode(identityValue, encodedBytes); + if (typeof createImageBitmap !== "function") throw new Error("CAPABILITY_MISSING: createImageBitmap is unavailable"); + const identity = identityValue as AssetPreviewIdentityIR; + const bitmap = await createImageBitmap(new Blob([encodedBytes], { type: plan.mimeType }), { + colorSpaceConversion: "none", + premultiplyAlpha: "none", + imageOrientation: "none", + }); + try { + if (bitmap.width !== plan.width || bitmap.height !== plan.height) { + throw new Error("ASSET_PREVIEW_IDENTITY_MISMATCH: decoded preview dimensions changed"); + } + canvas.width = plan.width; + canvas.height = plan.height; + const context = canvas.getContext("2d", { alpha: true, willReadFrequently: true }) as + | CanvasRenderingContext2D + | OffscreenCanvasRenderingContext2D + | null; + if (!context) throw new Error("CAPABILITY_MISSING: preview Canvas2D context is unavailable"); + context.clearRect(0, 0, plan.width, plan.height); + context.imageSmoothingEnabled = false; + context.globalCompositeOperation = "copy"; + context.drawImage(bitmap, 0, 0, plan.width, plan.height); + const pixels = new Uint8Array(context.getImageData(0, 0, plan.width, plan.height).data); + let nonTransparentPixels = 0; + for (let offset = 3; offset < pixels.byteLength; offset += 4) if (pixels[offset] !== 0) nonTransparentPixels++; + const receipt: AssetPreviewDisplayReceiptIR = { + schemaVersion: 1, + status: "READY", + backend, + identitySha256: identity.identitySha256, + contentSha256: identity.content.sha256, + width: plan.width, + height: plan.height, + pixelByteLength: pixels.byteLength, + pixelSha256: await sha256Bytes(pixels), + nonTransparentPixels, + bitmapClosed: true, + }; + if (typeof HTMLCanvasElement !== "undefined" && canvas instanceof HTMLCanvasElement) { + canvas.dataset.assetPreviewStatus = "ready"; + canvas.dataset.assetPreviewBackend = backend; + canvas.dataset.assetPreviewPixelSha256 = receipt.pixelSha256; + } + return { receipt, pixels }; + } + finally { + bitmap.close(); + } +} diff --git a/web/app/src/engine-client/WebEngineClient.ts b/web/app/src/engine-client/WebEngineClient.ts index dc54f453..03a29e77 100644 --- a/web/app/src/engine-client/WebEngineClient.ts +++ b/web/app/src/engine-client/WebEngineClient.ts @@ -30,6 +30,7 @@ import type { PaintStrokeSessionReceiptIR, } from "../../../protocol/paint-stroke-session"; import type { PaintPBVHCapabilityRequest } from "../../../protocol/paint-pbvh-capability"; +import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "../../../protocol/library-main-append"; interface PendingRequest { resolve: (result: WebEngineResult) => void; @@ -122,6 +123,28 @@ export class WebEngineClient { }; } + async appendLibraryObject( + source: ArrayBuffer, + request: LibraryMainAppendRequestIR, + ): Promise { + const result = await this.request({ type: "appendLibraryObject", request, source }, [source]); + if (!result.snapshot || !result.delta || !result.libraryAppend) { + throw this.report("ASSET_MANIFEST_INVALID", "WebEngine did not return the committed library append", true); + } + this.geometryBuffers = result.geometryDelta + ? applyMeshGeometryDelta(this.geometryBuffers, result.geometryDelta) + : result.geometryBuffers ?? this.geometryBuffers; + this.nonMeshGeometryBuffers = result.nonMeshGeometryBuffers ?? this.nonMeshGeometryBuffers; + return { + status: result.status, + snapshot: result.snapshot, + geometryBuffers: [...this.geometryBuffers], + nonMeshGeometryBuffers: [...this.nonMeshGeometryBuffers], + delta: result.delta, + receipt: result.libraryAppend, + }; + } + async beginPaintStroke(session: PaintStrokeSessionBeginIR): Promise { const result = await this.request({ type: "beginPaintStroke", session }); if (!result.paintStrokeSession) throw this.report("PAINT_SCHEMA_INVALID", "WebEngine did not open the paint pointer session", true); diff --git a/web/app/src/sequencer/SequencerMediaProxyCache.ts b/web/app/src/sequencer/SequencerMediaProxyCache.ts index c2464606..6d188e1e 100644 --- a/web/app/src/sequencer/SequencerMediaProxyCache.ts +++ b/web/app/src/sequencer/SequencerMediaProxyCache.ts @@ -42,7 +42,13 @@ async function sha256(data: ArrayBuffer): Promise { return Array.from(new Uint8Array(result), (value) => value.toString(16).padStart(2, "0")).join(""); } -function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number): Promise { +function cancelled(signal: AbortSignal | undefined): void { + if (signal?.aborted) { + throw new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled"); + } +} + +function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number, signal?: AbortSignal): Promise { if (video.readyState >= HTMLMediaElement.HAVE_CURRENT_DATA) return Promise.resolve(); return new Promise((resolve, reject) => { const timeout = window.setTimeout(() => finish(new Error("timeout")), timeoutMs); @@ -50,12 +56,16 @@ function waitForLoadedFrame(video: HTMLVideoElement, timeoutMs: number): Promise window.clearTimeout(timeout); video.removeEventListener("loadeddata", ready); video.removeEventListener("error", failed); + signal?.removeEventListener("abort", aborted); if (error) reject(error); else resolve(); }; const ready = (): void => finish(); const failed = (): void => finish(new Error("decode")); + const aborted = (): void => finish(new SequencerMediaCacheValidationError("SEQUENCER_CANCELLED", "Movie proxy generation was cancelled")); video.addEventListener("loadeddata", ready, { once: true }); video.addEventListener("error", failed, { once: true }); + signal?.addEventListener("abort", aborted, { once: true }); + if (signal?.aborted) aborted(); }); } @@ -64,7 +74,9 @@ export async function generateInitialSequencerMovieProxyFrame( capabilityValue: unknown, profileValue: unknown, sourceData: ArrayBuffer, + signal?: AbortSignal, ): Promise { + cancelled(signal); const source = parseSequencerCodecProbeRequest(sourceValue); const capability = parseSequencerCodecProbeResult(capabilityValue); const profile = parseSequencerMediaProxyProfile(profileValue); @@ -75,6 +87,7 @@ export async function generateInitialSequencerMovieProxyFrame( if (!(sourceData instanceof ArrayBuffer) || sourceData.byteLength !== source.byteLength || await sha256(sourceData) !== source.sourceSha256) { throw new SequencerMediaCacheValidationError("SEQUENCER_CACHE_SOURCE_MISMATCH", "Movie proxy source bytes failed identity verification"); } + cancelled(signal); if (typeof document === "undefined" || typeof HTMLMediaElement === "undefined") { throw new SequencerMediaCacheValidationError("SEQUENCER_CODEC_UNSUPPORTED", "HTML media proxy generation is unavailable"); } @@ -87,7 +100,8 @@ export async function generateInitialSequencerMovieProxyFrame( try { video.src = url; video.load(); - await waitForLoadedFrame(video, 10_000); + await waitForLoadedFrame(video, 10_000, signal); + cancelled(signal); const canvas = document.createElement("canvas"); canvas.width = profile.width; canvas.height = profile.height; @@ -97,6 +111,7 @@ export async function generateInitialSequencerMovieProxyFrame( } context.clearRect(0, 0, profile.width, profile.height); context.drawImage(video, 0, 0, profile.width, profile.height); + cancelled(signal); const pixels = context.getImageData(0, 0, profile.width, profile.height).data; const data = pixels.buffer.slice(pixels.byteOffset, pixels.byteOffset + pixels.byteLength); return { diff --git a/web/app/src/storage/asset-catalog-indexeddb.ts b/web/app/src/storage/asset-catalog-indexeddb.ts new file mode 100644 index 00000000..b68c358a --- /dev/null +++ b/web/app/src/storage/asset-catalog-indexeddb.ts @@ -0,0 +1,236 @@ +import { + migrateAssetCatalogV1ToV2, + type AssetCatalogMigrationReportIR, +} from "../../../protocol/asset-catalog-migration"; +import { + parseAssetCatalogManifestV2, + type AssetCatalogManifestV2IR, +} from "../../../protocol/asset-catalog-v2"; +import type { ErrorCode } from "../../../protocol/error"; + +export const ASSET_CATALOG_INDEX_V1_ID = "asset-catalog:index:v1" as const; +export const ASSET_CATALOG_INDEX_V2_ID = "asset-catalog:index:v2" as const; +export const ASSET_CATALOG_MIGRATION_ID = "asset-catalog:migration:v1-to-v2" as const; + +export type AssetCatalogMigrationFault = "AFTER_TARGET_PUT" | "AFTER_SOURCE_DELETE"; + +export interface AssetCatalogIndexRowV1 { + id: typeof ASSET_CATALOG_INDEX_V1_ID; + value: unknown; +} + +export interface AssetCatalogIndexRowV2 { + id: typeof ASSET_CATALOG_INDEX_V2_ID; + value: AssetCatalogManifestV2IR; +} + +export interface AssetCatalogIndexedDBMigrationReceiptIR { + schemaVersion: 1; + id: typeof ASSET_CATALOG_MIGRATION_ID; + task: "M12-01G"; + status: "MIGRATED"; + sourceRevision: number; + targetRevision: number; + sourceManifestSha256: string; + targetManifestSha256: string; +} + +export interface AssetCatalogIndexedDBMigrationResultIR { + status: "MIGRATED" | "ALREADY_MIGRATED"; + manifest: AssetCatalogManifestV2IR; + receipt: AssetCatalogIndexedDBMigrationReceiptIR; + migrationReport: AssetCatalogMigrationReportIR | null; +} + +export interface AssetCatalogIndexedDBSnapshotIR { + schemaVersion: 1; + revision: number; + manifestSha256: string; + catalogOrder: Array<{ catalogId: string; path: string }>; + assetOrder: string[]; + assetIdentities: Array<{ + assetId: string; + assetLibraryIdentifier: string | null; + relativeAssetIdentifier: string; + }>; +} + +export class AssetCatalogIndexedDBMigrationError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string, options?: ErrorOptions) { + super(`${code}: ${message}`, options); + this.name = "AssetCatalogIndexedDBMigrationError"; + this.code = code; + } +} + +interface StoredRow { + id: string; + value?: unknown; + [key: string]: unknown; +} + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256(value: unknown): Promise { + const bytes = new TextEncoder().encode(stableJSON(value)); + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration transaction failed")); + transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration transaction aborted")); + }); +} + +function readRow(store: IDBObjectStore, id: string): Promise { + return new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result as StoredRow | undefined); + request.onerror = () => reject(request.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", `Catalog index lookup failed for ${id}`)); + }); +} + +function assertStores(database: IDBDatabase, settingStore: string, migrationStore: string): void { + if (!database.objectStoreNames.contains(settingStore) || !database.objectStoreNames.contains(migrationStore)) { + throw new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog migration stores are unavailable"); + } +} + +function parseReceipt(value: unknown): AssetCatalogIndexedDBMigrationReceiptIR { + const digest = /^[a-f0-9]{64}$/; + if (!record(value) || value.schemaVersion !== 1 || value.id !== ASSET_CATALOG_MIGRATION_ID || value.task !== "M12-01G" || value.status !== "MIGRATED" || + !Number.isSafeInteger(value.sourceRevision) || Number(value.sourceRevision) < 0 || + !Number.isSafeInteger(value.targetRevision) || Number(value.targetRevision) < 0 || + typeof value.sourceManifestSha256 !== "string" || !digest.test(value.sourceManifestSha256) || + typeof value.targetManifestSha256 !== "string" || !digest.test(value.targetManifestSha256)) { + throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Stored catalog migration receipt is invalid"); + } + return value as unknown as AssetCatalogIndexedDBMigrationReceiptIR; +} + +export async function readAssetCatalogIndexedDBIndex( + database: IDBDatabase, + settingStore = "setting", + migrationStore = "migration", +): Promise<{ source: StoredRow | undefined; target: StoredRow | undefined; receipt: StoredRow | undefined }> { + assertStores(database, settingStore, migrationStore); + const transaction = database.transaction([settingStore, migrationStore], "readonly"); + const sourcePromise = readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V1_ID); + const targetPromise = readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V2_ID); + const receiptPromise = readRow(transaction.objectStore(migrationStore), ASSET_CATALOG_MIGRATION_ID); + const result = await Promise.all([sourcePromise, targetPromise, receiptPromise]); + await transactionComplete(transaction, () => undefined); + return { source: result[0], target: result[1], receipt: result[2] }; +} + +export async function migrateAssetCatalogIndexedDB( + database: IDBDatabase, + options: { + settingStore?: string; + migrationStore?: string; + faultAt?: AssetCatalogMigrationFault; + } = {}, +): Promise { + const settingStore = options.settingStore ?? "setting"; + const migrationStore = options.migrationStore ?? "migration"; + assertStores(database, settingStore, migrationStore); + + const before = await readAssetCatalogIndexedDBIndex(database, settingStore, migrationStore); + if (!before.source) { + if (!before.target || !before.receipt) { + throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog index has neither a complete v1 source nor a complete v2 migration"); + } + const manifest = await parseAssetCatalogManifestV2(before.target.value); + const receipt = parseReceipt(before.receipt); + if (receipt.targetRevision !== manifest.revision || await sha256(manifest) !== receipt.targetManifestSha256) { + throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog migration receipt does not match the stored v2 index"); + } + return { status: "ALREADY_MIGRATED", manifest, receipt, migrationReport: null }; + } + + if (before.target || before.receipt?.id === ASSET_CATALOG_MIGRATION_ID) { + throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Catalog index contains a partial v1 to v2 migration"); + } + + const prepared = await migrateAssetCatalogV1ToV2(before.source.value); + const receipt: AssetCatalogIndexedDBMigrationReceiptIR = { + schemaVersion: 1, + id: ASSET_CATALOG_MIGRATION_ID, + task: "M12-01G", + status: "MIGRATED", + sourceRevision: prepared.report.sourceRevision, + targetRevision: prepared.report.targetRevision, + sourceManifestSha256: prepared.report.sourceManifestSha256, + targetManifestSha256: prepared.report.targetManifestSha256, + }; + + let migrationFailure: Error | undefined; + const transaction = database.transaction([settingStore, migrationStore], "readwrite"); + const completion = transactionComplete(transaction, () => migrationFailure); + const settings = transaction.objectStore(settingStore); + const migrations = transaction.objectStore(migrationStore); + const sourceRequest = settings.get(ASSET_CATALOG_INDEX_V1_ID); + sourceRequest.onerror = () => { + migrationFailure = sourceRequest.error ?? new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Catalog source recheck failed"); + }; + sourceRequest.onsuccess = () => { + const current = sourceRequest.result as StoredRow | undefined; + if (!current || stableJSON(current.value) !== stableJSON(before.source?.value)) { + migrationFailure = new AssetCatalogIndexedDBMigrationError("REVISION_CONFLICT", "Catalog v1 index changed while migration was prepared"); + transaction.abort(); + return; + } + settings.put({ id: ASSET_CATALOG_INDEX_V2_ID, value: prepared.manifest } satisfies AssetCatalogIndexRowV2); + if (options.faultAt === "AFTER_TARGET_PUT") { + migrationFailure = new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Injected catalog migration failure after target write"); + transaction.abort(); + return; + } + migrations.put(receipt); + settings.delete(ASSET_CATALOG_INDEX_V1_ID); + if (options.faultAt === "AFTER_SOURCE_DELETE") { + migrationFailure = new AssetCatalogIndexedDBMigrationError("STORAGE_TRANSACTION", "Injected catalog migration failure after source delete"); + transaction.abort(); + } + }; + await completion; + return { status: "MIGRATED", manifest: prepared.manifest, receipt, migrationReport: prepared.report }; +} + +export async function loadAssetCatalogIndexedDBSnapshot( + database: IDBDatabase, + options: { settingStore?: string; migrationStore?: string } = {}, +): Promise { + const loaded = await migrateAssetCatalogIndexedDB(database, options); + const manifestSha256 = await sha256(loaded.manifest); + if (manifestSha256 !== loaded.receipt.targetManifestSha256) { + throw new AssetCatalogIndexedDBMigrationError("ASSET_MANIFEST_INVALID", "Loaded catalog index does not match its migration receipt"); + } + return { + schemaVersion: 1, + revision: loaded.manifest.revision, + manifestSha256, + catalogOrder: loaded.manifest.catalogs.map(({ catalogId, path }) => ({ catalogId, path })), + assetOrder: loaded.manifest.assets.map(({ assetId }) => assetId), + assetIdentities: loaded.manifest.assets.map(({ assetId, assetLibraryIdentifier, relativeAssetIdentifier }) => ({ + assetId, + assetLibraryIdentifier, + relativeAssetIdentifier, + })), + }; +} diff --git a/web/app/src/storage/asset-preview-opfs-commit.ts b/web/app/src/storage/asset-preview-opfs-commit.ts new file mode 100644 index 00000000..bf67ea7a --- /dev/null +++ b/web/app/src/storage/asset-preview-opfs-commit.ts @@ -0,0 +1,262 @@ +import { + parseAssetCatalogManifestV2, + type AssetCatalogManifestV2IR, + type AssetCatalogV2PreviewIR, +} from "../../../protocol/asset-catalog-v2"; +import { planAssetPreviewDecode } from "../../../protocol/asset-preview-decode"; +import { + createAssetPreviewIdentity, + parseAssetPreviewIdentity, + type AssetPreviewIdentityIR, +} from "../../../protocol/asset-preview"; +import type { ErrorCode } from "../../../protocol/error"; +import { + ASSET_CATALOG_INDEX_V2_ID, + type AssetCatalogIndexRowV2, +} from "./asset-catalog-indexeddb"; +import { readContentAsset, writeContentAsset } from "./opfs-files"; + +export const ASSET_PREVIEW_CATALOG_HEAD_ID = "asset-catalog:preview-head:v1" as const; +export { createAssetPreviewIdentity, parseAssetCatalogManifestV2 }; + +export type AssetPreviewCatalogCommitFault = + | "BEFORE_OPFS_WRITE" + | "AFTER_OPFS_WRITE" + | "AFTER_CATALOG_PUT"; + +export interface AssetPreviewCatalogCommitReceiptIR { + schemaVersion: 1; + id: typeof ASSET_PREVIEW_CATALOG_HEAD_ID; + task: "M12-02D"; + status: "COMMITTED"; + projectId: string; + assetId: string; + baseRevision: number; + committedRevision: number; + baseManifestSha256: string; + committedManifestSha256: string; + previewIdentitySha256: string; + contentSha256: string; + contentByteLength: number; + opfsPath: string; +} + +export interface AssetPreviewCatalogCommitResultIR { + manifest: AssetCatalogManifestV2IR; + receipt: AssetPreviewCatalogCommitReceiptIR; + preview: AssetCatalogV2PreviewIR; + deduplicated: boolean; +} + +export class AssetPreviewCatalogCommitError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string, options?: ErrorOptions) { + super(`${code}: ${message}`, options); + this.name = "AssetPreviewCatalogCommitError"; + this.code = code; + } +} + +interface StoredRow { + id: string; + value?: unknown; + [key: string]: unknown; +} + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256(value: unknown): Promise { + const bytes = new TextEncoder().encode(stableJSON(value)); + const digest = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +async function sha256Bytes(value: ArrayBuffer): Promise { + const digest = await crypto.subtle.digest("SHA-256", value); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function readRow(store: IDBObjectStore, id: string): Promise { + return new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result as StoredRow | undefined); + request.onerror = () => reject(request.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", `Catalog row lookup failed for ${id}`)); + }); +} + +function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview catalog transaction failed")); + transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview catalog transaction aborted")); + }); +} + +async function readCatalog(database: IDBDatabase, settingStore: string): Promise<{ row: StoredRow; manifest: AssetCatalogManifestV2IR }> { + if (!database.objectStoreNames.contains(settingStore)) { + throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Catalog setting store is unavailable"); + } + const transaction = database.transaction(settingStore, "readonly"); + const row = await readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V2_ID); + await transactionComplete(transaction, () => undefined); + if (!row) throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Catalog v2 index is missing"); + return { row, manifest: await parseAssetCatalogManifestV2(row.value) }; +} + +function parseReceipt(value: unknown): AssetPreviewCatalogCommitReceiptIR { + const digest = /^[a-f0-9]{64}$/; + if (!record(value) || value.schemaVersion !== 1 || value.id !== ASSET_PREVIEW_CATALOG_HEAD_ID || + value.task !== "M12-02D" || value.status !== "COMMITTED" || typeof value.projectId !== "string" || + typeof value.assetId !== "string" || !Number.isSafeInteger(value.baseRevision) || Number(value.baseRevision) < 0 || + !Number.isSafeInteger(value.committedRevision) || Number(value.committedRevision) !== Number(value.baseRevision) + 1 || + typeof value.baseManifestSha256 !== "string" || !digest.test(value.baseManifestSha256) || + typeof value.committedManifestSha256 !== "string" || !digest.test(value.committedManifestSha256) || + typeof value.previewIdentitySha256 !== "string" || !digest.test(value.previewIdentitySha256) || + typeof value.contentSha256 !== "string" || !digest.test(value.contentSha256) || + !Number.isSafeInteger(value.contentByteLength) || Number(value.contentByteLength) <= 0 || + typeof value.opfsPath !== "string" || value.opfsPath.length === 0) { + throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog commit receipt is invalid"); + } + return value as unknown as AssetPreviewCatalogCommitReceiptIR; +} + +function previewFromIdentity(identity: AssetPreviewIdentityIR): AssetCatalogV2PreviewIR { + return { + sha256: identity.content.sha256, + mimeType: identity.content.mimeType, + width: identity.content.width, + height: identity.content.height, + byteLength: identity.content.byteLength, + }; +} + +export async function commitAssetPreviewToOPFS( + database: IDBDatabase, + projectId: string, + expectedRevision: number, + identityValue: unknown, + encodedBytes: ArrayBuffer, + options: { + settingStore?: string; + storage?: StorageManager; + faultAt?: AssetPreviewCatalogCommitFault; + } = {}, +): Promise { + const settingStore = options.settingStore ?? "setting"; + const identity = await parseAssetPreviewIdentity(identityValue); + await planAssetPreviewDecode(identity, encodedBytes); + const before = await readCatalog(database, settingStore); + if (before.manifest.revision !== expectedRevision) { + throw new AssetPreviewCatalogCommitError("REVISION_CONFLICT", "Catalog revision changed before preview storage"); + } + const assetIndex = before.manifest.assets.findIndex((asset) => asset.assetId === identity.assetId); + if (assetIndex === -1) { + throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview identity does not belong to a catalog asset"); + } + if (expectedRevision === Number.MAX_SAFE_INTEGER) { + throw new AssetPreviewCatalogCommitError("ASSET_BUDGET_EXCEEDED", "Catalog revision cannot advance safely"); + } + const preview = previewFromIdentity(identity); + const candidate = await parseAssetCatalogManifestV2({ + ...before.manifest, + revision: expectedRevision + 1, + assets: before.manifest.assets.map((asset, index) => index === assetIndex ? { ...asset, preview } : asset), + }); + const baseManifestSha256 = await sha256(before.manifest); + const committedManifestSha256 = await sha256(candidate); + + if (options.faultAt === "BEFORE_OPFS_WRITE") { + throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure before preview OPFS write"); + } + let written: Awaited>; + try { + written = await writeContentAsset(projectId, identity.content.sha256, encodedBytes, options.storage); + const persisted = await readContentAsset(projectId, identity.content.sha256, options.storage); + await planAssetPreviewDecode(identity, persisted); + } + catch (error) { + if (error instanceof AssetPreviewCatalogCommitError || (error instanceof Error && "code" in error)) throw error; + throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Preview OPFS write or readback verification failed", { cause: error }); + } + if (options.faultAt === "AFTER_OPFS_WRITE") { + throw new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure after preview OPFS write"); + } + + const receipt: AssetPreviewCatalogCommitReceiptIR = { + schemaVersion: 1, + id: ASSET_PREVIEW_CATALOG_HEAD_ID, + task: "M12-02D", + status: "COMMITTED", + projectId, + assetId: identity.assetId, + baseRevision: expectedRevision, + committedRevision: candidate.revision, + baseManifestSha256, + committedManifestSha256, + previewIdentitySha256: identity.identitySha256, + contentSha256: identity.content.sha256, + contentByteLength: identity.content.byteLength, + opfsPath: written.path, + }; + + let commitFailure: Error | undefined; + const transaction = database.transaction(settingStore, "readwrite"); + const completion = transactionComplete(transaction, () => commitFailure); + const store = transaction.objectStore(settingStore); + const request = store.get(ASSET_CATALOG_INDEX_V2_ID); + request.onerror = () => { + commitFailure = request.error ?? new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Catalog source recheck failed"); + }; + request.onsuccess = () => { + const current = request.result as StoredRow | undefined; + if (!current || stableJSON(current.value) !== stableJSON(before.row.value)) { + commitFailure = new AssetPreviewCatalogCommitError("REVISION_CONFLICT", "Catalog index changed while preview storage was prepared"); + transaction.abort(); + return; + } + store.put({ id: ASSET_CATALOG_INDEX_V2_ID, value: candidate } satisfies AssetCatalogIndexRowV2); + store.put(receipt); + if (options.faultAt === "AFTER_CATALOG_PUT") { + commitFailure = new AssetPreviewCatalogCommitError("STORAGE_TRANSACTION", "Injected failure after preview catalog writes"); + transaction.abort(); + } + }; + await completion; + return { manifest: candidate, receipt, preview, deduplicated: written.deduplicated }; +} + +export async function loadCommittedAssetPreviewCatalog( + database: IDBDatabase, + options: { settingStore?: string; storage?: StorageManager } = {}, +): Promise { + const settingStore = options.settingStore ?? "setting"; + const catalog = await readCatalog(database, settingStore); + const transaction = database.transaction(settingStore, "readonly"); + const row = await readRow(transaction.objectStore(settingStore), ASSET_PREVIEW_CATALOG_HEAD_ID); + await transactionComplete(transaction, () => undefined); + if (!row) throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog commit receipt is missing"); + const receipt = parseReceipt(row); + if (catalog.manifest.revision !== receipt.committedRevision || await sha256(catalog.manifest) !== receipt.committedManifestSha256) { + throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Preview catalog receipt does not match the current catalog"); + } + const asset = catalog.manifest.assets.find((candidate) => candidate.assetId === receipt.assetId); + if (!asset?.preview || asset.preview.sha256 !== receipt.contentSha256 || asset.preview.byteLength !== receipt.contentByteLength) { + throw new AssetPreviewCatalogCommitError("ASSET_MANIFEST_INVALID", "Committed catalog preview does not match its receipt"); + } + const persisted = await readContentAsset(receipt.projectId, receipt.contentSha256, options.storage); + if (persisted.byteLength !== receipt.contentByteLength || await sha256Bytes(persisted) !== receipt.contentSha256) { + throw new AssetPreviewCatalogCommitError("ASSET_SOURCE_HASH_MISMATCH", "Committed preview payload identity changed"); + } + return { manifest: catalog.manifest, receipt, preview: asset.preview, deduplicated: false }; +} diff --git a/web/app/src/storage/asset-preview-quarantine.ts b/web/app/src/storage/asset-preview-quarantine.ts new file mode 100644 index 00000000..246d07d8 --- /dev/null +++ b/web/app/src/storage/asset-preview-quarantine.ts @@ -0,0 +1,226 @@ +import { + parseAssetCatalogManifestV2, + type AssetCatalogV2EntryIR, + type AssetCatalogV2PreviewIR, +} from "../../../protocol/asset-catalog-v2"; +import type { ErrorCode } from "../../../protocol/error"; +import { ASSET_CATALOG_INDEX_V2_ID } from "./asset-catalog-indexeddb"; +import { projectLayout } from "./opfs-files"; + +export const ASSET_PREVIEW_QUARANTINE_SCHEMA = 1 as const; +export const ASSET_PREVIEW_QUARANTINE_PREFIX = "asset-preview:quarantine:v1:" as const; + +export interface AssetPreviewQuarantineReceiptIR { + schemaVersion: typeof ASSET_PREVIEW_QUARANTINE_SCHEMA; + id: string; + task: "M12-02F"; + status: "QUARANTINED"; + projectId: string; + assetId: string; + catalogRevision: number; + expected: AssetCatalogV2PreviewIR; + actualByteLength: number | null; + actualSha256: string | null; + code: "ASSET_SOURCE_HASH_MISMATCH"; + quarantinePath: string | null; +} + +export type AssetPreviewInspectionIR = + | { + status: "READY"; + asset: AssetCatalogV2EntryIR; + preview: AssetCatalogV2PreviewIR; + data: ArrayBuffer; + receipt: null; + } + | { + status: "QUARANTINED"; + asset: AssetCatalogV2EntryIR; + preview: null; + data: null; + receipt: AssetPreviewQuarantineReceiptIR; + }; + +export class AssetPreviewQuarantineError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string, options?: ErrorOptions) { + super(`${code}: ${message}`, options); + this.name = "AssetPreviewQuarantineError"; + this.code = code; + } +} + +interface StoredRow { + id: string; + value?: unknown; + [key: string]: unknown; +} + +type OpfsStorage = StorageManager & { getDirectory?: () => Promise }; + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256Bytes(value: ArrayBuffer): Promise { + const digest = await crypto.subtle.digest("SHA-256", value); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "Preview quarantine transaction failed")); + transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "Preview quarantine transaction aborted")); + }); +} + +function readRow(store: IDBObjectStore, id: string): Promise { + return new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result as StoredRow | undefined); + request.onerror = () => reject(request.error ?? new AssetPreviewQuarantineError("STORAGE_TRANSACTION", `Preview quarantine lookup failed for ${id}`)); + }); +} + +function receiptId(assetId: string): string { + return `${ASSET_PREVIEW_QUARANTINE_PREFIX}${assetId}`; +} + +function parseReceipt(value: unknown, asset: AssetCatalogV2EntryIR, projectId: string, revision: number): AssetPreviewQuarantineReceiptIR | null { + const digest = /^[a-f0-9]{64}$/; + if (!record(value) || value.schemaVersion !== ASSET_PREVIEW_QUARANTINE_SCHEMA || value.id !== receiptId(asset.assetId) || + value.task !== "M12-02F" || value.status !== "QUARANTINED" || value.projectId !== projectId || + value.assetId !== asset.assetId || value.catalogRevision !== revision || value.code !== "ASSET_SOURCE_HASH_MISMATCH" || + !record(value.expected) || !asset.preview || stableJSON(value.expected) !== stableJSON(asset.preview) || + (value.actualByteLength !== null && (!Number.isSafeInteger(value.actualByteLength) || Number(value.actualByteLength) < 0)) || + (value.actualSha256 !== null && (typeof value.actualSha256 !== "string" || !digest.test(value.actualSha256))) || + (value.quarantinePath !== null && (typeof value.quarantinePath !== "string" || value.quarantinePath.length === 0))) { + return null; + } + return value as unknown as AssetPreviewQuarantineReceiptIR; +} + +async function directory(root: FileSystemDirectoryHandle, path: string, create = false): Promise { + let current = root; + for (const segment of path.split("/")) current = await current.getDirectoryHandle(segment, { create }); + return current; +} + +async function readPayload(projectId: string, sha256: string, storage?: StorageManager): Promise { + const manager = (storage ?? (typeof navigator === "undefined" ? undefined : navigator.storage)) as OpfsStorage | undefined; + if (!manager?.getDirectory) throw new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "OPFS is unavailable for preview inspection"); + try { + const layout = projectLayout(projectId); + const source = await directory(await manager.getDirectory(), `${layout.assetsPath}/sha256/${sha256.slice(0, 2)}`); + return (await (await source.getFileHandle(sha256)).getFile()).arrayBuffer(); + } + catch (error) { + if (error instanceof DOMException && error.name === "NotFoundError") return null; + throw error; + } +} + +async function quarantinePayload( + projectId: string, + expectedSha256: string, + data: ArrayBuffer | null, + actualSha256: string | null, + storage?: StorageManager, +): Promise { + if (!data || !actualSha256) return null; + const manager = (storage ?? (typeof navigator === "undefined" ? undefined : navigator.storage)) as OpfsStorage | undefined; + if (!manager?.getDirectory) throw new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "OPFS is unavailable for preview quarantine"); + const layout = projectLayout(projectId); + const root = await manager.getDirectory(); + const source = await directory(root, `${layout.assetsPath}/sha256/${expectedSha256.slice(0, 2)}`); + const quarantine = await directory(root, `${layout.assetsPath}/quarantine`, true); + const name = `${expectedSha256}.${actualSha256.slice(0, 12)}.corrupt`; + const handle = await quarantine.getFileHandle(name, { create: true }); + const writable = await handle.createWritable(); + await writable.write(data); + await writable.close(); + const copied = await (await handle.getFile()).arrayBuffer(); + if (copied.byteLength !== data.byteLength || await sha256Bytes(copied) !== actualSha256) { + await quarantine.removeEntry(name).catch(() => undefined); + throw new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "Preview quarantine copy verification failed"); + } + await source.removeEntry(expectedSha256); + return `${layout.assetsPath}/quarantine/${name}`; +} + +export async function inspectAssetPreviewWithQuarantine( + database: IDBDatabase, + projectId: string, + assetId: string, + options: { settingStore?: string; storage?: StorageManager } = {}, +): Promise { + const settingStore = options.settingStore ?? "setting"; + if (!database.objectStoreNames.contains(settingStore)) { + throw new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "Catalog setting store is unavailable"); + } + const readTransaction = database.transaction(settingStore, "readonly"); + const store = readTransaction.objectStore(settingStore); + const [catalogRow, quarantineRow] = await Promise.all([ + readRow(store, ASSET_CATALOG_INDEX_V2_ID), + readRow(store, receiptId(assetId)), + ]); + await transactionComplete(readTransaction, () => undefined); + if (!catalogRow) throw new AssetPreviewQuarantineError("ASSET_MANIFEST_INVALID", "Catalog v2 index is missing"); + const manifest = await parseAssetCatalogManifestV2(catalogRow.value); + const asset = manifest.assets.find((candidate) => candidate.assetId === assetId); + if (!asset) throw new AssetPreviewQuarantineError("ASSET_MANIFEST_INVALID", "Preview asset is not in the catalog"); + if (!asset.preview) throw new AssetPreviewQuarantineError("ASSET_MANIFEST_INVALID", "Catalog asset has no preview reference"); + const existingReceipt = parseReceipt(quarantineRow, asset, projectId, manifest.revision); + if (existingReceipt) return { status: "QUARANTINED", asset, preview: null, data: null, receipt: existingReceipt }; + + const data = await readPayload(projectId, asset.preview.sha256, options.storage); + const actualSha256 = data ? await sha256Bytes(data) : null; + if (data && data.byteLength === asset.preview.byteLength && actualSha256 === asset.preview.sha256) { + return { status: "READY", asset, preview: asset.preview, data, receipt: null }; + } + const quarantinePath = await quarantinePayload(projectId, asset.preview.sha256, data, actualSha256, options.storage); + const receipt: AssetPreviewQuarantineReceiptIR = { + schemaVersion: ASSET_PREVIEW_QUARANTINE_SCHEMA, + id: receiptId(asset.assetId), + task: "M12-02F", + status: "QUARANTINED", + projectId, + assetId: asset.assetId, + catalogRevision: manifest.revision, + expected: asset.preview, + actualByteLength: data?.byteLength ?? null, + actualSha256, + code: "ASSET_SOURCE_HASH_MISMATCH", + quarantinePath, + }; + + let failure: Error | undefined; + const transaction = database.transaction(settingStore, "readwrite"); + const completion = transactionComplete(transaction, () => failure); + const writeStore = transaction.objectStore(settingStore); + const request = writeStore.get(ASSET_CATALOG_INDEX_V2_ID); + request.onerror = () => { + failure = request.error ?? new AssetPreviewQuarantineError("STORAGE_TRANSACTION", "Catalog recheck failed during preview quarantine"); + }; + request.onsuccess = () => { + const current = request.result as StoredRow | undefined; + if (!current || stableJSON(current.value) !== stableJSON(catalogRow.value)) { + failure = new AssetPreviewQuarantineError("REVISION_CONFLICT", "Catalog changed while corrupt preview was quarantined"); + transaction.abort(); + return; + } + writeStore.put(receipt); + }; + await completion; + return { status: "QUARANTINED", asset, preview: null, data: null, receipt }; +} diff --git a/web/app/src/storage/asset-preview-reference-gc.ts b/web/app/src/storage/asset-preview-reference-gc.ts new file mode 100644 index 00000000..49c303a2 --- /dev/null +++ b/web/app/src/storage/asset-preview-reference-gc.ts @@ -0,0 +1,184 @@ +import { + parseAssetCatalogManifestV2, + type AssetCatalogManifestV2IR, + type AssetCatalogV2EntryIR, + type AssetCatalogV2PreviewIR, +} from "../../../protocol/asset-catalog-v2"; +import type { ErrorCode } from "../../../protocol/error"; +import { ASSET_CATALOG_INDEX_V2_ID, type AssetCatalogIndexRowV2 } from "./asset-catalog-indexeddb"; +import { ASSET_PREVIEW_CATALOG_HEAD_ID } from "./asset-preview-opfs-commit"; +import { deleteContentAsset } from "./opfs-files"; + +export const ASSET_PREVIEW_REFERENCE_GC_PREFIX = "asset-preview:reference-gc:v1:" as const; + +export interface AssetPreviewReferenceGCReceiptIR { + schemaVersion: 1; + id: string; + task: "M12-02G"; + status: "RETAINED" | "COLLECTED"; + projectId: string; + assetId: string; + baseRevision: number; + committedRevision: number; + baseManifestSha256: string; + committedManifestSha256: string; + removedPreview: AssetCatalogV2PreviewIR; + remainingProjectReferences: number; + payloadReclaimed: boolean; +} + +export interface AssetPreviewReferenceGCResultIR { + manifest: AssetCatalogManifestV2IR; + asset: AssetCatalogV2EntryIR; + receipt: AssetPreviewReferenceGCReceiptIR; +} + +export class AssetPreviewReferenceGCError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string, options?: ErrorOptions) { + super(`${code}: ${message}`, options); + this.name = "AssetPreviewReferenceGCError"; + this.code = code; + } +} + +interface StoredRow { + id: string; + value?: unknown; + [key: string]: unknown; +} + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256(value: unknown): Promise { + const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(stableJSON(value))); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function receiptId(assetId: string): string { + return `${ASSET_PREVIEW_REFERENCE_GC_PREFIX}${assetId}`; +} + +function readRow(store: IDBObjectStore, id: string): Promise { + return new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result as StoredRow | undefined); + request.onerror = () => reject(request.error ?? new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", `Preview reference lookup failed for ${id}`)); + }); +} + +function transactionComplete(transaction: IDBTransaction, failure: () => Error | undefined): Promise { + return new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(failure() ?? transaction.error ?? new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", "Preview reference transaction failed")); + transaction.onabort = () => reject(failure() ?? transaction.error ?? new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", "Preview reference transaction aborted")); + }); +} + +async function loadCatalog(database: IDBDatabase, settingStore: string): Promise<{ row: StoredRow; manifest: AssetCatalogManifestV2IR }> { + if (!database.objectStoreNames.contains(settingStore)) { + throw new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", "Catalog setting store is unavailable"); + } + const transaction = database.transaction(settingStore, "readonly"); + const row = await readRow(transaction.objectStore(settingStore), ASSET_CATALOG_INDEX_V2_ID); + await transactionComplete(transaction, () => undefined); + if (!row) throw new AssetPreviewReferenceGCError("ASSET_MANIFEST_INVALID", "Catalog v2 index is missing"); + return { row, manifest: await parseAssetCatalogManifestV2(row.value) }; +} + +export async function removeAssetPreviewReference( + database: IDBDatabase, + projectId: string, + expectedRevision: number, + assetId: string, + options: { settingStore?: string; storage?: StorageManager } = {}, +): Promise { + const settingStore = options.settingStore ?? "setting"; + const before = await loadCatalog(database, settingStore); + if (before.manifest.revision !== expectedRevision) { + throw new AssetPreviewReferenceGCError("REVISION_CONFLICT", "Catalog revision changed before preview reference removal"); + } + const assetIndex = before.manifest.assets.findIndex((asset) => asset.assetId === assetId); + const sourceAsset = before.manifest.assets[assetIndex]; + if (assetIndex === -1 || !sourceAsset?.preview) { + throw new AssetPreviewReferenceGCError("ASSET_MANIFEST_INVALID", "Catalog asset has no removable preview reference"); + } + if (expectedRevision === Number.MAX_SAFE_INTEGER) { + throw new AssetPreviewReferenceGCError("ASSET_BUDGET_EXCEEDED", "Catalog revision cannot advance safely"); + } + const removedPreview = sourceAsset.preview; + const candidate = await parseAssetCatalogManifestV2({ + ...before.manifest, + revision: expectedRevision + 1, + assets: before.manifest.assets.map((asset, index) => index === assetIndex ? { ...asset, preview: null } : asset), + }); + const remainingProjectReferences = candidate.assets.filter((asset) => asset.preview?.sha256 === removedPreview.sha256).length; + const baseManifestSha256 = await sha256(before.manifest); + const committedManifestSha256 = await sha256(candidate); + const pendingReceipt: AssetPreviewReferenceGCReceiptIR = { + schemaVersion: 1, + id: receiptId(assetId), + task: "M12-02G", + status: "RETAINED", + projectId, + assetId, + baseRevision: expectedRevision, + committedRevision: candidate.revision, + baseManifestSha256, + committedManifestSha256, + removedPreview, + remainingProjectReferences, + payloadReclaimed: false, + }; + + let commitFailure: Error | undefined; + const transaction = database.transaction(settingStore, "readwrite"); + const completion = transactionComplete(transaction, () => commitFailure); + const store = transaction.objectStore(settingStore); + const request = store.get(ASSET_CATALOG_INDEX_V2_ID); + request.onerror = () => { + commitFailure = request.error ?? new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", "Catalog recheck failed during preview reference removal"); + }; + request.onsuccess = () => { + const current = request.result as StoredRow | undefined; + if (!current || stableJSON(current.value) !== stableJSON(before.row.value)) { + commitFailure = new AssetPreviewReferenceGCError("REVISION_CONFLICT", "Catalog changed while preview reference removal was prepared"); + transaction.abort(); + return; + } + store.put({ id: ASSET_CATALOG_INDEX_V2_ID, value: candidate } satisfies AssetCatalogIndexRowV2); + store.delete(ASSET_PREVIEW_CATALOG_HEAD_ID); + store.put(pendingReceipt); + }; + await completion; + + const payloadReclaimed = remainingProjectReferences === 0; + if (payloadReclaimed) { + try { + await deleteContentAsset(projectId, removedPreview.sha256, options.storage); + } + catch (error) { + throw new AssetPreviewReferenceGCError("STORAGE_TRANSACTION", "Preview reference was removed but payload reclamation failed", { cause: error }); + } + } + const receipt: AssetPreviewReferenceGCReceiptIR = { + ...pendingReceipt, + status: payloadReclaimed ? "COLLECTED" : "RETAINED", + payloadReclaimed, + }; + const receiptTransaction = database.transaction(settingStore, "readwrite"); + receiptTransaction.objectStore(settingStore).put(receipt); + await transactionComplete(receiptTransaction, () => undefined); + return { manifest: candidate, asset: candidate.assets[assetIndex], receipt }; +} diff --git a/web/app/src/testing/render-compositor-media-recovery.ts b/web/app/src/testing/render-compositor-media-recovery.ts new file mode 100644 index 00000000..76321ce8 --- /dev/null +++ b/web/app/src/testing/render-compositor-media-recovery.ts @@ -0,0 +1,351 @@ +import { + parseRenderCompositorMediaRecoverySuite, + type RenderCompositorMediaRecoveryEvidenceIR, +} from "../../../protocol/render-compositor-media-recovery"; +import type { SceneSnapshotIR } from "../../../protocol/scene-ir"; +import { createGPUTextureAsset } from "../../../protocol/render-assets"; +import { WebEngineClient } from "../engine-client/WebEngineClient"; +import { ViewportRenderer } from "../three-adapter/viewport"; +import { GPUTextureStore } from "../three-adapter/texture-assets"; +import { + CompositorFrameCache, + CompositorValidationError, + executeCompositorGraph, + executeCompositorGraphCached, + type CompositorGraphIR, + type CompositorImageBuffer, +} from "../compositor/CompositorExecutor"; +import { createSequencerCodecProbeRequest, probeSequencerCodec } from "../sequencer/SequencerCodecProbe"; +import { generateInitialSequencerMovieProxyFrame, SequencerMediaProxyCache } from "../sequencer/SequencerMediaProxyCache"; + +export interface RenderCompositorMediaRecoveryInput { + renderBlend: ArrayBuffer; + compositorBlend: ArrayBuffer; + movie: { + mimeType: string; + data: ArrayBuffer; + }; + texturePng: ArrayBuffer; +} + +function stable(value: unknown): string { + if (value === null || typeof value !== "object") return JSON.stringify(value); + if (Array.isArray(value)) return `[${value.map(stable).join(",")}]`; + const source = value as Record; + return `{${Object.keys(source).sort().map((key) => `${JSON.stringify(key)}:${stable(source[key])}`).join(",")}}`; +} + +async function sha256(value: string | ArrayBuffer): Promise { + const bytes = typeof value === "string" ? new TextEncoder().encode(value) : new Uint8Array(value); + const result = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(result), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function code(error: unknown): string { + if (typeof error === "object" && error !== null && "code" in error && typeof error.code === "string") return error.code; + if (error instanceof Error) return error.message.split(":", 1)[0]; + return String(error); +} + +function requireInput(value: ArrayBuffer, label: string): ArrayBuffer { + if (!(value instanceof ArrayBuffer) || value.byteLength === 0) throw new Error(`M11_RECOVERY_INPUT_INVALID: ${label}`); + return value; +} + +function renderIdentity(snapshot: SceneSnapshotIR): Promise { + return sha256(stable({ + revision: snapshot.revision, + sceneId: snapshot.sceneId, + activeObjectId: snapshot.activeObjectId, + nodes: snapshot.nodes, + meshes: snapshot.meshes, + materials: snapshot.materials, + cameras: snapshot.cameras, + lights: snapshot.lights, + worlds: snapshot.worlds, + scenes: snapshot.scenes, + })); +} + +async function renderSnapshot( + snapshot: SceneSnapshotIR, + geometryBuffers: Parameters[1], + nonMeshGeometryBuffers: Parameters[2], +): Promise<{ sha256: string; byteLength: number; visibleUnits: number }> { + const canvas = document.createElement("canvas"); + canvas.style.width = "256px"; + canvas.style.height = "256px"; + canvas.style.position = "fixed"; + canvas.style.left = "-10000px"; + document.body.append(canvas); + const renderer = new ViewportRenderer(canvas); + try { + const renderable = structuredClone(snapshot); + renderable.activeObjectId = null; + renderer.setSnapshot(renderable, geometryBuffers ?? [], nonMeshGeometryBuffers ?? []); + await new Promise((resolve) => requestAnimationFrame(() => requestAnimationFrame(resolve))); + const gl = renderer.renderer.getContext(); + const width = gl.drawingBufferWidth; + const height = gl.drawingBufferHeight; + const pixels = new Uint8Array(width * height * 4); + gl.readPixels(0, 0, width, height, gl.RGBA, gl.UNSIGNED_BYTE, pixels); + let visibleUnits = 0; + for (let offset = 0; offset < pixels.length; offset += 4) { + if (pixels[offset] + pixels[offset + 1] + pixels[offset + 2] > 12) visibleUnits++; + } + return { sha256: await sha256(pixels.buffer), byteLength: pixels.byteLength, visibleUnits }; + } + finally { + renderer.dispose(); + canvas.remove(); + } +} + +async function runRenderRecovery(input: RenderCompositorMediaRecoveryInput): Promise { + const blend = requireInput(input.renderBlend, "renderBlend"); + const texturePng = requireInput(input.texturePng, "texturePng"); + const sourceSha256 = await sha256(blend); + const client = new WebEngineClient({ timeoutMs: 30_000 }); + const textureStore = new GPUTextureStore("THREE_WEBGL2"); + let released = { releasedTextures: 0, releasedPayloadBytes: 0, releasedGPUBytes: 0, resourcesAfter: 0 as const }; + try { + const first = await client.openBlend(blend.slice(0)); + const identityBefore = await renderIdentity(first.snapshot); + const outputBefore = await renderSnapshot(first.snapshot, first.geometryBuffers, first.nonMeshGeometryBuffers); + + const cancellation = new AbortController(); + cancellation.abort(); + let cancellationCode = ""; + try { await client.openBlend(blend.slice(0), undefined, cancellation.signal); } + catch (error) { cancellationCode = code(error); } + const afterCancellation = await client.snapshot(); + if (await renderIdentity(afterCancellation.snapshot) !== identityBefore) { + throw new Error("M11_RENDER_CANCELLATION_MUTATED_STATE"); + } + + await client.restart(); + const second = await client.openBlend(blend.slice(0)); + const identityAfter = await renderIdentity(second.snapshot); + const outputAfter = await renderSnapshot(second.snapshot, second.geometryBuffers, second.nonMeshGeometryBuffers); + + const textureAsset = await createGPUTextureAsset({ + assetId: "asset:m11-recovery-texture", + imageId: "image:m11-recovery-texture", + mimeType: "image/png", + width: 8, + height: 8, + usage: "BASE_COLOR", + colorSpace: "SRGB", + }, texturePng.slice(0)); + const accepted = await textureStore.upload([textureAsset]); + if (accepted.loaded !== 1) throw new Error("M11_RENDER_TEXTURE_UPLOAD_FAILED"); + const retainedBeforeBudget = textureStore.stats(); + const overflow = Array.from({ length: 257 }, (_, index) => ({ + ...textureAsset, + assetId: `asset:m11-recovery-overflow:${index}`, + imageId: `image:m11-recovery-overflow:${index}`, + data: textureAsset.data.slice(0), + })); + const blocked = await textureStore.upload(overflow); + const retainedAfterBudget = textureStore.stats(); + if (stable(retainedBeforeBudget) !== stable(retainedAfterBudget)) { + throw new Error("M11_RENDER_BUDGET_MUTATED_TEXTURES"); + } + released = textureStore.dispose(); + + return { + schemaVersion: 1, + domain: "RENDER", + source: { byteLength: blend.byteLength, sha256: sourceSha256 }, + cancellation: { status: "CANCELLED", code: cancellationCode as "OPEN_CANCELLED", publishedResults: 0, temporaryResourcesAfter: 0 }, + restart: { + status: "RECOVERED", + generationBefore: 1, + generationAfter: 2, + identityBefore, + identityAfter, + outputSha256Before: outputBefore.sha256, + outputSha256After: outputAfter.sha256, + }, + budget: { + status: "BLOCKED", + code: blocked.errorCodes[0] as "GPU_TEXTURE_BUDGET_EXCEEDED", + retainedIdentityHash: identityAfter, + temporaryResourcesAfter: 0, + }, + release: { + status: "RELEASED", + releasedBytes: released.releasedPayloadBytes, + releasedResources: released.releasedTextures, + resourcesAfter: released.resourcesAfter, + }, + recovery: { + status: "RECOVERED", + identityHash: identityAfter, + outputSha256: outputAfter.sha256, + outputBytes: outputAfter.byteLength, + visibleUnits: outputAfter.visibleUnits, + }, + }; + } + finally { + if (released.releasedTextures === 0) textureStore.dispose(); + client.terminate(); + } +} + +function compositorGraph(snapshot: SceneSnapshotIR): CompositorGraphIR { + const graph = snapshot.scenes.find((scene) => scene.name === "M11 Chain")?.compositorGraph; + if (!graph) throw new Error("M11_COMPOSITOR_GRAPH_MISSING"); + return graph; +} + +async function compositorOutput(image: CompositorImageBuffer): Promise<{ sha256: string; byteLength: number; visibleUnits: number }> { + const bytes = image.data.buffer.slice(image.data.byteOffset, image.data.byteOffset + image.data.byteLength) as ArrayBuffer; + let visibleUnits = 0; + for (let offset = 0; offset < image.data.length; offset += 4) { + if (Math.abs(image.data[offset]) + Math.abs(image.data[offset + 1]) + Math.abs(image.data[offset + 2]) > 0) visibleUnits++; + } + return { sha256: await sha256(bytes), byteLength: bytes.byteLength, visibleUnits }; +} + +async function runCompositorRecovery(input: RenderCompositorMediaRecoveryInput): Promise { + const blend = requireInput(input.compositorBlend, "compositorBlend"); + const sourceSha256 = await sha256(blend); + const client = new WebEngineClient({ timeoutMs: 30_000 }); + const firstCache = new CompositorFrameCache(1024); + const secondCache = new CompositorFrameCache(1024); + try { + const first = await client.openBlend(blend.slice(0)); + const firstGraph = compositorGraph(first.snapshot); + const identityBefore = await sha256(stable(firstGraph)); + const firstResult = await executeCompositorGraphCached(firstGraph, new Map(), firstCache, { frame: 1, width: 2, height: 2 }); + const outputBefore = await compositorOutput(firstResult.composite); + + let cancellationCode = ""; + try { executeCompositorGraph(firstGraph, new Map(), { width: 2, height: 2, cancelled: () => true }); } + catch (error) { cancellationCode = error instanceof CompositorValidationError ? error.code : code(error); } + + const cacheBytesBeforeBudget = firstCache.byteLength; + let budgetCode = ""; + try { await executeCompositorGraphCached(firstGraph, new Map(), firstCache, { frame: 2, width: 8_193, height: 1 }); } + catch (error) { budgetCode = error instanceof CompositorValidationError ? error.code : code(error); } + if (firstCache.byteLength !== cacheBytesBeforeBudget) throw new Error("M11_COMPOSITOR_BUDGET_MUTATED_CACHE"); + + await client.restart(); + const second = await client.openBlend(blend.slice(0)); + const secondGraph = compositorGraph(second.snapshot); + const identityAfter = await sha256(stable(secondGraph)); + const secondResult = await executeCompositorGraphCached(secondGraph, new Map(), secondCache, { frame: 1, width: 2, height: 2 }); + const outputAfter = await compositorOutput(secondResult.composite); + + const releasedResources = firstCache.size; + const releasedBytes = firstCache.clear(); + secondCache.clear(); + return { + schemaVersion: 1, + domain: "COMPOSITOR", + source: { byteLength: blend.byteLength, sha256: sourceSha256 }, + cancellation: { status: "CANCELLED", code: cancellationCode as "COMPOSITOR_CANCELLED", publishedResults: 0, temporaryResourcesAfter: 0 }, + restart: { + status: "RECOVERED", + generationBefore: 1, + generationAfter: 2, + identityBefore, + identityAfter, + outputSha256Before: outputBefore.sha256, + outputSha256After: outputAfter.sha256, + }, + budget: { status: "BLOCKED", code: budgetCode as "COMPOSITOR_BUDGET_EXCEEDED", retainedIdentityHash: identityAfter, temporaryResourcesAfter: 0 }, + release: { status: "RELEASED", releasedBytes, releasedResources, resourcesAfter: 0 }, + recovery: { + status: "RECOVERED", + identityHash: identityAfter, + outputSha256: outputAfter.sha256, + outputBytes: outputAfter.byteLength, + visibleUnits: outputAfter.visibleUnits, + }, + }; + } + finally { + firstCache.clear(); + secondCache.clear(); + client.terminate(); + } +} + +async function runMediaRecovery(input: RenderCompositorMediaRecoveryInput): Promise { + const movie = requireInput(input.movie.data, "movie.data"); + const sourceSha256 = await sha256(movie); + const request = createSequencerCodecProbeRequest("MOVIE", input.movie.mimeType, movie.byteLength, sourceSha256); + const capability = await probeSequencerCodec(request, movie.slice(0)); + const profile = { kind: "MOVIE_RGBA8_FRAME" as const, width: 8, height: 8, colorSpace: "SRGB8" as const, alphaMode: "STRAIGHT" as const }; + const first = await generateInitialSequencerMovieProxyFrame(request, capability, profile, movie.slice(0)); + const identityBefore = first.manifest.identitySha256; + const outputBefore = await sha256(first.data); + + const cancellation = new AbortController(); + cancellation.abort(); + let cancellationCode = ""; + try { await generateInitialSequencerMovieProxyFrame(request, capability, profile, movie.slice(0), cancellation.signal); } + catch (error) { cancellationCode = code(error); } + + const firstCache = new SequencerMediaProxyCache(first.data.byteLength); + await firstCache.put(first.manifest, first.data.slice(0), request, capability); + const blockedCache = new SequencerMediaProxyCache(first.data.byteLength - 1); + let budgetCode = ""; + try { await blockedCache.put(first.manifest, first.data.slice(0), request, capability); } + catch (error) { budgetCode = code(error); } + if (blockedCache.stats().bytes !== 0) throw new Error("M11_MEDIA_BUDGET_MUTATED_CACHE"); + + const releasedResources = firstCache.stats().entries; + const releasedBytes = firstCache.clear(); + const secondCapability = await probeSequencerCodec(request, movie.slice(0)); + const second = await generateInitialSequencerMovieProxyFrame(request, secondCapability, profile, movie.slice(0)); + const secondCache = new SequencerMediaProxyCache(second.data.byteLength); + try { + await secondCache.put(second.manifest, second.data.slice(0), request, secondCapability); + const reopened = await secondCache.get(request, secondCapability, profile, 0); + if (!reopened) throw new Error("M11_MEDIA_RESTART_CACHE_MISS"); + const identityAfter = reopened.manifest.identitySha256; + const outputAfter = await sha256(reopened.data); + let visibleUnits = 0; + const pixels = new Uint8Array(reopened.data); + for (let offset = 3; offset < pixels.length; offset += 4) if (pixels[offset] > 0) visibleUnits++; + secondCache.clear(); + return { + schemaVersion: 1, + domain: "MEDIA", + source: { byteLength: movie.byteLength, sha256: sourceSha256 }, + cancellation: { status: "CANCELLED", code: cancellationCode as "SEQUENCER_CANCELLED", publishedResults: 0, temporaryResourcesAfter: 0 }, + restart: { + status: "RECOVERED", + generationBefore: 1, + generationAfter: 2, + identityBefore, + identityAfter, + outputSha256Before: outputBefore, + outputSha256After: outputAfter, + }, + budget: { status: "BLOCKED", code: budgetCode as "SEQUENCER_BUDGET_EXCEEDED", retainedIdentityHash: identityAfter, temporaryResourcesAfter: 0 }, + release: { status: "RELEASED", releasedBytes, releasedResources, resourcesAfter: 0 }, + recovery: { status: "RECOVERED", identityHash: identityAfter, outputSha256: outputAfter, outputBytes: reopened.data.byteLength, visibleUnits }, + }; + } + finally { + firstCache.clear(); + blockedCache.clear(); + secondCache.clear(); + } +} + +export async function runRenderCompositorMediaRecoverySuite( + input: RenderCompositorMediaRecoveryInput, +): Promise { + const reports = [ + await runRenderRecovery(input), + await runCompositorRecovery(input), + await runMediaRecovery(input), + ]; + return parseRenderCompositorMediaRecoverySuite(reports); +} diff --git a/web/app/src/three-adapter/texture-assets.ts b/web/app/src/three-adapter/texture-assets.ts index 4efff5a6..46a5f8c0 100644 --- a/web/app/src/three-adapter/texture-assets.ts +++ b/web/app/src/three-adapter/texture-assets.ts @@ -22,6 +22,20 @@ export interface TextureUploadStatus { budget: PBRTextureBudgetReport; } +export interface GPUTextureStoreStatsIR { + entries: number; + payloadBytes: number; + decodedGPUBytes: number; + keys: string[]; +} + +export interface GPUTextureStoreReleaseIR { + releasedTextures: number; + releasedPayloadBytes: number; + releasedGPUBytes: number; + resourcesAfter: 0; +} + function key(imageId: string, usage: GPUTextureUsage): string { return `${imageId}:${usage}`; } @@ -67,6 +81,16 @@ export class GPUTextureStore { return this.assets.get(key(imageId, usage)); } + stats(): GPUTextureStoreStatsIR { + const assets = [...this.assets.values()]; + return { + entries: this.textures.size, + payloadBytes: assets.reduce((total, asset) => total + asset.byteLength, 0), + decodedGPUBytes: assets.reduce((total, asset) => total + asset.width * asset.height * 4, 0), + keys: [...this.textures.keys()].sort(), + }; + } + async upload(assets: readonly GPUTextureAsset[]): Promise { const candidate = new Map(this.assets); for (const asset of assets) { @@ -161,10 +185,17 @@ export class GPUTextureStore { return true; } - dispose(): void { + dispose(): GPUTextureStoreReleaseIR { + const before = this.stats(); for (const texture of this.textures.values()) texture.dispose(); this.textures.clear(); this.assets.clear(); this.udimTileCounts.clear(); + return { + releasedTextures: before.entries, + releasedPayloadBytes: before.payloadBytes, + releasedGPUBytes: before.decodedGPUBytes, + resourcesAfter: 0, + }; } } diff --git a/web/app/src/vendor/blender/web_engine.js b/web/app/src/vendor/blender/web_engine.js index 8616e241..c644158c 100644 --- a/web/app/src/vendor/blender/web_engine.js +++ b/web/app/src/vendor/blender/web_engine.js @@ -6,7 +6,7 @@ var Module = (() => { async function(moduleArg = {}) { var moduleRtn; -var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});["_malloc","_free","_web_engine_create","_web_engine_destroy","_web_engine_get_memory_stats","_web_engine_get_live_handles","_web_engine_get_allocated_bytes","_web_engine_open_blend","_web_engine_apply_command","_web_engine_undo","_web_engine_redo","_web_engine_get_scene_snapshot","_web_engine_get_scene_metadata","_web_engine_get_scene_geometry","_web_engine_get_scene_delta","_web_engine_get_packed_asset","_web_engine_evaluate_depsgraph","_web_engine_save_blend","_web_engine_free_buffer","_web_engine_last_error_code","_web_engine_last_error_message","_web_engine_decimate_apply","_memory","___indirect_function_table","___set_stack_limits","onRuntimeInitialized"].forEach(prop=>{if(!Object.getOwnPropertyDescriptor(readyPromise,prop)){Object.defineProperty(readyPromise,prop,{get:()=>abort("You are getting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js"),set:()=>abort("You are setting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js")})}});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");let dirname=import.meta.url;if(dirname.startsWith("data:")){dirname="/"}var require=createRequire(dirname)}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){if(typeof process=="undefined"||!process.release||process.release.name!=="node")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");var nodeVersion=process.versions.node;var numericVersion=nodeVersion.split(".").slice(0,3);numericVersion=numericVersion[0]*1e4+numericVersion[1]*100+numericVersion[2].split("-")[0]*1;if(numericVersion<16e4){throw new Error("This emscripten-generated code requires node v16.0.0 (detected v"+nodeVersion+")")}var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);assert(ret.buffer);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_SHELL){if(typeof process=="object"&&typeof require==="function"||typeof window=="object"||typeof importScripts=="function")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)")}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}if(!(typeof window=="object"||typeof importScripts=="function"))throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{throw new Error("environment detection error")}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];legacyModuleProp("arguments","arguments_");if(Module["thisProgram"])thisProgram=Module["thisProgram"];legacyModuleProp("thisProgram","thisProgram");assert(typeof Module["memoryInitializerPrefixURL"]=="undefined","Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["pthreadMainPrefixURL"]=="undefined","Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["cdInitializerPrefixURL"]=="undefined","Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["filePackagePrefixURL"]=="undefined","Module.filePackagePrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["read"]=="undefined","Module.read option was removed");assert(typeof Module["readAsync"]=="undefined","Module.readAsync option was removed (modify readAsync in JS)");assert(typeof Module["readBinary"]=="undefined","Module.readBinary option was removed (modify readBinary in JS)");assert(typeof Module["setWindowTitle"]=="undefined","Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)");assert(typeof Module["TOTAL_MEMORY"]=="undefined","Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");legacyModuleProp("asm","wasmExports");legacyModuleProp("readAsync","readAsync");legacyModuleProp("readBinary","readBinary");legacyModuleProp("setWindowTitle","setWindowTitle");assert(!ENVIRONMENT_IS_SHELL,"shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.");var wasmBinary=Module["wasmBinary"];legacyModuleProp("wasmBinary","wasmBinary");if(typeof WebAssembly!="object"){err("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}assert(!Module["STACK_SIZE"],"STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");assert(!Module["wasmMemory"],"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(!Module["INITIAL_MEMORY"],"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically");function writeStackCookie(){var max=_emscripten_stack_get_end();assert((max&3)==0);if(max==0){max+=4}HEAPU32[max>>2]=34821223;checkInt32(34821223);HEAPU32[max+4>>2]=2310721022;checkInt32(2310721022);HEAPU32[0>>2]=1668509029;checkInt32(1668509029)}function checkStackCookie(){if(ABORT)return;var max=_emscripten_stack_get_end();if(max==0){max+=4}var cookie1=HEAPU32[max>>2];var cookie2=HEAPU32[max+4>>2];if(cookie1!=34821223||cookie2!=2310721022){abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`)}if(HEAPU32[0>>2]!=1668509029){abort("Runtime error: The application has corrupted its heap memory area (address zero)!")}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();setStackLimits();if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){checkStackCookie();var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}assert(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;var runDependencyTracking={};function getUniqueRunDependency(id){var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(!runDependencyTracking[id]);runDependencyTracking[id]=1;if(runDependencyWatcher===null&&typeof setInterval!="undefined"){runDependencyWatcher=setInterval(()=>{if(ABORT){clearInterval(runDependencyWatcher);runDependencyWatcher=null;return}var shown=false;for(var dep in runDependencyTracking){if(!shown){shown=true;err("still waiting on run dependencies:")}err(`dependency: ${dep}`)}if(shown){err("(end of list)")}},1e4)}}else{err("warning: run dependency added without ID")}}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(runDependencyTracking[id]);delete runDependencyTracking[id]}else{err("warning: run dependency removed without ID")}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function createExportWrapper(name,nargs){return(...args)=>{assert(runtimeInitialized,`native function \`${name}\` called before runtime initialization`);var f=wasmExports[name];assert(f,`exported native function \`${name}\` not found`);assert(args.length<=nargs,`native function \`${name}\` called with ${args.length} args but expects ${nargs}`);return f(...args)}}function findWasmBinary(){if(Module["locateFile"]){var f="web_engine.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("web_engine.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);if(isFileURI(wasmBinaryFile)){err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`)}abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];assert(wasmMemory,"memory not found in wasm exports");updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];assert(wasmTable,"table not found in wasm exports");addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");var trueModule=Module;function receiveInstantiationResult(result){assert(Module===trueModule,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");trueModule=null;receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;(()=>{var h16=new Int16Array(1);var h8=new Int8Array(h16.buffer);h16[0]=25459;if(h8[0]!==115||h8[1]!==99)throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"})();if(Module["ENVIRONMENT"]){throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)")}function legacyModuleProp(prop,newName,incoming=true){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,get(){let extra=incoming?" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)":"";abort(`\`Module.${prop}\` has been replaced by \`${newName}\``+extra)}})}}function ignoredModuleProp(prop){if(Object.getOwnPropertyDescriptor(Module,prop)){abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`)}}function isExportedByForceFilesystem(name){return name==="FS_createPath"||name==="FS_createDataFile"||name==="FS_createPreloadedFile"||name==="FS_unlink"||name==="addRunDependency"||name==="FS_createLazyFile"||name==="FS_createDevice"||name==="removeRunDependency"}function hookGlobalSymbolAccess(sym,func){if(typeof globalThis!="undefined"&&!Object.getOwnPropertyDescriptor(globalThis,sym)){Object.defineProperty(globalThis,sym,{configurable:true,get(){func();return undefined}})}}function missingGlobal(sym,msg){hookGlobalSymbolAccess(sym,()=>{warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`)})}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){hookGlobalSymbolAccess(sym,()=>{var msg=`\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;var librarySymbol=sym;if(!librarySymbol.startsWith("_")){librarySymbol="$"+sym}msg+=` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}warnOnce(msg)});unexportedRuntimeSymbol(sym)}function unexportedRuntimeSymbol(sym){if(!Object.getOwnPropertyDescriptor(Module,sym)){Object.defineProperty(Module,sym,{configurable:true,get(){var msg=`'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}abort(msg)}})}}var MAX_UINT8=2**8-1;var MAX_UINT16=2**16-1;var MAX_UINT32=2**32-1;var MAX_UINT53=2**53-1;var MAX_UINT64=2**64-1;var MIN_INT8=-(2**(8-1));var MIN_INT16=-(2**(16-1));var MIN_INT32=-(2**(32-1));var MIN_INT53=-(2**(53-1));var MIN_INT64=-(2**(64-1));function checkInt(value,bits,min,max){assert(Number.isInteger(Number(value)),`attempt to write non-integer (${value}) into integer heap`);assert(value<=max,`value (${value}) too large to write as ${bits}-bit value`);assert(value>=min,`value (${value}) too small to write as ${bits}-bit value`)}var checkInt8=value=>checkInt(value,8,MIN_INT8,MAX_UINT8);var checkInt16=value=>checkInt(value,16,MIN_INT16,MAX_UINT16);var checkInt32=value=>checkInt(value,32,MIN_INT32,MAX_UINT32);var checkInt64=value=>checkInt(value,64,MIN_INT64,MAX_UINT64);function dbg(...args){console.warn(...args)}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var ptrToString=ptr=>{assert(typeof ptr==="number");ptr>>>=0;return"0x"+ptr.toString(16).padStart(8,"0")};var setStackLimits=()=>{var stackLow=_emscripten_stack_get_base();var stackHigh=_emscripten_stack_get_end();___set_stack_limits(stackLow,stackHigh)};var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text="warning: "+text;err(text)}};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}assert(wasmTable.get(funcPtr)==func,"JavaScript-side Wasm function table mirror is out of date!");return func};var ___call_sighandler=(fp,sig)=>getWasmTableEntry(fp)(sig);var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);assert(exceptionCaught.length>0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught;checkInt8(caught)}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown;checkInt8(rethrown)}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___handle_stack_overflow=requested=>{var base=_emscripten_stack_get_base();var end=_emscripten_stack_get_end();abort(`stack overflow (Attempt to set SP to ${ptrToString(requested)}`+`, with stack limits [${ptrToString(end)} - ${ptrToString(base)}`+"]). If you require more stack space build with -sSTACK_SIZE=")};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{assert(typeof str==="string",`stringToUTF8Array expects a string (got ${typeof str})`);if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;if(u>1114111)warnOnce("Invalid Unicode code point "+ptrToString(u)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size)};var alignMemory=(size,alignment)=>{assert(alignment,"alignment argument is required");return Math.ceil(size/alignment)*alignment};var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);assert(size>=0);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var strError=errno=>UTF8ToString(_strerror(errno));var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class extends Error{constructor(errno){super(runtimeInitialized?strError(errno):"");this.name="ErrnoError";this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){assert(typeof parent=="object");var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){assert(fd>=-1);stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){assert(FS.syncFSRequests>0);FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){if(typeof type=="string"){throw type}var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);assert(idx!==-1);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;i=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate(stream,offset,length){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){assert(offset>=0);if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1);assert(stdin.fd===0,`invalid handle for stdin (${stdin.fd})`);assert(stdout.fd===1,`invalid handle for stdout (${stdout.fd})`);assert(stderr.fd===2,`invalid handle for stderr (${stderr.fd})`)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){assert(!FS.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;_fflush(0);for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);assert(size>=0);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node},absolutePath(){abort("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder(){abort("FS.createFolder has been removed; use FS.mkdir instead")},createLink(){abort("FS.createLink has been removed; use FS.symlink instead")},joinPath(){abort("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc(){abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath(){abort("FS.standardizePath has been removed; use PATH.normalize instead")}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;checkInt32(stat.dev);HEAP32[buf+4>>2]=stat.mode;checkInt32(stat.mode);HEAPU32[buf+8>>2]=stat.nlink;checkInt32(stat.nlink);HEAP32[buf+12>>2]=stat.uid;checkInt32(stat.uid);HEAP32[buf+16>>2]=stat.gid;checkInt32(stat.gid);HEAP32[buf+20>>2]=stat.rdev;checkInt32(stat.rdev);tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];checkInt64(stat.size);HEAP32[buf+32>>2]=4096;checkInt32(4096);HEAP32[buf+36>>2]=stat.blocks;checkInt32(stat.blocks);var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];checkInt64(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;checkInt32(atime%1e3*1e3*1e3);tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];checkInt64(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;checkInt32(mtime%1e3*1e3*1e3);tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];checkInt64(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;checkInt32(ctime%1e3*1e3*1e3);tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];checkInt64(stat.ino);return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);assert(flags===0||flags==512);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){assert(SYSCALLS.varargs!=undefined);var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;checkInt16(2);return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>{assert(typeof maxBytesToWrite=="number","stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)};function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.getStreamFromFD(fd);stream.getdents||=FS.readdir(stream.path);var struct_size=280;var pos=0;var off=FS.llseek(stream,0,1);var idx=Math.floor(off/struct_size);while(idx>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];checkInt64(id);tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];checkInt64((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;checkInt16(280);HEAP8[dirp+pos+18]=type;checkInt8(type);stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;checkInt32(termios.c_iflag||0);HEAP32[argp+4>>2]=termios.c_oflag||0;checkInt32(termios.c_oflag||0);HEAP32[argp+8>>2]=termios.c_cflag||0;checkInt32(termios.c_cflag||0);HEAP32[argp+12>>2]=termios.c_lflag||0;checkInt32(termios.c_lflag||0);for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0;checkInt8(termios.c_cc[i]||0)}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;checkInt32(0);return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];checkInt16(winsize[0]);HEAP16[argp+2>>1]=winsize[1];checkInt16(winsize[1])}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;assert(!flags,`unknown flags in __syscall_newfstatat: ${flags}`);path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("native code called abort()")};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var __emscripten_throw_longjmp=()=>{throw Infinity};var convertI32PairToI53Checked=(lo,hi)=>{assert(lo==lo>>>0||lo==(lo|0));assert(hi===(hi|0));return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;checkInt32(res.allocated);HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;checkInt32(stdTimezoneOffset*60);HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);checkInt32(Number(winterOffset!=summerOffset));var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);assert(winterName);assert(summerName);assert(lengthBytesUTF8(winterName)<=16,`timezone name truncated to fit in TZNAME_MAX (${winterName})`);assert(lengthBytesUTF8(summerName)<=16,`timezone name truncated to fit in TZNAME_MAX (${summerName})`);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_get_now=()=>performance.now();var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){err(`growMemory: Attempted to grow heap from ${b.byteLength} bytes to ${size} bytes, but got error: ${e}`)}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;assert(requestedSize>oldSize);var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`);return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var t0=_emscripten_get_now();var replacement=growMemory(newSize);var t1=_emscripten_get_now();dbg(`Heap resize call from ${oldSize} to ${newSize} took ${t1-t0} msecs. Success: ${!!replacement}`);if(replacement){return true}}err(`Failed to grow the heap from ${oldSize} bytes to ${newSize} bytes, not enough memory!`);return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;checkInt32(ptr);stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;checkInt32(strings.length);var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;checkInt32(bufSize);return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;checkInt8(type);HEAP16[pbuf+2>>1]=flags;checkInt16(flags);tempI64=[rightsBase>>>0,(tempDouble=rightsBase,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+8>>2]=tempI64[0],HEAP32[pbuf+12>>2]=tempI64[1];checkInt64(rightsBase);tempI64=[rightsInheriting>>>0,(tempDouble=rightsInheriting,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+16>>2]=tempI64[0],HEAP32[pbuf+20>>2]=tempI64[1];checkInt64(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];checkInt64(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var _llvm_eh_typeid_for=type=>type;var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var getCFunc=ident=>{var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func};var writeArrayToMemory=(array,buffer)=>{assert(array.length>=0,"writeArrayToMemory array must have a length (should be an array or typed array)");HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;assert(returnType!=="array",'Return type should not be "array".');if(args){for(var i=0;i(...args)=>ccall(ident,returnType,argTypes,args,opts);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var wasmImports={__assert_fail:___assert_fail,__call_sighandler:___call_sighandler,__cxa_begin_catch:___cxa_begin_catch,__cxa_end_catch:___cxa_end_catch,__cxa_find_matching_catch_2:___cxa_find_matching_catch_2,__cxa_find_matching_catch_3:___cxa_find_matching_catch_3,__cxa_rethrow:___cxa_rethrow,__cxa_throw:___cxa_throw,__handle_stack_overflow:___handle_stack_overflow,__resumeException:___resumeException,__syscall_faccessat:___syscall_faccessat,__syscall_fcntl64:___syscall_fcntl64,__syscall_fstat64:___syscall_fstat64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_rmdir:___syscall_rmdir,__syscall_stat64:___syscall_stat64,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,_emscripten_memcpy_js:__emscripten_memcpy_js,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_emscripten_throw_longjmp:__emscripten_throw_longjmp,_mmap_js:__mmap_js,_munmap_js:__munmap_js,_tzset_js:__tzset_js,emscripten_date_now:_emscripten_date_now,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,getentropy:_getentropy,invoke_d,invoke_diii,invoke_fi,invoke_fif,invoke_fifff,invoke_fii,invoke_fiii,invoke_fiiii,invoke_fiiiiii,invoke_fiiiiiii,invoke_fij,invoke_i,invoke_idiii,invoke_ii,invoke_iif,invoke_iifi,invoke_iifii,invoke_iifiiiii,invoke_iii,invoke_iiid,invoke_iiidii,invoke_iiif,invoke_iiiffi,invoke_iiii,invoke_iiiidi,invoke_iiiiffifffffffi,invoke_iiiifiii,invoke_iiiii,invoke_iiiiif,invoke_iiiiifi,invoke_iiiiii,invoke_iiiiiifi,invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii,invoke_iiiiiii,invoke_iiiiiiifii,invoke_iiiiiiii,invoke_iiiiiiiii,invoke_iiiiiiiiiffi,invoke_iiiiiiiiii,invoke_iiiiiiiiiifi,invoke_iiiiiiiiiii,invoke_iiiiiiiiiiii,invoke_iiiiiiiiiiiif,invoke_iiiiiiiiiiiiiii,invoke_iiiiiiij,invoke_iiiiiij,invoke_iiiiij,invoke_iiiij,invoke_iiij,invoke_iiiji,invoke_iiijii,invoke_iiijiii,invoke_iij,invoke_iijiii,invoke_iijj,invoke_iijji,invoke_iijjiii,invoke_ij,invoke_ijjiii,invoke_j,invoke_ji,invoke_jii,invoke_jiii,invoke_jiji,invoke_v,invoke_vdiii,invoke_vi,invoke_vid,invoke_vif,invoke_vififiif,invoke_vifii,invoke_vifiiifiiiiiiiiiiiiifiiii,invoke_vii,invoke_viid,invoke_viif,invoke_viiff,invoke_viifi,invoke_viifiii,invoke_viii,invoke_viiid,invoke_viiif,invoke_viiiffii,invoke_viiiffiiii,invoke_viiifi,invoke_viiii,invoke_viiiif,invoke_viiiii,invoke_viiiiif,invoke_viiiiii,invoke_viiiiiid,invoke_viiiiiif,invoke_viiiiiifi,invoke_viiiiiifif,invoke_viiiiiifii,invoke_viiiiiii,invoke_viiiiiiidiiii,invoke_viiiiiiii,invoke_viiiiiiiif,invoke_viiiiiiiii,invoke_viiiiiiiiii,invoke_viiiiiiiiiidii,invoke_viiiiiiiiiii,invoke_viiiiiiiiiiii,invoke_viiiiiiiiiiiiii,invoke_viiiiiiiiiiiiiiii,invoke_viiiiij,invoke_viiiiiji,invoke_viiiiji,invoke_viiiijiiifi,invoke_viiij,invoke_viiiji,invoke_viiijii,invoke_viiijjji,invoke_viij,invoke_viiji,invoke_viijii,invoke_viijj,invoke_vij,invoke_viji,invoke_vijif,invoke_vijii,invoke_vijj,invoke_vjjiii,llvm_eh_typeid_for:_llvm_eh_typeid_for,proc_exit:_proc_exit};var wasmExports=createWasm();var ___wasm_call_ctors=createExportWrapper("__wasm_call_ctors",0);var _web_engine_create=Module["_web_engine_create"]=createExportWrapper("web_engine_create",0);var _web_engine_get_memory_stats=Module["_web_engine_get_memory_stats"]=createExportWrapper("web_engine_get_memory_stats",1);var _web_engine_destroy=Module["_web_engine_destroy"]=createExportWrapper("web_engine_destroy",1);var _fflush=createExportWrapper("fflush",1);var _web_engine_get_live_handles=Module["_web_engine_get_live_handles"]=createExportWrapper("web_engine_get_live_handles",0);var _web_engine_get_allocated_bytes=Module["_web_engine_get_allocated_bytes"]=createExportWrapper("web_engine_get_allocated_bytes",0);var _web_engine_open_blend=Module["_web_engine_open_blend"]=createExportWrapper("web_engine_open_blend",3);var _web_engine_apply_command=Module["_web_engine_apply_command"]=createExportWrapper("web_engine_apply_command",3);var _web_engine_decimate_apply=Module["_web_engine_decimate_apply"]=createExportWrapper("web_engine_decimate_apply",35);var _web_engine_undo=Module["_web_engine_undo"]=createExportWrapper("web_engine_undo",1);var _web_engine_redo=Module["_web_engine_redo"]=createExportWrapper("web_engine_redo",1);var _web_engine_get_scene_snapshot=Module["_web_engine_get_scene_snapshot"]=createExportWrapper("web_engine_get_scene_snapshot",3);var _web_engine_get_scene_metadata=Module["_web_engine_get_scene_metadata"]=createExportWrapper("web_engine_get_scene_metadata",3);var _web_engine_get_scene_geometry=Module["_web_engine_get_scene_geometry"]=createExportWrapper("web_engine_get_scene_geometry",3);var _web_engine_get_scene_delta=Module["_web_engine_get_scene_delta"]=createExportWrapper("web_engine_get_scene_delta",3);var _web_engine_get_packed_asset=Module["_web_engine_get_packed_asset"]=createExportWrapper("web_engine_get_packed_asset",5);var _web_engine_evaluate_depsgraph=Module["_web_engine_evaluate_depsgraph"]=createExportWrapper("web_engine_evaluate_depsgraph",3);var _web_engine_save_blend=Module["_web_engine_save_blend"]=createExportWrapper("web_engine_save_blend",3);var _malloc=Module["_malloc"]=createExportWrapper("malloc",1);var _web_engine_free_buffer=Module["_web_engine_free_buffer"]=createExportWrapper("web_engine_free_buffer",1);var _free=Module["_free"]=createExportWrapper("free",1);var _web_engine_last_error_code=Module["_web_engine_last_error_code"]=createExportWrapper("web_engine_last_error_code",0);var _web_engine_last_error_message=Module["_web_engine_last_error_message"]=createExportWrapper("web_engine_last_error_message",0);var _strerror=createExportWrapper("strerror",1);var _emscripten_builtin_memalign=createExportWrapper("emscripten_builtin_memalign",2);var _setThrew=createExportWrapper("setThrew",2);var __emscripten_tempret_set=createExportWrapper("_emscripten_tempret_set",1);var __emscripten_tempret_get=createExportWrapper("_emscripten_tempret_get",0);var _emscripten_stack_init=()=>(_emscripten_stack_init=wasmExports["emscripten_stack_init"])();var _emscripten_stack_get_free=()=>(_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"])();var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"])();var _emscripten_stack_get_end=()=>(_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();var ___cxa_decrement_exception_refcount=createExportWrapper("__cxa_decrement_exception_refcount",1);var ___cxa_increment_exception_refcount=createExportWrapper("__cxa_increment_exception_refcount",1);var ___cxa_can_catch=createExportWrapper("__cxa_can_catch",3);var ___cxa_get_exception_ptr=createExportWrapper("__cxa_get_exception_ptr",1);var ___set_stack_limits=Module["___set_stack_limits"]=createExportWrapper("__set_stack_limits",2);var dynCall_viij=Module["dynCall_viij"]=createExportWrapper("dynCall_viij",5);var dynCall_viiji=Module["dynCall_viiji"]=createExportWrapper("dynCall_viiji",6);var dynCall_viijii=Module["dynCall_viijii"]=createExportWrapper("dynCall_viijii",7);var dynCall_vij=Module["dynCall_vij"]=createExportWrapper("dynCall_vij",4);var dynCall_viiiijiiifi=Module["dynCall_viiiijiiifi"]=createExportWrapper("dynCall_viiiijiiifi",12);var dynCall_viiijjji=Module["dynCall_viiijjji"]=createExportWrapper("dynCall_viiijjji",11);var dynCall_viiiiji=Module["dynCall_viiiiji"]=createExportWrapper("dynCall_viiiiji",8);var dynCall_viiiji=Module["dynCall_viiiji"]=createExportWrapper("dynCall_viiiji",7);var dynCall_ij=Module["dynCall_ij"]=createExportWrapper("dynCall_ij",3);var dynCall_viji=Module["dynCall_viji"]=createExportWrapper("dynCall_viji",5);var dynCall_iij=Module["dynCall_iij"]=createExportWrapper("dynCall_iij",4);var dynCall_fij=Module["dynCall_fij"]=createExportWrapper("dynCall_fij",4);var dynCall_vijf=Module["dynCall_vijf"]=createExportWrapper("dynCall_vijf",5);var dynCall_jiii=Module["dynCall_jiii"]=createExportWrapper("dynCall_jiii",4);var dynCall_viiij=Module["dynCall_viiij"]=createExportWrapper("dynCall_viiij",6);var dynCall_iiijii=Module["dynCall_iiijii"]=createExportWrapper("dynCall_iiijii",7);var dynCall_iiijiii=Module["dynCall_iiijiii"]=createExportWrapper("dynCall_iiijiii",8);var dynCall_iiij=Module["dynCall_iiij"]=createExportWrapper("dynCall_iiij",5);var dynCall_vjjiii=Module["dynCall_vjjiii"]=createExportWrapper("dynCall_vjjiii",8);var dynCall_ijjiii=Module["dynCall_ijjiii"]=createExportWrapper("dynCall_ijjiii",8);var dynCall_iijiii=Module["dynCall_iijiii"]=createExportWrapper("dynCall_iijiii",7);var dynCall_iijjiii=Module["dynCall_iijjiii"]=createExportWrapper("dynCall_iijjiii",9);var dynCall_iiiji=Module["dynCall_iiiji"]=createExportWrapper("dynCall_iiiji",6);var dynCall_jii=Module["dynCall_jii"]=createExportWrapper("dynCall_jii",3);var dynCall_iijj=Module["dynCall_iijj"]=createExportWrapper("dynCall_iijj",6);var dynCall_ji=Module["dynCall_ji"]=createExportWrapper("dynCall_ji",2);var dynCall_vijii=Module["dynCall_vijii"]=createExportWrapper("dynCall_vijii",6);var dynCall_vijif=Module["dynCall_vijif"]=createExportWrapper("dynCall_vijif",6);var dynCall_viiiiij=Module["dynCall_viiiiij"]=createExportWrapper("dynCall_viiiiij",8);var dynCall_viiiiiji=Module["dynCall_viiiiiji"]=createExportWrapper("dynCall_viiiiiji",9);var dynCall_iiiij=Module["dynCall_iiiij"]=createExportWrapper("dynCall_iiiij",6);var dynCall_vijj=Module["dynCall_vijj"]=createExportWrapper("dynCall_vijj",6);var dynCall_iijji=Module["dynCall_iijji"]=createExportWrapper("dynCall_iijji",7);var dynCall_j=Module["dynCall_j"]=createExportWrapper("dynCall_j",1);var dynCall_iiiiiiij=Module["dynCall_iiiiiiij"]=createExportWrapper("dynCall_iiiiiiij",9);var dynCall_viijj=Module["dynCall_viijj"]=createExportWrapper("dynCall_viijj",7);var dynCall_iiiiij=Module["dynCall_iiiiij"]=createExportWrapper("dynCall_iiiiij",7);var dynCall_viiijii=Module["dynCall_viiijii"]=createExportWrapper("dynCall_viiijii",8);var dynCall_jij=Module["dynCall_jij"]=createExportWrapper("dynCall_jij",4);var dynCall_vijji=Module["dynCall_vijji"]=createExportWrapper("dynCall_vijji",7);var dynCall_iiiiiij=Module["dynCall_iiiiiij"]=createExportWrapper("dynCall_iiiiiij",8);var dynCall_jiji=Module["dynCall_jiji"]=createExportWrapper("dynCall_jiji",5);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=createExportWrapper("dynCall_iiiiijj",9);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=createExportWrapper("dynCall_iiiiiijj",10);function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiffifffffffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vififiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifi(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vdiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_idiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiifiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_d(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fifff(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifiiifiiiiiiiiiiiiifiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiid(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiidiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiidii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viid(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiid(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiid(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_fij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ij(index,a1,a2){var sp=stackSave();try{return dynCall_ij(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_jiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{dynCall_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{dynCall_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiijiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_vjjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ijjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_ijjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijif(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijj(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return dynCall_ji(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiij(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiiiij(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiijii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return dynCall_jii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiji(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_jiji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var missingLibrarySymbols=["writeI53ToI64","writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","readI53FromI64","readI53FromU64","convertI32PairToI53","convertU32PairToI53","getTempRet0","exitJS","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","jstoi_q","listenOnce","autoResumeAudioContext","dynCallLegacy","getDynCaller","dynCall","handleException","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asmjsMangle","HandleAllocator","getNativeTypeSize","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","UTF16ToString","stringToUTF16","lengthBytesUTF16","UTF32ToString","stringToUTF32","lengthBytesUTF32","stringToNewUTF8","registerKeyEventCallback","maybeCStringToJsString","findEventTarget","getBoundingClientRect","fillMouseEventData","registerMouseEventCallback","registerWheelEventCallback","registerUiEventCallback","registerFocusEventCallback","fillDeviceOrientationEventData","registerDeviceOrientationEventCallback","fillDeviceMotionEventData","registerDeviceMotionEventCallback","screenOrientation","fillOrientationChangeEventData","registerOrientationChangeEventCallback","fillFullscreenChangeEventData","registerFullscreenChangeEventCallback","JSEvents_requestFullscreen","JSEvents_resizeCanvasForFullscreen","registerRestoreOldStyle","hideEverythingExceptGivenElement","restoreHiddenElements","setLetterbox","softFullscreenResizeWebGLRenderTarget","doRequestFullscreen","fillPointerlockChangeEventData","registerPointerlockChangeEventCallback","registerPointerlockErrorEventCallback","requestPointerLock","fillVisibilityChangeEventData","registerVisibilityChangeEventCallback","registerTouchEventCallback","fillGamepadEventData","registerGamepadEventCallback","registerBeforeUnloadEventCallback","fillBatteryEventData","battery","registerBatteryEventCallback","setCanvasElementSize","getCanvasElementSize","jsStackTrace","getCallstack","convertPCtoSourceLocation","checkWasiClock","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","createDyncallWrapper","safeSetTimeout","setImmediateWrapped","clearImmediateWrapped","polyfillSetImmediate","registerPostMainLoop","registerPreMainLoop","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","safeRequestAnimationFrame","isLeapYear","ydayFromDate","arraySum","addDays","getSocketFromFD","getSocketAddress","FS_unlink","FS_mkdirTree","_setNetworkCallback","heapObjectForWebGLType","toTypedArrayIndex","webgl_enable_ANGLE_instanced_arrays","webgl_enable_OES_vertex_array_object","webgl_enable_WEBGL_draw_buffers","webgl_enable_WEBGL_multi_draw","webgl_enable_EXT_polygon_offset_clamp","webgl_enable_EXT_clip_control","webgl_enable_WEBGL_polygon_mode","emscriptenWebGLGet","computeUnpackAlignedImageSize","colorChannelsInGlTextureFormat","emscriptenWebGLGetTexPixelData","emscriptenWebGLGetUniform","webglGetUniformLocation","webglPrepareUniformLocationsBeforeFirstUse","webglGetLeftBracePos","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","setErrNo","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addOnPreRun","addOnInit","addOnPreMain","addOnExit","addOnPostRun","addRunDependency","removeRunDependency","out","err","callMain","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","convertI32PairToI53Checked","stackSave","stackRestore","stackAlloc","setTempRet0","ptrToString","zeroMemory","getHeapMax","growMemory","ENV","setStackLimits","ERRNO_CODES","strError","DNS","Protocols","Sockets","initRandomFill","randomFill","timers","warnOnce","readEmAsmArgsArray","jstoi_s","getExecutableName","keepRuntimeAlive","asyncLoad","alignMemory","mmapAlloc","wasmTable","noExitRuntime","getCFunc","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","stringToAscii","UTF16Decoder","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","doReadv","doWritev","promiseMap","uncaughtExceptionCount","exceptionLast","exceptionCaught","ExceptionInfo","findMatchingCatch","Browser","getPreloadedImageData__data","wget","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_createPath","FS_createDevice","FS_readFile","FS","FS_createDataFile","FS_createLazyFile","MEMFS","TTY","PIPEFS","SOCKFS","tempFixedLengthArray","miniTempWebGLFloatBuffers","miniTempWebGLIntBuffers","GL","AL","GLUT","EGL","GLEW","IDBStore","SDL","SDL_gfx","allocateUTF8","allocateUTF8OnStack","print","printErr"];unexportedSymbols.forEach(unexportedRuntimeSymbol);var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function stackCheckInit(){_emscripten_stack_init();writeStackCookie()}function run(){if(runDependencies>0){return}stackCheckInit();if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();assert(!Module["_main"],'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]');postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}checkStackCookie()}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise;for(const prop of Object.keys(Module)){if(!(prop in moduleArg)){Object.defineProperty(moduleArg,prop,{configurable:true,get(){abort(`Access to module property ('${prop}') is no longer possible via the module constructor argument; Instead, use the result of the module constructor.`)}})}} +var Module=moduleArg;var readyPromiseResolve,readyPromiseReject;var readyPromise=new Promise((resolve,reject)=>{readyPromiseResolve=resolve;readyPromiseReject=reject});["_malloc","_free","_web_engine_create","_web_engine_destroy","_web_engine_get_memory_stats","_web_engine_get_live_handles","_web_engine_get_allocated_bytes","_web_engine_open_blend","_web_engine_apply_command","_web_engine_append_library_object","_web_engine_undo","_web_engine_redo","_web_engine_get_scene_snapshot","_web_engine_get_scene_metadata","_web_engine_get_scene_geometry","_web_engine_get_scene_delta","_web_engine_get_packed_asset","_web_engine_evaluate_depsgraph","_web_engine_save_blend","_web_engine_free_buffer","_web_engine_last_error_code","_web_engine_last_error_message","_web_engine_decimate_apply","_memory","___indirect_function_table","___set_stack_limits","onRuntimeInitialized"].forEach(prop=>{if(!Object.getOwnPropertyDescriptor(readyPromise,prop)){Object.defineProperty(readyPromise,prop,{get:()=>abort("You are getting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js"),set:()=>abort("You are setting "+prop+" on the Promise object, instead of the instance. Use .then() to get called back with the instance, see the MODULARIZE docs in src/settings.js")})}});var ENVIRONMENT_IS_WEB=typeof window=="object";var ENVIRONMENT_IS_WORKER=typeof importScripts=="function";var ENVIRONMENT_IS_NODE=typeof process=="object"&&typeof process.versions=="object"&&typeof process.versions.node=="string"&&process.type!="renderer";var ENVIRONMENT_IS_SHELL=!ENVIRONMENT_IS_WEB&&!ENVIRONMENT_IS_NODE&&!ENVIRONMENT_IS_WORKER;if(ENVIRONMENT_IS_NODE){const{createRequire}=await import("module");let dirname=import.meta.url;if(dirname.startsWith("data:")){dirname="/"}var require=createRequire(dirname)}var moduleOverrides=Object.assign({},Module);var arguments_=[];var thisProgram="./this.program";var quit_=(status,toThrow)=>{throw toThrow};var scriptDirectory="";function locateFile(path){if(Module["locateFile"]){return Module["locateFile"](path,scriptDirectory)}return scriptDirectory+path}var readAsync,readBinary;if(ENVIRONMENT_IS_NODE){if(typeof process=="undefined"||!process.release||process.release.name!=="node")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");var nodeVersion=process.versions.node;var numericVersion=nodeVersion.split(".").slice(0,3);numericVersion=numericVersion[0]*1e4+numericVersion[1]*100+numericVersion[2].split("-")[0]*1;if(numericVersion<16e4){throw new Error("This emscripten-generated code requires node v16.0.0 (detected v"+nodeVersion+")")}var fs=require("fs");var nodePath=require("path");if(!import.meta.url.startsWith("data:")){scriptDirectory=nodePath.dirname(require("url").fileURLToPath(import.meta.url))+"/"}readBinary=filename=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);var ret=fs.readFileSync(filename);assert(ret.buffer);return ret};readAsync=(filename,binary=true)=>{filename=isFileURI(filename)?new URL(filename):nodePath.normalize(filename);return new Promise((resolve,reject)=>{fs.readFile(filename,binary?undefined:"utf8",(err,data)=>{if(err)reject(err);else resolve(binary?data.buffer:data)})})};if(!Module["thisProgram"]&&process.argv.length>1){thisProgram=process.argv[1].replace(/\\/g,"/")}arguments_=process.argv.slice(2);quit_=(status,toThrow)=>{process.exitCode=status;throw toThrow}}else if(ENVIRONMENT_IS_SHELL){if(typeof process=="object"&&typeof require==="function"||typeof window=="object"||typeof importScripts=="function")throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)")}else if(ENVIRONMENT_IS_WEB||ENVIRONMENT_IS_WORKER){if(ENVIRONMENT_IS_WORKER){scriptDirectory=self.location.href}else if(typeof document!="undefined"&&document.currentScript){scriptDirectory=document.currentScript.src}if(_scriptName){scriptDirectory=_scriptName}if(scriptDirectory.startsWith("blob:")){scriptDirectory=""}else{scriptDirectory=scriptDirectory.substr(0,scriptDirectory.replace(/[?#].*/,"").lastIndexOf("/")+1)}if(!(typeof window=="object"||typeof importScripts=="function"))throw new Error("not compiled for this environment (did you build to HTML and try to run it not on the web, or set ENVIRONMENT to something - like node - and run it someplace else - like on the web?)");{if(ENVIRONMENT_IS_WORKER){readBinary=url=>{var xhr=new XMLHttpRequest;xhr.open("GET",url,false);xhr.responseType="arraybuffer";xhr.send(null);return new Uint8Array(xhr.response)}}readAsync=url=>{assert(!isFileURI(url),"readAsync does not work with file:// URLs");return fetch(url,{credentials:"same-origin"}).then(response=>{if(response.ok){return response.arrayBuffer()}return Promise.reject(new Error(response.status+" : "+response.url))})}}}else{throw new Error("environment detection error")}var out=Module["print"]||console.log.bind(console);var err=Module["printErr"]||console.error.bind(console);Object.assign(Module,moduleOverrides);moduleOverrides=null;checkIncomingModuleAPI();if(Module["arguments"])arguments_=Module["arguments"];legacyModuleProp("arguments","arguments_");if(Module["thisProgram"])thisProgram=Module["thisProgram"];legacyModuleProp("thisProgram","thisProgram");assert(typeof Module["memoryInitializerPrefixURL"]=="undefined","Module.memoryInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["pthreadMainPrefixURL"]=="undefined","Module.pthreadMainPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["cdInitializerPrefixURL"]=="undefined","Module.cdInitializerPrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["filePackagePrefixURL"]=="undefined","Module.filePackagePrefixURL option was removed, use Module.locateFile instead");assert(typeof Module["read"]=="undefined","Module.read option was removed");assert(typeof Module["readAsync"]=="undefined","Module.readAsync option was removed (modify readAsync in JS)");assert(typeof Module["readBinary"]=="undefined","Module.readBinary option was removed (modify readBinary in JS)");assert(typeof Module["setWindowTitle"]=="undefined","Module.setWindowTitle option was removed (modify emscripten_set_window_title in JS)");assert(typeof Module["TOTAL_MEMORY"]=="undefined","Module.TOTAL_MEMORY has been renamed Module.INITIAL_MEMORY");legacyModuleProp("asm","wasmExports");legacyModuleProp("readAsync","readAsync");legacyModuleProp("readBinary","readBinary");legacyModuleProp("setWindowTitle","setWindowTitle");assert(!ENVIRONMENT_IS_SHELL,"shell environment detected but not enabled at build time. Add `shell` to `-sENVIRONMENT` to enable.");var wasmBinary=Module["wasmBinary"];legacyModuleProp("wasmBinary","wasmBinary");if(typeof WebAssembly!="object"){err("no native wasm support detected")}var wasmMemory;var ABORT=false;var EXITSTATUS;function assert(condition,text){if(!condition){abort("Assertion failed"+(text?": "+text:""))}}var HEAP8,HEAPU8,HEAP16,HEAPU16,HEAP32,HEAPU32,HEAPF32,HEAPF64;function updateMemoryViews(){var b=wasmMemory.buffer;Module["HEAP8"]=HEAP8=new Int8Array(b);Module["HEAP16"]=HEAP16=new Int16Array(b);Module["HEAPU8"]=HEAPU8=new Uint8Array(b);Module["HEAPU16"]=HEAPU16=new Uint16Array(b);Module["HEAP32"]=HEAP32=new Int32Array(b);Module["HEAPU32"]=HEAPU32=new Uint32Array(b);Module["HEAPF32"]=HEAPF32=new Float32Array(b);Module["HEAPF64"]=HEAPF64=new Float64Array(b)}assert(!Module["STACK_SIZE"],"STACK_SIZE can no longer be set at runtime. Use -sSTACK_SIZE at link time");assert(typeof Int32Array!="undefined"&&typeof Float64Array!=="undefined"&&Int32Array.prototype.subarray!=undefined&&Int32Array.prototype.set!=undefined,"JS engine does not provide full typed array support");assert(!Module["wasmMemory"],"Use of `wasmMemory` detected. Use -sIMPORTED_MEMORY to define wasmMemory externally");assert(!Module["INITIAL_MEMORY"],"Detected runtime INITIAL_MEMORY setting. Use -sIMPORTED_MEMORY to define wasmMemory dynamically");function writeStackCookie(){var max=_emscripten_stack_get_end();assert((max&3)==0);if(max==0){max+=4}HEAPU32[max>>2]=34821223;checkInt32(34821223);HEAPU32[max+4>>2]=2310721022;checkInt32(2310721022);HEAPU32[0>>2]=1668509029;checkInt32(1668509029)}function checkStackCookie(){if(ABORT)return;var max=_emscripten_stack_get_end();if(max==0){max+=4}var cookie1=HEAPU32[max>>2];var cookie2=HEAPU32[max+4>>2];if(cookie1!=34821223||cookie2!=2310721022){abort(`Stack overflow! Stack cookie has been overwritten at ${ptrToString(max)}, expected hex dwords 0x89BACDFE and 0x2135467, but received ${ptrToString(cookie2)} ${ptrToString(cookie1)}`)}if(HEAPU32[0>>2]!=1668509029){abort("Runtime error: The application has corrupted its heap memory area (address zero)!")}}var __ATPRERUN__=[];var __ATINIT__=[];var __ATPOSTRUN__=[];var runtimeInitialized=false;function preRun(){var preRuns=Module["preRun"];if(preRuns){if(typeof preRuns=="function")preRuns=[preRuns];preRuns.forEach(addOnPreRun)}callRuntimeCallbacks(__ATPRERUN__)}function initRuntime(){assert(!runtimeInitialized);runtimeInitialized=true;checkStackCookie();setStackLimits();if(!Module["noFSInit"]&&!FS.initialized)FS.init();FS.ignorePermissions=false;TTY.init();callRuntimeCallbacks(__ATINIT__)}function postRun(){checkStackCookie();var postRuns=Module["postRun"];if(postRuns){if(typeof postRuns=="function")postRuns=[postRuns];postRuns.forEach(addOnPostRun)}callRuntimeCallbacks(__ATPOSTRUN__)}function addOnPreRun(cb){__ATPRERUN__.unshift(cb)}function addOnInit(cb){__ATINIT__.unshift(cb)}function addOnPostRun(cb){__ATPOSTRUN__.unshift(cb)}assert(Math.imul,"This browser does not support Math.imul(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.fround,"This browser does not support Math.fround(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.clz32,"This browser does not support Math.clz32(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");assert(Math.trunc,"This browser does not support Math.trunc(), build with LEGACY_VM_SUPPORT or POLYFILL_OLD_MATH_FUNCTIONS to add in a polyfill");var runDependencies=0;var runDependencyWatcher=null;var dependenciesFulfilled=null;var runDependencyTracking={};function getUniqueRunDependency(id){var orig=id;while(1){if(!runDependencyTracking[id])return id;id=orig+Math.random()}}function addRunDependency(id){runDependencies++;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(!runDependencyTracking[id]);runDependencyTracking[id]=1;if(runDependencyWatcher===null&&typeof setInterval!="undefined"){runDependencyWatcher=setInterval(()=>{if(ABORT){clearInterval(runDependencyWatcher);runDependencyWatcher=null;return}var shown=false;for(var dep in runDependencyTracking){if(!shown){shown=true;err("still waiting on run dependencies:")}err(`dependency: ${dep}`)}if(shown){err("(end of list)")}},1e4)}}else{err("warning: run dependency added without ID")}}function removeRunDependency(id){runDependencies--;Module["monitorRunDependencies"]?.(runDependencies);if(id){assert(runDependencyTracking[id]);delete runDependencyTracking[id]}else{err("warning: run dependency removed without ID")}if(runDependencies==0){if(runDependencyWatcher!==null){clearInterval(runDependencyWatcher);runDependencyWatcher=null}if(dependenciesFulfilled){var callback=dependenciesFulfilled;dependenciesFulfilled=null;callback()}}}function abort(what){Module["onAbort"]?.(what);what="Aborted("+what+")";err(what);ABORT=true;var e=new WebAssembly.RuntimeError(what);readyPromiseReject(e);throw e}var dataURIPrefix="data:application/octet-stream;base64,";var isDataURI=filename=>filename.startsWith(dataURIPrefix);var isFileURI=filename=>filename.startsWith("file://");function createExportWrapper(name,nargs){return(...args)=>{assert(runtimeInitialized,`native function \`${name}\` called before runtime initialization`);var f=wasmExports[name];assert(f,`exported native function \`${name}\` not found`);assert(args.length<=nargs,`native function \`${name}\` called with ${args.length} args but expects ${nargs}`);return f(...args)}}function findWasmBinary(){if(Module["locateFile"]){var f="web_engine.wasm";if(!isDataURI(f)){return locateFile(f)}return f}return new URL("web_engine.wasm",import.meta.url).href}var wasmBinaryFile;function getBinarySync(file){if(file==wasmBinaryFile&&wasmBinary){return new Uint8Array(wasmBinary)}if(readBinary){return readBinary(file)}throw"both async and sync fetching of the wasm failed"}function getBinaryPromise(binaryFile){if(!wasmBinary){return readAsync(binaryFile).then(response=>new Uint8Array(response),()=>getBinarySync(binaryFile))}return Promise.resolve().then(()=>getBinarySync(binaryFile))}function instantiateArrayBuffer(binaryFile,imports,receiver){return getBinaryPromise(binaryFile).then(binary=>WebAssembly.instantiate(binary,imports)).then(receiver,reason=>{err(`failed to asynchronously prepare wasm: ${reason}`);if(isFileURI(wasmBinaryFile)){err(`warning: Loading from a file URI (${wasmBinaryFile}) is not supported in most browsers. See https://emscripten.org/docs/getting_started/FAQ.html#how-do-i-run-a-local-webserver-for-testing-why-does-my-program-stall-in-downloading-or-preparing`)}abort(reason)})}function instantiateAsync(binary,binaryFile,imports,callback){if(!binary&&typeof WebAssembly.instantiateStreaming=="function"&&!isDataURI(binaryFile)&&!ENVIRONMENT_IS_NODE&&typeof fetch=="function"){return fetch(binaryFile,{credentials:"same-origin"}).then(response=>{var result=WebAssembly.instantiateStreaming(response,imports);return result.then(callback,function(reason){err(`wasm streaming compile failed: ${reason}`);err("falling back to ArrayBuffer instantiation");return instantiateArrayBuffer(binaryFile,imports,callback)})})}return instantiateArrayBuffer(binaryFile,imports,callback)}function getWasmImports(){return{env:wasmImports,wasi_snapshot_preview1:wasmImports}}function createWasm(){var info=getWasmImports();function receiveInstance(instance,module){wasmExports=instance.exports;wasmMemory=wasmExports["memory"];assert(wasmMemory,"memory not found in wasm exports");updateMemoryViews();wasmTable=wasmExports["__indirect_function_table"];assert(wasmTable,"table not found in wasm exports");addOnInit(wasmExports["__wasm_call_ctors"]);removeRunDependency("wasm-instantiate");return wasmExports}addRunDependency("wasm-instantiate");var trueModule=Module;function receiveInstantiationResult(result){assert(Module===trueModule,"the Module object should not be replaced during async compilation - perhaps the order of HTML elements is wrong?");trueModule=null;receiveInstance(result["instance"])}if(Module["instantiateWasm"]){try{return Module["instantiateWasm"](info,receiveInstance)}catch(e){err(`Module.instantiateWasm callback failed with error: ${e}`);readyPromiseReject(e)}}wasmBinaryFile??=findWasmBinary();instantiateAsync(wasmBinary,wasmBinaryFile,info,receiveInstantiationResult).catch(readyPromiseReject);return{}}var tempDouble;var tempI64;(()=>{var h16=new Int16Array(1);var h8=new Int8Array(h16.buffer);h16[0]=25459;if(h8[0]!==115||h8[1]!==99)throw"Runtime error: expected the system to be little-endian! (Run with -sSUPPORT_BIG_ENDIAN to bypass)"})();if(Module["ENVIRONMENT"]){throw new Error("Module.ENVIRONMENT has been deprecated. To force the environment, use the ENVIRONMENT compile-time option (for example, -sENVIRONMENT=web or -sENVIRONMENT=node)")}function legacyModuleProp(prop,newName,incoming=true){if(!Object.getOwnPropertyDescriptor(Module,prop)){Object.defineProperty(Module,prop,{configurable:true,get(){let extra=incoming?" (the initial value can be provided on Module, but after startup the value is only looked for on a local variable of that name)":"";abort(`\`Module.${prop}\` has been replaced by \`${newName}\``+extra)}})}}function ignoredModuleProp(prop){if(Object.getOwnPropertyDescriptor(Module,prop)){abort(`\`Module.${prop}\` was supplied but \`${prop}\` not included in INCOMING_MODULE_JS_API`)}}function isExportedByForceFilesystem(name){return name==="FS_createPath"||name==="FS_createDataFile"||name==="FS_createPreloadedFile"||name==="FS_unlink"||name==="addRunDependency"||name==="FS_createLazyFile"||name==="FS_createDevice"||name==="removeRunDependency"}function hookGlobalSymbolAccess(sym,func){if(typeof globalThis!="undefined"&&!Object.getOwnPropertyDescriptor(globalThis,sym)){Object.defineProperty(globalThis,sym,{configurable:true,get(){func();return undefined}})}}function missingGlobal(sym,msg){hookGlobalSymbolAccess(sym,()=>{warnOnce(`\`${sym}\` is not longer defined by emscripten. ${msg}`)})}missingGlobal("buffer","Please use HEAP8.buffer or wasmMemory.buffer");missingGlobal("asm","Please use wasmExports instead");function missingLibrarySymbol(sym){hookGlobalSymbolAccess(sym,()=>{var msg=`\`${sym}\` is a library symbol and not included by default; add it to your library.js __deps or to DEFAULT_LIBRARY_FUNCS_TO_INCLUDE on the command line`;var librarySymbol=sym;if(!librarySymbol.startsWith("_")){librarySymbol="$"+sym}msg+=` (e.g. -sDEFAULT_LIBRARY_FUNCS_TO_INCLUDE='${librarySymbol}')`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}warnOnce(msg)});unexportedRuntimeSymbol(sym)}function unexportedRuntimeSymbol(sym){if(!Object.getOwnPropertyDescriptor(Module,sym)){Object.defineProperty(Module,sym,{configurable:true,get(){var msg=`'${sym}' was not exported. add it to EXPORTED_RUNTIME_METHODS (see the Emscripten FAQ)`;if(isExportedByForceFilesystem(sym)){msg+=". Alternatively, forcing filesystem support (-sFORCE_FILESYSTEM) can export this for you"}abort(msg)}})}}var MAX_UINT8=2**8-1;var MAX_UINT16=2**16-1;var MAX_UINT32=2**32-1;var MAX_UINT53=2**53-1;var MAX_UINT64=2**64-1;var MIN_INT8=-(2**(8-1));var MIN_INT16=-(2**(16-1));var MIN_INT32=-(2**(32-1));var MIN_INT53=-(2**(53-1));var MIN_INT64=-(2**(64-1));function checkInt(value,bits,min,max){assert(Number.isInteger(Number(value)),`attempt to write non-integer (${value}) into integer heap`);assert(value<=max,`value (${value}) too large to write as ${bits}-bit value`);assert(value>=min,`value (${value}) too small to write as ${bits}-bit value`)}var checkInt8=value=>checkInt(value,8,MIN_INT8,MAX_UINT8);var checkInt16=value=>checkInt(value,16,MIN_INT16,MAX_UINT16);var checkInt32=value=>checkInt(value,32,MIN_INT32,MAX_UINT32);var checkInt64=value=>checkInt(value,64,MIN_INT64,MAX_UINT64);function dbg(...args){console.warn(...args)}function ExitStatus(status){this.name="ExitStatus";this.message=`Program terminated with exit(${status})`;this.status=status}var callRuntimeCallbacks=callbacks=>{callbacks.forEach(f=>f(Module))};var noExitRuntime=Module["noExitRuntime"]||true;var ptrToString=ptr=>{assert(typeof ptr==="number");ptr>>>=0;return"0x"+ptr.toString(16).padStart(8,"0")};var setStackLimits=()=>{var stackLow=_emscripten_stack_get_base();var stackHigh=_emscripten_stack_get_end();___set_stack_limits(stackLow,stackHigh)};var stackRestore=val=>__emscripten_stack_restore(val);var stackSave=()=>_emscripten_stack_get_current();var warnOnce=text=>{warnOnce.shown||={};if(!warnOnce.shown[text]){warnOnce.shown[text]=1;if(ENVIRONMENT_IS_NODE)text="warning: "+text;err(text)}};var UTF8Decoder=typeof TextDecoder!="undefined"?new TextDecoder:undefined;var UTF8ArrayToString=(heapOrArray,idx=0,maxBytesToRead=NaN)=>{var endIdx=idx+maxBytesToRead;var endPtr=idx;while(heapOrArray[endPtr]&&!(endPtr>=endIdx))++endPtr;if(endPtr-idx>16&&heapOrArray.buffer&&UTF8Decoder){return UTF8Decoder.decode(heapOrArray.subarray(idx,endPtr))}var str="";while(idx>10,56320|ch&1023)}}return str};var UTF8ToString=(ptr,maxBytesToRead)=>{assert(typeof ptr=="number",`UTF8ToString expects a number (got ${typeof ptr})`);return ptr?UTF8ArrayToString(HEAPU8,ptr,maxBytesToRead):""};var ___assert_fail=(condition,filename,line,func)=>{abort(`Assertion failed: ${UTF8ToString(condition)}, at: `+[filename?UTF8ToString(filename):"unknown filename",line,func?UTF8ToString(func):"unknown function"])};var wasmTableMirror=[];var wasmTable;var getWasmTableEntry=funcPtr=>{var func=wasmTableMirror[funcPtr];if(!func){if(funcPtr>=wasmTableMirror.length)wasmTableMirror.length=funcPtr+1;wasmTableMirror[funcPtr]=func=wasmTable.get(funcPtr)}assert(wasmTable.get(funcPtr)==func,"JavaScript-side Wasm function table mirror is out of date!");return func};var ___call_sighandler=(fp,sig)=>getWasmTableEntry(fp)(sig);var exceptionCaught=[];var uncaughtExceptionCount=0;var ___cxa_begin_catch=ptr=>{var info=new ExceptionInfo(ptr);if(!info.get_caught()){info.set_caught(true);uncaughtExceptionCount--}info.set_rethrown(false);exceptionCaught.push(info);___cxa_increment_exception_refcount(ptr);return ___cxa_get_exception_ptr(ptr)};var exceptionLast=0;var ___cxa_end_catch=()=>{_setThrew(0,0);assert(exceptionCaught.length>0);var info=exceptionCaught.pop();___cxa_decrement_exception_refcount(info.excPtr);exceptionLast=0};class ExceptionInfo{constructor(excPtr){this.excPtr=excPtr;this.ptr=excPtr-24}set_type(type){HEAPU32[this.ptr+4>>2]=type}get_type(){return HEAPU32[this.ptr+4>>2]}set_destructor(destructor){HEAPU32[this.ptr+8>>2]=destructor}get_destructor(){return HEAPU32[this.ptr+8>>2]}set_caught(caught){caught=caught?1:0;HEAP8[this.ptr+12]=caught;checkInt8(caught)}get_caught(){return HEAP8[this.ptr+12]!=0}set_rethrown(rethrown){rethrown=rethrown?1:0;HEAP8[this.ptr+13]=rethrown;checkInt8(rethrown)}get_rethrown(){return HEAP8[this.ptr+13]!=0}init(type,destructor){this.set_adjusted_ptr(0);this.set_type(type);this.set_destructor(destructor)}set_adjusted_ptr(adjustedPtr){HEAPU32[this.ptr+16>>2]=adjustedPtr}get_adjusted_ptr(){return HEAPU32[this.ptr+16>>2]}}var ___resumeException=ptr=>{if(!exceptionLast){exceptionLast=ptr}assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var setTempRet0=val=>__emscripten_tempret_set(val);var findMatchingCatch=args=>{var thrown=exceptionLast;if(!thrown){setTempRet0(0);return 0}var info=new ExceptionInfo(thrown);info.set_adjusted_ptr(thrown);var thrownType=info.get_type();if(!thrownType){setTempRet0(0);return thrown}for(var caughtType of args){if(caughtType===0||caughtType===thrownType){break}var adjusted_ptr_addr=info.ptr+16;if(___cxa_can_catch(caughtType,thrownType,adjusted_ptr_addr)){setTempRet0(caughtType);return thrown}}setTempRet0(thrownType);return thrown};var ___cxa_find_matching_catch_2=()=>findMatchingCatch([]);var ___cxa_find_matching_catch_3=arg0=>findMatchingCatch([arg0]);var ___cxa_rethrow=()=>{var info=exceptionCaught.pop();if(!info){abort("no exception to throw")}var ptr=info.excPtr;if(!info.get_rethrown()){exceptionCaught.push(info);info.set_rethrown(true);info.set_caught(false);uncaughtExceptionCount++}exceptionLast=ptr;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___cxa_throw=(ptr,type,destructor)=>{var info=new ExceptionInfo(ptr);info.init(type,destructor);exceptionLast=ptr;uncaughtExceptionCount++;assert(false,"Exception thrown, but exception catching is not enabled. Compile with -sNO_DISABLE_EXCEPTION_CATCHING or -sEXCEPTION_CATCHING_ALLOWED=[..] to catch.")};var ___handle_stack_overflow=requested=>{var base=_emscripten_stack_get_base();var end=_emscripten_stack_get_end();abort(`stack overflow (Attempt to set SP to ${ptrToString(requested)}`+`, with stack limits [${ptrToString(end)} - ${ptrToString(base)}`+"]). If you require more stack space build with -sSTACK_SIZE=")};var PATH={isAbs:path=>path.charAt(0)==="/",splitPath:filename=>{var splitPathRe=/^(\/?|)([\s\S]*?)((?:\.{1,2}|[^\/]+?|)(\.[^.\/]*|))(?:[\/]*)$/;return splitPathRe.exec(filename).slice(1)},normalizeArray:(parts,allowAboveRoot)=>{var up=0;for(var i=parts.length-1;i>=0;i--){var last=parts[i];if(last==="."){parts.splice(i,1)}else if(last===".."){parts.splice(i,1);up++}else if(up){parts.splice(i,1);up--}}if(allowAboveRoot){for(;up;up--){parts.unshift("..")}}return parts},normalize:path=>{var isAbsolute=PATH.isAbs(path),trailingSlash=path.substr(-1)==="/";path=PATH.normalizeArray(path.split("/").filter(p=>!!p),!isAbsolute).join("/");if(!path&&!isAbsolute){path="."}if(path&&trailingSlash){path+="/"}return(isAbsolute?"/":"")+path},dirname:path=>{var result=PATH.splitPath(path),root=result[0],dir=result[1];if(!root&&!dir){return"."}if(dir){dir=dir.substr(0,dir.length-1)}return root+dir},basename:path=>{if(path==="/")return"/";path=PATH.normalize(path);path=path.replace(/\/$/,"");var lastSlash=path.lastIndexOf("/");if(lastSlash===-1)return path;return path.substr(lastSlash+1)},join:(...paths)=>PATH.normalize(paths.join("/")),join2:(l,r)=>PATH.normalize(l+"/"+r)};var initRandomFill=()=>{if(typeof crypto=="object"&&typeof crypto["getRandomValues"]=="function"){return view=>crypto.getRandomValues(view)}else if(ENVIRONMENT_IS_NODE){try{var crypto_module=require("crypto");var randomFillSync=crypto_module["randomFillSync"];if(randomFillSync){return view=>crypto_module["randomFillSync"](view)}var randomBytes=crypto_module["randomBytes"];return view=>(view.set(randomBytes(view.byteLength)),view)}catch(e){}}abort("no cryptographic support found for randomDevice. consider polyfilling it if you want to use something insecure like Math.random(), e.g. put this in a --pre-js: var crypto = { getRandomValues: (array) => { for (var i = 0; i < array.length; i++) array[i] = (Math.random()*256)|0 } };")};var randomFill=view=>(randomFill=initRandomFill())(view);var PATH_FS={resolve:(...args)=>{var resolvedPath="",resolvedAbsolute=false;for(var i=args.length-1;i>=-1&&!resolvedAbsolute;i--){var path=i>=0?args[i]:FS.cwd();if(typeof path!="string"){throw new TypeError("Arguments to path.resolve must be strings")}else if(!path){return""}resolvedPath=path+"/"+resolvedPath;resolvedAbsolute=PATH.isAbs(path)}resolvedPath=PATH.normalizeArray(resolvedPath.split("/").filter(p=>!!p),!resolvedAbsolute).join("/");return(resolvedAbsolute?"/":"")+resolvedPath||"."},relative:(from,to)=>{from=PATH_FS.resolve(from).substr(1);to=PATH_FS.resolve(to).substr(1);function trim(arr){var start=0;for(;start=0;end--){if(arr[end]!=="")break}if(start>end)return[];return arr.slice(start,end-start+1)}var fromParts=trim(from.split("/"));var toParts=trim(to.split("/"));var length=Math.min(fromParts.length,toParts.length);var samePartsLength=length;for(var i=0;i{var len=0;for(var i=0;i=55296&&c<=57343){len+=4;++i}else{len+=3}}return len};var stringToUTF8Array=(str,heap,outIdx,maxBytesToWrite)=>{assert(typeof str==="string",`stringToUTF8Array expects a string (got ${typeof str})`);if(!(maxBytesToWrite>0))return 0;var startIdx=outIdx;var endIdx=outIdx+maxBytesToWrite-1;for(var i=0;i=55296&&u<=57343){var u1=str.charCodeAt(++i);u=65536+((u&1023)<<10)|u1&1023}if(u<=127){if(outIdx>=endIdx)break;heap[outIdx++]=u}else if(u<=2047){if(outIdx+1>=endIdx)break;heap[outIdx++]=192|u>>6;heap[outIdx++]=128|u&63}else if(u<=65535){if(outIdx+2>=endIdx)break;heap[outIdx++]=224|u>>12;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}else{if(outIdx+3>=endIdx)break;if(u>1114111)warnOnce("Invalid Unicode code point "+ptrToString(u)+" encountered when serializing a JS string to a UTF-8 string in wasm memory! (Valid unicode code points should be in range 0-0x10FFFF).");heap[outIdx++]=240|u>>18;heap[outIdx++]=128|u>>12&63;heap[outIdx++]=128|u>>6&63;heap[outIdx++]=128|u&63}}heap[outIdx]=0;return outIdx-startIdx};function intArrayFromString(stringy,dontAddNull,length){var len=length>0?length:lengthBytesUTF8(stringy)+1;var u8array=new Array(len);var numBytesWritten=stringToUTF8Array(stringy,u8array,0,u8array.length);if(dontAddNull)u8array.length=numBytesWritten;return u8array}var FS_stdin_getChar=()=>{if(!FS_stdin_getChar_buffer.length){var result=null;if(ENVIRONMENT_IS_NODE){var BUFSIZE=256;var buf=Buffer.alloc(BUFSIZE);var bytesRead=0;var fd=process.stdin.fd;try{bytesRead=fs.readSync(fd,buf,0,BUFSIZE)}catch(e){if(e.toString().includes("EOF"))bytesRead=0;else throw e}if(bytesRead>0){result=buf.slice(0,bytesRead).toString("utf-8")}}else if(typeof window!="undefined"&&typeof window.prompt=="function"){result=window.prompt("Input: ");if(result!==null){result+="\n"}}else{}if(!result){return null}FS_stdin_getChar_buffer=intArrayFromString(result,true)}return FS_stdin_getChar_buffer.shift()};var TTY={ttys:[],init(){},shutdown(){},register(dev,ops){TTY.ttys[dev]={input:[],output:[],ops};FS.registerDevice(dev,TTY.stream_ops)},stream_ops:{open(stream){var tty=TTY.ttys[stream.node.rdev];if(!tty){throw new FS.ErrnoError(43)}stream.tty=tty;stream.seekable=false},close(stream){stream.tty.ops.fsync(stream.tty)},fsync(stream){stream.tty.ops.fsync(stream.tty)},read(stream,buffer,offset,length,pos){if(!stream.tty||!stream.tty.ops.get_char){throw new FS.ErrnoError(60)}var bytesRead=0;for(var i=0;i0){out(UTF8ArrayToString(tty.output));tty.output=[]}},ioctl_tcgets(tty){return{c_iflag:25856,c_oflag:5,c_cflag:191,c_lflag:35387,c_cc:[3,28,127,21,4,0,1,0,17,19,26,0,18,15,23,22,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]}},ioctl_tcsets(tty,optional_actions,data){return 0},ioctl_tiocgwinsz(tty){return[24,80]}},default_tty1_ops:{put_char(tty,val){if(val===null||val===10){err(UTF8ArrayToString(tty.output));tty.output=[]}else{if(val!=0)tty.output.push(val)}},fsync(tty){if(tty.output&&tty.output.length>0){err(UTF8ArrayToString(tty.output));tty.output=[]}}}};var zeroMemory=(address,size)=>{HEAPU8.fill(0,address,address+size)};var alignMemory=(size,alignment)=>{assert(alignment,"alignment argument is required");return Math.ceil(size/alignment)*alignment};var mmapAlloc=size=>{size=alignMemory(size,65536);var ptr=_emscripten_builtin_memalign(65536,size);if(ptr)zeroMemory(ptr,size);return ptr};var MEMFS={ops_table:null,mount(mount){return MEMFS.createNode(null,"/",16384|511,0)},createNode(parent,name,mode,dev){if(FS.isBlkdev(mode)||FS.isFIFO(mode)){throw new FS.ErrnoError(63)}MEMFS.ops_table||={dir:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,lookup:MEMFS.node_ops.lookup,mknod:MEMFS.node_ops.mknod,rename:MEMFS.node_ops.rename,unlink:MEMFS.node_ops.unlink,rmdir:MEMFS.node_ops.rmdir,readdir:MEMFS.node_ops.readdir,symlink:MEMFS.node_ops.symlink},stream:{llseek:MEMFS.stream_ops.llseek}},file:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:{llseek:MEMFS.stream_ops.llseek,read:MEMFS.stream_ops.read,write:MEMFS.stream_ops.write,allocate:MEMFS.stream_ops.allocate,mmap:MEMFS.stream_ops.mmap,msync:MEMFS.stream_ops.msync}},link:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr,readlink:MEMFS.node_ops.readlink},stream:{}},chrdev:{node:{getattr:MEMFS.node_ops.getattr,setattr:MEMFS.node_ops.setattr},stream:FS.chrdev_stream_ops}};var node=FS.createNode(parent,name,mode,dev);if(FS.isDir(node.mode)){node.node_ops=MEMFS.ops_table.dir.node;node.stream_ops=MEMFS.ops_table.dir.stream;node.contents={}}else if(FS.isFile(node.mode)){node.node_ops=MEMFS.ops_table.file.node;node.stream_ops=MEMFS.ops_table.file.stream;node.usedBytes=0;node.contents=null}else if(FS.isLink(node.mode)){node.node_ops=MEMFS.ops_table.link.node;node.stream_ops=MEMFS.ops_table.link.stream}else if(FS.isChrdev(node.mode)){node.node_ops=MEMFS.ops_table.chrdev.node;node.stream_ops=MEMFS.ops_table.chrdev.stream}node.timestamp=Date.now();if(parent){parent.contents[name]=node;parent.timestamp=node.timestamp}return node},getFileDataAsTypedArray(node){if(!node.contents)return new Uint8Array(0);if(node.contents.subarray)return node.contents.subarray(0,node.usedBytes);return new Uint8Array(node.contents)},expandFileStorage(node,newCapacity){var prevCapacity=node.contents?node.contents.length:0;if(prevCapacity>=newCapacity)return;var CAPACITY_DOUBLING_MAX=1024*1024;newCapacity=Math.max(newCapacity,prevCapacity*(prevCapacity>>0);if(prevCapacity!=0)newCapacity=Math.max(newCapacity,256);var oldContents=node.contents;node.contents=new Uint8Array(newCapacity);if(node.usedBytes>0)node.contents.set(oldContents.subarray(0,node.usedBytes),0)},resizeFileStorage(node,newSize){if(node.usedBytes==newSize)return;if(newSize==0){node.contents=null;node.usedBytes=0}else{var oldContents=node.contents;node.contents=new Uint8Array(newSize);if(oldContents){node.contents.set(oldContents.subarray(0,Math.min(newSize,node.usedBytes)))}node.usedBytes=newSize}},node_ops:{getattr(node){var attr={};attr.dev=FS.isChrdev(node.mode)?node.id:1;attr.ino=node.id;attr.mode=node.mode;attr.nlink=1;attr.uid=0;attr.gid=0;attr.rdev=node.rdev;if(FS.isDir(node.mode)){attr.size=4096}else if(FS.isFile(node.mode)){attr.size=node.usedBytes}else if(FS.isLink(node.mode)){attr.size=node.link.length}else{attr.size=0}attr.atime=new Date(node.timestamp);attr.mtime=new Date(node.timestamp);attr.ctime=new Date(node.timestamp);attr.blksize=4096;attr.blocks=Math.ceil(attr.size/attr.blksize);return attr},setattr(node,attr){if(attr.mode!==undefined){node.mode=attr.mode}if(attr.timestamp!==undefined){node.timestamp=attr.timestamp}if(attr.size!==undefined){MEMFS.resizeFileStorage(node,attr.size)}},lookup(parent,name){throw FS.genericErrors[44]},mknod(parent,name,mode,dev){return MEMFS.createNode(parent,name,mode,dev)},rename(old_node,new_dir,new_name){if(FS.isDir(old_node.mode)){var new_node;try{new_node=FS.lookupNode(new_dir,new_name)}catch(e){}if(new_node){for(var i in new_node.contents){throw new FS.ErrnoError(55)}}}delete old_node.parent.contents[old_node.name];old_node.parent.timestamp=Date.now();old_node.name=new_name;new_dir.contents[new_name]=old_node;new_dir.timestamp=old_node.parent.timestamp},unlink(parent,name){delete parent.contents[name];parent.timestamp=Date.now()},rmdir(parent,name){var node=FS.lookupNode(parent,name);for(var i in node.contents){throw new FS.ErrnoError(55)}delete parent.contents[name];parent.timestamp=Date.now()},readdir(node){var entries=[".",".."];for(var key of Object.keys(node.contents)){entries.push(key)}return entries},symlink(parent,newname,oldpath){var node=MEMFS.createNode(parent,newname,511|40960,0);node.link=oldpath;return node},readlink(node){if(!FS.isLink(node.mode)){throw new FS.ErrnoError(28)}return node.link}},stream_ops:{read(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=stream.node.usedBytes)return 0;var size=Math.min(stream.node.usedBytes-position,length);assert(size>=0);if(size>8&&contents.subarray){buffer.set(contents.subarray(position,position+size),offset)}else{for(var i=0;i0||position+length{var dep=!noRunDep?getUniqueRunDependency(`al ${url}`):"";readAsync(url).then(arrayBuffer=>{assert(arrayBuffer,`Loading data file "${url}" failed (no arrayBuffer).`);onload(new Uint8Array(arrayBuffer));if(dep)removeRunDependency(dep)},err=>{if(onerror){onerror()}else{throw`Loading data file "${url}" failed.`}});if(dep)addRunDependency(dep)};var FS_createDataFile=(parent,name,fileData,canRead,canWrite,canOwn)=>{FS.createDataFile(parent,name,fileData,canRead,canWrite,canOwn)};var preloadPlugins=Module["preloadPlugins"]||[];var FS_handledByPreloadPlugin=(byteArray,fullname,finish,onerror)=>{if(typeof Browser!="undefined")Browser.init();var handled=false;preloadPlugins.forEach(plugin=>{if(handled)return;if(plugin["canHandle"](fullname)){plugin["handle"](byteArray,fullname,finish,onerror);handled=true}});return handled};var FS_createPreloadedFile=(parent,name,url,canRead,canWrite,onload,onerror,dontCreateFile,canOwn,preFinish)=>{var fullname=name?PATH_FS.resolve(PATH.join2(parent,name)):parent;var dep=getUniqueRunDependency(`cp ${fullname}`);function processData(byteArray){function finish(byteArray){preFinish?.();if(!dontCreateFile){FS_createDataFile(parent,name,byteArray,canRead,canWrite,canOwn)}onload?.();removeRunDependency(dep)}if(FS_handledByPreloadPlugin(byteArray,fullname,finish,()=>{onerror?.();removeRunDependency(dep)})){return}finish(byteArray)}addRunDependency(dep);if(typeof url=="string"){asyncLoad(url,processData,onerror)}else{processData(url)}};var FS_modeStringToFlags=str=>{var flagModes={r:0,"r+":2,w:512|64|1,"w+":512|64|2,a:1024|64|1,"a+":1024|64|2};var flags=flagModes[str];if(typeof flags=="undefined"){throw new Error(`Unknown file open mode: ${str}`)}return flags};var FS_getMode=(canRead,canWrite)=>{var mode=0;if(canRead)mode|=292|73;if(canWrite)mode|=146;return mode};var strError=errno=>UTF8ToString(_strerror(errno));var ERRNO_CODES={EPERM:63,ENOENT:44,ESRCH:71,EINTR:27,EIO:29,ENXIO:60,E2BIG:1,ENOEXEC:45,EBADF:8,ECHILD:12,EAGAIN:6,EWOULDBLOCK:6,ENOMEM:48,EACCES:2,EFAULT:21,ENOTBLK:105,EBUSY:10,EEXIST:20,EXDEV:75,ENODEV:43,ENOTDIR:54,EISDIR:31,EINVAL:28,ENFILE:41,EMFILE:33,ENOTTY:59,ETXTBSY:74,EFBIG:22,ENOSPC:51,ESPIPE:70,EROFS:69,EMLINK:34,EPIPE:64,EDOM:18,ERANGE:68,ENOMSG:49,EIDRM:24,ECHRNG:106,EL2NSYNC:156,EL3HLT:107,EL3RST:108,ELNRNG:109,EUNATCH:110,ENOCSI:111,EL2HLT:112,EDEADLK:16,ENOLCK:46,EBADE:113,EBADR:114,EXFULL:115,ENOANO:104,EBADRQC:103,EBADSLT:102,EDEADLOCK:16,EBFONT:101,ENOSTR:100,ENODATA:116,ETIME:117,ENOSR:118,ENONET:119,ENOPKG:120,EREMOTE:121,ENOLINK:47,EADV:122,ESRMNT:123,ECOMM:124,EPROTO:65,EMULTIHOP:36,EDOTDOT:125,EBADMSG:9,ENOTUNIQ:126,EBADFD:127,EREMCHG:128,ELIBACC:129,ELIBBAD:130,ELIBSCN:131,ELIBMAX:132,ELIBEXEC:133,ENOSYS:52,ENOTEMPTY:55,ENAMETOOLONG:37,ELOOP:32,EOPNOTSUPP:138,EPFNOSUPPORT:139,ECONNRESET:15,ENOBUFS:42,EAFNOSUPPORT:5,EPROTOTYPE:67,ENOTSOCK:57,ENOPROTOOPT:50,ESHUTDOWN:140,ECONNREFUSED:14,EADDRINUSE:3,ECONNABORTED:13,ENETUNREACH:40,ENETDOWN:38,ETIMEDOUT:73,EHOSTDOWN:142,EHOSTUNREACH:23,EINPROGRESS:26,EALREADY:7,EDESTADDRREQ:17,EMSGSIZE:35,EPROTONOSUPPORT:66,ESOCKTNOSUPPORT:137,EADDRNOTAVAIL:4,ENETRESET:39,EISCONN:30,ENOTCONN:53,ETOOMANYREFS:141,EUSERS:136,EDQUOT:19,ESTALE:72,ENOTSUP:138,ENOMEDIUM:148,EILSEQ:25,EOVERFLOW:61,ECANCELED:11,ENOTRECOVERABLE:56,EOWNERDEAD:62,ESTRPIPE:135};var FS={root:null,mounts:[],devices:{},streams:[],nextInode:1,nameTable:null,currentPath:"/",initialized:false,ignorePermissions:true,ErrnoError:class extends Error{constructor(errno){super(runtimeInitialized?strError(errno):"");this.name="ErrnoError";this.errno=errno;for(var key in ERRNO_CODES){if(ERRNO_CODES[key]===errno){this.code=key;break}}}},genericErrors:{},filesystems:null,syncFSRequests:0,readFiles:{},FSStream:class{constructor(){this.shared={}}get object(){return this.node}set object(val){this.node=val}get isRead(){return(this.flags&2097155)!==1}get isWrite(){return(this.flags&2097155)!==0}get isAppend(){return this.flags&1024}get flags(){return this.shared.flags}set flags(val){this.shared.flags=val}get position(){return this.shared.position}set position(val){this.shared.position=val}},FSNode:class{constructor(parent,name,mode,rdev){if(!parent){parent=this}this.parent=parent;this.mount=parent.mount;this.mounted=null;this.id=FS.nextInode++;this.name=name;this.mode=mode;this.node_ops={};this.stream_ops={};this.rdev=rdev;this.readMode=292|73;this.writeMode=146}get read(){return(this.mode&this.readMode)===this.readMode}set read(val){val?this.mode|=this.readMode:this.mode&=~this.readMode}get write(){return(this.mode&this.writeMode)===this.writeMode}set write(val){val?this.mode|=this.writeMode:this.mode&=~this.writeMode}get isFolder(){return FS.isDir(this.mode)}get isDevice(){return FS.isChrdev(this.mode)}},lookupPath(path,opts={}){path=PATH_FS.resolve(path);if(!path)return{path:"",node:null};var defaults={follow_mount:true,recurse_count:0};opts=Object.assign(defaults,opts);if(opts.recurse_count>8){throw new FS.ErrnoError(32)}var parts=path.split("/").filter(p=>!!p);var current=FS.root;var current_path="/";for(var i=0;i40){throw new FS.ErrnoError(32)}}}}return{path:current_path,node:current}},getPath(node){var path;while(true){if(FS.isRoot(node)){var mount=node.mount.mountpoint;if(!path)return mount;return mount[mount.length-1]!=="/"?`${mount}/${path}`:mount+path}path=path?`${node.name}/${path}`:node.name;node=node.parent}},hashName(parentid,name){var hash=0;for(var i=0;i>>0)%FS.nameTable.length},hashAddNode(node){var hash=FS.hashName(node.parent.id,node.name);node.name_next=FS.nameTable[hash];FS.nameTable[hash]=node},hashRemoveNode(node){var hash=FS.hashName(node.parent.id,node.name);if(FS.nameTable[hash]===node){FS.nameTable[hash]=node.name_next}else{var current=FS.nameTable[hash];while(current){if(current.name_next===node){current.name_next=node.name_next;break}current=current.name_next}}},lookupNode(parent,name){var errCode=FS.mayLookup(parent);if(errCode){throw new FS.ErrnoError(errCode)}var hash=FS.hashName(parent.id,name);for(var node=FS.nameTable[hash];node;node=node.name_next){var nodeName=node.name;if(node.parent.id===parent.id&&nodeName===name){return node}}return FS.lookup(parent,name)},createNode(parent,name,mode,rdev){assert(typeof parent=="object");var node=new FS.FSNode(parent,name,mode,rdev);FS.hashAddNode(node);return node},destroyNode(node){FS.hashRemoveNode(node)},isRoot(node){return node===node.parent},isMountpoint(node){return!!node.mounted},isFile(mode){return(mode&61440)===32768},isDir(mode){return(mode&61440)===16384},isLink(mode){return(mode&61440)===40960},isChrdev(mode){return(mode&61440)===8192},isBlkdev(mode){return(mode&61440)===24576},isFIFO(mode){return(mode&61440)===4096},isSocket(mode){return(mode&49152)===49152},flagsToPermissionString(flag){var perms=["r","w","rw"][flag&3];if(flag&512){perms+="w"}return perms},nodePermissions(node,perms){if(FS.ignorePermissions){return 0}if(perms.includes("r")&&!(node.mode&292)){return 2}else if(perms.includes("w")&&!(node.mode&146)){return 2}else if(perms.includes("x")&&!(node.mode&73)){return 2}return 0},mayLookup(dir){if(!FS.isDir(dir.mode))return 54;var errCode=FS.nodePermissions(dir,"x");if(errCode)return errCode;if(!dir.node_ops.lookup)return 2;return 0},mayCreate(dir,name){try{var node=FS.lookupNode(dir,name);return 20}catch(e){}return FS.nodePermissions(dir,"wx")},mayDelete(dir,name,isdir){var node;try{node=FS.lookupNode(dir,name)}catch(e){return e.errno}var errCode=FS.nodePermissions(dir,"wx");if(errCode){return errCode}if(isdir){if(!FS.isDir(node.mode)){return 54}if(FS.isRoot(node)||FS.getPath(node)===FS.cwd()){return 10}}else{if(FS.isDir(node.mode)){return 31}}return 0},mayOpen(node,flags){if(!node){return 44}if(FS.isLink(node.mode)){return 32}else if(FS.isDir(node.mode)){if(FS.flagsToPermissionString(flags)!=="r"||flags&512){return 31}}return FS.nodePermissions(node,FS.flagsToPermissionString(flags))},MAX_OPEN_FDS:4096,nextfd(){for(var fd=0;fd<=FS.MAX_OPEN_FDS;fd++){if(!FS.streams[fd]){return fd}}throw new FS.ErrnoError(33)},getStreamChecked(fd){var stream=FS.getStream(fd);if(!stream){throw new FS.ErrnoError(8)}return stream},getStream:fd=>FS.streams[fd],createStream(stream,fd=-1){assert(fd>=-1);stream=Object.assign(new FS.FSStream,stream);if(fd==-1){fd=FS.nextfd()}stream.fd=fd;FS.streams[fd]=stream;return stream},closeStream(fd){FS.streams[fd]=null},dupStream(origStream,fd=-1){var stream=FS.createStream(origStream,fd);stream.stream_ops?.dup?.(stream);return stream},chrdev_stream_ops:{open(stream){var device=FS.getDevice(stream.node.rdev);stream.stream_ops=device.stream_ops;stream.stream_ops.open?.(stream)},llseek(){throw new FS.ErrnoError(70)}},major:dev=>dev>>8,minor:dev=>dev&255,makedev:(ma,mi)=>ma<<8|mi,registerDevice(dev,ops){FS.devices[dev]={stream_ops:ops}},getDevice:dev=>FS.devices[dev],getMounts(mount){var mounts=[];var check=[mount];while(check.length){var m=check.pop();mounts.push(m);check.push(...m.mounts)}return mounts},syncfs(populate,callback){if(typeof populate=="function"){callback=populate;populate=false}FS.syncFSRequests++;if(FS.syncFSRequests>1){err(`warning: ${FS.syncFSRequests} FS.syncfs operations in flight at once, probably just doing extra work`)}var mounts=FS.getMounts(FS.root.mount);var completed=0;function doCallback(errCode){assert(FS.syncFSRequests>0);FS.syncFSRequests--;return callback(errCode)}function done(errCode){if(errCode){if(!done.errored){done.errored=true;return doCallback(errCode)}return}if(++completed>=mounts.length){doCallback(null)}}mounts.forEach(mount=>{if(!mount.type.syncfs){return done(null)}mount.type.syncfs(mount,populate,done)})},mount(type,opts,mountpoint){if(typeof type=="string"){throw type}var root=mountpoint==="/";var pseudo=!mountpoint;var node;if(root&&FS.root){throw new FS.ErrnoError(10)}else if(!root&&!pseudo){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});mountpoint=lookup.path;node=lookup.node;if(FS.isMountpoint(node)){throw new FS.ErrnoError(10)}if(!FS.isDir(node.mode)){throw new FS.ErrnoError(54)}}var mount={type,opts,mountpoint,mounts:[]};var mountRoot=type.mount(mount);mountRoot.mount=mount;mount.root=mountRoot;if(root){FS.root=mountRoot}else if(node){node.mounted=mount;if(node.mount){node.mount.mounts.push(mount)}}return mountRoot},unmount(mountpoint){var lookup=FS.lookupPath(mountpoint,{follow_mount:false});if(!FS.isMountpoint(lookup.node)){throw new FS.ErrnoError(28)}var node=lookup.node;var mount=node.mounted;var mounts=FS.getMounts(mount);Object.keys(FS.nameTable).forEach(hash=>{var current=FS.nameTable[hash];while(current){var next=current.name_next;if(mounts.includes(current.mount)){FS.destroyNode(current)}current=next}});node.mounted=null;var idx=node.mount.mounts.indexOf(mount);assert(idx!==-1);node.mount.mounts.splice(idx,1)},lookup(parent,name){return parent.node_ops.lookup(parent,name)},mknod(path,mode,dev){var lookup=FS.lookupPath(path,{parent:true});var parent=lookup.node;var name=PATH.basename(path);if(!name||name==="."||name===".."){throw new FS.ErrnoError(28)}var errCode=FS.mayCreate(parent,name);if(errCode){throw new FS.ErrnoError(errCode)}if(!parent.node_ops.mknod){throw new FS.ErrnoError(63)}return parent.node_ops.mknod(parent,name,mode,dev)},create(path,mode){mode=mode!==undefined?mode:438;mode&=4095;mode|=32768;return FS.mknod(path,mode,0)},mkdir(path,mode){mode=mode!==undefined?mode:511;mode&=511|512;mode|=16384;return FS.mknod(path,mode,0)},mkdirTree(path,mode){var dirs=path.split("/");var d="";for(var i=0;i=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.read){throw new FS.ErrnoError(28)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesRead=stream.stream_ops.read(stream,buffer,offset,length,position);if(!seeking)stream.position+=bytesRead;return bytesRead},write(stream,buffer,offset,length,position,canOwn){assert(offset>=0);if(length<0||position<0){throw new FS.ErrnoError(28)}if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(FS.isDir(stream.node.mode)){throw new FS.ErrnoError(31)}if(!stream.stream_ops.write){throw new FS.ErrnoError(28)}if(stream.seekable&&stream.flags&1024){FS.llseek(stream,0,2)}var seeking=typeof position!="undefined";if(!seeking){position=stream.position}else if(!stream.seekable){throw new FS.ErrnoError(70)}var bytesWritten=stream.stream_ops.write(stream,buffer,offset,length,position,canOwn);if(!seeking)stream.position+=bytesWritten;return bytesWritten},allocate(stream,offset,length){if(FS.isClosed(stream)){throw new FS.ErrnoError(8)}if(offset<0||length<=0){throw new FS.ErrnoError(28)}if((stream.flags&2097155)===0){throw new FS.ErrnoError(8)}if(!FS.isFile(stream.node.mode)&&!FS.isDir(stream.node.mode)){throw new FS.ErrnoError(43)}if(!stream.stream_ops.allocate){throw new FS.ErrnoError(138)}stream.stream_ops.allocate(stream,offset,length)},mmap(stream,length,position,prot,flags){if((prot&2)!==0&&(flags&2)===0&&(stream.flags&2097155)!==2){throw new FS.ErrnoError(2)}if((stream.flags&2097155)===1){throw new FS.ErrnoError(2)}if(!stream.stream_ops.mmap){throw new FS.ErrnoError(43)}if(!length){throw new FS.ErrnoError(28)}return stream.stream_ops.mmap(stream,length,position,prot,flags)},msync(stream,buffer,offset,length,mmapFlags){assert(offset>=0);if(!stream.stream_ops.msync){return 0}return stream.stream_ops.msync(stream,buffer,offset,length,mmapFlags)},ioctl(stream,cmd,arg){if(!stream.stream_ops.ioctl){throw new FS.ErrnoError(59)}return stream.stream_ops.ioctl(stream,cmd,arg)},readFile(path,opts={}){opts.flags=opts.flags||0;opts.encoding=opts.encoding||"binary";if(opts.encoding!=="utf8"&&opts.encoding!=="binary"){throw new Error(`Invalid encoding type "${opts.encoding}"`)}var ret;var stream=FS.open(path,opts.flags);var stat=FS.stat(path);var length=stat.size;var buf=new Uint8Array(length);FS.read(stream,buf,0,length,0);if(opts.encoding==="utf8"){ret=UTF8ArrayToString(buf)}else if(opts.encoding==="binary"){ret=buf}FS.close(stream);return ret},writeFile(path,data,opts={}){opts.flags=opts.flags||577;var stream=FS.open(path,opts.flags,opts.mode);if(typeof data=="string"){var buf=new Uint8Array(lengthBytesUTF8(data)+1);var actualNumBytes=stringToUTF8Array(data,buf,0,buf.length);FS.write(stream,buf,0,actualNumBytes,undefined,opts.canOwn)}else if(ArrayBuffer.isView(data)){FS.write(stream,data,0,data.byteLength,undefined,opts.canOwn)}else{throw new Error("Unsupported data type")}FS.close(stream)},cwd:()=>FS.currentPath,chdir(path){var lookup=FS.lookupPath(path,{follow:true});if(lookup.node===null){throw new FS.ErrnoError(44)}if(!FS.isDir(lookup.node.mode)){throw new FS.ErrnoError(54)}var errCode=FS.nodePermissions(lookup.node,"x");if(errCode){throw new FS.ErrnoError(errCode)}FS.currentPath=lookup.path},createDefaultDirectories(){FS.mkdir("/tmp");FS.mkdir("/home");FS.mkdir("/home/web_user")},createDefaultDevices(){FS.mkdir("/dev");FS.registerDevice(FS.makedev(1,3),{read:()=>0,write:(stream,buffer,offset,length,pos)=>length});FS.mkdev("/dev/null",FS.makedev(1,3));TTY.register(FS.makedev(5,0),TTY.default_tty_ops);TTY.register(FS.makedev(6,0),TTY.default_tty1_ops);FS.mkdev("/dev/tty",FS.makedev(5,0));FS.mkdev("/dev/tty1",FS.makedev(6,0));var randomBuffer=new Uint8Array(1024),randomLeft=0;var randomByte=()=>{if(randomLeft===0){randomLeft=randomFill(randomBuffer).byteLength}return randomBuffer[--randomLeft]};FS.createDevice("/dev","random",randomByte);FS.createDevice("/dev","urandom",randomByte);FS.mkdir("/dev/shm");FS.mkdir("/dev/shm/tmp")},createSpecialDirectories(){FS.mkdir("/proc");var proc_self=FS.mkdir("/proc/self");FS.mkdir("/proc/self/fd");FS.mount({mount(){var node=FS.createNode(proc_self,"fd",16384|511,73);node.node_ops={lookup(parent,name){var fd=+name;var stream=FS.getStreamChecked(fd);var ret={parent:null,mount:{mountpoint:"fake"},node_ops:{readlink:()=>stream.path}};ret.parent=ret;return ret}};return node}},{},"/proc/self/fd")},createStandardStreams(input,output,error){if(input){FS.createDevice("/dev","stdin",input)}else{FS.symlink("/dev/tty","/dev/stdin")}if(output){FS.createDevice("/dev","stdout",null,output)}else{FS.symlink("/dev/tty","/dev/stdout")}if(error){FS.createDevice("/dev","stderr",null,error)}else{FS.symlink("/dev/tty1","/dev/stderr")}var stdin=FS.open("/dev/stdin",0);var stdout=FS.open("/dev/stdout",1);var stderr=FS.open("/dev/stderr",1);assert(stdin.fd===0,`invalid handle for stdin (${stdin.fd})`);assert(stdout.fd===1,`invalid handle for stdout (${stdout.fd})`);assert(stderr.fd===2,`invalid handle for stderr (${stderr.fd})`)},staticInit(){[44].forEach(code=>{FS.genericErrors[code]=new FS.ErrnoError(code);FS.genericErrors[code].stack=""});FS.nameTable=new Array(4096);FS.mount(MEMFS,{},"/");FS.createDefaultDirectories();FS.createDefaultDevices();FS.createSpecialDirectories();FS.filesystems={MEMFS}},init(input,output,error){assert(!FS.initialized,"FS.init was previously called. If you want to initialize later with custom parameters, remove any earlier calls (note that one is automatically added to the generated code)");FS.initialized=true;input??=Module["stdin"];output??=Module["stdout"];error??=Module["stderr"];FS.createStandardStreams(input,output,error)},quit(){FS.initialized=false;_fflush(0);for(var i=0;ithis.length-1||idx<0){return undefined}var chunkOffset=idx%this.chunkSize;var chunkNum=idx/this.chunkSize|0;return this.getter(chunkNum)[chunkOffset]}setDataGetter(getter){this.getter=getter}cacheLength(){var xhr=new XMLHttpRequest;xhr.open("HEAD",url,false);xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);var datalength=Number(xhr.getResponseHeader("Content-length"));var header;var hasByteServing=(header=xhr.getResponseHeader("Accept-Ranges"))&&header==="bytes";var usesGzip=(header=xhr.getResponseHeader("Content-Encoding"))&&header==="gzip";var chunkSize=1024*1024;if(!hasByteServing)chunkSize=datalength;var doXHR=(from,to)=>{if(from>to)throw new Error("invalid range ("+from+", "+to+") or no bytes requested!");if(to>datalength-1)throw new Error("only "+datalength+" bytes available! programmer error!");var xhr=new XMLHttpRequest;xhr.open("GET",url,false);if(datalength!==chunkSize)xhr.setRequestHeader("Range","bytes="+from+"-"+to);xhr.responseType="arraybuffer";if(xhr.overrideMimeType){xhr.overrideMimeType("text/plain; charset=x-user-defined")}xhr.send(null);if(!(xhr.status>=200&&xhr.status<300||xhr.status===304))throw new Error("Couldn't load "+url+". Status: "+xhr.status);if(xhr.response!==undefined){return new Uint8Array(xhr.response||[])}return intArrayFromString(xhr.responseText||"",true)};var lazyArray=this;lazyArray.setDataGetter(chunkNum=>{var start=chunkNum*chunkSize;var end=(chunkNum+1)*chunkSize-1;end=Math.min(end,datalength-1);if(typeof lazyArray.chunks[chunkNum]=="undefined"){lazyArray.chunks[chunkNum]=doXHR(start,end)}if(typeof lazyArray.chunks[chunkNum]=="undefined")throw new Error("doXHR failed!");return lazyArray.chunks[chunkNum]});if(usesGzip||!datalength){chunkSize=datalength=1;datalength=this.getter(0).length;chunkSize=datalength;out("LazyFiles on gzip forces download of the whole file when length is accessed")}this._length=datalength;this._chunkSize=chunkSize;this.lengthKnown=true}get length(){if(!this.lengthKnown){this.cacheLength()}return this._length}get chunkSize(){if(!this.lengthKnown){this.cacheLength()}return this._chunkSize}}if(typeof XMLHttpRequest!="undefined"){if(!ENVIRONMENT_IS_WORKER)throw"Cannot do synchronous binary XHRs outside webworkers in modern browsers. Use --embed-file or --preload-file in emcc";var lazyArray=new LazyUint8Array;var properties={isDevice:false,contents:lazyArray}}else{var properties={isDevice:false,url}}var node=FS.createFile(parent,name,properties,canRead,canWrite);if(properties.contents){node.contents=properties.contents}else if(properties.url){node.contents=null;node.url=properties.url}Object.defineProperties(node,{usedBytes:{get:function(){return this.contents.length}}});var stream_ops={};var keys=Object.keys(node.stream_ops);keys.forEach(key=>{var fn=node.stream_ops[key];stream_ops[key]=(...args)=>{FS.forceLoadFile(node);return fn(...args)}});function writeChunks(stream,buffer,offset,length,position){var contents=stream.node.contents;if(position>=contents.length)return 0;var size=Math.min(contents.length-position,length);assert(size>=0);if(contents.slice){for(var i=0;i{FS.forceLoadFile(node);return writeChunks(stream,buffer,offset,length,position)};stream_ops.mmap=(stream,length,position,prot,flags)=>{FS.forceLoadFile(node);var ptr=mmapAlloc(length);if(!ptr){throw new FS.ErrnoError(48)}writeChunks(stream,HEAP8,ptr,length,position);return{ptr,allocated:true}};node.stream_ops=stream_ops;return node},absolutePath(){abort("FS.absolutePath has been removed; use PATH_FS.resolve instead")},createFolder(){abort("FS.createFolder has been removed; use FS.mkdir instead")},createLink(){abort("FS.createLink has been removed; use FS.symlink instead")},joinPath(){abort("FS.joinPath has been removed; use PATH.join instead")},mmapAlloc(){abort("FS.mmapAlloc has been replaced by the top level function mmapAlloc")},standardizePath(){abort("FS.standardizePath has been removed; use PATH.normalize instead")}};var SYSCALLS={DEFAULT_POLLMASK:5,calculateAt(dirfd,path,allowEmpty){if(PATH.isAbs(path)){return path}var dir;if(dirfd===-100){dir=FS.cwd()}else{var dirstream=SYSCALLS.getStreamFromFD(dirfd);dir=dirstream.path}if(path.length==0){if(!allowEmpty){throw new FS.ErrnoError(44)}return dir}return PATH.join2(dir,path)},doStat(func,path,buf){var stat=func(path);HEAP32[buf>>2]=stat.dev;checkInt32(stat.dev);HEAP32[buf+4>>2]=stat.mode;checkInt32(stat.mode);HEAPU32[buf+8>>2]=stat.nlink;checkInt32(stat.nlink);HEAP32[buf+12>>2]=stat.uid;checkInt32(stat.uid);HEAP32[buf+16>>2]=stat.gid;checkInt32(stat.gid);HEAP32[buf+20>>2]=stat.rdev;checkInt32(stat.rdev);tempI64=[stat.size>>>0,(tempDouble=stat.size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+24>>2]=tempI64[0],HEAP32[buf+28>>2]=tempI64[1];checkInt64(stat.size);HEAP32[buf+32>>2]=4096;checkInt32(4096);HEAP32[buf+36>>2]=stat.blocks;checkInt32(stat.blocks);var atime=stat.atime.getTime();var mtime=stat.mtime.getTime();var ctime=stat.ctime.getTime();tempI64=[Math.floor(atime/1e3)>>>0,(tempDouble=Math.floor(atime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+40>>2]=tempI64[0],HEAP32[buf+44>>2]=tempI64[1];checkInt64(Math.floor(atime/1e3));HEAPU32[buf+48>>2]=atime%1e3*1e3*1e3;checkInt32(atime%1e3*1e3*1e3);tempI64=[Math.floor(mtime/1e3)>>>0,(tempDouble=Math.floor(mtime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+56>>2]=tempI64[0],HEAP32[buf+60>>2]=tempI64[1];checkInt64(Math.floor(mtime/1e3));HEAPU32[buf+64>>2]=mtime%1e3*1e3*1e3;checkInt32(mtime%1e3*1e3*1e3);tempI64=[Math.floor(ctime/1e3)>>>0,(tempDouble=Math.floor(ctime/1e3),+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+72>>2]=tempI64[0],HEAP32[buf+76>>2]=tempI64[1];checkInt64(Math.floor(ctime/1e3));HEAPU32[buf+80>>2]=ctime%1e3*1e3*1e3;checkInt32(ctime%1e3*1e3*1e3);tempI64=[stat.ino>>>0,(tempDouble=stat.ino,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[buf+88>>2]=tempI64[0],HEAP32[buf+92>>2]=tempI64[1];checkInt64(stat.ino);return 0},doMsync(addr,stream,len,flags,offset){if(!FS.isFile(stream.node.mode)){throw new FS.ErrnoError(43)}if(flags&2){return 0}var buffer=HEAPU8.slice(addr,addr+len);FS.msync(stream,buffer,offset,len,flags)},getStreamFromFD(fd){var stream=FS.getStreamChecked(fd);return stream},varargs:undefined,getStr(ptr){var ret=UTF8ToString(ptr);return ret}};function ___syscall_faccessat(dirfd,path,amode,flags){try{path=SYSCALLS.getStr(path);assert(flags===0||flags==512);path=SYSCALLS.calculateAt(dirfd,path);if(amode&~7){return-28}var lookup=FS.lookupPath(path,{follow:true});var node=lookup.node;if(!node){return-44}var perms="";if(amode&4)perms+="r";if(amode&2)perms+="w";if(amode&1)perms+="x";if(perms&&FS.nodePermissions(node,perms)){return-2}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function syscallGetVarargI(){assert(SYSCALLS.varargs!=undefined);var ret=HEAP32[+SYSCALLS.varargs>>2];SYSCALLS.varargs+=4;return ret}var syscallGetVarargP=syscallGetVarargI;function ___syscall_fcntl64(fd,cmd,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(cmd){case 0:{var arg=syscallGetVarargI();if(arg<0){return-28}while(FS.streams[arg]){arg++}var newStream;newStream=FS.dupStream(stream,arg);return newStream.fd}case 1:case 2:return 0;case 3:return stream.flags;case 4:{var arg=syscallGetVarargI();stream.flags|=arg;return 0}case 12:{var arg=syscallGetVarargP();var offset=0;HEAP16[arg+offset>>1]=2;checkInt16(2);return 0}case 13:case 14:return 0}return-28}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_fstat64(fd,buf){try{var stream=SYSCALLS.getStreamFromFD(fd);return SYSCALLS.doStat(FS.stat,stream.path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var stringToUTF8=(str,outPtr,maxBytesToWrite)=>{assert(typeof maxBytesToWrite=="number","stringToUTF8(str, outPtr, maxBytesToWrite) is missing the third parameter that specifies the length of the output buffer!");return stringToUTF8Array(str,HEAPU8,outPtr,maxBytesToWrite)};function ___syscall_getdents64(fd,dirp,count){try{var stream=SYSCALLS.getStreamFromFD(fd);stream.getdents||=FS.readdir(stream.path);var struct_size=280;var pos=0;var off=FS.llseek(stream,0,1);var idx=Math.floor(off/struct_size);while(idx>>0,(tempDouble=id,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos>>2]=tempI64[0],HEAP32[dirp+pos+4>>2]=tempI64[1];checkInt64(id);tempI64=[(idx+1)*struct_size>>>0,(tempDouble=(idx+1)*struct_size,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[dirp+pos+8>>2]=tempI64[0],HEAP32[dirp+pos+12>>2]=tempI64[1];checkInt64((idx+1)*struct_size);HEAP16[dirp+pos+16>>1]=280;checkInt16(280);HEAP8[dirp+pos+18]=type;checkInt8(type);stringToUTF8(name,dirp+pos+19,256);pos+=struct_size;idx+=1}FS.llseek(stream,idx*struct_size,0);return pos}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_ioctl(fd,op,varargs){SYSCALLS.varargs=varargs;try{var stream=SYSCALLS.getStreamFromFD(fd);switch(op){case 21509:{if(!stream.tty)return-59;return 0}case 21505:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcgets){var termios=stream.tty.ops.ioctl_tcgets(stream);var argp=syscallGetVarargP();HEAP32[argp>>2]=termios.c_iflag||0;checkInt32(termios.c_iflag||0);HEAP32[argp+4>>2]=termios.c_oflag||0;checkInt32(termios.c_oflag||0);HEAP32[argp+8>>2]=termios.c_cflag||0;checkInt32(termios.c_cflag||0);HEAP32[argp+12>>2]=termios.c_lflag||0;checkInt32(termios.c_lflag||0);for(var i=0;i<32;i++){HEAP8[argp+i+17]=termios.c_cc[i]||0;checkInt8(termios.c_cc[i]||0)}return 0}return 0}case 21510:case 21511:case 21512:{if(!stream.tty)return-59;return 0}case 21506:case 21507:case 21508:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tcsets){var argp=syscallGetVarargP();var c_iflag=HEAP32[argp>>2];var c_oflag=HEAP32[argp+4>>2];var c_cflag=HEAP32[argp+8>>2];var c_lflag=HEAP32[argp+12>>2];var c_cc=[];for(var i=0;i<32;i++){c_cc.push(HEAP8[argp+i+17])}return stream.tty.ops.ioctl_tcsets(stream.tty,op,{c_iflag,c_oflag,c_cflag,c_lflag,c_cc})}return 0}case 21519:{if(!stream.tty)return-59;var argp=syscallGetVarargP();HEAP32[argp>>2]=0;checkInt32(0);return 0}case 21520:{if(!stream.tty)return-59;return-28}case 21531:{var argp=syscallGetVarargP();return FS.ioctl(stream,op,argp)}case 21523:{if(!stream.tty)return-59;if(stream.tty.ops.ioctl_tiocgwinsz){var winsize=stream.tty.ops.ioctl_tiocgwinsz(stream.tty);var argp=syscallGetVarargP();HEAP16[argp>>1]=winsize[0];checkInt16(winsize[0]);HEAP16[argp+2>>1]=winsize[1];checkInt16(winsize[1])}return 0}case 21524:{if(!stream.tty)return-59;return 0}case 21515:{if(!stream.tty)return-59;return 0}default:return-28}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_lstat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.lstat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_mkdirat(dirfd,path,mode){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);path=PATH.normalize(path);if(path[path.length-1]==="/")path=path.substr(0,path.length-1);FS.mkdir(path,mode,0);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_newfstatat(dirfd,path,buf,flags){try{path=SYSCALLS.getStr(path);var nofollow=flags&256;var allowEmpty=flags&4096;flags=flags&~6400;assert(!flags,`unknown flags in __syscall_newfstatat: ${flags}`);path=SYSCALLS.calculateAt(dirfd,path,allowEmpty);return SYSCALLS.doStat(nofollow?FS.lstat:FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_openat(dirfd,path,flags,varargs){SYSCALLS.varargs=varargs;try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);var mode=varargs?syscallGetVarargI():0;return FS.open(path,flags,mode).fd}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_rmdir(path){try{path=SYSCALLS.getStr(path);FS.rmdir(path);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_stat64(path,buf){try{path=SYSCALLS.getStr(path);return SYSCALLS.doStat(FS.stat,path,buf)}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function ___syscall_unlinkat(dirfd,path,flags){try{path=SYSCALLS.getStr(path);path=SYSCALLS.calculateAt(dirfd,path);if(flags===0){FS.unlink(path)}else if(flags===512){FS.rmdir(path)}else{abort("Invalid flags passed to unlinkat")}return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __abort_js=()=>{abort("native code called abort()")};var nowIsMonotonic=1;var __emscripten_get_now_is_monotonic=()=>nowIsMonotonic;var __emscripten_memcpy_js=(dest,src,num)=>HEAPU8.copyWithin(dest,src,src+num);var __emscripten_runtime_keepalive_clear=()=>{noExitRuntime=false;runtimeKeepaliveCounter=0};var __emscripten_throw_longjmp=()=>{throw Infinity};var convertI32PairToI53Checked=(lo,hi)=>{assert(lo==lo>>>0||lo==(lo|0));assert(hi===(hi|0));return hi+2097152>>>0<4194305-!!lo?(lo>>>0)+hi*4294967296:NaN};function __mmap_js(len,prot,flags,fd,offset_low,offset_high,allocated,addr){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);var res=FS.mmap(stream,len,offset,prot,flags);var ptr=res.ptr;HEAP32[allocated>>2]=res.allocated;checkInt32(res.allocated);HEAPU32[addr>>2]=ptr;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}function __munmap_js(addr,len,prot,flags,fd,offset_low,offset_high){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{var stream=SYSCALLS.getStreamFromFD(fd);if(prot&2){SYSCALLS.doMsync(addr,stream,len,flags,offset)}}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return-e.errno}}var __tzset_js=(timezone,daylight,std_name,dst_name)=>{var currentYear=(new Date).getFullYear();var winter=new Date(currentYear,0,1);var summer=new Date(currentYear,6,1);var winterOffset=winter.getTimezoneOffset();var summerOffset=summer.getTimezoneOffset();var stdTimezoneOffset=Math.max(winterOffset,summerOffset);HEAPU32[timezone>>2]=stdTimezoneOffset*60;checkInt32(stdTimezoneOffset*60);HEAP32[daylight>>2]=Number(winterOffset!=summerOffset);checkInt32(Number(winterOffset!=summerOffset));var extractZone=timezoneOffset=>{var sign=timezoneOffset>=0?"-":"+";var absOffset=Math.abs(timezoneOffset);var hours=String(Math.floor(absOffset/60)).padStart(2,"0");var minutes=String(absOffset%60).padStart(2,"0");return`UTC${sign}${hours}${minutes}`};var winterName=extractZone(winterOffset);var summerName=extractZone(summerOffset);assert(winterName);assert(summerName);assert(lengthBytesUTF8(winterName)<=16,`timezone name truncated to fit in TZNAME_MAX (${winterName})`);assert(lengthBytesUTF8(summerName)<=16,`timezone name truncated to fit in TZNAME_MAX (${summerName})`);if(summerOffsetDate.now();var getHeapMax=()=>2147483648;var _emscripten_get_heap_max=()=>getHeapMax();var _emscripten_get_now=()=>performance.now();var growMemory=size=>{var b=wasmMemory.buffer;var pages=(size-b.byteLength+65535)/65536|0;try{wasmMemory.grow(pages);updateMemoryViews();return 1}catch(e){err(`growMemory: Attempted to grow heap from ${b.byteLength} bytes to ${size} bytes, but got error: ${e}`)}};var _emscripten_resize_heap=requestedSize=>{var oldSize=HEAPU8.length;requestedSize>>>=0;assert(requestedSize>oldSize);var maxHeapSize=getHeapMax();if(requestedSize>maxHeapSize){err(`Cannot enlarge memory, requested ${requestedSize} bytes, but the limit is ${maxHeapSize} bytes!`);return false}for(var cutDown=1;cutDown<=4;cutDown*=2){var overGrownHeapSize=oldSize*(1+.2/cutDown);overGrownHeapSize=Math.min(overGrownHeapSize,requestedSize+100663296);var newSize=Math.min(maxHeapSize,alignMemory(Math.max(requestedSize,overGrownHeapSize),65536));var t0=_emscripten_get_now();var replacement=growMemory(newSize);var t1=_emscripten_get_now();dbg(`Heap resize call from ${oldSize} to ${newSize} took ${t1-t0} msecs. Success: ${!!replacement}`);if(replacement){return true}}err(`Failed to grow the heap from ${oldSize} bytes to ${newSize} bytes, not enough memory!`);return false};var ENV={};var getExecutableName=()=>thisProgram||"./this.program";var getEnvStrings=()=>{if(!getEnvStrings.strings){var lang=(typeof navigator=="object"&&navigator.languages&&navigator.languages[0]||"C").replace("-","_")+".UTF-8";var env={USER:"web_user",LOGNAME:"web_user",PATH:"/",PWD:"/",HOME:"/home/web_user",LANG:lang,_:getExecutableName()};for(var x in ENV){if(ENV[x]===undefined)delete env[x];else env[x]=ENV[x]}var strings=[];for(var x in env){strings.push(`${x}=${env[x]}`)}getEnvStrings.strings=strings}return getEnvStrings.strings};var stringToAscii=(str,buffer)=>{for(var i=0;i{var bufSize=0;getEnvStrings().forEach((string,i)=>{var ptr=environ_buf+bufSize;HEAPU32[__environ+i*4>>2]=ptr;checkInt32(ptr);stringToAscii(string,ptr);bufSize+=string.length+1});return 0};var _environ_sizes_get=(penviron_count,penviron_buf_size)=>{var strings=getEnvStrings();HEAPU32[penviron_count>>2]=strings.length;checkInt32(strings.length);var bufSize=0;strings.forEach(string=>bufSize+=string.length+1);HEAPU32[penviron_buf_size>>2]=bufSize;checkInt32(bufSize);return 0};function _fd_close(fd){try{var stream=SYSCALLS.getStreamFromFD(fd);FS.close(stream);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_fdstat_get(fd,pbuf){try{var rightsBase=0;var rightsInheriting=0;var flags=0;{var stream=SYSCALLS.getStreamFromFD(fd);var type=stream.tty?2:FS.isDir(stream.mode)?3:FS.isLink(stream.mode)?7:4}HEAP8[pbuf]=type;checkInt8(type);HEAP16[pbuf+2>>1]=flags;checkInt16(flags);tempI64=[rightsBase>>>0,(tempDouble=rightsBase,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+8>>2]=tempI64[0],HEAP32[pbuf+12>>2]=tempI64[1];checkInt64(rightsBase);tempI64=[rightsInheriting>>>0,(tempDouble=rightsInheriting,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[pbuf+16>>2]=tempI64[0],HEAP32[pbuf+20>>2]=tempI64[1];checkInt64(rightsInheriting);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doReadv=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.read(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}function _fd_seek(fd,offset_low,offset_high,whence,newOffset){var offset=convertI32PairToI53Checked(offset_low,offset_high);try{if(isNaN(offset))return 61;var stream=SYSCALLS.getStreamFromFD(fd);FS.llseek(stream,offset,whence);tempI64=[stream.position>>>0,(tempDouble=stream.position,+Math.abs(tempDouble)>=1?tempDouble>0?+Math.floor(tempDouble/4294967296)>>>0:~~+Math.ceil((tempDouble-+(~~tempDouble>>>0))/4294967296)>>>0:0)],HEAP32[newOffset>>2]=tempI64[0],HEAP32[newOffset+4>>2]=tempI64[1];checkInt64(stream.position);if(stream.getdents&&offset===0&&whence===0)stream.getdents=null;return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var doWritev=(stream,iov,iovcnt,offset)=>{var ret=0;for(var i=0;i>2];var len=HEAPU32[iov+4>>2];iov+=8;var curr=FS.write(stream,HEAP8,ptr,len,offset);if(curr<0)return-1;ret+=curr;if(curr>2]=num;checkInt32(num);return 0}catch(e){if(typeof FS=="undefined"||!(e.name==="ErrnoError"))throw e;return e.errno}}var _getentropy=(buffer,size)=>{randomFill(HEAPU8.subarray(buffer,buffer+size));return 0};var _llvm_eh_typeid_for=type=>type;var runtimeKeepaliveCounter=0;var keepRuntimeAlive=()=>noExitRuntime||runtimeKeepaliveCounter>0;var _proc_exit=code=>{EXITSTATUS=code;if(!keepRuntimeAlive()){Module["onExit"]?.(code);ABORT=true}quit_(code,new ExitStatus(code))};var getCFunc=ident=>{var func=Module["_"+ident];assert(func,"Cannot call unknown function "+ident+", make sure it is exported");return func};var writeArrayToMemory=(array,buffer)=>{assert(array.length>=0,"writeArrayToMemory array must have a length (should be an array or typed array)");HEAP8.set(array,buffer)};var stackAlloc=sz=>__emscripten_stack_alloc(sz);var stringToUTF8OnStack=str=>{var size=lengthBytesUTF8(str)+1;var ret=stackAlloc(size);stringToUTF8(str,ret,size);return ret};var ccall=(ident,returnType,argTypes,args,opts)=>{var toC={string:str=>{var ret=0;if(str!==null&&str!==undefined&&str!==0){ret=stringToUTF8OnStack(str)}return ret},array:arr=>{var ret=stackAlloc(arr.length);writeArrayToMemory(arr,ret);return ret}};function convertReturnValue(ret){if(returnType==="string"){return UTF8ToString(ret)}if(returnType==="boolean")return Boolean(ret);return ret}var func=getCFunc(ident);var cArgs=[];var stack=0;assert(returnType!=="array",'Return type should not be "array".');if(args){for(var i=0;i(...args)=>ccall(ident,returnType,argTypes,args,opts);FS.createPreloadedFile=FS_createPreloadedFile;FS.staticInit();function checkIncomingModuleAPI(){ignoredModuleProp("fetchSettings")}var wasmImports={__assert_fail:___assert_fail,__call_sighandler:___call_sighandler,__cxa_begin_catch:___cxa_begin_catch,__cxa_end_catch:___cxa_end_catch,__cxa_find_matching_catch_2:___cxa_find_matching_catch_2,__cxa_find_matching_catch_3:___cxa_find_matching_catch_3,__cxa_rethrow:___cxa_rethrow,__cxa_throw:___cxa_throw,__handle_stack_overflow:___handle_stack_overflow,__resumeException:___resumeException,__syscall_faccessat:___syscall_faccessat,__syscall_fcntl64:___syscall_fcntl64,__syscall_fstat64:___syscall_fstat64,__syscall_getdents64:___syscall_getdents64,__syscall_ioctl:___syscall_ioctl,__syscall_lstat64:___syscall_lstat64,__syscall_mkdirat:___syscall_mkdirat,__syscall_newfstatat:___syscall_newfstatat,__syscall_openat:___syscall_openat,__syscall_rmdir:___syscall_rmdir,__syscall_stat64:___syscall_stat64,__syscall_unlinkat:___syscall_unlinkat,_abort_js:__abort_js,_emscripten_get_now_is_monotonic:__emscripten_get_now_is_monotonic,_emscripten_memcpy_js:__emscripten_memcpy_js,_emscripten_runtime_keepalive_clear:__emscripten_runtime_keepalive_clear,_emscripten_throw_longjmp:__emscripten_throw_longjmp,_mmap_js:__mmap_js,_munmap_js:__munmap_js,_tzset_js:__tzset_js,emscripten_date_now:_emscripten_date_now,emscripten_get_heap_max:_emscripten_get_heap_max,emscripten_get_now:_emscripten_get_now,emscripten_resize_heap:_emscripten_resize_heap,environ_get:_environ_get,environ_sizes_get:_environ_sizes_get,fd_close:_fd_close,fd_fdstat_get:_fd_fdstat_get,fd_read:_fd_read,fd_seek:_fd_seek,fd_write:_fd_write,getentropy:_getentropy,invoke_d,invoke_diii,invoke_fi,invoke_fif,invoke_fifff,invoke_fii,invoke_fiii,invoke_fiiii,invoke_fiiiiii,invoke_fiiiiiii,invoke_fij,invoke_i,invoke_idiii,invoke_ii,invoke_iif,invoke_iifi,invoke_iifii,invoke_iifiiiii,invoke_iii,invoke_iiid,invoke_iiidii,invoke_iiif,invoke_iiiffi,invoke_iiii,invoke_iiiidi,invoke_iiiiffifffffffi,invoke_iiiifiii,invoke_iiiii,invoke_iiiiif,invoke_iiiiifi,invoke_iiiiii,invoke_iiiiiifi,invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii,invoke_iiiiiii,invoke_iiiiiiifii,invoke_iiiiiiii,invoke_iiiiiiiii,invoke_iiiiiiiiiffi,invoke_iiiiiiiiii,invoke_iiiiiiiiiifi,invoke_iiiiiiiiiii,invoke_iiiiiiiiiiii,invoke_iiiiiiiiiiiif,invoke_iiiiiiiiiiiiiii,invoke_iiiiiiij,invoke_iiiiiij,invoke_iiiiij,invoke_iiiij,invoke_iiij,invoke_iiiji,invoke_iiijii,invoke_iiijiii,invoke_iij,invoke_iijiii,invoke_iijj,invoke_iijji,invoke_iijjiii,invoke_ij,invoke_ijjiii,invoke_j,invoke_ji,invoke_jii,invoke_jiii,invoke_jiji,invoke_v,invoke_vdiii,invoke_vi,invoke_vid,invoke_vif,invoke_vififiif,invoke_vifii,invoke_vifiiifiiiiiiiiiiiiifiiii,invoke_vii,invoke_viid,invoke_viif,invoke_viiff,invoke_viifi,invoke_viifiii,invoke_viii,invoke_viiid,invoke_viiif,invoke_viiiffii,invoke_viiiffiiii,invoke_viiifi,invoke_viiii,invoke_viiiif,invoke_viiiii,invoke_viiiiif,invoke_viiiiii,invoke_viiiiiid,invoke_viiiiiif,invoke_viiiiiifi,invoke_viiiiiifif,invoke_viiiiiifii,invoke_viiiiiii,invoke_viiiiiiidiiii,invoke_viiiiiiii,invoke_viiiiiiiif,invoke_viiiiiiiii,invoke_viiiiiiiiii,invoke_viiiiiiiiiidii,invoke_viiiiiiiiiii,invoke_viiiiiiiiiiii,invoke_viiiiiiiiiiiiii,invoke_viiiiiiiiiiiiiiii,invoke_viiiiij,invoke_viiiiiji,invoke_viiiiji,invoke_viiiijiiifi,invoke_viiij,invoke_viiiji,invoke_viiijii,invoke_viiijjji,invoke_viij,invoke_viiji,invoke_viijii,invoke_viijj,invoke_vij,invoke_viji,invoke_vijif,invoke_vijii,invoke_vijj,invoke_vjjiii,llvm_eh_typeid_for:_llvm_eh_typeid_for,proc_exit:_proc_exit};var wasmExports=createWasm();var ___wasm_call_ctors=createExportWrapper("__wasm_call_ctors",0);var _web_engine_create=Module["_web_engine_create"]=createExportWrapper("web_engine_create",0);var _web_engine_get_memory_stats=Module["_web_engine_get_memory_stats"]=createExportWrapper("web_engine_get_memory_stats",1);var _web_engine_destroy=Module["_web_engine_destroy"]=createExportWrapper("web_engine_destroy",1);var _fflush=createExportWrapper("fflush",1);var _web_engine_get_live_handles=Module["_web_engine_get_live_handles"]=createExportWrapper("web_engine_get_live_handles",0);var _web_engine_get_allocated_bytes=Module["_web_engine_get_allocated_bytes"]=createExportWrapper("web_engine_get_allocated_bytes",0);var _web_engine_open_blend=Module["_web_engine_open_blend"]=createExportWrapper("web_engine_open_blend",3);var _web_engine_apply_command=Module["_web_engine_apply_command"]=createExportWrapper("web_engine_apply_command",3);var _web_engine_decimate_apply=Module["_web_engine_decimate_apply"]=createExportWrapper("web_engine_decimate_apply",35);var _web_engine_append_library_object=Module["_web_engine_append_library_object"]=createExportWrapper("web_engine_append_library_object",5);var _web_engine_undo=Module["_web_engine_undo"]=createExportWrapper("web_engine_undo",1);var _web_engine_redo=Module["_web_engine_redo"]=createExportWrapper("web_engine_redo",1);var _web_engine_get_scene_snapshot=Module["_web_engine_get_scene_snapshot"]=createExportWrapper("web_engine_get_scene_snapshot",3);var _web_engine_get_scene_metadata=Module["_web_engine_get_scene_metadata"]=createExportWrapper("web_engine_get_scene_metadata",3);var _web_engine_get_scene_geometry=Module["_web_engine_get_scene_geometry"]=createExportWrapper("web_engine_get_scene_geometry",3);var _web_engine_get_scene_delta=Module["_web_engine_get_scene_delta"]=createExportWrapper("web_engine_get_scene_delta",3);var _web_engine_get_packed_asset=Module["_web_engine_get_packed_asset"]=createExportWrapper("web_engine_get_packed_asset",5);var _web_engine_evaluate_depsgraph=Module["_web_engine_evaluate_depsgraph"]=createExportWrapper("web_engine_evaluate_depsgraph",3);var _web_engine_save_blend=Module["_web_engine_save_blend"]=createExportWrapper("web_engine_save_blend",3);var _malloc=Module["_malloc"]=createExportWrapper("malloc",1);var _web_engine_free_buffer=Module["_web_engine_free_buffer"]=createExportWrapper("web_engine_free_buffer",1);var _free=Module["_free"]=createExportWrapper("free",1);var _web_engine_last_error_code=Module["_web_engine_last_error_code"]=createExportWrapper("web_engine_last_error_code",0);var _web_engine_last_error_message=Module["_web_engine_last_error_message"]=createExportWrapper("web_engine_last_error_message",0);var _strerror=createExportWrapper("strerror",1);var _emscripten_builtin_memalign=createExportWrapper("emscripten_builtin_memalign",2);var _setThrew=createExportWrapper("setThrew",2);var __emscripten_tempret_set=createExportWrapper("_emscripten_tempret_set",1);var __emscripten_tempret_get=createExportWrapper("_emscripten_tempret_get",0);var _emscripten_stack_init=()=>(_emscripten_stack_init=wasmExports["emscripten_stack_init"])();var _emscripten_stack_get_free=()=>(_emscripten_stack_get_free=wasmExports["emscripten_stack_get_free"])();var _emscripten_stack_get_base=()=>(_emscripten_stack_get_base=wasmExports["emscripten_stack_get_base"])();var _emscripten_stack_get_end=()=>(_emscripten_stack_get_end=wasmExports["emscripten_stack_get_end"])();var __emscripten_stack_restore=a0=>(__emscripten_stack_restore=wasmExports["_emscripten_stack_restore"])(a0);var __emscripten_stack_alloc=a0=>(__emscripten_stack_alloc=wasmExports["_emscripten_stack_alloc"])(a0);var _emscripten_stack_get_current=()=>(_emscripten_stack_get_current=wasmExports["emscripten_stack_get_current"])();var ___cxa_decrement_exception_refcount=createExportWrapper("__cxa_decrement_exception_refcount",1);var ___cxa_increment_exception_refcount=createExportWrapper("__cxa_increment_exception_refcount",1);var ___cxa_can_catch=createExportWrapper("__cxa_can_catch",3);var ___cxa_get_exception_ptr=createExportWrapper("__cxa_get_exception_ptr",1);var ___set_stack_limits=Module["___set_stack_limits"]=createExportWrapper("__set_stack_limits",2);var dynCall_viij=Module["dynCall_viij"]=createExportWrapper("dynCall_viij",5);var dynCall_viiji=Module["dynCall_viiji"]=createExportWrapper("dynCall_viiji",6);var dynCall_viijii=Module["dynCall_viijii"]=createExportWrapper("dynCall_viijii",7);var dynCall_vij=Module["dynCall_vij"]=createExportWrapper("dynCall_vij",4);var dynCall_viiiijiiifi=Module["dynCall_viiiijiiifi"]=createExportWrapper("dynCall_viiiijiiifi",12);var dynCall_viiijjji=Module["dynCall_viiijjji"]=createExportWrapper("dynCall_viiijjji",11);var dynCall_viiiiji=Module["dynCall_viiiiji"]=createExportWrapper("dynCall_viiiiji",8);var dynCall_viiiji=Module["dynCall_viiiji"]=createExportWrapper("dynCall_viiiji",7);var dynCall_ij=Module["dynCall_ij"]=createExportWrapper("dynCall_ij",3);var dynCall_viji=Module["dynCall_viji"]=createExportWrapper("dynCall_viji",5);var dynCall_iij=Module["dynCall_iij"]=createExportWrapper("dynCall_iij",4);var dynCall_fij=Module["dynCall_fij"]=createExportWrapper("dynCall_fij",4);var dynCall_vijf=Module["dynCall_vijf"]=createExportWrapper("dynCall_vijf",5);var dynCall_jiii=Module["dynCall_jiii"]=createExportWrapper("dynCall_jiii",4);var dynCall_viiij=Module["dynCall_viiij"]=createExportWrapper("dynCall_viiij",6);var dynCall_iiijii=Module["dynCall_iiijii"]=createExportWrapper("dynCall_iiijii",7);var dynCall_iiijiii=Module["dynCall_iiijiii"]=createExportWrapper("dynCall_iiijiii",8);var dynCall_iiij=Module["dynCall_iiij"]=createExportWrapper("dynCall_iiij",5);var dynCall_vjjiii=Module["dynCall_vjjiii"]=createExportWrapper("dynCall_vjjiii",8);var dynCall_ijjiii=Module["dynCall_ijjiii"]=createExportWrapper("dynCall_ijjiii",8);var dynCall_iijiii=Module["dynCall_iijiii"]=createExportWrapper("dynCall_iijiii",7);var dynCall_iijjiii=Module["dynCall_iijjiii"]=createExportWrapper("dynCall_iijjiii",9);var dynCall_iiiji=Module["dynCall_iiiji"]=createExportWrapper("dynCall_iiiji",6);var dynCall_jii=Module["dynCall_jii"]=createExportWrapper("dynCall_jii",3);var dynCall_iijj=Module["dynCall_iijj"]=createExportWrapper("dynCall_iijj",6);var dynCall_vijj=Module["dynCall_vijj"]=createExportWrapper("dynCall_vijj",6);var dynCall_ji=Module["dynCall_ji"]=createExportWrapper("dynCall_ji",2);var dynCall_vijii=Module["dynCall_vijii"]=createExportWrapper("dynCall_vijii",6);var dynCall_vijif=Module["dynCall_vijif"]=createExportWrapper("dynCall_vijif",6);var dynCall_viiiiij=Module["dynCall_viiiiij"]=createExportWrapper("dynCall_viiiiij",8);var dynCall_viiiiiji=Module["dynCall_viiiiiji"]=createExportWrapper("dynCall_viiiiiji",9);var dynCall_iiiij=Module["dynCall_iiiij"]=createExportWrapper("dynCall_iiiij",6);var dynCall_iijji=Module["dynCall_iijji"]=createExportWrapper("dynCall_iijji",7);var dynCall_j=Module["dynCall_j"]=createExportWrapper("dynCall_j",1);var dynCall_iiiiiiij=Module["dynCall_iiiiiiij"]=createExportWrapper("dynCall_iiiiiiij",9);var dynCall_viijj=Module["dynCall_viijj"]=createExportWrapper("dynCall_viijj",7);var dynCall_iiiiij=Module["dynCall_iiiiij"]=createExportWrapper("dynCall_iiiiij",7);var dynCall_viiijii=Module["dynCall_viiijii"]=createExportWrapper("dynCall_viiijii",8);var dynCall_jij=Module["dynCall_jij"]=createExportWrapper("dynCall_jij",4);var dynCall_vijji=Module["dynCall_vijji"]=createExportWrapper("dynCall_vijji",7);var dynCall_iiiiiij=Module["dynCall_iiiiiij"]=createExportWrapper("dynCall_iiiiiij",8);var dynCall_jiji=Module["dynCall_jiji"]=createExportWrapper("dynCall_jiji",5);var dynCall_iiiiijj=Module["dynCall_iiiiijj"]=createExportWrapper("dynCall_iiiiijj",9);var dynCall_iiiiiijj=Module["dynCall_iiiiiijj"]=createExportWrapper("dynCall_iiiiiijj",10);function invoke_ii(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_v(index){var sp=stackSave();try{getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viii(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vi(index,a1){var sp=stackSave();try{getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vif(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fi(index,a1){var sp=stackSave();try{return getWasmTableEntry(index)(a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiif(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vii(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_i(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_diii(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiffi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiffifffffffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiifi(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiififiiiififiiiiiiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24,a25,a26,a27,a28,a29,a30,a31,a32,a33,a34,a35)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vififiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viif(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viifi(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiif(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fii(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifi(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vdiii(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_idiii(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiif(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiifiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_d(index){var sp=stackSave();try{return getWasmTableEntry(index)()}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fifff(index,a1,a2,a3,a4){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiifi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fiiiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiifi(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vid(index,a1,a2){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiff(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iif(index,a1,a2){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiffi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iifiiiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiifif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiif(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vifiiifiiiiiiiiiiiiifiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16,a17,a18,a19,a20,a21,a22,a23,a24)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiif(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiid(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiidiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiidii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiidi(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viid(index,a1,a2,a3){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14,a15,a16)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiiiiiiiiii(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11,a12,a13,a14)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiidii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiid(index,a1,a2,a3,a4){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiffii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{getWasmTableEntry(index)(a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiid(index,a1,a2,a3){var sp=stackSave();try{return getWasmTableEntry(index)(a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_fij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_fij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viij(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iij(index,a1,a2,a3){var sp=stackSave();try{return dynCall_iij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ij(index,a1,a2){var sp=stackSave();try{return dynCall_ij(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vij(index,a1,a2,a3){var sp=stackSave();try{dynCall_vij(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiii(index,a1,a2,a3){var sp=stackSave();try{return dynCall_jiii(index,a1,a2,a3)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11){var sp=stackSave();try{dynCall_viiiijiiifi(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10,a11)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10){var sp=stackSave();try{dynCall_viiijjji(index,a1,a2,a3,a4,a5,a6,a7,a8,a9,a10)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiji(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiji(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viiiji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_viiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiijii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiijiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiijiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiij(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_iiij(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viji(index,a1,a2,a3,a4){var sp=stackSave();try{dynCall_viji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijii(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijii(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiji(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiji(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijiii(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijiii(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iijjiii(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vjjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_vjjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ijjiii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_ijjiii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijj(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijj(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_vijif(index,a1,a2,a3,a4,a5){var sp=stackSave();try{dynCall_vijif(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{dynCall_viiiiiji(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iijji(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iijji(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viijj(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{dynCall_viijj(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_ji(index,a1){var sp=stackSave();try{return dynCall_ji(index,a1)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiij(index,a1,a2,a3,a4,a5,a6){var sp=stackSave();try{return dynCall_iiiiij(index,a1,a2,a3,a4,a5,a6)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiij(index,a1,a2,a3,a4,a5){var sp=stackSave();try{return dynCall_iiiij(index,a1,a2,a3,a4,a5)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_j(index){var sp=stackSave();try{return dynCall_j(index)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8){var sp=stackSave();try{return dynCall_iiiiiiij(index,a1,a2,a3,a4,a5,a6,a7,a8)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_viiijii(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{dynCall_viiijii(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jii(index,a1,a2){var sp=stackSave();try{return dynCall_jii(index,a1,a2)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7){var sp=stackSave();try{return dynCall_iiiiiij(index,a1,a2,a3,a4,a5,a6,a7)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}function invoke_jiji(index,a1,a2,a3,a4){var sp=stackSave();try{return dynCall_jiji(index,a1,a2,a3,a4)}catch(e){stackRestore(sp);if(e!==e+0)throw e;_setThrew(1,0)}}Module["ccall"]=ccall;Module["cwrap"]=cwrap;Module["UTF8ToString"]=UTF8ToString;var missingLibrarySymbols=["writeI53ToI64","writeI53ToI64Clamped","writeI53ToI64Signaling","writeI53ToU64Clamped","writeI53ToU64Signaling","readI53FromI64","readI53FromU64","convertI32PairToI53","convertU32PairToI53","getTempRet0","exitJS","inetPton4","inetNtop4","inetPton6","inetNtop6","readSockaddr","writeSockaddr","emscriptenLog","readEmAsmArgs","jstoi_q","listenOnce","autoResumeAudioContext","dynCallLegacy","getDynCaller","dynCall","handleException","runtimeKeepalivePush","runtimeKeepalivePop","callUserCallback","maybeExit","asmjsMangle","HandleAllocator","getNativeTypeSize","STACK_SIZE","STACK_ALIGN","POINTER_SIZE","ASSERTIONS","uleb128Encode","sigToWasmTypes","generateFuncType","convertJsFunctionToWasm","getEmptyTableSlot","updateTableMap","getFunctionAddress","addFunction","removeFunction","reallyNegative","unSign","strLen","reSign","formatString","intArrayToString","AsciiToString","UTF16ToString","stringToUTF16","lengthBytesUTF16","UTF32ToString","stringToUTF32","lengthBytesUTF32","stringToNewUTF8","registerKeyEventCallback","maybeCStringToJsString","findEventTarget","getBoundingClientRect","fillMouseEventData","registerMouseEventCallback","registerWheelEventCallback","registerUiEventCallback","registerFocusEventCallback","fillDeviceOrientationEventData","registerDeviceOrientationEventCallback","fillDeviceMotionEventData","registerDeviceMotionEventCallback","screenOrientation","fillOrientationChangeEventData","registerOrientationChangeEventCallback","fillFullscreenChangeEventData","registerFullscreenChangeEventCallback","JSEvents_requestFullscreen","JSEvents_resizeCanvasForFullscreen","registerRestoreOldStyle","hideEverythingExceptGivenElement","restoreHiddenElements","setLetterbox","softFullscreenResizeWebGLRenderTarget","doRequestFullscreen","fillPointerlockChangeEventData","registerPointerlockChangeEventCallback","registerPointerlockErrorEventCallback","requestPointerLock","fillVisibilityChangeEventData","registerVisibilityChangeEventCallback","registerTouchEventCallback","fillGamepadEventData","registerGamepadEventCallback","registerBeforeUnloadEventCallback","fillBatteryEventData","battery","registerBatteryEventCallback","setCanvasElementSize","getCanvasElementSize","jsStackTrace","getCallstack","convertPCtoSourceLocation","checkWasiClock","wasiRightsToMuslOFlags","wasiOFlagsToMuslOFlags","createDyncallWrapper","safeSetTimeout","setImmediateWrapped","clearImmediateWrapped","polyfillSetImmediate","registerPostMainLoop","registerPreMainLoop","getPromise","makePromise","idsToPromises","makePromiseCallback","Browser_asyncPrepareDataCounter","safeRequestAnimationFrame","isLeapYear","ydayFromDate","arraySum","addDays","getSocketFromFD","getSocketAddress","FS_unlink","FS_mkdirTree","_setNetworkCallback","heapObjectForWebGLType","toTypedArrayIndex","webgl_enable_ANGLE_instanced_arrays","webgl_enable_OES_vertex_array_object","webgl_enable_WEBGL_draw_buffers","webgl_enable_WEBGL_multi_draw","webgl_enable_EXT_polygon_offset_clamp","webgl_enable_EXT_clip_control","webgl_enable_WEBGL_polygon_mode","emscriptenWebGLGet","computeUnpackAlignedImageSize","colorChannelsInGlTextureFormat","emscriptenWebGLGetTexPixelData","emscriptenWebGLGetUniform","webglGetUniformLocation","webglPrepareUniformLocationsBeforeFirstUse","webglGetLeftBracePos","emscriptenWebGLGetVertexAttrib","__glGetActiveAttribOrUniform","writeGLArray","registerWebGlEventCallback","runAndAbortIfError","ALLOC_NORMAL","ALLOC_STACK","allocate","writeStringToMemory","writeAsciiToMemory","setErrNo","demangle","stackTrace"];missingLibrarySymbols.forEach(missingLibrarySymbol);var unexportedSymbols=["run","addOnPreRun","addOnInit","addOnPreMain","addOnExit","addOnPostRun","addRunDependency","removeRunDependency","out","err","callMain","abort","wasmMemory","wasmExports","writeStackCookie","checkStackCookie","convertI32PairToI53Checked","stackSave","stackRestore","stackAlloc","setTempRet0","ptrToString","zeroMemory","getHeapMax","growMemory","ENV","setStackLimits","ERRNO_CODES","strError","DNS","Protocols","Sockets","initRandomFill","randomFill","timers","warnOnce","readEmAsmArgsArray","jstoi_s","getExecutableName","keepRuntimeAlive","asyncLoad","alignMemory","mmapAlloc","wasmTable","noExitRuntime","getCFunc","freeTableIndexes","functionsInTableMap","setValue","getValue","PATH","PATH_FS","UTF8Decoder","UTF8ArrayToString","stringToUTF8Array","stringToUTF8","lengthBytesUTF8","intArrayFromString","stringToAscii","UTF16Decoder","stringToUTF8OnStack","writeArrayToMemory","JSEvents","specialHTMLTargets","findCanvasEventTarget","currentFullscreenStrategy","restoreOldWindowedStyle","UNWIND_CACHE","ExitStatus","getEnvStrings","doReadv","doWritev","promiseMap","uncaughtExceptionCount","exceptionLast","exceptionCaught","ExceptionInfo","findMatchingCatch","Browser","getPreloadedImageData__data","wget","MONTH_DAYS_REGULAR","MONTH_DAYS_LEAP","MONTH_DAYS_REGULAR_CUMULATIVE","MONTH_DAYS_LEAP_CUMULATIVE","SYSCALLS","preloadPlugins","FS_createPreloadedFile","FS_modeStringToFlags","FS_getMode","FS_stdin_getChar_buffer","FS_stdin_getChar","FS_createPath","FS_createDevice","FS_readFile","FS","FS_createDataFile","FS_createLazyFile","MEMFS","TTY","PIPEFS","SOCKFS","tempFixedLengthArray","miniTempWebGLFloatBuffers","miniTempWebGLIntBuffers","GL","AL","GLUT","EGL","GLEW","IDBStore","SDL","SDL_gfx","allocateUTF8","allocateUTF8OnStack","print","printErr"];unexportedSymbols.forEach(unexportedRuntimeSymbol);var calledRun;var calledPrerun;dependenciesFulfilled=function runCaller(){if(!calledRun)run();if(!calledRun)dependenciesFulfilled=runCaller};function stackCheckInit(){_emscripten_stack_init();writeStackCookie()}function run(){if(runDependencies>0){return}stackCheckInit();if(!calledPrerun){calledPrerun=1;preRun();if(runDependencies>0){return}}function doRun(){if(calledRun)return;calledRun=1;Module["calledRun"]=1;if(ABORT)return;initRuntime();readyPromiseResolve(Module);Module["onRuntimeInitialized"]?.();assert(!Module["_main"],'compiled without a main, but one is present. if you added it from JS, use Module["onRuntimeInitialized"]');postRun()}if(Module["setStatus"]){Module["setStatus"]("Running...");setTimeout(()=>{setTimeout(()=>Module["setStatus"](""),1);doRun()},1)}else{doRun()}checkStackCookie()}if(Module["preInit"]){if(typeof Module["preInit"]=="function")Module["preInit"]=[Module["preInit"]];while(Module["preInit"].length>0){Module["preInit"].pop()()}}run();moduleRtn=readyPromise;for(const prop of Object.keys(Module)){if(!(prop in moduleArg)){Object.defineProperty(moduleArg,prop,{configurable:true,get(){abort(`Access to module property ('${prop}') is no longer possible via the module constructor argument; Instead, use the result of the module constructor.`)}})}} return moduleRtn; diff --git a/web/app/src/vendor/blender/web_engine.wasm b/web/app/src/vendor/blender/web_engine.wasm index 4568056d..a213cd82 100644 Binary files a/web/app/src/vendor/blender/web_engine.wasm and b/web/app/src/vendor/blender/web_engine.wasm differ diff --git a/web/app/src/workers/asset-catalog-restart-test.worker.ts b/web/app/src/workers/asset-catalog-restart-test.worker.ts new file mode 100644 index 00000000..86f3cc73 --- /dev/null +++ b/web/app/src/workers/asset-catalog-restart-test.worker.ts @@ -0,0 +1,26 @@ +import { loadAssetCatalogIndexedDBSnapshot } from "../storage/asset-catalog-indexeddb"; + +const scope = self as unknown as { + onmessage: ((event: MessageEvent<{ databaseName: string }>) => void) | null; + postMessage: (value: unknown) => void; +}; + +function openDatabase(name: string): Promise { + return new Promise((resolve, reject) => { + const request = indexedDB.open(name); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error ?? new Error("Catalog restart database open failed")); + }); +} + +scope.onmessage = (event): void => { + void openDatabase(event.data.databaseName).then(async (database) => { + try { + return await loadAssetCatalogIndexedDBSnapshot(database); + } + finally { + database.close(); + } + }).then((snapshot) => scope.postMessage({ ok: true, snapshot })) + .catch((error) => scope.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) })); +}; diff --git a/web/app/src/workers/asset-preview-display-test.worker.ts b/web/app/src/workers/asset-preview-display-test.worker.ts new file mode 100644 index 00000000..4075775d --- /dev/null +++ b/web/app/src/workers/asset-preview-display-test.worker.ts @@ -0,0 +1,13 @@ +import { displayAssetPreview } from "../assets/AssetPreviewDisplay"; + +const scope = self as unknown as { + onmessage: ((event: MessageEvent<{ identity: unknown; bytes: ArrayBuffer }>) => void) | null; + postMessage: (value: unknown, transfer?: Transferable[]) => void; +}; + +scope.onmessage = (event): void => { + const canvas = new OffscreenCanvas(1, 1); + void displayAssetPreview(canvas, "OFFSCREEN_CANVAS_2D", event.data.identity, event.data.bytes) + .then((result) => scope.postMessage({ ok: true, receipt: result.receipt, pixels: result.pixels }, [result.pixels.buffer])) + .catch((error) => scope.postMessage({ ok: false, error: error instanceof Error ? error.message : String(error) })); +}; diff --git a/web/app/src/workers/web-engine.worker.ts b/web/app/src/workers/web-engine.worker.ts index f28d7d5b..7ed2ce1d 100644 --- a/web/app/src/workers/web-engine.worker.ts +++ b/web/app/src/workers/web-engine.worker.ts @@ -29,6 +29,12 @@ import { gatePaintPBVHCapability, PAINT_PBVH_WASM_ENTRYPOINT, } from "../../../protocol/paint-pbvh-capability"; +import { + createLibraryMainAppendReceipt, + parseLibraryMainAppendRequest, + sha256LibraryBytes, + type LibraryAppendClosureIR, +} from "../../../protocol/library-main-append"; type WasmModule = { _malloc: (size: number) => number; @@ -39,6 +45,7 @@ type WasmModule = { _web_engine_get_allocated_bytes: () => number; _web_engine_open_blend: (handle: number, data: number, length: number) => number; _web_engine_apply_command: (handle: number, data: number, length: number) => number; + _web_engine_append_library_object: (handle: number, source: number, sourceLength: number, metadata: number, metadataLength: number) => number; _web_engine_undo: (handle: number) => number; _web_engine_redo: (handle: number) => number; _web_engine_get_scene_snapshot: (handle: number, data: number, length: number) => number; @@ -603,6 +610,30 @@ function nativeErrorFor(target: WasmModule, fallbackCode: ErrorReport["code"]): return report(mappedCode, message); } +function assertAppendedClosure( + snapshot: ReturnType, + closure: LibraryAppendClosureIR, +): void { + const name = (value: string) => value.slice(value.indexOf("/") + 1); + const objectId = `object:${name(closure.object)}`; + const meshId = `mesh:${name(closure.mesh)}`; + const materialId = `material:${name(closure.material)}`; + const imageId = `image:${name(closure.image)}`; + const object = snapshot.nodes.find((item) => item.id === objectId); + const mesh = snapshot.meshes.find((item) => item.id === meshId); + const material = snapshot.materials.find((item) => item.id === materialId); + const image = snapshot.images.find((item) => item.id === imageId); + const materialImageIds = new Set([ + ...(material?.imageIds ?? []), + ...(material?.nodes?.flatMap((node) => node.imageId ? [node.imageId] : []) ?? []), + ]); + if (!object || object.type !== "MESH" || object.dataId !== meshId || !mesh || + mesh.materialSlotIds?.length !== 1 || mesh.materialSlotIds[0] !== materialId || + !material || !materialImageIds.has(imageId) || !image || image.libraryLinked !== false) { + throw report("ASSET_SOURCE_HASH_MISMATCH", "WASM Main append did not publish the expected fully-local dependency closure"); + } +} + async function initialize(): Promise { if (module) return status(); if (!initializationPromise) { @@ -1316,6 +1347,55 @@ async function handleRequest(event: MessageEvent): Promise &, diff --git a/web/package.json b/web/package.json index bfa7d8f4..aaee8939 100644 --- a/web/package.json +++ b/web/package.json @@ -121,8 +121,30 @@ "test:sequencer-media-revision": "node --test tests/unit/sequencer-media-revision.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/sequencer-media-revision.spec.ts", "test:sequencer-final-export": "node --test tests/unit/sequencer-final-export.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/sequencer-final-export.spec.ts", "test:sequencer-audio-recovery": "node --test tests/unit/sequencer-audio-recovery.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/sequencer-audio-recovery.spec.ts", + "test:render-compositor-media-recovery": "node --test tests/unit/render-compositor-media-recovery.test.mjs && node ../tools/web/check-render-reference.mjs && node ../tools/web/check-compositor-node-golden.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/render-compositor-media-recovery.spec.ts", "test:tracking-mask": "playwright test --config playwright.config.ts -g \"N-022 tracking\"", "test:mask-main-reader": "node ../tools/web/check-mask-main-reader.mjs", + "test:asset-catalog-inventory": "node ../tools/web/check-asset-catalog-field-inventory.mjs", + "test:asset-catalog-v1-fixture": "node ../tools/web/check-asset-catalog-v1-fixture.mjs", + "test:asset-catalog-v2": "node --test tests/unit/asset-catalog-v2.test.mjs", + "test:asset-catalog-migration": "node --test tests/unit/asset-catalog-migration.test.mjs", + "test:asset-catalog-legacy-reader": "node --test tests/unit/asset-catalog-compatibility.test.mjs", + "test:asset-catalog-negatives": "node --test tests/unit/asset-catalog-negatives.test.mjs", + "test:asset-catalog-indexeddb-migration": "node --test tests/unit/asset-catalog-migration.test.mjs tests/unit/asset-catalog-indexeddb-migration.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-catalog-indexeddb-migration.spec.ts", + "test:asset-catalog-restart": "node --test tests/unit/asset-catalog-migration.test.mjs tests/unit/asset-catalog-indexeddb-migration.test.mjs tests/unit/asset-catalog-restart.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-catalog-indexeddb-migration.spec.ts tests/e2e/asset-catalog-restart.spec.ts", + "test:asset-catalog-m12-evidence": "npm run test:asset-catalog-inventory && npm run test:asset-catalog-v1-fixture && npm run test:asset-catalog-v2 && npm run test:asset-catalog-migration && npm run test:asset-catalog-legacy-reader && npm run test:asset-catalog-negatives && npm run test:asset-catalog-restart && node ../tools/web/check-asset-catalog-m12-evidence.mjs", + "test:asset-preview-inventory": "node ../tools/web/check-asset-preview-inventory.mjs", + "test:asset-preview-identity": "node --test tests/unit/asset-preview-identity.test.mjs && node ../tools/web/check-asset-preview-identity.mjs", + "test:asset-preview-decode-budget": "node --test tests/unit/asset-preview-identity.test.mjs tests/unit/asset-preview-decode.test.mjs", + "test:asset-preview-opfs-commit": "node --test tests/unit/asset-preview-opfs-commit.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-preview-opfs-commit.spec.ts", + "test:asset-preview-dedup": "node --test tests/unit/asset-preview-dedup.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-preview-dedup.spec.ts", + "test:asset-preview-quarantine": "node --test tests/unit/asset-preview-quarantine.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-preview-quarantine.spec.ts", + "test:asset-preview-reference-gc": "node --test tests/unit/asset-preview-reference-gc.test.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-preview-reference-gc.spec.ts", + "test:asset-preview-display": "node --test tests/unit/asset-preview-display.test.mjs && node ../tools/web/check-asset-preview-display.mjs && playwright test --config playwright.config.ts --workers=1 tests/e2e/asset-preview-display.spec.ts", + "test:library-operation-inventory": "node ../tools/web/check-library-operation-inventory.mjs", + "test:library-operation-identity": "node --test tests/unit/library-operation-identity.test.mjs", + "test:library-append-desktop": "node ../tools/web/check-library-append-fixture.mjs", + "test:library-main-append": "playwright test --config playwright.config.ts --workers=1 tests/e2e/library-append-main.spec.ts", "test:asset-library": "playwright test --config playwright.config.ts -g \"N-023 asset\"", "test:library-main-reader": "node ../tools/web/check-library-main-reader.mjs", "test:editor-workflow": "playwright test --config playwright.config.ts -g \"N-024 editor\"", diff --git a/web/protocol/asset-catalog-compatibility.ts b/web/protocol/asset-catalog-compatibility.ts new file mode 100644 index 00000000..52a10f06 --- /dev/null +++ b/web/protocol/asset-catalog-compatibility.ts @@ -0,0 +1,161 @@ +import { parseAssetLibraryManifest } from "./asset-library-io"; +import { ASSET_CATALOG_SCHEMA, parseAssetCatalogManifestV2 } from "./asset-catalog-v2"; +import type { ErrorCode } from "./error"; + +export const ASSET_CATALOG_COMPATIBILITY_SCHEMA = 1 as const; +export type AssetCatalogLegacyOperation = "READ" | "CATALOG_WRITE" | "ASSET_WRITE" | "SAVE"; + +export interface AssetCatalogLegacySnapshotIR { + revision: number; + catalogs: Array<{ catalogId: string; path: string; simpleName: string }>; + assets: Array<{ assetId: string; relativeAssetIdentifier: string; idType: string; name: string; catalogId: string | null }>; + libraries: Array<{ libraryId: string; name: string; readOnly: boolean }>; +} + +export interface AssetCatalogLegacyCompatibilityIR { + schemaVersion: typeof ASSET_CATALOG_COMPATIBILITY_SCHEMA; + task: "M12-01E"; + readerSchemaVersion: 1; + documentSchemaVersion: number; + operation: AssetCatalogLegacyOperation; + status: "READY" | "READ_ONLY" | "BLOCKED"; + code: ErrorCode | null; + recoverable: boolean; + sourceSha256: string; + snapshot: AssetCatalogLegacySnapshotIR | null; + nextTask: "M12-01F"; +} + +const encoder = new TextEncoder(); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + return JSON.stringify(value); +} + +async function sha256(value: unknown): Promise { + const digest = await crypto.subtle.digest("SHA-256", encoder.encode(stableJSON(value))); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function operation(value: unknown): AssetCatalogLegacyOperation { + if (value !== "READ" && value !== "CATALOG_WRITE" && value !== "ASSET_WRITE" && value !== "SAVE") { + throw new Error("ASSET_MANIFEST_INVALID: legacy reader operation is invalid"); + } + return value; +} + +export async function inspectAssetCatalogForLegacyReader( + value: unknown, + operationValue: unknown, +): Promise { + const requestedOperation = operation(operationValue); + const sourceSha256 = await sha256(value); + if (!record(value) || typeof value.schemaVersion !== "number" || !Number.isSafeInteger(value.schemaVersion)) { + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: 0, + operation: requestedOperation, + status: "BLOCKED", + code: "PROTOCOL_MISMATCH", + recoverable: false, + sourceSha256, + snapshot: null, + nextTask: "M12-01F", + }; + } + if (value.schemaVersion === 1) { + try { + parseAssetLibraryManifest(value); + } + catch { + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: 1, + operation: requestedOperation, + status: "BLOCKED", + code: "ASSET_MANIFEST_INVALID", + recoverable: true, + sourceSha256, + snapshot: null, + nextTask: "M12-01F", + }; + } + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: 1, + operation: requestedOperation, + status: "READY", + code: null, + recoverable: true, + sourceSha256, + snapshot: null, + nextTask: "M12-01F", + }; + } + if (value.schemaVersion !== ASSET_CATALOG_SCHEMA) { + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: value.schemaVersion, + operation: requestedOperation, + status: "BLOCKED", + code: "PROTOCOL_MISMATCH", + recoverable: false, + sourceSha256, + snapshot: null, + nextTask: "M12-01F", + }; + } + + let document; + try { + document = await parseAssetCatalogManifestV2(value); + } + catch { + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: ASSET_CATALOG_SCHEMA, + operation: requestedOperation, + status: "BLOCKED", + code: "ASSET_MANIFEST_INVALID", + recoverable: true, + sourceSha256, + snapshot: null, + nextTask: "M12-01F", + }; + } + const snapshot: AssetCatalogLegacySnapshotIR = { + revision: document.revision, + catalogs: document.catalogs.map(({ catalogId, path, simpleName }) => ({ catalogId, path, simpleName })), + assets: document.assets.map(({ assetId, relativeAssetIdentifier, idType, name, catalogId }) => ({ assetId, relativeAssetIdentifier, idType, name, catalogId })), + libraries: document.libraries.map(({ libraryId, name, readOnly }) => ({ libraryId, name, readOnly })), + }; + return { + schemaVersion: ASSET_CATALOG_COMPATIBILITY_SCHEMA, + task: "M12-01E", + readerSchemaVersion: 1, + documentSchemaVersion: ASSET_CATALOG_SCHEMA, + operation: requestedOperation, + status: requestedOperation === "READ" ? "READ_ONLY" : "BLOCKED", + code: "ASSET_SCHEMA_DOWNGRADE_BLOCKED", + recoverable: true, + sourceSha256, + snapshot, + nextTask: "M12-01F", + }; +} diff --git a/web/protocol/asset-catalog-migration.ts b/web/protocol/asset-catalog-migration.ts new file mode 100644 index 00000000..b1f4d084 --- /dev/null +++ b/web/protocol/asset-catalog-migration.ts @@ -0,0 +1,260 @@ +import { parseAssetLibraryManifest } from "./asset-library-io"; +import { + ASSET_CATALOG_SCHEMA, + createAssetCatalogV2StableId, + parseAssetCatalogManifestV2, + type AssetCatalogManifestV2IR, + type AssetCatalogV2IDType, +} from "./asset-catalog-v2"; +import type { ErrorCode } from "./error"; + +export const ASSET_CATALOG_MIGRATION_SCHEMA = 1 as const; +export const ASSET_CATALOG_V1_UUID_NAMESPACE = "d15c0cb7-e77a-5d56-b7da-f7d3ecb31df1" as const; + +export interface AssetCatalogMigrationReportIR { + schemaVersion: typeof ASSET_CATALOG_MIGRATION_SCHEMA; + task: "M12-01D"; + status: "MIGRATED"; + sourceSchemaVersion: 1; + targetSchemaVersion: typeof ASSET_CATALOG_SCHEMA; + sourceRevision: number; + targetRevision: number; + sourceManifestSha256: string; + targetManifestSha256: string; + catalogMappings: Array<{ legacyId: string; catalogId: string; path: string }>; + assetMappings: Array<{ legacyId: string; assetId: string; relativeAssetIdentifier: string }>; + defaultsApplied: { + dynamicMetadataFields: number; + preferredImportMethods: number; + customPropertyCollections: number; + }; + preserved: { + catalogs: number; + assets: number; + libraries: number; + previews: number; + sourceBindings: number; + }; + nextTask: "M12-01E"; +} + +export interface AssetCatalogMigrationResultIR { + manifest: AssetCatalogManifestV2IR; + report: AssetCatalogMigrationReportIR; +} + +export class AssetCatalogMigrationError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "AssetCatalogMigrationError"; + this.code = code; + } +} + +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const encoder = new TextEncoder(); +const ID_TYPE_DIRECTORY: Readonly> = Object.freeze({ + ACTION: "Action", + COLLECTION: "Collection", + IMAGE: "Image", + MATERIAL: "Material", + NODE_GROUP: "NodeTree", + OBJECT: "Object", + WORLD: "World", +}); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record, allowed: readonly string[], path: string): void { + const allowedSet = new Set(allowed); + if (Object.keys(value).some((key) => !allowedSet.has(key))) { + throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `${path} contains fields schema v1 cannot migrate losslessly`); + } +} + +function validateLegacyShape(value: unknown): asserts value is Record { + if (!record(value)) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", "schema v1 manifest must be an object"); + exactKeys(value, ["schemaVersion", "revision", "catalogs", "assets", "libraries"], "manifest"); + if (!Array.isArray(value.catalogs) || !Array.isArray(value.assets) || !Array.isArray(value.libraries)) { + throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", "schema v1 manifest collections are invalid"); + } + value.catalogs.forEach((item, index) => { + if (!record(item)) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `catalogs[${index}] must be an object`); + exactKeys(item, ["id", "name", "parentId"], `catalogs[${index}]`); + }); + value.assets.forEach((item, index) => { + if (!record(item)) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `assets[${index}] must be an object`); + exactKeys(item, ["id", "name", "kind", "catalogId", "tags", "author", "license", "sourceSha256", "sourcePath", "preview"], `assets[${index}]`); + if (item.preview !== undefined) { + if (!record(item.preview)) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `assets[${index}].preview must be an object`); + exactKeys(item.preview, ["assetId", "sha256", "mimeType", "width", "height", "byteLength"], `assets[${index}].preview`); + if (item.preview.assetId !== item.id) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `assets[${index}].preview belongs to another asset`); + } + }); + value.libraries.forEach((item, index) => { + if (!record(item)) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `libraries[${index}] must be an object`); + exactKeys(item, ["id", "name", "sourcePath", "sourceSha256", "dependencyIds", "readOnly"], `libraries[${index}]`); + }); +} + +function uuidBytes(value: string): Uint8Array { + return Uint8Array.from(value.replaceAll("-", "").match(/.{2}/g) ?? [], (byte) => Number.parseInt(byte, 16)); +} + +function formatUUID(value: Uint8Array): string { + const hex = Array.from(value, (byte) => byte.toString(16).padStart(2, "0")).join(""); + return `${hex.slice(0, 8)}-${hex.slice(8, 12)}-${hex.slice(12, 16)}-${hex.slice(16, 20)}-${hex.slice(20)}`; +} + +async function catalogUUID(legacyId: string): Promise { + if (UUID.test(legacyId)) return legacyId; + const namespace = uuidBytes(ASSET_CATALOG_V1_UUID_NAMESPACE); + const name = encoder.encode(legacyId); + const input = new Uint8Array(namespace.byteLength + name.byteLength); + input.set(namespace); + input.set(name, namespace.byteLength); + const bytes = new Uint8Array(await crypto.subtle.digest("SHA-1", input)).slice(0, 16); + bytes[6] = (bytes[6] & 0x0f) | 0x50; + bytes[8] = (bytes[8] & 0x3f) | 0x80; + return formatUUID(bytes); +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + return JSON.stringify(value); +} + +async function sha256(value: unknown): Promise { + const digest = await crypto.subtle.digest("SHA-256", encoder.encode(stableJSON(value))); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function compareText(left: string, right: string): number { + return left < right ? -1 : left > right ? 1 : 0; +} + +export async function migrateAssetCatalogV1ToV2(value: unknown): Promise { + validateLegacyShape(value); + let legacy; + try { + legacy = parseAssetLibraryManifest(value); + } + catch (error) { + if (error instanceof Error) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", error.message); + throw error; + } + + const legacyCatalogById = new Map(legacy.catalogs.map((catalog) => [catalog.id, catalog])); + const pathCache = new Map(); + const resolvePath = (id: string): string => { + const cached = pathCache.get(id); + if (cached) return cached; + const catalog = legacyCatalogById.get(id); + if (!catalog) throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `catalog ${id} is missing`); + if (catalog.name.includes("/") || catalog.name === "." || catalog.name === ".." || catalog.name.trim() !== catalog.name) { + throw new AssetCatalogMigrationError("ASSET_MANIFEST_INVALID", `catalog ${id} name cannot be represented as a Blender path component`); + } + const result = catalog.parentId === null ? catalog.name : `${resolvePath(catalog.parentId)}/${catalog.name}`; + pathCache.set(id, result); + return result; + }; + + const catalogMappings = await Promise.all(legacy.catalogs.map(async (catalog) => ({ + legacyId: catalog.id, + catalogId: await catalogUUID(catalog.id), + path: resolvePath(catalog.id), + }))); + const mappingByLegacyId = new Map(catalogMappings.map((mapping) => [mapping.legacyId, mapping])); + const catalogs = catalogMappings.map((mapping) => ({ + catalogId: mapping.catalogId, + path: mapping.path, + simpleName: legacyCatalogById.get(mapping.legacyId)?.name ?? "", + parentPath: mapping.path.includes("/") ? mapping.path.slice(0, mapping.path.lastIndexOf("/")) : null, + })).sort((left, right) => compareText(left.path, right.path) || compareText(left.catalogId, right.catalogId)); + + const assetMappings: AssetCatalogMigrationReportIR["assetMappings"] = []; + const assets = await Promise.all(legacy.assets.map(async (asset) => { + const idType = asset.kind as AssetCatalogV2IDType; + const relativeAssetIdentifier = `${ID_TYPE_DIRECTORY[idType]}/${asset.name}`; + const assetId = await createAssetCatalogV2StableId({ assetLibraryIdentifier: null, relativeAssetIdentifier }); + assetMappings.push({ legacyId: asset.id, assetId, relativeAssetIdentifier }); + const mappedCatalog = asset.catalogId === null ? null : mappingByLegacyId.get(asset.catalogId); + return { + assetId, + assetLibraryIdentifier: null, + relativeAssetIdentifier, + idType, + name: asset.name, + catalogId: mappedCatalog?.catalogId ?? null, + catalogSimpleName: asset.catalogId === null ? "" : legacyCatalogById.get(asset.catalogId)?.name ?? "", + author: asset.author, + description: "", + copyright: "", + license: asset.license, + tags: [...asset.tags], + activeTag: 0, + usePreferredImportMethod: false, + preferredImportMethod: "APPEND" as const, + customProperties: [], + sourceSha256: asset.sourceSha256, + sourcePath: asset.sourcePath ?? null, + preview: asset.preview ? { + sha256: asset.preview.sha256, + mimeType: asset.preview.mimeType, + width: asset.preview.width, + height: asset.preview.height, + byteLength: asset.preview.byteLength, + } : null, + }; + })); + assets.sort((left, right) => compareText(left.assetId, right.assetId)); + assetMappings.sort((left, right) => compareText(left.legacyId, right.legacyId)); + + const libraries = legacy.libraries.map((library) => ({ + libraryId: library.id, + name: library.name, + sourcePath: library.sourcePath, + sourceSha256: library.sourceSha256, + dependencyIds: [...library.dependencyIds], + readOnly: library.readOnly, + })).sort((left, right) => compareText(left.libraryId, right.libraryId)); + const manifest = await parseAssetCatalogManifestV2({ + schemaVersion: ASSET_CATALOG_SCHEMA, + revision: legacy.revision, + catalogs, + assets, + libraries, + }); + const report: AssetCatalogMigrationReportIR = { + schemaVersion: ASSET_CATALOG_MIGRATION_SCHEMA, + task: "M12-01D", + status: "MIGRATED", + sourceSchemaVersion: 1, + targetSchemaVersion: ASSET_CATALOG_SCHEMA, + sourceRevision: legacy.revision, + targetRevision: manifest.revision, + sourceManifestSha256: await sha256(legacy), + targetManifestSha256: await sha256(manifest), + catalogMappings: [...catalogMappings].sort((left, right) => compareText(left.legacyId, right.legacyId)), + assetMappings, + defaultsApplied: { + dynamicMetadataFields: legacy.assets.length * 2, + preferredImportMethods: legacy.assets.length, + customPropertyCollections: legacy.assets.length, + }, + preserved: { + catalogs: legacy.catalogs.length, + assets: legacy.assets.length, + libraries: legacy.libraries.length, + previews: legacy.assets.filter((asset) => asset.preview !== undefined).length, + sourceBindings: legacy.assets.length, + }, + nextTask: "M12-01E", + }; + return { manifest, report }; +} diff --git a/web/protocol/asset-catalog-v2.ts b/web/protocol/asset-catalog-v2.ts new file mode 100644 index 00000000..55197b78 --- /dev/null +++ b/web/protocol/asset-catalog-v2.ts @@ -0,0 +1,474 @@ +import type { ErrorCode } from "./error"; + +export const ASSET_CATALOG_SCHEMA = 2 as const; +export const ASSET_CATALOG_V2_BUDGET = Object.freeze({ + maxCatalogs: 10_000, + maxAssets: 100_000, + maxLibraries: 1_024, + maxDependenciesPerLibrary: 1_024, + maxCatalogPathBytes: 4_096, + maxCatalogSimpleNameBytes: 63, + maxLibraryIdentifierBytes: 1_024, + maxRelativeAssetIdentifierBytes: 4_096, + maxAssetNameBytes: 1_024, + maxMetadataTextBytes: 64 * 1024, + maxTagBytes: 63, + maxTagsPerAsset: 128, + maxCustomPropertiesPerAsset: 256, + maxCustomPropertyNameBytes: 63, + maxCustomPropertyStringBytes: 64 * 1024, + maxCustomPropertyArrayItems: 4_096, + maxTotalTextBytes: 64 * 1024 * 1024, +}); + +export const ASSET_CATALOG_V2_ID_TYPES = ["ACTION", "COLLECTION", "IMAGE", "MATERIAL", "NODE_GROUP", "OBJECT", "WORLD"] as const; +export type AssetCatalogV2IDType = typeof ASSET_CATALOG_V2_ID_TYPES[number]; +export type AssetCatalogV2ImportMethod = "LINK" | "APPEND" | "PACK"; +export type AssetCatalogV2CustomProperty = + | { name: string; type: "BOOL"; value: boolean } + | { name: string; type: "INT"; value: number } + | { name: string; type: "FLOAT"; value: number } + | { name: string; type: "STRING"; value: string } + | { name: string; type: "INT_ARRAY"; value: number[] } + | { name: string; type: "FLOAT_ARRAY"; value: number[] } + | { name: string; type: "STRING_ARRAY"; value: string[] }; + +export interface AssetCatalogV2IR { + catalogId: string; + path: string; + simpleName: string; + parentPath: string | null; +} + +export interface AssetCatalogV2EntryIR { + assetId: string; + assetLibraryIdentifier: string | null; + relativeAssetIdentifier: string; + idType: AssetCatalogV2IDType; + name: string; + catalogId: string | null; + catalogSimpleName: string; + author: string; + description: string; + copyright: string; + license: string; + tags: string[]; + activeTag: number; + usePreferredImportMethod: boolean; + preferredImportMethod: AssetCatalogV2ImportMethod; + customProperties: AssetCatalogV2CustomProperty[]; + sourceSha256: string | null; + sourcePath: string | null; + preview: AssetCatalogV2PreviewIR | null; +} + +export interface AssetCatalogV2PreviewIR { + sha256: string; + mimeType: "image/png" | "image/webp"; + width: number; + height: number; + byteLength: number; +} + +export interface AssetCatalogV2LibraryIR { + libraryId: string; + name: string; + sourcePath: string; + sourceSha256: string; + dependencyIds: string[]; + readOnly: boolean; +} + +export interface AssetCatalogManifestV2IR { + schemaVersion: typeof ASSET_CATALOG_SCHEMA; + revision: number; + catalogs: AssetCatalogV2IR[]; + assets: AssetCatalogV2EntryIR[]; + libraries: AssetCatalogV2LibraryIR[]; +} + +export class AssetCatalogV2ValidationError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "AssetCatalogV2ValidationError"; + this.code = code; + } +} + +const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/; +const NIL_UUID = "00000000-0000-0000-0000-000000000000"; +const ASSET_ID = /^asset:[a-f0-9]{64}$/; +const SHA256 = /^[a-f0-9]{64}$/; +const encoder = new TextEncoder(); +const ID_TYPE_DIRECTORY: Readonly> = Object.freeze({ + ACTION: "Action", + COLLECTION: "Collection", + IMAGE: "Image", + MATERIAL: "Material", + NODE_GROUP: "NodeTree", + OBJECT: "Object", + WORLD: "World", +}); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record, allowed: readonly string[], name: string): void { + const allowedSet = new Set(allowed); + if (Object.keys(value).some((key) => !allowedSet.has(key))) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} contains undeclared fields`); + } +} + +function boundedInteger(value: unknown, name: string, minimum: number, maximum: number): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum || value > maximum) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} is outside the bounded integer range`); + } + return value; +} + +interface TextBudgetState { total: number } + +function boundedText( + value: unknown, + name: string, + maximumBytes: number, + state: TextBudgetState, + allowEmpty = false, +): string { + if (typeof value !== "string") { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be text`); + } + const bytes = encoder.encode(value).byteLength; + if ((!allowEmpty && bytes === 0) || bytes > maximumBytes) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${name} exceeds its UTF-8 byte budget`); + } + state.total += bytes; + if (!Number.isSafeInteger(state.total) || state.total > ASSET_CATALOG_V2_BUDGET.maxTotalTextBytes) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", "catalog manifest exceeds the aggregate text budget"); + } + return value; +} + +function canonicalUUID(value: unknown, name: string): string { + if (typeof value !== "string" || !UUID.test(value)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be a canonical RFC4122 UUID`); + } + return value; +} + +function parseCatalogPath(value: unknown, name: string, state: TextBudgetState): string { + const result = boundedText(value, name, ASSET_CATALOG_V2_BUDGET.maxCatalogPathBytes, state); + const components = result.split("/"); + if (components.some((component) => component.length === 0 || component === "." || component === ".." || component.trim() !== component)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} is not a cleaned Blender catalog path`); + } + return result; +} + +function expectedParentPath(path: string): string | null { + const separator = path.lastIndexOf("/"); + return separator === -1 ? null : path.slice(0, separator); +} + +function parseCatalog(value: unknown, index: number, state: TextBudgetState): AssetCatalogV2IR { + const name = `catalogs[${index}]`; + if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be an object`); + exactKeys(value, ["catalogId", "path", "simpleName", "parentPath"], name); + const path = parseCatalogPath(value.path, `${name}.path`, state); + const parentPath = value.parentPath === null ? null : parseCatalogPath(value.parentPath, `${name}.parentPath`, state); + if (parentPath !== expectedParentPath(path)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name}.parentPath does not match the Blender path hierarchy`); + } + return { + catalogId: canonicalUUID(value.catalogId, `${name}.catalogId`), + path, + simpleName: boundedText(value.simpleName, `${name}.simpleName`, ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, state), + parentPath, + }; +} + +function digest(value: unknown, name: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${name} must be a lowercase SHA-256 digest`); + } + return value; +} + +function parsePreview(value: unknown, path: string): AssetCatalogV2PreviewIR { + if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`); + exactKeys(value, ["sha256", "mimeType", "width", "height", "byteLength"], path); + if (value.mimeType !== "image/png" && value.mimeType !== "image/webp") { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.mimeType is unsupported`); + } + return { + sha256: digest(value.sha256, `${path}.sha256`), + mimeType: value.mimeType, + width: boundedInteger(value.width, `${path}.width`, 1, 4_096), + height: boundedInteger(value.height, `${path}.height`, 1, 4_096), + byteLength: boundedInteger(value.byteLength, `${path}.byteLength`, 1, 64 * 1024 * 1024), + }; +} + +function parseLibrary(value: unknown, index: number, state: TextBudgetState): AssetCatalogV2LibraryIR { + const path = `libraries[${index}]`; + if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`); + exactKeys(value, ["libraryId", "name", "sourcePath", "sourceSha256", "dependencyIds", "readOnly"], path); + if (!Array.isArray(value.dependencyIds) || value.dependencyIds.length > ASSET_CATALOG_V2_BUDGET.maxDependenciesPerLibrary) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.dependencyIds exceeds the entry budget`); + } + if (typeof value.readOnly !== "boolean") throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.readOnly must be boolean`); + return { + libraryId: boundedText(value.libraryId, `${path}.libraryId`, ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes, state), + name: boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxAssetNameBytes, state), + sourcePath: boundedText(value.sourcePath, `${path}.sourcePath`, ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes, state), + sourceSha256: digest(value.sourceSha256, `${path}.sourceSha256`), + dependencyIds: value.dependencyIds.map((item, dependencyIndex) => boundedText(item, `${path}.dependencyIds[${dependencyIndex}]`, ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes, state)), + readOnly: value.readOnly, + }; +} + +function assertLibraryGraph(libraries: readonly AssetCatalogV2LibraryIR[]): void { + const byId = new Map(libraries.map((library) => [library.libraryId, library])); + if (byId.size !== libraries.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate library IDs"); + const active = new Set(); + const complete = new Set(); + const visit = (id: string): void => { + if (active.has(id)) throw new AssetCatalogV2ValidationError("LIBRARY_DEPENDENCY_CYCLE", `library dependency cycle includes ${id}`); + if (complete.has(id)) return; + const library = byId.get(id); + if (!library) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `library dependency ${id} is missing`); + active.add(id); + for (const dependency of library.dependencyIds) visit(dependency); + active.delete(id); + complete.add(id); + }; + for (const id of byId.keys()) visit(id); +} + +function parseCustomProperty(value: unknown, path: string, state: TextBudgetState): AssetCatalogV2CustomProperty { + if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`); + exactKeys(value, ["name", "type", "value"], path); + const name = boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyNameBytes, state); + switch (value.type) { + case "BOOL": + if (typeof value.value !== "boolean") throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must be boolean`); + return { name, type: "BOOL", value: value.value }; + case "INT": + return { name, type: "INT", value: boundedInteger(value.value, `${path}.value`, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER) }; + case "FLOAT": + if (typeof value.value !== "number" || !Number.isFinite(value.value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must be finite`); + return { name, type: "FLOAT", value: value.value }; + case "STRING": + return { name, type: "STRING", value: boundedText(value.value, `${path}.value`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyStringBytes, state, true) }; + case "INT_ARRAY": + case "FLOAT_ARRAY": + case "STRING_ARRAY": { + if (!Array.isArray(value.value) || value.value.length > ASSET_CATALOG_V2_BUDGET.maxCustomPropertyArrayItems) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.value exceeds the array budget`); + } + if (value.type === "STRING_ARRAY") { + return { name, type: value.type, value: value.value.map((item, index) => boundedText(item, `${path}.value[${index}]`, ASSET_CATALOG_V2_BUDGET.maxCustomPropertyStringBytes, state, true)) }; + } + if (value.type === "INT_ARRAY") { + return { name, type: value.type, value: value.value.map((item, index) => boundedInteger(item, `${path}.value[${index}]`, Number.MIN_SAFE_INTEGER, Number.MAX_SAFE_INTEGER)) }; + } + if (value.value.some((item) => typeof item !== "number" || !Number.isFinite(item))) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.value must contain finite numbers`); + } + return { name, type: value.type, value: [...value.value] as number[] }; + } + default: + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.type is unsupported`); + } +} + +function parseAssetIdentity( + value: { assetLibraryIdentifier: unknown; relativeAssetIdentifier: unknown }, + state: TextBudgetState, + path: string, +): { assetLibraryIdentifier: string | null; relativeAssetIdentifier: string } { + const assetLibraryIdentifier = value.assetLibraryIdentifier === null ? null : boundedText( + value.assetLibraryIdentifier, + `${path}.assetLibraryIdentifier`, + ASSET_CATALOG_V2_BUDGET.maxLibraryIdentifierBytes, + state, + ); + const relativeAssetIdentifier = boundedText( + value.relativeAssetIdentifier, + `${path}.relativeAssetIdentifier`, + ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes, + state, + ); + return { assetLibraryIdentifier, relativeAssetIdentifier }; +} + +async function sha256(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function createAssetCatalogV2StableId(identityValue: unknown): Promise { + if (!record(identityValue)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "asset identity must be an object"); + exactKeys(identityValue, ["assetLibraryIdentifier", "relativeAssetIdentifier"], "asset identity"); + const identity = parseAssetIdentity({ + assetLibraryIdentifier: identityValue.assetLibraryIdentifier, + relativeAssetIdentifier: identityValue.relativeAssetIdentifier, + }, { total: 0 }, "asset identity"); + const canonical = JSON.stringify({ + schema: "BLENDER_ASSET_WEAK_REFERENCE_V1", + assetLibraryIdentifier: identity.assetLibraryIdentifier, + relativeAssetIdentifier: identity.relativeAssetIdentifier, + }); + return `asset:${await sha256(canonical)}`; +} + +async function parseAsset(value: unknown, index: number, catalogIds: ReadonlySet, state: TextBudgetState): Promise { + const path = `assets[${index}]`; + if (!record(value)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} must be an object`); + exactKeys(value, [ + "assetId", "assetLibraryIdentifier", "relativeAssetIdentifier", "idType", "name", "catalogId", + "catalogSimpleName", "author", "description", "copyright", "license", "tags", "activeTag", + "usePreferredImportMethod", "preferredImportMethod", "customProperties", + "sourceSha256", "sourcePath", "preview", + ], path); + if (!ASSET_CATALOG_V2_ID_TYPES.includes(value.idType as AssetCatalogV2IDType)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.idType is unsupported`); + } + const idType = value.idType as AssetCatalogV2IDType; + const identity = parseAssetIdentity({ + assetLibraryIdentifier: value.assetLibraryIdentifier, + relativeAssetIdentifier: value.relativeAssetIdentifier, + }, state, path); + if (!identity.relativeAssetIdentifier.startsWith(`${ID_TYPE_DIRECTORY[idType]}/`)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.relativeAssetIdentifier disagrees with idType`); + } + if (typeof value.assetId !== "string" || !ASSET_ID.test(value.assetId) || value.assetId !== await createAssetCatalogV2StableId(identity)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.assetId does not bind the Blender weak reference`); + } + const catalogId = value.catalogId === null ? null : canonicalUUID(value.catalogId, `${path}.catalogId`); + if (catalogId !== null && !catalogIds.has(catalogId)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.catalogId is not declared`); + } + if (!Array.isArray(value.tags) || value.tags.length > ASSET_CATALOG_V2_BUDGET.maxTagsPerAsset) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.tags exceeds the entry budget`); + } + const tags = value.tags.map((tag, tagIndex) => boundedText(tag, `${path}.tags[${tagIndex}]`, ASSET_CATALOG_V2_BUDGET.maxTagBytes, state)); + if (!Array.isArray(value.customProperties) || value.customProperties.length > ASSET_CATALOG_V2_BUDGET.maxCustomPropertiesPerAsset) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", `${path}.customProperties exceeds the entry budget`); + } + const customProperties = value.customProperties.map((item, propertyIndex) => parseCustomProperty(item, `${path}.customProperties[${propertyIndex}]`, state)); + const activeTag = boundedInteger(value.activeTag, `${path}.activeTag`, 0, Math.max(tags.length - 1, 0)); + if (typeof value.usePreferredImportMethod !== "boolean" || !["LINK", "APPEND", "PACK"].includes(value.preferredImportMethod as string)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} has invalid import method metadata`); + } + return { + assetId: value.assetId, + ...identity, + idType, + name: boundedText(value.name, `${path}.name`, ASSET_CATALOG_V2_BUDGET.maxAssetNameBytes, state), + catalogId, + catalogSimpleName: boundedText(value.catalogSimpleName, `${path}.catalogSimpleName`, ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, state, true), + author: boundedText(value.author, `${path}.author`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true), + description: boundedText(value.description, `${path}.description`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true), + copyright: boundedText(value.copyright, `${path}.copyright`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true), + license: boundedText(value.license, `${path}.license`, ASSET_CATALOG_V2_BUDGET.maxMetadataTextBytes, state, true), + tags, + activeTag, + usePreferredImportMethod: value.usePreferredImportMethod, + preferredImportMethod: value.preferredImportMethod as AssetCatalogV2ImportMethod, + customProperties, + sourceSha256: value.sourceSha256 === null ? null : digest(value.sourceSha256, `${path}.sourceSha256`), + sourcePath: value.sourcePath === null ? null : boundedText(value.sourcePath, `${path}.sourcePath`, ASSET_CATALOG_V2_BUDGET.maxRelativeAssetIdentifierBytes, state), + preview: value.preview === null ? null : parsePreview(value.preview, `${path}.preview`), + }; +} + +export async function parseAssetCatalogManifestV2(value: unknown): Promise { + if (!record(value) || value.schemaVersion !== ASSET_CATALOG_SCHEMA) { + throw new AssetCatalogV2ValidationError("PROTOCOL_MISMATCH", "unsupported asset catalog schema"); + } + exactKeys(value, ["schemaVersion", "revision", "catalogs", "assets", "libraries"], "manifest"); + if (!Array.isArray(value.catalogs) || value.catalogs.length > ASSET_CATALOG_V2_BUDGET.maxCatalogs || + !Array.isArray(value.assets) || value.assets.length > ASSET_CATALOG_V2_BUDGET.maxAssets || + !Array.isArray(value.libraries) || value.libraries.length > ASSET_CATALOG_V2_BUDGET.maxLibraries) { + throw new AssetCatalogV2ValidationError("ASSET_BUDGET_EXCEEDED", "manifest exceeds the catalog, asset, or library entry budget"); + } + const state = { total: 0 }; + const catalogs = value.catalogs.map((item, index) => parseCatalog(item, index, state)); + const catalogIds = new Set(catalogs.map((item) => item.catalogId)); + if (catalogIds.size !== catalogs.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate catalog IDs"); + const assets = await Promise.all(value.assets.map((item, index) => parseAsset(item, index, catalogIds, state))); + if (new Set(assets.map((item) => item.assetId)).size !== assets.length) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "manifest has duplicate asset IDs"); + const libraries = value.libraries.map((item, index) => parseLibrary(item, index, state)); + assertLibraryGraph(libraries); + return { + schemaVersion: ASSET_CATALOG_SCHEMA, + revision: boundedInteger(value.revision, "revision", 0, Number.MAX_SAFE_INTEGER), + catalogs, + assets, + libraries, + }; +} + +function desktopProperty(value: unknown, path: string): AssetCatalogV2CustomProperty { + if (!record(value) || typeof value.name !== "string" || typeof value.type !== "string") { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path} is invalid`); + } + if (value.type === "STR") return { name: value.name, type: "STRING", value: value.value as string }; + if (value.type === "IDPROPERTYARRAY") { + return { name: value.name, type: "FLOAT_ARRAY", value: value.value as number[] }; + } + if (["BOOL", "INT", "FLOAT"].includes(value.type)) { + return { name: value.name, type: value.type as "BOOL", value: value.value as boolean } as AssetCatalogV2CustomProperty; + } + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `${path}.type is unsupported`); +} + +export async function createAssetCatalogManifestV2FromDesktop(value: unknown, revision = 0): Promise { + if (!record(value) || value.schemaVersion !== 1 || value.task !== "M12-01B" || !record(value.catalogDefinition) || + !Array.isArray(value.catalogDefinition.records) || !Array.isArray(value.assets)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", "desktop catalog canonical report is invalid"); + } + const catalogs = value.catalogDefinition.records.map((item, index) => { + if (!record(item)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop catalog ${index} is invalid`); + return { catalogId: item.catalogId, path: item.path, simpleName: item.simpleName, parentPath: item.parentPath }; + }); + const catalogIds = new Set(catalogs.map((item) => item.catalogId)); + const assets = await Promise.all(value.assets.map(async (item, index) => { + if (!record(item) || !ASSET_CATALOG_V2_ID_TYPES.includes(item.idType as AssetCatalogV2IDType) || !Array.isArray(item.tags) || !Array.isArray(item.customProperties)) { + throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop asset ${index} is invalid`); + } + const idType = item.idType as AssetCatalogV2IDType; + const assetLibraryIdentifier = null; + const relativeAssetIdentifier = `${ID_TYPE_DIRECTORY[idType]}/${item.name}`; + const desktopCatalogId = item.catalogId === NIL_UUID ? null : item.catalogId; + if (desktopCatalogId !== null && !catalogIds.has(desktopCatalogId)) throw new AssetCatalogV2ValidationError("ASSET_MANIFEST_INVALID", `desktop asset ${index} references an unknown catalog`); + return { + assetId: await createAssetCatalogV2StableId({ assetLibraryIdentifier, relativeAssetIdentifier }), + assetLibraryIdentifier, + relativeAssetIdentifier, + idType, + name: item.name, + catalogId: desktopCatalogId, + catalogSimpleName: item.catalogSimpleName, + author: item.author, + description: item.description, + copyright: item.copyright, + license: item.license, + tags: item.tags, + activeTag: item.activeTag, + usePreferredImportMethod: item.usePreferredImportMethod, + preferredImportMethod: item.preferredImportMethod, + customProperties: item.customProperties.map((property, propertyIndex) => desktopProperty(property, `desktop asset ${index} property ${propertyIndex}`)), + sourceSha256: null, + sourcePath: null, + preview: null, + }; + })); + const manifest = { schemaVersion: ASSET_CATALOG_SCHEMA, revision, catalogs, assets, libraries: [] }; + return parseAssetCatalogManifestV2(manifest); +} diff --git a/web/protocol/asset-preview-decode.ts b/web/protocol/asset-preview-decode.ts new file mode 100644 index 00000000..d9ec6c01 --- /dev/null +++ b/web/protocol/asset-preview-decode.ts @@ -0,0 +1,131 @@ +import { + parseAssetPreviewIdentity, + type AssetPreviewContentMimeType, + type AssetPreviewIdentityIR, +} from "./asset-preview"; +import type { ErrorCode } from "./error"; + +export const ASSET_PREVIEW_DECODE_SCHEMA = 1 as const; +export const ASSET_PREVIEW_DECODE_BUDGET = Object.freeze({ + maxEncodedBytes: 16 * 1024 * 1024, + maxWidth: 4096, + maxHeight: 4096, + maxPixels: 4096 * 4096, + maxDecodedBytes: 64 * 1024 * 1024, + maxCompressionRatio: 100, +}); + +export interface AssetPreviewDecodePlanIR { + schemaVersion: typeof ASSET_PREVIEW_DECODE_SCHEMA; + identitySha256: string; + mimeType: AssetPreviewContentMimeType; + width: number; + height: number; + pixelCount: number; + encodedByteLength: number; + decodedByteLength: number; + compressionRatio: number; +} + +export class AssetPreviewDecodeError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "AssetPreviewDecodeError"; + this.code = code; + } +} + +function text(bytes: Uint8Array, offset: number, length: number): string { + return String.fromCharCode(...bytes.subarray(offset, offset + length)); +} + +function pngDimensions(bytes: Uint8Array): { width: number; height: number } | null { + const signature = [137, 80, 78, 71, 13, 10, 26, 10]; + if (bytes.byteLength < 24 || !signature.every((value, index) => bytes[index] === value)) return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + if (view.getUint32(8, false) !== 13 || text(bytes, 12, 4) !== "IHDR") return null; + return { width: view.getUint32(16, false), height: view.getUint32(20, false) }; +} + +function webPDimensions(bytes: Uint8Array): { width: number; height: number } | null { + if (bytes.byteLength < 30 || text(bytes, 0, 4) !== "RIFF" || text(bytes, 8, 4) !== "WEBP") return null; + const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength); + if (view.getUint32(4, true) + 8 !== bytes.byteLength) return null; + const chunk = text(bytes, 12, 4); + if (chunk === "VP8X") { + const width = 1 + bytes[24] + (bytes[25] << 8) + (bytes[26] << 16); + const height = 1 + bytes[27] + (bytes[28] << 8) + (bytes[29] << 16); + return { width, height }; + } + if (chunk === "VP8L" && bytes[20] === 0x2f) { + const packed = view.getUint32(21, true); + return { width: (packed & 0x3fff) + 1, height: ((packed >>> 14) & 0x3fff) + 1 }; + } + if (chunk === "VP8 " && bytes[23] === 0x9d && bytes[24] === 0x01 && bytes[25] === 0x2a) { + return { width: view.getUint16(26, true) & 0x3fff, height: view.getUint16(28, true) & 0x3fff }; + } + return null; +} + +function encodedDimensions(mimeType: AssetPreviewContentMimeType, bytes: Uint8Array): { width: number; height: number } { + const dimensions = mimeType === "image/png" ? pngDimensions(bytes) : webPDimensions(bytes); + if (!dimensions) throw new AssetPreviewDecodeError("ASSET_MANIFEST_INVALID", `encoded bytes are not a valid ${mimeType} header`); + return dimensions; +} + +async function sha256(bytes: ArrayBuffer): Promise { + const result = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(result), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +function assertEncodedBudget(identity: AssetPreviewIdentityIR, encodedBytes: ArrayBuffer): void { + if (!(encodedBytes instanceof ArrayBuffer)) { + throw new AssetPreviewDecodeError("ASSET_MANIFEST_INVALID", "preview encoded payload must be an ArrayBuffer"); + } + if (identity.content.byteLength > ASSET_PREVIEW_DECODE_BUDGET.maxEncodedBytes || + encodedBytes.byteLength > ASSET_PREVIEW_DECODE_BUDGET.maxEncodedBytes) { + throw new AssetPreviewDecodeError("ASSET_BUDGET_EXCEEDED", "preview encoded byte length exceeds the pre-decode budget"); + } + if (encodedBytes.byteLength !== identity.content.byteLength) { + throw new AssetPreviewDecodeError("ASSET_SOURCE_HASH_MISMATCH", "preview encoded byte length does not match the identity"); + } +} + +export async function planAssetPreviewDecode(value: unknown, encodedBytes: ArrayBuffer): Promise { + const identity = await parseAssetPreviewIdentity(value); + assertEncodedBudget(identity, encodedBytes); + if (await sha256(encodedBytes) !== identity.content.sha256) { + throw new AssetPreviewDecodeError("ASSET_SOURCE_HASH_MISMATCH", "preview encoded SHA-256 does not match the identity"); + } + + const dimensions = encodedDimensions(identity.content.mimeType, new Uint8Array(encodedBytes)); + if (dimensions.width !== identity.content.width || dimensions.height !== identity.content.height) { + throw new AssetPreviewDecodeError("ASSET_MANIFEST_INVALID", "preview encoded dimensions do not match the identity"); + } + if (dimensions.width > ASSET_PREVIEW_DECODE_BUDGET.maxWidth || dimensions.height > ASSET_PREVIEW_DECODE_BUDGET.maxHeight) { + throw new AssetPreviewDecodeError("ASSET_BUDGET_EXCEEDED", "preview dimensions exceed the pre-decode budget"); + } + const pixelCount = dimensions.width * dimensions.height; + const decodedByteLength = pixelCount * 4; + if (!Number.isSafeInteger(pixelCount) || pixelCount > ASSET_PREVIEW_DECODE_BUDGET.maxPixels || + !Number.isSafeInteger(decodedByteLength) || decodedByteLength > ASSET_PREVIEW_DECODE_BUDGET.maxDecodedBytes) { + throw new AssetPreviewDecodeError("ASSET_BUDGET_EXCEEDED", "preview pixel or decoded byte count exceeds the pre-decode budget"); + } + const compressionRatio = decodedByteLength / encodedBytes.byteLength; + if (!Number.isFinite(compressionRatio) || compressionRatio > ASSET_PREVIEW_DECODE_BUDGET.maxCompressionRatio) { + throw new AssetPreviewDecodeError("ASSET_BUDGET_EXCEEDED", "preview compression ratio exceeds the pre-decode budget"); + } + return { + schemaVersion: ASSET_PREVIEW_DECODE_SCHEMA, + identitySha256: identity.identitySha256, + mimeType: identity.content.mimeType, + width: dimensions.width, + height: dimensions.height, + pixelCount, + encodedByteLength: encodedBytes.byteLength, + decodedByteLength, + compressionRatio, + }; +} diff --git a/web/protocol/asset-preview.ts b/web/protocol/asset-preview.ts new file mode 100644 index 00000000..c24a878e --- /dev/null +++ b/web/protocol/asset-preview.ts @@ -0,0 +1,202 @@ +import type { ErrorCode } from "./error"; + +export const ASSET_PREVIEW_IDENTITY_SCHEMA = 1 as const; +export const ASSET_PREVIEW_SOURCE_MIME_TYPES = [ + "image/png", + "image/webp", + "application/x-blender-preview-rgba8", +] as const; +export const ASSET_PREVIEW_CONTENT_MIME_TYPES = ["image/png", "image/webp"] as const; + +export type AssetPreviewSourceMimeType = (typeof ASSET_PREVIEW_SOURCE_MIME_TYPES)[number]; +export type AssetPreviewContentMimeType = (typeof ASSET_PREVIEW_CONTENT_MIME_TYPES)[number]; + +export interface AssetPreviewSourceIdentityIR { + mimeType: AssetPreviewSourceMimeType; + byteLength: number; + sha256: string; +} + +export interface AssetPreviewContentIdentityIR { + mimeType: AssetPreviewContentMimeType; + byteLength: number; + sha256: string; + width: number; + height: number; + pixelFormat: "RGBA8"; + colorSpace: "SRGB"; + alphaMode: "STRAIGHT" | "PREMULTIPLIED"; +} + +export interface AssetPreviewGeneratorIdentityIR { + name: string; + version: string; + executableSha256: string; + scriptSha256: string; + settingsSha256: string; +} + +export interface AssetPreviewIdentityIR { + schemaVersion: typeof ASSET_PREVIEW_IDENTITY_SCHEMA; + assetId: string; + slot: "PREVIEW"; + source: AssetPreviewSourceIdentityIR; + content: AssetPreviewContentIdentityIR; + generator: AssetPreviewGeneratorIdentityIR; + identitySha256: string; +} + +export class AssetPreviewIdentityError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "AssetPreviewIdentityError"; + this.code = code; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const encoder = new TextEncoder(); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record, allowed: readonly string[], path: string): void { + const keys = new Set(allowed); + if (Object.keys(value).some((key) => !keys.has(key))) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", `${path} contains unknown fields`); + } +} + +function text(value: unknown, path: string, maximum = 256): string { + if (typeof value !== "string" || value.length === 0 || encoder.encode(value).byteLength > maximum) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", `${path} is invalid`); + } + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", `${path} must be a lowercase SHA-256 digest`); + } + return value; +} + +function positiveInteger(value: unknown, path: string): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value <= 0) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", `${path} must be a positive safe integer`); + } + return value; +} + +function parseSource(value: unknown): AssetPreviewSourceIdentityIR { + if (!record(value)) throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "source is invalid"); + exactKeys(value, ["mimeType", "byteLength", "sha256"], "source"); + if (!ASSET_PREVIEW_SOURCE_MIME_TYPES.includes(value.mimeType as AssetPreviewSourceMimeType)) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "source.mimeType is unsupported"); + } + return { + mimeType: value.mimeType as AssetPreviewSourceMimeType, + byteLength: positiveInteger(value.byteLength, "source.byteLength"), + sha256: digest(value.sha256, "source.sha256"), + }; +} + +function parseContent(value: unknown): AssetPreviewContentIdentityIR { + if (!record(value)) throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "content is invalid"); + exactKeys(value, ["mimeType", "byteLength", "sha256", "width", "height", "pixelFormat", "colorSpace", "alphaMode"], "content"); + if (!ASSET_PREVIEW_CONTENT_MIME_TYPES.includes(value.mimeType as AssetPreviewContentMimeType) || + value.pixelFormat !== "RGBA8" || value.colorSpace !== "SRGB" || + !["STRAIGHT", "PREMULTIPLIED"].includes(value.alphaMode as string)) { + throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "content encoding profile is unsupported"); + } + return { + mimeType: value.mimeType as AssetPreviewContentMimeType, + byteLength: positiveInteger(value.byteLength, "content.byteLength"), + sha256: digest(value.sha256, "content.sha256"), + width: positiveInteger(value.width, "content.width"), + height: positiveInteger(value.height, "content.height"), + pixelFormat: "RGBA8", + colorSpace: "SRGB", + alphaMode: value.alphaMode as AssetPreviewContentIdentityIR["alphaMode"], + }; +} + +function parseGenerator(value: unknown): AssetPreviewGeneratorIdentityIR { + if (!record(value)) throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "generator is invalid"); + exactKeys(value, ["name", "version", "executableSha256", "scriptSha256", "settingsSha256"], "generator"); + return { + name: text(value.name, "generator.name", 128), + version: text(value.version, "generator.version", 64), + executableSha256: digest(value.executableSha256, "generator.executableSha256"), + scriptSha256: digest(value.scriptSha256, "generator.scriptSha256"), + settingsSha256: digest(value.settingsSha256, "generator.settingsSha256"), + }; +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + return JSON.stringify(value); +} + +async function sha256Bytes(bytes: ArrayBuffer): Promise { + const result = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(result), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function computeAssetPreviewIdentity(value: Omit): Promise { + return sha256Bytes(encoder.encode(stableJSON(value)).buffer); +} + +function parseBase(value: unknown): Omit { + if (!record(value) || value.schemaVersion !== ASSET_PREVIEW_IDENTITY_SCHEMA || value.slot !== "PREVIEW") { + throw new AssetPreviewIdentityError("PROTOCOL_MISMATCH", "unsupported asset preview identity schema or slot"); + } + return { + schemaVersion: ASSET_PREVIEW_IDENTITY_SCHEMA, + assetId: text(value.assetId, "assetId", 256), + slot: "PREVIEW", + source: parseSource(value.source), + content: parseContent(value.content), + generator: parseGenerator(value.generator), + }; +} + +export async function createAssetPreviewIdentity(value: Omit): Promise { + const parsed = parseBase(value); + return { ...parsed, identitySha256: await computeAssetPreviewIdentity(parsed) }; +} + +export async function parseAssetPreviewIdentity(value: unknown): Promise { + if (!record(value)) throw new AssetPreviewIdentityError("ASSET_MANIFEST_INVALID", "preview identity is invalid"); + exactKeys(value, ["schemaVersion", "assetId", "slot", "source", "content", "generator", "identitySha256"], "preview"); + const parsed = parseBase(value); + const identitySha256 = digest(value.identitySha256, "identitySha256"); + if (await computeAssetPreviewIdentity(parsed) !== identitySha256) { + throw new AssetPreviewIdentityError("ASSET_PREVIEW_IDENTITY_MISMATCH", "preview identity hash does not match its fields"); + } + return { ...parsed, identitySha256 }; +} + +export async function verifyAssetPreviewIdentity( + value: unknown, + sourceBytes: ArrayBuffer, + contentBytes: ArrayBuffer, + generator: AssetPreviewGeneratorIdentityIR, +): Promise { + const identity = await parseAssetPreviewIdentity(value); + const parsedGenerator = parseGenerator(generator); + if (stableJSON(parsedGenerator) !== stableJSON(identity.generator)) { + throw new AssetPreviewIdentityError("ASSET_PREVIEW_IDENTITY_MISMATCH", "preview generator identity changed"); + } + if (!(sourceBytes instanceof ArrayBuffer) || sourceBytes.byteLength !== identity.source.byteLength || await sha256Bytes(sourceBytes) !== identity.source.sha256) { + throw new AssetPreviewIdentityError("ASSET_SOURCE_HASH_MISMATCH", "preview source bytes do not match the source identity"); + } + if (!(contentBytes instanceof ArrayBuffer) || contentBytes.byteLength !== identity.content.byteLength || await sha256Bytes(contentBytes) !== identity.content.sha256) { + throw new AssetPreviewIdentityError("ASSET_SOURCE_HASH_MISMATCH", "preview content bytes do not match the content identity"); + } + return identity; +} diff --git a/web/protocol/compositor.ts b/web/protocol/compositor.ts index c31089f4..30ceb592 100644 --- a/web/protocol/compositor.ts +++ b/web/protocol/compositor.ts @@ -160,6 +160,13 @@ export class CompositorFrameCache { this.entries.set(key, { result: clone, byteLength }); this.currentBytes += byteLength; } + + clear(): number { + const releasedBytes = this.currentBytes; + this.entries.clear(); + this.currentBytes = 0; + return releasedBytes; + } } export class CompositorValidationError extends Error { diff --git a/web/protocol/error.ts b/web/protocol/error.ts index ba53acbf..f0c04814 100644 --- a/web/protocol/error.ts +++ b/web/protocol/error.ts @@ -160,6 +160,8 @@ export type ErrorCode = | "ASSET_BUDGET_EXCEEDED" | "ASSET_SOURCE_HASH_MISMATCH" | "ASSET_LICENSE_MISSING" + | "ASSET_SCHEMA_DOWNGRADE_BLOCKED" + | "ASSET_PREVIEW_IDENTITY_MISMATCH" | "LIBRARY_DEPENDENCY_CYCLE" | "LIBRARY_MUTATION_UNAVAILABLE" | "IO_FORMAT_UNSUPPORTED" diff --git a/web/protocol/library-main-append.ts b/web/protocol/library-main-append.ts new file mode 100644 index 00000000..bbe4f06b --- /dev/null +++ b/web/protocol/library-main-append.ts @@ -0,0 +1,173 @@ +import type { ErrorCode } from "./error"; +import { + parseLibraryOperationBinding, + type LibraryOperationBindingIR, +} from "./library-operation-identity"; + +export const LIBRARY_MAIN_APPEND_SCHEMA = 1 as const; +export const LIBRARY_MAIN_APPEND_MAX_SOURCE_BYTES = 64 * 1024 * 1024; + +export interface LibraryAppendClosureIR { + object: string; + mesh: string; + material: string; + image: string; +} + +export interface LibraryMainAppendRequestIR { + schemaVersion: typeof LIBRARY_MAIN_APPEND_SCHEMA; + baseRevision: number; + binding: LibraryOperationBindingIR; + expectedClosure: LibraryAppendClosureIR; +} + +export interface LibraryMainAppendMappingIR { + source: string; + local: string; + owner: "LOCAL_MAIN"; + readOnly: false; +} + +export interface LibraryMainAppendReceiptIR { + schemaVersion: typeof LIBRARY_MAIN_APPEND_SCHEMA; + operation: "APPEND"; + sourceLibraryId: string; + sourceDataBlockId: string; + dependencyClosureSha256: string; + baseRevision: number; + nextRevision: number; + transactionCount: 1; + mapping: LibraryMainAppendMappingIR[]; +} + +export class LibraryMainAppendError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "LibraryMainAppendError"; + this.code = code; + } +} + +const encoder = new TextEncoder(); +const PREFIXES = Object.freeze({ + object: "Object/", + mesh: "Mesh/", + material: "Material/", + image: "Image/", +}); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record, keys: readonly string[], path: string): void { + const allowed = new Set(keys); + if (Object.keys(value).length !== keys.length || Object.keys(value).some((key) => !allowed.has(key))) { + throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", `${path} fields are not exact`); + } +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256Text(value: string): Promise { + const digest = await crypto.subtle.digest("SHA-256", encoder.encode(value)); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function sha256LibraryBytes(value: ArrayBuffer): Promise { + if (!(value instanceof ArrayBuffer) || value.byteLength === 0 || value.byteLength > LIBRARY_MAIN_APPEND_MAX_SOURCE_BYTES) { + throw new LibraryMainAppendError("ASSET_BUDGET_EXCEEDED", "library source must contain 1..64 MiB"); + } + const digest = await crypto.subtle.digest("SHA-256", value); + return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export function parseLibraryAppendClosure(value: unknown): LibraryAppendClosureIR { + if (!record(value)) throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "expectedClosure must be an object"); + exactKeys(value, Object.keys(PREFIXES), "expectedClosure"); + const result = {} as Record; + for (const key of Object.keys(PREFIXES) as Array) { + const prefix = PREFIXES[key]; + const item = value[key]; + if (typeof item !== "string" || !item.startsWith(prefix) || item.length === prefix.length || + encoder.encode(item.slice(prefix.length)).byteLength > 63) { + throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", `expectedClosure.${key} is invalid`); + } + result[key] = item; + } + if (new Set(Object.values(result)).size !== 4) { + throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "append closure IDs must be unique"); + } + return result; +} + +export async function computeLibraryAppendClosureSha256(value: unknown): Promise { + const closure = parseLibraryAppendClosure(value); + return sha256Text(stableJSON({ schema: "BLENDER_APPEND_OBJECT_CLOSURE_V1", ...closure })); +} + +export async function parseLibraryMainAppendRequest( + value: unknown, + currentRevision?: number, +): Promise { + if (!record(value) || value.schemaVersion !== LIBRARY_MAIN_APPEND_SCHEMA) { + throw new LibraryMainAppendError("PROTOCOL_MISMATCH", "unsupported library Main append schema"); + } + exactKeys(value, ["schemaVersion", "baseRevision", "binding", "expectedClosure"], "request"); + if (typeof value.baseRevision !== "number" || !Number.isSafeInteger(value.baseRevision) || value.baseRevision < 0) { + throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "baseRevision must be a non-negative safe integer"); + } + if (currentRevision !== undefined && value.baseRevision !== currentRevision) { + throw new LibraryMainAppendError("REVISION_CONFLICT", "library append base revision is stale"); + } + const binding = await parseLibraryOperationBinding(value.binding); + const expectedClosure = parseLibraryAppendClosure(value.expectedClosure); + if (binding.operation !== "APPEND" || binding.owner.kind !== "LOCAL_MAIN" || binding.readOnly || binding.referenceReadOnly) { + throw new LibraryMainAppendError("ASSET_MANIFEST_INVALID", "library Main append requires a writable LOCAL_MAIN binding"); + } + if (binding.sourceDataBlockId !== expectedClosure.object || binding.owner.localDataBlockId !== expectedClosure.object) { + throw new LibraryMainAppendError("ASSET_SOURCE_HASH_MISMATCH", "append root and local owner do not match the expected Object"); + } + if (binding.dependencyClosureSha256 !== await computeLibraryAppendClosureSha256(expectedClosure)) { + throw new LibraryMainAppendError("REVISION_CONFLICT", "append dependency closure binding is stale"); + } + return { + schemaVersion: LIBRARY_MAIN_APPEND_SCHEMA, + baseRevision: value.baseRevision, + binding, + expectedClosure, + }; +} + +export function createLibraryMainAppendReceipt( + request: LibraryMainAppendRequestIR, + nextRevision: number, +): LibraryMainAppendReceiptIR { + if (!Number.isSafeInteger(nextRevision) || nextRevision !== request.baseRevision + 1) { + throw new LibraryMainAppendError("REVISION_CONFLICT", "library append must advance exactly one Main revision"); + } + return { + schemaVersion: LIBRARY_MAIN_APPEND_SCHEMA, + operation: "APPEND", + sourceLibraryId: request.binding.source.sourceLibraryId, + sourceDataBlockId: request.binding.sourceDataBlockId, + dependencyClosureSha256: request.binding.dependencyClosureSha256, + baseRevision: request.baseRevision, + nextRevision, + transactionCount: 1, + mapping: Object.values(request.expectedClosure).map((source) => ({ + source, + local: source, + owner: "LOCAL_MAIN", + readOnly: false, + })), + }; +} diff --git a/web/protocol/library-operation-identity.ts b/web/protocol/library-operation-identity.ts new file mode 100644 index 00000000..fc3c64e5 --- /dev/null +++ b/web/protocol/library-operation-identity.ts @@ -0,0 +1,292 @@ +import type { ErrorCode } from "./error"; + +export const LIBRARY_OPERATION_IDENTITY_SCHEMA = 1 as const; +export const LIBRARY_OPERATION_IDENTITY_BUDGET = Object.freeze({ + maxSourceLocatorBytes: 4_096, + maxDataBlockIdBytes: 1_024, + maxOwnerIdBytes: 1_024, +}); + +export type LibraryOperation = "APPEND" | "LINK" | "LIBRARY_OVERRIDE"; + +export interface LibrarySourceIdentityIR { + schemaVersion: typeof LIBRARY_OPERATION_IDENTITY_SCHEMA; + sourceLibraryId: string; + sourceLocator: string; + sourceSha256: string; +} + +export interface LocalMainOwnerIR { + kind: "LOCAL_MAIN"; + projectId: string; + localDataBlockId: string; +} + +export interface SourceLibraryOwnerIR { + kind: "SOURCE_LIBRARY"; + sourceLibraryId: string; + sourceDataBlockId: string; +} + +export interface LocalOverrideOwnerIR { + kind: "LOCAL_OVERRIDE"; + projectId: string; + localDataBlockId: string; + referenceSourceDataBlockId: string; + hierarchyRootDataBlockId: string; +} + +export type LibraryOperationOwnerIR = LocalMainOwnerIR | SourceLibraryOwnerIR | LocalOverrideOwnerIR; + +export interface LibraryOperationBindingIR { + schemaVersion: typeof LIBRARY_OPERATION_IDENTITY_SCHEMA; + operation: LibraryOperation; + source: LibrarySourceIdentityIR; + sourceDataBlockId: string; + owner: LibraryOperationOwnerIR; + readOnly: boolean; + referenceReadOnly: boolean; + sourceGeneration: number; + sourceRevision: number; + dependencyClosureSha256: string; + invalidationToken: string; +} + +export interface LibrarySourceStateIR { + sourceLibraryId: string; + sourceSha256: string; + sourceGeneration: number; + sourceRevision: number; + dependencyClosureSha256: string; +} + +export class LibraryOperationIdentityError extends Error { + readonly code: ErrorCode; + + constructor(code: ErrorCode, message: string) { + super(`${code}: ${message}`); + this.name = "LibraryOperationIdentityError"; + this.code = code; + } +} + +const SHA256 = /^[a-f0-9]{64}$/; +const LIBRARY_ID = /^library:[a-f0-9]{64}$/; +const INVALIDATION_TOKEN = /^libtoken:[a-f0-9]{64}$/; +const encoder = new TextEncoder(); + +function record(value: unknown): value is Record { + return typeof value === "object" && value !== null && !Array.isArray(value); +} + +function exactKeys(value: Record, allowed: readonly string[], path: string): void { + const keys = new Set(allowed); + if (Object.keys(value).some((key) => !keys.has(key))) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", `${path} contains undeclared fields`); + } +} + +function text(value: unknown, path: string, maximumBytes: number): string { + if (typeof value !== "string" || value.length === 0 || encoder.encode(value).byteLength > maximumBytes) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", `${path} is outside its UTF-8 byte budget`); + } + return value; +} + +function digest(value: unknown, path: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", `${path} must be a lowercase SHA-256 digest`); + } + return value; +} + +function integer(value: unknown, path: string, minimum: number): number { + if (typeof value !== "number" || !Number.isSafeInteger(value) || value < minimum) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", `${path} must be a safe integer >= ${minimum}`); + } + return value; +} + +function stableJSON(value: unknown): string { + if (Array.isArray(value)) return `[${value.map(stableJSON).join(",")}]`; + if (record(value)) { + return `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}`; + } + return JSON.stringify(value); +} + +async function sha256(value: string): Promise { + const result = await crypto.subtle.digest("SHA-256", encoder.encode(value)); + return Array.from(new Uint8Array(result), (byte) => byte.toString(16).padStart(2, "0")).join(""); +} + +export async function computeLibrarySourceId(value: { sourceLocator: string; sourceSha256: string }): Promise { + return `library:${await sha256(stableJSON({ + schema: "BLENDER_LIBRARY_SOURCE_V1", + sourceLocator: value.sourceLocator, + sourceSha256: value.sourceSha256, + }))}`; +} + +export async function createLibrarySourceIdentity(value: { + sourceLocator: string; + sourceSha256: string; +}): Promise { + const sourceLocator = text(value.sourceLocator, "sourceLocator", LIBRARY_OPERATION_IDENTITY_BUDGET.maxSourceLocatorBytes); + const sourceSha256 = digest(value.sourceSha256, "sourceSha256"); + return { + schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA, + sourceLibraryId: await computeLibrarySourceId({ sourceLocator, sourceSha256 }), + sourceLocator, + sourceSha256, + }; +} + +export async function parseLibrarySourceIdentity(value: unknown): Promise { + if (!record(value) || value.schemaVersion !== LIBRARY_OPERATION_IDENTITY_SCHEMA) { + throw new LibraryOperationIdentityError("PROTOCOL_MISMATCH", "unsupported library source identity schema"); + } + exactKeys(value, ["schemaVersion", "sourceLibraryId", "sourceLocator", "sourceSha256"], "source"); + const sourceLocator = text(value.sourceLocator, "source.sourceLocator", LIBRARY_OPERATION_IDENTITY_BUDGET.maxSourceLocatorBytes); + const sourceSha256 = digest(value.sourceSha256, "source.sourceSha256"); + if (typeof value.sourceLibraryId !== "string" || !LIBRARY_ID.test(value.sourceLibraryId) || + value.sourceLibraryId !== await computeLibrarySourceId({ sourceLocator, sourceSha256 })) { + throw new LibraryOperationIdentityError("ASSET_SOURCE_HASH_MISMATCH", "sourceLibraryId does not bind locator and source SHA-256"); + } + return { + schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA, + sourceLibraryId: value.sourceLibraryId, + sourceLocator, + sourceSha256, + }; +} + +function parseOwner(value: unknown, operation: LibraryOperation, source: LibrarySourceIdentityIR, sourceDataBlockId: string): LibraryOperationOwnerIR { + if (!record(value)) throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "owner must be an object"); + if (operation === "APPEND") { + exactKeys(value, ["kind", "projectId", "localDataBlockId"], "owner"); + if (value.kind !== "LOCAL_MAIN") throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "APPEND owner must be LOCAL_MAIN"); + return { + kind: "LOCAL_MAIN", + projectId: text(value.projectId, "owner.projectId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxOwnerIdBytes), + localDataBlockId: text(value.localDataBlockId, "owner.localDataBlockId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxDataBlockIdBytes), + }; + } + if (operation === "LINK") { + exactKeys(value, ["kind", "sourceLibraryId", "sourceDataBlockId"], "owner"); + if (value.kind !== "SOURCE_LIBRARY" || value.sourceLibraryId !== source.sourceLibraryId || value.sourceDataBlockId !== sourceDataBlockId) { + throw new LibraryOperationIdentityError("ASSET_SOURCE_HASH_MISMATCH", "LINK owner must match the source library and data-block"); + } + return { kind: "SOURCE_LIBRARY", sourceLibraryId: source.sourceLibraryId, sourceDataBlockId }; + } + exactKeys(value, ["kind", "projectId", "localDataBlockId", "referenceSourceDataBlockId", "hierarchyRootDataBlockId"], "owner"); + if (value.kind !== "LOCAL_OVERRIDE" || value.referenceSourceDataBlockId !== sourceDataBlockId) { + throw new LibraryOperationIdentityError("ASSET_SOURCE_HASH_MISMATCH", "LIBRARY_OVERRIDE owner must retain the source reference"); + } + return { + kind: "LOCAL_OVERRIDE", + projectId: text(value.projectId, "owner.projectId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxOwnerIdBytes), + localDataBlockId: text(value.localDataBlockId, "owner.localDataBlockId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxDataBlockIdBytes), + referenceSourceDataBlockId: sourceDataBlockId, + hierarchyRootDataBlockId: text(value.hierarchyRootDataBlockId, "owner.hierarchyRootDataBlockId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxDataBlockIdBytes), + }; +} + +type LibraryOperationBindingInput = Omit; + +function assertOwnershipSemantics(operation: LibraryOperation, readOnly: boolean, referenceReadOnly: boolean): void { + const valid = operation === "APPEND" + ? !readOnly && !referenceReadOnly + : operation === "LINK" + ? readOnly && referenceReadOnly + : !readOnly && referenceReadOnly; + if (!valid) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", `${operation} read-only semantics are invalid`); + } +} + +async function parseBindingBase(value: unknown): Promise { + if (!record(value) || value.schemaVersion !== LIBRARY_OPERATION_IDENTITY_SCHEMA) { + throw new LibraryOperationIdentityError("PROTOCOL_MISMATCH", "unsupported library operation binding schema"); + } + const operation = value.operation; + if (operation !== "APPEND" && operation !== "LINK" && operation !== "LIBRARY_OVERRIDE") { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "operation is invalid"); + } + const source = await parseLibrarySourceIdentity(value.source); + const sourceDataBlockId = text(value.sourceDataBlockId, "sourceDataBlockId", LIBRARY_OPERATION_IDENTITY_BUDGET.maxDataBlockIdBytes); + if (typeof value.readOnly !== "boolean" || typeof value.referenceReadOnly !== "boolean") { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "read-only fields must be boolean"); + } + assertOwnershipSemantics(operation, value.readOnly, value.referenceReadOnly); + return { + schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA, + operation, + source, + sourceDataBlockId, + owner: parseOwner(value.owner, operation, source, sourceDataBlockId), + readOnly: value.readOnly, + referenceReadOnly: value.referenceReadOnly, + sourceGeneration: integer(value.sourceGeneration, "sourceGeneration", 1), + sourceRevision: integer(value.sourceRevision, "sourceRevision", 0), + dependencyClosureSha256: digest(value.dependencyClosureSha256, "dependencyClosureSha256"), + }; +} + +export async function computeLibraryInvalidationToken(value: LibraryOperationBindingInput): Promise { + return `libtoken:${await sha256(stableJSON(value))}`; +} + +export async function createLibraryOperationBinding(value: LibraryOperationBindingInput): Promise { + if (!record(value)) throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "binding must be an object"); + exactKeys(value, [ + "schemaVersion", "operation", "source", "sourceDataBlockId", "owner", "readOnly", + "referenceReadOnly", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", + ], "binding"); + const parsed = await parseBindingBase(value); + return { ...parsed, invalidationToken: await computeLibraryInvalidationToken(parsed) }; +} + +export async function parseLibraryOperationBinding(value: unknown): Promise { + if (!record(value)) throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "binding must be an object"); + exactKeys(value, [ + "schemaVersion", "operation", "source", "sourceDataBlockId", "owner", "readOnly", + "referenceReadOnly", "sourceGeneration", "sourceRevision", "dependencyClosureSha256", + "invalidationToken", + ], "binding"); + const parsed = await parseBindingBase(value); + if (typeof value.invalidationToken !== "string" || !INVALIDATION_TOKEN.test(value.invalidationToken) || + value.invalidationToken !== await computeLibraryInvalidationToken(parsed)) { + throw new LibraryOperationIdentityError("REVISION_CONFLICT", "library operation invalidation token is stale or forged"); + } + return { ...parsed, invalidationToken: value.invalidationToken }; +} + +function parseSourceState(value: unknown): LibrarySourceStateIR { + if (!record(value)) throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "source state must be an object"); + exactKeys(value, ["sourceLibraryId", "sourceSha256", "sourceGeneration", "sourceRevision", "dependencyClosureSha256"], "source state"); + if (typeof value.sourceLibraryId !== "string" || !LIBRARY_ID.test(value.sourceLibraryId)) { + throw new LibraryOperationIdentityError("ASSET_MANIFEST_INVALID", "source state library ID is invalid"); + } + return { + sourceLibraryId: value.sourceLibraryId, + sourceSha256: digest(value.sourceSha256, "source state sourceSha256"), + sourceGeneration: integer(value.sourceGeneration, "source state sourceGeneration", 1), + sourceRevision: integer(value.sourceRevision, "source state sourceRevision", 0), + dependencyClosureSha256: digest(value.dependencyClosureSha256, "source state dependencyClosureSha256"), + }; +} + +export async function assertLibraryOperationBindingCurrent( + bindingValue: unknown, + sourceStateValue: unknown, +): Promise { + const binding = await parseLibraryOperationBinding(bindingValue); + const state = parseSourceState(sourceStateValue); + if (binding.source.sourceLibraryId !== state.sourceLibraryId || binding.source.sourceSha256 !== state.sourceSha256 || + binding.sourceGeneration !== state.sourceGeneration || binding.sourceRevision !== state.sourceRevision || + binding.dependencyClosureSha256 !== state.dependencyClosureSha256) { + throw new LibraryOperationIdentityError("REVISION_CONFLICT", "library source or dependency closure has been invalidated"); + } + return binding; +} diff --git a/web/protocol/render-compositor-media-recovery.ts b/web/protocol/render-compositor-media-recovery.ts new file mode 100644 index 00000000..c4bb0506 --- /dev/null +++ b/web/protocol/render-compositor-media-recovery.ts @@ -0,0 +1,197 @@ +export const RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION = 1 as const; +export const RENDER_COMPOSITOR_MEDIA_DOMAINS = ["RENDER", "COMPOSITOR", "MEDIA"] as const; +export type RenderCompositorMediaDomain = typeof RENDER_COMPOSITOR_MEDIA_DOMAINS[number]; + +const SHA256 = /^[a-f0-9]{64}$/; +const CANCELLATION_CODES = { + RENDER: "OPEN_CANCELLED", + COMPOSITOR: "COMPOSITOR_CANCELLED", + MEDIA: "SEQUENCER_CANCELLED", +} as const; +const BUDGET_CODES = { + RENDER: "GPU_TEXTURE_BUDGET_EXCEEDED", + COMPOSITOR: "COMPOSITOR_BUDGET_EXCEEDED", + MEDIA: "SEQUENCER_BUDGET_EXCEEDED", +} as const; + +export interface RenderCompositorMediaRecoveryEvidenceIR { + schemaVersion: typeof RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION; + domain: RenderCompositorMediaDomain; + source: { + byteLength: number; + sha256: string; + }; + cancellation: { + status: "CANCELLED"; + code: typeof CANCELLATION_CODES[RenderCompositorMediaDomain]; + publishedResults: 0; + temporaryResourcesAfter: 0; + }; + restart: { + status: "RECOVERED"; + generationBefore: 1; + generationAfter: 2; + identityBefore: string; + identityAfter: string; + outputSha256Before: string; + outputSha256After: string; + }; + budget: { + status: "BLOCKED"; + code: typeof BUDGET_CODES[RenderCompositorMediaDomain]; + retainedIdentityHash: string; + temporaryResourcesAfter: 0; + }; + release: { + status: "RELEASED"; + releasedBytes: number; + releasedResources: number; + resourcesAfter: 0; + }; + recovery: { + status: "RECOVERED"; + identityHash: string; + outputSha256: string; + outputBytes: number; + visibleUnits: number; + }; +} + +function record(value: unknown, label: string): Record { + if (typeof value !== "object" || value === null || Array.isArray(value)) { + throw new Error(`M11_RECOVERY_INVALID: ${label} must be an object`); + } + return value as Record; +} + +function exact(value: Record, fields: readonly string[], label: string): void { + const allowed = new Set(fields); + const extra = Object.keys(value).find((field) => !allowed.has(field)); + if (extra) throw new Error(`M11_RECOVERY_INVALID: ${label}.${extra} is undeclared`); +} + +function integer(value: unknown, label: string, minimum = 0): number { + if (!Number.isSafeInteger(value) || (value as number) < minimum) { + throw new Error(`M11_RECOVERY_INVALID: ${label} must be a safe integer >= ${minimum}`); + } + return value as number; +} + +function digest(value: unknown, label: string): string { + if (typeof value !== "string" || !SHA256.test(value)) { + throw new Error(`M11_RECOVERY_INVALID: ${label} must be a SHA-256 digest`); + } + return value; +} + +function zero(value: unknown, label: string): 0 { + if (value !== 0) throw new Error(`M11_RECOVERY_INVALID: ${label} must be zero`); + return 0; +} + +export function parseRenderCompositorMediaRecoveryEvidence(value: unknown): RenderCompositorMediaRecoveryEvidenceIR { + const input = record(value, "evidence"); + exact(input, ["schemaVersion", "domain", "source", "cancellation", "restart", "budget", "release", "recovery"], "evidence"); + if (input.schemaVersion !== RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION) { + throw new Error("M11_RECOVERY_INVALID: unsupported schema version"); + } + if (!RENDER_COMPOSITOR_MEDIA_DOMAINS.includes(input.domain as RenderCompositorMediaDomain)) { + throw new Error("M11_RECOVERY_INVALID: domain is invalid"); + } + const domain = input.domain as RenderCompositorMediaDomain; + + const source = record(input.source, "source"); + exact(source, ["byteLength", "sha256"], "source"); + const sourceByteLength = integer(source.byteLength, "source.byteLength", 1); + const sourceSha256 = digest(source.sha256, "source.sha256"); + + const cancellation = record(input.cancellation, "cancellation"); + exact(cancellation, ["status", "code", "publishedResults", "temporaryResourcesAfter"], "cancellation"); + if (cancellation.status !== "CANCELLED" || cancellation.code !== CANCELLATION_CODES[domain]) { + throw new Error(`M11_RECOVERY_INVALID: ${domain} cancellation contract is invalid`); + } + zero(cancellation.publishedResults, "cancellation.publishedResults"); + zero(cancellation.temporaryResourcesAfter, "cancellation.temporaryResourcesAfter"); + + const restart = record(input.restart, "restart"); + exact(restart, ["status", "generationBefore", "generationAfter", "identityBefore", "identityAfter", "outputSha256Before", "outputSha256After"], "restart"); + if (restart.status !== "RECOVERED" || restart.generationBefore !== 1 || restart.generationAfter !== 2) { + throw new Error("M11_RECOVERY_INVALID: restart must advance exactly from generation 1 to 2"); + } + const identityBefore = digest(restart.identityBefore, "restart.identityBefore"); + const identityAfter = digest(restart.identityAfter, "restart.identityAfter"); + const outputSha256Before = digest(restart.outputSha256Before, "restart.outputSha256Before"); + const outputSha256After = digest(restart.outputSha256After, "restart.outputSha256After"); + if (identityBefore !== identityAfter || outputSha256Before !== outputSha256After) { + throw new Error("M11_RECOVERY_INVALID: restart changed committed identity or output"); + } + + const budget = record(input.budget, "budget"); + exact(budget, ["status", "code", "retainedIdentityHash", "temporaryResourcesAfter"], "budget"); + if (budget.status !== "BLOCKED" || budget.code !== BUDGET_CODES[domain]) { + throw new Error(`M11_RECOVERY_INVALID: ${domain} budget contract is invalid`); + } + const retainedIdentityHash = digest(budget.retainedIdentityHash, "budget.retainedIdentityHash"); + if (retainedIdentityHash !== identityAfter) { + throw new Error("M11_RECOVERY_INVALID: budget failure changed committed identity"); + } + zero(budget.temporaryResourcesAfter, "budget.temporaryResourcesAfter"); + + const release = record(input.release, "release"); + exact(release, ["status", "releasedBytes", "releasedResources", "resourcesAfter"], "release"); + if (release.status !== "RELEASED") throw new Error("M11_RECOVERY_INVALID: release.status"); + const releasedBytes = integer(release.releasedBytes, "release.releasedBytes", 1); + const releasedResources = integer(release.releasedResources, "release.releasedResources", 1); + zero(release.resourcesAfter, "release.resourcesAfter"); + + const recovery = record(input.recovery, "recovery"); + exact(recovery, ["status", "identityHash", "outputSha256", "outputBytes", "visibleUnits"], "recovery"); + if (recovery.status !== "RECOVERED") throw new Error("M11_RECOVERY_INVALID: recovery.status"); + const recoveryIdentity = digest(recovery.identityHash, "recovery.identityHash"); + const recoveryOutput = digest(recovery.outputSha256, "recovery.outputSha256"); + if (recoveryIdentity !== identityAfter || recoveryOutput !== outputSha256After) { + throw new Error("M11_RECOVERY_INVALID: recovered identity or output does not match generation 2"); + } + const outputBytes = integer(recovery.outputBytes, "recovery.outputBytes", 1); + const visibleUnits = integer(recovery.visibleUnits, "recovery.visibleUnits", 1); + + return { + schemaVersion: RENDER_COMPOSITOR_MEDIA_RECOVERY_SCHEMA_VERSION, + domain, + source: { byteLength: sourceByteLength, sha256: sourceSha256 }, + cancellation: { + status: "CANCELLED", + code: CANCELLATION_CODES[domain], + publishedResults: 0, + temporaryResourcesAfter: 0, + }, + restart: { + status: "RECOVERED", + generationBefore: 1, + generationAfter: 2, + identityBefore, + identityAfter, + outputSha256Before, + outputSha256After, + }, + budget: { + status: "BLOCKED", + code: BUDGET_CODES[domain], + retainedIdentityHash, + temporaryResourcesAfter: 0, + }, + release: { status: "RELEASED", releasedBytes, releasedResources, resourcesAfter: 0 }, + recovery: { status: "RECOVERED", identityHash: recoveryIdentity, outputSha256: recoveryOutput, outputBytes, visibleUnits }, + }; +} + +export function parseRenderCompositorMediaRecoverySuite(value: unknown): RenderCompositorMediaRecoveryEvidenceIR[] { + if (!Array.isArray(value) || value.length !== RENDER_COMPOSITOR_MEDIA_DOMAINS.length) { + throw new Error("M11_RECOVERY_INVALID: suite must contain exactly three domains"); + } + const reports = value.map(parseRenderCompositorMediaRecoveryEvidence); + if (new Set(reports.map((report) => report.domain)).size !== RENDER_COMPOSITOR_MEDIA_DOMAINS.length) { + throw new Error("M11_RECOVERY_INVALID: suite contains duplicate domains"); + } + return RENDER_COMPOSITOR_MEDIA_DOMAINS.map((domain) => reports.find((report) => report.domain === domain)!); +} diff --git a/web/protocol/web-engine.ts b/web/protocol/web-engine.ts index d70bdbd0..6ed29fa6 100644 --- a/web/protocol/web-engine.ts +++ b/web/protocol/web-engine.ts @@ -23,6 +23,7 @@ import type { PaintStrokeSessionReceiptIR, } from "./paint-stroke-session"; import type { PaintPBVHCapabilityRequest } from "./paint-pbvh-capability"; +import type { LibraryMainAppendReceiptIR, LibraryMainAppendRequestIR } from "./library-main-append"; export interface MeshGeometryBuffer { schemaVersion: 1; @@ -154,6 +155,7 @@ export type WebEngineRequest = | { requestId: string; command: { type: "openResourceStatus" } } | { requestId: string; command: { type: "snapshot" } } | { requestId: string; command: { type: "applyCommand"; payload: WebEngineEditCommand } } + | { requestId: string; command: { type: "appendLibraryObject"; request: LibraryMainAppendRequestIR; source: ArrayBuffer } } | { requestId: string; command: { type: "beginPaintStroke"; session: PaintStrokeSessionBeginIR } } | { requestId: string; command: { type: "appendPaintStrokeChunk"; chunk: PaintStrokeSessionChunkIR } } | { requestId: string; command: { type: "commitPaintStroke"; session: PaintStrokeSessionCommitIR } } @@ -232,6 +234,7 @@ export interface WebEngineResult { blend?: ArrayBuffer; openResources?: WebEngineOpenResourceStatus; paintStrokeSession?: PaintStrokeSessionReceiptIR; + libraryAppend?: LibraryMainAppendReceiptIR; } export type WebEngineResponse = diff --git a/web/tests/e2e/asset-catalog-indexeddb-migration.spec.ts b/web/tests/e2e/asset-catalog-indexeddb-migration.spec.ts new file mode 100644 index 00000000..1a99bb8b --- /dev/null +++ b/web/tests/e2e/asset-catalog-indexeddb-migration.spec.ts @@ -0,0 +1,114 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const fixture = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v1.json"), + "utf8", +)) as unknown; + +test("M12-01G aborts catalog migration without changing the old IndexedDB index", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async (source) => { + const storage = await import("/src/storage/asset-catalog-indexeddb.ts"); + const open = (name: string): Promise => new Promise((resolve, reject) => { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => { + request.result.createObjectStore("setting", { keyPath: "id" }); + request.result.createObjectStore("migration", { keyPath: "id" }); + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + const seed = async (database: IDBDatabase): Promise => { + const transaction = database.transaction(["setting", "migration"], "readwrite"); + transaction.objectStore("setting").put({ id: storage.ASSET_CATALOG_INDEX_V1_ID, value: source }); + transaction.objectStore("migration").put({ id: "schema-legacy", version: 1, marker: "unchanged" }); + await complete(transaction); + }; + const get = (store: IDBObjectStore, id: string): Promise => new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const snapshot = async (database: IDBDatabase) => { + const transaction = database.transaction(["setting", "migration"], "readonly"); + const settings = transaction.objectStore("setting"); + const migrations = transaction.objectStore("migration"); + const values = await Promise.all([ + get(settings, storage.ASSET_CATALOG_INDEX_V1_ID), + get(settings, storage.ASSET_CATALOG_INDEX_V2_ID), + get(migrations, storage.ASSET_CATALOG_MIGRATION_ID), + get(migrations, "schema-legacy"), + ]); + await complete(transaction); + return { version: database.version, source: values[0], target: values[1], receipt: values[2], legacy: values[3] }; + }; + const faultCode = async (database: IDBDatabase, faultAt: "AFTER_TARGET_PUT" | "AFTER_SOURCE_DELETE"): Promise => { + try { + await storage.migrateAssetCatalogIndexedDB(database, { faultAt }); + return ""; + } + catch (error) { + return String((error as Error & { code?: string }).code ?? ""); + } + }; + + const databaseName = `m12-01g-${crypto.randomUUID()}`; + let database = await open(databaseName); + await seed(database); + const baseline = await snapshot(database); + const afterTargetCode = await faultCode(database, "AFTER_TARGET_PUT"); + database.close(); + database = await open(databaseName); + const afterTargetFault = await snapshot(database); + const afterDeleteCode = await faultCode(database, "AFTER_SOURCE_DELETE"); + database.close(); + database = await open(databaseName); + const afterDeleteFault = await snapshot(database); + const migrated = await storage.migrateAssetCatalogIndexedDB(database); + const committed = await snapshot(database); + database.close(); + indexedDB.deleteDatabase(databaseName); + return { + baseline, + afterTargetCode, + afterTargetFault, + afterDeleteCode, + afterDeleteFault, + migrated: { + status: migrated.status, + revision: migrated.manifest.revision, + sourceHash: migrated.receipt.sourceManifestSha256, + targetHash: migrated.receipt.targetManifestSha256, + }, + committed, + }; + }, fixture); + + expect(result.afterTargetCode).toBe("STORAGE_TRANSACTION"); + expect(result.afterDeleteCode).toBe("STORAGE_TRANSACTION"); + expect(result.afterTargetFault).toEqual(result.baseline); + expect(result.afterDeleteFault).toEqual(result.baseline); + expect(result.baseline.version).toBe(1); + expect(result.baseline.source).toEqual({ id: "asset-catalog:index:v1", value: fixture }); + expect(result.baseline.target).toBeUndefined(); + expect(result.baseline.receipt).toBeUndefined(); + expect(result.baseline.legacy).toEqual({ id: "schema-legacy", version: 1, marker: "unchanged" }); + expect(result.migrated).toEqual({ + status: "MIGRATED", + revision: 7, + sourceHash: "cbdc8b03dc9ce1f38b44bae1c7459eb273126cbd6c7dd33115d2b1966e975881", + targetHash: "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + }); + expect(result.committed.version).toBe(1); + expect(result.committed.source).toBeUndefined(); + expect(result.committed.target).toMatchObject({ id: "asset-catalog:index:v2", value: { schemaVersion: 2, revision: 7 } }); + expect(result.committed.receipt).toMatchObject({ id: "asset-catalog:migration:v1-to-v2", task: "M12-01G", status: "MIGRATED" }); + expect(result.committed.legacy).toEqual(result.baseline.legacy); +}); diff --git a/web/tests/e2e/asset-catalog-restart.spec.ts b/web/tests/e2e/asset-catalog-restart.spec.ts new file mode 100644 index 00000000..9a149196 --- /dev/null +++ b/web/tests/e2e/asset-catalog-restart.spec.ts @@ -0,0 +1,91 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const fixture = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v1.json"), + "utf8", +)) as unknown; + +test("M12-01H preserves catalog order and asset identity across page and Worker restarts", async ({ page }) => { + await page.goto("/"); + const databaseName = `m12-01h-${Date.now()}`; + const baseline = await page.evaluate(async ({ databaseName, source }) => { + const storage = await import("/src/storage/asset-catalog-indexeddb.ts"); + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open(databaseName, 1); + request.onupgradeneeded = () => { + request.result.createObjectStore("setting", { keyPath: "id" }); + request.result.createObjectStore("migration", { keyPath: "id" }); + }; + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const transaction = database.transaction("setting", "readwrite"); + transaction.objectStore("setting").put({ id: storage.ASSET_CATALOG_INDEX_V1_ID, value: source }); + await new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + const snapshot = await storage.loadAssetCatalogIndexedDBSnapshot(database); + database.close(); + return snapshot; + }, { databaseName, source: fixture }); + + await page.reload(); + const pageRestart = await page.evaluate(async (name) => { + const storage = await import("/src/storage/asset-catalog-indexeddb.ts"); + const database = await new Promise((resolve, reject) => { + const request = indexedDB.open(name); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const snapshot = await storage.loadAssetCatalogIndexedDBSnapshot(database); + database.close(); + return snapshot; + }, databaseName); + + const workerSnapshot = async () => page.evaluate((name) => new Promise((resolve, reject) => { + const worker = new Worker("/src/workers/asset-catalog-restart-test.worker.ts", { type: "module" }); + worker.onmessage = (event) => { + worker.terminate(); + event.data.ok ? resolve(event.data.snapshot) : reject(new Error(event.data.error)); + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ databaseName: name }); + }), databaseName); + const firstWorker = await workerSnapshot(); + const secondWorker = await workerSnapshot(); + + expect(pageRestart).toEqual(baseline); + expect(firstWorker).toEqual(baseline); + expect(secondWorker).toEqual(baseline); + expect(baseline).toEqual({ + schemaVersion: 1, + revision: 7, + manifestSha256: "0c08ed1af1dd0998c809f76d969fd21d8fdae28ad37cf5571bdc99b16bf3ec90", + catalogOrder: [ + { catalogId: "44444444-4444-4444-8444-444444444444", path: "Animation" }, + { catalogId: "fe3ca14c-95d7-549f-a13b-1bb4c07b6074", path: "Characters" }, + { catalogId: "b4bb3608-9267-5aba-bb9e-9e97b006d5ab", path: "Characters/Heroes" }, + ], + assetOrder: [ + "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + ], + assetIdentities: [ + { + assetId: "asset:ac88c6147ada877adccf4df8ca54d64e1a9cb50a13514212f6d19b1da1660f8d", + assetLibraryIdentifier: null, + relativeAssetIdentifier: "Action/Legacy Walk", + }, + { + assetId: "asset:bb91ef0dddf69bb7d87dd4217a01e7f549b50ad628db0367d3896c6840b3a326", + assetLibraryIdentifier: null, + relativeAssetIdentifier: "Object/Legacy Hero", + }, + ], + }); + await page.evaluate((name) => indexedDB.deleteDatabase(name), databaseName); +}); diff --git a/web/tests/e2e/asset-preview-dedup.spec.ts b/web/tests/e2e/asset-preview-dedup.spec.ts new file mode 100644 index 00000000..a1d701d8 --- /dev/null +++ b/web/tests/e2e/asset-preview-dedup.spec.ts @@ -0,0 +1,113 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const catalog = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), + "utf8", +)) as Record; +const identity = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), + "utf8", +)) as Record; +const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); + +test("M12-02E deduplicates preview content without merging asset metadata", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { + const storage = await import("/src/storage/asset-preview-opfs-commit.ts"); + const encoded = new Uint8Array(bytes).buffer; + const base = await storage.parseAssetCatalogManifestV2({ + ...sourceCatalog, + assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), + }); + const identities = []; + for (const asset of base.assets) { + const input = { ...sourceIdentity, assetId: asset.assetId } as Record; + delete input.identitySha256; + identities.push(await storage.createAssetPreviewIdentity(input as never)); + } + const databaseName = `m12-02e-${crypto.randomUUID()}`; + const projectId = `m12-02e-${crypto.randomUUID()}`; + const open = (): Promise => new Promise((resolve, reject) => { + const request = indexedDB.open(databaseName, 1); + request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + let database = await open(); + const seed = database.transaction("setting", "readwrite"); + seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); + await complete(seed); + const first = await storage.commitAssetPreviewToOPFS(database, projectId, 7, identities[0], encoded.slice(0)); + const second = await storage.commitAssetPreviewToOPFS(database, projectId, 8, identities[1], encoded.slice(0)); + database.close(); + database = await open(); + const reopened = await storage.loadCommittedAssetPreviewCatalog(database); + + const root = await navigator.storage.getDirectory(); + let contentDirectory = root; + for (const segment of ["projects", projectId, "assets", "sha256", identities[0].content.sha256.slice(0, 2)]) { + contentDirectory = await contentDirectory.getDirectoryHandle(segment); + } + const files: string[] = []; + for await (const [name, handle] of (contentDirectory as unknown as { entries: () => AsyncIterable<[string, FileSystemHandle]> }).entries()) { + if (handle.kind === "file") files.push(name); + } + files.sort(); + const stored = await (await contentDirectory.getFileHandle(identities[0].content.sha256)).getFile(); + const storedBytes = await stored.arrayBuffer(); + const storedSha256 = Array.from(new Uint8Array(await crypto.subtle.digest("SHA-256", storedBytes)), + (byte) => byte.toString(16).padStart(2, "0")).join(""); + const withoutPreview = (asset: Record) => { + const copy = { ...asset }; + delete copy.preview; + return copy; + }; + const resultValue = { + first: { revision: first.manifest.revision, deduplicated: first.deduplicated, identity: identities[0].identitySha256 }, + second: { revision: second.manifest.revision, deduplicated: second.deduplicated, identity: identities[1].identitySha256 }, + contentSha256: identities[0].content.sha256, + files, + storedBytes: stored.size, + storedSha256, + baseMetadata: base.assets.map((asset) => withoutPreview(asset as unknown as Record)), + committedMetadata: second.manifest.assets.map((asset) => withoutPreview(asset as unknown as Record)), + previews: second.manifest.assets.map((asset) => asset.preview), + reopened: { + revision: reopened.manifest.revision, + receiptAssetId: reopened.receipt.assetId, + preview: reopened.preview, + }, + assetIds: base.assets.map((asset) => asset.assetId), + }; + database.close(); + indexedDB.deleteDatabase(databaseName); + const projects = await root.getDirectoryHandle("projects"); + await projects.removeEntry(projectId, { recursive: true }); + return resultValue; + }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); + + expect(result.first).toMatchObject({ revision: 8, deduplicated: false }); + expect(result.second).toMatchObject({ revision: 9, deduplicated: true }); + expect(result.first.identity).not.toBe(result.second.identity); + expect(result.files).toEqual([result.contentSha256]); + expect(result.storedBytes).toBe(513); + expect(result.storedSha256).toBe(result.contentSha256); + expect(result.committedMetadata).toEqual(result.baseMetadata); + expect(result.committedMetadata[0]).not.toEqual(result.committedMetadata[1]); + expect(result.previews).toEqual([ + { sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 }, + { sha256: result.contentSha256, mimeType: "image/png", width: 8, height: 8, byteLength: 513 }, + ]); + expect(result.reopened).toEqual({ + revision: 9, + receiptAssetId: result.assetIds[1], + preview: result.previews[1], + }); +}); diff --git a/web/tests/e2e/asset-preview-display.spec.ts b/web/tests/e2e/asset-preview-display.spec.ts new file mode 100644 index 00000000..8bbfd5d3 --- /dev/null +++ b/web/tests/e2e/asset-preview-display.spec.ts @@ -0,0 +1,95 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); +const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8")); +const preview = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png")); + +test("M12-02H main-thread and Offscreen preview displays match the Blender RGBA8 metrics", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async ({ identityValue, bytes, desktopValue, thresholds }) => { + const { displayAssetPreview } = await import("/src/assets/AssetPreviewDisplay.ts"); + const { compareRenderImages } = await import("/src/three-adapter/render-image-comparison.ts"); + document.body.replaceChildren(); + document.body.style.margin = "0"; + const canvas = document.createElement("canvas"); + canvas.id = "m12-asset-preview"; + canvas.setAttribute("aria-label", "Asset preview"); + canvas.style.width = "128px"; + canvas.style.height = "128px"; + canvas.style.imageRendering = "pixelated"; + document.body.append(canvas); + const encoded = new Uint8Array(bytes).buffer; + const main = await displayAssetPreview(canvas, "MAIN_THREAD_CANVAS_2D", identityValue, encoded.slice(0)); + const workerResult = await new Promise<{ receipt: unknown; pixels: Uint8Array }>((resolve, reject) => { + const worker = new Worker("/src/workers/asset-preview-display-test.worker.ts", { type: "module" }); + worker.onmessage = (event) => { + worker.terminate(); + event.data.ok ? resolve(event.data) : reject(new Error(event.data.error)); + }; + worker.onerror = (event) => { worker.terminate(); reject(new Error(event.message)); }; + worker.postMessage({ identity: identityValue, bytes: encoded.slice(0) }); + }); + const reference = new Uint8Array(desktopValue.pixelCount * 4); + for (let offset = 0; offset < reference.byteLength; offset += 4) reference.set(desktopValue.referencePixel, offset); + const mainMetrics = compareRenderImages(reference, main.pixels, desktopValue.width, desktopValue.height, thresholds); + const offscreenPixels = new Uint8Array(workerResult.pixels); + const offscreenMetrics = compareRenderImages(reference, offscreenPixels, desktopValue.width, desktopValue.height, thresholds); + const wrong = new Uint8Array(main.pixels); + wrong[0] ^= 0xff; + const wrongMetrics = compareRenderImages(reference, wrong, desktopValue.width, desktopValue.height, thresholds); + return { + main: { receipt: main.receipt, pixels: Array.from(main.pixels) }, + offscreen: { receipt: workerResult.receipt, pixels: Array.from(offscreenPixels) }, + mainMetrics, + offscreenMetrics, + wrongMetrics, + }; + }, { identityValue: identity, bytes: [...preview], desktopValue: desktop, thresholds: manifest.thresholds }); + + const canvas = page.locator("#m12-asset-preview"); + await expect(canvas).toBeVisible(); + await expect(canvas).toHaveAttribute("width", "8"); + await expect(canvas).toHaveAttribute("height", "8"); + await expect(canvas).toHaveAttribute("data-asset-preview-status", "ready"); + await expect(canvas).toHaveAttribute("data-asset-preview-backend", "MAIN_THREAD_CANVAS_2D"); + await expect(canvas).toHaveCSS("width", "128px"); + await expect(canvas).toHaveCSS("height", "128px"); + expect(result.main.receipt).toMatchObject({ + status: "READY", + backend: "MAIN_THREAD_CANVAS_2D", + width: 8, + height: 8, + pixelByteLength: 256, + pixelSha256: desktop.rgbaSha256, + nonTransparentPixels: 64, + bitmapClosed: true, + }); + expect(result.offscreen.receipt).toMatchObject({ + status: "READY", + backend: "OFFSCREEN_CANVAS_2D", + width: 8, + height: 8, + pixelByteLength: 256, + pixelSha256: desktop.rgbaSha256, + nonTransparentPixels: 64, + bitmapClosed: true, + }); + expect(result.main.pixels).toEqual(result.offscreen.pixels); + for (const metrics of [result.mainMetrics, result.offscreenMetrics]) { + expect(metrics.status).toBe("READY"); + expect(metrics.errorCode).toBeNull(); + expect(metrics.meanAbsoluteError).toBe(0); + expect(metrics.rootMeanSquaredError).toBe(0); + expect(metrics.p95ChannelError).toBe(0); + expect(metrics.maxChannelError).toBe(0); + expect(metrics.badPixelRatio).toBe(0); + expect(metrics.foregroundIntersectionOverUnion).toBe(1); + expect(metrics.alphaCoverageDeltaRatio).toBe(0); + } + expect(result.wrongMetrics.status).toBe("BLOCKED"); + expect(result.wrongMetrics.errorCode).toBe("RENDER_REFERENCE_MISMATCH"); +}); diff --git a/web/tests/e2e/asset-preview-opfs-commit.spec.ts b/web/tests/e2e/asset-preview-opfs-commit.spec.ts new file mode 100644 index 00000000..37516456 --- /dev/null +++ b/web/tests/e2e/asset-preview-opfs-commit.spec.ts @@ -0,0 +1,171 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const catalog = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), + "utf8", +)) as Record; +const identity = JSON.parse(fs.readFileSync( + path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), + "utf8", +)) as Record; +const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); + +test("M12-02D publishes a catalog preview only after verified OPFS persistence", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { + const storage = await import("/src/storage/asset-preview-opfs-commit.ts"); + const encoded = new Uint8Array(bytes).buffer; + const base = await storage.parseAssetCatalogManifestV2({ + ...sourceCatalog, + assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), + }); + const targetAssetId = base.assets[0].assetId; + const identityInput = { ...sourceIdentity, assetId: targetAssetId } as Record; + delete identityInput.identitySha256; + const boundIdentity = await storage.createAssetPreviewIdentity(identityInput as never); + + const open = (name: string): Promise => new Promise((resolve, reject) => { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + const get = (store: IDBObjectStore, id: string): Promise => new Promise((resolve, reject) => { + const request = store.get(id); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const seed = async (database: IDBDatabase): Promise => { + const transaction = database.transaction("setting", "readwrite"); + transaction.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); + await complete(transaction); + }; + const snapshot = async (database: IDBDatabase) => { + const transaction = database.transaction("setting", "readonly"); + const store = transaction.objectStore("setting"); + const [index, receipt] = await Promise.all([ + get(store, "asset-catalog:index:v2"), + get(store, storage.ASSET_PREVIEW_CATALOG_HEAD_ID), + ]); + await complete(transaction); + return { index, receipt }; + }; + const payloadExists = async (projectId: string): Promise => { + try { + const root = await navigator.storage.getDirectory(); + let current = root; + for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) { + current = await current.getDirectoryHandle(segment); + } + const file = await (await current.getFileHandle(boundIdentity.content.sha256)).getFile(); + return file.size === encoded.byteLength; + } + catch { return false; } + }; + const cleanupProject = async (projectId: string): Promise => { + const root = await navigator.storage.getDirectory(); + try { + const projects = await root.getDirectoryHandle("projects"); + await projects.removeEntry(projectId, { recursive: true }); + } + catch { /* The before-write fault creates no project directory. */ } + }; + const fault = async (faultAt: "BEFORE_OPFS_WRITE" | "AFTER_OPFS_WRITE" | "AFTER_CATALOG_PUT") => { + const databaseName = `m12-02d-${faultAt.toLowerCase()}-${crypto.randomUUID()}`; + const projectId = `m12-02d-${crypto.randomUUID()}`; + let database = await open(databaseName); + await seed(database); + let code = ""; + try { + await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0), { faultAt }); + } + catch (error) { code = String((error as Error & { code?: string }).code ?? ""); } + database.close(); + database = await open(databaseName); + const state = await snapshot(database); + const persisted = await payloadExists(projectId); + database.close(); + indexedDB.deleteDatabase(databaseName); + await cleanupProject(projectId); + return { faultAt, code, state, persisted }; + }; + + const faults = []; + for (const faultAt of ["BEFORE_OPFS_WRITE", "AFTER_OPFS_WRITE", "AFTER_CATALOG_PUT"] as const) { + faults.push(await fault(faultAt)); + } + + const databaseName = `m12-02d-success-${crypto.randomUUID()}`; + const projectId = `m12-02d-${crypto.randomUUID()}`; + let database = await open(databaseName); + await seed(database); + const committed = await storage.commitAssetPreviewToOPFS(database, projectId, base.revision, boundIdentity, encoded.slice(0)); + database.close(); + database = await open(databaseName); + const reopened = await storage.loadCommittedAssetPreviewCatalog(database); + const successState = await snapshot(database); + const persisted = await payloadExists(projectId); + database.close(); + indexedDB.deleteDatabase(databaseName); + await cleanupProject(projectId); + return { + base, + targetAssetId, + identitySha256: boundIdentity.identitySha256, + contentSha256: boundIdentity.content.sha256, + faults, + committed: { + revision: committed.manifest.revision, + preview: committed.preview, + receipt: committed.receipt, + deduplicated: committed.deduplicated, + }, + reopened: { + revision: reopened.manifest.revision, + preview: reopened.preview, + receipt: reopened.receipt, + }, + successState, + persisted, + }; + }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); + + for (const failure of result.faults) { + expect(failure.code).toBe("STORAGE_TRANSACTION"); + expect(failure.state.index).toEqual({ id: "asset-catalog:index:v2", value: result.base }); + expect(failure.state.receipt).toBeUndefined(); + } + expect(result.faults.map((failure) => failure.persisted)).toEqual([false, true, true]); + expect(result.persisted).toBe(true); + expect(result.committed.revision).toBe(8); + expect(result.committed.deduplicated).toBe(false); + expect(result.committed.preview).toEqual({ + sha256: result.contentSha256, + mimeType: "image/png", + width: 8, + height: 8, + byteLength: 513, + }); + expect(result.committed.receipt).toMatchObject({ + task: "M12-02D", + status: "COMMITTED", + assetId: result.targetAssetId, + baseRevision: 7, + committedRevision: 8, + previewIdentitySha256: result.identitySha256, + contentSha256: result.contentSha256, + }); + expect(result.reopened).toEqual({ + revision: result.committed.revision, + preview: result.committed.preview, + receipt: result.committed.receipt, + }); + expect(result.successState.receipt).toEqual(result.committed.receipt); +}); diff --git a/web/tests/e2e/asset-preview-quarantine.spec.ts b/web/tests/e2e/asset-preview-quarantine.spec.ts new file mode 100644 index 00000000..4b3efdfc --- /dev/null +++ b/web/tests/e2e/asset-preview-quarantine.spec.ts @@ -0,0 +1,118 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const catalog = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), "utf8")) as Record; +const identity = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8")) as Record; +const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); + +test("M12-02F quarantines corrupt preview bytes while preserving catalog metadata", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { + const commit = await import("/src/storage/asset-preview-opfs-commit.ts"); + const quarantine = await import("/src/storage/asset-preview-quarantine.ts"); + const encoded = new Uint8Array(bytes).buffer; + const base = await commit.parseAssetCatalogManifestV2({ + ...sourceCatalog, + assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), + }); + const target = base.assets[0]; + const identityInput = { ...sourceIdentity, assetId: target.assetId } as Record; + delete identityInput.identitySha256; + const boundIdentity = await commit.createAssetPreviewIdentity(identityInput as never); + const databaseName = `m12-02f-${crypto.randomUUID()}`; + const projectId = `m12-02f-${crypto.randomUUID()}`; + const open = (): Promise => new Promise((resolve, reject) => { + const request = indexedDB.open(databaseName, 1); + request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + let database = await open(); + const seed = database.transaction("setting", "readwrite"); + seed.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); + await complete(seed); + const committed = await commit.commitAssetPreviewToOPFS(database, projectId, 7, boundIdentity, encoded.slice(0)); + const ready = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); + + const root = await navigator.storage.getDirectory(); + let contentDirectory = root; + for (const segment of ["projects", projectId, "assets", "sha256", boundIdentity.content.sha256.slice(0, 2)]) { + contentDirectory = await contentDirectory.getDirectoryHandle(segment); + } + const fileHandle = await contentDirectory.getFileHandle(boundIdentity.content.sha256); + const corrupt = new Uint8Array(await (await fileHandle.getFile()).arrayBuffer()); + corrupt[corrupt.byteLength - 1] ^= 0xff; + const writer = await fileHandle.createWritable(); + await writer.write(corrupt); + await writer.close(); + + const quarantined = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); + let contentExists = true; + try { await contentDirectory.getFileHandle(boundIdentity.content.sha256); } + catch { contentExists = false; } + database.close(); + database = await open(); + const reopened = await quarantine.inspectAssetPreviewWithQuarantine(database, projectId, target.assetId); + const catalogTransaction = database.transaction("setting", "readonly"); + const catalogRow = await new Promise>((resolve, reject) => { + const request = catalogTransaction.objectStore("setting").get("asset-catalog:index:v2"); + request.onsuccess = () => resolve(request.result as Record); + request.onerror = () => reject(request.error); + }); + await complete(catalogTransaction); + const storedCatalog = await commit.parseAssetCatalogManifestV2(catalogRow.value); + const quarantinePath = quarantined.status === "QUARANTINED" ? quarantined.receipt.quarantinePath : null; + let quarantineFileBytes = 0; + if (quarantinePath) { + let current = root; + const segments = quarantinePath.split("/"); + for (const segment of segments.slice(0, -1)) current = await current.getDirectoryHandle(segment); + quarantineFileBytes = (await (await current.getFileHandle(segments.at(-1)!)).getFile()).size; + } + const value = { + baseMetadata: target, + ready: { status: ready.status, bytes: ready.status === "READY" ? ready.data.byteLength : 0 }, + quarantined, + reopened, + contentExists, + quarantineFileBytes, + storedAsset: storedCatalog.assets[0], + storedRevision: storedCatalog.revision, + committedRevision: committed.manifest.revision, + otherAsset: storedCatalog.assets[1], + baseOtherAsset: base.assets[1], + }; + database.close(); + indexedDB.deleteDatabase(databaseName); + const projects = await root.getDirectoryHandle("projects"); + await projects.removeEntry(projectId, { recursive: true }); + return value; + }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); + + expect(result.ready).toEqual({ status: "READY", bytes: 513 }); + expect(result.quarantined.status).toBe("QUARANTINED"); + expect(result.quarantined.preview).toBeNull(); + expect(result.quarantined.data).toBeNull(); + expect(result.quarantined.receipt).toMatchObject({ + task: "M12-02F", + status: "QUARANTINED", + catalogRevision: 8, + actualByteLength: 513, + code: "ASSET_SOURCE_HASH_MISMATCH", + }); + expect(result.quarantined.receipt.actualSha256).not.toBe(result.quarantined.receipt.expected.sha256); + expect(result.quarantined.receipt.quarantinePath).toContain("/assets/quarantine/"); + expect(result.reopened).toEqual(result.quarantined); + expect(result.contentExists).toBe(false); + expect(result.quarantineFileBytes).toBe(513); + expect(result.storedRevision).toBe(result.committedRevision); + expect(result.storedAsset).toEqual(result.quarantined.asset); + expect({ ...result.storedAsset, preview: null }).toEqual(result.baseMetadata); + expect(result.otherAsset).toEqual(result.baseOtherAsset); +}); diff --git a/web/tests/e2e/asset-preview-reference-gc.spec.ts b/web/tests/e2e/asset-preview-reference-gc.spec.ts new file mode 100644 index 00000000..d24d6006 --- /dev/null +++ b/web/tests/e2e/asset-preview-reference-gc.spec.ts @@ -0,0 +1,132 @@ +import { expect, test } from "@playwright/test"; +import fs from "node:fs"; +import path from "node:path"; + +const catalog = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-01D/catalog-v2.json"), "utf8")) as Record; +const identity = JSON.parse(fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/identity.json"), "utf8")) as Record; +const preview = fs.readFileSync(path.resolve(import.meta.dirname, "../../../tests/golden/M12-02B/preview.png")); + +test("M12-02G reclaims only the final project-scoped preview payload reference", async ({ page }) => { + await page.goto("/"); + const result = await page.evaluate(async ({ sourceCatalog, sourceIdentity, bytes }) => { + const commit = await import("/src/storage/asset-preview-opfs-commit.ts"); + const gc = await import("/src/storage/asset-preview-reference-gc.ts"); + const quarantine = await import("/src/storage/asset-preview-quarantine.ts"); + const encoded = new Uint8Array(bytes).buffer; + const base = await commit.parseAssetCatalogManifestV2({ + ...sourceCatalog, + assets: (sourceCatalog.assets as Array>).map((asset) => ({ ...asset, preview: null })), + }); + const identityFor = async (assetId: string) => { + const input = { ...sourceIdentity, assetId } as Record; + delete input.identitySha256; + return commit.createAssetPreviewIdentity(input as never); + }; + const identities = await Promise.all(base.assets.map((asset) => identityFor(asset.assetId))); + const open = (name: string): Promise => new Promise((resolve, reject) => { + const request = indexedDB.open(name, 1); + request.onupgradeneeded = () => request.result.createObjectStore("setting", { keyPath: "id" }); + request.onsuccess = () => resolve(request.result); + request.onerror = () => reject(request.error); + }); + const complete = (transaction: IDBTransaction): Promise => new Promise((resolve, reject) => { + transaction.oncomplete = () => resolve(); + transaction.onerror = () => reject(transaction.error); + transaction.onabort = () => reject(transaction.error); + }); + const seed = async (database: IDBDatabase) => { + const transaction = database.transaction("setting", "readwrite"); + transaction.objectStore("setting").put({ id: "asset-catalog:index:v2", value: base }); + await complete(transaction); + }; + const exists = async (projectId: string, sha256: string): Promise => { + try { + const root = await navigator.storage.getDirectory(); + let current = root; + for (const segment of ["projects", projectId, "assets", "sha256", sha256.slice(0, 2)]) current = await current.getDirectoryHandle(segment); + await current.getFileHandle(sha256); + return true; + } + catch { return false; } + }; + const databaseAName = `m12-02g-a-${crypto.randomUUID()}`; + const databaseBName = `m12-02g-b-${crypto.randomUUID()}`; + const projectA = `m12-02g-a-${crypto.randomUUID()}`; + const projectB = `m12-02g-b-${crypto.randomUUID()}`; + let databaseA = await open(databaseAName); + let databaseB = await open(databaseBName); + await seed(databaseA); + await seed(databaseB); + await commit.commitAssetPreviewToOPFS(databaseA, projectA, 7, identities[0], encoded.slice(0)); + await commit.commitAssetPreviewToOPFS(databaseA, projectA, 8, identities[1], encoded.slice(0)); + await commit.commitAssetPreviewToOPFS(databaseB, projectB, 7, identities[0], encoded.slice(0)); + + const firstRemoval = await gc.removeAssetPreviewReference(databaseA, projectA, 9, base.assets[0].assetId); + const afterFirstA = await exists(projectA, identities[0].content.sha256); + const afterFirstB = await exists(projectB, identities[0].content.sha256); + const finalRemoval = await gc.removeAssetPreviewReference(databaseA, projectA, 10, base.assets[1].assetId); + const afterFinalA = await exists(projectA, identities[0].content.sha256); + const afterFinalB = await exists(projectB, identities[0].content.sha256); + const projectBReady = await quarantine.inspectAssetPreviewWithQuarantine(databaseB, projectB, base.assets[0].assetId); + + databaseA.close(); + databaseA = await open(databaseAName); + const transaction = databaseA.transaction("setting", "readonly"); + const row = await new Promise>((resolve, reject) => { + const request = transaction.objectStore("setting").get("asset-catalog:index:v2"); + request.onsuccess = () => resolve(request.result as Record); + request.onerror = () => reject(request.error); + }); + await complete(transaction); + const reopenedA = await commit.parseAssetCatalogManifestV2(row.value); + const value = { + firstReceipt: firstRemoval.receipt, + finalReceipt: finalRemoval.receipt, + afterFirstA, + afterFirstB, + afterFinalA, + afterFinalB, + projectBReady: { status: projectBReady.status, bytes: projectBReady.status === "READY" ? projectBReady.data.byteLength : 0 }, + reopenedRevision: reopenedA.revision, + reopenedPreviews: reopenedA.assets.map((asset) => asset.preview), + baseMetadata: base.assets, + reopenedMetadata: reopenedA.assets.map((asset) => ({ ...asset, preview: null })), + }; + databaseA.close(); + databaseB.close(); + indexedDB.deleteDatabase(databaseAName); + indexedDB.deleteDatabase(databaseBName); + const root = await navigator.storage.getDirectory(); + const projects = await root.getDirectoryHandle("projects"); + await projects.removeEntry(projectA, { recursive: true }); + await projects.removeEntry(projectB, { recursive: true }); + return value; + }, { sourceCatalog: catalog, sourceIdentity: identity, bytes: [...preview] }); + + expect(result.firstReceipt).toMatchObject({ + task: "M12-02G", + status: "RETAINED", + baseRevision: 9, + committedRevision: 10, + remainingProjectReferences: 1, + payloadReclaimed: false, + }); + expect(result.finalReceipt).toMatchObject({ + task: "M12-02G", + status: "COLLECTED", + baseRevision: 10, + committedRevision: 11, + remainingProjectReferences: 0, + payloadReclaimed: true, + }); + expect({ + afterFirstA: result.afterFirstA, + afterFirstB: result.afterFirstB, + afterFinalA: result.afterFinalA, + afterFinalB: result.afterFinalB, + }).toEqual({ afterFirstA: true, afterFirstB: true, afterFinalA: false, afterFinalB: true }); + expect(result.projectBReady).toEqual({ status: "READY", bytes: 513 }); + expect(result.reopenedRevision).toBe(11); + expect(result.reopenedPreviews).toEqual([null, null]); + expect(result.reopenedMetadata).toEqual(result.baseMetadata); +}); diff --git a/web/tests/e2e/library-append-main.spec.ts b/web/tests/e2e/library-append-main.spec.ts new file mode 100644 index 00000000..fe106874 --- /dev/null +++ b/web/tests/e2e/library-append-main.spec.ts @@ -0,0 +1,139 @@ +import { expect, test } from "@playwright/test"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import { computeLibraryAppendClosureSha256, LIBRARY_MAIN_APPEND_SCHEMA } from "../../protocol/library-main-append"; +import { createLibraryOperationBinding, createLibrarySourceIdentity, LIBRARY_OPERATION_IDENTITY_SCHEMA } from "../../protocol/library-operation-identity"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const source = fs.readFileSync(path.join(root, "tests/files/web/m12_library_append_v1/m12_append_source.blend")); +const target = fs.readFileSync(path.join(root, "tests/files/web/empty.blend")); + +test("M12-03D appends one fully-local dependency closure through WASM Main", async ({ page }) => { + test.setTimeout(120_000); + await page.goto("/"); + const closure = { + object: "Object/M12 Append Object", + mesh: "Mesh/M12 Append Mesh", + material: "Material/M12 Append Material", + image: "Image/M12 Append Image", + } as const; + const sourceSha256 = crypto.createHash("sha256").update(source).digest("hex"); + const sourceIdentity = await createLibrarySourceIdentity({ + sourceLocator: "project-assets/libraries/m12_append_source.blend", + sourceSha256, + }); + const dependencyClosureSha256 = await computeLibraryAppendClosureSha256(closure); + const binding = await createLibraryOperationBinding({ + schemaVersion: LIBRARY_OPERATION_IDENTITY_SCHEMA, + operation: "APPEND", + source: sourceIdentity, + sourceDataBlockId: closure.object, + owner: { kind: "LOCAL_MAIN", projectId: "project:m12-03d", localDataBlockId: closure.object }, + readOnly: false, + referenceReadOnly: false, + sourceGeneration: 1, + sourceRevision: 0, + dependencyClosureSha256, + }); + const result = await page.evaluate(async ({ sourceBytes, targetBytes, closure, binding }) => { + const { WebEngineClient } = await import("/src/engine-client/WebEngineClient.ts"); + const sourceBuffer = Uint8Array.from(sourceBytes).buffer; + const targetBuffer = Uint8Array.from(targetBytes).buffer; + const digest = async (bytes: ArrayBuffer): Promise => { + const hash = await crypto.subtle.digest("SHA-256", bytes); + return Array.from(new Uint8Array(hash), (value) => value.toString(16).padStart(2, "0")).join(""); + }; + const makeRequest = (baseRevision: number) => ({ + schemaVersion: 1 as const, + baseRevision, + binding, + expectedClosure: closure, + }); + const client = new WebEngineClient({ timeoutMs: 60_000 }); + const reopened = new WebEngineClient({ timeoutMs: 60_000 }); + const ids = { + object: "object:M12 Append Object", + mesh: "mesh:M12 Append Mesh", + material: "material:M12 Append Material", + image: "image:M12 Append Image", + }; + const closureState = (snapshot: any) => ({ + object: snapshot.nodes.find((item: any) => item.id === ids.object), + mesh: snapshot.meshes.find((item: any) => item.id === ids.mesh), + material: snapshot.materials.find((item: any) => item.id === ids.material), + image: snapshot.images.find((item: any) => item.id === ids.image), + }); + try { + const opened = await client.openBlend(targetBuffer.slice(0)); + const baseRevision = opened.snapshot.revision; + const request = await makeRequest(baseRevision); + const appended = await client.appendLibraryObject(sourceBuffer.slice(0), request); + const appendedClosure = closureState(appended.snapshot); + const stale = await client.appendLibraryObject(sourceBuffer.slice(0), makeRequest(baseRevision)) + .then(() => ({ code: "NO_ERROR" })) + .catch((error: any) => ({ code: error.code, message: error.message })); + const collisionRequest = await makeRequest(appended.snapshot.revision); + const collision = await client.appendLibraryObject(sourceBuffer.slice(0), collisionRequest) + .then(() => ({ code: "NO_ERROR" })) + .catch((error: any) => ({ code: error.code, message: error.message })); + const afterCollision = await client.snapshot(); + const undone = await client.applyCommand({ type: "undo" }); + const redone = await client.applyCommand({ type: "redo" }); + const saved = await client.saveBlend(); + const reopenedResult = await reopened.openBlend(saved); + const reopenedClosure = closureState(reopenedResult.snapshot); + return { + sourceSha256: await digest(sourceBuffer), + baseRevision, + appendedRevision: appended.snapshot.revision, + receipt: appended.receipt, + delta: appended.delta, + appendedClosure: { + object: appendedClosure.object && { type: appendedClosure.object.type, dataId: appendedClosure.object.dataId }, + mesh: appendedClosure.mesh && { vertexCount: appendedClosure.mesh.vertexCount, materialSlotIds: appendedClosure.mesh.materialSlotIds }, + material: appendedClosure.material && { + imageIds: appendedClosure.material.imageIds ?? appendedClosure.material.nodes?.flatMap((node: any) => node.imageId ? [node.imageId] : []), + }, + image: appendedClosure.image && { libraryLinked: appendedClosure.image.libraryLinked, width: appendedClosure.image.width, height: appendedClosure.image.height }, + }, + stale, + collision, + collisionRevision: afterCollision.snapshot.revision, + undoHasObject: Boolean(closureState(undone.snapshot).object), + redoHasObject: Boolean(closureState(redone.snapshot).object), + reopenedRevision: reopenedResult.snapshot.revision, + reopenedHasObject: Boolean(reopenedClosure.object), + reopenedHasLocalImage: reopenedClosure.image?.libraryLinked === false, + }; + } + catch (error: any) { + return { error: { code: error?.code, message: error?.message, detail: error?.detail } }; + } + finally { + client.terminate(); + reopened.terminate(); + } + }, { sourceBytes: Array.from(source), targetBytes: Array.from(target), closure, binding }); + + expect(result.error).toBeUndefined(); + expect(result.sourceSha256).toMatch(/^[a-f0-9]{64}$/); + expect(result.appendedRevision).toBe(result.baseRevision + 1); + expect(result.receipt).toMatchObject({ operation: "APPEND", transactionCount: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision }); + expect(result.receipt.mapping).toHaveLength(4); + expect(result.receipt.mapping.every((item: any) => item.owner === "LOCAL_MAIN" && item.readOnly === false && item.source === item.local)).toBe(true); + expect(result.delta).toMatchObject({ schemaVersion: 1, baseRevision: result.baseRevision, nextRevision: result.appendedRevision }); + expect(result.appendedClosure).toEqual({ + object: { type: "MESH", dataId: "mesh:M12 Append Mesh" }, + mesh: { vertexCount: 4, materialSlotIds: ["material:M12 Append Material"] }, + material: { imageIds: ["image:M12 Append Image"] }, + image: { libraryLinked: false, width: 2, height: 2 }, + }); + expect(result.stale.code).toBe("REVISION_CONFLICT"); + expect(result.collision.code).toBe("ASSET_MANIFEST_INVALID"); + expect(result.collisionRevision).toBe(result.appendedRevision); + expect(result.undoHasObject).toBe(false); + expect(result.redoHasObject).toBe(true); + expect(result.reopenedHasObject).toBe(true); + expect(result.reopenedHasLocalImage).toBe(true); +}); diff --git a/web/tests/e2e/render-compositor-media-recovery.spec.ts b/web/tests/e2e/render-compositor-media-recovery.spec.ts new file mode 100644 index 00000000..e0851da5 --- /dev/null +++ b/web/tests/e2e/render-compositor-media-recovery.spec.ts @@ -0,0 +1,72 @@ +import { expect, test } from "@playwright/test"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M11-14/render-compositor-media-recovery.json"), "utf8")) as { + domains: Array<{ + domain: "RENDER" | "COMPOSITOR" | "MEDIA"; + fixture: string; + sha256: string; + cancellationCode: string; + budgetCode: string; + }>; + textureFixture: { path: string; sha256: string }; +}; +const fixtures = Object.fromEntries(manifest.domains.map((item) => [item.domain, fs.readFileSync(path.join(root, item.fixture))])); +const texture = fs.readFileSync(path.join(root, manifest.textureFixture.path)); +const sha256 = (value: Buffer) => crypto.createHash("sha256").update(value).digest("hex"); + +test("M11-14 recovers real render, compositor and media production paths after lifecycle faults", async ({ page }) => { + test.setTimeout(120_000); + for (const item of manifest.domains) expect(sha256(fixtures[item.domain])).toBe(item.sha256); + expect(sha256(texture)).toBe(manifest.textureFixture.sha256); + await page.goto("/"); + const reports = await page.evaluate(async ({ renderBlend, compositorBlend, movie, texturePng }) => { + const { runRenderCompositorMediaRecoverySuite } = await import("/src/testing/render-compositor-media-recovery.ts"); + return runRenderCompositorMediaRecoverySuite({ + renderBlend: Uint8Array.from(renderBlend).buffer, + compositorBlend: Uint8Array.from(compositorBlend).buffer, + movie: { mimeType: "video/mp4", data: Uint8Array.from(movie).buffer }, + texturePng: Uint8Array.from(texturePng).buffer, + }); + }, { + renderBlend: Array.from(fixtures.RENDER), + compositorBlend: Array.from(fixtures.COMPOSITOR), + movie: Array.from(fixtures.MEDIA), + texturePng: Array.from(texture), + }); + + expect(reports.map((report) => report.domain)).toEqual(["RENDER", "COMPOSITOR", "MEDIA"]); + for (const [index, report] of reports.entries()) { + const expected = manifest.domains[index]; + expect(report.source.sha256).toBe(expected.sha256); + expect(report.cancellation).toEqual({ + status: "CANCELLED", + code: expected.cancellationCode, + publishedResults: 0, + temporaryResourcesAfter: 0, + }); + expect(report.restart).toMatchObject({ status: "RECOVERED", generationBefore: 1, generationAfter: 2 }); + expect(report.restart.identityAfter).toBe(report.restart.identityBefore); + expect(report.restart.outputSha256After).toBe(report.restart.outputSha256Before); + expect(report.budget).toEqual({ + status: "BLOCKED", + code: expected.budgetCode, + retainedIdentityHash: report.restart.identityAfter, + temporaryResourcesAfter: 0, + }); + expect(report.release.status).toBe("RELEASED"); + expect(report.release.releasedBytes).toBeGreaterThan(0); + expect(report.release.releasedResources).toBeGreaterThan(0); + expect(report.release.resourcesAfter).toBe(0); + expect(report.recovery).toMatchObject({ + status: "RECOVERED", + identityHash: report.restart.identityAfter, + outputSha256: report.restart.outputSha256After, + }); + expect(report.recovery.outputBytes).toBeGreaterThan(0); + expect(report.recovery.visibleUnits).toBeGreaterThan(0); + } +}); diff --git a/web/tests/unit/asset-catalog-compatibility.test.mjs b/web/tests/unit/asset-catalog-compatibility.test.mjs new file mode 100644 index 00000000..ff8356ab --- /dev/null +++ b/web/tests/unit/asset-catalog-compatibility.test.mjs @@ -0,0 +1,64 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const repoRoot = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-compatibility-unit-")); +const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-compatibility.ts"]; +for (const sourceName of sources) { + const sourcePath = path.join(repoRoot, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"')); +} +const compatibility = await import(pathToFileURL(path.join(temporary, "asset-catalog-compatibility.mjs"))); +const v1 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8")); +const v2 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8")); +const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01E/compatibility-report.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01E/manifest.json"), "utf8")); + +test("M12-01E binds the compatibility policy and generated reports", () => { + assert.equal(manifest.nextTask, "M12-01F"); + for (const artifact of Object.values(manifest.artifacts)) { + const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex"); + assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`); + } +}); + +test("M12-01E exposes schema v2 to a v1 reader as a bounded read-only snapshot", async () => { + const before = structuredClone(v2); + const report = await compatibility.inspectAssetCatalogForLegacyReader(v2, "READ"); + assert.deepEqual(report, golden.read); + assert.equal(report.status, "READ_ONLY"); + assert.equal(report.code, "ASSET_SCHEMA_DOWNGRADE_BLOCKED"); + assert.deepEqual(report.snapshot.catalogs.map((item) => item.path), ["Animation", "Characters", "Characters/Heroes"]); + assert.deepEqual(v2, before); +}); + +test("M12-01E blocks every legacy write and save without changing the source hash", async () => { + for (const [operation, key] of [["CATALOG_WRITE", "catalogWrite"], ["ASSET_WRITE", "assetWrite"], ["SAVE", "save"]]) { + const report = await compatibility.inspectAssetCatalogForLegacyReader(v2, operation); + assert.deepEqual(report, golden[key]); + assert.equal(report.status, "BLOCKED"); + assert.equal(report.code, "ASSET_SCHEMA_DOWNGRADE_BLOCKED"); + assert.equal(report.sourceSha256, golden.read.sourceSha256); + } +}); + +test("M12-01E keeps native v1 ready and blocks unknown future schemas", async () => { + assert.equal((await compatibility.inspectAssetCatalogForLegacyReader(v1, "SAVE")).status, "READY"); + assert.deepEqual(await compatibility.inspectAssetCatalogForLegacyReader({ schemaVersion: 3 }, "READ"), golden.future); + assert.equal(golden.future.code, "PROTOCOL_MISMATCH"); + assert.equal(golden.future.recoverable, false); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-catalog-indexeddb-migration.test.mjs b/web/tests/unit/asset-catalog-indexeddb-migration.test.mjs new file mode 100644 index 00000000..94aa56c0 --- /dev/null +++ b/web/tests/unit/asset-catalog-indexeddb-migration.test.mjs @@ -0,0 +1,22 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-01G/manifest.json"), "utf8")); + +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-01G binds the production transaction, browser suite, and migration inputs", () => { + assert.equal(manifest.task, "M12-01G"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-01H"); + assert.deepEqual(manifest.transaction.faultPoints, ["AFTER_TARGET_PUT", "AFTER_SOURCE_DELETE"]); + assert.equal(manifest.transaction.failureCode, "STORAGE_TRANSACTION"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/asset-catalog-migration.test.mjs b/web/tests/unit/asset-catalog-migration.test.mjs new file mode 100644 index 00000000..7b463557 --- /dev/null +++ b/web/tests/unit/asset-catalog-migration.test.mjs @@ -0,0 +1,60 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const repoRoot = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-migration-unit-")); +const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"]; +for (const sourceName of sources) { + const sourcePath = path.join(repoRoot, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + const output = result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"'); + fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), output); +} +const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs"))); +const source = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8")); +const target = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8")); +const report = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/migration-report.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/manifest.json"), "utf8")); + +test("M12-01D binds migration inputs, outputs, and production protocols", () => { + assert.equal(manifest.nextTask, "M12-01E"); + for (const artifact of Object.values(manifest.artifacts)) { + const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex"); + assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`); + } +}); + +test("M12-01D migrates schema v1 to v2 without dropping source, preview, or library data", async () => { + const actual = await migration.migrateAssetCatalogV1ToV2(source); + assert.deepEqual(actual.manifest, target); + assert.deepEqual(actual.report, report); + assert.equal(actual.manifest.revision, 7); + assert.deepEqual(actual.manifest.catalogs.map((item) => item.path), ["Animation", "Characters", "Characters/Heroes"]); + assert.equal(actual.manifest.assets.filter((item) => item.preview !== null).length, 1); + assert.deepEqual(actual.manifest.libraries.map((item) => item.libraryId), ["library:characters", "library:materials"]); + assert.equal(actual.report.preserved.sourceBindings, 2); +}); + +test("M12-01D preserves canonical UUIDs and deterministically maps legacy IDs", async () => { + const first = await migration.migrateAssetCatalogV1ToV2(source); + const second = await migration.migrateAssetCatalogV1ToV2(structuredClone(source)); + assert.deepEqual(second, first); + const canonical = first.report.catalogMappings.find((item) => item.legacyId.startsWith("4444")); + assert.equal(canonical.catalogId, canonical.legacyId); + assert.match(first.report.catalogMappings.find((item) => item.legacyId === "catalog:root").catalogId, /^[a-f0-9-]{36}$/); + assert.equal(first.report.sourceManifestSha256, report.sourceManifestSha256); + assert.equal(first.report.targetManifestSha256, report.targetManifestSha256); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-catalog-negatives.test.mjs b/web/tests/unit/asset-catalog-negatives.test.mjs new file mode 100644 index 00000000..71c5b098 --- /dev/null +++ b/web/tests/unit/asset-catalog-negatives.test.mjs @@ -0,0 +1,89 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const repoRoot = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-negatives-unit-")); +const sources = ["asset-path.ts", "capability-gates.ts", "asset-library-io.ts", "asset-catalog-v2.ts", "asset-catalog-migration.ts"]; +for (const sourceName of sources) { + const sourcePath = path.join(repoRoot, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + fs.writeFileSync(path.join(temporary, sourceName.replace(/\.ts$/, ".mjs")), result.outputText.replaceAll(/from "\.\/([a-z0-9-]+)"/g, 'from "./$1.mjs"')); +} +const v2Protocol = await import(pathToFileURL(path.join(temporary, "asset-catalog-v2.mjs"))); +const migration = await import(pathToFileURL(path.join(temporary, "asset-catalog-migration.mjs"))); +const v1 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v1.json"), "utf8")); +const v2 = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01D/catalog-v2.json"), "utf8")); +const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01F/negative-cases.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01F/manifest.json"), "utf8")); + +test("M12-01F binds every negative input contract", () => { + assert.equal(manifest.negativeCaseCount, golden.cases.length); + assert.equal(manifest.nextTask, "M12-01G"); + for (const artifact of Object.values(manifest.artifacts)) { + const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex"); + assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`); + } +}); + +async function codeFrom(run) { + try { + await run(); + return "NO_ERROR"; + } + catch (error) { + return error?.code ?? "UNKNOWN_ERROR"; + } +} + +test("M12-01F rejects duplicate identities, cycles, budgets, and unknown fields", async () => { + const duplicateCatalog = structuredClone(v2); + duplicateCatalog.catalogs.push(structuredClone(duplicateCatalog.catalogs[0])); + const duplicateAsset = structuredClone(v2); + duplicateAsset.assets.push(structuredClone(duplicateAsset.assets[0])); + const legacyCycle = structuredClone(v1); + legacyCycle.catalogs[0].parentId = legacyCycle.catalogs[1].id; + const parentMismatch = structuredClone(v2); + parentMismatch.catalogs[0].parentPath = "Characters"; + const simpleName = structuredClone(v2); + simpleName.catalogs[0].simpleName = "\u00e9".repeat(32); + const tag = structuredClone(v2); + tag.assets[0].tags = ["\u00e9".repeat(32)]; + tag.assets[0].activeTag = 0; + const unknownTop = { ...structuredClone(v2), future: true }; + const unknownAsset = structuredClone(v2); + unknownAsset.assets[0].future = true; + const unknownLegacyAsset = structuredClone(v1); + unknownLegacyAsset.assets[0].future = true; + + const cases = [ + ["DUPLICATE_CATALOG_ID", () => v2Protocol.parseAssetCatalogManifestV2(duplicateCatalog)], + ["DUPLICATE_ASSET_ID", () => v2Protocol.parseAssetCatalogManifestV2(duplicateAsset)], + ["LEGACY_PARENT_CYCLE", () => migration.migrateAssetCatalogV1ToV2(legacyCycle)], + ["PARENT_PATH_MISMATCH", () => v2Protocol.parseAssetCatalogManifestV2(parentMismatch)], + ["OVERLONG_SIMPLE_NAME_UTF8", () => v2Protocol.parseAssetCatalogManifestV2(simpleName)], + ["OVERLONG_TAG_UTF8", () => v2Protocol.parseAssetCatalogManifestV2(tag)], + ["V2_UNKNOWN_TOP_LEVEL", () => v2Protocol.parseAssetCatalogManifestV2(unknownTop)], + ["V2_UNKNOWN_ASSET_FIELD", () => v2Protocol.parseAssetCatalogManifestV2(unknownAsset)], + ["V1_UNKNOWN_ASSET_FIELD", () => migration.migrateAssetCatalogV1ToV2(unknownLegacyAsset)], + ]; + const actual = []; + for (const [id, run] of cases) { + actual.push({ id, code: await codeFrom(run) }); + assert.deepEqual(await v2Protocol.parseAssetCatalogManifestV2(v2), v2, `${id} poisoned the valid parser path`); + } + assert.deepEqual(actual, golden.cases); + assert.equal(golden.nextTask, "M12-01G"); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-catalog-restart.test.mjs b/web/tests/unit/asset-catalog-restart.test.mjs new file mode 100644 index 00000000..953d876b --- /dev/null +++ b/web/tests/unit/asset-catalog-restart.test.mjs @@ -0,0 +1,23 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-01H/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-01H binds page and Worker restart identity evidence", () => { + assert.equal(manifest.task, "M12-01H"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-01I"); + assert.deepEqual(manifest.restart.contexts, ["INITIAL_PAGE", "RELOADED_PAGE", "WORKER_GENERATION_1", "WORKER_GENERATION_2"]); + assert.equal(manifest.restart.catalogOrder.length, manifest.restart.catalogCount); + assert.equal(manifest.restart.assetOrder.length, manifest.restart.assetCount); + assert.match(manifest.restart.manifestSha256, /^[a-f0-9]{64}$/); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/asset-catalog-v2.test.mjs b/web/tests/unit/asset-catalog-v2.test.mjs new file mode 100644 index 00000000..3f047c1b --- /dev/null +++ b/web/tests/unit/asset-catalog-v2.test.mjs @@ -0,0 +1,70 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const repoRoot = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(repoRoot, "web/protocol/asset-catalog-v2.ts"); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-catalog-v2-unit-")); +const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, +}); +assert.deepEqual(result.diagnostics, []); +fs.writeFileSync(path.join(temporary, "asset-catalog-v2.mjs"), result.outputText); +const catalog = await import(pathToFileURL(path.join(temporary, "asset-catalog-v2.mjs"))); +const desktop = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01B/canonical.json"), "utf8")); +const golden = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01C/asset-catalog-v2.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(repoRoot, "tests/golden/M12-01C/manifest.json"), "utf8")); + +test("M12-01C binds the desktop and Blender weak-reference sources", () => { + assert.equal(manifest.nextTask, "M12-01D"); + for (const artifact of Object.values(manifest.artifacts)) { + const actual = crypto.createHash("sha256").update(fs.readFileSync(path.join(repoRoot, artifact.path))).digest("hex"); + assert.equal(actual, artifact.sha256, `${artifact.path} hash drifted`); + } + const weakReferenceSource = fs.readFileSync(path.join(repoRoot, manifest.artifacts.blenderWeakReferenceSource.path), "utf8"); + assert.match(weakReferenceSource, /AssetWeakReference AssetRepresentation::make_weak_reference\(\) const/); + assert.match(weakReferenceSource, /library_relative_identifier\(\) const/); +}); + +test("M12-01C converts the desktop catalog baseline into schema v2", async () => { + const generated = await catalog.createAssetCatalogManifestV2FromDesktop(desktop, 1); + assert.deepEqual(generated, golden); + assert.deepEqual(generated.catalogs.map((item) => item.path), ["Characters", "Characters/Heroes", "Materials/Metal"]); + assert.deepEqual(generated.assets.map((item) => item.relativeAssetIdentifier), [ + "Material/M12 Brushed Metal", "Object/M12 Hero", "World/M12 Uncataloged World", + ]); + assert.equal(generated.assets[2].catalogId, null); + assert.equal(generated.assets[0].author, ""); + assert.equal(generated.assets[0].sourceSha256, null); + assert.deepEqual(generated.libraries, []); + assert.equal(generated.assets[1].customProperties.find((item) => item.name === "dimensions").type, "FLOAT_ARRAY"); +}); + +test("M12-01C stable IDs bind Blender weak-reference identity", async () => { + const local = await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: null, relativeAssetIdentifier: "Object/M12 Hero" }); + const external = await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: "Studio", relativeAssetIdentifier: "hero.blend/Object/M12 Hero" }); + assert.match(local, /^asset:[a-f0-9]{64}$/); + assert.match(external, /^asset:[a-f0-9]{64}$/); + assert.notEqual(local, external); + assert.equal(local, golden.assets[1].assetId); + assert.equal(local, await catalog.createAssetCatalogV2StableId({ assetLibraryIdentifier: null, relativeAssetIdentifier: "Object/M12 Hero" })); +}); + +test("M12-01C exposes UTF-8 byte and collection budgets", async () => { + assert.equal(catalog.ASSET_CATALOG_SCHEMA, 2); + assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxCatalogSimpleNameBytes, 63); + assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxTagBytes, 63); + assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxCatalogs, 10_000); + assert.equal(catalog.ASSET_CATALOG_V2_BUDGET.maxAssets, 100_000); + assert.deepEqual(catalog.ASSET_CATALOG_V2_ID_TYPES, ["ACTION", "COLLECTION", "IMAGE", "MATERIAL", "NODE_GROUP", "OBJECT", "WORLD"]); + assert.deepEqual(await catalog.parseAssetCatalogManifestV2(golden), golden); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-preview-decode.test.mjs b/web/tests/unit/asset-preview-decode.test.mjs new file mode 100644 index 00000000..1957de07 --- /dev/null +++ b/web/tests/unit/asset-preview-decode.test.mjs @@ -0,0 +1,102 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-decode-unit-")); +const transpile = (sourceName, outputName, replacements = []) => { + const sourcePath = path.join(root, "web/protocol", sourceName); + const result = ts.transpileModule(fs.readFileSync(sourcePath, "utf8"), { + compilerOptions: { module: ts.ModuleKind.ES2022, target: ts.ScriptTarget.ES2022 }, + fileName: sourcePath, + reportDiagnostics: true, + }); + assert.deepEqual(result.diagnostics, []); + fs.writeFileSync(path.join(temporary, outputName), replacements.reduce((value, [from, to]) => value.replaceAll(from, to), result.outputText)); +}; +transpile("asset-preview.ts", "asset-preview.mjs"); +transpile("asset-preview-decode.ts", "asset-preview-decode.mjs", [['from "./asset-preview"', 'from "./asset-preview.mjs"']]); +const identityProtocol = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs"))); +const decode = await import(pathToFileURL(path.join(temporary, "asset-preview-decode.mjs"))); +const identity = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); +const content = fs.readFileSync(path.join(root, "tests/golden/M12-02B/preview.png")); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02C/manifest.json"), "utf8")); +const buffer = (bytes) => bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength); +const sha256 = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex"); + +async function identityFor(bytes, changes = {}) { + const base = { + ...identity, + content: { + ...identity.content, + byteLength: bytes.byteLength, + sha256: sha256(bytes), + ...changes, + }, + }; + delete base.identitySha256; + return identityProtocol.createAssetPreviewIdentity(base); +} + +test("M12-02C plans the checked-in PNG before decode", async () => { + assert.equal(manifest.task, "M12-02C"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02D"); + assert.equal(manifest.negativeCaseCount, 8); + assert.deepEqual(decode.ASSET_PREVIEW_DECODE_BUDGET, manifest.budget); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); + } + assert.deepEqual(await decode.planAssetPreviewDecode(identity, buffer(content)), { + schemaVersion: 1, + identitySha256: identity.identitySha256, + mimeType: "image/png", + width: 8, + height: 8, + pixelCount: 64, + encodedByteLength: 513, + decodedByteLength: 256, + compressionRatio: 256 / 513, + }); +}); + +test("M12-02C rejects byte, hash, MIME, dimension, pixel, and compression budgets before decode", async () => { + const cases = []; + const changedHash = buffer(content); new Uint8Array(changedHash)[changedHash.byteLength - 1] ^= 1; + cases.push([identity, changedHash, "ASSET_SOURCE_HASH_MISMATCH"]); + cases.push([{ ...identity, content: { ...identity.content, byteLength: 512 } }, buffer(content), "ASSET_PREVIEW_IDENTITY_MISMATCH"]); + cases.push([await identityFor(content, { mimeType: "image/webp" }), buffer(content), "ASSET_MANIFEST_INVALID"]); + + const wrongDimensions = Buffer.from(content); + wrongDimensions.writeUInt32BE(9, 16); + cases.push([await identityFor(wrongDimensions), buffer(wrongDimensions), "ASSET_MANIFEST_INVALID"]); + + const hugeDimensions = Buffer.from(content); + hugeDimensions.writeUInt32BE(4097, 16); + hugeDimensions.writeUInt32BE(4097, 20); + cases.push([await identityFor(hugeDimensions, { width: 4097, height: 4097 }), buffer(hugeDimensions), "ASSET_BUDGET_EXCEEDED"]); + + const ratioDimensions = Buffer.from(content); + ratioDimensions.writeUInt32BE(4096, 16); + ratioDimensions.writeUInt32BE(4096, 20); + cases.push([await identityFor(ratioDimensions, { width: 4096, height: 4096 }), buffer(ratioDimensions), "ASSET_BUDGET_EXCEEDED"]); + + const oversized = new Uint8Array(decode.ASSET_PREVIEW_DECODE_BUDGET.maxEncodedBytes + 1); + cases.push([await identityFor(oversized, { width: 1, height: 1 }), oversized.buffer, "ASSET_BUDGET_EXCEEDED"]); + + const corrupt = Buffer.from(content); corrupt[0] = 0; + cases.push([await identityFor(corrupt), buffer(corrupt), "ASSET_MANIFEST_INVALID"]); + + for (const [manifest, bytes, code] of cases) { + await assert.rejects(decode.planAssetPreviewDecode(manifest, bytes), { code }); + assert.equal((await decode.planAssetPreviewDecode(identity, buffer(content))).identitySha256, identity.identitySha256); + } +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-preview-dedup.test.mjs b/web/tests/unit/asset-preview-dedup.test.mjs new file mode 100644 index 00000000..392ff8d1 --- /dev/null +++ b/web/tests/unit/asset-preview-dedup.test.mjs @@ -0,0 +1,25 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02E/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-02E binds payload deduplication and metadata-isolation evidence", () => { + assert.equal(manifest.task, "M12-02E"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02F"); + assert.deepEqual(manifest.claims, [ + "ONE_CONTENT_HASH_ONE_OPFS_PAYLOAD", + "DISTINCT_ASSET_IDENTITY_PRESERVED", + "DISTINCT_ASSET_METADATA_PRESERVED", + "REOPENED_HEAD_VERIFIED", + ]); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/asset-preview-display.test.mjs b/web/tests/unit/asset-preview-display.test.mjs new file mode 100644 index 00000000..e55dac30 --- /dev/null +++ b/web/tests/unit/asset-preview-display.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/manifest.json"), "utf8")); +const desktop = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02H/desktop-report.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-02H binds desktop, main-thread, Offscreen, and metric evidence", () => { + assert.equal(manifest.task, "M12-02H"); + assert.equal(manifest.enablingTask, false); + assert.equal(manifest.parityStateChange, true); + assert.equal(manifest.implementationClass, "LOCAL_EXACT"); + assert.equal(manifest.nextTask, "M12-03A"); + assert.deepEqual(manifest.backends, ["BLENDER_5_2_DESKTOP", "MAIN_THREAD_CANVAS_2D", "OFFSCREEN_CANVAS_2D"]); + assert.equal(desktop.rgbaSha256, manifest.reference.rgbaSha256); + for (const artifact of Object.values(manifest.artifacts)) assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); +}); diff --git a/web/tests/unit/asset-preview-identity.test.mjs b/web/tests/unit/asset-preview-identity.test.mjs new file mode 100644 index 00000000..0e0f39ba --- /dev/null +++ b/web/tests/unit/asset-preview-identity.test.mjs @@ -0,0 +1,70 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "asset-preview-identity-unit-")); +const sourcePath = path.join(root, "web/protocol/asset-preview.ts"); +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, []); +fs.writeFileSync(path.join(temporary, "asset-preview.mjs"), transpiled.outputText); +const preview = await import(pathToFileURL(path.join(temporary, "asset-preview.mjs"))); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/manifest.json"), "utf8")); +const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02B/identity.json"), "utf8")); +const sourceBytes = fs.readFileSync(path.join(root, manifest.artifacts.source.path)); +const contentBytes = fs.readFileSync(path.join(root, manifest.artifacts.content.path)); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); +const stableJSON = (value) => Array.isArray(value) + ? `[${value.map(stableJSON).join(",")}]` + : value && typeof value === "object" + ? `{${Object.keys(value).sort().map((key) => `${JSON.stringify(key)}:${stableJSON(value[key])}`).join(",")}}` + : JSON.stringify(value); + +test("M12-02B binds source, content, generator, protocol, and settings artifacts", () => { + assert.equal(manifest.task, "M12-02B"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02C"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); + } + assert.equal(sha256(stableJSON(manifest.generatorSettings)), manifest.generatorSettingsSha256); + assert.equal(manifest.generatorSettingsSha256, golden.generator.settingsSha256); +}); + +test("M12-02B creates and parses the canonical preview identity", async () => { + const { identitySha256, ...base } = golden; + assert.deepEqual(await preview.createAssetPreviewIdentity(base), golden); + assert.deepEqual(await preview.parseAssetPreviewIdentity(golden), golden); + assert.equal(identitySha256, await preview.computeAssetPreviewIdentity(base)); + assert.notEqual(golden.source.sha256, golden.content.sha256); + assert.deepEqual(await preview.verifyAssetPreviewIdentity( + golden, + sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength), + contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength), + golden.generator, + ), golden); +}); + +test("M12-02B rejects independent source, content, generator, dimensions, and unknown-field drift", async () => { + const source = sourceBytes.buffer.slice(sourceBytes.byteOffset, sourceBytes.byteOffset + sourceBytes.byteLength); + const content = contentBytes.buffer.slice(contentBytes.byteOffset, contentBytes.byteOffset + contentBytes.byteLength); + const changedSource = source.slice(0); new Uint8Array(changedSource)[0] ^= 1; + const changedContent = content.slice(0); new Uint8Array(changedContent)[0] ^= 1; + await assert.rejects(preview.verifyAssetPreviewIdentity(golden, changedSource, content, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" }); + await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, changedContent, golden.generator), { code: "ASSET_SOURCE_HASH_MISMATCH" }); + await assert.rejects(preview.verifyAssetPreviewIdentity(golden, source, content, { ...golden.generator, settingsSha256: "f".repeat(64) }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" }); + await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, content: { ...golden.content, width: 9 } }), { code: "ASSET_PREVIEW_IDENTITY_MISMATCH" }); + await assert.rejects(preview.parseAssetPreviewIdentity({ ...golden, future: true }), { code: "ASSET_MANIFEST_INVALID" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/asset-preview-opfs-commit.test.mjs b/web/tests/unit/asset-preview-opfs-commit.test.mjs new file mode 100644 index 00000000..d310f8c2 --- /dev/null +++ b/web/tests/unit/asset-preview-opfs-commit.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02D/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-02D binds OPFS-before-catalog ordering and browser evidence", () => { + assert.equal(manifest.task, "M12-02D"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02E"); + assert.deepEqual(manifest.commitOrder, ["PRE_DECODE_GATE", "OPFS_WRITE", "OPFS_READBACK", "CATALOG_TRANSACTION"]); + assert.deepEqual(manifest.faultPoints, ["BEFORE_OPFS_WRITE", "AFTER_OPFS_WRITE", "AFTER_CATALOG_PUT"]); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/asset-preview-quarantine.test.mjs b/web/tests/unit/asset-preview-quarantine.test.mjs new file mode 100644 index 00000000..9cfe09c1 --- /dev/null +++ b/web/tests/unit/asset-preview-quarantine.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02F/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-02F binds corrupt-payload quarantine and readable metadata evidence", () => { + assert.equal(manifest.task, "M12-02F"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02G"); + assert.deepEqual(manifest.states, ["READY", "QUARANTINED", "REOPENED_QUARANTINED"]); + assert.equal(manifest.failureCode, "ASSET_SOURCE_HASH_MISMATCH"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/asset-preview-reference-gc.test.mjs b/web/tests/unit/asset-preview-reference-gc.test.mjs new file mode 100644 index 00000000..0fa76752 --- /dev/null +++ b/web/tests/unit/asset-preview-reference-gc.test.mjs @@ -0,0 +1,21 @@ +import assert from "node:assert/strict"; +import crypto from "node:crypto"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-02G/manifest.json"), "utf8")); +const sha256 = (file) => crypto.createHash("sha256").update(fs.readFileSync(path.join(root, file))).digest("hex"); + +test("M12-02G binds final-reference and cross-project reclamation evidence", () => { + assert.equal(manifest.task, "M12-02G"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-02H"); + assert.deepEqual(manifest.sequence, ["REMOVE_FIRST_REFERENCE", "RETAIN_PAYLOAD", "REMOVE_FINAL_REFERENCE", "COLLECT_PAYLOAD"]); + assert.equal(manifest.otherProjectPayload, "PRESERVED_READY"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(artifact.path), artifact.sha256, artifact.path); + } +}); diff --git a/web/tests/unit/library-operation-identity.test.mjs b/web/tests/unit/library-operation-identity.test.mjs new file mode 100644 index 00000000..6eb3b0e0 --- /dev/null +++ b/web/tests/unit/library-operation-identity.test.mjs @@ -0,0 +1,111 @@ +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 { pathToFileURL } from "node:url"; +import test from "node:test"; +import ts from "typescript"; + +const root = path.resolve(import.meta.dirname, "../../.."); +const temporary = fs.mkdtempSync(path.join(os.tmpdir(), "library-operation-identity-unit-")); +const sourcePath = path.join(root, "web/protocol/library-operation-identity.ts"); +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, []); +fs.writeFileSync(path.join(temporary, "library-operation-identity.mjs"), transpiled.outputText); +const identity = await import(pathToFileURL(path.join(temporary, "library-operation-identity.mjs"))); +const golden = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03B/library-operation-bindings.json"), "utf8")); +const manifest = JSON.parse(fs.readFileSync(path.join(root, "tests/golden/M12-03B/manifest.json"), "utf8")); +const sha256 = (value) => crypto.createHash("sha256").update(value).digest("hex"); + +const bindingInput = (binding) => { + const { invalidationToken, ...input } = structuredClone(binding); + return input; +}; +const stateFor = (binding) => ({ + sourceLibraryId: binding.source.sourceLibraryId, + sourceSha256: binding.source.sourceSha256, + sourceGeneration: binding.sourceGeneration, + sourceRevision: binding.sourceRevision, + dependencyClosureSha256: binding.dependencyClosureSha256, +}); + +test("M12-03B binds the inventory, protocol, golden, and unit artifacts", () => { + assert.equal(manifest.task, "M12-03B"); + assert.equal(manifest.enablingTask, true); + assert.equal(manifest.parityStateChange, false); + assert.equal(manifest.nextTask, "M12-03C"); + for (const artifact of Object.values(manifest.artifacts)) { + assert.equal(sha256(fs.readFileSync(path.join(root, artifact.path))), artifact.sha256, artifact.path); + } +}); + +test("M12-03B derives a stable source library ID from locator and bytes", async () => { + const source = await identity.createLibrarySourceIdentity({ + sourceLocator: golden.source.sourceLocator, + sourceSha256: golden.source.sourceSha256, + }); + assert.deepEqual(source, golden.source); + assert.deepEqual(await identity.parseLibrarySourceIdentity(source), source); + assert.equal(source.sourceLibraryId, await identity.computeLibrarySourceId(source)); + assert.notEqual(source.sourceLibraryId, (await identity.createLibrarySourceIdentity({ + sourceLocator: `${source.sourceLocator}.moved`, + sourceSha256: source.sourceSha256, + })).sourceLibraryId); +}); + +test("M12-03B fixes operation-specific owner and read-only semantics", async () => { + const expected = [ + ["APPEND", "LOCAL_MAIN", false, false], + ["LINK", "SOURCE_LIBRARY", true, true], + ["LIBRARY_OVERRIDE", "LOCAL_OVERRIDE", false, true], + ]; + for (let index = 0; index < golden.bindings.length; index++) { + const binding = golden.bindings[index]; + assert.deepEqual(await identity.createLibraryOperationBinding(bindingInput(binding)), binding); + assert.deepEqual(await identity.parseLibraryOperationBinding(binding), binding); + assert.deepEqual(await identity.assertLibraryOperationBindingCurrent(binding, stateFor(binding)), binding); + assert.deepEqual([binding.operation, binding.owner.kind, binding.readOnly, binding.referenceReadOnly], expected[index]); + } + assert.equal(new Set(golden.bindings.map((binding) => binding.invalidationToken)).size, 3); +}); + +test("M12-03B rejects owner, read-only, source, and token substitution", async () => { + const [append, link, override] = golden.bindings; + await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(append), readOnly: true }), { code: "ASSET_MANIFEST_INVALID" }); + await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(link), owner: append.owner }), { code: "ASSET_MANIFEST_INVALID" }); + await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(override), referenceReadOnly: false }), { code: "ASSET_MANIFEST_INVALID" }); + await assert.rejects(identity.createLibraryOperationBinding({ + ...bindingInput(override), + owner: { ...override.owner, referenceSourceDataBlockId: "Object/Other" }, + }), { code: "ASSET_SOURCE_HASH_MISMATCH" }); + await assert.rejects(identity.parseLibrarySourceIdentity({ ...golden.source, sourceSha256: "f".repeat(64) }), { code: "ASSET_SOURCE_HASH_MISMATCH" }); + await assert.rejects(identity.parseLibraryOperationBinding({ ...append, sourceRevision: 8 }), { code: "REVISION_CONFLICT" }); + await assert.rejects(identity.parseLibraryOperationBinding({ ...link, owner: { ...link.owner, sourceDataBlockId: "Object/Other" } }), { code: "ASSET_SOURCE_HASH_MISMATCH" }); + await assert.rejects(identity.parseLibraryOperationBinding({ ...override, future: true }), { code: "ASSET_MANIFEST_INVALID" }); +}); + +test("M12-03B invalidates generation, revision, source, and dependency closure drift", async () => { + const binding = golden.bindings[2]; + for (const drift of [ + { sourceGeneration: 4 }, + { sourceRevision: 8 }, + { sourceSha256: "c".repeat(64) }, + { dependencyClosureSha256: "d".repeat(64) }, + { sourceLibraryId: `library:${"e".repeat(64)}` }, + ]) { + await assert.rejects(identity.assertLibraryOperationBindingCurrent(binding, { ...stateFor(binding), ...drift }), { code: "REVISION_CONFLICT" }); + } +}); + +test("M12-03B enforces exact schemas and UTF-8 budgets", async () => { + await assert.rejects(identity.parseLibrarySourceIdentity({ schemaVersion: 2 }), { code: "PROTOCOL_MISMATCH" }); + await assert.rejects(identity.createLibrarySourceIdentity({ sourceLocator: "x".repeat(4_097), sourceSha256: "a".repeat(64) }), { code: "ASSET_MANIFEST_INVALID" }); + await assert.rejects(identity.createLibraryOperationBinding({ ...bindingInput(golden.bindings[0]), sourceGeneration: 0 }), { code: "ASSET_MANIFEST_INVALID" }); +}); + +test.after(() => fs.rmSync(temporary, { recursive: true, force: true })); diff --git a/web/tests/unit/render-compositor-media-recovery.test.mjs b/web/tests/unit/render-compositor-media-recovery.test.mjs new file mode 100644 index 00000000..c684943d --- /dev/null +++ b/web/tests/unit/render-compositor-media-recovery.test.mjs @@ -0,0 +1,81 @@ +import assert from "node:assert/strict"; +import fs from "node:fs"; +import path from "node:path"; +import test from "node:test"; +import ts from "typescript"; + +const repoRoot = path.resolve(import.meta.dirname, "../../.."); +const sourcePath = path.join(repoRoot, "web/protocol/render-compositor-media-recovery.ts"); +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 moduleUrl = "data:text/javascript;base64," + Buffer.from(transpiled.outputText).toString("base64"); +const recovery = await import(moduleUrl); + +const hash = "a".repeat(64); +const codes = { + RENDER: ["OPEN_CANCELLED", "GPU_TEXTURE_BUDGET_EXCEEDED"], + COMPOSITOR: ["COMPOSITOR_CANCELLED", "COMPOSITOR_BUDGET_EXCEEDED"], + MEDIA: ["SEQUENCER_CANCELLED", "SEQUENCER_BUDGET_EXCEEDED"], +}; +const report = (domain) => ({ + schemaVersion: 1, + domain, + source: { byteLength: 64, sha256: hash }, + cancellation: { status: "CANCELLED", code: codes[domain][0], publishedResults: 0, temporaryResourcesAfter: 0 }, + restart: { + status: "RECOVERED", + generationBefore: 1, + generationAfter: 2, + identityBefore: hash, + identityAfter: hash, + outputSha256Before: hash, + outputSha256After: hash, + }, + budget: { status: "BLOCKED", code: codes[domain][1], retainedIdentityHash: hash, temporaryResourcesAfter: 0 }, + release: { status: "RELEASED", releasedBytes: 64, releasedResources: 1, resourcesAfter: 0 }, + recovery: { status: "RECOVERED", identityHash: hash, outputSha256: hash, outputBytes: 64, visibleUnits: 4 }, +}); + +test("M11-14 accepts the complete render, compositor and media recovery suite", () => { + const parsed = recovery.parseRenderCompositorMediaRecoverySuite([ + report("MEDIA"), + report("RENDER"), + report("COMPOSITOR"), + ]); + assert.deepEqual(parsed.map((item) => item.domain), ["RENDER", "COMPOSITOR", "MEDIA"]); + assert.equal(parsed.every((item) => item.release.resourcesAfter === 0), true); +}); + +test("M11-14 rejects cancellation publication, domain code drift and duplicate domains", () => { + const published = report("RENDER"); + published.cancellation.publishedResults = 1; + assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(published), /must be zero/); + + const wrongCode = report("MEDIA"); + wrongCode.cancellation.code = "COMPOSITOR_CANCELLED"; + assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(wrongCode), /cancellation contract/); + + assert.throws(() => recovery.parseRenderCompositorMediaRecoverySuite([ + report("RENDER"), + report("RENDER"), + report("MEDIA"), + ]), /duplicate domains/); +}); + +test("M11-14 rejects restart drift, budget mutation and incomplete release", () => { + const restartDrift = report("COMPOSITOR"); + restartDrift.restart.outputSha256After = "b".repeat(64); + assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(restartDrift), /restart changed/); + + const budgetMutation = report("RENDER"); + budgetMutation.budget.retainedIdentityHash = "b".repeat(64); + assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(budgetMutation), /budget failure changed/); + + const leaked = report("MEDIA"); + leaked.release.resourcesAfter = 1; + assert.throws(() => recovery.parseRenderCompositorMediaRecoveryEvidence(leaked), /must be zero/); +}); diff --git a/后续工作.txt b/后续工作.txt index 0f168a55..85c23cf5 100644 --- a/后续工作.txt +++ b/后续工作.txt @@ -14,9 +14,11 @@ Web Blender M6/M7/M8/M9/M10/M11 接续执行记录 - M8 当前完成:20/20;剩余:0。 - M9 当前完成:14/14;剩余:0。 - M10 正式完成:15/15;剩余:0。 -- M11 当前完成:13/14;剩余:1。 -- 已完成到:M11-13。 -- 下一领取点:机器队列的最新 `nextTask`(本文不缓存任务名称)。 +- M11 正式完成:14/14;剩余:0。 +- 已完成到:M11-14。 +- M12 当前完成:M12-01A-I、M12-02A-H 与 M12-03A-D。 +- 已完成到:M12-03D。 +- 下一领取点:机器队列的最新 `nextTask` 为 `M12-03E`。 - quick lane:READY,7/7 命令通过,总命令耗时 11,455 ms,报告 release/ci-reports/quick.json。 - Chromium lane:READY,9/9 命令通过;每个 browser server 使用独立动态端口,报告 @@ -96,6 +98,9 @@ Web Blender M6/M7/M8/M9/M10/M11 接续执行记录 - M11-13 已完成实时音频恢复门:schema 1 区分 context/output/mute 状态;真实 Chromium `AudioContext` 通过 suspend/resume、muted resume、unmute gain 恢复和 close。缺 API/ 构造失败与 resume failure 均保持结构化静音阻断。 +- M11-14 已完成 Render/Compositor/Media 三域故障生命周期门:真实 `.blend`、H.264 和 WebGL + 输出通过取消不发布、generation 2 重开、预算失败不改已提交状态、显式 clear/dispose 回执、 + owned resource 归零和恢复后 identity/output SHA-256 一致性验证。 本轮新增并已验证 - M6-09A-C:binary archive 解包到 mktemp 后由包内 deployment contract 启动;新浏览器 @@ -2329,3 +2334,456 @@ M11-13 实际验证(2026-08-17 America/New_York) 已同步;M11 更新为 `13/14`,machine slice 计数为 191 completed/58 blocked。waveform、A/V sync、实际多 strip 混音/设备选择和 M11-14 跨域 fault lifecycle 均未提前声明。下一领取点 继续只读取机器队列最新 `nextTask`,本文不缓存任务名称。 + +M11-14 实际验证(2026-08-17 America/New_York) +- 领取 M11-14:新增 schema 1 `RenderCompositorMediaRecoveryEvidenceIR`,固定 RENDER、 + COMPOSITOR、MEDIA 三域 source、cancellation、restart、budget、release、recovery 字段;逐域 + cancellation/budget code 固定,协议拒绝缺失/重复域、取消后发布、restart hash drift、预算 + 改变 committed identity、非正释放回执和资源未归零。 +- Render 生产路径打开真实 `m11_render_reference.blend`,以 `ViewportRenderer` 读取非空 WebGL + RGBA8;预取消 open 返回 `OPEN_CANCELLED` 且旧 Main identity 不变,WebEngine generation 2 + 重开后 scene/output SHA-256 一致。纹理 aggregate 超限返回 + `GPU_TEXTURE_BUDGET_EXCEEDED` 且已加载 texture stats 不变,dispose 返回正数 byte/resource + 并归零。Compositor 从真实 Main 读取 allowlist GraphIR,周期取消和 8193 宽超预算分别返回 + `COMPOSITOR_CANCELLED`/`COMPOSITOR_BUDGET_EXCEEDED`,重开后 Float32 output hash 一致, + LRU clear 返回释放字节并归零。 +- Media 通过真实 H.264 HTMLMedia probe/decode 生成 8x8 RGBA8 proxy;预取消返回 + `SEQUENCER_CANCELLED`,低于一帧预算返回 `SEQUENCER_BUDGET_EXCEEDED` 且 cache 仍为空。 + 首会话 clear 后重新 probe/decode H.264 并重建 cache,source/decode/profile identity 与 + payload SHA-256 保持一致。 +- `WEB_TEST_PORT=5592 npm --prefix web run test:render-compositor-media-recovery` 首次完整运行通过: + protocol unit `3/3`、Eevee reference checker、Compositor desktop golden checker、Chromium + `1/1`。全量 Node `193/193`、typecheck、lint 和 production build(75 modules)通过;受影响 + 回归中的 render resource budget unit `3/3` + Chromium 主线程/Offscreen `3/3`、Compositor + golden unit `3/3` + Chromium `1/1`、media proxy cache unit `3/3` + Chromium `1/1` 全部通过。 + status consistency 为 12 parity blocked、0 release blocked、17 evidence、0 missing;release + evidence 为 `READY`,`git diff --check` 通过。 +- M11-14 hashes:protocol + `a1b0b36cf93173347d55a955ff2e02f3666f7c28ef7f06442b86f0ee2b272286`;production suite + `ef9cd58ce3bbd522c72075e82d6423387b61039509996851d8d5ebf3a0d86789`;media proxy cache + `7a1dbf8e838c47e2b99bc8932997e79d3c3cf2636b6285ace083800d833e2e64`;texture store + `a87adefa67b243cfd0cfcad0573a1b646f1e8be8abd0a187e1dc29cab2954d44`;compositor cache + `3ce4819dadb2d6de7305c3676b2708689cd8be95e6fa7894651d6a02c4658b31`;unit + `3cec4a33f28a76423a9678bcb61244cc463f5f2f5939c3f0b6865d34063e1f55`;Chromium spec + `1578848eda7cb26cdf1584fa13f4cc2e252e782959d489dc4454e45c8d06595a`;golden manifest + `87f17b0437d819e00c4a0902146d6549cf713482667ead627a45ba2b46050247`;package + `0e731a5c826a999eef372f875c6c5ce9d697fd369a929025370a74cf18a93582`;parity ledger + `abf91dd37de5fe1b5ce43912f87672b1346994500da27090fabce6cff66c6786`;release evidence + `7cc927bf068500e0dfabd5dbb76082292c0eef92969850ba11a3ee533601daca`。 +- `docs/status/M11-14.md`、N-019/020/021、当前/完整对标计划、项目状态、parity ledger、release + evidence 和本文件已同步;M11 更新为 `14/14`,machine slice 计数为 194 completed/58 + blocked。12 个 family 的全域 `parityStatus` 仍保持 `BLOCKED`;机器队列唯一下一任务切换为 + `M12-01A`,未提前实现 catalog inventory。 + +M12-01A 实际验证(2026-08-17 America/New_York) +- 领取 M12-01A:新增 machine inventory,逐项绑定 Blender 5.2 `DNA_asset_types.h`、 + `rna_asset.cc`、Blend IO、AssetCatalog/CDF/path 源码和当前 Web v1 protocol 共 8 个 source + SHA-256。清单覆盖 `AssetMetaData` 14 个 DNA 字段、10 个真实 RNA 属性、`AssetTag` 3 个 + 存储字段、catalog 3 个持久语义字段、3 个 runtime flag、CDF v1 格式和 path hierarchy。 +- 实际 Blender 5.2 background runtime 返回 author/description/copyright/license/tags/active_tag/ + catalog_id/catalog_simple_name/use_preferred_import_method/preferred_import_method 共 10 个 RNA + 属性;类型和 readonly/runtime 标志与源码 inventory 完全一致。 +- Web v1 snapshot 通过 TypeScript AST 固定 `AssetCatalogIR` 3 字段和 `AssetEntryIR` 10 字段。 + 当前缺 properties、description、copyright、catalog recovery name、active tag 和 preferred import + 两字段共 7 项;同时记录 Blender optional author/license、path hierarchy 与 Web parentId graph、 + 64-byte tag 和 custom ID properties 的语义差异,未在本任务直接改 schema。 +- `npm --prefix web run test:asset-catalog-inventory` 首次完整通过: + `dna=14 rna=10 tag=3 catalog=3 gaps=7 next=M12-01B`。inventory hash + `194d5fd0fef5044f5e82a51931a061e226445f0f53410af9dc0594c70a79e936`;checker hash + `38839f47dbe82ad7931e5e444e124e87b64a604d9358be6d71cce7769439e57b`;package hash + `4b234613a016a43ef918876b69bfdd65db0a57aae0c37324d1f58c4dd8acb736`。 +- `docs/status/M12-01A.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked, + release evidence 无需伪造新功能记录;机器队列唯一下一任务切换为 `M12-01B`。 + +M12-01B 实际验证(2026-08-17 America/New_York) +- 领取 M12-01B:锁定 Blender 5.2 generator 在临时目录生成 CDF v1 和 `.blend`,覆盖 3 条 + catalog、嵌套 path、Object/Material/World 三类资产、assigned/nil UUID、可空文本、ordered + tags、active tag、preferred import method 和 custom ID properties。Object `asset_mark()` 自动 + 生成的 dimensions metadata 也进入 canonical,不手写删除真实字段。 +- 独立 exporter 由新的 Blender 进程重开 fixture 后读取 AssetMetaData 和 CDF,稳定排序并输出 + schema 1 canonical JSON。`catalog_simple_name` 如实保持 RNA setter 在无 Asset Browser context + 下产生的空恢复名,不伪造只读字段。 +- `npm --prefix web run test:asset-catalog-v1-fixture` 完整通过:从新临时目录重新生成 fixture/CDF, + 新进程重开生成物和 checked-in fixture,两次 canonical 均与 golden 精确一致;输出 + `catalogs=3 assets=3 metadata=complete next=M12-01C`。 +- hashes:generator `4156ec6293562d44f87ad5a268de52fbfa31a65da8263a67a2bc45bf22999307`; + exporter `3dbccd8df54d8686aa526f7967f58fd1008336c4c5b1deed01a136ba15aa3e41`;checker + `c2813aa36482eff0b23ca0209f488cfe70ea11e4f39cc6c6036d6c47bbc8e17b`;fixture + `8facfe82e3ca0a0605c139ef5bdcbb82d27a6953a6add4e008b68af90bc1211f`;CDF + `a72f542acb2d2239951a2b20cda0f92305e1d6d8222f84072ad1aab52e3e4c98`;canonical + `6fc6def0b9d0e4f51ef40c1d87a1f17074f6340c126a01aee49e5e53271c961e`;manifest + `1ee0abf961b82195e5fb586eb9e59302e4f7f839615f1ebbdf5cdcba8b60accd`。 +- `docs/status/M12-01B.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked, + release evidence 无需修改;机器队列唯一下一任务切换为 `M12-01C`。 + +M12-01C 实际验证(2026-08-17 America/New_York) +- 领取 M12-01C:新增独立 schema v2。catalog 以 canonical RFC4122 UUID 和 cleaned slash path + 为权威,`parentPath` 必须从 path 精确导出,不延续 Web v1 `parentId` 图;asset 以 Blender + AssetWeakReference 的 library identifier + relative asset identifier 生成 SHA-256 stable Web ID。 +- schema 覆盖 M12-01B 全部 metadata,nil UUID 映射为 null,同时保留空 author/license 等合法 + Blender 状态。catalog/tag simple text 固定 63-byte payload,所有 name/path/dynamic metadata/ + custom property 和 aggregate text 预算均按 UTF-8 字节计算;catalog/assets/tags/properties/array + 元素均有显式上限。 +- 生产 converter 从 M12-01B desktop canonical 生成 M12-01C golden,不复制测试实现。 + `npm --prefix web run test:asset-catalog-v2` 4/4 通过,覆盖 Blender source/hash 绑定、desktop + 转换、local/external stable ID、预算常量和 golden parse;typecheck、lint、diff-check 通过。 +- hashes:protocol `0949838efb2f69466368185b80bb1d2e851672d78311fcd5270eb0814576e5d7`; + generator `b20e7dbd8508ca8ecf17d7da507b45b1225315bac7759c55c7b324683d660881`;unit + `73038022124ddc7e683602b47a4804297b3a64ecd4819160c7ec086d2f6190ae`;golden + `2abe279cebbffd1341dc61b1fdc5366dd1f6f42769ba36171782019ca61fb929`;manifest + `612892b03a689ebe19804e495bd29744634ab43a3b29fff41adc2d7a5d0b67b9`;Blender source + `ace3d6468bcdb2f6190431e967495794b7afa35758fd82fc3941b1ce7d374635`。 +- `docs/status/M12-01C.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01D`。 + +M12-01D 实际验证(2026-08-17 America/New_York) +- 领取 M12-01D:迁移先走现有 v1 production parser,再对所有层级做 exact-key 审计,任何未知 + 字段在转换前阻断,避免静默丢失。canonical UUID 原样保留;其他 legacy catalog ID 用固定 + namespace 的 RFC4122 UUIDv5 映射;parent graph 转为 Blender cleaned slash path。 +- asset 按 Blender weak reference identity 生成 v2 ID。logical revision 保持不变,author/license/ + tags/source hash/path/preview receipt/library dependency graph 全量保留;v1 缺少的 dynamic metadata、 + preferred method 和 custom collection 使用显式默认并计入 report。 +- migration report 绑定 canonical source/target SHA-256、逐 catalog/asset mapping 和 preserved/default + 计数。`npm --prefix web run test:asset-catalog-migration` 3/3 通过;3 catalog、2 asset、1 preview、 + 2 source、2 library 正例与重复执行完全一致;M12-01C、typecheck、lint、diff-check 回归通过。 +- hashes:migration protocol `6d74bc5605f94e05208eda82830192d38f88066dfc409058555a8876c785f226`; + generator `954d15c02d68e3f644b57c8c4c136ce09bd037a0af2012bf74b97f681c77dd60`;unit + `6b927e7a8238e4e0a77cc0fdf783b099d8add22c5f875df2299012eb32730186`;v1 + `1cc43d181afefd63b53cb9c3626bb58195fad20187d6e534adc1ecbe65f75da5`;v2 + `38e5d1771b3d2d63ce5e10268eac4513f2043b0cbf71ab1799c1868bc2aa97fb`;report + `57b972c1bbb9ae326c3f264c225c21546682442ff3194fbc343e1c46a2cd410a`;manifest + `fea9e01b51f64a7e9c15dcfc521b3e84636525ffbe0a849f7a6f1b0be2b02e9d`。 +- `docs/status/M12-01D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01E`。 + +M12-01E 实际验证(2026-08-17 America/New_York) +- 领取 M12-01E:新增 compatibility contract。原生 v1 文档保持 READY;合法 v2 对 v1 reader + 只返回有界 catalog/asset/library metadata snapshot 和 canonical source hash,状态为 READ_ONLY; + CATALOG_WRITE、ASSET_WRITE、SAVE 都返回 `ASSET_SCHEMA_DOWNGRADE_BLOCKED`。 +- 未知未来 schema 返回不可恢复 `PROTOCOL_MISMATCH` 且不产生 snapshot;非法 v1/v2 返回 + `ASSET_MANIFEST_INVALID`。所有检查只读,不能把 lossy projection 序列化回 source。 +- generator 固定 read、3 类 write/save 和 future 共 5 份报告。`npm --prefix web run + test:asset-catalog-legacy-reader` 4/4 通过,验证 snapshot、全写入阻断、native v1、future reject、 + source hash 一致和输入对象不变;M12-01C/D、typecheck、lint、diff-check 回归通过。 +- hashes:protocol `5d51029b232e03f08fc0778d4933910b2a54818b4d653fbcc185c8d764641349`; + error contract `309ab84d5c755a69466ddb73e4b54e87caf62cbcac0f10f34d904e9f6f4a5f34`;generator + `208f568db092e1712095d77ab28db92d5a1eb11fe62813b237ce466f9268fd4e`;unit + `f6477222412044e6784b64c1be83dc9bd6050fe6a955ba5430a4ea963b20dbb4`;report + `3c2f9241b56ad5bd742dc4152dec703bdc56eef4e6d8d60e6edf163c4571ad54`;manifest + `cf55ea1f9430df04fb55bf07c68c1b2099b044b0da0fdb07a7251b13c459724b`。 +- `docs/status/M12-01E.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01F`。 + +M12-01F 实际验证(2026-08-17 America/New_York) +- 领取 M12-01F:直接对 production v2 parser 和 v1→v2 migrator 注入 duplicate catalog ID、 + duplicate asset ID、legacy parent cycle、v2 parentPath mismatch、64-byte UTF-8 simple-name/tag、 + v2 top/nested unknown field 和 v1 nested unknown field 共 9 类负例。 +- duplicate/cycle/parent/unknown 均稳定返回 `ASSET_MANIFEST_INVALID`;多字节文本用 32 个双字节 + code point 证明 63-byte 上限按 UTF-8 byte 而非 JS length,稳定返回 `ASSET_BUDGET_EXCEEDED`。 + 每个负例后立即重新 parse 原始 v2 golden,9/9 均通过,失败不污染 parser 状态。 +- `npm --prefix web run test:asset-catalog-negatives` 2/2 通过;typecheck、lint、diff-check 通过。 + unit hash `3a11605ac8666c8c4e1e2e85ccfd423115915820d0de6f92bdf72dff1f01ce41`;cases + `74672fa0aec248fd5d9a265743ef4dfe4d417fce8a24622b8b3d6be2f95c8874`;manifest + `127b2b605ac79a891cd6ab516ee031c02624a512e1043b111bfac2ebf1d8278d`。 +- `docs/status/M12-01F.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01G`。 + +M12-01G 实际验证(2026-08-17 America/New_York) +- 领取 M12-01G:新增 production IndexedDB catalog migration。v2 manifest 先由现有 v1 parser/ + migrator 完整准备,readwrite transaction 内再次核对 source row,再共同写入 v2 index、migration + receipt 并删除 v1 index;source 并发变化返回 `REVISION_CONFLICT`。 +- Chromium 分别在 target put 后和 source delete 后注入 `STORAGE_TRANSACTION`。每次都关闭并重开 + 数据库,database version、完整 v1 index、无关 legacy migration 行、缺失 v2/receipt 与基线精确 + 相同;随后无故障重试才原子提交 v2 index/receipt 并保留 revision 7 和 M12-01D 双 hash。 +- `WEB_TEST_PORT=5593 npm --prefix web run test:asset-catalog-indexeddb-migration` 通过:M12-01D + production migration unit `3/3`、M12-01G artifact unit `1/1`、Chromium `1/1`;typecheck 和 + diff-check 通过。 +- hashes(M12-01H 扩展同一生产 reader 后重绑):production migration `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`; + browser `82c40f38898277b4053a9654537ec9c21bc01a784d1091831e139361575a81e9`;unit + `a4366a16f28b91b3ff213e0232308eaecd392731f27972cfa49a11568eb0feca`;manifest + `2596067956211698f1b7a07d9423411e2db830b72e6b78593354953a568738e7`。 +- `docs/status/M12-01G.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01H`。 + +M12-01H 实际验证(2026-08-17 America/New_York) +- 领取 M12-01H:production loader 对 stored v2 index 重新 parse、计算 canonical SHA-256 并与 + transaction receipt 校验,再发布 revision、catalog order、asset order 和完整 AssetWeakReference + identity snapshot;receipt revision/hash drift 在发布前返回 `ASSET_MANIFEST_INVALID`。 +- 初始页面从 v1 原子迁移后读取 snapshot,真实 page reload 后重开相同 IndexedDB;随后两个独立 + module Worker 顺序启动,generation 1 terminate 后 generation 2 再打开数据库。四个 context 的 + 3 个 catalog、2 个 asset、revision 7 和 manifest hash 逐字段完全相同。 +- `WEB_TEST_PORT=5594 npm --prefix web run test:asset-catalog-restart` 从同步 G hash 后完整通过: + Node `5/5`、Chromium `2/2`;首次运行由 G manifest 旧 module hash 正确阻断,重绑后从头通过。 +- hashes:reader `283a08bccb5ea301bd2aef8f01d4496972e3962f75e340479336c50e3539ba57`;Worker + `e6007d352555be046b89d0becd719692652ee0418cc97087bf7599155f2a538e`;browser + `7945ff38dd474a74c265594b66534a280eb69407509137e60d41f1bea3b3c19b`;unit + `eb22b5cc852fa47d9bdd30afec2977facfca0dfa554e7ce738dc1bf8c4bf2122`;manifest + `59138eb83d63ae0d397b152e587fbe005d02bc2d229ee484c279b452ea3f2eae`。 +- `docs/status/M12-01H.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-01I`。 + +M12-01I 实际验证(2026-08-17 America/New_York) +- 领取 M12-01I:新增 M12-01 aggregate evidence,按精确队列顺序绑定 A-H 八份 manifest,并递归 + 核对每份声明的 Blender/Web source 与 artifact SHA-256;单独绑定 desktop canonical、Web v2 + golden/protocol、migration v1/v2 fixture/report/protocol、IndexedDB runtime 和 restart Worker。 +- checker 对 v1/v2 manifest 重新做 stable JSON SHA-256,结果与 migration report 的 source/target + hash、revision 7、preserved catalog/assets count 一致;H 的 catalog/asset order 与 target fixture + 一致,G 的两个 fault point 与 evidence 一致。 +- `WEB_TEST_PORT=5595 npm --prefix web run test:asset-catalog-m12-evidence` 完整通过:Blender 5.2 + inventory/fixture、schema v2 `4/4`、migration `3/3`、legacy `4/4`、negatives `2/2`、runtime Node + `5/5`、Chromium `2/2`;aggregate 输出 `subtasks=8 catalogs=3 assets=2 next=M12-02A`。 +- hashes:evidence `2b99f64a207c2ffb966963be8a2150feef67d5bf78946939d3a49a6335b55bc3`;checker + `7d0d9f5390648d4af79a8da6226627edb1bf99945bae1a7b1acfb2c5e2419a8b`;package + `a810fec6a9a24cad94cb01475f9529ac2f85da827d391a70ab7ddb57421003f8`。 +- `docs/status/M12-01I.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + M12.1 收口,机器队列唯一下一任务切换为 `M12-02A`。 + +M12-02A 实际验证(2026-08-17 America/New_York) +- 领取 M12-02A:machine inventory 绑定 Blender 5.2 PreviewImage DNA、slot enum、BKE/RNA、 + Blend reader、asset generator、尺寸常量和当前 Web v1/v2 共 11 个 source hash,以及 PNG/no-preview + 两个 fixture hash。原生有 ICON/PREVIEW 两槽,持久字段为 w/h/flag/timestamp/rect,runtime pointer + 不持久化;rect 每像素一个 packed RGBA32。 +- 延迟加载路径显式 premultiply alpha;RNA packed pixels 长度为 w*h,float components 为 w*h*4, + 只执行 byte/255。PreviewImage DNA/RNA 均无 color-space 字段,因此 inventory 固定 colorSpace=null, + 不伪造 sRGB 声明。 +- 锁定 asset fixture 重开后 Object/Material/World 三项 preview 都是真实 null;真实 8x8 PNG 经 + Blender preview API 加载得到 image 8x8、icon 32x32、packed/float count 64/256 和 icon count 1024。 +- `npm --prefix web run test:asset-preview-inventory` 通过,输出 + `slots=2 rna=9 absent=3 loaded=8x8 next=M12-02B`。inventory hash + `a4427426d94afdf1a2ccf5379d76d1797f8604a742a3f0d15bdca629272864bd`;checker + `c9cc83d21bac5f34c6d68cb1225dffcd92a5ec76c158ddce909b620cbbcb245c`;package + `04f832a3c5773c8cc5294ecdcf175478eed9710496e48c63698d79043384e328`。 +- `docs/status/M12-02A.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02B`。 + +M12-02B 实际验证(2026-08-17 America/New_York) +- 领取 M12-02B:新增 schema 1 preview identity。source 与 encoded content 分别绑定 MIME、bytes、 + SHA-256;content 额外绑定 width/height、RGBA8、SRGB 和 alpha mode;generator 绑定 name/version、 + Blender executable、script 与 canonical settings SHA-256。完整 record 再生成 canonical identity hash。 +- 锁定 Blender 5.2 用明确 PNG RGBA8/compression 0/Standard-sRGB/零 exposure/gamma 1 设置重编码 + 8x8 PNG。source 261 bytes 与 content 513 bytes 的 hash 不同,第二进程解码 pixel max error=0; + 两次临时重生成 content SHA-256 完全一致。 +- `npm --prefix web run test:asset-preview-identity` 通过:unit `3/3` 及 Blender generator checker。 + source/content bytes、generator、dimensions、identity 和 unknown field 漂移均 fail-closed。 +- hashes:protocol `2a1d877af42424014097c669e9d44c21b27e3171be185320554693cbefcd7438`;generator + `753a26be90f7d97828737ebc3a4ab88275a7c655b4f9992406c7c35fba6535ce`;checker + `4d8ece1a5440d202f4e79a48f1f26bc75b3fb11a02d821b8ebc5aee14b11e41a`;unit + `5b7b3ccaf410d627365579fa82bf2b48fed9ef17144aad963cdbd4bb7fbd4b5a`;identity + `1b1eb8416ce4aa4d83b0dc42b9cb375f1da965e3f0cb9650991289fc619a0a7b`;content + `ff139c4a1c5d388c78c5168dfd3272e80ea0d1b15b3743e29a38cd908e8c70e7`;manifest + `096f125461b963ee2c414f98f6bb00f02034a380a0cc9c43e0d84754324adb17`。 +- `docs/status/M12-02B.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02C`。 + +M12-02C 实际验证(2026-08-17 America/New_York) +- 领取 M12-02C:新增 schema 1 pre-decode planner。production 入口先 parse M12-02B identity, + 在 content hash 前限制 encoded payload 为 16 MiB;随后核对 SHA-256、PNG/WebP header、header + dimensions 与 identity,并计算 pixel count、RGBA8 decoded bytes 和 compression ratio。 +- 所有门均在 browser decoder 与 decoded allocation 前执行。预算固定为 width/height 各 4096、 + 16,777,216 pixels、64 MiB decoded RGBA8 和 100:1 ratio;PNG 只接受 signature + first IHDR, + WebP 只接受 size-bound RIFF/WEBP VP8X、VP8L 或 VP8 header。 +- `npm --prefix web run test:asset-preview-decode-budget` 5/5 通过。8 类负例覆盖 payload hash/length、 + MIME、dimensions、pixel/decoded bytes、compression ratio、encoded bytes 和 corrupt signature; + 每次失败后 513-byte/8x8 小正例立即再次通过。typecheck、lint、diff-check 通过。 +- hashes:decode protocol `3d1c491af2503a7a6012541ed43400d680f8e666cc12a54db284aeb6ac18ff63`; + unit `1a65df86d568ae8defd7cd9242f426ac900d92637eb3bed2cbb4a0fae2e86c20`;package + `8563916637fc3cb29b53432ae9b80d1dcf6269f2f5c092b595e72c6b0ada55ab`;manifest + `934b3ca29d905ad2f5c46f35acff4cf4b9ea291c423fb3aad4afe7e9f10e6244`。 +- `docs/status/M12-02C.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02D`。 + +M12-02D 实际验证(2026-08-17 America/New_York) +- 领取 M12-02D:production coordinator 先执行 M12-02C gate,将 content SHA-256 写入项目 OPFS, + 再回读并重复 identity/hash/header/budget 验证;只有这些步骤全部通过才构造 catalog revision+1。 +- IndexedDB transaction 重新读取并比较完整 source row,随后共同写 v2 catalog preview reference + 和 M12-02D head receipt。receipt 绑定 project/asset、base/committed revision、双 manifest hash、 + preview identity、payload bytes/hash 和 OPFS path;并发变化返回 `REVISION_CONFLICT`。 +- `BEFORE_OPFS_WRITE`、`AFTER_OPFS_WRITE`、`AFTER_CATALOG_PUT` 三处 Chromium fault 均保持完整 + revision 7 catalog 且无 receipt;后两处只留下未引用 payload。成功路径先持久化 513-byte PNG, + 再提交 revision 8;关闭/重开数据库后 receipt/reference 相同,OPFS bytes 再次复算 hash。 +- `WEB_TEST_PORT=5596 npm --prefix web run test:asset-preview-opfs-commit` 通过 Node `1/1`、Chromium + `1/1`;typecheck、lint、diff-check 通过。 +- hashes:coordinator `36418e480b108eb885b5d8da6633174ed1face1fa62742a64372a45280e727a2`;browser + `043b9b06d4c0275938cc1d4f9837df95f58f281130f3c0f2b90c10540244592e`;unit + `f489a1d9a4c5fb24a99bce6d31841534c7cf3981b53076a0d9e19d106e9d0d74`;manifest + `34fe6c6ab097e58f7b648dd84cb919ecb407a5742e387c513bb3ed53db50eed6`;package + `7363a8a03cb5f6b1d6bf40d8eabc90b9d48e91aeb4787b175deb4f242c676243`。 +- `docs/status/M12-02D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02E`。 + +M12-02E 实际验证(2026-08-17 America/New_York) +- 领取 M12-02E:使用同一 production M12-02D coordinator,让 catalog 中 Action/Object 两个不同 + Blender weak-reference asset 依次绑定同一 content SHA-256。两个 preview identity 因 assetId + 不同而不同,catalog author/license/type/path/tags 等 metadata 也保持各自原值。 +- 首次 commit 在 revision 8 写入 513-byte payload 并报告 `deduplicated=false`;第二次在 revision 9 + 先读取同一 SHA-256 path,完整长度/hash 复验后报告 `deduplicated=true`。真实 OPFS prefix 目录 + 最终只含一个以 content hash 命名的文件,无临时文件或 metadata sidecar 覆盖 payload。 +- 关闭/重开 IndexedDB 后 revision 9、第二 asset head receipt 和 preview reference 一致;两条 asset + 都引用相同 content,去除 preview 后的完整 catalog metadata 与初始两条分别精确相同。 +- `WEB_TEST_PORT=5597 npm --prefix web run test:asset-preview-dedup` 通过 Node `1/1`、Chromium `1/1`; + typecheck、diff-check 通过。 +- hashes:browser `4e53b7a35eb109b152bd6dc0c3348dcb08879d17619968d37fc761c67aa38b40`;unit + `e023fdd33e0f40c1331dfdd73cd287dc684256b9d134971ab17db8cfe2a83835`;manifest + `7d25f1d8335481795257c643c8589d1607a865634b27a7e5d145a57ce2a92a62`;package + `5a83676e821bb768f6d3e282ea4728413cf84c5f3d57672fc664fac3883873e5`。 +- `docs/status/M12-02E.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02F`。 + +M12-02F 实际验证(2026-08-17 America/New_York) +- 领取 M12-02F:新增 production preview inspector/quarantine。它先独立核对 catalog reference 的 + expected bytes/hash;健康数据返回 READY。缺失或 hash/length 漂移返回稳定 + `ASSET_SOURCE_HASH_MISMATCH`,不让 optional preview 阻断 asset metadata 读取。 +- 对存在的损坏 bytes,先复制到项目 `assets/quarantine`、复算实际 SHA-256/长度,再删除原 + content-addressed path,最后在 catalog full-row guard 下提交隔离 receipt。catalog v2 row 不改写, + receipt 绑定 project/asset/revision、完整 expected preview、actual bytes/hash 和 quarantine path。 +- Chromium 先确认 513-byte PNG READY,再翻转末字节制造同长度 hash drift。隔离后 SHA-256 path + 不存在、quarantine 文件 513 bytes;target asset、另一 asset 和 revision 8 全部不变。关闭/重开 + IndexedDB 后直接从相同 receipt 返回 QUARANTINED、preview/data null 和完整 asset metadata。 +- `WEB_TEST_PORT=5598 npm --prefix web run test:asset-preview-quarantine` 通过 Node `1/1`、Chromium + `1/1`;typecheck、diff-check 通过。 +- hashes:protocol `a5f3330dbd16d9f578d85d9504c980fffea85b834b77340a307ce21ce9b1cb04`;browser + `d8e1d0741765e8f8bfd47b45cf03213d8629efdbbf77dfd33846785a3ec80c27`;unit + `79c836c7a3be4e9f305a962775444c766577cca87fa6d1698811e75eb21f2d9a`;manifest + `d1a9feeb98c5332b1a14a14c5b813d75a29f172601590e1b5cde254c5059a7c5`;package + `77b5cd3d3af68ac390d8fbdb9b2f64764fb4521a1d2bf22cd8a89e40d69424f6`。 +- `docs/status/M12-02F.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02G`。 + +M12-02G 实际验证(2026-08-17 America/New_York) +- 领取 M12-02G:新增 production reference GC。它先 parse v2 catalog、核对 expected revision, + 只清空目标 asset.preview 并推进 revision;IndexedDB transaction 对完整 source row 再校验, + 提交新 catalog/hash receipt 后才允许触及 payload。 +- committed manifest 中仍有同 SHA-256 reference 时返回 RETAINED;计数为零时才删除 + `projects//assets/sha256` 下该文件并把 receipt 更新为 COLLECTED。OPFS project namespace + 是跨项目 ownership 边界,回收函数无法定位或删除另一 projectId 的 payload。 +- Chromium 中项目 A 两个不同 asset 先共享同一 payload,项目 B 另持同 hash payload。A 的首次 + removal revision 9->10 保留文件;末次 10->11 回收 A 文件。B 文件始终存在,inspector 返回 + READY/513 bytes;重开 A 后两 preview 都为 null,完整 metadata 与初始一致。 +- `WEB_TEST_PORT=5599 npm --prefix web run test:asset-preview-reference-gc` 通过 Node `1/1`、Chromium + `1/1`;typecheck、diff-check 通过。 +- hashes:GC protocol `76b9d8ccd3e7992356f33adc694001f9b0ff364788234389eacdb09198d1415e`;browser + `2e9e9b47883997092c5afb0fb7859a484754764de19006e24a8ff2bdeb24f35a`;unit + `c5de3122b5d8aab17daf0179cb4f5bb0b5d299408818849cbe7b18e40a47706e`;manifest + `3449f2ea9488f66ea5e3f5f2423413e8e290d8ffed3b3d941bdeee3f76addd0a`;package + `7d1db0932fbe7405ffdbd44b770ce7bf9815b9dfb829c1906a06bfff635ae702`。 +- `docs/status/M12-02G.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步。 + 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 194 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-02H`。 + +M12-02H 实际验证(2026-08-17 America/New_York) +- 领取 M12-02H:新增共享的 production preview display。它复用 M12-02C pre-decode 门,随后以 + `colorSpaceConversion/premultiplyAlpha/imageOrientation=none` 创建 ImageBitmap,禁用 smoothing, + 用 copy composition 绘制并读取 RGBA8;receipt 绑定 identity/content/backend/dimensions/pixel hash, + bitmap 在成功和失败路径均关闭。 +- Blender 5.2 重新生成 8x8 `SRGB/RGBA8/STRAIGHT` PNG,ImageMagick 读取的 desktop reference + 为 256 bytes,SHA-256 `c2ff81750b41193ce1a06d47d0ff18168a756136dbf0f2586cd0638d4bf0ef00`。 + 主线程 HTMLCanvasElement 与独立 Worker OffscreenCanvas 共用同一实现并逐字节匹配 reference; + MAE/RMS/P95/max/bad-pixel/alpha delta 全为 0,foreground IoU=1,单通道错误负例被阻断。 +- `WEB_TEST_PORT=5600 npm --prefix web run test:asset-preview-display` 通过 Node `1/1`、desktop + checker 与 Chromium `1/1`。manifest 分类纠正为 `LOCAL_EXACT`,未把渲染结果误标为平台等价。 +- hashes:runtime `e4a46a40291427afc185caf016b02120ea71639ab4ab4ac250a5c3de28ee8d28`;Worker + `6322562f54b608b7eaac4ca3bc0f8d861f47bb8e9b37b3c2ae3d61163108724f`;desktop checker/report + `d03d766e69ea0547be2c247b5d7ef8fd5ceab6b399dc996eba711fa0cac0ca54` / + `cdf841f5d27362f6df380772ada8b7b1427d051a8b46b5c2c20aab0b597c25d0`;browser + `340d386c6a854c44cd6f6207e3699f870b8ded2aada4747afd6ff1103c719520`;unit + `f10a05900c4d1282251f7a76e7c413a18602910f55fe8400f1713f548eba3873`;manifest + `30e6e4b6f53e07768d2ae8d853fe08a5c0782f8608320a72e41db77f6d87f2c4`;package + `2510242fbc0c2da787f558365733a4db6faf9f490e2594f69dc978920ed09f21`。 +- `docs/status/M12-02H.md`、N-023、当前/完整对标计划、项目状态、WBS、ledger 和本文件已同步。 + 本项增加 `A-preview-desktop-browser-main-offscreen-display`,ledger 更新为 195 completed/58 + blocked;机器队列唯一下一任务切换为 `M12-03A`。 + +M12-03A 实际验证(2026-08-17 America/New_York) +- 领取 M12-03A:新增 Append/Link/Library Override machine inventory,绑定 link/append context、 + closure、operator、Python load、Blend reader、override DNA/API/implementation 与当前 Web gate + 共 9 个 source hash,并绑定真实 nested-library `.blend` fixture。 +- Blender runtime 公开 36 类 selectable collections;Screen/WorkSpace 为 append-only,Library 为 + source/transitive metadata,Key 为 embedded owner dependency。Append 盘点 direct/indirect、跨库、 + override-only、reuse-local 和 instantiate 角色;Link 盘点 EXTERN/INDIRECT、ID.lib 和 parent library; + Override 盘点 linked reference/local owner/hierarchy root/system dependency/property operation。 +- `npm --prefix web run test:library-operation-inventory` 通过,报告 + `roots=36 appendOnly=2 operations=3 overrideFields=17 gaps=8 next=M12-03B`。首轮源码 token 与 + 第二轮单行 Python 语法问题均已纠正后完整重跑通过。 +- hashes:inventory `0dcb16233b52b3c0be2373f489d6bf3c771c9d6564993ff98b5d63dcd322fef7`; + checker `1581490d1923578293202d27a1c379eb2a8c5e13be418547d148cf09c6a783fd`;package + `a7cf339b0cf6fab17ef63e9394b2e540af60327f7bf1f7fe95823d182e0d3e63`。 +- 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-03B`。 + +M12-03B 实际验证(2026-08-17 America/New_York) +- 领取 M12-03B:新增 schema 1 library operation identity。sourceLibraryId 由 canonical locator 与 + `.blend` SHA-256 生成;binding 再绑定 operation、source root、owner、read-only、generation、 + revision、dependency closure 与 invalidation token。 +- APPEND 强制 `LOCAL_MAIN/readOnly=false/referenceReadOnly=false`;LINK 强制匹配 source 的 + `SOURCE_LIBRARY/true/true`;LIBRARY_OVERRIDE 强制 `LOCAL_OVERRIDE/false/true` 并保留 linked + reference/hierarchy root。三种 operation 对相同 source 生成三个不同 token。 +- generation/revision/source ID/source hash/dependency closure 任一 current state 漂移均返回 + `REVISION_CONFLICT`;owner 互换、只读漂移、reference 替换、未知字段和伪造 token 均 fail-closed。 +- `npm --prefix web run test:library-operation-identity` 完整通过 `6/6`;typecheck 通过。 +- hashes:protocol `5371133bd98e05935cd75bab582abedec32a36559af8d212f0bc8a534ef19451`; + golden `b39c0f26697ac688c1203ae4e3e27d8a02686c7c9da9afc438c89a0b4d69370f`;unit + `0e5b2c82d481180eb8eb22de16ad8cdef5e58fa7423c26011faaffbac788e547`;manifest + `f388aedb9c0901932cf90c058f2f2b83259d9797cd6b144566a25e37a348c64b`;package + `48cf41f0d0041bf4f497bc2a454cfa9469874eded1775cea50fe3249d4e0b59e`。 +- 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-03C`。 + +M12-03C 实际验证(2026-08-17 America/New_York) +- 领取 M12-03C:新增 Blender 5.2 desktop 单 Object append generator,source library 中的 + Object 通过 Mesh material slot 和 Material Image Texture node 形成 Mesh/Material/packed Image + 依赖闭包。干净 Main 只选择 append Object,Blender 自动带入全部依赖。 +- target 中 Object/Mesh/Material/Image 四个 ID 的 library pointer 均为 null,无 library + override,全部映射为可写 `LOCAL_MAIN`。保存并重开后 3 条依赖边、4/4/1 geometry、 + UVMap、material slot、2x2 sRGB RGBA packed image 及 Float32 pixel hash 不变。 +- `npm --prefix web run test:library-append-desktop` 通过,输出 + `roots=1 mapping=4 edges=3 pixels=6f0f8c231d65149e69e6ed12d370bcfa095ef90adc202065492ea8c8ef17e45a next=M12-03D`。 +- hashes:generator `b9d8e3bd24966b0d7d419382d7ef0b35ae8def9ee2c2236dfd3ad1a90aebe66f`; + checker `6f8cc0d51be19423cf6fb7203c3c16f8d27e0fbb3b5b357bad5dec6c5ceb432d`;source/target + `.blend` `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` / + `5cde4927f47e33f0338de38184ce5ba971825fcefd1d8dc6feb48d41a6cdb60c`;report + `b1d7b8b9e832d18d69081f63981062800e0f00f0c9aec025b18274510ed76e2a`;manifest + `cbfbd8c919125108334dd24925b9ef8e69880983515e56841e6ead4a2b182aed`。 +- 本项保持 `enablingTask=true/parityStateChange=false`,ledger 仍为 195 completed/58 blocked; + 机器队列唯一下一任务切换为 `M12-03D`。 + +M12-03D 实际验证(2026-08-17 America/New_York) +- 领取 M12-03D:Worker 对 source `.blend` 做 SHA-256、closure、owner/read-only 和 base revision + 校验后,调用新导出的 `_web_engine_append_library_object`。Native Blender Main 用递归 + link/append context 在一次 transaction 中导入 Object/Mesh/Material/packed Image,并在发布前 + 检查四个 ID 均无 `ID.lib`/override、均为可写 `LOCAL_MAIN`。 +- `source tools/web/emscripten-env.sh && cmake --build build_web_blender6 --target web_engine -- -j8` + 通过(当前构件无待重编译);`bash tools/web/install-web-engine-assets.sh` 通过并完成双 variant + manifest 资产校验。`npm --prefix web run test:library-append-desktop`、 + `npm --prefix web run test:library-operation-identity`、`npm --prefix web run typecheck` 和 + `WEB_TEST_PORT=5601 npm --prefix web run test:library-main-append` 均通过;Chromium 为 1/1。 +- 浏览器结果确认 `transactionCount=1`、四条 source/local mapping 均为 `LOCAL_MAIN`、revision + 只增加 1,Object→Mesh→Material→Image 依赖闭包完整;stale revision 返回 `REVISION_CONFLICT`, + local ID collision 返回 `ASSET_MANIFEST_INVALID` 且 revision 不变。失败前后 history、undo/redo + 和保存 buffer 均保持可恢复,完整 desktop canonical 对标仍留给 M12-03E。 +- hashes:test `7130453cbc81f22eb1bcf598ff3745672ac8177dccb11cef667f5b68b43e87c6`;worker + `e3bb430e401144bf17cdcbd0de13cde0a4f904f1f9ff9d5b10a73d90f0d4c648`;protocol + `bfd98561cbe797d7b8f07c92c53a25460c839a64ab7222b7795cf58d54dff8d7`;source/target `.blend` + `5b60d02926efd588a6ca300ba31414cdf37dbc48786c17b383a70319057c0606` / + `9b1ecbcc3d7f8079ee5469de64193ffcaa0a7b01e0f2ac340bbb18aa88734a63`;WASM JS/binary + `9c831d0351ac9d0714cce3b1da748cba416ced3086e3c59595cf24ac4da459ed` / + `7821f408687e455c6f4e185fc3741c199c1c60d409836a2fe1b32f521d81e0ea`;Main append API/implementation + `5a71260c542fa559f73c6b82e23e72d39bd2844f5ebe12c6edc809a49d79791b` / + `d0c484ab62e6ec051c8c2f67c49d750a456578aaccc2aa4f461026307af0db95`。 +- `docs/status/M12-03D.md`、N-023、当前/完整对标计划、项目状态、WBS 和本文件已同步;本项 + 保持 `parityStatus=BLOCKED`,ledger 仍为 195 completed/58 blocked。机器队列唯一下一任务切换为 + `M12-03E`。