Compare commits

..

115 Commits

Author SHA1 Message Date
sindoring f35dcf3a09 targets: accept WMO runtime Mesh finalizer 2026-07-18 14:35:39 +04:00
sindoring d65ebee57f merge: WMO runtime Mesh finalizer 2026-07-18 14:34:02 +04:00
sindoring a59086fdf0 coordination: hand off WMO runtime Mesh finalizer 2026-07-18 14:33:57 +04:00
sindoring ae40e3f1d4 render: extract WMO runtime Mesh finalizer 2026-07-18 14:33:48 +04:00
sindoring 49e70260ed coordination: claim WMO runtime Mesh finalizer 2026-07-18 14:27:17 +04:00
sindoring 406dc464c3 targets: accept WMO scene resource finalizer 2026-07-18 14:18:29 +04:00
sindoring 6a0f9bd3ba merge: WMO scene resource finalizer 2026-07-18 14:17:09 +04:00
sindoring 3525cc0ead coordination: hand off WMO scene resource finalizer 2026-07-18 14:17:04 +04:00
sindoring 7779e9e75f render: extract WMO scene resource finalizer 2026-07-18 14:16:49 +04:00
sindoring 9ba7ca3263 coordination: claim WMO scene resource finalizer 2026-07-18 14:08:02 +04:00
sindoring 8c54313f7d targets: accept WMO render resource finalizer 2026-07-18 14:04:51 +04:00
sindoring 1acddab8a5 merge: WMO render resource finalizer 2026-07-18 14:03:48 +04:00
sindoring 5a1cd9d7c8 coordination: hand off WMO render resource finalizer 2026-07-18 14:03:41 +04:00
sindoring a86f8f2212 render: extract WMO render resource finalizer 2026-07-18 14:03:27 +04:00
sindoring fb7c9f174e coordination: claim WMO render resource finalizer 2026-07-18 13:55:50 +04:00
sindoring f1a7400ed1 targets: accept M2 Mesh resource finalizer 2026-07-18 13:52:55 +04:00
sindoring 34b700051f merge: M2 Mesh resource finalizer 2026-07-18 13:51:25 +04:00
sindoring 71a1012779 coordination: hand off M2 Mesh resource finalizer 2026-07-18 13:51:21 +04:00
sindoring ddeb708c08 render: extract M2 Mesh resource finalizer 2026-07-18 13:51:08 +04:00
sindoring c328d86554 coordination: claim M2 Mesh resource finalizer 2026-07-18 13:38:02 +04:00
sindoring 75ddf6a2ed targets: accept M2 animation resource finalizer 2026-07-18 13:31:34 +04:00
sindoring 06f6394043 merge: M2 animation resource finalizer 2026-07-18 13:30:43 +04:00
sindoring b20f0d7f6f coordination: hand off M2 animation resource finalizer 2026-07-18 13:30:39 +04:00
sindoring e7cd967dce render: extract M2 animation resource finalizer 2026-07-18 13:30:24 +04:00
sindoring ff952da7d8 coordination: claim M2 animation resource finalizer 2026-07-18 13:18:53 +04:00
sindoring 70729bb341 targets: accept native M2 animation resource observer 2026-07-18 10:57:18 +04:00
sindoring d37c799850 merge: native M2 animation resource observer 2026-07-18 10:56:08 +04:00
sindoring c24c3f159c coordination: hand off native M2 animation resource observer 2026-07-18 10:56:04 +04:00
sindoring 1cb0101a73 render: extract native M2 animation resource observer 2026-07-18 10:55:47 +04:00
sindoring d22a9cd743 coordination: claim native M2 animation resource observer 2026-07-18 10:42:13 +04:00
sindoring 6f385ed261 targets: accept cached M2 animation resource observer 2026-07-18 10:24:30 +04:00
sindoring f79e064d25 merge: cached M2 animation resource observer 2026-07-18 10:23:29 +04:00
sindoring 6a23c1b996 coordination: hand off cached M2 animation resource observer 2026-07-18 10:23:22 +04:00
sindoring df87619220 render: extract cached M2 animation resource observer 2026-07-18 10:23:03 +04:00
sindoring 3519f183bb coordination: claim cached M2 animation resource observer 2026-07-18 10:10:19 +04:00
sindoring 5ebf4de2ff targets: accept static M2 build resource observer 2026-07-18 03:04:01 +04:00
sindoring a043c79654 merge: static M2 build resource observer 2026-07-18 03:02:06 +04:00
sindoring 194b64d030 coordination: hand off static M2 build resource observer 2026-07-18 03:02:02 +04:00
sindoring 7cb3e3412f render: extract static M2 build resource observer 2026-07-18 03:01:43 +04:00
sindoring f062ea91cd coordination: claim static M2 build resource observer 2026-07-18 02:51:46 +04:00
sindoring 032a256e70 targets: accept M2 build resource snapshot 2026-07-18 02:48:54 +04:00
sindoring 4354834c50 merge: M2 build resource snapshot 2026-07-18 02:47:35 +04:00
sindoring 41b3b63215 coordination: hand off M2 build resource snapshot 2026-07-18 02:47:26 +04:00
sindoring ec1b90f1e4 render: add M2 build resource snapshot 2026-07-18 02:47:10 +04:00
sindoring 0decd10e09 coordination: claim M2 build resource snapshot 2026-07-18 02:38:46 +04:00
sindoring b113db01cd targets: accept M2 build dispatch planner 2026-07-18 02:35:49 +04:00
sindoring 99a90ddfb3 merge: M2 build dispatch planner 2026-07-18 02:34:36 +04:00
sindoring ed71b36aec coordination: hand off M2 build dispatch planner 2026-07-18 02:34:31 +04:00
sindoring 17f5cc0faa render: extract M2 build dispatch planner 2026-07-18 02:34:18 +04:00
sindoring 1fb566f9bc coordination: claim M2 build dispatch planner 2026-07-18 02:27:16 +04:00
sindoring c74b90a8ea targets: accept M2 build queue 2026-07-18 02:25:14 +04:00
sindoring 1dc013e5c2 merge: M2 build queue 2026-07-18 02:24:00 +04:00
sindoring 8938350a59 coordination: hand off M2 build queue 2026-07-18 02:23:53 +04:00
sindoring 4a8338f2f0 render: extract M2 build queue 2026-07-18 02:23:40 +04:00
sindoring 3ee9e77422 coordination: claim M2 build queue 2026-07-18 02:13:15 +04:00
sindoring 81e33a9bc4 targets: accept static M2 batch materializer 2026-07-18 02:02:10 +04:00
sindoring 5e82daacb9 merge: static M2 batch materializer 2026-07-18 02:01:15 +04:00
sindoring 8339189907 coordination: hand off static M2 batch materializer 2026-07-18 02:01:09 +04:00
sindoring 252927d90c render: extract static M2 batch materializer 2026-07-18 02:00:55 +04:00
sindoring 0fd052923d coordination: claim static M2 batch materializer 2026-07-18 01:54:26 +04:00
sindoring 86814deca1 targets: accept animated M2 materializer 2026-07-18 01:50:05 +04:00
sindoring c251985256 merge: animated M2 instance materializer 2026-07-18 01:49:17 +04:00
sindoring 899a9ae012 coordination: hand off animated M2 materializer 2026-07-18 01:49:08 +04:00
sindoring 456f3e2334 render: extract animated M2 materializer 2026-07-18 01:48:55 +04:00
sindoring 9ab4c0762d coordination: claim animated M2 materializer 2026-07-18 01:39:44 +04:00
sindoring 5b2d9f23fe docs(M03): accept M2 animation playback 2026-07-18 01:27:33 +04:00
sindoring c0fc191bcd merge M03 M2 animation playback 2026-07-18 01:26:18 +04:00
sindoring 15c24692f0 coord(M03): hand off M2 animation playback 2026-07-18 01:26:10 +04:00
sindoring 9b73571dcb refactor(M03): extract M2 animation playback 2026-07-18 01:25:54 +04:00
sindoring 5be36376c2 coord(M03): claim M2 animation playback 2026-07-18 01:19:13 +04:00
sindoring c0a7bfbd8d docs(M03): accept M2 animated scene finalizer 2026-07-18 01:16:19 +04:00
sindoring e7a7c67f6d merge M03 M2 animated scene finalizer 2026-07-18 01:14:40 +04:00
sindoring bc2bd06abc coord(M03): hand off M2 animated scene finalizer 2026-07-18 01:14:32 +04:00
sindoring 1b450dc580 refactor(M03): extract M2 animated scene finalizer 2026-07-18 01:14:17 +04:00
sindoring 03ba129a52 coord(M03): claim M2 animated scene finalizer 2026-07-18 01:07:09 +04:00
sindoring c20bc62ac0 docs(M03): accept M2 animation load pipeline 2026-07-17 18:25:20 +04:00
sindoring 3dccd3e3ee merge M03 M2 animation load pipeline 2026-07-17 18:24:16 +04:00
sindoring 672e0ce2a5 coord(M03): hand off M2 animation load pipeline 2026-07-17 18:24:10 +04:00
sindoring b8e0f11ed5 refactor(M03): extract M2 animation load pipeline state 2026-07-17 18:23:55 +04:00
sindoring 2695afcdba coord(M03): claim M2 animation load pipeline 2026-07-17 18:16:59 +04:00
sindoring 20a64b5dfc docs(M03): accept M2 prototype cache state 2026-07-17 17:07:27 +04:00
sindoring 1de50f98ce merge M03 M2 prototype cache state 2026-07-17 17:00:59 +04:00
sindoring 5e828aaf01 docs(M03): hand off M2 prototype cache state 2026-07-17 16:29:28 +04:00
sindoring cd664eb4cb refactor(M03): extract M2 prototype cache state 2026-07-17 16:29:11 +04:00
sindoring 87bf067493 coord(M03): claim M2 prototype cache state 2026-07-17 16:18:29 +04:00
sindoring ca77d32be1 docs(M03): accept raw M2 model repository 2026-07-17 15:54:59 +04:00
sindoring 7cd07dda4f merge M03 raw M2 model repository 2026-07-17 15:53:54 +04:00
sindoring 8605b43b1f docs(M03): hand off raw M2 model repository 2026-07-17 15:53:39 +04:00
sindoring b608a81766 refactor(M03): extract raw M2 model repository 2026-07-17 15:53:20 +04:00
sindoring e4bb10cee7 coord(M03): claim raw M2 model repository
Work-Package: M03-RND-M2-RAW-MODEL-REPOSITORY-001
Agent: sindo-main-codex
Tests: coordination gate passed before claim
Fidelity: native-boundary extraction only; parser methods and paths unchanged
2026-07-17 15:44:15 +04:00
sindoring 1d94eace68 docs(M03): accept M2 runtime mesh finalizer
Work-Package: M03-RND-M2-RUNTIME-MESH-FINALIZER-001
Agent: sindo-main-codex
Tests: post-merge finalizer, classifier, extractor, cache, pipeline, materials, shutdown, facade, internal-access, manifest, documentation and coordination gates pass
Fidelity: preserves refresh version 2 and rebuild/fallback transitions; asset-backed visual parity remains unclaimed
2026-07-17 15:03:48 +04:00
sindoring 1fafddecb6 merge M03 M2 runtime mesh finalizer 2026-07-17 15:02:28 +04:00
sindoring 0007a69769 docs(M03): hand off M2 runtime mesh finalizer
Work-Package: M03-RND-M2-RUNTIME-MESH-FINALIZER-001
Agent: sindo-main-codex
Tests: 37 headless regressions, checkpoint dry-run, documentation and coordination gates pass
Fidelity: refresh/rebuild/fallback extraction only; asset-backed visual parity remains unclaimed
2026-07-17 15:02:16 +04:00
sindoring ece7724a28 refactor(M03): extract M2 runtime mesh finalizer
Work-Package: M03-RND-M2-RUNTIME-MESH-FINALIZER-001
Agent: sindo-main-codex
Tests: 37 headless renderer/coordinate contracts pass; checkpoint dry-run 7/7; documentation and coordination gates pass
Fidelity: preserves refresh version 2, classifier, rebuild and fallback transitions; no visual parity claim
2026-07-17 15:01:52 +04:00
sindoring 951bd54fff coord(M03): claim M2 runtime mesh finalizer
Work-Package: M03-RND-M2-RUNTIME-MESH-FINALIZER-001
Agent: sindo-main-codex
Tests: coordination gate passed before claim
Fidelity: extraction-only package; refresh version and visible behavior unchanged
2026-07-17 14:54:16 +04:00
sindoring dd8cfbf79c docs(M03): accept M2 mesh resource extractor
Work-Package: M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001
Agent: sindo-main-codex
Tests: post-merge extractor, cache, pipeline, classifier, materials, shutdown, facade, internal-access, manifest, documentation and coordination gates pass
Fidelity: preserves first-Mesh preorder and temporary PackedScene cleanup; asset-backed visual parity remains unclaimed
2026-07-17 13:04:17 +04:00
sindoring fa85985ec5 merge M03 M2 mesh resource extractor 2026-07-17 13:03:11 +04:00
sindoring f68e971cce docs(M03): hand off M2 mesh resource extractor
Work-Package: M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001
Agent: sindo-main-codex
Tests: 36 headless regressions, checkpoint dry-run, documentation and coordination gates pass
Fidelity: traversal/lifetime extraction only; asset-backed visual parity remains unclaimed
2026-07-17 13:02:58 +04:00
sindoring 7b0e1eac5f refactor(M03): extract M2 mesh resource traversal
Work-Package: M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001
Agent: sindo-main-codex
Tests: 36 headless renderer/coordinate contracts pass; checkpoint dry-run 7/7; documentation and coordination gates pass
Fidelity: preserves first-Mesh preorder and temporary PackedScene cleanup; no visual parity claim
2026-07-17 13:02:40 +04:00
sindoring 6607100686 coord(M03): claim M2 mesh resource extractor
Work-Package: M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001
Agent: sindo-main-codex
Tests: coordination gate passed before claim
Fidelity: extraction-only package; traversal and visible behavior unchanged
2026-07-17 12:57:38 +04:00
sindoring 5e2afbc400 docs(M03): accept M2 mesh resource cache state
Work-Package: M03-RND-M2-MESH-RESOURCE-CACHE-001
Agent: sindo-main-codex
Tests: post-merge cache, pipeline, classifier, materials, shutdown, facade, internal-access, manifest, documentation and coordination gates pass
Fidelity: preserves Mesh identity/replacement/final-shutdown lifetime; asset-backed visual parity remains unclaimed
2026-07-17 12:12:19 +04:00
sindoring 30945615b9 merge M03 M2 mesh resource cache state 2026-07-17 12:11:09 +04:00
sindoring c5ecaba0ab docs(M03): hand off M2 mesh resource cache state
Work-Package: M03-RND-M2-MESH-RESOURCE-CACHE-001
Agent: sindo-main-codex
Tests: 35 headless regressions, checkpoint dry-run, documentation and coordination gates pass
Fidelity: cache ownership/lifetime extraction only; asset-backed visual parity remains unclaimed
2026-07-17 12:10:53 +04:00
sindoring e990a6503d refactor(M03): extract M2 mesh resource cache state
Work-Package: M03-RND-M2-MESH-RESOURCE-CACHE-001
Agent: sindo-main-codex
Tests: 35 headless renderer/coordinate contracts pass; checkpoint dry-run 7/7; documentation and coordination gates pass
Fidelity: preserves exact Mesh references, replacement semantics and final-shutdown lifetime; no visual parity claim
2026-07-17 12:10:37 +04:00
sindoring e2bb501695 coord(M03): claim M2 mesh resource cache state
Work-Package: M03-RND-M2-MESH-RESOURCE-CACHE-001
Agent: sindo-main-codex
Tests: coordination gate passed before claim
Fidelity: extraction-only package; visible behavior unchanged
2026-07-17 12:04:34 +04:00
sindoring ef782d1f2b docs(M03): accept M2 mesh load pipeline state
Work-Package: M03-RND-M2-MESH-LOAD-PIPELINE-001
Agent: sindo-main-codex
Tests: post-merge pipeline, classifier, materials, shutdown, facade, internal-access, manifest, documentation and coordination gates pass
Fidelity: preserves request polling/completion order, statuses, metrics and shutdown drain; asset-backed visual parity remains unclaimed
2026-07-17 11:09:20 +04:00
sindoring 83b3618c50 merge M03 M2 mesh load pipeline state 2026-07-17 10:24:50 +04:00
sindoring ccbb27a9fd docs(M03): hand off M2 mesh load pipeline state
Work-Package: M03-RND-M2-MESH-LOAD-PIPELINE-001
Agent: sindo-main-codex
Tests: 34 headless regressions, documentation/coordination gates and checkpoint dry-run pass
Fidelity: lifecycle extraction only; asset-backed visual parity remains unclaimed
2026-07-17 10:24:41 +04:00
sindoring 82df7a555d refactor(M03): extract M2 mesh load pipeline state
Work-Package: M03-RND-M2-MESH-LOAD-PIPELINE-001
Agent: sindo-main-codex
Tests: 34 headless contract regressions pass; documentation and coordination gates pass; checkpoint dry-run 7/7
Fidelity: preserves request insertion polling order, terminal completion FIFO/statuses, metrics and clear/shutdown behavior; no visual parity claim
2026-07-17 10:23:52 +04:00
sindoring 5f6287905c coord(M03): claim M2 mesh load pipeline state 2026-07-17 10:15:17 +04:00
sindoring 8ec13d6018 docs(M03): accept M2 runtime rebuild classifier 2026-07-17 10:02:55 +04:00
sindoring 38f831ea2f merge M03 M2 runtime rebuild classifier 2026-07-17 10:01:54 +04:00
sindoring 0544cf22df docs(M03): hand off M2 runtime rebuild classifier 2026-07-17 10:01:47 +04:00
sindoring 19ca8f48e8 refactor(M03): extract M2 runtime rebuild classifier 2026-07-17 10:01:29 +04:00
sindoring 324e64dd6e coord(M03): claim M2 runtime rebuild classifier 2026-07-17 09:56:16 +04:00
150 changed files with 17769 additions and 968 deletions
+322 -10
View File
@@ -32,9 +32,29 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/wmo/wmo_render_build_step_planner.gd` - mesh-first lightweight WMO group operation and cursor planning without Nodes or Resources.
- `src/render/wmo/wmo_render_build_queue.gd` / `wmo_render_build_job.gd` - typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction.
- `src/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.
- `src/render/wmo/wmo_render_resource_finalizer.gd` - lightweight WMO terminal polling, script/format validation and Resource/missing publication.
- `src/render/wmo/wmo_scene_resource_finalizer.gd` - cached WMO terminal polling, PackedScene probe validation/lifetime and scene/missing publication.
- `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership.
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
- `src/render/m2/m2_animated_scene_finalizer.gd` - animated PackedScene candidate ownership, historical material repair and AnimationPlayer validation.
- `src/render/m2/m2_animation_playback_controller.gd` - deterministic per-instance phase, default animation selection and native/imported playback mutation.
- `src/render/m2/m2_animated_instance_materializer.gd` - main-thread animated instance duplication, render settings, playback startup and non-empty batch attachment.
- `src/render/m2/m2_static_batch_materializer.gd` - main-thread static M2 MultiMesh construction, render settings and attachment.
- `src/render/m2/m2_build_queue.gd` / `m2_build_job.gd` - typed pending M2 jobs, FIFO/stale tile keys, grouped-transform references and progress cursors without engine destruction.
- `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision.
- `src/render/m2/m2_build_resource_snapshot.gd` - typed per-step animated/static resource observations without engine destruction.
- `src/render/m2/m2_static_build_resource_observer.gd` - static Mesh cache lookup, threaded request selection and missing transition.
- `src/render/m2/m2_cached_animation_resource_observer.gd` - cached animated GLB eligibility, threaded request admission and snapshot production.
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
- `src/render/m2/m2_animation_resource_finalizer.gd` - cached animated M2 terminal status polling, Resource load, scene finalization and prototype/static-only outcome.
- `src/render/m2/m2_mesh_resource_finalizer.gd` - static M2 terminal status polling, Mesh extraction/preparation and cache/missing outcome.
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
- `src/render/m2/m2_runtime_mesh_finalizer.gd` - refresh version, classifier lifetime, M2Builder rebuild and original-Mesh fallback.
- `src/render/m2/m2_prototype_cache_state.gd` - shutdown-lifetime static/animated prototype Nodes and missing/static-only path outcomes.
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
@@ -313,7 +333,7 @@ Native M2 animation first pass for composite doodads:
- `M2NativeAnimator` evaluates the selected Stand sequence and applies WoW-style bone matrices:
`T(pivot + translation) * R * S * T(-pivot) * parent`.
- Vertex influences are resolved through `.skin` local bone indices and the M2 `boneCombos` palette, matching the original section/batch renderer model used by WoW/whoa.
- `StreamingWorldLoader` routes only `gryphonroost` through this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh.
- `M2NativeAnimationResourceObserver` routes only `gryphonroost` through this native path; simple critters still use the existing GLB allowlist and most M2 world props stay on static MultiMesh.
- Current implementation rebuilds the animated `ArrayMesh` on CPU. This is correct enough for the problematic rare doodads and gives us the same data layout that can later move to shader/GPU skinning.
Причина: композитные doodad вроде `GryphonRoost` снова ломают визуал при GLB-анимации. До M2-native renderer все world placement M2 должны оставаться статическими. GLB-анимация оставлена только как вручную включаемый debug experiment через allowlist.
@@ -803,7 +823,12 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Billboard metadata is resolved per drawn vertex through the skin section bone palette and vertex bone indices. This avoids treating an entire mixed surface as billboard just because the palette contains a billboard bone.
- `M2Builder` stores billboard pivot/mode in `Mesh.ARRAY_CUSTOM0` as RGBA float data. `CUSTOM0.xyz` is the Godot-space bone pivot; `CUSTOM0.w` is the billboard mode: spherical, lock X, lock Y, or lock Z.
- `WowM2Material` applies camera-facing billboarding in the vertex shader for marked vertices. This works with grouped ADT M2 `MultiMesh` batches because the shader uses each instance transform.
- Runtime M2 cache refresh now has `M2_MATERIAL_REFRESH_VERSION`. Old cached `.tscn/.glb` meshes are replaced in memory from raw `.m2` only when the raw model actually needs custom billboard attributes or UV-rotation data. Do not rebuild every stale cached M2 during streaming: creating fresh materials for ordinary props can exhaust the D3D12 resource descriptor heap.
- Runtime M2 cache refresh uses material refresh version `2` (now
`M2RuntimeMeshFinalizer.MATERIAL_REFRESH_VERSION`). Old cached `.tscn/.glb`
meshes are replaced in memory from raw `.m2` only when the raw model actually
needs custom billboard attributes or UV-rotation data. Do not rebuild every
stale cached M2 during streaming: creating fresh materials for ordinary props
can exhaust the D3D12 resource descriptor heap.
- Verified with `ElwynnGrass1.m2`: native data exposes a billboard batch, the built mesh contains `CUSTOM0`, and the M2 shader compiles with the billboard uniform enabled.
- Remaining parity work: many Elwynn tree canopy M2s do not carry billboard bone flags, so their flat-looking leaves are a separate material/geometry parity issue, not this bone-billboard path.
@@ -813,7 +838,8 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Texture transform dictionaries now expose static UV rotation matrices and approximate Z-rotation speeds, in addition to existing translation, translation speed and scale.
- `WowM2Material` applies UV rotation and scale around WoW's texture center `(0.5, 0.5)`, then applies translation/scroll. This is closer to the client path shown by `CM2Model::AnimateTextureTransformsMT`.
- All four M2 texture stages receive UV rotation and rotation-speed uniforms, matching the existing four-stage texture binding path.
- `M2_MATERIAL_REFRESH_VERSION` and `M2Builder.MATERIAL_FORMAT_VERSION` are bumped to `2`, so cached M2 meshes are rebuilt in memory when raw `.m2` data is available.
- Runtime M2 material refresh and `M2Builder.MATERIAL_FORMAT_VERSION` are `2`,
so stale cached M2 meshes can be rebuilt in memory when raw `.m2` data is available.
- Verified with `Spells/ArcaneForceShield_Blue.m2`: native data exposes a non-identity UV rotation, the built material receives the stage rotation uniform, and the shader compiles.
## 2026-07-08 Renderer Material Regression Smoke
@@ -1034,11 +1060,250 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
remaining-count capping and next-offset/group-completion calculation.
- Existing animated/static limits still clamp to at least one; resource readiness,
queue rotation, serial progression and `M2_BUILD` permit consumption are unchanged.
- The loader retains build jobs/queues, tile cancellation, animation/mesh caches,
retries and all Node/MultiMesh/Mesh/material/RID finalization.
- `M2BuildQueue` now retains typed pending jobs, FIFO/stale keys and cursors.
The loader retains tile cancellation, resource readiness/retries and all
Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, quality profiles, batching output and visible rules are unchanged.
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
## 2026-07-18 M2 Build Dispatch Planner Extraction
- `M2BuildDispatchPlanner` now owns the pure action priority between pending
animation, animated materialization, pending/ready static Mesh and terminally
missing-model advancement.
- Pending animation still wins before batch planning. Unresolved static Meshes
still rotate and consume one `M2_BUILD` permit without advancing progress.
- Empty batches advance without serial change; animated, static and terminally
missing positive batches retain the historical serial increment.
- Resource lookup/request order, queue/cursor adoption, materialization, permits,
Node lifetime, cache formats, profiles and visible rules remain unchanged.
## 2026-07-18 M2 Build Resource Snapshot Extraction
- `M2BuildResourceSnapshot` now carries one build-step normalized path, optional
animated prototype, pending-animation state, optional static Mesh and terminal
missing-model state through typed accessors.
- Animation observation is captured first. Static observation is adopted only
for a positive non-animated batch after animation is no longer pending.
- The dispatch planner consumes the snapshot; the loader borrows the selected
prototype/Mesh for the existing materializer calls.
- ResourceLoader/cache requests, permit/cursor transitions, engine lifetime,
cache formats, profiles and visible rules remain unchanged.
## 2026-07-18 M2 Static Build Resource Observer Extraction
- `M2StaticBuildResourceObserver` now owns prepared-Mesh lookup, existing-request
detection, `.tscn`-before-`.glb` candidate selection, pivot-prefix GLB rejection,
threaded request admission and terminal missing transition for build jobs.
- The observer fills `M2BuildResourceSnapshot`; the loader retains animation
observation, finalize drains, materialization, permits and engine lifetime.
- Candidate order, request errors, cache formats, profiles and visuals are unchanged.
## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
stale cached M2 mesh may need an in-memory rebuild from raw data.
- Existing triggers are unchanged: any billboard flag/positive billboard vertex
count, or a referenced non-identity UV rotation/absolute rotation speed above
`0.000001` across at most four texture stages.
- Invalid batch variants and combo/transform indices remain skipped. The first
decision for a normalized path remains cached until the same two historical
map/shutdown clear sites.
- `StreamingWorldLoader` still requests raw M2 data and owns ResourceLoader,
returned-Mesh adoption and finalization budgets. Refresh-version checks,
M2Builder rebuild and fallback now belong to `M2RuntimeMeshFinalizer`.
- No cache version, shader, material, geometry or visible rule changed. Synthetic
predicate timing is not asset-backed descriptor-pressure/p95/p99 evidence.
## 2026-07-17 M2 Mesh Load Pipeline State Extraction
- `M2MeshLoadPipelineState` now owns static M2 successful threaded-load request
records and the FIFO populated when ResourceLoader polling reaches loaded or
failed terminal status.
- Pending polling snapshots preserve Dictionary insertion order; terminal FIFO
order follows completion transition order. Existing `m2_mesh` metrics still
count pending plus finalize records at all three sites.
- Shutdown still drains every pending ResourceLoader path before state clear;
map reset and shutdown retain the same two clear sites.
- `StreamingWorldLoader` still selects `.tscn`/`.glb` paths, excludes animated
pivot-prefix GLBs, performs all ResourceLoader calls, consumes
`M2_MESH_FINALIZE`, extracts/prepares Meshes and updates Mesh/missing caches.
- Cache formats, material refresh, profiles and visible output are unchanged.
Synthetic state timing is not asset-backed I/O, leak or p95/p99 evidence.
## 2026-07-17 M2 Animation Load Pipeline State Extraction
- `M2AnimationLoadPipelineState` now owns animated M2 successful threaded-load
request records and the FIFO populated at loaded/failed terminal transitions.
- Pending snapshots preserve insertion order, while finalization follows completion
order. All three historical `m2_animation` metrics still count both stages.
- Shutdown still drains pending ResourceLoader paths before clear; map reset and
shutdown preserve the same two clear sites.
- At this extraction stage, `StreamingWorldLoader` retained GLB eligibility/cache selection, every
ResourceLoader call, `M2_ANIMATION_FINALIZE` permits, scene instantiation,
material repair and prototype/static-fallback decisions.
- Cache formats, animation behavior and visible output are unchanged. Synthetic
timing is not asset-backed I/O, leak, animation-fidelity or p95/p99 evidence.
## 2026-07-18 M2 Cached Animation Resource Observer Extraction
- `M2CachedAnimationResourceObserver` now owns cached animated-prototype lookup,
allow/deny matching, historical `.glb` candidate selection, GLB animation/
primitive/schema safety validation, request admission and static-only fallback.
- The observer produces `M2BuildResourceSnapshot` with the exact borrowed cached
prototype or pending state and retains/frees no engine object.
- Native GryphonRoost raw-data build/debug logging belongs to the sibling native
observer; terminal polling, finalization, permits and SceneTree mutation remain
loader-owned.
- Defaults, path order, accepted empty/`pivot_prefix_v1` schemas, cache format,
metrics and visible behavior are unchanged. Generated GLB metadata fixtures
are not private asset, leak, p95/p99 or original-client animation evidence.
## 2026-07-18 M2 Animation Resource Finalizer Extraction
- `M2AnimationResourceFinalizer` now owns cached animated M2 terminal status
polling, completion-FIFO pops, terminal Resource retrieval, candidate
instantiation, repair/validation, prototype/static-only adoption and success log.
- The two-phase preparation/completion API preserves the prior ordering: the
loader resolves a material prototype only after a loaded PackedScene produces
a detached Node3D candidate. One scheduler permit still pops one record.
- `StreamingWorldLoader` retains the `M2_ANIMATION_FINALIZE` permit loop,
material-prototype lookup, build dispatch, materialization and SceneTree lifetime.
- No request order, status rule, cache format, profile or visible behavior changed.
Synthetic fixtures are not asset-backed animation/leak/p95/p99 evidence.
## 2026-07-18 M2 Mesh Resource Finalizer Extraction
- `M2MeshResourceFinalizer` now owns static M2 terminal status polling,
completion-FIFO pops, terminal Resource retrieval, first-Mesh extraction,
stale/current runtime preparation and Mesh/missing cache adoption.
- Pending paths are still polled in insertion order. One
`M2_MESH_FINALIZE` permit still pops at most one terminal record; cached Mesh
outcomes skip terminal retrieval exactly as before.
- Current Meshes retain exact identity and skip raw reads. Stale Meshes still
request raw data only when `M2RuntimeMeshFinalizer` requires it, preserving
refresh-version, rebuild and original-Mesh fallback rules.
- `StreamingWorldLoader` retains request admission, scheduler permits,
composition, static materialization and shutdown drain ordering. Cache paths,
profiles and visible behavior are unchanged; synthetic fixtures are not
asset-backed visual/leak/p95/p99 evidence.
## 2026-07-18 M2 Native Animation Resource Observer Extraction
- `M2NativeAnimationResourceObserver` now owns the exact case-insensitive
`gryphonroost` candidate rule, animated prototype/static-only cache checks,
synchronous raw animated read, native builder call, adoption and success log.
- The native attempt remains first. Empty raw data/surfaces and null/childless
builds retain the same static-only fallback; accepted Nodes remain owned by
`M2PrototypeCacheState` until final shutdown.
- `StreamingWorldLoader` retains observer order, typed snapshot construction,
cached-GLB fallback, terminal ResourceLoader polling/finalize, permits,
materialization and SceneTree lifetime.
- The historical unfreed childless builder result is deliberately preserved and
documented as a leak risk. No parser, cache, profile or visible rule changed;
synthetic fixtures are not asset-backed animation or performance evidence.
## 2026-07-18 M2 Animated Scene Finalizer Extraction
- `M2AnimatedSceneFinalizer` now owns terminal animated PackedScene candidate
instantiation, depth-first Mesh/AnimationPlayer traversal, static-prototype
material override mapping and rejected detached-root destruction.
- Material repair retains source/target depth-first ordering, source-index clamp,
per-surface override priority and first discovered material fallback.
- A candidate still requires at least one AnimationPlayer. Accepted exact Node3D
and player count return to the loader; invalid candidates are freed, including
the previously unhandled non-Node3D imported-root case.
- `StreamingWorldLoader` retains ResourceLoader I/O/statuses, finalize permits,
material-prototype selection/loading, prototype adoption/static fallback,
animation playback policy and diagnostic logging.
- Cache formats, profiles and visible rules are unchanged. Synthetic scene and
material fixtures are not asset-backed animation-fidelity or p95/p99 evidence.
## 2026-07-18 M2 Animation Playback Controller Extraction
- `M2AnimationPlaybackController` now owns the stable path/index phase formula,
ordinary/fish/bird animation-name priority, substring/first-name fallbacks and
AnimationPlayer linear-loop/play/seek mutation.
- Exact-script native animators retain depth-first pairing, the same five copied
fields, optional `prepare_runtime`, phase assignment and debug-state sampling.
- Native debug records are detached and sampled only when `debug_streaming` is
enabled; `StreamingWorldLoader` retains path normalization and exact log text.
- Loader still duplicates/transforms/attaches instances, applies visibility,
shadows and Editor ownership, owns build cursors/permits and obtains player inventory.
- Cache formats, playback priorities, phase behavior and visible rules are unchanged.
Synthetic timing is not asset-backed animation fidelity or p95/p99 evidence.
## 2026-07-17 M2 Mesh Resource Cache State Extraction
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
static M2 Mesh resources. Lookup returns the exact retained reference and a
later store for one path keeps the historical replacement behavior.
- The cache retains map-refresh lifetime and clears only at the existing final
runtime-cache shutdown site after asynchronous work is drained.
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, cache adoption
decisions, permits and MultiMesh materialization. Prototype/negative state is
now a sibling cache service.
- Cache formats, refresh metadata, profiles and visible output are unchanged.
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
## 2026-07-17 M2 Mesh Resource Extractor
- `M2MeshResourceExtractor` now selects the first Mesh from direct Mesh
Resources, temporary PackedScene instances and existing prototype/rebuild
Node subtrees using the prior depth-first child order.
- PackedScene extraction still instantiates exactly once and synchronously frees
the temporary root before returning the borrowed Mesh reference.
- `StreamingWorldLoader` still owns ResourceLoader I/O and raw M2 consumers,
material refresh/rebuild, cache/missing adoption, permits and MultiMesh builds.
- Cache formats, traversal rules, profiles and visible output are unchanged.
Synthetic traversal timing is not asset-backed leak or p95/p99 evidence.
## 2026-07-17 M2 Runtime Mesh Finalizer
- `M2RuntimeMeshFinalizer` now owns material refresh version `2`, stale-Mesh
rebuild classification, M2Builder rebuild and original-Mesh fallback.
- Current Meshes still skip raw `.m2` loading. `M2RawModelRepository` owns
FileAccess/ClassDB M2Loader I/O and supplies raw data through
`M2MeshResourceFinalizer` only when the runtime finalizer reports a stale Mesh;
both historical clear sites persist.
- Billboard/UV-rotation predicates, rebuild extraction, metadata key and failure
fallback are unchanged. Mesh resource finalization owns cache adoption;
permits and MultiMesh materialization remain loader-owned, while negative
outcomes belong to prototype state.
- Synthetic triangle rebuild/fallback timing is not asset-backed material,
descriptor-pressure/leak or p95/p99 evidence.
## 2026-07-17 M2 Raw Model Repository
- `M2RawModelRepository` now owns the repeated extracted-file and optional
native `M2Loader` boundary for static `load_m2` and animated
`load_m2_animated` raw Dictionaries.
- `StreamingWorldLoader` delegates stale-Mesh refresh and static reads directly;
`M2NativeAnimationResourceObserver` delegates native animated reads. The loader
retains normalization, `.tscn/.glb` fallback order, permits and Node/Mesh use;
prototype/negative state is isolated in `M2PrototypeCacheState`.
- The repository retains no path, native object or parsed data. Empty paths,
absent files/classes/methods and invalid results produce the same empty-value
fallback contract; path join/globalization and native method names are exact.
- No cache format, parser, material, geometry, animation or visible rule changed.
Synthetic missing-input timing is not asset-backed I/O/p95/p99 or fidelity evidence.
## 2026-07-17 M2 Prototype Cache State
- `M2PrototypeCacheState` now owns the exact static and animated prototype Node
references plus missing-model and static-animation negative path outcomes.
- Sync static, native animated, threaded GLB animation, Mesh finalize and
material-prototype paths delegate lookup/adoption/negative transitions to the
service. First valid prototype remains canonical for each normalized path.
- Map refresh still retains all four states. Final shutdown still occurs after
asynchronous drain and now calls one idempotent release operation that frees
detached prototypes, queues defensive in-tree Nodes and clears path outcomes.
- `StreamingWorldLoader` retains normalization, ResourceLoader/native I/O,
builders, animation policy, fallback order, permits and materialization.
- No cache format, visible rule or profile changed. Synthetic ownership/timing
evidence is not an asset-backed traversal/leak/p95/p99 or fidelity claim.
## 2026-07-17 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
@@ -1055,9 +1320,10 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- `WmoRenderResourceCacheState` now owns validated lightweight-WMO render
Resources, negative entries and normalized-path to pending-cache-path records.
- `StreamingWorldLoader` still constructs cache paths, calls `ResourceLoader`,
polls requests and validates `WMOStreamingResource` script identity plus
`FORMAT_VERSION` before completing cache state.
- `StreamingWorldLoader` still constructs cache paths and starts requests.
`WmoRenderResourceFinalizer` polls terminal requests and validates exact
`WMOStreamingResource` script identity plus `FORMAT_VERSION` before completing
cache state.
- Map reset and orderly request draining clear pending/negative state while
retaining accepted Resources; final runtime cache release clears all state.
- Missing render-cache files still are not negatively cached, preserving retry
@@ -1065,13 +1331,28 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Asset-backed corrupt-cache, traversal/leak p95/p99 and paired fidelity evidence
remain pending.
## 2026-07-18 WMO Render Resource Finalizer Extraction
- `WmoRenderResourceFinalizer` now owns lightweight WMO render-cache terminal
status polling, loaded Resource retrieval, exact script/current-format
validation and Resource/missing publication.
- Detached pending snapshots retain Dictionary insertion order. Non-terminal
requests remain pending; failed, null, wrong-script and stale-format outcomes
retain the historical negative-cache transition.
- Accepted current-or-newer Resources keep exact identity. Loader retains cache
path selection, request admission, fallback/build orchestration, Node lifetime
and shutdown drain order.
- Cache format, profiles and visible output are unchanged. Synthetic fixtures
are not serialized private assets, leak, p95/p99 or visual-fidelity evidence.
## 2026-07-17 WMO Scene Resource Cache State Extraction
- `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes,
negative entries and normalized-path to pending-`.tscn` records.
- `StreamingWorldLoader` still checks file existence and
`wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation
probe, checks WMOBuilder cache metadata and frees the probe before adoption.
`wmo_max_runtime_scene_mb` and starts requests. `WmoSceneResourceFinalizer`
owns terminal ResourceLoader I/O, validation-probe instantiation, WMOBuilder
metadata validation and probe release before adoption.
- Missing files, oversize scenes, request errors, load failures and stale scenes
retain their prior negative-cache and live-prototype fallback behavior.
- Map reset clears pending/negative state while retaining accepted scenes; final
@@ -1079,6 +1360,21 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity
evidence remain pending.
## 2026-07-18 WMO Scene Resource Finalizer Extraction
- `WmoSceneResourceFinalizer` now owns cached WMO `.tscn` terminal status
polling, loaded Resource/PackedScene validation, call-local probe lifetime and
exact scene/missing publication.
- Pending snapshots preserve Dictionary insertion order. Non-terminal requests
remain pending; failed, null, wrong-type, wrong-root and stale outcomes keep
the existing negative-cache transition.
- Current scenes retain exact PackedScene identity. Accepted and rejected probes
are freed before return; the rejected non-Node3D root now also releases its
temporary Node, closing a leak without changing fallback or visible output.
- Loader retains file/size admission, oversize log, request start, live fallback,
placed Node materialization and shutdown order. Synthetic fixtures are not
serialized private assets, long leak, p95/p99 or visual-fidelity evidence.
## 2026-07-17 ADT Water Load Pipeline State Extraction
- `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication,
@@ -1153,6 +1449,22 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- ADT parsing, quality tasks/results, tile state, cache format versions, material/
Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
## 2026-07-18 WMO Runtime Mesh Finalizer Extraction
- `WmoRuntimeMeshFinalizer` now owns cached WMO Mesh material refresh version
`10`, in-place ArrayMesh surface iteration and WMOBuilder material-definition
reconstruction.
- `StreamingWorldLoader` retains cached-scene traversal, lightweight build-job
traversal, Node/MultiMesh attachment, permits and lifetime, and delegates each
borrowed Mesh to the service.
- The historical metadata keys, compact texture0/texture1/texture2 ordering,
flags/shader/blend values, cached shader colors and exact Mesh identity are
unchanged. Null/unmarked surfaces and null builder results retain their prior
Material.
- Asset-free verification covers `27` identity/version/type/material/source
cases and 1,000 current-Mesh calls. This is orchestration extraction, not new
build-12340 material or visual parity evidence.
## Practical Rule For Future Work
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
@@ -0,0 +1,104 @@
# M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-animated-instance-materializer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-animated-instance-materializer`
## Outcome
Extract main-thread animated M2 instance duplication, render-property application,
playback startup and batch attachment from `StreamingWorldLoader`.
## Non-goals
- Change build-job cursors, budgets, prototype selection or animation eligibility.
- Change animation selection, phase, native animator behavior or diagnostic text.
- Own Editor scene persistence, static MultiMesh materialization or tile cleanup.
- Change visible transforms, visibility ranges, shadows or placement ordering.
## Paths
- Exclusive: `src/render/m2/m2_animated_instance_materializer.gd`,
`src/tools/verify_m2_animated_instance_materializer.gd`,
`docs/modules/m2-animated-instance-materializer.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 animation
playback/finalizer/build/facade/internal-access/manifest verifiers and specs,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- Duplicate flags, names, transforms and input order remain unchanged.
- Visibility end/margin and shadow mode are applied recursively before attachment.
- Playback data copy/start delegates to the accepted playback controller contract.
- The batch is attached only when at least one duplicate succeeds.
- Loader retains diagnostic formatting/path normalization and Editor ownership.
## Dependencies
- Requires: accepted playback package on master `5b2d9f2`
- Blocks: further animated M2 build orchestration extraction
- External state: Godot Node duplication and SceneTree attachment remain main-thread APIs
## Verification
- Commands: dedicated duplication/order/render/playback/empty/source/timing verifier;
M2 animated pipeline/finalizer/playback/prototype/build/shutdown/material/facade/
internal-access/manifest regressions; docs/coordination/dry-run gates
- Fixtures: synthetic prototype trees with geometry, AnimationPlayer and native animator
- Fidelity evidence: exact transforms, flags, render settings, playback delegation and order
- Performance budget: 1,000 synthetic instances under 1 second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent playback/finalizer/build/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2AnimatedInstanceMaterializer`
- Simplest solution: synchronous main-thread RefCounted returning batch diagnostics
- Rejected complexity: signals, async queue, callbacks and generic scene factory
- Unavoidable complexity: imported AnimationPlayer and native animator paths coexist
- Measured optimization evidence: bounded synthetic batch materialization
## Status
- State: ready for integration
- Done: materializer, loader adapter, verifier and required documentation
- Next: integrator merge and post-merge acceptance
- Blocked by:
## Handoff
- Commit: `456f3e2`
- Results: materializer passes 12 duplication/order/render/playback/diagnostic/
source cases and 1,000 instances in 9.267 ms; all 50 autonomous headless
verifiers pass, while the proprietary ADT placement probe is unavailable
without `data/extracted`; checkpoint dry-run passes 7/7; documentation and
coordination gates pass; loader-private inventory remains 30.
- Remaining risks: private asset traversal/visual comparison, descriptor
pressure, leaks and p95/p99 remain unavailable; materialization is still
synchronous main-thread work and no original-client comparison is claimed.
- Documentation updated: inline API; `m2-animated-instance-materializer.md`
with data-flow, lifecycle, sequence and dependency diagrams; playback,
finalizer, build, world-renderer specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001:456f3e2 -->
## Integration
- Merge commit: `c251985`
- Post-merge results: materializer `cases=12 instances=1000 elapsed_ms=13.719`;
playback/finalizer, shutdown, facade, internal-access `30`, checkpoint dry-run
`7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001:c251985 -->
@@ -0,0 +1,110 @@
# M03-RND-M2-ANIMATED-SCENE-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATED-SCENE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-animated-scene-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-animated-scene-finalizer`
## Outcome
Extract animated M2 PackedScene candidate instantiation, material override repair
and AnimationPlayer validation from `StreamingWorldLoader` into an explicit
main-thread finalizer.
## Non-goals
- Call ResourceLoader or interpret threaded-load statuses.
- Choose animation eligibility, cache paths, material prototypes or permits.
- Adopt/cache prototype Nodes or change static/native animation paths.
- Change visible materials, animation selection, cache formats or profiles.
## Paths
- Exclusive: `src/render/m2/m2_animated_scene_finalizer.gd`,
`src/tools/verify_m2_animated_scene_finalizer.gd`,
`docs/modules/m2-animated-scene-finalizer.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
animation pipeline/prototype/material/build/shutdown/facade/internal-access/
manifest verifiers and module specs, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- Only a PackedScene whose instantiated root is Node3D becomes a candidate;
unsupported or invalid roots return null and any created invalid root is freed.
- Material repair preserves depth-first MeshInstance3D order, source-index
clamping, per-surface override priority and first-material fallback.
- Finalization accepts a candidate only when at least one descendant
AnimationPlayer exists; rejected detached candidates are freed synchronously.
- Accepted result transfers the exact Node3D and player count to the loader.
- Loader retains ResourceLoader, material-prototype acquisition, permit,
prototype adoption/static fallback and diagnostic-log decisions.
## Dependencies
- Requires: accepted animation load pipeline package on master `c20bc62`
- Blocks: further animated M2 build/materialization decomposition
- External state: imported GLB/PackedScene shape remains engine-owned input
## Verification
- Commands: dedicated invalid/ownership/traversal/material/finalization/source/
timing verifier; animation pipeline/prototype/shutdown/material/M2 build/facade/
internal-access/manifest and adjacent M2 regressions; docs/coordination/dry-run gates
- Fixtures: synthetic PackedScenes, nested AnimationPlayers and ArrayMesh materials
- Fidelity evidence: exact material mapping and accepted/rejected Node transitions
- Performance budget: 10,000 depth-first candidate queries under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent animation/prototype/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2AnimatedSceneFinalizer`
- Simplest solution: one stateless main-thread RefCounted over engine scene types
- Rejected complexity: callbacks, generic scene pipeline, signals or cache ownership
- Unavoidable complexity: material repair occurs between candidate creation and acceptance
- Measured optimization evidence: bounded synthetic subtree traversal
## Status
- State: accepted
- Done: finalizer, loader adapters, lifecycle/material verifier and required documentation
- Next: continue animated M2 playback/instance materialization extraction
- Blocked by:
## Handoff
- Commit: `1b450dc`
- Results: animated scene finalizer passes 13 invalid/type/lifetime/traversal/
material/transfer/source cases and 10,000 depth-eight queries in 40.126 ms;
all 48 autonomous headless verifiers pass, while the proprietary ADT placement
probe is unavailable without `data/extracted`; checkpoint dry-run passes 7/7;
documentation and coordination gates pass; loader-private inventory remains 30.
- Remaining risks: proprietary GLB traversal, material/animation comparison,
descriptor pressure, leaks and p95/p99 remain unavailable; no paired original-
client visual comparison is claimed. Positional material mapping is unchanged.
- Documentation updated: inline API; `m2-animated-scene-finalizer.md` with
data-flow, lifecycle, sequence and dependency diagrams; animation pipeline,
prototype/world-renderer specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATED-SCENE-FINALIZER-001:1b450dc -->
## Integration
- Merge commit: `e7a7c67`
- Post-merge results: finalizer `cases=13 iterations=10000
elapsed_ms=40.314`; animation pipeline, prototype state, shutdown, materials,
facade, internal-access `30`, manifest `7/7`, documentation and coordination passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-ANIMATED-SCENE-FINALIZER-001:e7a7c67 -->
@@ -0,0 +1,112 @@
# M03-RND-M2-ANIMATION-LOAD-PIPELINE-001 — M2 animation load pipeline state
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATION-LOAD-PIPELINE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-animation-load-pipeline`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract threaded animated-M2 pending request records and completion-order
finalize FIFO from `StreamingWorldLoader` into an explicit state service.
## Non-goals
- Call ResourceLoader, select/validate GLB paths or interpret status values.
- Instantiate/repair/cache animated Nodes or own prototype outcomes.
- Change animation allow/deny/candidate policy, permits or visible behavior.
- Add cancellation, persistence, retries, dependencies or cache-format changes.
## Paths
- Exclusive: `src/render/m2/m2_animation_load_pipeline_state.gd`,
`src/tools/verify_m2_animation_load_pipeline_state.gd`,
`docs/modules/m2-animation-load-pipeline-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
prototype/shutdown/material/build/facade/internal-access/manifest verifiers
and module specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
`RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- One non-empty normalized path has at most one pending cache path.
- Pending snapshots preserve Dictionary insertion order and detach records.
- Completing a known request moves one copied record with opaque status into a
completion-order FIFO; unknown/duplicate completion is rejected.
- Defensive discard removes pending state without creating a finalize record.
- Total work remains pending plus finalize count at all three historical metrics.
- Map reset and shutdown retain the same two clear sites; shutdown still drains
ResourceLoader paths before clear.
## Dependencies
- Requires: accepted prototype cache package on master `20a64b5`
- Blocks: animated M2 finalization/materialization service extraction
- External state: ResourceLoader request statuses remain loader-owned
## Verification
- Commands: dedicated validation/dedupe/order/transition/discard/snapshot/clear/
source/timing verifier; prototype/shutdown/material/M2 build/facade/internal-
access/manifest and adjacent M2 regressions; docs/coordination/dry-run gates
- Fixtures: synthetic normalized/cache paths and opaque integer statuses
- Fidelity evidence: request insertion, terminal FIFO, metrics and clear transitions
- Performance budget: 100 cycles over 256 request completions under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent M2/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2AnimationLoadPipelineState`
- Simplest solution: one main-thread RefCounted with pending map and finalize FIFO
- Rejected complexity: generic Resource pipeline, signals, callbacks, typed
ResourceLoader enum wrapper, Node ownership or async abstraction
- Unavoidable complexity: pending insertion order and terminal completion order differ
- Measured optimization evidence: bounded synthetic transition loop
## Status
- State: accepted
- Done: state, loader adapters, lifecycle verifier and required documentation
- Next: continue animated M2 finalization/materialization extraction
- Blocked by:
## Handoff
- Commit: `b8e0f11`
- Results: animation pipeline passes 11 validation/dedupe/order/transition/FIFO/
discard/snapshot/source cases and 100-by-256 cycles in 69.798 ms; all 47
autonomous headless verifiers pass, while the proprietary ADT placement probe
is unavailable without `data/extracted`; checkpoint dry-run passes 7/7;
documentation and coordination gates pass; loader-private inventory decreases
from 31 to 30.
- Remaining risks: successful animated-GLB traversal, descriptor pressure,
leaks, animation fidelity and p95/p99 require proprietary data; no paired
original-client visual comparison is claimed.
- Documentation updated: inline API; `m2-animation-load-pipeline-state.md` with
data-flow, lifecycle, sequence and dependency diagrams; prototype/world-renderer
specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATION-LOAD-PIPELINE-001:b8e0f11 -->
## Integration
- Merge commit: `3dccd3e`
- Post-merge results: animation pipeline `cases=11 iterations=100
elapsed_ms=70.309`; prototype state, shutdown, materials, facade,
internal-access `30`, manifest `7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-ANIMATION-LOAD-PIPELINE-001:3dccd3e -->
@@ -0,0 +1,110 @@
# M03-RND-M2-ANIMATION-PLAYBACK-001
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATION-PLAYBACK-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-animation-playback`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-animation-playback`
## Outcome
Extract deterministic animated M2 playback selection/phase, AnimationPlayer
mutation and native animator data/start behavior from `StreamingWorldLoader`.
## Non-goals
- Duplicate or attach animated instances/batch roots.
- Own visibility, shadow, Editor owner or build-job cursor rules.
- Load/finalize/cache scenes or change animation eligibility.
- Change candidate priority, phase formula, logging text or visible playback.
## Paths
- Exclusive: `src/render/m2/m2_animation_playback_controller.gd`,
`src/tools/verify_m2_animation_playback_controller.gd`,
`docs/modules/m2-animation-playback-controller.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 animated
finalizer/build/prototype/material/facade/internal-access/manifest verifiers and
specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- Phase remains `abs(hash("path:index")) % 1000 / 1000.0`.
- Exact animation-name priority remains ordinary Stand/Idle/Run/Walk, fish
Run/Walk/Swim/Stand/Idle/Death, birds Run/Walk/Swim/Stand/Idle, then
case-insensitive substring fallback and first-name fallback.
- Every AnimationPlayer animation becomes linear-looping before selected play;
positive-length selection seeks to deterministic phase.
- Native animators match exact script identity in depth-first order, copy the
same five fields, call prepare_runtime when available and set phase.
- Runtime debug state is sampled only when existing debug mode requests it;
loader retains final log formatting/path normalization.
## Dependencies
- Requires: accepted animated scene finalizer package on master `c0a7bfb`
- Blocks: animated M2 instance materializer extraction
- External state: AnimationPlayer and M2NativeAnimator APIs remain engine/scene contracts
## Verification
- Commands: dedicated phase/selection/loop/play/seek/native-copy/start/diagnostic/
source/timing verifier; animated finalizer/pipeline/prototype/build/shutdown/
material/facade/internal-access/manifest regressions; docs/coordination/dry-run gates
- Fixtures: synthetic AnimationPlayers and real M2NativeAnimator nodes
- Fidelity evidence: exact priority, phase and mutation/reference contracts
- Performance budget: 20,000 selection/phase operations under 1 second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent finalizer/build/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2AnimationPlaybackController`
- Simplest solution: stateless main-thread RefCounted over supplied Nodes/players
- Rejected complexity: signals, playback state machine, generic animation framework
- Unavoidable complexity: native and imported AnimationPlayer paths coexist
- Measured optimization evidence: bounded selection/phase loop
## Status
- State: accepted
- Done: playback controller, loader adapters, verifier and required documentation
- Next: extract animated M2 instance/batch materialization
- Blocked by:
## Handoff
- Commit: `9b73571`
- Results: playback controller passes 15 phase/selection/loop/play/seek/native-
copy/start/diagnostic/source cases and 20,000 phase-selection pairs in
23.415 ms; all 49 autonomous headless verifiers pass, while the proprietary
ADT placement probe is unavailable without `data/extracted`; checkpoint
dry-run passes 7/7; documentation and coordination gates pass; loader-private
inventory remains 30.
- Remaining risks: private animation-name/timing/native visual comparison,
descriptor pressure, leaks and p95/p99 remain unavailable; default selection
is still heuristic and no paired original-client comparison is claimed.
- Documentation updated: inline API; `m2-animation-playback-controller.md` with
data-flow, lifecycle, sequence and dependency diagrams; finalizer/build/world-
renderer specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATION-PLAYBACK-001:9b73571 -->
## Integration
- Merge commit: `c0fc191`
- Post-merge results: playback `cases=15 iterations=20000 elapsed_ms=23.008`;
animated finalizer/pipeline, prototype, build, shutdown, materials, facade,
internal-access `30`, manifest `7/7`, documentation and coordination passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-ANIMATION-PLAYBACK-001:c0fc191 -->
@@ -0,0 +1,84 @@
# M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-animation-resource-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-animation-resource-finalizer`
## Outcome
Move cached animated M2 ResourceLoader status polling, terminal Resource load,
candidate finalization, prototype/static-only adoption and success diagnostics
from the loader into a dedicated two-phase finalization service.
## Non-goals
- Change cached animation eligibility, request admission or native-first order.
- Move material-prototype lookup, permit ownership, playback or materialization.
- Change ResourceLoader status order, cache formats, profiles or visible output.
- Generalize static and animated ResourceLoader paths behind callbacks/frameworks.
## Paths
- Exclusive: `src/render/m2/m2_animation_resource_finalizer.gd`,
`src/tools/verify_m2_animation_resource_finalizer.gd`,
`docs/modules/m2-animation-resource-finalizer.md`, this claim
- Shared: loader, animation pipeline/scene finalizer/prototype cache/cached
observer/world-renderer specs and verifiers, module registry, `RENDER.md`, M03 Evidence
## Contracts and data
- Pending request snapshots are polled in existing Dictionary insertion order.
- Empty Resource paths are discarded and marked static-only immediately.
- Only LOADED/FAILED statuses move into completion-order finalize FIFO.
- One permit processes one popped terminal record, including skipped/stale records.
- Existing prototype/static-only state suppresses terminal Resource loading.
- LOADED Resource is instantiated before material-prototype lookup is requested.
- Candidate repair/finalize/adoption and exact `M2_ANIM_CACHE` debug fields remain.
- Rejected candidates are released by the existing animated scene finalizer.
## Dependencies
- Requires: accepted animation pipeline, scene finalizer, cached/native observers
- Blocks: remaining loader-owned M2 material-prototype lookup and action execution
## Verification
- Synthetic polling/status/FIFO/skip/load/candidate/repair/adoption/log/source and
timing contracts; adjacent M2/renderer gates; full headless suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: static M2 Resource terminal polling/finalize extraction
- Blocked by:
## Handoff
- Commit: `e7cd967`
- Results: finalizer `28` cases / `1,000` timing iterations; all `58/58`
autonomous headless regressions; facade/internal-access/shutdown/manifest;
documentation `45`; coordination; checkpoint dry-run `7/7`.
- Remaining risks: private asset animation/visual/leak/p95/p99 evidence is absent;
terminal Resource get/instantiation/repair stays synchronous; immediate prepared-
candidate completion is a composition precondition; material lookup stays loader-owned.
- Documentation updated: new finalizer API/I/O/data-flow/state/sequence/dependency/
ownership spec; pipeline, scene finalizer, cached observer, prototype cache,
world renderer, module registry, `RENDER.md` and M03 Evidence.
## Integration
- Merge: `06f6394`
- Post-merge: finalizer, pipeline, scene finalizer, cached observer, prototype
cache, snapshot, dispatch, shutdown, facade, internal-access `30`, manifest
`7/7`, documentation `45`, coordination and checkpoint dry-run `7/7` passed.
@@ -0,0 +1,97 @@
# M03-RND-M2-BUILD-DISPATCH-PLANNER-001
<!-- OPENWC_CLAIM:M03-RND-M2-BUILD-DISPATCH-PLANNER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-build-dispatch-planner`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-build-dispatch-planner`
## Outcome
Extract the pure M2 build-step dispatch decision from the loader while preserving
animation-wait priority, static-resource retry, missing-model advancement and
exact permit/cursor behavior.
## Non-goals
- Move ResourceLoader I/O, path lookup, cache state or retry ownership.
- Create/free Nodes, Meshes, MultiMeshes or animated instances in the planner.
- Change batch sizing, queue ordering, transforms, visuals or cache versions.
- Add spatial-cell batching or new diagnostics/profile settings.
## Paths
- Exclusive: `src/render/m2/m2_build_dispatch_planner.gd`,
`src/tools/verify_m2_build_dispatch_planner.gd`,
`docs/modules/m2-build-dispatch-planner.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 build/
queue/materializer/cache verifiers and specs, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- Pending animation request wins before empty/animated/static decisions.
- Positive batch with animated prototype selects animated materialization.
- Positive static batch selects prepared Mesh, waits when unresolved, and advances
without materialization only when the model is terminally missing.
- Zero/non-positive batch advances without materialization.
- Planner returns a fresh action plan and owns no state or engine reference.
## Dependencies
- Requires: accepted typed M2 build queue on master `c74b90a`
- Blocks: extracting stateful M2 resource readiness/dispatch orchestration
- External state: cache lookups, requests and engine lifetime remain loader APIs
## Verification
- Commands: dedicated action-priority/matrix/fresh-result/source/timing verifier;
M2 queue/planner/materializer/cache/prototype/shutdown/facade/internal-access/
manifest regressions; documentation, coordination and checkpoint dry-run gates
- Fixtures: scalar availability and terminal-state combinations only
- Fidelity evidence: exact existing branch priority and retry/advance behavior
- Performance budget: 20,000 pure dispatch plans under one second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent M2 build/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name: `M2BuildDispatchPlanner`
- Simplest solution: one stateless RefCounted with explicit action constants
- Rejected complexity: callbacks, signals, cache references and generic state machine
- Unavoidable complexity: animation-pending priority precedes every other action
- Measured optimization evidence: bounded synthetic decision loop
## Status
- State: integration accepted
- Done: implementation, verification, documentation and worktree handoff
- Next: extract typed M2 resource observation/request orchestration
- Blocked by:
## Handoff
- Commit: `17f5cc0faa376c8ac84a7ac72d5b8c0366866980`
- Results: dispatch planner `cases=10 iterations=20000 elapsed_ms=11.933`;
all autonomous headless regressions `53/53`; internal access
`private_symbols=30`; baseline manifest/dry-run `7/7`; documentation
`module_specs=40`; coordination passed with 30 historical expired warnings.
- Remaining risks: proprietary ADT visual/p95/p99 evidence is unavailable;
resource observation/requests, action execution, permits and engine lifetime
intentionally remain loader-owned.
- Documentation updated: new `m2-build-dispatch-planner.md` with API/I/O,
data-flow, lifecycle, sequence, dependency and ownership diagrams; batch/queue/
world renderer specs, module registry, RENDER and M03 Evidence updated.
- Integration: master merge `99a90ddfb3364d6672198887198602b84dd19e58`;
post-merge dispatch planner `cases=10 iterations=20000 elapsed_ms=12.291`,
twelve adjacent M2/resource/lifetime/facade/internal-access/manifest checks,
documentation and coordination all passed; checkpoint dry-run retained `7/7` plans.
@@ -0,0 +1,98 @@
# M03-RND-M2-BUILD-QUEUE-001
<!-- OPENWC_CLAIM:M03-RND-M2-BUILD-QUEUE-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-build-queue`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-build-queue`
## Outcome
Replace loader-owned raw M2 build-job Dictionary/FIFO fields with typed job and
queue state while preserving cursor, rotation, stale-key and lifetime behavior.
## Non-goals
- Move tile eligibility, resource readiness/retry or scheduler policy.
- Create/free Nodes, Meshes, MultiMeshes or animated instances inside queue state.
- Change grouping, batch planning, materialization, transform order or visuals.
- Change M2 cache versions, diagnostics text or Editor persistence.
## Paths
- Exclusive: `src/render/m2/m2_build_job.gd`,
`src/render/m2/m2_build_queue.gd`,
`src/tools/verify_m2_build_queue.gd`,
`docs/modules/m2-build-queue.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 build/
materializer/cache/shutdown/facade/internal-access verifiers and specs,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- A job retains tile key, root, exact groups Dictionary, insertion-order
`groups.keys()` snapshot, group index, transform offset and batch serial.
- FIFO keeps duplicate/stale keys; front rotation moves exactly one key to tail.
- Job erase does not erase FIFO entries, preserving external-cancel stale cleanup.
- Progress adopts group index, transform offset and serial atomically.
- Queue release never frees engine objects; loader retains finish/cancel cleanup.
## Dependencies
- Requires: accepted static materializer package on master `81e33a9`
- Blocks: extracting remaining M2 build/resource orchestration
- External state: Node validity and SceneTree destruction remain loader APIs
## Verification
- Commands: dedicated invalid/FIFO/duplicate/stale/rotation/cursor/lifetime/
diagnostics/source/timing verifier; M2 planner/materializer/cache/prototype/
shutdown/facade/internal-access/manifest regressions; docs/coordination/dry-run gates
- Fixtures: synthetic Node3D roots and ordered transform-group Dictionaries
- Fidelity evidence: exact existing keyed state, FIFO and cursor transitions
- Performance budget: 100 cycles of 256 enqueue/rotate/erase operations under one second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent build/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important names: `M2BuildJob`, `M2BuildQueue`
- Simplest solution: two small RefCounted types mirroring accepted WMO queue pattern
- Rejected complexity: signals, generic queue base, callbacks and engine destruction
- Unavoidable complexity: keyed jobs and FIFO keys intentionally permit stale entries
- Measured optimization evidence: bounded synthetic lifecycle loop
## Status
- State: integration accepted
- Done: implementation, verification, documentation and worktree handoff
- Next: extract the next unclaimed M2 resource-dispatch seam
- Blocked by:
## Handoff
- Commit: `4a8338f2f0503c2ba31c495e00a696d359518c06`
- Results: cold editor parse `exit=0`; M2 build queue `cases=13 iterations=100
elapsed_ms=66.247`; all autonomous headless regressions `52/52`; internal
access `private_symbols=30`; baseline manifest/dry-run `7/7`; documentation
`module_specs=39`; coordination passed with 30 historical expired warnings.
- Remaining risks: proprietary ADT visual/p95/p99 evidence is unavailable;
resource readiness/dispatch, permits, materializer choice and root destruction
intentionally remain loader-owned.
- Documentation updated: new `m2-build-queue.md` with API/I/O, data-flow,
lifecycle, sequence, dependency and ownership diagrams; world renderer,
batch planner, module registry, RENDER and M03 Evidence updated.
- Integration: master merge `1dc013e5c2597d5e6f26f44807f5f0a943bf1517`;
post-merge queue `cases=13 iterations=100 elapsed_ms=65.998`, ten adjacent
M2/lifetime/facade/internal-access/manifest checks, documentation and
coordination all passed; checkpoint dry-run retained `7/7` plans.
@@ -0,0 +1,98 @@
# M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001
<!-- OPENWC_CLAIM:M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-build-resource-snapshot`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-build-resource-snapshot`
## Outcome
Replace loose per-step M2 animated/static resource variables with a typed
observation snapshot consumed by the dispatch planner and materializer adapter.
## Non-goals
- Move ResourceLoader I/O, cache lookup/request execution or retry ownership.
- Own or free prototype Nodes or Mesh Resources.
- Change dispatch priority, batch sizing, queue ordering, permits or visuals.
- Change M2 cache paths, formats, allowlists or native animation selection.
## Paths
- Exclusive: `src/render/m2/m2_build_resource_snapshot.gd`,
`src/tools/verify_m2_build_resource_snapshot.gd`,
`docs/modules/m2-build-resource-snapshot.md`, this claim
- Shared/hotspots: `src/render/m2/m2_build_dispatch_planner.gd`, its verifier/spec,
`src/scenes/streaming/streaming_world_loader.gd`, M2 queue/materializer/cache
specs and tests, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
`RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- Snapshot retains normalized path, optional animated prototype, animation
request state, optional static Mesh and terminal missing-model state.
- Static observation is adopted only after animation is not pending, the batch
is positive and no animated prototype exists.
- Accessors borrow exact engine references without transferring/freeing ownership.
- Diagnostics expose only path and scalar availability flags.
## Dependencies
- Requires: accepted M2 dispatch planner on master `b113db0`
- Blocks: extracting M2 resource observation/request service ownership
- External state: prototype/cache/pipeline/resource lifetime remains loader-owned
## Verification
- Commands: dedicated identity/adoption/diagnostic/lifetime/source/timing verifier;
dispatch/queue/batch/materializer/cache/pipeline/shutdown/facade/internal-access/
manifest regressions; documentation, coordination and checkpoint dry-run gates
- Fixtures: synthetic Node3D prototype and ArrayMesh resources
- Fidelity evidence: exact resource presence/pending/missing values and branch order
- Performance budget: 20,000 snapshot construct/adopt/read cycles under one second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent dispatch/queue/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name: `M2BuildResourceSnapshot`
- Simplest solution: one small RefCounted value holder with explicit accessors
- Rejected complexity: generic resource union, signals, callbacks and cache ownership
- Unavoidable complexity: two-phase animation then optional static observation
- Measured optimization evidence: bounded synthetic lifecycle loop
## Status
- State: integration accepted
- Done: implementation, verification, documentation and worktree handoff
- Next: extract the M2 resource observation/request producer service
- Blocked by:
## Handoff
- Commit: `ec1b90f1e44037572e3e5052e6dedc016c29859a`
- Results: resource snapshot `cases=12 iterations=20000 elapsed_ms=42.995`;
dispatch planner remained green; all autonomous headless regressions `54/54`;
internal access `private_symbols=30`; baseline manifest/dry-run `7/7`;
documentation `module_specs=41`; coordination passed with 30 historical warnings.
- Remaining risks: proprietary ADT visual/p95/p99 evidence is unavailable;
resource lookup/request execution, action execution, permits and engine lifetime
intentionally remain loader-owned.
- Documentation updated: new `m2-build-resource-snapshot.md` with API/I/O,
data-flow, lifecycle, sequence, dependency and ownership diagrams; dispatch/
queue/world renderer specs, module registry, RENDER and M03 Evidence updated.
- Integration: master merge `4354834c50ae426640c721041af67d5265e82205`;
post-merge snapshot `cases=12 iterations=20000 elapsed_ms=42.211`, dispatch
planner and twelve adjacent resource/materializer/lifetime/facade/internal-
access/manifest checks, documentation and coordination all passed; checkpoint
dry-run retained `7/7` plans.
@@ -0,0 +1,91 @@
# M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001
<!-- OPENWC_CLAIM:M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-cached-animation-resource-observer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-cached-animation-resource-observer`
## Outcome
Move cached-GLB animated M2 lookup, allow/deny and primitive/schema safety
selection, threaded request admission and static-only transition from the loader
into a service that produces `M2BuildResourceSnapshot`.
## Non-goals
- Move native GryphonRoost raw-data build, debug logging or finalize drains.
- Own/free prototype Nodes or change cache/pipeline ownership.
- Change allow/deny defaults, primitive limits, candidate order or GLB rules.
- Change batching, permits, cache formats, profiles or visible output.
## Paths
- Exclusive: `src/render/m2/m2_cached_animation_resource_observer.gd`,
`src/tools/verify_m2_cached_animation_resource_observer.gd`,
`docs/modules/m2-cached-animation-resource-observer.md`, this claim
- Shared: loader, M2 snapshot/animation pipeline/prototype specs and verifiers,
world renderer, module registry, `RENDER.md` and M03 Evidence
## Contracts and data
- Cached animated prototype wins; static-only and pending states do not request.
- Empty allowlist rejects; denylist wins; matching is stripped/case-insensitive.
- Historical nested/lowercase/basename `.glb` order remains exact.
- Candidate requires animations, primitive limit and accepted empty or
`pivot_prefix_v1` schema; other schemas are rejected.
- Successful/`ERR_BUSY` request is remembered and reported pending; no usable
candidate or request failure marks animation static.
- Service produces a snapshot borrowing the exact prototype and frees nothing.
## Dependencies
- Requires: accepted static observer `5ebf4de`, snapshot and animation pipeline
- Blocks: native animation observation and remaining finalize extraction
## Verification
- Synthetic cache/pipeline/snapshot lifecycle, policy/path/GLB metadata fixtures,
source boundaries and timing; adjacent M2/renderer gates; full headless suite
and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, integration and post-merge acceptance
- Next: native M2 animation observation extraction
- Blocked by:
## Handoff
- Commit: `df87619` (`render: extract cached M2 animation resource observer`)
- Results: observer PASS (`33` cases, `20,000` iterations, `61.745ms`);
full autonomous suite PASS (`56/56`); renderer checkpoint dry-run PASS (`7/7`);
internal-access PASS (`30` private symbols); documentation PASS (`43` module
specs); coordination PASS with `34` historical warnings.
- Remaining risks: successful asynchronous admission is covered by source and
existing pipeline regressions because an undrained unit request leaks; generated
GLB metadata does not prove Godot import fidelity; private traversal, p95/p99,
leak and paired visual evidence remain unavailable; native observation and
terminal ResourceLoader polling/finalize remain loader-owned.
- Documentation updated: new cached observer API/I/O/data-flow/state/sequence/
dependency/ownership specification; M2 snapshot, batch/dispatch, animation
pipeline/prototype, world-renderer, module registry, `RENDER.md` and M03 Evidence.
## Integration
- Merge: `f79e064` (`merge: cached M2 animation resource observer`)
- Post-merge: observer `33` cases / `20,000` iterations / `61.514ms`;
animation pipeline `11` / `66.049ms`; snapshot `12` / `39.947ms`;
dispatch `10` / `17.915ms`; prototype cache `16` / `35.053ms`; shutdown,
facade, internal-access `30`, manifest `7/7`, documentation `43` and
coordination passed. Checkpoint dry-run completed `7/7`; expected private
Azeroth/character assets remained unavailable.
@@ -0,0 +1,109 @@
# M03-RND-M2-MESH-LOAD-PIPELINE-001 — M2 mesh load pipeline state
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-LOAD-PIPELINE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-mesh-load-pipeline`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract static M2 threaded ResourceLoader request records and their terminal
finalize FIFO from `StreamingWorldLoader`, preserving request order, status
transition, metrics and reset/shutdown behavior.
## Non-goals
- Call ResourceLoader or choose `.tscn`/`.glb` cache paths.
- Own M2 Mesh, scene, missing or material-refresh caches.
- Extract Mesh from Resources or rebuild/adopt runtime Meshes.
- Consume `M2_MESH_FINALIZE` permits.
- Change cache formats, animation loading or visible M2 behavior.
## Paths
- Exclusive: `src/render/m2/m2_mesh_load_pipeline_state.gd`,
`src/tools/verify_m2_mesh_load_pipeline_state.gd`,
`docs/modules/m2-mesh-load-pipeline-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- One normalized path owns at most one pending request.
- Pending records preserve Dictionary insertion order for polling snapshots.
- Terminal completion removes the request and appends its record/status to FIFO.
- Finalize pop remains oldest-first; duplicate/unknown transitions are rejected.
- Total work count remains pending plus finalize records.
- Clear drops bookkeeping only; loader drains ResourceLoader requests first.
## Dependencies
- Requires: accepted classifier package on current master `8ec13d6`
- Blocks: further M2 Mesh cache/finalization service extraction
- External state: none; verification uses synthetic paths and opaque statuses
## Verification
- Commands: dedicated validation/order/transition/FIFO/clear/source/timing
verifier; M2 classifier/builder/grouper/transform/registry, materials, shutdown,
facade, internal-access, manifest, documentation and coordination gates
- Fixtures: synthetic normalized paths, resource paths and integer terminal statuses
- Fidelity evidence: exact request/finalize transitions and metric/reset sites preserved
- Performance budget: 100 cycles over 256 request/completion/pop records under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- World-renderer, module registry and RENDER status updates
## Simplicity and naming
- Important names introduced: `M2MeshLoadPipelineState`
- Simplest considered solution: one RefCounted with request map and finalize FIFO
- Rejected complexity/abstractions: generic ResourceLoader queue, signals,
callbacks, Mesh cache ownership or animation/static pipeline merger
- Unavoidable complexity and justification: polling and budgeted finalization are
distinct lifecycle stages across frames
- Measured optimization evidence: bounded synthetic transition loop
## Status
- State: accepted
- Done: state holder, loader adapters, contract verifier and required documentation
- Next: further M2 Mesh cache/finalizer extraction
- Blocked by:
## Handoff
- Commit: `82df7a5`
- Results: 34 headless contract regressions pass; dedicated pipeline state
verifier passes 100-by-256 transitions in 100.273 ms; documentation and
coordination gates pass; checkpoint dry-run passes 7/7.
- Remaining risks: no private asset traversal, leak/descriptor-pressure or
original-client visual comparison; ResourceLoader I/O and Mesh cache/finalizer
ownership remain in `StreamingWorldLoader` by design.
- Documentation updated: inline API; `m2-mesh-load-pipeline-state.md` with
data-flow, state, sequence and dependency diagrams; module registry,
`world-renderer.md` and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-LOAD-PIPELINE-001:82df7a5 -->
## Integration
- Merge commit: `83b3618`
- Post-merge results: pipeline state `cases=11 iterations=100
elapsed_ms=69.196`; classifier, materials, shutdown, facade, internal-access
`34`, manifest `7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-LOAD-PIPELINE-001:83b3618 -->
@@ -0,0 +1,108 @@
# M03-RND-M2-MESH-RESOURCE-CACHE-001 — M2 mesh resource cache state
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-mesh-resource-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract the normalized-path to static M2 Mesh resource cache from
`StreamingWorldLoader`, preserving lookup, replacement, shutdown lifetime and
all existing cache consumers.
## Non-goals
- Own the shared M2 missing-path cache or animated/prototype node caches.
- Call ResourceLoader, extract Meshes from Resources or select cache paths.
- Prepare, rebuild or materialize M2 Meshes.
- Change cache formats, material refresh, animation or visible behavior.
## Paths
- Exclusive: `src/render/m2/m2_mesh_resource_cache_state.gd`,
`src/tools/verify_m2_mesh_resource_cache_state.gd`,
`docs/modules/m2-mesh-resource-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_m2_mesh_load_pipeline_state.gd`,
`src/tools/verify_render_runtime_cache_shutdown.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- Non-empty normalized M2 paths map to at most one Mesh resource reference.
- Store replaces the previous reference for the same path.
- Lookup returns the exact retained Mesh reference; unknown/empty paths return null.
- Clear releases all cache references only at the existing final-shutdown site.
- Shared missing/prototype/animated caches remain loader-owned and unchanged.
## Dependencies
- Requires: accepted mesh load pipeline package on master `ef782d1`
- Blocks: isolated M2 Mesh resource finalizer extraction
- External state: none; verification uses synthetic Mesh resources
## Verification
- Commands: dedicated validation/store/replace/lookup/clear/source/timing
verifier; mesh pipeline/classifier/material/shutdown/M2 build/facade/internal-
access/manifest, documentation and coordination gates
- Fixtures: synthetic ArrayMesh resources and normalized paths
- Fidelity evidence: exact cache call-site and shutdown lifetime preservation
- Performance budget: 100 cycles over 256 store/lookup entries under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state and dependency diagrams
- World-renderer, module registry and RENDER status updates
## Simplicity and naming
- Important name introduced: `M2MeshResourceCacheState`
- Simplest solution: one RefCounted with one typed-purpose Dictionary
- Rejected complexity: generic resource cache, missing-state merger, eviction,
signals, callbacks or materialization ownership
- Unavoidable complexity: none beyond retaining engine Resource references
- Measured optimization evidence: bounded synthetic store/lookup loop
## Status
- State: accepted
- Done: cache state, loader/test adapters, contract verifier and required documentation
- Next: isolated M2 Mesh resource finalizer extraction
- Blocked by:
## Handoff
- Commit: `e990a65`
- Results: dedicated cache verifier passes 100-by-256 store/lookups in
38.974 ms; 35 headless renderer/coordinate regressions pass; checkpoint
dry-run passes 7/7; documentation and coordination gates pass.
- Remaining risks: no private M2 traversal, Mesh memory/leak/descriptor-pressure
run or original-client visual comparison; extraction/preparation, shared
missing state and materialization remain loader-owned by design.
- Documentation updated: inline API; `m2-mesh-resource-cache-state.md` with
data-flow, lifecycle, sequence and dependency diagrams; pipeline spec, module
registry, `world-renderer.md` and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-CACHE-001:e990a65 -->
## Integration
- Merge commit: `3094561`
- Post-merge results: cache state `cases=9 iterations=100
elapsed_ms=38.686`; pipeline, classifier, materials, shutdown, facade,
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-CACHE-001:3094561 -->
@@ -0,0 +1,109 @@
# M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001 — M2 mesh resource extractor
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-mesh-resource-extractor`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract first-Mesh selection from loaded M2 Resources and Node subtrees,
including temporary PackedScene instance cleanup, from `StreamingWorldLoader`.
## Non-goals
- Call ResourceLoader or select/request cache paths.
- Refresh/rebuild materials or load raw M2 data.
- Own the Mesh cache, missing state, prototypes or MultiMesh materialization.
- Change traversal order, cache formats, profiles or visible behavior.
## Paths
- Exclusive: `src/render/m2/m2_mesh_resource_extractor.gd`,
`src/tools/verify_m2_mesh_resource_extractor.gd`,
`docs/modules/m2-mesh-resource-extractor.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
M2 pipeline/cache/material/shutdown verifiers, `docs/modules/world-renderer.md`,
`docs/modules/README.md`, M2 module specs, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- A direct Mesh Resource returns the same Mesh reference.
- A PackedScene is instantiated once, searched depth-first in child order and
its temporary root is freed before return.
- A Node subtree search returns the first MeshInstance3D Mesh in historical
depth-first preorder without changing the tree.
- Null, unsupported Resources and subtrees without a Mesh return null.
## Dependencies
- Requires: accepted Mesh resource cache package on master `5e2afbc`
- Blocks: isolated material refresh/rebuild finalizer extraction
- External state: none; verification uses synthetic Mesh/Node/PackedScene fixtures
## Verification
- Commands: dedicated direct/PackedScene/subtree/order/lifetime/source/timing
verifier; M2 pipeline/cache/classifier/material/shutdown/build/facade/internal-
access/manifest, documentation and coordination gates
- Fixtures: ArrayMesh, MeshInstance3D subtrees and synthetic PackedScene
- Fidelity evidence: exact traversal order and temporary-node destruction
- Performance budget: 10,000 synthetic subtree lookups under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- M2 cache/pipeline, world-renderer, registry and RENDER updates
## Simplicity and naming
- Important name introduced: `M2MeshResourceExtractor`
- Simplest solution: one stateless RefCounted with two explicit extraction APIs
- Rejected complexity: callback-driven finalizer, generic scene utility,
cache ownership, signals or visitor framework
- Unavoidable complexity: temporary PackedScene instance must be freed while
its referenced Mesh remains borrowed
- Measured optimization evidence: bounded synthetic depth-first traversal
## Status
- State: accepted
- Done: extractor, three loader/test adapters, contract verifier and required documentation
- Next: isolated M2 Mesh material preparation finalizer extraction
- Blocked by:
## Handoff
- Commit: `7b0e1ea`
- Results: extractor passes direct/PackedScene/subtree/order/lifetime contracts
and 10,000 traversals in 39.243 ms; 36 headless renderer/coordinate
regressions pass; checkpoint dry-run passes 7/7; documentation and
coordination gates pass.
- Remaining risks: synchronous PackedScene instantiation remains behind the
existing permit; no private M2 corrupt-scene/traversal/leak/p95/p99 or
original-client comparison; material preparation remains loader-owned.
- Documentation updated: inline API; `m2-mesh-resource-extractor.md` with
data-flow, lifecycle, sequence and dependency diagrams; cache/pipeline specs,
module registry, `world-renderer.md` and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:7b0e1ea -->
## Integration
- Merge commit: `fa85985`
- Post-merge results: extractor `cases=10 iterations=10000
elapsed_ms=38.172`; cache, pipeline, classifier, materials, shutdown, facade,
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:fa85985 -->
@@ -0,0 +1,84 @@
# M03-RND-M2-MESH-RESOURCE-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-mesh-resource-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-mesh-resource-finalizer`
## Outcome
Move static M2 ResourceLoader status polling, terminal Resource/Mesh extraction,
runtime Mesh preparation, Mesh-cache adoption and missing outcome from the
loader into one dedicated finalization service.
## Non-goals
- Change static cache path selection or threaded request admission.
- Merge static and animated terminal services behind a generic callback layer.
- Own permits, MultiMesh materialization, prototype Nodes or SceneTree roots.
- Change material refresh version, rebuild rules, cache formats or visible output.
## Paths
- Exclusive: `src/render/m2/m2_mesh_resource_finalizer.gd`,
`src/tools/verify_m2_mesh_resource_finalizer.gd`,
`docs/modules/m2-mesh-resource-finalizer.md`, this claim
- Shared: loader, Mesh pipeline/cache/extractor/runtime-finalizer/raw-repository/
prototype specs and verifiers, world renderer, registry, `RENDER.md`, M03 Evidence
## Contracts and data
- Pending requests are polled in existing Dictionary insertion order.
- Empty Resource paths are discarded and marked missing immediately.
- Only LOADED/FAILED statuses enter completion-order finalize FIFO.
- One permit pops exactly one terminal record, including skipped/cached records.
- Existing cached Mesh suppresses terminal Resource retrieval.
- Loaded Resource uses exact first-Mesh extraction and current refresh-version path.
- Raw M2 data loads only when the extracted Mesh is stale.
- Prepared Mesh is stored by existing replacement semantics; failures mark missing.
## Dependencies
- Requires: accepted Mesh pipeline/cache/extractor/runtime finalizer/raw repository
- Blocks: remaining loader-owned M2 materialization and orchestration cleanup
## Verification
- Synthetic polling/FIFO/skip/load/extract/refresh/store/missing/source/timing
contracts; adjacent M2/renderer gates; full headless suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: next unclaimed M03 renderer extraction package
- Blocked by:
## Handoff
- Commit: `ddeb708` (`render: extract M2 Mesh resource finalizer`)
- Results: finalizer PASS `cases=31 iterations=1000 elapsed_ms=0.767`;
adjacent Mesh pipeline/cache/extractor/runtime-finalizer/raw-repository/
prototype/shutdown checks passed; autonomous headless suite `59/59`;
documentation `46`; coordination passed with `34` historical warnings;
checkpoint dry-run `7/7`.
- Remaining risks: terminal instantiation/raw parsing/rebuild remain synchronous
main-thread work behind existing permits; no private-asset visual, leak,
descriptor-pressure or p95/p99 evidence was added.
- Documentation updated: new full module specification with API/I/O and
data-flow/state/sequence/dependency diagrams; adjacent M2 module specs,
world-renderer registry/spec, `RENDER.md` and M03 Evidence.
- Merge: `34b7000` (`merge: M2 Mesh resource finalizer`)
- Post-merge: finalizer PASS `cases=31 iterations=1000 elapsed_ms=0.826`;
adjacent M2 pipeline/cache/extractor/runtime/repository/prototype/shutdown,
facade, internal-access `30`, manifest `7/7`, documentation `46` and
coordination gates passed.
@@ -0,0 +1,84 @@
# M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001
<!-- OPENWC_CLAIM:M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-native-animation-resource-observer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-native-animation-resource-observer`
## Outcome
Move native GryphonRoost M2 animation candidate selection, synchronous raw read,
native animated build, prototype/static-only adoption and debug diagnostics from
the loader into a dedicated observer.
## Non-goals
- Change the exact native candidate predicate or add more animated models.
- Move cached-GLB observation, ResourceLoader polling/finalize or playback.
- Own attached instances, consume permits or change queue/batch behavior.
- Change native parsing/building, cache formats, profiles or visible output.
## Paths
- Exclusive: `src/render/m2/m2_native_animation_resource_observer.gd`,
`src/tools/verify_m2_native_animation_resource_observer.gd`,
`docs/modules/m2-native-animation-resource-observer.md`, this claim
- Shared: loader, M2 cached observer/snapshot/raw repository/prototype specs and
verifiers, world renderer, module registry, `RENDER.md` and M03 Evidence
## Contracts and data
- Only case-insensitive paths containing `gryphonroost` are native candidates.
- Cached animated prototype wins; static-only state suppresses repeated reads.
- Empty raw data or animated surfaces marks animation static.
- Builder result must be non-null with at least one child; rejection marks static.
- Successful result is adopted through first-wins prototype cache semantics.
- Existing `M2_NATIVE_ANIM_CACHE` fields and debug gating remain exact.
- Observer attaches/frees no Node and starts no async work.
## Dependencies
- Requires: accepted cached observer `6f385ed`, raw repository and prototype state
- Blocks: remaining ResourceLoader polling/finalize extraction
## Verification
- Synthetic candidate/cache/static/raw/build/adoption/lifetime/logging/source and
timing contracts; adjacent M2/renderer gates; full headless suite and checkpoint
dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: remaining cached ResourceLoader terminal polling/finalize extraction
- Blocked by:
## Handoff
- Commit: `1cb0101`
- Results: observer `33` cases / `20,000` timing iterations; all `57/57`
autonomous headless regressions; facade/internal-access/shutdown/manifest;
documentation `44`; coordination; checkpoint dry-run `7/7`.
- Remaining risks: private asset animation/visual/leak/p95/p99 evidence is absent;
native parse/build stays synchronous; historical childless rejected Node remains
unfreed; cached ResourceLoader terminal polling/finalize stays loader-owned.
- Documentation updated: new native observer API/I/O/data-flow/state/sequence/
dependency/ownership spec; cached observer, snapshot, dispatch, repository,
prototype cache, world renderer, module registry, `RENDER.md` and M03 Evidence.
## Integration
- Merge: `d37c799`
- Post-merge: native/cached observers, repository, cache, snapshot, dispatch,
shutdown, facade, internal-access `30`, manifest `7/7`, documentation `44`,
coordination and checkpoint dry-run `7/7` passed on master.
@@ -0,0 +1,111 @@
# M03-RND-M2-PROTOTYPE-CACHE-001 — M2 prototype cache state
<!-- OPENWC_CLAIM:M03-RND-M2-PROTOTYPE-CACHE-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-prototype-cache`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract static/animated detached M2 prototype references plus missing-model and
static-animation negative outcomes from `StreamingWorldLoader` into one explicit
shutdown-lifetime cache-state service.
## Non-goals
- Choose `.tscn/.glb` paths, call ResourceLoader/native loaders or build Nodes.
- Own animation/Mesh request queues, Mesh resources, materials or MultiMeshes.
- Change candidate/allowlist/denylist rules, fallback order or visible behavior.
- Add persistence, eviction, async work, dependencies or cache-format changes.
## Paths
- Exclusive: `src/render/m2/m2_prototype_cache_state.gd`,
`src/tools/verify_m2_prototype_cache_state.gd`,
`docs/modules/m2-prototype-cache-state.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, runtime
cache shutdown/M2 raw/finalizer/extractor/pipeline/material/animation verifiers
and module specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
`RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- Normalized non-empty paths key static and animated detached `Node3D` prototypes.
- Missing-model and static-animation outcomes are independent path sets.
- Exact cached Node reference is returned; first adoption for an occupied path wins.
- Shutdown frees valid detached nodes synchronously, queues in-tree nodes for free,
clears all four states and remains idempotent.
- Map refresh retains the state exactly as before; only final shutdown clears it.
## Dependencies
- Requires: accepted raw model repository package on master `ca77d32`
- Blocks: remaining M2 animation request/materialization extraction
- External state: none; verification uses synthetic Node3D instances
## Verification
- Commands: dedicated admission/identity/independence/duplicate/free/source/timing
verifier; runtime shutdown/raw/finalizer/extractor/cache/pipeline/material/
animation/M2 build/facade/internal-access/manifest, docs and coordination gates
- Fixtures: detached and in-tree synthetic Node3D prototypes
- Fidelity evidence: all existing lookup/store/negative/clear caller transitions
- Performance budget: 100 cycles over 256 path-state operations under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent M2/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2PrototypeCacheState`
- Simplest solution: one main-thread RefCounted with two Node maps and two path sets
- Rejected complexity: generic Node cache, signals, callbacks, eviction policy,
weak references, async cleanup or dependency injection
- Unavoidable complexity: detached Nodes require explicit free at final shutdown
- Measured optimization evidence: bounded synthetic cache-cycle loop
## Status
- State: accepted
- Done: cache state, loader/test adapters, lifecycle verifier and required documentation
- Next: continue M2 animation request/materialization extraction
- Blocked by:
## Handoff
- Commit: `cd664eb`
- Results: prototype state passes 16 admission/identity/negative/lifecycle/source
cases and 100-by-256 cycles in 50.266 ms; 46 autonomous headless verifiers
pass, while the proprietary ADT placement probe is unavailable without
`data/extracted`; checkpoint dry-run passes 7/7; documentation and
coordination gates pass; loader-private inventory decreases from 34 to 31.
- Remaining risks: strong prototypes remain unbounded until shutdown; successful
asset traversal, descriptor pressure, leaks and p95/p99 require proprietary
data; no original-client visual comparison is claimed.
- Documentation updated: inline API; `m2-prototype-cache-state.md` with data-flow,
lifecycle, sequence and dependency diagrams; pipeline/Mesh-cache/extractor/raw
repository/world-renderer specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PROTOTYPE-CACHE-001:cd664eb -->
## Integration
- Merge commit: `1de50f9`
- Post-merge results: prototype cache `cases=16 iterations=100
elapsed_ms=37.978`; shutdown, raw repository, finalizer, extractor, Mesh cache,
pipeline, materials, facade, internal-access `31`, manifest `7/7`,
documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-PROTOTYPE-CACHE-001:1de50f9 -->
@@ -0,0 +1,109 @@
# M03-RND-M2-RAW-MODEL-REPOSITORY-001 — raw M2 model repository
<!-- OPENWC_CLAIM:M03-RND-M2-RAW-MODEL-REPOSITORY-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-raw-model-repository`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract the repeated FileAccess/ClassDB native `M2Loader` boundary for static
and animated raw model Dictionaries from `StreamingWorldLoader`.
## Non-goals
- Cache raw data, Meshes, prototypes, missing paths or animation decisions.
- Build Nodes/Meshes, refresh materials or choose `.tscn/.glb` caches.
- Change static/animated parser methods, path resolution or fallback order.
- Add dependencies, persistence, retries or visible behavior.
## Paths
- Exclusive: `src/render/m2/m2_raw_model_repository.gd`,
`src/tools/verify_m2_raw_model_repository.gd`,
`docs/modules/m2-raw-model-repository.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
finalizer/extractor/cache/pipeline/material/animation/shutdown verifiers and
module specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
`RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- Static load calls native `load_m2`; animated load calls `load_m2_animated`.
- Paths remain `globalize_path(extracted_directory.path_join(normalized_path))`.
- Empty path, missing native class/file/instance/method, parse failure or
non-Dictionary result returns an empty Dictionary without retained state.
- Each successful request creates one native loader instance as before.
## Dependencies
- Requires: accepted runtime Mesh finalizer package on master `1d94eac`
- Blocks: M2 prototype/cache state extraction
- External state: native M2Loader registration and local extracted files
## Verification
- Commands: dedicated invalid/missing/class/source/timing verifier; finalizer/
classifier/extractor/cache/pipeline/material/animation/shutdown/M2 build/facade/
internal-access/manifest, documentation and coordination gates
- Fixtures: empty/missing normalized paths plus source contract; proprietary
success corpus remains external
- Fidelity evidence: exact native method names, path expression and three caller transitions
- Performance budget: 10,000 rejected empty-path calls under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Finalizer/extractor/cache/pipeline, world-renderer, registry and RENDER updates
## Simplicity and naming
- Important name introduced: `M2RawModelRepository`
- Simplest solution: one stateless RefCounted with static and animated load methods
- Rejected complexity: generic native repository, caching, dependency injection,
retries, diagnostics framework or async wrapper
- Unavoidable complexity: animated native method availability is a distinct guard
- Measured optimization evidence: bounded rejected-input loop
## Status
- State: accepted
- Done: repository, three loader adapters, contract verifier and required documentation
- Next: continue M2 prototype/cache state extraction
- Blocked by:
## Handoff
- Commit: `b608a81`
- Results: repository passes invalid/missing/stateless/native-boundary contracts
and 10,000 empty-path rejects in 3.275 ms; 45 repository-independent
headless verifiers pass, while the proprietary ADT placement probe is
unavailable without `data/extracted`; checkpoint dry-run passes 7/7;
documentation and coordination gates pass.
- Remaining risks: successful/corrupt native parsing, descriptor pressure,
leaks and p95/p99 remain asset-backed work; native calls are synchronous and
cannot be cancelled mid-parse; no original-client visual comparison is claimed.
- Documentation updated: inline API; `m2-raw-model-repository.md` with data-flow,
lifecycle, sequence and dependency diagrams; finalizer/classifier/world-renderer
specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RAW-MODEL-REPOSITORY-001:b608a81 -->
## Integration
- Merge commit: `7cd07dd`
- Post-merge results: repository `cases=10 iterations=10000
elapsed_ms=3.200`; finalizer, extractor, cache, pipeline, shutdown, facade,
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RAW-MODEL-REPOSITORY-001:7cd07dd -->
@@ -0,0 +1,110 @@
# M03-RND-M2-RUNTIME-MESH-FINALIZER-001 — M2 runtime mesh finalizer
<!-- OPENWC_CLAIM:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-runtime-mesh-finalizer`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract stale cached-Mesh refresh classification, metadata adoption,
M2Builder rebuild and fallback from `StreamingWorldLoader` into one runtime M2
Mesh finalizer while retaining raw-file I/O in the loader.
## Non-goals
- Load raw `.m2` files, call FileAccess/ClassDB M2Loader or choose cache paths.
- Own ResourceLoader requests, Mesh cache/missing state or MultiMesh builds.
- Change material refresh version `2`, classifier predicates or rebuild behavior.
- Change cache formats, profiles or visible output.
## Paths
- Exclusive: `src/render/m2/m2_runtime_mesh_finalizer.gd`,
`src/tools/verify_m2_runtime_mesh_finalizer.gd`,
`docs/modules/m2-runtime-mesh-finalizer.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
classifier/extractor/cache/pipeline/material/shutdown verifiers and module
specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
proprietary renderer corpus
## Contracts and data
- Refresh version remains `2`; already-current Meshes return unchanged without raw data.
- Stale Meshes require caller-supplied raw data only when refresh is necessary.
- Empty/ordinary raw data marks the original Mesh current and returns it.
- Billboard/UV-rotation data attempts M2Builder rebuild and returns the rebuilt
first Mesh; rebuild failure marks and returns the original Mesh.
- Classifier memoization retains both existing clear sites through finalizer clear.
## Dependencies
- Requires: accepted Mesh resource extractor package on master `dd8cfbf`
- Blocks: further M2 raw-data I/O/prototype state extraction
- External state: none; verification uses synthetic Mesh/raw-data fixtures
## Verification
- Commands: dedicated current/stale/ordinary/rebuild/fallback/clear/source/timing
verifier; classifier/extractor/cache/pipeline/material/shutdown/M2 build/facade/
internal-access/manifest, documentation and coordination gates
- Fixtures: ArrayMesh and one synthetic triangle/billboard M2 Dictionary
- Fidelity evidence: exact refresh version, predicate, rebuild and fallback transitions
- Performance budget: 100 cycles over 256 ordinary finalizations under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Classifier/extractor/cache/pipeline, world-renderer, registry and RENDER updates
## Simplicity and naming
- Important name introduced: `M2RuntimeMeshFinalizer`
- Simplest solution: one service composing existing classifier/extractor and M2Builder
- Rejected complexity: callbacks for loader I/O, generic material pipeline,
cache ownership, signals or strategy framework
- Unavoidable complexity: two-phase raw-data request avoids loading raw files for current Meshes
- Measured optimization evidence: bounded synthetic ordinary finalization loop
## Status
- State: accepted
- Done: finalizer, loader/test adapters, transition/rebuild verifier and required documentation
- Next: further M2 raw-data I/O/prototype state extraction
- Blocked by:
## Handoff
- Commit: `ece7724`
- Results: finalizer passes current/stale/ordinary/synthetic-rebuild/fallback/
clear contracts and 100-by-256 finalizations in 53.719 ms; 37 headless
renderer/coordinate regressions pass; checkpoint dry-run passes 7/7;
documentation and coordination gates pass.
- Remaining risks: rebuild remains synchronous behind the existing permit; no
private M2 material/traversal/descriptor-pressure/leak/p95/p99 or original-
client comparison; raw `.m2` I/O remains loader-owned by design.
- Documentation updated: inline API; `m2-runtime-mesh-finalizer.md` with
data-flow, lifecycle, sequence and dependency diagrams; classifier/extractor/
cache/pipeline specs, module registry, `world-renderer.md` and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:ece7724 -->
## Integration
- Merge commit: `1fafdde`
- Post-merge results: finalizer `cases=11 iterations=100
elapsed_ms=54.349`; classifier, extractor, cache, pipeline, materials,
shutdown, facade, internal-access `34`, manifest `7/7`, documentation and
coordination gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:1fafdde -->
@@ -0,0 +1,115 @@
# M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001 — M2 runtime rebuild classifier
<!-- OPENWC_CLAIM:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:sindo-main-codex:2026-07-20 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`
- Lease expires UTC: 2026-07-20
- Integrator: M03 milestone integrator
## Outcome
Extract the memoized decision that stale cached M2 meshes require an in-memory
runtime rebuild because raw batches contain billboard vertices or referenced UV
rotation transforms.
## Non-goals
- Load raw M2 files or cache Resources.
- Build, replace or mutate Meshes, materials, Nodes or RIDs.
- Change `M2_MATERIAL_REFRESH_VERSION` or cache formats.
- Interpret animation, particles, ribbons or general M2 shader fidelity.
- Change path normalization, ResourceLoader transitions or finalize budgets.
## Paths
- Exclusive: `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd`,
`src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd`,
`docs/modules/m2-runtime-mesh-rebuild-classifier.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT/M2 resources, caches
and proprietary renderer corpus
## Contracts and data
- Billboard metadata requires rebuild when `has_billboard` is true or
`billboard_vertex_count` is positive in any Dictionary batch.
- UV rotation requires non-empty transforms and combos plus a referenced
non-identity `Vector4` rotation or absolute rotation speed above `0.000001`.
- Only up to four texture stages per batch are inspected; invalid variants and
out-of-range combo/transform indices are skipped.
- The first decision for a normalized path is memoized until clear, matching the
existing loader cache lifetime.
## Dependencies
- Requires: current accepted master `90b1dac`
- Blocks: further M2 mesh request/cache/finalization extraction
- External state: none; verification uses synthetic raw M2 Dictionaries
## Verification
- Commands: dedicated billboard/UV/index/stage/cache/clear/source/timing verifier;
M2 builder/grouper/transform/registry, materials, shutdown, facade,
internal-access, manifest, documentation and coordination gates
- Fixtures: synthetic batches, transform combos and rotation dictionaries
- Fidelity evidence: exact existing predicate thresholds, stage cap and cache lifetime preserved
- Performance budget: 100 classifications over 256 paths under 1 second
## Documentation deliverables
- Inline public API docs
- Module specification with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- World-renderer, module registry and RENDER status updates
## Simplicity and naming
- Important names introduced: `M2RuntimeMeshRebuildClassifier`
- Simplest considered solution: one RefCounted with one memoized query and clear
- Rejected complexity/abstractions: generic predicate cache, strategy interface,
ResourceLoader wrapper, signals or typed rewrite of raw parser Dictionaries
- Unavoidable complexity and justification: four-stage combo indirection is the
existing build-12340 M2 material boundary
- Measured optimization evidence: bounded synthetic classification loop
## Status
- State: accepted
- Done: classifier/cache implemented; loader query and both clear sites migrated;
synthetic predicate/cache verifier and required documentation added
- Next: continue M2 mesh request/cache/finalization extraction
- Blocked by:
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:38f831e -->
## Handoff
- Commit: `19ca8f4`
- Results: classifier PASS (`cases=12`, `iterations=100`,
`elapsed_ms=64.585`; initial `35.461ms`); billboard flag/count, invalid
variants/indices, UV rotation/speed/epsilon, four-stage cap, memoization,
detached diagnostics and clear contracts PASS; 33 M2/WMO/liquid/terrain/
facade/scheduler/streaming/coordinate/material/shutdown regressions PASS;
internal-access decreased to `35`; documentation and coordination gates PASS;
checkpoint dry-run PASS `7/7`.
- Remaining risks: raw parser data remains Dictionary-based; the first decision
for a path remains authoritative until clear; classification covers the current
four-stage material implementation only; private asset descriptor-pressure,
traversal, p95/p99 and paired original-client evidence remain absent.
- Documentation updated: inline public API; new
`docs/modules/m2-runtime-mesh-rebuild-classifier.md`; module registry, world
renderer and `RENDER.md`; data-flow/state/sequence/dependency diagrams,
ownership, failure/recovery, thresholds, cache lifetime and source map included.
- Integration: merged as `38f831e`; post-merge classifier (`cases=12`,
`iterations=100`, `elapsed_ms=45.439`), materials, shutdown, M2 batch planner,
facade, internal-access (`private_symbols=35`), `7/7` manifest, documentation
and coordination gates passed.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:19ca8f4 -->
@@ -0,0 +1,106 @@
# M03-RND-M2-STATIC-BATCH-MATERIALIZER-001
<!-- OPENWC_CLAIM:M03-RND-M2-STATIC-BATCH-MATERIALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-static-batch-materializer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-static-batch-materializer`
## Outcome
Extract main-thread static M2 `MultiMesh` construction, render-property setup
and batch attachment from `StreamingWorldLoader`.
## Non-goals
- Change M2 grouping, batch sizing, build cursors, budgets or retry behavior.
- Change Mesh loading, refresh, cache ownership or missing-model outcomes.
- Own animated M2 instances, Editor persistence or tile cleanup.
- Introduce spatial cells or change transform order, visibility or shadows.
## Paths
- Exclusive: `src/render/m2/m2_static_batch_materializer.gd`,
`src/tools/verify_m2_static_batch_materializer.gd`,
`docs/modules/m2-static-batch-materializer.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2 build/
Mesh-cache/facade/internal-access/manifest verifiers and specs,
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
`targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated resources/caches and proprietary corpus
## Contracts and data
- `MultiMesh.TRANSFORM_3D`, exact Mesh identity, instance count and transform
slice order remain unchanged.
- Batch node name remains model basename plus serial.
- Visibility end/margin and shadow setting remain exact renderer inputs.
- A valid batch attaches once to the supplied M2 parent and returns its node.
- Loader retains Editor-owner assignment, build cursor, budgets and retries.
## Dependencies
- Requires: accepted animated materializer package on master `86814de`
- Blocks: further M2 build-job orchestration extraction
- External state: Godot MultiMesh and SceneTree mutation remain main-thread APIs
## Verification
- Commands: dedicated invalid/name/mesh/count/order/render/attachment/source/timing
verifier; M2 build/cache/prototype/animated/shutdown/material/facade/internal-
access/manifest regressions; docs/coordination/dry-run gates
- Fixtures: synthetic ArrayMesh and ordered Transform3D slices
- Fidelity evidence: exact existing MultiMesh construction and render settings
- Performance budget: materialize 10,000 transforms under one second
## Documentation deliverables
- Inline API docs
- Module spec with inputs/outputs, ownership and source map
- Data-flow, lifecycle/state, sequence and dependency diagrams
- Adjacent build/cache/world-renderer/module-registry/RENDER updates
## Simplicity and naming
- Important name introduced: `M2StaticBatchMaterializer`
- Simplest solution: synchronous main-thread RefCounted returning attached node
- Rejected complexity: signals, async queue, callbacks and generic materializer base
- Unavoidable complexity: MultiMesh is a Godot Resource owned through its node
- Measured optimization evidence: bounded synthetic transform upload
## Status
- State: ready for integration
- Done: materializer, loader adapter, verifier and required documentation
- Next: integrator merge and post-merge acceptance
- Blocked by:
## Handoff
- Commit: `252927d`
- Results: materializer passes 11 invalid/node/Mesh/render/attachment/source
cases and 10,000 transform API writes in 0.552 ms; all 51 autonomous headless
verifiers pass, while the proprietary ADT placement probe is unavailable
without `data/extracted`; checkpoint dry-run passes 7/7; documentation and
coordination gates pass; loader-private inventory remains 30.
- Remaining risks: headless dummy timing excludes GPU upload cost; private asset
traversal/visual comparison, spatial-cell culling, descriptor pressure, leaks
and p95/p99 remain unavailable; no original-client comparison is claimed.
- Documentation updated: inline API; `m2-static-batch-materializer.md` with
data-flow, lifecycle, sequence and dependency diagrams; build/cache/world-
renderer specs, module registry and `RENDER.md`.
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-STATIC-BATCH-MATERIALIZER-001:252927d -->
## Integration
- Merge commit: `5e82daa`
- Post-merge results: static materializer `cases=11 instances=10000
elapsed_ms=1.327`; build/cache/animated materializer, shutdown, facade,
internal-access `30`, checkpoint dry-run `7/7`, documentation and coordination
gates passed.
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-STATIC-BATCH-MATERIALIZER-001:5e82daa -->
@@ -0,0 +1,86 @@
# M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001
<!-- OPENWC_CLAIM:M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-m2-static-build-resource-observer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-m2-static-build-resource-observer`
## Outcome
Move static M2 build cache lookup, threaded request selection and terminal
missing transition from the loader into a service that fills
`M2BuildResourceSnapshot`.
## Non-goals
- Move animation/native prototype observation or finalize drains.
- Own/free Meshes, Nodes or cache entries.
- Change path candidate order, GLB filtering, retry behavior, permits or visuals.
- Change cache formats, profiles or batching.
## Paths
- Exclusive: `src/render/m2/m2_static_build_resource_observer.gd`,
`src/tools/verify_m2_static_build_resource_observer.gd`,
`docs/modules/m2-static-build-resource-observer.md`, this claim
- Shared: loader, M2 snapshot/dispatch/pipeline/cache specs and verifiers,
world renderer, module registry, RENDER and M03 Evidence
## Contracts and data
- Cached Mesh wins; terminal missing returns missing; existing pending request waits.
- Candidate order remains normalized/lowercase nested then basename per extension.
- `.tscn` precedes `.glb`; pivot-prefix GLB is skipped for static build.
- Successful/`ERR_BUSY` request is remembered; no usable candidate marks missing.
- Snapshot adopts exact Mesh/missing result; service never frees engine objects.
## Dependencies
- Requires: accepted resource snapshot `032a256`
- Blocks: remaining animated resource observer extraction
## Verification
- Synthetic cache/pipeline/snapshot lifecycle, path order, source boundaries and timing;
adjacent M2 and renderer gates; full headless suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, integration and post-merge acceptance
- Next: remaining animated resource observer extraction
- Blocked by:
## Handoff
- Commit: `7cb3e34` (`render: extract static M2 build resource observer`)
- Results: observer PASS (`11` cases, `20,000` iterations, `64.654ms`);
full autonomous suite PASS (`55/55`); renderer checkpoint dry-run PASS (`7/7`);
internal-access PASS (`30` private symbols); documentation PASS (`42` module
specs); coordination PASS with `30` historical warnings.
- Remaining risks: successful asynchronous admission is covered by source and
existing pipeline regressions because an undrained unit request leaks; private
asset traversal, p95/p99, leak and visual evidence remain unavailable;
animated observation and static finalize polling remain loader-owned.
- Documentation updated: new observer module API/I/O/data-flow/state/sequence/
dependency/ownership specification; M2 snapshot, pipeline/cache, world-renderer,
module registry, `RENDER.md` and M03 Evidence updated.
## Integration
- Merge: `a043c79` (`merge: static M2 build resource observer`)
- Post-merge: observer `11` cases / `20,000` iterations / `64.202ms`;
snapshot `12` / `42.895ms`; dispatch `10` / `18.096ms`; Mesh pipeline
`11` / `73.615ms`; Mesh cache `9` / `40.229ms`; prototype cache `16` /
`35.947ms`; shutdown, facade, internal-access `30`, manifest `7/7`,
documentation `42` and coordination passed. Checkpoint dry-run completed
`7/7`; expected private Azeroth/character assets remained unavailable.
@@ -0,0 +1,79 @@
# M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-wmo-render-resource-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-render-resource-finalizer`
## Outcome
Move lightweight WMO render-cache terminal ResourceLoader polling, script/format
validation and Resource/missing publication from the loader into one service.
## Non-goals
- Change `.res` cache path selection or threaded request admission.
- Own WMO build permits, queues, placement state, Nodes or materialization.
- Change `WMOStreamingResource.FORMAT_VERSION`, fallback order or visible output.
- Combine render Resource and PackedScene finalization behind a generic callback.
## Paths
- Exclusive: `src/render/wmo/wmo_render_resource_finalizer.gd`,
`src/tools/verify_wmo_render_resource_finalizer.gd`,
`docs/modules/wmo-render-resource-finalizer.md`, this claim
- Shared: loader, WMO render cache state spec/verifier, world renderer registry/spec,
`RENDER.md`, M03 Evidence
## Contracts and data
- Pending requests are polled in detached Dictionary insertion order.
- Only LOADED/FAILED statuses complete requests.
- Failed, null, wrong-script and stale-format Resources publish missing outcome.
- Current exact-script Resources are adopted without duplication.
- Request admission, reset/shutdown drain order and cache lifetime are unchanged.
## Dependencies
- Requires: accepted WMO render Resource cache state
- Blocks: remaining loader-owned WMO orchestration/materialization cleanup
## Verification
- Synthetic status/order/load/validation/adoption/source/timing contracts;
adjacent WMO/renderer gates; full headless suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: next unclaimed M03 renderer extraction package
- Blocked by:
## Handoff
- Commit: `a86f8f2` (`render: extract WMO render resource finalizer`)
- Results: finalizer PASS `cases=24 iterations=1000 elapsed_ms=3.938`;
adjacent WMO cache/queue/planner/registry/resolver/scene-cache/shutdown checks
passed; autonomous headless suite `60/60`; documentation `47`; coordination
passed with `34` historical warnings; checkpoint dry-run `7/7`.
- Remaining risks: terminal ResourceLoader polling remains a synchronous
main-thread boundary; no serialized private WMO, corrupt-cache, visual, leak,
descriptor-pressure or p95/p99 evidence was added.
- Documentation updated: new full module specification with API/I/O and
data-flow/state/sequence/dependency diagrams; adjacent WMO cache/world-renderer
specs, module registry, `RENDER.md` and M03 Evidence.
- Merge: `1acddab` (`merge: WMO render resource finalizer`)
- Post-merge: finalizer PASS `cases=24 iterations=1000 elapsed_ms=3.777`;
WMO cache/queue/planner/registry/resolver/scene-cache/shutdown, facade,
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation
`47` and coordination gates passed.
@@ -0,0 +1,78 @@
# M03-RND-WMO-RUNTIME-MESH-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-WMO-RUNTIME-MESH-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-wmo-runtime-mesh-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-runtime-mesh-finalizer`
## Outcome
Move cached WMO runtime Mesh material refresh versioning, surface iteration and
material-definition reconstruction from the loader into one service.
## Non-goals
- Change WMO traversal, placement, build-job scheduling or Node materialization.
- Change WMOBuilder shader/material behavior or cache formats.
- Change texture path ordering, metadata names or visible output.
- Generalize M2 and WMO finalization behind a shared abstraction.
## Paths
- Exclusive: `src/render/wmo/wmo_runtime_mesh_finalizer.gd`,
`src/tools/verify_wmo_runtime_mesh_finalizer.gd`,
`docs/modules/wmo-runtime-mesh-finalizer.md`, this claim
- Shared: loader, WMO build/cache and world renderer specs/verifiers,
`RENDER.md`, M03 Evidence
## Contracts and data
- Null meshes remain null; accepted meshes retain exact Resource identity.
- Refresh metadata version `10` remains the admission boundary.
- Only stale `ArrayMesh` surfaces with cached WMO texture metadata are rebuilt.
- Texture indices remain compact and ordered texture0, texture1, texture2.
- WMO flags, shader, blend mode and cached colors are forwarded unchanged.
- Scene traversal, builder material semantics and shutdown order are unchanged.
## Dependencies
- Requires: accepted WMO render and scene Resource finalizers
- Blocks: remaining loader-owned WMO runtime materialization cleanup
## Verification
- Synthetic identity/version/type/surface/metadata/color/definition/source/timing
contracts; adjacent WMO/renderer gates; full suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: next unclaimed M03 renderer extraction package
- Blocked by:
## Handoff
- Commit: `ae40e3f` (`render: extract WMO runtime Mesh finalizer`)
- Results: finalizer PASS `cases=27 iterations=1000 elapsed_ms=0.402` in the
full run; autonomous headless suite `63/63`; documentation `49`; coordination
passed with `34` historical warnings; checkpoint dry-run `7/7`.
- Remaining risks: WMOBuilder surface mutation remains synchronous main-thread
work; no private WMO corpus, visual comparison, leak/GPU or p95/p99 evidence.
- Documentation updated: new full module specification with API/I/O and
data-flow/state/sequence/dependency diagrams; renderer registry/source map,
`RENDER.md` and M03 Evidence.
- Merge: `d65ebee` (`merge: WMO runtime Mesh finalizer`)
- Post-merge: finalizer PASS `cases=27 iterations=1000 elapsed_ms=0.202`;
WMO scene/render cache/finalizer/queue/planner/registry/resolver, shutdown,
materials, facade, internal-access `30`, manifest and checkpoint dry-run
`7/7`, documentation `49` and coordination gates passed.
@@ -0,0 +1,80 @@
# M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
## Owner
- Agent ID: `sindo-main-codex`
- Target: M03 Renderer Facade and Safe Extraction
- Branch: `work/sindo-main-codex/m03-wmo-scene-resource-finalizer`
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-scene-resource-finalizer`
## Outcome
Move cached WMO PackedScene terminal ResourceLoader polling, probe validation,
probe lifetime and scene/missing publication from the loader into one service.
## Non-goals
- Change `.tscn` path selection, file-size admission or request start behavior.
- Own live fallback, placement/build jobs, attached Nodes or scheduler permits.
- Change WMOBuilder cache metadata/version rules or visible output.
- Merge PackedScene and lightweight render Resource finalization generically.
## Paths
- Exclusive: `src/render/wmo/wmo_scene_resource_finalizer.gd`,
`src/tools/verify_wmo_scene_resource_finalizer.gd`,
`docs/modules/wmo-scene-resource-finalizer.md`, this claim
- Shared: loader, WMO scene cache state spec/verifier, world renderer registry/spec,
`RENDER.md`, M03 Evidence
## Contracts and data
- Pending requests are polled in detached Dictionary insertion order.
- Only LOADED/FAILED statuses complete requests.
- Failed, null, non-PackedScene, non-Node3D and stale scenes publish missing.
- Accepted PackedScenes retain exact Resource identity.
- Validation instantiates once and releases accepted/rejected Node3D probes once.
- Admission, oversize logging, live fallback and shutdown order are unchanged.
## Dependencies
- Requires: accepted WMO scene Resource cache state
- Blocks: remaining loader-owned WMO live fallback/materialization cleanup
## Verification
- Synthetic status/order/type/probe/validation/lifetime/adoption/source/timing
contracts; adjacent WMO/renderer gates; full suite and checkpoint dry-run.
## Documentation deliverables
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
diagrams; adjacent renderer docs and M03 Evidence.
## Status
- State: accepted
- Done: implementation, verification and documentation
- Next: next unclaimed M03 renderer extraction package
- Blocked by:
## Handoff
- Commit: `7779e9e` (`render: extract WMO scene resource finalizer`)
- Results: finalizer PASS `cases=26 iterations=1000 elapsed_ms=4.186`;
adjacent WMO scene/render cache/finalizer/queue/planner/registry/resolver and
shutdown checks passed; autonomous headless suite `61/61`; documentation `48`;
coordination passed with `34` historical warnings; checkpoint dry-run `7/7`.
- Remaining risks: PackedScene probe validation remains synchronous main-thread
work; no serialized private WMO stale/oversize, long leak, visual,
descriptor-pressure or p95/p99 evidence was added.
- Documentation updated: new full module specification with API/I/O and
data-flow/state/sequence/dependency diagrams; adjacent WMO cache/world-renderer
specs, module registry, `RENDER.md` and M03 Evidence.
- Merge: `6a0f9bd` (`merge: WMO scene resource finalizer`)
- Post-merge: finalizer PASS `cases=26 iterations=1000 elapsed_ms=3.970`;
WMO scene/render cache/finalizer/queue/planner/registry/resolver/shutdown,
facade, internal-access `30`, manifest and checkpoint dry-run `7/7`,
documentation `48` and coordination gates passed.
+23
View File
@@ -21,12 +21,35 @@
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
| M2 build dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) |
| M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) |
| M2 static build resource observer | Implemented extraction | [`m2-static-build-resource-observer.md`](m2-static-build-resource-observer.md) |
| M2 cached animation resource observer | Implemented extraction | [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md) |
| M2 native animation resource observer | Implemented extraction | [`m2-native-animation-resource-observer.md`](m2-native-animation-resource-observer.md) |
| M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) |
| M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) |
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
| M2 animated scene finalizer | Implemented extraction | [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) |
| M2 animation playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) |
| M2 animated instance materializer | Implemented extraction | [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md) |
| M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) |
| M2 animation resource finalizer | Implemented extraction | [`m2-animation-resource-finalizer.md`](m2-animation-resource-finalizer.md) |
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
| M2 mesh resource finalizer | Implemented extraction | [`m2-mesh-resource-finalizer.md`](m2-mesh-resource-finalizer.md) |
| M2 runtime mesh finalizer | Implemented extraction | [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md) |
| M2 raw model repository | Implemented extraction | [`m2-raw-model-repository.md`](m2-raw-model-repository.md) |
| M2 prototype cache state | Implemented extraction | [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md) |
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
| WMO render build queue | Implemented extraction | [`wmo-render-build-queue.md`](wmo-render-build-queue.md) |
| WMO render Resource cache state | Implemented extraction | [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md) |
| WMO render Resource finalizer | Implemented extraction | [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) |
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
| WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) |
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
@@ -0,0 +1,231 @@
# M2 Animated Instance Materializer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001` |
| Owners | Animated M2 duplicate/batch construction, render settings, playback startup and attachment |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-instance-materializer`, 2026-07-18 |
| Profiles/capabilities | Imported GLB and native experimental animated M2 instances |
## Purpose
Materialize one ordered animated M2 build batch on the main thread. The service
duplicates an accepted prototype, preserves historical names and transforms,
applies visibility/shadow settings recursively, starts native/imported playback,
attaches a non-empty batch and returns detached native diagnostic entries.
## Non-goals
- Plan build batches, advance job cursors or consume render budgets.
- Load, finalize, select or cache animated prototypes.
- Select animations or implement native animator deformation.
- Format logs, normalize paths, assign Editor owners or clean up tile roots.
- Materialize static M2 `MultiMesh` batches.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader adapter] --> Materializer[M2AnimatedInstanceMaterializer]
Prototype[Accepted animated prototype] --> Materializer
Plan[Transforms and batch slice] --> Materializer
Materializer --> Playback[M2AnimationPlaybackController]
Materializer --> Finalizer[M2AnimatedSceneFinalizer traversal]
Materializer --> Batch[Attached animated batch]
Materializer --> Diagnostics[Detached diagnostic entries]
```
Allowed dependencies are Godot Node/Node3D/GeometryInstance3D APIs and the
accepted animated finalizer and playback services. Resource loading, files,
workers, caches, scheduler policy, Editor ownership and `MultiMesh` are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `materialize_batch(parent, path, prototype, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows, native_script, collect_diagnostics)` | Command/query | Build, start and attach one ordered animated batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input or all failed duplicates returns `{}` |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | M2 parent root | Loader build job | Batch attachment | Borrowed Node3D | Tile/job lifetime |
| Input | Relative path, start/count and serial | Batch planner/loader adapter | Names, phase and slice | Copied scalars | One call |
| Input | Accepted animated prototype | Prototype cache | Duplicate source | Borrowed Node3D | Cache-owned |
| Input | Ordered transforms | Placement grouping/build job | Instance transforms | Borrowed Array | Job lifetime |
| Input | Visibility/shadow settings | Renderer configuration | Recursive render mutation | Copied scalars | One call |
| Input | Native animator Script identity | Loader composition | Playback controller | Borrowed Script | Application lifetime |
| Output | `batch_root` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned Node3D | Tile lifetime |
| Output | `native_diagnostics` entries | Playback controller/materializer | Loader log adapter | Caller-owned Array/Dictionaries | Debug call |
Side effects are Node duplication, instance naming/transform assignment,
recursive geometry mutation, playback mutation and SceneTree attachment. The
service retains no batch, prototype, transform or diagnostic references.
## Data flow
```mermaid
flowchart TD
Slice[Ordered transform slice] --> Duplicate[Duplicate prototype with signals, groups and scripts]
Duplicate --> Identity[Assign historical name and transform]
Identity --> Render[Apply visibility margin and shadow recursively]
Render --> NativeCopy[Copy native animator runtime fields]
NativeCopy --> AttachInstance[Attach duplicate to detached batch]
AttachInstance --> Players[Inventory AnimationPlayers]
Players --> Start[Start deterministic native/imported playback]
Start --> More{More slice entries?}
More -->|yes| Duplicate
More -->|no, non-empty| AttachBatch[Attach batch to supplied parent]
Start --> Diagnostics[Tag detached states with instance index]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Validating
Validating --> Empty: invalid input
Validating --> Building: valid slice
Building --> Building: next successful duplicate
Building --> Empty: no successful duplicates
Building --> Attached: non-empty batch
Empty --> [*]
Attached --> [*]: parent/tile owner releases subtree
```
The `RefCounted` service is stateless; lifecycle labels describe each batch call.
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant M as M2AnimatedInstanceMaterializer
participant F as M2AnimatedSceneFinalizer
participant P as M2AnimationPlaybackController
participant R as M2 parent root
L->>M: materialize_batch(slice, prototype, render settings)
loop ordered instance
M->>M: duplicate, name, transform, render settings
M->>P: copy native animator fields
M->>F: animation_players_in_subtree(duplicate)
M->>P: start_instance_playback(path, index, players, debug)
end
M->>R: add_child(non-empty batch)
M-->>L: batch root and indexed diagnostics
L->>L: format logs and assign Editor owner
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer]
Materializer --> Playback[M2AnimationPlaybackController]
Materializer --> Finalizer[M2AnimatedSceneFinalizer]
Materializer --> Engine[Node3D / GeometryInstance3D / SceneTree APIs]
Loader --> EditorOwner[Editor ownership adapter]
Loader --> BuildJob[Build cursor and budget]
Materializer -. no dependency .-> Cache[Prototype/cache/load state]
Materializer -. no dependency .-> MultiMesh[Static M2 materialization]
```
## Ownership, threading and resources
- Loader/build state borrows the accepted prototype and ordered transform Array.
- Each successful duplicate becomes a child of the detached batch immediately.
- A non-empty batch transfers to the supplied parent; the parent owns its lifetime.
- A batch with no successful duplicates is queued for deletion and no reference returns.
- Playback/finalizer services borrow each duplicate only during synchronous calls.
- Returned diagnostic entries contain the stable instance index and a detached state.
- All Node duplication, mutation and attachment is main-thread-only.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null parent/prototype | Entry guard | Return `{}` without mutation | Contract verifier | Correct composition/cache state |
| Empty transforms or non-positive count | Entry guard | Return `{}` | Contract verifier | Planner supplies a non-empty slice |
| Duplicate is not Node3D | Cast result | Skip that entry and preserve order of successes | Child-count contract | Repair prototype scene root |
| Every duplicate fails | Empty batch check | Queue empty batch, return `{}` | Empty-result contract | Rebuild/import prototype |
| Shutdown during/after call | Outside service | Call is synchronous; loader owns cancellation and subtree release | Shutdown regression | Loader cancels job/releases parent |
Transform bounds remain a planner/build-job precondition exactly as before the
extraction; this service does not clamp or silently reorder an invalid slice.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Duplicate flags | Signals + groups + scripts | All | No | Preserves historical instance behavior |
| Visibility end/margin | Loader renderer settings / chunk size | All | Yes, between calls | Applied to every GeometryInstance3D descendant |
| Shadow mode | Loader `m2_cast_shadows` | All | Yes, between calls | Enables/disables descendant shadow casting |
| Native diagnostics | Loader `debug_streaming` | Debug | Yes | Collects indexed state only when requested |
## Persistence, cache and migration
No data is serialized and no cache/schema version changes. Editor owner
assignment remains a loader concern after attachment, so generated-scene
persistence behavior and migration remain unchanged.
## Diagnostics and observability
- The materializer emits no logs, metrics or debug views.
- Native state remains detached and is tagged with the absolute instance index.
- Loader preserves the existing normalized-path `M2_NATIVE_ANIMATOR` log text.
- No correlation ID is introduced; path/index remains the existing identity.
## Verification
- `verify_m2_animated_instance_materializer.gd` covers invalid input, names,
order, transforms, recursive visibility/margin/shadows, playback, native field
references, indexed diagnostics, prototype immutability and ownership boundaries.
- Playback/finalizer/pipeline/build/prototype/shutdown/material/facade/internal-
access regressions protect adjacent behavior.
- Fidelity evidence is exact extraction of existing flags/order/settings/calls;
no original-client visual comparison or proprietary asset result is claimed.
- Performance budget: 1,000 synthetic Node3D instances under one second.
## Extension points
A later facade-owned animated presentation command may supply the same explicit
batch contract. Async SceneTree mutation, callbacks and a generic scene factory
are intentionally excluded until measurements show a need.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Ordered animated instance/batch materialization | Implemented extraction | Synthetic batch contract verifier | Proprietary asset traversal pending |
| Recursive visibility/shadow settings | Implemented extraction | Nested geometry fixture | Visual distance/shadow comparison pending |
| Native/imported playback startup | Delegated to implemented controller | Playback + materializer verifiers | Full WoW animation-state mapping pending |
| Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate |
## Known gaps and risks
- The existing build planner must continue to guarantee valid transform bounds.
- Default animation selection and native deformation retain their documented gaps.
- No private asset, paired original-client, descriptor-pressure, leak or p95/p99 run exists.
- Recursive render mutation remains proportional to duplicate subtree size.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animated_instance_materializer.gd` | Duplicate, render mutation, playback startup and non-empty batch attachment |
| `src/render/m2/m2_animation_playback_controller.gd` | Phase, selection and native/imported playback mutation |
| `src/render/m2/m2_animated_scene_finalizer.gd` | AnimationPlayer inventory for each duplicate |
| `src/scenes/streaming/streaming_world_loader.gd` | Build adapter, diagnostic formatting and Editor ownership |
| `src/tools/verify_m2_animated_instance_materializer.gd` | Batch/render/playback/boundary/timing regression |
## Related decisions and references
- [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+212
View File
@@ -0,0 +1,212 @@
# M2 Animated Scene Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATED-SCENE-FINALIZER-001` |
| Owners | Detached animated candidate, material repair and player validation |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animated-scene-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
## Purpose
Finalize one loaded animated M2 scene on the main thread: instantiate a valid
detached Node3D candidate, copy the historical static-prototype material mapping
and accept only candidates containing AnimationPlayer descendants.
## Non-goals
- Call ResourceLoader or interpret threaded-load statuses.
- Choose animation eligibility, cache paths, permits or material prototypes.
- Adopt prototypes, mark static fallback or select/play animations.
- Process native M2 animators or static Mesh rebuilds.
## Context and boundaries
```mermaid
flowchart LR
ResourceFinalizer[M2AnimationResourceFinalizer] -->|loaded Resource and material source| Finalizer[M2AnimatedSceneFinalizer]
Finalizer -->|accepted Node3D and player count| ResourceFinalizer
ResourceFinalizer --> Cache[M2PrototypeCacheState]
```
Allowed dependencies are Godot scene/resource/material types. ResourceLoader,
filesystem, workers, scheduler, builders, raw repositories, prototype cache and
other application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `instantiate_candidate(resource)` | Ownership query | Instantiate PackedScene as detached Node3D | Main thread; caller receives valid candidate | Unsupported/invalid null; created wrong-type root freed |
| `repair_materials(animated_root, material_source_root)` | Command | Apply historical material overrides | Main thread; borrowed roots | Null/empty roots no-op |
| `finalize_candidate(candidate_root)` | Ownership command/query | Require AnimationPlayer and transfer root/count | Main thread; detached candidate | Null empty; rejected candidate freed |
| `mesh_instances_in_subtree(root)` | Query | Depth-first MeshInstance3D inventory | Main thread; borrowed subtree | Null returns empty |
| `animation_players_in_subtree(root)` | Query | Depth-first AnimationPlayer inventory | Main thread; borrowed subtree | Null returns empty |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Loaded Resource | Animation resource finalizer | Candidate instantiator | Borrowed Resource | One finalize permit |
| Input | Static material prototype Node3D | Loader cache/build adapter | Material repair | Borrowed Node | One repair call |
| Input | Detached animated candidate | Instantiator | Repair/final validation | Finalizer then caller/release | One attempt |
| Output | Accepted prototype and player count | Finalizer | Resource-finalizer adoption/log adapter | Exact Node transferred | Shutdown cache lifetime |
| Output | Depth-first engine-node arrays | Traversal | Loader preparation/playback | Borrowed references | One call |
Side effects are PackedScene instantiation, surface override assignment and
synchronous destruction of rejected detached roots.
## Data flow
```mermaid
flowchart TD
Resource[Loaded Resource] --> Packed{PackedScene?}
Packed -->|no| Reject[Return null]
Packed -->|yes| Instantiate[Instantiate]
Instantiate --> Root{Node3D?}
Root -->|no| FreeWrong[Free root and reject]
Root -->|yes| Repair[Repair materials]
Repair --> Players{AnimationPlayer exists?}
Players -->|no| FreeCandidate[Free and reject]
Players -->|yes| Transfer[Transfer exact root and count]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Candidate: instantiate valid scene
Candidate --> Candidate: repair materials
Candidate --> Accepted: player found
Candidate --> Released: no player
Accepted --> [*]: ownership transferred
Released --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant R as M2AnimationResourceFinalizer
participant F as M2AnimatedSceneFinalizer
participant C as M2PrototypeCacheState
R->>F: instantiate_candidate(Resource)
F-->>R: detached Node3D or null
R->>F: repair_materials(candidate, source)
R->>F: finalize_candidate(candidate)
alt accepted
F-->>R: exact Node3D and player count
R->>C: adopt animated prototype
else rejected
F-->>R: empty; candidate freed
R->>C: mark animation static
end
```
## Dependency diagram
```mermaid
flowchart TB
ResourceFinalizer[M2AnimationResourceFinalizer] --> Finalizer[M2AnimatedSceneFinalizer]
Finalizer --> Engine[PackedScene / Node3D / Mesh / Material / AnimationPlayer]
ResourceFinalizer --> Resource[ResourceLoader]
Loader --> Budget[RenderBudgetScheduler]
ResourceFinalizer --> Prototype[M2PrototypeCacheState]
Finalizer -. no dependency .-> Resource
Finalizer -. no dependency .-> Budget
Finalizer -. no dependency .-> Prototype
```
## Ownership, threading and resources
- Every method runs synchronously on the renderer main thread.
- A valid candidate is detached and finalizer-owned until acceptance.
- Acceptance transfers the exact Node3D to the resource-finalizer/cache path.
- Rejection frees the candidate synchronously, including wrong-type roots.
- Traversal results borrow Nodes; source materials remain Resource-owned.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null/unsupported Resource | Type guard | Return null | Dedicated verifier | Resource finalizer marks static |
| Wrong root type | Instantiated type guard | Free and return null | Node-count regression | Rebuild cache |
| No material source/meshes | Null/empty traversal | Leave materials unchanged | Material fixture | Imported materials remain |
| Missing source surface | Material lookup | First source material fallback | Mapping fixture | Repair static source |
| No AnimationPlayer | Descendant inventory | Free and return empty | Lifetime regression | Resource finalizer marks static |
| Shutdown/cancellation | Not owned | No retained state | N/A | Loader drains first |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Finalize permit | `m2_animation_finalize_ops_per_tick = 1` | Existing profiles | Yes | Caller bounds calls |
| Material mapping | Depth-first/index-clamped | All | No | Preserves current appearance |
| Required players | At least one | All | No | Rejects non-animated scenes |
## Persistence, cache and migration
The service retains no state and serializes nothing. GLB/cache formats, material
versions and prototype lifetimes are unchanged; no rebake is required.
## Diagnostics and observability
- Accepted result reports player count for the existing `M2_ANIM_CACHE` log.
- Traversal order is deterministic depth-first preorder.
- The service owns no logging or metrics.
## Verification
- `verify_m2_animated_scene_finalizer.gd` covers invalid Resources, wrong-root
release, exact transfer, player traversal/count, override priority, fallback,
source-index mapping, source boundaries and timing.
- Adjacent animation pipeline/prototype/shutdown/material/build tests protect callers.
- Fidelity evidence is exact mapping/lifecycle extraction; no asset-backed or
original-client visual parity claim is made.
- Performance budget: 10,000 depth-eight player inventories under one second.
## Extension points
Animation playback policy and native animator field copying now belong to
`M2AnimationPlaybackController`; accepted instance/batch materialization is owned
by `M2AnimatedInstanceMaterializer`.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| PackedScene candidate | Implemented extraction | Type/lifetime fixture | Proprietary corrupt corpus pending |
| Material repair | Implemented extraction | Exact-reference mapping | Asset-backed comparison pending |
| Player validation | Implemented extraction | Nested traversal/transfer | Multi-player GLB corpus pending |
## Known gaps and risks
- Material matching remains positional rather than semantic.
- Accepted prototypes remain unbounded until final loader shutdown.
- No private traversal, descriptor-pressure, p95/p99 or paired-client run exists.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate ownership, traversal, material repair and validation |
| `src/render/m2/m2_animation_playback_controller.gd` | Accepted-instance playback and native animator startup |
| `src/render/m2/m2_animated_instance_materializer.gd` | Per-duplicate player inventory consumer and batch owner |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending/terminal records before finalization |
| `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only outcomes |
| `src/render/m2/m2_animation_resource_finalizer.gd` | I/O, candidate composition, adoption and logs |
| `src/scenes/streaming/streaming_world_loader.gd` | Permits and material-source lookup |
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Scene/material/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,231 @@
# M2 Animation Load Pipeline State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATION-LOAD-PIPELINE-001` |
| Owners | Animated M2 threaded request records and terminal finalize FIFO |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-load-pipeline`, 2026-07-17 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
## Purpose
Own cross-frame bookkeeping between a successful animated M2 ResourceLoader
request, terminal polling and budgeted main-thread scene finalization. This is
an exact state extraction; cached animation eligibility and request admission
belong to `M2CachedAnimationResourceObserver`; terminal I/O/outcomes belong to
`M2AnimationResourceFinalizer`, while validation and retained Node lifetime
belong to `M2AnimatedSceneFinalizer` and `M2PrototypeCacheState`.
## Non-goals
- Select cache paths, interpret ResourceLoader statuses or perform I/O.
- Decide animated/static fallback or own prototype Nodes.
- Instantiate PackedScenes, repair materials or consume render permits.
- Merge the distinct animated and static-Mesh pipelines.
## Context and boundaries
```mermaid
flowchart LR
Observer[M2CachedAnimationResourceObserver] --> IO[ResourceLoader request]
Observer --> State[M2AnimationLoadPipelineState]
Finalizer[M2AnimationResourceFinalizer] --> IO
Finalizer --> State
State -->|detached pending records| Finalizer
Finalizer -->|opaque terminal status| State
State -->|completion FIFO| Finalizer
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Budget[M2_ANIMATION_FINALIZE permit]
Loader --> Prototype[M2PrototypeCacheState]
```
Allowed dependencies are value containers, Strings and opaque integer statuses.
ResourceLoader, workers, scheduler, Node/Resource/Mesh ownership and other
renderer services are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `remember_request(normalized_relative_path, resource_path)` | Command/query | Insert one pending request | Renderer main thread; until transition/clear | Empty/duplicate false |
| `has_request(path)` | Query | Pending-request dedupe | Main thread | Empty/unknown false |
| `request_records_snapshot()` | Query | Detached pending records in insertion order | Main thread; caller-owned | None |
| `complete_request(path, terminal_status)` | Command/query | Move copied record into completion FIFO | Main thread after poll | Unknown false |
| `discard_request(path)` | Command/query | Remove without finalization | Main thread | Unknown false |
| `has_finalize_record()` / `pop_finalize_record()` | Query/command | Drain completion-order FIFO | Main-thread budget drain | Empty pop returns `{}` |
| `total_work_count()` | Query | Pending plus finalize metric | Main thread | None |
| `pending_request_count()` / `finalize_record_count()` | Query | Stage diagnostics | Main thread | None |
| `clear()` | Command | Drop bookkeeping after caller I/O drain/reset | Main thread | Idempotent; no I/O drain |
| `diagnostic_snapshot()` | Query | Detached paths/status records | Main thread | No Resources exposed |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized M2 path and GLB Resource path | Loader request adapter | Pending map | Copied Strings | Until completion/discard/clear |
| Input | Opaque terminal status | Resource finalizer | Finalize FIFO | Integer value | Until pop/clear |
| Output | Detached pending records | State | Resource finalizer/shutdown adapter | Caller-owned copies | One pass |
| Output | Oldest completion record | State | Resource finalizer | Transferred Dictionary | One finalize attempt |
| Output | Detached diagnostics | State | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
Side effects are limited to collection mutation and retaining String/integer values.
## Data flow
```mermaid
flowchart TD
Start[Successful threaded request] --> Remember[Remember request]
Remember --> Poll[Resource finalizer polls detached snapshot]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Poll
Terminal -->|yes| Complete[Complete with opaque status]
Complete --> FIFO[Finalize FIFO]
FIFO --> Permit{Permit available?}
Permit -->|no| FIFO
Permit -->|yes| Pop[Pop oldest record]
Pop --> Finalize[Resource finalizer loads/instantiates or marks static]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: remember
Pending --> TerminalQueued: complete
Pending --> Absent: discard or clear
TerminalQueued --> Absent: pop or clear
```
## Main sequence
```mermaid
sequenceDiagram
participant O as CachedAnimationObserver
participant L as StreamingWorldLoader
participant F as AnimationResourceFinalizer
participant R as ResourceLoader
participant S as M2AnimationLoadPipelineState
participant P as M2PrototypeCacheState
O->>R: load_threaded_request(GLB)
O->>S: remember_request(path, GLB)
loop frames
L->>F: poll terminal requests
F->>S: request_records_snapshot()
F->>R: load_threaded_get_status(GLB)
end
F->>S: complete_request(path, status)
L->>F: prepare after permit
F->>S: pop_finalize_record()
F->>R: load_threaded_get(GLB)
F->>P: adopt animated prototype or mark static
```
## Ownership, threading and resources
- Main thread serializes all mutation.
- State owns only request/finalize Dictionaries with copied paths and statuses.
- Loader drains pending ResourceLoader paths before orderly shutdown clear.
- Resource finalizer owns terminal I/O/outcomes and composes scene validation;
prototype state owns accepted detached Nodes and static-only outcomes.
## Dependency diagram
```mermaid
flowchart TB
Observer[M2CachedAnimationResourceObserver] --> State[M2AnimationLoadPipelineState]
Observer --> Resource[ResourceLoader request]
Finalizer[M2AnimationResourceFinalizer] --> State[M2AnimationLoadPipelineState]
Finalizer --> Resource
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Budget[RenderBudgetScheduler]
Loader --> Prototype[M2PrototypeCacheState]
State -. no dependency .-> Resource
State -. no dependency .-> Budget
State -. no dependency .-> Prototype
```
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller or request later |
| Request start/cache miss | Cached observer | No insertion; mark static | Observer contract | Cache correction/reload |
| Non-terminal status | Loader | Keep pending | Existing metric | Poll next frame |
| Failed terminal load | Popped status | Loader marks static | Existing behavior | Future map/session reload |
| Empty defensive path | Loader poll | Discard and mark static | Source contract | Correct producer |
| Shutdown | Loader drains pending paths | Clear state | Source/shutdown regressions | New loader starts empty |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `enable_m2_animated_instances` | `true` | Existing renderer profile | Yes | Enables caller request path |
| `m2_animation_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds caller FIFO drain |
| Animated allow/deny/primitive rules | Existing values | Existing renderer profile | Yes | Observer filters before state insertion |
## Persistence, cache and migration
State is not serialized. Cache formats and prototype lifetimes are unchanged;
no rebake or migration is required.
## Diagnostics and observability
- `total_work_count()` preserves all three historical `m2_animation` metrics.
- Snapshots expose only paths and opaque statuses, never Resources or Nodes.
- Normalized M2 path is the correlation key; existing loader logs are unchanged.
## Verification
- `verify_m2_animation_load_pipeline_state.gd` covers validation, dedupe,
insertion order, completion FIFO, opaque status, discard, detached snapshots,
source boundaries and 100-by-256 timing.
- Adjacent renderer, scheduler, prototype and shutdown regressions cover callers.
- Fidelity evidence is exact state/lifecycle extraction; no visual 3.3.5a parity
or proprietary asset-backed claim is made.
- Performance budget: 25,600 request/complete/pop transitions under one second.
## Extension points
ResourceLoader polling/finalization now belongs to a sibling service without
changing this value-only state contract.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Animated request/finalize state | Implemented extraction | Synthetic contract/source/timing verifier | Asset-backed traversal/leak/p95/p99 pending |
| Cached request admission and GLB selection | Implemented in observer | Policy/GLB/source verifier | Asset-backed traversal pending |
| Terminal ResourceLoader polling | Implemented finalizer extraction | Status/order/source regressions | Asset-backed traversal pending |
| Animated prototype outcomes | Implemented extraction | Prototype cache verifier | Asset-backed animation fidelity pending |
## Known gaps and risks
- Dictionary records preserve the existing dynamic ResourceLoader boundary.
- `clear()` does not drain I/O; caller ordering remains mandatory.
- No private asset traversal, leak/descriptor-pressure or paired-client run is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Terminal polling, Resource load and prototype outcome |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Eligibility, GLB selection and request admission |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate instantiation, material repair and player validation |
| `src/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
| `src/scenes/streaming/streaming_world_loader.gd` | Permit loop, material lookup and composition |
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
## Related decisions and references
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-animation-resource-finalizer.md`](m2-animation-resource-finalizer.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,214 @@
# M2 Animation Playback Controller
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATION-PLAYBACK-001` |
| Owners | Per-instance AnimationPlayer/native animator playback mutation |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
| Profiles/capabilities | Imported GLB and native experimental animated M2 instances |
## Purpose
Apply deterministic playback to one already duplicated M2 instance: copy native
animator runtime fields, derive a stable phase, prepare/phase native animators,
select an imported animation and configure linear looping/playback/seek.
## Non-goals
- Duplicate, name, transform or attach instances/batch roots.
- Apply visibility, shadows or Editor ownership.
- Load/finalize/cache animated scenes or decide eligibility.
- Change animation priorities, phase formula or native animator implementation.
## Context and boundaries
```mermaid
flowchart LR
Prototype[Animated prototype] --> Materializer[M2AnimatedInstanceMaterializer]
Materializer -->|source and duplicate| Playback[M2AnimationPlaybackController]
Finalizer[M2AnimatedSceneFinalizer player inventory] --> Playback
Playback --> Native[M2NativeAnimator mutation]
Playback --> Imported[AnimationPlayer mutation]
```
Allowed dependencies are supplied Nodes, Script identity, AnimationPlayer and
Animation resources. ResourceLoader, files, workers, caches, scheduler,
MultiMesh, SceneTree attachment and application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `copy_native_animator_data(source_root, target_root, script)` | Command | Copy five runtime fields in depth-first pair order | Main thread; borrowed nodes | Null/script mismatch yields no pairs |
| `start_instance_playback(root, path, index, script, players, collect_diagnostics)` | Command/query | Start native/imported playback and optionally return native state | Main thread; one instance | Null/empty inventories no-op |
| `native_animators_in_subtree(root, script)` | Query | Exact-script depth-first inventory | Main thread; borrowed nodes | Null root/script returns empty |
| `phase_for_instance(path, index)` | Pure query | Stable phase in `[0, 1)` | Any thread; scalar | Historical hash behavior retained |
| `choose_default_animation(player, path)` | Pure engine query | Apply exact ordinary/fish/bird/fallback priority | Main thread; borrowed player | Null/no animations returns empty |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Prototype and duplicate subtrees | M2 animated instance materializer | Native field copy | Borrowed Nodes | One duplicate |
| Input | Exact native animator Script | Loader composition | Native inventory | Borrowed Script | Call-local |
| Input | Relative path and instance index | Build batch adapter | Phase/selection | Copied values | One start |
| Input | AnimationPlayer inventory | Animated scene finalizer traversal | Imported playback | Borrowed players | One start |
| Output | Mutated native/imported playback | Controller | Rendered instance | Nodes retain state/resources | Instance lifetime |
| Output | Detached native diagnostic records | Controller | Loader log adapter | Caller-owned Dictionaries | Debug call |
Side effects are native field assignment, prepare/phase calls, animation loop
mutation, play and seek. The service retains no inputs.
## Data flow
```mermaid
flowchart TD
Identity[Path and index] --> Phase[Stable hash phase]
NativeInventory[Exact-script native inventory] --> Prepare[prepare runtime if available]
Phase --> NativePhase[set native phase]
Players[AnimationPlayers] --> Select[Choose path-specific default]
Select --> Loop[Set every animation LOOP_LINEAR]
Loop --> Play[Play selected name]
Phase --> Seek[Seek positive-length selection]
NativePhase --> Diagnostics{Debug requested?}
Diagnostics -->|yes| Snapshot[Detached runtime state]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Unstarted
Unstarted --> Prepared: copy native data and start
Prepared --> Playing: native phase and/or AnimationPlayer play
Playing --> Playing: repeated deterministic start
Playing --> [*]: instance owner releases subtree
```
The controller itself is stateless; lifecycle labels describe borrowed instance state.
## Main sequence
```mermaid
sequenceDiagram
participant M as M2AnimatedInstanceMaterializer
participant F as M2AnimatedSceneFinalizer
participant P as M2AnimationPlaybackController
participant N as M2NativeAnimator
participant A as AnimationPlayer
M->>P: copy_native_animator_data(prototype, duplicate, script)
M->>F: animation_players_in_subtree(duplicate)
F-->>M: ordered players
M->>P: start_instance_playback(path, index, players, debug)
P->>N: prepare_runtime and set_phase
P->>A: choose, loop, play and seek
P-->>M: optional detached native diagnostics
M-->>M: tag states with instance index
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Materializer[M2AnimatedInstanceMaterializer]
Materializer --> Playback[M2AnimationPlaybackController]
Materializer --> Finalizer[M2AnimatedSceneFinalizer]
Playback --> Engine[Node / Script / AnimationPlayer / Animation]
Materializer --> Batch[SceneTree batch materialization]
Loader --> NativeScript[M2NativeAnimator script]
Playback -. no dependency .-> ResourceLoader
Playback -. no dependency .-> Cache[M2PrototypeCacheState]
Playback -. no dependency .-> Scheduler[RenderBudgetScheduler]
```
## Ownership, threading and resources
- Materializer owns prototype duplication, instance/batch roots and SceneTree attachment.
- Materializer uses finalizer traversal for borrowed AnimationPlayer references.
- Controller borrows Nodes/Script/players only for the synchronous call.
- Native arrays are assigned by reference exactly as before extraction.
- Diagnostic Dictionaries are deep-duplicated before return.
- Main thread performs all engine-object mutation; pure phase math is thread-safe.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Missing roots/script | Inventory guard | No native copy/start | Contract fixture | Correct composition |
| Fewer target animators | Pair-count minimum | Copy available pairs only | Native copy fixture | Rebuild import/prototype |
| No matching name | Ordered fallback | Substring, first name, then empty | Selection fixtures | Add compatible animation |
| Empty animation selection | Empty result | Skip loop/play/seek for that player | Selection fixture | Static/native path continues |
| Zero-length selected animation | Length guard | Play without seek | Playback contract | Imported timing remains engine-owned |
| Cancellation/shutdown | Outside service | No retained state | N/A | Loader owns subtree release |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Phase buckets | 1000 | All | No | Stable per path/index desynchronization |
| Loop mode | `LOOP_LINEAR` | All | No | Every available imported animation loops |
| Native diagnostics | `debug_streaming` | Debug | Yes | Samples state only when requested |
## Persistence, cache and migration
No state or format is serialized. GLB/native cache formats, prototype lifetime
and material versions are unchanged; no rebake is required.
## Diagnostics and observability
- Optional native states remain detached and contain existing prepared,
processing, mesh, bone, surface and length fields.
- Loader retains `M2_NATIVE_ANIMATOR` log formatting and path normalization.
- Controller emits no logs or metrics.
## Verification
- `verify_m2_animation_playback_controller.gd` covers exact phase, ordinary/
fish/bird priorities, substring/first fallback, loop/play/seek, native exact-
script order, five-field copy, phase, detached diagnostics and boundaries.
- Finalizer/build/prototype/material/shutdown regressions protect adjacent behavior.
- Fidelity evidence is exact policy/mutation extraction; no private asset or
original-client animation comparison is claimed.
- Performance budget: 20,000 phase-and-selection pairs under one second.
## Extension points
Server-driven animation state may later replace default selection through a
separate presentation contract; this fallback controller must remain available
for world doodads and compatibility fixtures.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Imported default selection/playback | Implemented extraction | Synthetic priority/play/seek verifier | Asset-backed animation-name corpus pending |
| Native data/phase startup | Implemented extraction | Exact-reference/native state verifier | Native visual fidelity remains experimental |
| Instance/batch materialization | Implemented extraction | Materializer contract verifier | Asset-backed traversal pending |
## Known gaps and risks
- Hash phase intentionally depends on existing Godot String hashing behavior.
- Default-name heuristics are not a complete WoW animation-state mapping.
- No proprietary traversal, animation timing comparison, p95/p99 or paired-client run exists.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animation_playback_controller.gd` | Phase, selection, native copy/start and imported playback |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Accepted prototype/player inventory |
| `src/scenes/streaming/m2_native_animator.gd` | Experimental native deformation runtime |
| `src/render/m2/m2_animated_instance_materializer.gd` | Instance duplication/attachment and playback composition |
| `src/scenes/streaming/streaming_world_loader.gd` | Build adapter, diagnostic formatting and Editor ownership |
| `src/tools/verify_m2_animation_playback_controller.gd` | Policy/mutation/boundary/timing regression |
## Related decisions and references
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,249 @@
# M2 Animation Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001` |
| Owners | Cached animated Resource polling, terminal load and prototype outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
## Purpose
Drain cached animated M2 ResourceLoader work without keeping terminal I/O and
prototype outcome logic in `StreamingWorldLoader`. The service polls pending
requests, prepares a detached scene candidate and completes material repair,
validation, cache adoption or static-only fallback.
## Non-goals
- Select cached GLB paths, apply allow/deny policy or admit threaded requests.
- Select/build native animated M2 prototypes.
- Look up the static material prototype used for repair.
- Own render permits, build jobs, playback, instances or SceneTree roots.
- Change ResourceLoader ordering, cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Observer[M2CachedAnimationResourceObserver] --> Pipeline[M2AnimationLoadPipelineState]
Loader[StreamingWorldLoader] --> Finalizer[M2AnimationResourceFinalizer]
Pipeline --> Finalizer
ResourceLoader --> Finalizer
Finalizer --> SceneFinalizer[M2AnimatedSceneFinalizer]
Finalizer --> Cache[M2PrototypeCacheState]
Loader --> Material[Material prototype lookup]
Material --> Finalizer
```
The two-phase API preserves the historical ordering: a loaded Resource must
instantiate a candidate before the loader performs material-prototype lookup.
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `poll_terminal_requests(pipeline, cache)` | I/O command | Move LOADED/FAILED requests to completion FIFO | Invalid composition returns zero |
| `prepare_next_candidate(pipeline, cache)` | I/O command/query | Pop one record, load Resource and instantiate candidate | Skip/failed/invalid outcome returns empty Dictionary |
| `finalize_prepared_candidate(preparation, material_source, cache, debug)` | Command/query | Repair, validate, adopt/log or mark static-only | Invalid/finalization failure returns null |
| `load_threaded_get_status(path)` | I/O adapter | Production status query with injectable test seam | Returns ResourceLoader status |
| `load_threaded_get(path)` | I/O adapter | Production terminal Resource retrieval with injectable test seam | May return null |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Pending/finalize records | Animation pipeline | Finalizer | Pipeline-owned copied Dictionaries |
| Input | ResourceLoader status/Resource | Godot ResourceLoader | Finalizer | Resource reference for one operation |
| Input | Material source root | Loader lookup | Scene finalizer | Borrowed exact Node3D |
| Input | Prototype/static-only state | Prototype cache | Finalizer | Borrowed service; loader session |
| Output | Prepared candidate Dictionary | Finalizer | Loader/finalizer completion | Caller-owned, immediate operation |
| Output | Canonical animated prototype | Prototype cache | Loader build path | Borrowed exact Node3D |
| Output | Static-only outcome | Finalizer | Prototype cache | Copied path; loader session |
| Output | Success diagnostic | Finalizer | Runtime log | Debug-gated one-line record |
## Data flow
```mermaid
flowchart TD
Poll[Poll pending records in insertion order] --> Path{Resource path empty?}
Path -->|yes| Discard[Discard and mark static-only]
Path -->|no| Status{LOADED or FAILED?}
Status -->|no| Pending[Keep pending]
Status -->|yes| Complete[Append completion FIFO]
Complete --> Permit[Loader consumes one finalize permit]
Permit --> Pop[Pop oldest terminal record]
Pop --> Existing{Empty/cached/static-only?}
Existing -->|yes| Skip[Finish permit operation]
Existing -->|no| Loaded{Status LOADED?}
Loaded -->|no| Mark[Mark static-only]
Loaded -->|yes| Get[Get terminal Resource]
Get --> Instantiate[Instantiate detached candidate]
Instantiate --> Valid{Candidate exists?}
Valid -->|no| Mark
Valid -->|yes| Material[Loader resolves material source]
Material --> Repair[Repair and validate]
Repair --> Prototype{Prototype accepted?}
Prototype -->|no| Mark
Prototype -->|yes| Adopt[Adopt and optionally log]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: nonterminal status
Polling --> TerminalQueued: LOADED or FAILED
Polling --> StaticOnly: empty Resource path
TerminalQueued --> Skipped: cached/static/invalid record
TerminalQueued --> StaticOnly: failed load or invalid candidate
TerminalQueued --> Prepared: detached candidate
Prepared --> Adopted: repair/finalize succeeds
Prepared --> StaticOnly: validation fails
Pending --> [*]
Skipped --> [*]
StaticOnly --> [*]
Adopted --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant F as AnimationResourceFinalizer
participant P as AnimationLoadPipelineState
participant R as ResourceLoader
participant S as AnimatedSceneFinalizer
participant C as PrototypeCacheState
L->>F: poll_terminal_requests(P, C)
F->>R: load_threaded_get_status(path)
F->>P: complete terminal request
L->>L: consume M2_ANIMATION_FINALIZE permit
L->>F: prepare_next_candidate(P, C)
F->>P: pop oldest finalize record
F->>R: load_threaded_get(path)
F->>S: instantiate_candidate(Resource)
alt candidate available
F-->>L: normalized/path/candidate
L->>L: resolve material prototype
L->>F: finalize_prepared_candidate(...)
F->>S: repair_materials + finalize_candidate
F->>C: adopt prototype or mark static-only
else rejected
F->>C: mark static-only
end
```
## Dependency diagram
```mermaid
flowchart TB
Finalizer[M2AnimationResourceFinalizer] --> ResourceLoader
Finalizer --> Pipeline[M2AnimationLoadPipelineState]
Finalizer --> SceneFinalizer[M2AnimatedSceneFinalizer]
Finalizer --> Cache[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Scheduler[RenderBudgetScheduler]
Loader --> MaterialLookup[Material prototype lookup]
Finalizer -. no dependency .-> Scheduler
Finalizer -. no dependency .-> BuildQueue[M2BuildQueue]
Finalizer -. no dependency .-> Playback[M2AnimationPlaybackController]
```
## Ownership, threading and resources
- All methods run synchronously on the renderer main thread.
- Pipeline owns pending and completion records; one loader permit pops one record.
- The prepared Dictionary temporarily owns the detached candidate reference. The
loader must call completion synchronously after material lookup.
- Existing scene finalizer frees rejected candidate roots. Prototype cache owns
accepted detached prototypes until final shutdown.
- Service retains only scene-finalizer and optional test-adapter references. The
production ResourceLoader path creates no self-reference or retained Resource.
- Loader retains scheduler permits, material lookup and all instance/SceneTree work.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Missing service dependency | Return zero/empty/null | Correct renderer composition |
| Empty Resource path | Discard pending request and mark static-only | Repair cache and start new session |
| Nonterminal status | Keep request pending | Poll next tick |
| FAILED terminal status | Pop one record and mark static-only | Repair cache and start new session |
| Existing cached/static state | Pop and skip without terminal Resource get | None |
| Null/unsupported Resource | Mark static-only | Repair imported cache |
| Candidate validation failure | Scene finalizer frees candidate; mark static-only | Repair animation/cache |
| Tile cancellation | No direct transition; shared cache work continues | Loader queue remains authoritative |
| Shutdown | Loader drains ResourceLoader before pipeline/cache clear | New loader starts empty |
## Configuration and capabilities
The module adds no setting. Existing cached-animation allow/deny policy,
`m2_animation_finalize_ops_per_tick`, debug flag and scheduler lane remain exact.
## Persistence, cache and migration
No persistence, schema or cache path changes are introduced. Existing `.glb`
imports and `pivot_prefix_v1` eligibility remain owned by the cached observer.
## Diagnostics and observability
Successful debug-enabled adoption emits the unchanged `M2_ANIM_CACHE` record
with normalized path, Resource cache path and AnimationPlayer count. Pipeline
work metrics remain pending plus completion FIFO size.
## Verification
- Dedicated verifier covers pending/loaded/failed polling order, empty path,
completion FIFO, cached/static skips, terminal Resource get, candidate identity,
material repair arguments, adoption, rejection, source ownership and timing.
- Pipeline, scene-finalizer, prototype-cache, cached-observer, shutdown, facade,
internal-access and baseline regressions protect adjacent behavior.
- Fidelity evidence is exact lifecycle/I/O extraction only; no private asset,
original-client animation, visual, leak-pressure or p95/p99 claim is made.
## Extension points
Material-prototype lookup may later become a dedicated resource observer, which
would allow the entire two-phase operation to compose outside the loader. Static
Mesh Resource finalization remains separate to avoid premature generalization.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Animated request terminal polling | Implemented extraction | Status/order/source verifier | Asset-backed long traversal pending |
| Animated candidate finalization/outcome | Implemented extraction | Identity/repair/adoption verifier | Private visual/leak/p95/p99 pending |
| Material prototype lookup | Loader-owned | Existing renderer regressions | Dedicated observer optional |
## Known gaps and risks
- Terminal Resource get, instantiation and repair remain synchronous main-thread work.
- Prepared candidate ownership relies on immediate completion by the loader adapter.
- Private asset traversal, descriptor pressure, leak and p95/p99 evidence is absent.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_animation_resource_finalizer.gd` | Poll/load/prepare/finalize/cache/log service |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending and completion FIFO ownership |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Candidate instantiation/repair/validation/rejection cleanup |
| `src/render/m2/m2_prototype_cache_state.gd` | Accepted prototype/static-only ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Permits, material lookup and synchronous composition |
| `src/tools/verify_m2_animation_resource_finalizer.gd` | Polling/finalization/boundary/timing regression |
## Related decisions and references
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+20 -9
View File
@@ -32,8 +32,8 @@ flowchart LR
Loader --> Planner
Planner --> Plan[Detached batch plan]
Plan --> Loader
Loader --> Ready{Resource ready?}
Ready --> Materialize[Animated or MultiMesh materialization]
Loader --> Dispatch[M2BuildDispatchPlanner]
Dispatch --> Materialize[Animated or MultiMesh materialization]
Loader --> Budget[RenderBudgetScheduler permit]
```
@@ -102,8 +102,10 @@ sequenceDiagram
## Ownership, threading and resources
- The planner owns only call-local scalar values and the returned Dictionary.
- The loader owns build-job Dictionaries, queue ordering, tile checks, resource
readiness/retry, serial numbers and group-index mutation.
- `M2BuildQueue` owns typed jobs, FIFO ordering, serial numbers and group/offset
cursors. `M2BuildDispatchPlanner` owns the pure resource-state action decision;
the loader owns tile checks and native-first orchestration, while static and
cached animation observers own their observation/retry phases.
- Materializers own main-thread Node/MultiMesh construction under loader roots.
- The scheduler owns the frame-local `M2_BUILD` counter.
- Pure planning is thread-safe, though the current adapter calls it on main thread.
@@ -149,8 +151,8 @@ queue depth, build activity and hitch observability.
## Extension points
- A later package may define typed build-job state once queue/resource transitions
are extracted together with explicit cancellation and recovery.
- Remaining native resource observation may be extracted while retaining the
dispatch and typed build-job/FIFO contracts.
- Spatial-cell batching must use measured culling/performance evidence and must
not silently change this model-path batch cursor.
@@ -159,12 +161,15 @@ queue depth, build activity and hitch observability.
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static/animated batch cursor planning | Implemented extraction | Contract/source/timing verifier | Asset-backed p95/p99 pending |
| Build queue/resource state machine | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Typed build queue/cursor state | Implemented extraction | M2 build queue lifecycle verifier | Asset-backed traversal pending |
| Resource dispatch decision | Implemented extraction | M2 build dispatch planner verifier | Asset-backed traversal pending |
| Resource observation/requests | Remains in loader | Existing lifecycle regressions | Stateful extraction pending |
| Spatial-cell batching | Planned | Renderer roadmap | Culling evidence/design pending |
## Known gaps and risks
- The result remains a Dictionary while loader build jobs are untyped Dictionaries.
- The planner result remains a Dictionary; `M2BuildQueue` adopts its scalar
cursor values through one explicit progress call.
- Negative offsets are preserved rather than rejected to avoid a hidden behavior change.
- Planning timing does not measure resource loading or materialization.
- Private asset-backed p95/p99 and visual comparison remain unavailable.
@@ -174,7 +179,12 @@ queue depth, build activity and hitch observability.
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, materialization and budgets |
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure observed-state action/transition planning |
| `src/render/m2/m2_build_queue.gd` | Typed pending jobs, FIFO order and cursor ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Tile checks, resource readiness, progress adoption, materializer adapters and budgets |
| `src/render/m2/m2_static_batch_materializer.gd` | Planned static-slice MultiMesh construction and attachment |
| `src/render/m2/m2_animation_playback_controller.gd` | Animated-instance phase/selection/native/imported playback |
| `src/render/m2/m2_animated_instance_materializer.gd` | Planned animated-slice duplication, playback startup and attachment |
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
| `src/tools/verify_m2_build_batch_planner.gd` | Formula, source and timing regression |
@@ -182,6 +192,7 @@ queue depth, build activity and hitch observability.
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+237
View File
@@ -0,0 +1,237 @@
# M2 Build Dispatch Planner
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-BUILD-DISPATCH-PLANNER-001` |
| Owners | Pure M2 resource-state to build-action decision |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-dispatch-planner`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Select one M2 build action after renderer observers produce animation/static
resource state. The planner makes wait priority, materializer selection and the
historical missing-model serial transition explicit without owning resources or
engine work.
## Non-goals
- Locate, request, load, cache or validate M2 resources.
- Size batches, own build jobs/queues or consume renderer permits.
- Create, attach or free Nodes, Meshes, MultiMeshes or animated instances.
- Change transforms, ordering, render settings, profiles or visible output.
- Add spatial-cell batching or M2 format/fidelity behavior.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader resource observations] --> Planner[M2BuildDispatchPlanner]
Planner --> Action[Detached action and transition plan]
Action --> Loader
Loader --> Queue[M2BuildQueue rotate/progress]
Loader --> Animated[M2AnimatedInstanceMaterializer]
Loader --> Static[M2StaticBatchMaterializer]
Loader --> Budget[RenderBudgetScheduler]
```
The planner depends only on `RefCounted`, scalar values, `StringName` constants
and a fresh Dictionary. ResourceLoader, caches, Nodes, Meshes, RenderingServer,
SceneTree, files, workers, mutexes, gameplay, network and Editor APIs are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `ACTION_WAIT_FOR_ANIMATION` | Constant | Pending animation request blocks all other actions | Immutable | None |
| `ACTION_MATERIALIZE_ANIMATED` | Constant | Build the selected slice from animated prototype | Immutable | None |
| `ACTION_WAIT_FOR_STATIC_MESH` | Constant | Static resource is unresolved and queue must rotate | Immutable | None |
| `ACTION_MATERIALIZE_STATIC` | Constant | Build the selected slice from prepared static Mesh | Immutable | None |
| `ACTION_ADVANCE_WITHOUT_MATERIALIZATION` | Constant | Empty or terminally missing slice advances without a Node | Immutable | None |
| `plan_step(batch_count, resource_snapshot)` | Pure query | Return action, queue-rotation and serial-transition values | Any thread; call-local result | Null snapshot waits for static Mesh; non-positive count advances |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Planned batch count | `M2BuildBatchPlanner` through loader | Dispatch planner | Integer copy | One call |
| Input | Typed animated/static availability, pending and missing observations | Loader / `M2BuildResourceSnapshot` | Dispatch planner | Borrowed snapshot; engine references are not read | One call |
| Output | Action | Dispatch planner | Loader materializer/wait branch | Immutable StringName | One operation |
| Output | `rotate_queue` | Dispatch planner | Loader queue adapter | Detached boolean | One operation |
| Output | `increment_batch_serial` | Dispatch planner | Loader progress adapter | Detached boolean | One operation |
The output Dictionary is newly allocated and caller-owned. The planner retains
no state, resource or engine-object reference.
## Data flow
```mermaid
flowchart TD
Input[Observed state] --> AnimationPending{Animation request pending?}
AnimationPending -->|yes| WaitAnimation[Wait animation; rotate]
AnimationPending -->|no| Positive{Batch count positive?}
Positive -->|no| AdvanceEmpty[Advance; keep serial]
Positive -->|yes| Animated{Animated prototype?}
Animated -->|yes| BuildAnimated[Materialize animated; increment serial]
Animated -->|no| StaticReady{Static Mesh ready?}
StaticReady -->|yes| BuildStatic[Materialize static; increment serial]
StaticReady -->|no| Missing{Model terminally missing?}
Missing -->|yes| AdvanceMissing[Advance without Node; increment serial]
Missing -->|no| WaitStatic[Wait static Mesh; rotate]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observe
Observe --> Waiting: animation/static unresolved
Observe --> Materializing: animated/static resource ready
Observe --> Advancing: empty or terminally missing batch
Waiting --> [*]: detached wait plan
Materializing --> [*]: detached materialization plan
Advancing --> [*]: detached advance plan
```
The planner itself is stateless. Queue rotation, cursor adoption, cancellation,
map reset and shutdown remain external lifecycle transitions.
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant B as M2BuildBatchPlanner
participant D as M2BuildDispatchPlanner
participant Q as M2BuildQueue
participant M as M2 materializer
participant S as RenderBudgetScheduler
L->>L: observe animation request/prototype
alt animation not pending
L->>B: plan_batch
L->>L: lookup/request static Mesh when required
end
L->>D: plan_step(batch count, resource snapshot)
D-->>L: action, rotate flag, serial flag
alt wait action
L->>Q: rotate_front
else materialize action
L->>M: materialize selected slice
L->>Q: adopt progress and serial
else advance action
L->>Q: adopt progress and optional serial
end
L->>S: consume one M2_BUILD permit
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Dispatch[M2BuildDispatchPlanner]
Loader --> Batch[M2BuildBatchPlanner]
Loader --> Queue[M2BuildQueue]
Loader --> Caches[M2 cache/pipeline state]
Loader --> Materializers[M2 materializers]
Dispatch --> Snapshot[M2BuildResourceSnapshot accessors]
Dispatch --> Values[Integers + booleans + StringName]
Dispatch -. no dependency .-> Engine[Node / Mesh / ResourceLoader]
Dispatch -. no dependency .-> State[Queue / cache / scheduler state]
```
## Ownership, threading and resources
- Planner owns call-local comparisons and the returned Dictionary only.
- `M2BuildResourceSnapshot` owns the typed call-local observation contract;
dispatch reads availability/pending/missing accessors but no engine references.
- Loader owns native-first observation order, action execution and permit use;
native/static/cached-animation observers own their resource phases.
- `M2BuildQueue` owns pending jobs, FIFO keys and progress cursors.
- Materializers own main-thread scene construction under loader-owned roots.
- Pure calls are thread-safe; the current loader adapter calls on main thread.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Animation request pending | Boolean observation | Wait wins even if other flags are true | Priority fixture | Retry when queue returns front |
| Non-positive batch | Count comparison | Advance without serial increment | Zero/negative fixtures | Next group/cursor operation |
| Static Mesh unresolved | No Mesh and not missing | Rotate without serial increment | Retry fixture | Loader request/cache completes |
| Static model missing | Terminal missing flag | Advance without Node and increment serial | Missing fixture | Later group continues |
| Contradictory static ready/missing | Both true | Ready Mesh wins | Priority fixture | Loader normally prevents contradiction |
| Tile cancellation/root invalid | Outside planner | Loader cancels before dispatch | Queue/shutdown regressions | Eligible tile may requeue |
## Configuration and capabilities
The planner adds no setting. Existing animated/static batch limits, animation
enable flag, visibility/shadow settings and `M2_BUILD` permits remain external.
## Persistence, cache and migration
No state is serialized. No ADT/M2 cache format or version changes; no rebake or
migration is required.
## Diagnostics and observability
The planner emits no logs. Its verifier reports the action matrix, source
boundaries and bounded 20,000-call timing. Existing loader queue/hitch metrics
remain unchanged.
## Verification
- `verify_m2_build_dispatch_planner.gd` covers wait priority, zero/negative
counts, animated/static priority, unresolved retry, terminal missing, detached
results, loader/dependency boundaries and bounded timing.
- Queue, batch planner, materializers, caches, shutdown, facade, internal-access
and checkpoint tests protect adjacent behavior.
- Fidelity evidence is exact branch/transition extraction only. No private asset
or original-client visual comparison is claimed.
## Extension points
Static, cached animated and native animated observation now live behind sibling
services that produce `M2BuildResourceSnapshot`. Generic callbacks, signals and a shared state-machine
framework remain intentionally excluded.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| M2 build action selection | Implemented extraction | Priority/matrix/source/timing verifier | Asset-backed traversal pending |
| Static/cached/native animation observation | Implemented separately | Observer/cache/pipeline regressions | Asset-backed traversal pending |
| Queue/cursor state | Implemented separately | M2 build queue verifier | Asset-backed traversal pending |
| Materialization | Implemented separately | Static/animated materializer verifiers | GPU/p95/p99 evidence pending |
## Known gaps and risks
- Native prototype attempts remain synchronous through the observer; loader still
owns static request I/O and terminal animated ResourceLoader polling.
- Synthetic timing does not measure ResourceLoader, Node or GPU work.
- Private traversal, leak, visual and p95/p99 evidence remains unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_dispatch_planner.gd` | Pure action priority and transition plan |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observation contract |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animation observation/request phase |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation |
| `src/scenes/streaming/streaming_world_loader.gd` | Observation order, action execution, permits and engine lifetime |
| `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor plan |
| `src/render/m2/m2_build_queue.gd` | Pending jobs, FIFO and cursor ownership |
| `src/tools/verify_m2_build_dispatch_planner.gd` | Matrix, boundary and timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-build-queue.md`](m2-build-queue.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+265
View File
@@ -0,0 +1,265 @@
# M2 Build Queue
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-BUILD-QUEUE-001` |
| Owners | Pending M2 build-job records, FIFO tile keys and build cursors |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-queue`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Own typed pending M2 build state outside `StreamingWorldLoader`: a keyed job
retains its M2 root, grouped transforms, insertion-order group keys and three
progress cursors, while a separate FIFO retains scheduling order and stale keys.
## Non-goals
- Decide tile eligibility, detail radius, resource readiness or retries.
- Consume scheduler permits or plan batch sizes.
- Create, attach or free Nodes, Meshes, MultiMeshes or animated instances.
- Load/cache resources, group placements or mutate tile state.
- Change model order, transform order, render settings or visible output.
## Context and boundaries
```mermaid
flowchart LR
Groups[M2PlacementGrouper result] --> Loader[StreamingWorldLoader adapter]
Loader --> Queue[M2BuildQueue]
Queue --> Job[M2BuildJob]
Queue --> Loader
Loader --> Planner[M2BuildBatchPlanner]
Loader --> Dispatch[M2BuildDispatchPlanner]
Loader --> Static[M2StaticBatchMaterializer]
Loader --> Animated[M2AnimatedInstanceMaterializer]
Loader --> Scheduler[RenderBudgetScheduler]
```
Allowed dependencies are String, Dictionary, Array, RefCounted and a strong
Node3D reference. SceneTree destruction, materializers, Mesh/MultiMesh,
RenderingServer/RIDs, ResourceLoader/files, workers/mutexes, scheduler policy,
gameplay, network and Editor APIs are forbidden.
## Public API
### `M2BuildJob`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `tile_key()` | Query | Immutable keyed identity | Main thread; job lifetime | None |
| `root()` | Query | Borrow strongly retained M2 root | Main thread; no ownership transfer | May be externally invalidated |
| `groups()` | Query | Borrow exact grouped-transform Dictionary | Main thread; job lifetime | None |
| `group_keys()` | Query | Borrow enqueue-time insertion-order key snapshot | Main thread; job lifetime | None |
| `group_index()` | Query | Current model-path cursor | Main thread | None |
| `transform_offset()` | Query | Current transform cursor within group | Main thread | None |
| `batch_serial()` | Query | Current batch naming serial | Main thread | None |
| `adopt_progress(group_index, offset, serial)` | Command | Atomically replace all progress values | Main thread | Raw integer semantics retained |
| `diagnostic_snapshot()` | Query | Detached scalar/count state | Any serialized caller | Omits engine references |
### `M2BuildQueue`
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `enqueue(tile_key, root, groups)` | Command | Replace keyed job and append FIFO key | Main thread; until erase/clear | Invalid/empty input returns false |
| `has_pending/front_key/pop_front/rotate_front` | Commands/queries | Inspect, drain or rotate FIFO independently of job validity | Main thread | Empty returns false/empty String |
| `has_job/job_for` | Queries | Inspect keyed record, including stale-key distinction | Main thread | Missing returns false/null |
| `root_for/groups_for/group_keys_for` | Queries | Borrow current job inputs | Main thread | Stale returns null/empty collection |
| `group_index_for/transform_offset_for/batch_serial_for` | Queries | Read current progress | Main thread | Stale returns zero |
| `adopt_progress(tile_key, group_index, offset, serial)` | Command | Atomically update current job progress | Main thread | Stale returns false |
| `erase_job(tile_key)` | Command | Remove keyed job but preserve FIFO keys | Main thread | Unknown returns false |
| `job_keys()` | Query | Detached active-key snapshot for loader cleanup | Main thread | Empty returns empty Array |
| `clear()` | Command | Release all job and FIFO references | Main thread | Idempotent; never frees Nodes |
| `pending_count/active_job_count` | Queries | FIFO and keyed-job metrics | Main thread | None |
| `diagnostic_snapshot()` | Query | Detached FIFO and sorted job diagnostics | Main thread | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile key and M2 root | Loader group-result finalizer | Queue/job | Copied String; strong borrowed Node reference | Until erase/clear |
| Input | Grouped transforms Dictionary | M2PlacementGrouper through loader mailbox | Job | Exact strong reference | Job lifetime |
| Input | `groups.keys()` order | Job constructor | Loader group selection | Job-owned Array snapshot | Job lifetime |
| Input | Next group/offset/serial | Planner/materializer adapter | Job progress | Copied integers | Until next adoption |
| Output | Front/rotated/popped tile key | Queue | Loader scheduling loop | Copied String | One drain step |
| Output | Borrowed root/groups/group keys | Job/queue | Loader readiness/materialization | No ownership transfer | One drain step |
| Output | Detached diagnostics/key snapshots | Queue/job | Tests/metrics/cleanup adapter | Caller-owned collections | Call result |
Side effects are keyed/FIFO collection mutation, strong-reference retention and
cursor mutation. No engine object is created, destroyed or attached.
## Data flow
```mermaid
flowchart TD
Enqueue[Tile key, root and grouped transforms] --> Job[Create or replace M2BuildJob]
Enqueue --> FIFO[Append tile key to FIFO]
FIFO --> Front[Borrow front key]
Front --> Current{Current keyed job exists?}
Current -->|no| Pop[Pop stale key]
Current -->|yes| Loader[Loader eligibility/readiness/materialization]
Loader -->|resource pending| Rotate[Move front key to tail]
Loader -->|operation complete| Progress[Adopt group, offset and serial]
Loader -->|finish/cancel| Erase[Erase job only]
Erase --> Pop
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Queued: enqueue valid job and FIFO key
Queued --> Rotated: resource pending
Rotated --> Queued: returns to FIFO front
Queued --> Progressed: adopt progress
Progressed --> Queued: group remains
Queued --> StaleQueued: erase job before FIFO pop
Progressed --> StaleQueued: finish or cancel
StaleQueued --> Absent: pop stale key
Queued --> Absent: clear
StaleQueued --> Absent: clear
```
Duplicate enqueue replaces the current keyed job and adds another FIFO entry;
later erase can therefore leave multiple stale entries, matching historical raw state.
## Main sequence
```mermaid
sequenceDiagram
participant G as M2 group-result drain
participant Q as M2BuildQueue
participant L as StreamingWorldLoader
participant P as M2BuildBatchPlanner
participant M as M2 materializer
G->>Q: enqueue(tile key, root, groups)
L->>Q: front_key and has_job
Q-->>L: borrowed root/groups/keys and cursors
L->>P: plan selected transform slice
alt resource pending
L->>Q: rotate_front
else materializable
L->>M: materialize selected slice
L->>Q: adopt_progress(next index, offset, serial)
else cancelled or complete
L->>L: free/queue-free root if required
L->>Q: erase_job
L->>Q: pop_front
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Queue[M2BuildQueue]
Queue --> Job[M2BuildJob]
Job --> EngineBase[Node3D strong reference]
Loader --> Planner[M2BuildBatchPlanner]
Loader --> Materializers[Static and animated materializers]
Loader --> Scheduler[RenderBudgetScheduler]
Queue -. no dependency .-> SceneMutation[SceneTree destruction/attachment]
Queue -. no dependency .-> Resources[Mesh / MultiMesh / ResourceLoader]
```
## Ownership, threading and resources
- Queue owns keyed job records and FIFO String entries.
- Job retains the exact groups Dictionary, a fresh group-key snapshot and root.
- Root ownership remains with the loader/tile SceneTree; queue release never frees it.
- Loader validates `is_instance_valid`, frees empty/aborted roots and mutates tile state.
- `M2BuildDispatchPlanner` selects wait/materializer/advance actions without
borrowing queue-owned engine references.
- All current operations run on the renderer main thread; no mutex is required.
- Group worker results cross their existing mutex mailbox before enqueue.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Invalid enqueue input | Entry guard | Reject without mutation | Contract fixture | Correct caller composition |
| Duplicate tile key | Keyed assignment | Replace job and append FIFO key | Duplicate fixture | Stale entries drain normally |
| Stale FIFO key | `has_job == false` | Loader pops and continues without permit | Stale fixture | No action required |
| Resource pending | Loader cache state | Rotate one front key and consume existing permit | Rotation fixture/build diagnostics | Retry when key returns front |
| Root externally invalid | Loader validity check | Erase job and pop key | Loader source contract | Tile may regroup/requeue later |
| Tile cancelled | Loader policy | Queue-free root, erase job; FIFO key drains/pops | Shutdown/lifetime regression | Eligible tile may requeue |
| World clear | Loader job-key snapshot | Cancel roots, then clear queue | Shutdown regression | New map starts empty |
## Configuration and capabilities
The queue introduces no settings. Existing static/animated batch limits,
visibility/shadow settings, resource caches and `M2_BUILD` permits remain loader,
planner, materializer and scheduler contracts.
## Persistence, cache and migration
No state is serialized. No ADT/M2 cache format or version changes; no rebake or
migration is required.
## Diagnostics and observability
- `pending_count` preserves the existing FIFO-backed `m2_build` queue metric.
- `active_job_count` distinguishes keyed jobs from stale/duplicate FIFO entries.
- Diagnostic snapshots retain FIFO order and sort job records by tile key.
- Job diagnostics expose cursors/counts but never root/groups references.
- The module emits no logs.
## Verification
- `verify_m2_build_queue.gd` covers invalid input, exact groups/root retention,
FIFO, duplicate replacement, rotation, stale entries, atomic progress, erase/
clear engine lifetime, detached sorted diagnostics and loader boundaries.
- Planner, static/animated materializer, Mesh pipeline/cache/prototype, shutdown,
facade, internal-access and checkpoint regressions protect adjacent behavior.
- Fidelity evidence is exact state-transition extraction; visual rules are not changed.
- Performance budget: 100 cycles of 256 enqueue/rotate/erase operations under one second.
- No original-client or proprietary asset comparison is claimed for bookkeeping state.
## Extension points
Remaining resource readiness and dispatch logic may later move behind explicit
commands once its cancellation semantics are independently fixed. A generic
queue base, signals and callbacks are intentionally excluded.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Typed keyed M2 jobs and FIFO | Implemented extraction | Lifecycle/order/source/timing verifier | Asset-backed traversal pending |
| Cursor/serial ownership | Implemented extraction | Atomic progress fixtures | Typed batch-plan result remains Dictionary |
| Root destruction | Existing loader-owned | Lifetime/source/shutdown regressions | Keep outside state service |
| Resource dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending |
| Resource observation/requests | Existing loader-owned | Adjacent cache/build tests | Safe extraction remains |
## Known gaps and risks
- Grouped transforms remain untyped Dictionary/Array data.
- Queue permits duplicate and stale FIFO keys intentionally for compatibility.
- Job accessors return borrowed mutable groups/key collections; current loader is the sole consumer.
- Private asset traversal, long-flight p95/p99 and leak evidence remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_job.gd` | Strong root/groups references, key snapshot and progress cursors |
| `src/render/m2/m2_build_queue.gd` | Keyed job ownership, FIFO/stale/rotation lifecycle and diagnostics |
| `src/scenes/streaming/streaming_world_loader.gd` | Eligibility, readiness, permits, materialization, tile state and root cleanup |
| `src/render/m2/m2_build_batch_planner.gd` | Batch count and cursor-plan calculation |
| `src/render/m2/m2_build_dispatch_planner.gd` | Resource-state action and transition planning |
| `src/tools/verify_m2_build_queue.gd` | Lifecycle/order/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md)
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+239
View File
@@ -0,0 +1,239 @@
# M2 Build Resource Snapshot
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001` |
| Owners | Per-build-step observed M2 resource references and outcome flags |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-build-resource-snapshot`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh and animated-instance build paths |
## Purpose
Carry the resource observations for one M2 build operation as a typed contract:
normalized path, optional animated prototype, pending-animation state, optional
static Mesh and terminal missing-model state. The snapshot never owns engine lifetime.
## Non-goals
- Locate, request, load, validate or cache M2 resources.
- Decide dispatch action, batch count, queue order or permit consumption.
- Create, attach or free Nodes, Meshes, MultiMeshes or animated instances.
- Change animation allowlists, native candidate rules or cache paths/formats.
- Persist observations beyond one build operation.
## Context and boundaries
```mermaid
flowchart LR
Native[M2NativeAnimationResourceObserver] --> Snapshot[M2BuildResourceSnapshot]
Cached[M2CachedAnimationResourceObserver] --> Snapshot
Static[M2StaticBuildResourceObserver] --> Snapshot
Snapshot --> Dispatch[M2BuildDispatchPlanner]
Snapshot --> Loader
Loader --> Animated[M2AnimatedInstanceMaterializer]
Loader --> Static[M2StaticBatchMaterializer]
```
The snapshot depends only on `RefCounted`, String, booleans and borrowed Node3D/
Mesh references. ResourceLoader, cache/pipeline state, SceneTree mutation,
RenderingServer/RIDs, files, workers, mutexes, gameplay, network and Editor APIs
are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| Constructor `(normalized_path, animated_prototype, animation_pending)` | Command | Capture first-phase animation observation | Main thread; one build operation | Values retained exactly |
| `normalized_relative_path()` | Query | Return observed normalized path | Snapshot lifetime | None |
| `animated_prototype()` | Query | Borrow exact animated prototype | Main thread; no ownership transfer | May be null |
| `has_animated_prototype()` | Query | Report prototype availability | Any serialized caller | None |
| `animation_request_pending()` | Query | Report pending animation request | Any serialized caller | None |
| `adopt_static_observation(mesh, missing)` | Command | Replace second-phase static observation | Main thread | Values retained exactly, including contradictory inputs |
| `static_mesh()` | Query | Borrow exact prepared Mesh | Main thread; no ownership transfer | May be null |
| `has_static_mesh()` | Query | Report static Mesh availability | Any serialized caller | None |
| `static_model_missing()` | Query | Report terminal missing outcome | Any serialized caller | None |
| `diagnostic_snapshot()` | Query | Return detached path/availability flags | Any serialized caller | Omits engine references |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized relative path | Loader path adapter | Snapshot/diagnostics | Copied String | Snapshot lifetime |
| Input | Optional animated prototype | Native/cached animation observer | Snapshot/materializer adapter | Borrowed exact Node3D reference | One build operation |
| Input | Animation request pending | Cached animation observer | Snapshot/dispatch planner | Boolean copy | One build operation |
| Input | Optional prepared static Mesh | Mesh cache/request path | Snapshot/materializer adapter | Borrowed exact Mesh reference | One build operation |
| Input | Static model terminally missing | Prototype cache state | Snapshot/dispatch planner | Boolean copy | One build operation |
| Output | Availability/pending/missing values | Snapshot | Dispatch planner | Scalar reads | One dispatch plan |
| Output | Borrowed prototype or Mesh | Snapshot | Loader materializer adapter | No ownership transfer | One materialization call |
| Output | Detached diagnostics | Snapshot | Tests/future metrics | Caller-owned Dictionary | Call result |
The snapshot is released after the build-loop operation. Releasing or replacing
it never frees the borrowed Node3D or Mesh.
## Data flow
```mermaid
flowchart TD
Path[Normalized M2 path] --> Construct[Create snapshot]
Animated[Animated prototype or null] --> Construct
Pending[Animation request pending] --> Construct
Construct --> PendingCheck{Animation pending?}
PendingCheck -->|yes| Dispatch[Dispatch planner]
PendingCheck -->|no| Batch[Batch planner]
Batch --> StaticNeeded{Positive static batch?}
StaticNeeded -->|yes| Adopt[Adopt Mesh and missing observation]
StaticNeeded -->|no| Dispatch
Adopt --> Dispatch
Dispatch --> Borrow[Loader borrows selected resource]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> AnimationObserved: construct
AnimationObserved --> AnimationObserved: animation pending or animated path
AnimationObserved --> StaticObserved: adopt static result
StaticObserved --> StaticObserved: replace static result
AnimationObserved --> Released: operation ends
StaticObserved --> Released: operation ends
Released --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant S as M2BuildResourceSnapshot
participant B as M2BuildBatchPlanner
participant D as M2BuildDispatchPlanner
participant M as M2 materializer
L->>L: delegate native animated prototype observation
L->>S: construct native result
opt no native prototype
L->>L: delegate cached animation observer
L->>S: receive cached/pending snapshot
end
alt animation not pending
L->>B: plan batch using snapshot availability
opt positive static batch
L->>L: lookup/request static Mesh
L->>S: adopt_static_observation
end
end
L->>D: plan_step(batch count, snapshot)
D-->>L: action plan
opt materialization action
L->>S: borrow prototype or Mesh
L->>M: materialize selected slice
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Snapshot[M2BuildResourceSnapshot]
Dispatch[M2BuildDispatchPlanner] --> Snapshot
Snapshot --> Values[String and booleans]
Snapshot --> Borrowed[Borrowed Node3D and Mesh]
Snapshot -. no dependency .-> IO[ResourceLoader / FileAccess]
Snapshot -. no dependency .-> State[Cache / pipeline / queue / scheduler]
Snapshot -. no dependency .-> Mutation[SceneTree / RenderingServer]
```
## Ownership, threading and resources
- Snapshot owns only copied scalar state and temporary references.
- Prototype/cache services retain resource state; the native observer owns its
phase while loader owns native-first ordering among resource observers.
- Scene roots remain loader/tile-owned; Mesh lifetime follows cache/resource refs.
- Dispatch planner reads only snapshot accessors and never mutates the snapshot.
- Current construction/adoption occurs on renderer main thread.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path | Exact String retained | No validation or hidden normalization | Contract fixture | Loader already normalizes input |
| Null prototype/Mesh | Availability accessor false | Dispatch uses pending/missing state | Contract fixture | Loader request/cache path retries |
| Mesh and missing both true | Exact adoption | Both retained; dispatch gives ready Mesh priority | Contradiction fixture | Loader normally prevents combination |
| Static observation replaced | Explicit adoption | Latest Mesh/missing values win | Replacement fixture | None required |
| Borrowed Node externally freed | Outside snapshot | Loader validity/lifecycle gates remain authoritative | Shutdown regressions | Tile may cancel/requeue |
| Tile cancellation | Outside snapshot | Ephemeral snapshot releases references | Queue/shutdown regressions | Eligible tile may rebuild |
## Configuration and capabilities
The snapshot adds no setting. Existing animation enable/allow/deny rules, batch
limits, cache paths, render settings and scheduler permits remain external.
## Persistence, cache and migration
No observation is serialized. No ADT/M2 cache version changes and no rebake or
migration are required.
## Diagnostics and observability
`diagnostic_snapshot()` exposes normalized path and four availability/outcome
flags without Node/Mesh references. The module emits no logs. Existing loader
queue/hitch metrics remain unchanged.
## Verification
- `verify_m2_build_resource_snapshot.gd` covers exact path/reference identity,
defaults, static adoption/replacement/clear, contradictory flags, detached
diagnostics, engine lifetime, loader/dispatch boundaries and bounded timing.
- Dispatch, queue, batch, materializer, cache/pipeline, shutdown, facade,
internal-access and checkpoint regressions protect adjacent behavior.
- Fidelity evidence is exact observation transfer only; no private asset or
original-client visual claim is made.
## Extension points
`M2CachedAnimationResourceObserver` produces the cached animated phase and
`M2StaticBuildResourceObserver` produces the static phase. Native animation is
produced by `M2NativeAnimationResourceObserver`. The value stays independent of all
producers and of generic callback frameworks.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Typed per-step M2 resource observation | Implemented extraction | Identity/adoption/lifetime/source/timing verifier | Asset-backed traversal pending |
| Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending |
| Static lookup/request execution | Implemented separately | Static observer verifier | Asset-backed traversal pending |
| Cached animated lookup/request execution | Implemented separately | Cached observer verifier | Asset-backed traversal pending |
| Native animated lookup/build execution | Implemented observer extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending |
| Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending |
## Known gaps and risks
- Static observation is mutable during its short two-phase construction.
- Raw Node3D/Mesh references remain necessary for current materializer APIs.
- Native parsing/build remains synchronous; loader still owns terminal polling.
- Synthetic timing does not measure resource, Node or GPU work.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observations and diagnostics |
| `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning |
| `src/render/m2/m2_static_build_resource_observer.gd` | Static snapshot production and requests |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animated snapshot production and requests |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native prototype observation producer |
| `src/scenes/streaming/streaming_world_loader.gd` | Observer ordering and materializer borrowing |
| `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md)
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-build-queue.md`](m2-build-queue.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,242 @@
# M2 Cached Animation Resource Observer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001` |
| Owners | Cached animated M2 eligibility, request admission and initial snapshot |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-cached-animation-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
## Purpose
Produce the cached-animation phase of `M2BuildResourceSnapshot`: reuse an exact
animated prototype, wait for a pending request, admit the first safe GLB request,
or record a terminal static-only animation outcome without changing behavior.
## Non-goals
- Load/build native GryphonRoost animation or emit its diagnostics.
- Poll/finalize threaded Resources or instantiate/fix imported scenes.
- Own/free prototype Nodes, snapshots, pipeline entries or cache state.
- Materialize instances, consume permits or change animation policy defaults.
- Change GLB layout, cache format, batching, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> Native[M2NativeAnimationResourceObserver]
Native -->|no prototype| Observer[M2CachedAnimationResourceObserver]
Prototype[M2PrototypeCacheState] --> Observer
Pipeline[M2AnimationLoadPipelineState] --> Observer
Resource[ResourceLoader and GLB JSON] --> Observer
Observer --> Snapshot[M2BuildResourceSnapshot]
Snapshot --> Dispatch[M2BuildDispatchPlanner]
```
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `observe(path, cache_dir, max_primitives, allow, deny, prototype_state, pipeline_state, debug)` | Command/query | Produce cached prototype/pending/static-only snapshot | Invalid input returns empty non-pending snapshot |
| `find_eligible_glb_cache_path(...)` | I/O query | Select first existing safe historical candidate | Returns empty String |
| `is_animation_path_allowed(path, allow, deny)` | Pure query | Apply stripped case-insensitive substring policy | Empty path/allowlist rejects; deny wins |
| `cache_resource_paths(cache_dir, path)` | Pure query | Return nested/lowercase/basename GLB candidates | Empty values yield fewer candidates |
| `glb_cache_is_safe_for_runtime_animation(path, max, debug)` | I/O query | Validate animations, primitive limit and schema | Invalid/missing GLB rejects |
| `read_glb_json(path)` | I/O query | Read version-2 GLB JSON chunk | Returns empty Dictionary |
| `glb_primitive_count(gltf)` | Pure query | Count primitives across meshes | Invalid entries contribute zero |
| `glb_animation_schema(gltf)` | Pure query | Read OpenWC schema marker | Missing marker returns empty String |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized M2 path/cache directory | Loader | Observer | Copied Strings; one observation |
| Input | Primitive cap and allow/deny patterns | Loader exports | Observer | Copied/read-only call values |
| Input | Prototype and request states | M2 state services | Observer | Borrowed services; map/session |
| Input | Existing GLB cache file | Offline cache pipeline | Observer | Read-only file access |
| Output | Resource snapshot | Observer | Loader/dispatch/materializer | Caller-owned snapshot; one build step |
| Output | Borrowed exact prototype | Prototype state | Snapshot/materializer | No ownership transfer |
| Output | Threaded request record | Observer | Animation pipeline | Pipeline-owned copied paths |
| Output | Static-only transition | Observer | Prototype state | State-owned copied path |
## Data flow
```mermaid
flowchart TD
Start[Observe normalized path] --> Valid{Valid composition?}
Valid -->|no| Empty[Empty non-pending snapshot]
Valid -->|yes| Cached{Prototype cached?}
Cached -->|yes| Ready[Snapshot with exact prototype]
Cached -->|no| Static{Already static-only?}
Static -->|yes| Empty
Static -->|no| Pending{Request exists?}
Pending -->|yes| Wait[Pending snapshot]
Pending -->|no| Policy{Allow and not deny?}
Policy -->|no| Mark[Mark static-only]
Policy -->|yes| Candidate[Historical GLB candidates]
Candidate --> Safe{Animations, primitive cap, schema safe?}
Safe -->|no usable| Mark
Safe -->|yes| Request[Threaded request]
Request -->|OK or busy| Remember[Remember and return pending snapshot]
Request -->|error| Mark
Mark --> Empty
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observing
Observing --> Cached
Observing --> Pending
Observing --> Requested
Observing --> StaticOnly
Observing --> Rejected
Cached --> [*]
Pending --> [*]
Requested --> [*]
StaticOnly --> [*]
Rejected --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant O as CachedAnimationObserver
participant C as PrototypeCacheState
participant P as AnimationLoadPipelineState
participant R as ResourceLoader
participant S as M2BuildResourceSnapshot
L->>L: attempt native candidate
alt no native prototype
L->>O: observe(path, cache/policy/state)
O->>C: find animated / static-only
O->>P: has request
opt eligible cache candidate
O->>R: exists + GLB JSON + threaded request
O->>P: remember request
end
O-->>L: snapshot
else native prototype
L->>S: construct native snapshot
end
```
## Dependency diagram
```mermaid
flowchart TB
Observer[M2CachedAnimationResourceObserver] --> Snapshot[M2BuildResourceSnapshot]
Observer --> Prototype[M2PrototypeCacheState]
Observer --> Pipeline[M2AnimationLoadPipelineState]
Observer --> ResourceLoader
Observer --> FileAccess[GLB header and JSON]
Observer -. no dependency .-> Native[M2 native builder/repository]
Observer -. no dependency .-> Finalizer[M2AnimatedSceneFinalizer]
Observer -. no dependency .-> Scheduler[RenderBudgetScheduler]
Observer -. no dependency .-> SceneTree
```
## Ownership, threading and resources
- Observer retains no service, request, snapshot, Resource or Node reference.
- Prototype state retains accepted Nodes and static-only paths until shutdown.
- Pipeline state retains copied request paths until terminal polling/finalize.
- Snapshot borrows the exact cached prototype; observer never frees it.
- Resource existence/request and synchronous GLB JSON inspection run on the
renderer main thread, matching the previous loader behavior.
- The sibling native observer owns the native build attempt and the animation
resource finalizer owns terminal polling/finalize. Loader retains permits,
material lookup, materialization and SceneTree mutation.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Empty path/dependency | Return empty non-pending snapshot without mutation | Correct composition |
| Cached prototype | Return exact reference immediately | None |
| Existing request | Return pending without duplicate admission | Retry after terminal drain |
| Disallowed/denied path | Mark static-only | New loader session/configuration |
| Missing/invalid/unsafe GLB | Continue candidates, then mark static-only | Repair/rebake cache and restart session |
| Request error | Mark static-only | New loader session/cache repair |
| Tile cancellation | Observer retains nothing | Loader queue remains authoritative |
| Shutdown | Loader drains requests before state clear | New loader starts empty |
## Configuration and capabilities
The service consumes existing `m2_cache_dir`, `m2_animated_max_primitives`,
`m2_animated_allowlist_patterns`, `m2_animated_denylist_patterns` and debug flag.
It adds no setting and preserves runtime mutability and defaults.
## Persistence, cache and migration
No persistence or cache format changes are introduced. Existing `.glb` candidate
layout and OpenWC schema markers are read unchanged; no rebake is required.
## Diagnostics and observability
Unsupported non-empty schemas preserve the historical optional
`M2_ANIM_REJECT` debug line. Snapshot and pipeline diagnostics remain detached;
queue and hitch metrics are unchanged.
## Verification
- Dedicated verifier covers invalid/cached identity/static/pending/missing
lifecycle, allow/deny semantics, exact path order, generated GLB header/JSON,
animation presence, primitive cap, accepted/rejected schemas, source boundaries
and bounded timing.
- Snapshot, dispatch, pipeline, prototype, shutdown, facade, internal-access and
checkpoint regressions protect adjacent behavior.
- Fidelity evidence is exact policy/I/O extraction. No original-client visual or
animation parity claim is made.
## Extension points
Native and cached observation remain sibling services. Terminal ResourceLoader
polling/finalization uses the dedicated finalizer while retaining pipeline state.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached animation observation/request | Implemented extraction | Lifecycle/policy/GLB/source/timing verifier | Asset-backed traversal pending |
| Native animation observation | Implemented sibling extraction | Native observer lifecycle/source verifier | Asset-backed traversal pending |
| Animated finalize/preparation | Implemented finalizer extraction | Pipeline/finalizer regressions | Asset-backed traversal pending |
## Known gaps and risks
- A successful asynchronous request is not started by the unit fixture because
leaving it undrained leaks process work; source and pipeline tests cover admission.
- Generated GLB metadata fixtures validate policy, not Godot import fidelity.
- Private traversal, leak/descriptor pressure, p95/p99 and paired visuals remain
unavailable without private assets.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached GLB policy, request and snapshot production |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step observation value |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending request/finalize state |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Terminal polling/load/finalize outcomes |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and static-only outcomes |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native-first candidate/read/build/cache observation |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, fallback order, finalize and execution |
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Lifecycle/policy/GLB/boundary/timing regression |
## Related decisions and references
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-native-animation-resource-observer.md`](m2-native-animation-resource-observer.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+250
View File
@@ -0,0 +1,250 @@
# M2 Mesh Load Pipeline State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-MESH-LOAD-PIPELINE-001` |
| Owners | Static M2 threaded request records and terminal finalize FIFO |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-load-pipeline`, 2026-07-17 |
| Profiles/capabilities | Existing static M2 cached `.tscn`/`.glb` load path |
## Purpose
Own cross-frame bookkeeping between successful static M2 ResourceLoader request
start, terminal loaded/failed polling and budgeted main-thread finalization. The
loader retains every I/O and engine-resource operation.
## Non-goals
- Call ResourceLoader or select cache paths/formats.
- Own M2 scene, missing or material-refresh caches, or prepared Mesh references.
- Extract Meshes from PackedScene/GLB Resources.
- Consume finalize permits or rebuild/adopt runtime Meshes.
- Merge static and animated M2 pipelines.
## Context and boundaries
```mermaid
flowchart LR
Need[Static M2 mesh needed] --> Loader[StreamingWorldLoader]
Loader --> IO[ResourceLoader request]
Loader --> State[M2MeshLoadPipelineState]
State --> Poll[Detached pending records]
Poll --> Loader
Loader --> IO
Loader -->|terminal status| State
State --> FIFO[Finalize FIFO]
FIFO --> Loader
Loader --> Budget[M2_MESH_FINALIZE permit]
Loader --> Cache[M2 Mesh resource cache or M2 prototype outcome state]
```
Allowed dependencies are value containers, Strings and opaque integer statuses.
ResourceLoader, filesystem, WorkerThreadPool, Mesh/Resource/Node/RID ownership,
scheduler and other application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `remember_request(normalized_relative_path, resource_path)` | Command/query | Insert one pending request | Renderer main thread; until terminal/discard/clear | Empty/duplicate rejected |
| `has_request(path)` | Query | Test pending dedupe | Main thread | Empty/unknown false |
| `request_records_snapshot()` | Query | Copy pending records in insertion order | Main thread; caller-owned | None |
| `complete_request(path, terminal_status)` | Command/query | Move request to terminal FIFO | Main thread after poll | Unknown rejected |
| `discard_request(path)` | Command/query | Remove request without finalization | Main thread | Unknown false |
| `has_finalize_record()` / `pop_finalize_record()` | Query/command | Observe/pop completion-order FIFO | Main-thread budget drain | Empty pop returns `{}` |
| `total_work_count()` | Query | Preserve pending-plus-finalize metric | Main thread | None |
| `pending_request_count()` / `finalize_record_count()` | Query | Stage diagnostics | Main thread | None |
| `clear()` | Command | Drop bookkeeping after I/O drain/reset | Main thread | Idempotent; does not drain I/O |
| `diagnostic_snapshot()` | Query | Detached request/finalize records | Main thread | No Resources exposed |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized M2 path and cache Resource path | Loader request adapter | Pending map | Copied Strings | Until terminal/discard/clear |
| Input | Opaque terminal ResourceLoader status | Loader poll adapter | Finalize record | Integer value | Until pop/clear |
| Output | Detached pending records | Pipeline state | Loader poll/shutdown adapter | Caller-owned copies | One poll/wait pass |
| Output | Oldest terminal record | Pipeline state | Loader finalizer | Record ownership transferred | One finalize attempt |
| Output | Detached diagnostics | Pipeline state | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
Side effects are limited to collection mutation and retaining String/integer values.
## Data flow
```mermaid
flowchart TD
Start[Successful ResourceLoader request] --> Remember[remember request]
Remember --> Snapshot[request records snapshot]
Snapshot --> Poll[Mesh resource finalizer polls status]
Poll --> Active{In progress?}
Active -->|yes| Snapshot
Active -->|no| Complete[complete request with terminal status]
Complete --> FIFO[Append finalize FIFO]
FIFO --> Permit{Loader permit available?}
Permit -->|no| FIFO
Permit -->|yes| Pop[Pop oldest terminal record]
Pop --> Finalize[Mesh resource finalizer gets Resource and extracts first Mesh]
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
Prepare --> Adopt[Loader adopts Mesh or marks prototype outcome state]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Pending: remember request
Pending --> Pending: non-terminal poll
Pending --> TerminalQueued: complete request
Pending --> Absent: discard
TerminalQueued --> Absent: pop finalize
Pending --> Absent: clear
TerminalQueued --> Absent: clear
```
One path has at most one pending request. After completion it may be requested
again only if loader cache/missing rules permit it.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as M2MeshResourceFinalizer
participant Resource as ResourceLoader
participant State as M2MeshLoadPipelineState
participant Budget as RenderBudgetScheduler
Loader->>Resource: load_threaded_request(cache path)
Resource-->>Loader: OK or ERR_BUSY
Loader->>State: remember_request(normalized, cache path)
loop frames
Loader->>Finalizer: poll_terminal_requests(state, prototype cache)
Finalizer->>State: request_records_snapshot()
Finalizer->>Resource: load_threaded_get_status(path)
end
Finalizer->>State: complete_request(normalized, terminal status)
Loader->>Budget: try_consume_permit(M2_MESH_FINALIZE)
Loader->>Finalizer: finalize_next_resource(state, caches, directory)
Finalizer->>State: pop_finalize_record()
Finalizer->>Resource: load_threaded_get(path)
Finalizer->>Finalizer: extract/refresh/adopt Mesh or mark missing
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
Loader --> MeshFinalizer[M2MeshResourceFinalizer]
MeshFinalizer --> State
MeshFinalizer --> Resource[ResourceLoader]
Loader --> Budget[RenderBudgetScheduler]
Loader --> MeshCache[M2 Mesh resource and prototype outcome cache states]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
State -. no dependency .-> Resource
State -. no dependency .-> Budget
State -. no dependency .-> MeshCache
```
## Ownership, threading and resources
- Main thread serializes all state mutation and snapshots.
- State owns only request/finalize Dictionaries containing Strings and status integers.
- `M2MeshResourceFinalizer` owns terminal status polling, terminal Resource
retrieval, first-Mesh extraction, runtime preparation and cache/missing adoption.
- Loader owns request admission, scheduler permits and shutdown drain ordering.
- `M2MeshResourceCacheState` owns prepared static Mesh references and
`M2RuntimeMeshFinalizer` owns refresh/rebuild/fallback. The loader owns shared
adoption decisions, raw I/O and remaining engine resources; the prototype
cache service owns missing outcomes.
- Snapshot and diagnostic records are detached; caller mutation cannot affect state.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller/request later |
| Request start failure | Loader return code | No state insert; mark missing | Existing loader behavior | Cache/source correction |
| Non-terminal status | Finalizer poll | Keep pending | Existing queue metric | Poll next frame |
| Terminal load failure | Status in popped record | Finalizer marks missing | Existing missing behavior | World/cache reload |
| Empty defensive path | Finalizer before poll | Discard and mark missing | Source regression | Correct request producer |
| Shutdown | Loader drains pending Resource paths | Clear state | Shutdown verifier | New loader starts empty |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `m2_mesh_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds terminal FIFO pops outside state |
| Cache extension order | `.tscn`, `.glb` | Existing M2 path | No | Loader selects request path |
| Animated pivot-prefix GLB exclusion | Existing rule | All | No | Loader filters before request |
## Persistence, cache and migration
No pipeline state is serialized. M2 cache formats, material refresh version and
rebuild policy are unchanged; no migration or rebake is required.
## Diagnostics and observability
- `total_work_count` preserves all three historical `m2_mesh` metrics.
- Diagnostic snapshots expose only paths/statuses, never loaded Resources or Meshes.
- Existing loader logs and hitch sections remain unchanged.
- Normalized M2 path is the correlation key.
## Verification
- `verify_m2_mesh_load_pipeline_state.gd`: validation, dedupe, insertion-order
polling, terminal transition, completion FIFO, opaque status, discard, metrics,
detached diagnostics, loader ownership and 100-by-256 timing.
- Classifier/material/shutdown/M2 placement/build regressions cover adjacent behavior.
- Fidelity evidence is exact state/lifecycle extraction; no asset-backed or
original-client visual parity claim is made.
- Performance budget: 25,600 request/complete/pop transitions under one second.
## Extension points
- Mesh/missing cache adoption can move to a separate finalizer without changing this state.
- Animated M2 has distinct candidate/static fallback semantics and remains separate.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static M2 request/finalize state | Implemented extraction | Contract/source/timing verifier | Asset-backed long traversal pending |
| Static request admission I/O | Implemented in observer | Observer/shutdown regressions | Asset-backed traversal pending |
| Static request polling/finalize I/O | Implemented extraction | Finalizer contract/source/timing verifier | Asset-backed traversal pending |
| Mesh cache | Implemented extraction | Mesh resource cache state verifier | Asset-backed memory/leak run pending |
| First-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
| Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
| Mesh materialization | Existing loader-owned | Renderer/material tests | Further safe extraction pending |
## Known gaps and risks
- Records remain Dictionaries because ResourceLoader polling is a dynamic Godot boundary.
- Clear does not drain ResourceLoader; caller ordering is mandatory.
- Status integers are intentionally opaque inside state.
- No private asset traversal, leak/descriptor-pressure or p95/p99 run is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending records, terminal FIFO and metrics |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Terminal polling, Resource extraction, preparation and adoption |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh references and final clear |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
| `src/render/m2/m2_prototype_cache_state.gd` | Missing-model outcome retention |
| `src/scenes/streaming/streaming_world_loader.gd` | Cache path choice, request admission, permits and composition |
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Downstream stale Mesh rebuild decision |
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
## Related decisions and references
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,220 @@
# M2 Mesh Resource Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-CACHE-001` |
| Owners | Prepared static M2 Mesh resource references by normalized path |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-cache`, 2026-07-17 |
| Profiles/capabilities | Existing static M2 cached and raw fallback paths |
## Purpose
Own the runtime cache of prepared static M2 `Mesh` references outside
`StreamingWorldLoader`. Dedicated services extract and prepare each Mesh; the
loader remains responsible for finding, loading, adopting and materializing it.
## Non-goals
- Own the shared M2 missing-path, prototype or animated-scene caches.
- Call ResourceLoader/FileAccess or select `.tscn`/`.glb` paths.
- Extract a Mesh from a Resource or Node.
- Refresh materials, rebuild Meshes or create MultiMesh instances.
- Add eviction, persistence, cache versions or quality-profile behavior.
## Context and boundaries
```mermaid
flowchart LR
Need[Static M2 mesh lookup] --> Loader[StreamingWorldLoader]
Loader --> Cache[M2MeshResourceCacheState]
Cache -->|cached Mesh or null| Loader
Loader --> Pipeline[M2MeshLoadPipelineState]
Pipeline --> IO[ResourceLoader]
IO --> Extract[M2MeshResourceExtractor]
Extract --> Prepare[M2RuntimeMeshFinalizer]
Prepare --> Cache
Loader --> Build[M2StaticBatchMaterializer]
```
The cache may retain `Mesh` resources and copied normalized-path Strings. It has
no Node, ResourceLoader, filesystem, worker, scheduler, missing-state, builder,
gameplay, network or editor dependency.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `store_mesh(path, mesh)` | Command/query | Retain or replace one prepared Mesh | Renderer main thread; final shutdown | Empty path/null Mesh rejected |
| `has_mesh(path)` | Query | Test whether a Mesh is retained | Renderer main thread | Empty/unknown false |
| `find_mesh(path)` | Query | Borrow exact retained Mesh reference | Renderer main thread | Empty/unknown null |
| `mesh_count()` | Query | Return retained entry count | Diagnostics/tests | None |
| `normalized_paths_snapshot()` | Query | Copy insertion-order path keys | Diagnostics/tests | Never exposes Mesh references |
| `clear()` | Command | Release every retained Mesh reference | Final shutdown after async drain | Idempotent |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized relative M2 path | Loader normalization adapter | Cache key | Copied String | Cache lifetime |
| Input | Prepared static Mesh | Loader finalizer or synchronous fallback | Cache value | Strong reference retained | Until replacement/clear |
| Output | Borrowed exact Mesh | Cache lookup | Static M2 batch builder | Cache retains reference | One build/materialization pass |
| Output | Detached normalized paths | Cache diagnostics | Verifier/future metrics | Caller-owned Strings | Snapshot lifetime |
Store and clear mutate only the cache Dictionary and Resource reference counts.
## Data flow
```mermaid
flowchart TD
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
Cached -->|yes| Return[Return retained Mesh]
Cached -->|no| Missing{Prototype outcome says missing?}
Missing -->|yes| Null[Return null]
Missing -->|no| Load[Loader request or synchronous fallback]
Load --> Prepare[Extractor and runtime finalizer prepare Mesh]
Prepare --> Valid{Prepared Mesh valid?}
Valid -->|yes| Store[Store or replace cache entry]
Valid -->|no| MarkMissing[Loader updates prototype outcome state]
Store --> Return
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Absent
Absent --> Cached: store valid Mesh
Cached --> Cached: replace Mesh reference
Cached --> Absent: final shutdown clear
```
Map/world refresh retains cached Meshes. Only the existing final runtime-cache
release clears them after asynchronous shutdown drain.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Cache as M2MeshResourceCacheState
participant Pipeline as M2MeshLoadPipelineState
Loader->>Cache: find/has normalized path
alt cache miss
Loader->>Pipeline: request record
Loader->>Finalizer: poll/finalize one permitted record
Finalizer->>Cache: store_mesh(path, prepared Mesh)
end
Cache-->>Loader: exact retained Mesh
Loader->>Loader: materialize static M2 batch
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
Loader --> Pipeline[M2MeshLoadPipelineState]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
Loader --> Builder[M2Builder]
Loader --> Materializer[M2StaticBatchMaterializer]
Cache --> Mesh[Godot Mesh Resource]
Cache -. no dependency .-> Pipeline
Cache -. no dependency .-> Classifier
Cache -. no dependency .-> Builder
```
## Ownership, threading and resources
- Renderer main-thread adapters serialize every cache operation.
- The cache owns normalized-path Strings and strong Mesh references.
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
instances. `M2PrototypeCacheState` owns missing/prototype/animated state; the
static materializer owns MultiMesh construction/attachment;
`M2MeshResourceFinalizer` owns resource adoption, while the loader owns
build-job and scheduler-permit decisions.
- The loader drains asynchronous work before the final cache clear.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path or null Mesh | Cache guard | Reject unchanged | Contract verifier | Correct caller input |
| Unknown lookup | Membership query | Return null | Existing loader path | Request/load or missing decision |
| Replacement | Existing key | Retain new exact reference | Contract verifier | Normal subsequent lookup |
| Load/preparation failure | Loader boundary | Do not store; loader marks missing | Existing renderer behavior | Cache/source correction and reload |
| Final shutdown | Loader lifecycle | Clear releases references | Shutdown/source verifier | New loader starts empty |
## Configuration and capabilities
The module adds no setting, budget or profile branch. Cache path order, finalize
permits and batching remain loader-owned; refresh version `2` belongs to
`M2RuntimeMeshFinalizer`.
## Persistence, cache and migration
The state is runtime-only and serializes nothing. Existing `.tscn`/`.glb`
formats, import caches and material refresh metadata are unchanged; no migration
or rebake is required.
## Diagnostics and observability
`mesh_count` and `normalized_paths_snapshot` expose scalar ownership diagnostics
without Mesh references. Existing renderer metrics/logging are unchanged because
the historical Mesh cache had no queue contribution or log site.
## Verification
- `verify_m2_mesh_resource_cache_state.gd`: invalid input, exact reference,
replacement, lookup, detached path snapshot, idempotent clear, loader source
ownership and 100-by-256 bounded timing.
- Mesh pipeline/classifier/material/shutdown/M2 build/facade regressions cover
adjacent behavior.
- Fidelity evidence is exact cache/lifetime extraction; no original-client
visual-parity or asset-backed performance claim is made.
## Extension points
- `M2RuntimeMeshFinalizer` returns a prepared Mesh for `store_mesh` without
changing cache ownership.
- Eviction requires measured memory pressure and explicit lifetime semantics.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Prepared static M2 Mesh cache | Implemented extraction | Contract/source/timing verifier | Asset-backed memory/leak run pending |
| M2 Mesh request lifecycle | Implemented extraction | Pipeline/finalizer verifiers | Asset-backed traversal pending |
| M2 Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
| M2 Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
## Known gaps and risks
- Strong Mesh references intentionally remain until final shutdown.
- Missing/prototype/animated lookup state is now a separate sibling service.
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh references and final clear |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Prepared Mesh producer and cache adoption |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype references and negative lookup outcomes |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, request admission, permits and materialization |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending request and terminal finalize records |
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Cache ownership/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+211
View File
@@ -0,0 +1,211 @@
# M2 Mesh Resource Extractor
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001` |
| Owners | First-Mesh selection and temporary PackedScene instance lifetime |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-extractor`, 2026-07-17 |
| Profiles/capabilities | Static M2 threaded cache and synchronous raw/prototype fallback paths |
## Purpose
Select the first M2 Mesh from a direct Resource, PackedScene or existing Node
subtree while keeping traversal and temporary-node lifetime outside
`StreamingWorldLoader`.
## Non-goals
- Request/load Resources or choose cache paths.
- Load raw M2 data, refresh materials or rebuild Meshes.
- Own Mesh/prototype/missing caches or pending/finalize queues.
- Create MultiMeshes, attach Nodes or consume render permits.
- Change first-Mesh selection order or visible output.
## Context and boundaries
```mermaid
flowchart LR
Pipeline[M2 load pipeline] --> Loader[StreamingWorldLoader]
Loader --> Resource[Loaded Mesh or PackedScene Resource]
Resource --> Extractor[M2MeshResourceExtractor]
Extractor -->|first Mesh or null| Loader
Loader --> Prepare[M2RuntimeMeshFinalizer]
Prepare --> Cache[M2MeshResourceCacheState]
Prototype[Existing M2 Node subtree] --> Extractor
```
The extractor may inspect `Resource`, `PackedScene`, `Node`, `MeshInstance3D`
and `Mesh` engine types. It has no ResourceLoader, filesystem, worker, cache,
scheduler, builder, gameplay, network or editor dependency.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `extract_first_mesh(resource)` | Query with temporary ownership | Return direct Mesh or first Mesh from one PackedScene instance | Renderer main thread; temporary root freed before return | Null/unsupported/empty returns null |
| `find_first_mesh_in_subtree(root)` | Query | Return first Mesh in historical depth-first preorder | Renderer main thread; caller-owned subtree unchanged | Null/no Mesh returns null |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Direct Mesh Resource | Loader threaded finalizer | Extractor | Borrowed Resource | One call |
| Input | PackedScene Resource | Loader threaded finalizer | Extractor | Borrowed scene; extractor owns temporary instance | Temporary root until return |
| Input | Existing prototype/rebuild Node subtree | Loader builder/fallback | Extractor | Borrowed Node tree | One traversal |
| Output | First Mesh reference | Extractor | Loader preparation/cache adapter | Borrowed exact reference | Cache may retain later |
The only side effect is synchronous creation and immediate `free()` of a
temporary PackedScene instance.
## Data flow
```mermaid
flowchart TD
Input[Resource or Node subtree] --> Direct{Direct Mesh?}
Direct -->|yes| Return[Return exact Mesh]
Direct -->|no| Packed{PackedScene?}
Packed -->|yes| Instantiate[Instantiate temporary root]
Packed -->|no| Traverse[Traverse caller-owned subtree]
Instantiate --> TraverseTemp[Depth-first preorder traversal]
TraverseTemp --> Free[Free temporary root]
Free --> ReturnOrNull[Return first Mesh or null]
Traverse --> ReturnOrNull
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Stateless
Stateless --> TemporaryInstance: extract PackedScene
TemporaryInstance --> Traversing: instantiate succeeded
Traversing --> Stateless: free temporary root before return
Stateless --> Stateless: direct Mesh/subtree/invalid input
```
No state or Resource reference is retained between calls.
## Main sequence
```mermaid
sequenceDiagram
participant Finalizer as M2MeshResourceFinalizer
participant Extractor as M2MeshResourceExtractor
participant Scene as PackedScene
Finalizer->>Extractor: extract_first_mesh(loaded Resource)
alt direct Mesh
Extractor-->>Finalizer: same Mesh reference
else PackedScene
Extractor->>Scene: instantiate()
Scene-->>Extractor: temporary root
Extractor->>Extractor: depth-first first-Mesh search
Extractor->>Extractor: temporary_root.free()
Extractor-->>Finalizer: Mesh or null
end
Finalizer->>Finalizer: prepare and cache Mesh or mark missing
```
## Dependency diagram
```mermaid
flowchart TB
Finalizer[M2MeshResourceFinalizer] --> Extractor[M2MeshResourceExtractor]
Extractor --> Types[Resource / PackedScene / Node / Mesh]
Loader --> Pipeline[M2MeshLoadPipelineState]
Loader --> Cache[M2MeshResourceCacheState]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
Extractor -. no dependency .-> Pipeline
Extractor -. no dependency .-> Cache
Extractor -. no dependency .-> Classifier
```
## Ownership, threading and resources
- The renderer main thread performs all traversal and PackedScene instantiation.
- Direct and subtree Mesh references are borrowed without duplication.
- A PackedScene temporary root is extractor-owned and synchronously freed.
- Existing prototype/rebuild subtrees remain caller-owned and are never mutated.
- Cache retention remains a cache-state concern; runtime material preparation
belongs to `M2RuntimeMeshFinalizer`.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null/unsupported Resource | Type guard | Return null | Contract verifier | Loader marks missing/falls back |
| PackedScene instantiate failure | Null root | Return null | Existing loader behavior | Rebuild cache/source |
| No Mesh in subtree | Traversal exhaustion | Return null | Contract verifier | Loader marks missing/falls back |
| Null MeshInstance Mesh | Null traversal result | Parent continues with later siblings | Order verifier | Normal sibling search |
| Shutdown/cancel | No retained state | No action | Ownership docs | Caller owns surrounding job |
## Configuration and capabilities
The module adds no settings or profile branches. Cache format order, material
refresh version, render permits and batching remain unchanged outside it.
## Persistence, cache and migration
The extractor is stateless and serializes nothing. Existing `.tscn`/`.glb`
formats, cache versions and rebuild metadata are unchanged; no migration or
rebake is required.
## Diagnostics and observability
The module emits no logs and retains no counters. Loader correlation remains the
normalized M2 path. Its verifier measures traversal time and temporary Node count.
## Verification
- `verify_m2_mesh_resource_extractor.gd`: direct/invalid Resources, depth-first
child order, null-Mesh sibling continuation, subtree ownership, PackedScene
exact Mesh identity, temporary Node release, source boundaries and 10,000
bounded traversals.
- M2 pipeline/cache/classifier/material/shutdown/build regressions cover adjacent
behavior. No asset-backed visual parity claim is made.
## Extension points
- `M2MeshResourceFinalizer` passes the Mesh to `M2RuntimeMeshFinalizer` and
stores its result in `M2MeshResourceCacheState`.
- Broader generic scene traversal is intentionally excluded until another real
consumer requires the same exact contract.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Direct/PackedScene first-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
| Prototype/rebuild subtree selection | Implemented extraction | Synthetic preorder verifier | Asset-backed M2 traversal pending |
| Material refresh/rebuild finalization | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
## Known gaps and risks
- PackedScene instantiation remains synchronous main-thread work behind the
existing finalize permit.
- The first Mesh rule intentionally ignores additional MeshInstances.
- No proprietary M2 corpus, corrupt cache, leak/descriptor-pressure, p95/p99 or
paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Terminal Resource consumer and extraction caller |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and missing/static-only lookup state |
| `src/scenes/streaming/streaming_world_loader.gd` | Request admission, permits and materialization |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh retention |
| `src/tools/verify_m2_mesh_resource_extractor.gd` | Resource/order/lifetime/boundary/timing regression |
## Related decisions and references
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+251
View File
@@ -0,0 +1,251 @@
# M2 Mesh Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-FINALIZER-001` |
| Owners | Static M2 terminal Resource I/O, Mesh preparation and cache outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing cached/static MultiMesh M2 path |
## Purpose
Drain static M2 ResourceLoader work and publish a prepared Mesh or missing-model
outcome without keeping terminal I/O and refresh logic in `StreamingWorldLoader`.
The same preparation method serves threaded and synchronous Mesh consumers.
## Non-goals
- Select cache candidates or admit threaded ResourceLoader requests.
- Own scheduler permits, prototype Nodes, MultiMeshes or SceneTree roots.
- Change first-Mesh traversal, refresh version, rebuild rules or raw parser.
- Merge static and animated terminal services through callbacks/frameworks.
- Change cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Observer[M2StaticBuildResourceObserver] --> Pipeline[M2MeshLoadPipelineState]
Loader[StreamingWorldLoader] --> Finalizer[M2MeshResourceFinalizer]
Pipeline --> Finalizer
ResourceLoader --> Finalizer
Finalizer --> Extractor[M2MeshResourceExtractor]
Finalizer --> Runtime[M2RuntimeMeshFinalizer]
Finalizer --> Raw[M2RawModelRepository]
Finalizer --> MeshCache[M2MeshResourceCacheState]
Finalizer --> Prototype[M2PrototypeCacheState]
```
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `poll_terminal_requests(pipeline, prototype_cache)` | I/O command | Move LOADED/FAILED records into completion FIFO | Invalid composition returns zero |
| `finalize_next_resource(pipeline, mesh_cache, prototype_cache, extracted_dir)` | I/O command | Pop one record and publish Mesh/missing outcome | Empty/invalid composition returns false |
| `prepare_mesh_for_runtime(path, mesh, extracted_dir)` | I/O preparation | Preserve current Mesh or load raw data and refresh stale Mesh | Null returns null |
| `load_threaded_get_status(path)` | I/O adapter | Production status query with injectable test seam | Returns ResourceLoader status |
| `load_threaded_get(path)` | I/O adapter | Production terminal Resource retrieval with injectable test seam | May return null |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Pending/finalize records | Mesh pipeline | Finalizer | Pipeline-owned copied Dictionaries |
| Input | Terminal Resource | ResourceLoader | Mesh extractor | Borrowed one operation |
| Input | Extracted/cached Mesh | Mesh extractor/loader consumer | Runtime preparation | Borrowed Resource reference |
| Input | Raw M2 Dictionary | Raw repository | Runtime finalizer | Call-local value |
| Output | Prepared Mesh | Runtime finalizer | Mesh cache/loader consumer | Resource reference |
| Output | Missing-model outcome | Finalizer | Prototype cache | Copied path; loader session |
| Output | Processed-record flag | Finalizer | Loader permit loop/tests | Boolean value |
## Data flow
```mermaid
flowchart TD
Poll[Poll pending records in insertion order] --> Path{Resource path empty?}
Path -->|yes| Discard[Discard and mark missing]
Path -->|no| Status{LOADED or FAILED?}
Status -->|no| Pending[Keep pending]
Status -->|yes| Complete[Append completion FIFO]
Complete --> Permit[Loader consumes M2_MESH_FINALIZE permit]
Permit --> Pop[Pop oldest terminal record]
Pop --> Cached{Empty path or Mesh cached?}
Cached -->|yes| Skip[Finish one permit operation]
Cached -->|no| Loaded{Status LOADED?}
Loaded -->|no| Missing[Mark model missing]
Loaded -->|yes| Get[Get terminal Resource]
Get --> Extract[Extract first Mesh]
Extract --> Found{Mesh found?}
Found -->|no| Missing
Found -->|yes| Current{Refresh version current?}
Current -->|yes| Store[Store exact Mesh]
Current -->|no| Raw[Read raw M2 data]
Raw --> Refresh[Finalize/rebuild/fallback Mesh]
Refresh --> Store
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: nonterminal status
Polling --> TerminalQueued: LOADED or FAILED
Polling --> Missing: empty Resource path
TerminalQueued --> Skipped: empty/cached record
TerminalQueued --> Missing: failed load or no Mesh
TerminalQueued --> Extracted: Mesh found
Extracted --> Current: refresh version current
Extracted --> Refreshing: stale Mesh
Refreshing --> Prepared: rebuild or fallback
Current --> Cached
Prepared --> Cached
Pending --> [*]
Skipped --> [*]
Missing --> [*]
Cached --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant F as M2MeshResourceFinalizer
participant P as M2MeshLoadPipelineState
participant R as ResourceLoader
participant E as M2MeshResourceExtractor
participant N as M2RuntimeMeshFinalizer
participant C as M2MeshResourceCacheState
L->>F: poll_terminal_requests(P, prototype cache)
F->>R: load_threaded_get_status(path)
F->>P: complete terminal request
L->>L: consume M2_MESH_FINALIZE permit
L->>F: finalize_next_resource(...)
F->>P: pop oldest record
F->>R: load_threaded_get(path)
F->>E: extract_first_mesh(Resource)
F->>N: requires_raw_data_for_refresh(Mesh)
opt stale Mesh
F->>F: read raw M2 data
F->>N: finalize_mesh(...)
end
F->>C: store prepared Mesh
```
## Dependency diagram
```mermaid
flowchart TB
Finalizer[M2MeshResourceFinalizer] --> ResourceLoader
Finalizer --> Pipeline[M2MeshLoadPipelineState]
Finalizer --> Extractor[M2MeshResourceExtractor]
Finalizer --> Runtime[M2RuntimeMeshFinalizer]
Finalizer --> Raw[M2RawModelRepository]
Finalizer --> MeshCache[M2MeshResourceCacheState]
Finalizer --> Prototype[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Scheduler[RenderBudgetScheduler]
Finalizer -. no dependency .-> Scheduler
Finalizer -. no dependency .-> MultiMesh
Finalizer -. no dependency .-> SceneTree
```
## Ownership, threading and resources
- All operations run synchronously on the renderer main thread.
- Pipeline owns pending/completion records; one permit pops one terminal record.
- Extractor owns temporary PackedScene instance cleanup and returns a Mesh reference.
- Runtime finalizer owns stale/current/rebuild/fallback rules; raw repository owns
only call-local native I/O.
- Mesh cache retains prepared Mesh references until final shutdown; prototype
cache retains missing-model paths.
- Service retains dependency references but no pending record, Resource, Mesh or
raw Dictionary between calls. Loader retains permits and materialization.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Missing composition dependency | Return zero/false; preparation keeps Mesh where possible | Correct composition |
| Empty Resource path | Discard request and mark missing | Repair cache and restart session |
| Nonterminal status | Keep pending | Poll next tick |
| FAILED status | Pop and mark missing | Repair cache and restart session |
| Existing cached Mesh | Pop and skip terminal Resource get | None |
| Null/unsupported/no-Mesh Resource | Mark missing | Repair imported cache |
| Raw read/rebuild unavailable | Runtime finalizer marks/reuses original Mesh | Existing fallback |
| Tile cancellation | Shared cache work continues | Loader queue remains authoritative |
| Shutdown | Loader drains ResourceLoader before pipeline/cache clear | New loader starts empty |
## Configuration and capabilities
The service consumes the existing extracted directory. The scheduler retains
`m2_mesh_finalize_ops_per_tick`; material refresh version remains `2`.
## Persistence, cache and migration
No cache path, schema or version changes are introduced. Existing `.tscn/.glb`
Resources and in-memory refresh metadata remain compatible; no rebake is required.
## Diagnostics and observability
The service adds no log. Existing pending-plus-finalize metrics and refresh
metadata remain unchanged. Normalized relative path remains the correlation key.
## Verification
- Dedicated verifier covers status insertion order, empty-path discard, FIFO,
failed/cached/null outcomes, terminal get, exact Mesh identity, stale-only raw
read, exact finalizer arguments, cache adoption, source ownership and timing.
- Pipeline/cache/extractor/runtime-finalizer/raw-repository/prototype/shutdown,
facade, internal-access and baseline regressions protect adjacent behavior.
- Fidelity evidence is exact lifecycle/I/O extraction only; no private asset,
original-client visual, leak-pressure or p95/p99 claim is made.
## Extension points
Further extraction can move static prototype/material-source construction without
changing this terminal Resource contract. Animated finalization remains separate
because its candidate-before-material ordering and Node ownership differ.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static terminal polling/load | Implemented extraction | Status/FIFO/load/source verifier | Asset-backed traversal pending |
| Mesh extraction/preparation/cache outcome | Implemented extraction | Identity/raw/finalizer/store verifier | Private leak/p95/p99 pending |
| Static MultiMesh materialization | Separate implemented service | Materializer verifier | GPU/asset-backed evidence pending |
## Known gaps and risks
- Terminal Resource get, extraction and stale rebuild remain synchronous main-thread work.
- Prepared Mesh cache remains unbounded until final shutdown.
- Private asset traversal, descriptor pressure, leak and p95/p99 evidence is absent.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Poll/load/extract/prepare/cache/missing service |
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending and completion FIFO ownership |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh and temporary-instance ownership |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback rules |
| `src/render/m2/m2_raw_model_repository.gd` | Optional native raw M2 boundary |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh ownership |
| `src/render/m2/m2_prototype_cache_state.gd` | Missing-model outcome ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Permit loop and materialization composition |
| `src/tools/verify_m2_mesh_resource_finalizer.gd` | Polling/preparation/boundary/timing regression |
## Related decisions and references
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md)
- [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md)
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,245 @@
# M2 Native Animation Resource Observer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001` |
| Owners | Native animation candidate policy, raw read, build and cache outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-native-animation-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing native GryphonRoost animated M2 path |
## Purpose
Resolve the historical native animated M2 path as one synchronous observation:
select a GryphonRoost candidate, reuse cached state, read animated raw data,
build a detached prototype and publish either the prototype or static-only result.
## Non-goals
- Add native animation candidates or change the `gryphonroost` predicate.
- Observe/request cached GLB animation or poll/finalize ResourceLoader work.
- Duplicate, attach, animate or free accepted instances in the SceneTree.
- Consume render permits or change build queue, batching and dispatch policy.
- Change native parsing/building, cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> Observer[M2NativeAnimationResourceObserver]
Observer --> Repository[M2RawModelRepository]
Observer --> Builder[M2NativeAnimatedBuilder]
Observer --> Cache[M2PrototypeCacheState]
Observer --> Prototype[Detached animated prototype]
Prototype --> Snapshot[M2BuildResourceSnapshot]
Observer -->|no prototype| Cached[M2CachedAnimationResourceObserver]
```
The observer is the native-first resource boundary. The loader supplies an
already-normalized path and remains responsible for fallback ordering and for
constructing the snapshot consumed by dispatch.
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `is_native_animation_candidate(path)` | Pure query | Apply the historical case-insensitive GryphonRoost substring policy | Empty/unmatched path returns false |
| `observe(path, extracted_dir, repository, cache, debug)` | Command/query | Return an exact cached/new native prototype or record static-only fallback | Invalid/non-candidate input returns null without mutation |
| `build_animated_prototype(data, extracted_dir)` | Adapter command | Call the retained native animated builder dependency | Missing builder or invalid result returns null |
The optional constructor dependency exists for deterministic tests; production
uses `M2NativeAnimatedBuilder` without a new abstraction or runtime setting.
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized relative M2 path | Loader path adapter | Observer | Copied String; one call |
| Input | Extracted directory | Loader configuration | Repository/builder | Copied String; one call |
| Input | Raw animated Dictionary | Repository | Observer/builder | Call-local value |
| Input | Prototype cache state | Loader composition | Observer | Borrowed service; loader session |
| Output | Cached or adopted prototype | Cache/observer | Loader snapshot | Borrowed exact Node3D reference |
| Output | Static-only transition | Observer | Prototype cache | Copied path; loader session |
| Output | Debug line | Observer | Runtime log | Emitted only when enabled and build succeeds |
## Data flow
```mermaid
flowchart TD
Start[Observe normalized path] --> Candidate{Valid native candidate?}
Candidate -->|no| None[Return null]
Candidate -->|yes| Cached{Cached prototype?}
Cached -->|yes| Return[Return exact prototype]
Cached -->|no| Static{Marked static-only?}
Static -->|yes| None
Static -->|no| Read[Read animated raw data]
Read --> Surfaces{Data and animated surfaces?}
Surfaces -->|no| Mark[Mark static-only]
Surfaces -->|yes| Build[Build detached prototype]
Build --> Children{Prototype has children?}
Children -->|no| Mark
Children -->|yes| Adopt[Adopt first cache prototype]
Adopt --> Log{Debug enabled?}
Log -->|yes| Emit[Emit exact native cache fields]
Log -->|no| Return
Emit --> Return
Mark --> None
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observing
Observing --> Rejected: invalid or non-candidate
Observing --> Cached: positive cache hit
Observing --> StaticOnly: negative cache hit
Observing --> Reading: uncached candidate
Reading --> StaticOnly: no animated surfaces
Reading --> Building: usable raw data
Building --> StaticOnly: null or childless result
Building --> Adopted: valid result
Adopted --> [*]
Cached --> [*]
StaticOnly --> [*]
Rejected --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant O as NativeAnimationObserver
participant C as M2PrototypeCacheState
participant R as M2RawModelRepository
participant B as M2NativeAnimatedBuilder
L->>O: observe(path, directory, repository, cache, debug)
O->>C: find animated / is static-only
alt uncached native candidate
O->>R: load_animated_model_data(directory, path)
R-->>O: raw Dictionary
O->>B: build(raw, directory)
alt valid prototype
O->>C: adopt_animated_prototype(path, prototype)
O-->>L: canonical prototype
else unavailable
O->>C: mark_animation_static(path)
O-->>L: null
end
else cached result
O-->>L: prototype or null
end
```
## Dependency diagram
```mermaid
flowchart TB
Observer[M2NativeAnimationResourceObserver] --> Repository[M2RawModelRepository]
Observer --> Builder[M2NativeAnimatedBuilder]
Observer --> Cache[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Observer
Observer -. no dependency .-> ResourceLoader
Observer -. no dependency .-> Pipeline[M2AnimationLoadPipelineState]
Observer -. no dependency .-> Scheduler[RenderBudgetScheduler]
Observer -. no dependency .-> SceneTree
```
## Ownership, threading and resources
- Observation and native parsing/building remain synchronous on the renderer
main thread, exactly as before extraction; there is no mid-call cancellation.
- The observer retains no path, raw Dictionary, Node or service reference.
- Prototype cache owns an accepted detached Node until final shutdown; callers
only borrow the canonical reference.
- The historical childless-builder rejection does not free its detached Node.
This deliberately preserved lifetime edge is documented as a remaining leak
risk rather than silently changed during architectural extraction.
- Loader owns snapshots, fallback sequencing, permits, materialization and every
SceneTree mutation.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Empty/non-candidate path or missing dependency | Return null without state mutation | Correct composition/path |
| Cached prototype | Return exact canonical Node immediately | None |
| Existing static-only outcome | Return null without repeated I/O | New loader session after source repair |
| Empty raw data or animated surfaces | Mark static-only and return null | Repair source/parser and start new session |
| Null/childless build | Mark static-only and return null | Repair builder/data and start new session |
| Duplicate successful build | Cache releases later candidate and returns first | None |
| Cancellation during native call | Not supported; call completes | Loader controls whether the call starts |
| Shutdown | Observer retains nothing | Cache releases adopted Nodes after drains |
## Configuration and capabilities
The observer consumes the existing extracted directory and `debug_streaming`
flag. Candidate policy remains the case-insensitive substring `gryphonroost`.
No profile, cache, budget or animation setting is added.
## Persistence, cache and migration
No persistence or format changes are introduced. Positive and static-only state
continues to use the shutdown-lifetime prototype cache; no asset rebake is needed.
## Diagnostics and observability
A successful debug-enabled build emits the unchanged `M2_NATIVE_ANIM_CACHE`
record with path, surface count, bone count, animation id, sequence index,
activity score and length. Rejections remain silent as before.
## Verification
- Dedicated synthetic verifier covers candidate policy, invalid dependencies,
cache identity, static suppression, raw/surface/build failures, preserved
childless lifetime, exact repository/builder arguments, adoption, source
ownership and bounded candidate checks.
- Cached observer, raw repository, prototype cache, snapshot, dispatch, shutdown,
facade, internal-access and render-baseline checks protect adjacent behavior.
- Fidelity evidence is exact control-flow and diagnostic extraction. It is not
original-client animation, visual, memory or asset-backed performance evidence.
## Extension points
Terminal cached-GLB polling/finalization can move behind a separate adapter
without changing this native-first contract. Candidate expansion requires its
own fidelity evidence and must not silently change `Blizzlike335` behavior.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Native candidate selection | Implemented extraction | Policy/source/timing verifier | Additional candidates require fidelity work |
| Raw read/build/cache outcome | Implemented extraction | Failure/identity/adoption verifier | Asset-backed traversal pending |
| Terminal cached ResourceLoader polling | Loader-owned | Existing pipeline regressions | Dedicated finalize adapter |
## Known gaps and risks
- Native parsing/building is synchronous and not bounded by a render permit.
- Rejected childless builder Nodes retain the historical unowned lifetime edge.
- Private asset traversal, descriptor/leak pressure, p95/p99 and paired visual/
animation evidence remain unavailable in this package.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_native_animation_resource_observer.gd` | Candidate/cache/raw/build/outcome/logging observation |
| `src/render/m2/m2_raw_model_repository.gd` | Optional native file/parser boundary |
| `src/render/m2/m2_prototype_cache_state.gd` | Positive prototype and static-only ownership |
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Existing animated prototype construction |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, native-first fallback order and execution |
| `src/tools/verify_m2_native_animation_resource_observer.gd` | Lifecycle/identity/boundary/timing regression |
## Related decisions and references
- [`m2-cached-animation-resource-observer.md`](m2-cached-animation-resource-observer.md)
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+258
View File
@@ -0,0 +1,258 @@
# M2 Prototype Cache State
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-PROTOTYPE-CACHE-001` |
| Owners | Detached static/animated prototypes and missing/static-animation outcomes |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-prototype-cache`, 2026-07-17 |
| Profiles/capabilities | Static, GLB animated and native animated M2 prototype reuse |
## Purpose
Own the four shutdown-lifetime M2 prototype lookup states previously stored in
`StreamingWorldLoader`: static and animated `Node3D` references, missing model
paths and paths whose animation fallback is static-only.
## Non-goals
- Normalize paths, choose cache files, call ResourceLoader or parse raw M2 data.
- Build, repair or instantiate prototypes, Meshes, materials or MultiMeshes.
- Own animation/Mesh request queues, permits or tile/build state.
- Add eviction, persistence, retries, profile rules or format changes.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
Observer[M2 resource observers] --> State
Resource[ResourceLoader / raw builders] --> Loader
Resource --> Observer
State --> Static[Static prototype Node3D]
State --> Animated[Animated prototype Node3D]
State --> Missing[Missing-model path set]
State --> StaticOnly[Static-animation path set]
Loader --> Instances[Duplicate/animated instance materialization]
```
The state may depend on `Node3D` lifetime APIs and value-only path containers.
Filesystem/native/ResourceLoader APIs, builders, worker pools, render scheduling,
Mesh traversal and other application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `has_static_prototype(path)` | Query | Test for one valid retained static prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
| `find_static_prototype(path)` | Query | Return exact retained static Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
| `adopt_static_prototype(path, prototype)` | Command/query | Retain first valid static prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
| `has_animated_prototype(path)` | Query | Test for one valid retained animated prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
| `find_animated_prototype(path)` | Query | Return exact retained animated Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
| `adopt_animated_prototype(path, prototype)` | Command/query | Retain first valid animated prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
| `mark_model_missing(path)` / `is_model_missing(path)` | Command/query | Store/query a missing-model outcome | Renderer main thread; until shutdown | Empty mark rejected |
| `mark_animation_static(path)` / `is_animation_static(path)` | Command/query | Store/query static-only animation outcome | Renderer main thread; until shutdown | Empty mark rejected |
| `diagnostic_snapshot()` | Diagnostic query | Return detached sorted path arrays for all four states | Caller-owned result | Never exposes Nodes |
| `clear_and_release()` | Lifecycle command | Release prototypes and clear all state | Final shutdown after async drain | Idempotent |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized non-empty M2 path | Loader normalization | Cache state | Copied String key | Shutdown lifetime |
| Input | Detached static Node3D | Cached scene/raw builder adapter | Cache state | Adopted on success | Until final shutdown |
| Input | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
| Input | Missing/static-only outcome | Resource observers/finalizers | Cache state | Boolean set entry | Until final shutdown |
| Output | Canonical prototype Node3D | Cache state | Loader instance/material adapter | Borrowed exact reference | One lookup/use |
| Output | Detached path-only snapshot | Cache state | Tests/diagnostics | Fresh caller-owned arrays | One query |
Side effects are strong Node retention, release of rejected duplicate candidates,
final Node destruction/queueing and mutation of runtime-only path state.
## Data flow
```mermaid
flowchart TD
Request[Normalized prototype request] --> Positive{Valid prototype cached?}
Positive -->|yes| Return[Return exact prototype]
Positive -->|no| Negative{Missing or static-only outcome?}
Negative -->|yes| Fallback[Return caller fallback]
Negative -->|no| Load[Loader performs existing I/O/build]
Load --> Success{Valid prototype?}
Success -->|yes| Adopt[Adopt first candidate]
Adopt --> Return
Success -->|no| Mark[Mark relevant negative outcome]
Mark --> Fallback
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Positive: adopt static/animated prototype
Empty --> Negative: mark missing/static-only path
Positive --> Positive: lookup or same-reference adoption
Positive --> Positive: duplicate candidate released; first wins
Negative --> Negative: repeated mark/query
Positive --> Released: final shutdown
Negative --> Released: final shutdown
Released --> Released: repeated clear
Released --> [*]
```
Map refresh does not transition or clear this service. Final scene shutdown calls
`clear_and_release` only after worker and ResourceLoader work is drained.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as M2PrototypeCacheState
participant Source as ResourceLoader/raw builder
Loader->>State: find static/animated(path)
alt positive hit
State-->>Loader: exact retained Node3D
else negative hit
Loader->>State: is missing/static-only(path)
State-->>Loader: true; use historical fallback
else uncached
Loader->>Source: existing load/build operation
alt valid prototype
Loader->>State: adopt(path, prototype)
State-->>Loader: canonical Node3D
else failure
Loader->>State: mark missing/static-only(path)
end
end
Loader->>State: shutdown clear_and_release()
State-->>Loader: all paths empty; Nodes released
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
Observer[M2 resource observers] --> State
State --> Node3D
Loader --> Raw[M2RawModelRepository]
Observer --> ResourceLoader
Loader --> StaticBuilder[M2Builder]
Observer --> AnimatedBuilder[M2NativeAnimatedBuilder]
State -. no dependency .-> Raw
State -. no dependency .-> ResourceLoader
State -. no dependency .-> StaticBuilder
State -. no dependency .-> MeshCache[M2MeshResourceCacheState]
```
## Ownership, threading and resources
- Renderer main-thread adapters serialize all operations.
- The state owns strong references to adopted static and animated prototype Nodes.
- A duplicate detached candidate is released immediately; the first valid Node
stays canonical for that path.
- Lookups borrow exact Node references without transferring ownership.
- Detached Nodes are synchronously `free()`d at shutdown. Defensive in-tree
Nodes use `queue_free()` to preserve SceneTree rules.
- Negative entries own only copied normalized-path Strings.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty path/null/invalid Node | Admission guard | Reject/null/false | Dedicated verifier | Correct caller input |
| Unknown positive lookup | Map miss | Return null | Path snapshot | Loader continues existing load/fallback |
| Duplicate candidate | Occupied valid path | Release candidate; return first | Identity fixture | None required |
| Model source/load failure | Loader result | Mark missing outcome | Existing fallback behavior | New loader session/source repair |
| Animation unavailable/unsafe | Observer/native result | Mark static-only outcome | Existing fallback behavior | New loader session/cache repair |
| Shutdown | Loader lifecycle | Release positive and clear all state | Shutdown verifier | New loader begins empty |
| Cancellation | Not owned | No state transition inside this service | N/A | Loader drains/cancels before shutdown clear |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Positive cache lifetime | Final loader shutdown | All | No | Reuses detached prototypes across map refresh |
| Negative cache lifetime | Final loader shutdown | All | No | Preserves historical fallback suppression |
| Eviction capacity | Unbounded historical behavior | All | No | No mid-session Node destruction |
Animation enablement, candidate/allow/deny rules and cache paths remain loader
configuration consumed by the cached observer. Per-frame permits remain loader-owned.
## Persistence, cache and migration
This is runtime-only state and serializes nothing. `.tscn/.glb`, material refresh
and native M2 formats are unchanged; no migration or rebake is introduced.
## Diagnostics and observability
- `diagnostic_snapshot` exposes four sorted path arrays without Node references.
- Cached eligibility rejection logging belongs to the cached observer; native
success logging belongs to the native observer and cached terminal success
logging belongs to the animation resource finalizer. Static terminal missing
outcomes are produced by `M2MeshResourceFinalizer`.
- Existing renderer queue metrics remain unchanged because these tables never
contributed work counts.
- Normalized relative path remains the correlation key.
## Verification
- `verify_m2_prototype_cache_state.gd`: invalid admission, exact static/animated
identity, independent positive/negative states, first-wins duplicate release,
detached/in-tree shutdown ownership, idempotent clear, detached diagnostics,
loader source boundaries and 100-by-256 bounded path-state timing.
- Runtime-cache shutdown regression verifies final release through the real loader.
- Raw repository/finalizer/extractor/Mesh cache/pipeline/material/animation/M2
build regressions protect adjacent behavior.
- Fidelity evidence is exact cache transition and lifetime extraction. No new
proprietary asset or original-client visual comparison is claimed.
## Extension points
Eviction or byte/count budgets require measured memory evidence and explicit
prototype-user lifetime rules. Cached and native animation observers consume
this state without moving ResourceLoader or builder ownership into cache state.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static prototype retention | Implemented extraction | Identity/source/shutdown verifier | Asset-backed traversal/leak run pending |
| Animated prototype retention | Implemented extraction | Independent identity/shutdown verifier | Legal animated success fixture pending |
| Missing/static-only outcomes | Implemented extraction | Independent negative-state verifier | Recovery still requires new loader session |
| Bounded eviction | Historical unbounded behavior | Documented lifetime | Add only after memory profiling |
## Known gaps and risks
- Strong Node references intentionally live until final shutdown.
- Negative outcomes do not retain reason codes and cannot be individually retried,
preserving historical behavior.
- No proprietary long traversal, descriptor pressure, leak, p95/p99 or paired
original-client evidence is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_prototype_cache_state.gd` | Positive Node ownership, negative path state and shutdown release |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw/build and positive/static-only transitions |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Cached terminal positive/static-only transitions |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Static terminal missing transitions |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, observer order, remaining I/O/fallback adapters |
| `src/tools/verify_m2_prototype_cache_state.gd` | Admission/identity/lifecycle/source/timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Integrated final-shutdown release regression |
## Related decisions and references
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md)
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+235
View File
@@ -0,0 +1,235 @@
# M2 Raw Model Repository
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-RAW-MODEL-REPOSITORY-001` |
| Owners | Native M2Loader availability, file existence and raw Dictionary loading |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-raw-model-repository`, 2026-07-17 |
| Profiles/capabilities | Static and native-animated raw M2 reads |
## Purpose
Provide one stateless boundary between renderer orchestration and the optional
native `M2Loader`. The repository resolves an extracted path and returns the raw
static or animated Dictionary consumed by existing builders and classifiers.
## Non-goals
- Normalize relative paths or choose `.tscn`/`.glb` cache fallbacks.
- Cache successful data, missing paths, Meshes, Nodes or animation decisions.
- Build geometry/materials, own queues or attach renderer objects.
- Change native parsing, cache formats, retry policy or visible behavior.
## Context and boundaries
```mermaid
flowchart LR
MeshFinalizer[M2MeshResourceFinalizer] --> Repository[M2RawModelRepository]
Loader[StreamingWorldLoader] --> Repository
Observer[M2NativeAnimationResourceObserver] --> Repository
Repository --> File[Extracted M2 file]
Repository --> Native[ClassDB M2Loader]
Native --> Raw[Raw Dictionary]
Raw --> MeshFinalizer
Raw --> Loader
Raw --> Observer
Loader --> Builder[Existing M2 builders/finalizer]
```
Allowed dependencies are `ProjectSettings`, `FileAccess` and `ClassDB` for the
native file boundary. Resource caches, builders, SceneTree types, worker pools,
renderer policy and other application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `load_static_model_data(extracted_directory, normalized_relative_path)` | Query | Invoke native `load_m2` for one existing extracted file | Synchronous; stateless | Returns `{}` for rejected/unavailable/invalid result |
| `load_animated_model_data(extracted_directory, normalized_relative_path)` | Query | Invoke native `load_m2_animated` for one existing extracted file | Synchronous; stateless | Returns `{}` for rejected/unavailable/invalid result |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Extracted directory String | Loader configuration | Repository path resolution | Copied value | One call |
| Input | Already-normalized relative M2 path String | Loader normalization | Repository | Copied value | One call |
| Input | Extracted `.m2` bytes | Local legal extraction | Native M2Loader | File-owned | Native call |
| Output | Static raw M2 Dictionary | Native `load_m2` | Mesh resource finalizer/M2Builder | Fresh native result | One caller operation |
| Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Native animation observer/builder | Fresh native result | One caller operation |
| Output | Empty Dictionary | Repository guards | Loader fallback and prototype outcome adapter | Fresh value | One failed call |
Side effects are limited to file-existence inspection, synchronous native file
parsing and creation of one temporary native loader instance per accepted call.
## Data flow
```mermaid
flowchart TD
Request[Directory plus normalized path] --> Empty{Path empty?}
Empty -->|yes| Reject[Return empty Dictionary]
Empty -->|no| Class{M2Loader registered?}
Class -->|no| Reject
Class -->|yes| Resolve[globalize directory/path join]
Resolve --> Exists{File exists?}
Exists -->|no| Reject
Exists -->|yes| Instance[Instantiate M2Loader]
Instance --> Method{Instance and method available?}
Method -->|no| Reject
Method -->|yes| Parse[Call static or animated method]
Parse --> Type{Dictionary result?}
Type -->|no| Reject
Type -->|yes| Return[Return raw Dictionary]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Ready: construct repository
Ready --> Resolving: load request
Resolving --> Ready: rejected input/unavailable dependency
Resolving --> Parsing: file and native method available
Parsing --> Ready: Dictionary or empty result returned
Ready --> [*]: caller releases reference
```
No request, result or failure state survives a call.
## Main sequence
```mermaid
sequenceDiagram
participant Caller as Loader or NativeAnimationObserver
participant Repo as M2RawModelRepository
participant File as FileAccess
participant Native as M2Loader
Caller->>Repo: load static/animated(directory, path)
Repo->>File: file_exists(globalized joined path)
alt dependency or file unavailable
Repo-->>Caller: empty Dictionary
else available
Repo->>Native: instantiate and call exact native method
Native-->>Repo: Variant
Repo-->>Caller: Dictionary or empty Dictionary
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository]
Observer[M2NativeAnimationResourceObserver] --> Repository
Repository --> ProjectSettings
Repository --> FileAccess
Repository --> ClassDB
ClassDB --> Native[M2Loader extension]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
Loader --> StaticBuilder[M2Builder]
Observer --> AnimatedBuilder[M2NativeAnimatedBuilder]
Repository -. no dependency .-> Finalizer
Repository -. no dependency .-> StaticBuilder
Repository -. no dependency .-> Cache[Renderer caches/queues]
```
## Ownership, threading and resources
- The repository owns only call-local path, native instance and result values.
- The loader owns path normalization and fallback selection; the Mesh resource
finalizer owns static refresh reads, while the native observer owns the
animated raw-read/build decision;
`M2PrototypeCacheState` owns prototype/negative adoption.
- Native `M2Loader` owns parsing behavior and returns a new Dictionary value.
- Calls are synchronous on the caller's thread; current renderer callers use the
main thread. The module starts no tasks and touches no SceneTree objects.
- No file handle, native instance, Dictionary or engine Resource is retained.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty normalized path | String guard | Return `{}` | Dedicated verifier | Correct caller normalization |
| Native extension absent | `ClassDB.class_exists` | Return `{}` | Existing renderer fallback | Install/load extension and retry |
| Extracted file absent | `FileAccess.file_exists` | Return `{}` | Prototype cache records historical missing state through loader | Restore extraction and start a new loader session |
| Instance/method absent | Null/`has_method` guard | Return `{}` | Source contract verifier | Repair extension registration |
| Parse/non-Dictionary result | Result type check | Return `{}` | Existing loader fallback | Repair asset/parser and retry |
| Cancellation requested | Not supported inside synchronous native call | Call completes | None | Caller controls whether to start call |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Native class name | `M2Loader` | All | No | Exact GDExtension registration |
| Static method | `load_m2` | All | No | Existing static parser path |
| Animated method | `load_m2_animated` | Experimental animated path | No | Existing native animation parser |
| Extracted directory | Loader `extracted_dir` | Scene configuration | Yes, caller-owned | Root used for path join |
## Persistence, cache and migration
The module adds no persistence or cache. Native methods, normalized relative
paths and `.tscn/.glb`/material versions are unchanged, so no migration or rebake
is required.
## Diagnostics and observability
- Logs: none; loader retains existing fallback and debug output.
- Metrics: none; rejected-input timing is verifier-only.
- Debug views: none.
- Correlation ID: normalized relative M2 path in the caller.
## Verification
- `verify_m2_raw_model_repository.gd` covers empty/missing/repeated failures,
exact static/animated method ownership, three loader delegations, forbidden
dependencies and 10,000 empty-path rejects under one second.
- Adjacent finalizer/extractor/cache/pipeline/animation/material/shutdown tests
protect caller behavior and ownership.
- Fidelity evidence is structural: exact path join/globalization and native
method names are preserved. No proprietary M2 or original-client visual claim
is made by this extraction.
## Extension points
Future asset-backed verification may add a legal fixture around these value
contracts. Caching, asynchronous I/O and parser replacement require separate
measured work packages rather than expansion of this repository.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static raw M2 read | Implemented extraction | Source/invalid/missing verifier | Legal success fixture pending |
| Native animated raw M2 read | Implemented extraction | Exact method/source contract | Legal animated fixture pending |
| Stateless failure behavior | Verified synthetic | Repeated missing call and source boundary | Asset-backed corrupt input pending |
## Known gaps and risks
- Native parsing remains synchronous and has no mid-call cancellation.
- The repository deliberately does not distinguish missing dependency, missing
file and parser failure; loader fallback behavior historically treats them alike.
- Proprietary asset traversal, descriptor pressure, p95/p99 and visual comparison
remain outside this structural extraction.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Static refresh repository consumer |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Animated raw-data consumer and builder adapter |
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and negative-result retention |
| `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback decisions and result consumers |
| `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation |
| `src/tools/verify_m2_raw_model_repository.gd` | Rejected-input/source/dependency/timing regression |
## Related decisions and references
- [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+225
View File
@@ -0,0 +1,225 @@
# M2 Runtime Mesh Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-RUNTIME-MESH-FINALIZER-001` |
| Owners | Stale Mesh refresh version, rebuild classification, M2Builder rebuild and fallback |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-mesh-finalizer`, 2026-07-17 |
| Profiles/capabilities | Static cached and prototype M2 runtime material preparation |
## Purpose
Finalize an extracted static M2 Mesh for runtime use by accepting already-loaded
raw M2 data, preserving material refresh version `2`, rebuilding only billboard/
UV-rotation cases and returning the historical original-Mesh fallback.
## Non-goals
- Read raw `.m2` files or call ClassDB/FileAccess/ResourceLoader.
- Own request/finalize queues, Mesh/missing/prototype caches or MultiMeshes.
- Select cache paths or extract a Mesh from the originally loaded Resource.
- Change classifier predicates, builder materials, refresh version or profiles.
## Context and boundaries
```mermaid
flowchart LR
ResourceFinalizer[M2MeshResourceFinalizer] -->|is raw data required?| Finalizer[M2RuntimeMeshFinalizer]
ResourceFinalizer --> Raw[M2RawModelRepository]
Raw --> Finalizer
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
Finalizer --> Builder[M2Builder]
Builder --> Subtree[M2MeshResourceExtractor]
Finalizer -->|prepared Mesh| Cache[M2MeshResourceCacheState]
```
The finalizer may use Mesh metadata, raw value Dictionaries, M2Builder and the
two existing scene-free M2 services. Raw-file I/O, cache state, render permits,
Nodes outside temporary rebuild roots and other application layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `requires_raw_data_for_refresh(mesh)` | Query | Tell loader whether stale Mesh needs raw-file lookup | Renderer main thread | Null/current false |
| `finalize_mesh(path, mesh, raw_data, extracted_directory)` | Command/query | Return current, marked, rebuilt or fallback Mesh | Renderer main thread; one finalize operation | Null Mesh returns null; invalid rebuild falls back |
| `clear()` | Command | Clear memoized classifier decisions | Map reset/final shutdown | Idempotent |
| `cached_rebuild_decision_count()` | Diagnostic query | Expose memoized path-decision count | Tests/diagnostics | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Extracted Mesh and normalized M2 path | Mesh resource finalizer | Finalizer | Borrowed Mesh reference/String copy | One call |
| Input | Already-loaded raw M2 Dictionary | `M2RawModelRepository` through resource finalizer | Classifier/M2Builder | Caller-owned value container | One call |
| Input | Extracted directory path | Loader configuration | M2Builder texture resolution | Copied String | One rebuild |
| Output | Original or rebuilt current Mesh | Finalizer | Cache/prototype adapter | Borrowed/new Resource reference | Cache may retain |
Side effects are Mesh refresh metadata, classifier memoization and temporary
M2Builder prototype creation/destruction during required rebuilds.
## Data flow
```mermaid
flowchart TD
Mesh[Extracted Mesh] --> Current{Refresh version >= 2?}
Current -->|yes| ReturnOriginal[Return original unchanged]
Current -->|no| NeedRaw[Repository supplies raw M2 data through loader]
NeedRaw --> HasData{Raw data available?}
HasData -->|no| Mark[Mark original version 2]
HasData -->|yes| Classify{Billboard or UV rotation?}
Classify -->|no| Mark
Classify -->|yes| Build[M2Builder temporary prototype]
Build --> Extract[M2MeshResourceExtractor first Mesh]
Extract --> Rebuilt{Rebuilt Mesh exists?}
Rebuilt -->|yes| ReturnRebuilt[Return rebuilt Mesh]
Rebuilt -->|no| Mark
Mark --> ReturnOriginal
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Current: Mesh metadata >= 2
[*] --> Stale: Mesh metadata < 2
Stale --> Current: empty/ordinary data marks original
Stale --> Rebuilding: classifier requires rebuild
Rebuilding --> Current: rebuilt Mesh returned
Rebuilding --> Current: failure marks original fallback
Current --> Current: later preparation is no-op
```
Classifier decisions are memoized per normalized path until either existing
map/reset clear site calls `clear()`.
## Main sequence
```mermaid
sequenceDiagram
participant ResourceFinalizer as M2MeshResourceFinalizer
participant Finalizer as M2RuntimeMeshFinalizer
participant Raw as M2Loader boundary
participant Classifier as RebuildClassifier
participant Builder as M2Builder
ResourceFinalizer->>Finalizer: requires_raw_data_for_refresh(mesh)
alt current Mesh
Finalizer-->>ResourceFinalizer: false; reuse Mesh
else stale Mesh
Finalizer-->>ResourceFinalizer: true
ResourceFinalizer->>Raw: load static raw Dictionary
ResourceFinalizer->>Finalizer: finalize_mesh(path, mesh, raw, extracted dir)
Finalizer->>Classifier: needs_runtime_mesh_rebuild(path, raw)
opt rebuild required
Finalizer->>Builder: build(raw, extracted dir)
Finalizer->>Finalizer: extract first Mesh; free prototype
end
Finalizer-->>ResourceFinalizer: rebuilt or marked fallback Mesh
end
```
## Dependency diagram
```mermaid
flowchart TB
ResourceFinalizer[M2MeshResourceFinalizer] --> Finalizer[M2RuntimeMeshFinalizer]
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
Finalizer --> Extractor[M2MeshResourceExtractor]
Finalizer --> Builder[M2Builder]
ResourceFinalizer --> Raw[M2RawModelRepository]
ResourceFinalizer --> Cache[M2MeshResourceCacheState]
Finalizer -. no dependency .-> Raw
Finalizer -. no dependency .-> Cache
```
## Ownership, threading and resources
- Renderer main thread executes metadata changes and M2Builder work.
- `M2RawModelRepository` owns raw M2 file/native calls; the Mesh resource
finalizer supplies its value result.
- Finalizer owns classifier memoization and temporary rebuild prototype lifetime.
- M2Builder owns construction rules; extractor selects the first rebuilt Mesh.
- Cache/prototype adapters decide where the returned Mesh reference is retained.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null Mesh | Guard | Return null | Contract verifier | Caller marks missing |
| Current Mesh | Metadata query | Return exact reference; no raw load | Contract verifier | None needed |
| Raw file/parse unavailable | Empty Dictionary from loader | Mark/reuse original | Existing renderer behavior | Source/cache repair |
| Ordinary stale Mesh | Classifier false | Mark/reuse original | Classifier verifier | None needed |
| Builder produces no Mesh | Extractor null | Mark/reuse original | Finalizer fallback fixture | Source/builder repair |
| Reset/shutdown | Loader lifecycle | Clear classifier decisions | Source/clear verifier | Recompute next session |
## Configuration and capabilities
`MATERIAL_REFRESH_VERSION` remains `2` and is now owned by this finalizer.
Finalize permits, cache paths, extracted directory configuration and quality
profiles remain unchanged in the loader.
## Persistence, cache and migration
No new format is serialized. Existing Mesh metadata key
`wow_m2_material_refresh_version`, M2Builder `MATERIAL_FORMAT_VERSION == 2` and
`.tscn/.glb` caches are unchanged; no migration or rebake is introduced.
## Diagnostics and observability
`cached_rebuild_decision_count` exposes only a scalar count. Existing renderer
logs/queue metrics are unchanged. Normalized M2 path remains the correlation key.
## Verification
- `verify_m2_runtime_mesh_finalizer.gd`: null/current/raw requirement, empty and
ordinary marking, synthetic triangle billboard rebuild, rebuild failure
fallback, refresh metadata, classifier clear, source boundary and 100-by-256
bounded finalization.
- Classifier/extractor/cache/pipeline/material/shutdown/M2 build regressions cover
adjacent behavior. No asset-backed or original-client visual claim is made.
## Extension points
- Asset-backed fixture coverage can be added without changing the finalizer's
value-data contract.
- Refresh versions above `2` require explicit cache/fidelity evidence and docs.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Runtime Mesh refresh/fallback | Implemented extraction | Synthetic transition/rebuild verifier | Asset-backed corrupt/stale cache pending |
| Rebuild predicate | Implemented extraction dependency | Classifier fixtures | Original-client material comparison pending |
| Raw M2 refresh I/O | Implemented repository dependency | Repository source/invalid verifier | Legal success fixture pending |
## Known gaps and risks
- Required rebuild remains synchronous main-thread work behind existing permits.
- Synthetic geometry proves transition/build behavior, not full asset materials.
- No proprietary M2 traversal, descriptor-pressure/leak, p95/p99 or paired
original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh version, classification, rebuild and fallback |
| `src/render/m2/m2_raw_model_repository.gd` | Raw-file/native I/O and Dictionary result |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Repository call and returned-Mesh adoption |
| `src/scenes/streaming/streaming_world_loader.gd` | Service composition, permits and materialization |
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Memoized billboard/UV-rotation predicate |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First rebuilt-Mesh selection |
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | Transition/rebuild/boundary/timing regression |
## Related decisions and references
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
- [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,224 @@
# M2 Runtime Mesh Rebuild Classifier
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001` |
| Owners | Billboard/UV-rotation rebuild decision and per-path memoization |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`, 2026-07-17 |
| Profiles/capabilities | Existing M2 material-refresh path; profile-independent |
## Purpose
Determine whether a stale cached M2 mesh must be rebuilt in memory from raw M2
data to restore billboard custom attributes or UV-rotation material inputs. The
first decision is memoized by normalized relative path for the current map/
runtime-cache lifetime.
## Non-goals
- Read M2 files or invoke `M2Loader`/`M2Builder`.
- Own cached Meshes, scenes, materials, Nodes or RIDs.
- Select cache paths, perform ResourceLoader transitions or consume permits.
- Change refresh/cache format versions or shader behavior.
- Classify animation, particles, ribbons or general visual fidelity.
## Context and boundaries
```mermaid
flowchart LR
Cache[Stale cached M2 Mesh] --> Finalizer[M2RuntimeMeshFinalizer]
Loader[StreamingWorldLoader] --> Raw[M2Loader raw Dictionary]
Raw --> Classifier[M2RuntimeMeshRebuildClassifier]
Classifier --> Decision[Memoized rebuild boolean]
Decision --> Finalizer
Finalizer -->|true| Builder[M2Builder rebuild]
Finalizer -->|false| Refresh[Mark material refresh version]
```
Allowed dependencies are Dictionary/Array, `PackedInt32Array`, `Vector4` and
scalar math. Files, ResourceLoader, parser/builder classes, SceneTree,
WorkerThreadPool, Mesh/Node/RID ownership and other layers are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `needs_runtime_mesh_rebuild(normalized_relative_path, raw_m2_data)` | Memoized query | Detect billboard or referenced UV rotation requirements | Renderer main thread; cached until clear | Invalid variants/indices are skipped; first path decision wins |
| `clear()` | Command | Drop all memoized path decisions | Main thread; map reset/shutdown | Idempotent |
| `cached_path_count()` | Query | Return memoized path count | Main thread | None |
| `diagnostic_snapshot()` | Query | Return detached path/decision map | Main thread; caller-owned | None |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Normalized M2 relative path | Loader cache lookup | Classifier cache key | String value copied | Until clear |
| Input | Raw M2 batches/transforms/combos Dictionary | `M2RawModelRepository` through loader adapter | Classifier | Borrowed; not mutated | First classification call per path |
| Output | Rebuild-required boolean | Classifier | Runtime Mesh finalizer | Scalar | Memoized until clear |
| Output | Detached decision Dictionary | Classifier | Verifier/future diagnostics | Caller-owned copy | Snapshot lifetime |
Side effects are limited to inserting and clearing boolean memoization entries.
## Data flow
```mermaid
flowchart TD
Query[path + raw M2 data] --> Cached{Path cached?}
Cached -->|yes| Return[Return first decision]
Cached -->|no| Billboard{Any billboard flag or positive vertex count?}
Billboard -->|yes| StoreTrue[Store true]
Billboard -->|no| Inputs{Transforms and combos present?}
Inputs -->|no| StoreFalse[Store false]
Inputs -->|yes| Batch[For each Dictionary batch]
Batch --> Stage[Inspect at most four referenced stages]
Stage --> Rotation{Non-identity rotation or speed above epsilon?}
Rotation -->|yes| StoreTrue
Rotation -->|no| StoreFalse
StoreTrue --> Return
StoreFalse --> Return
```
## Lifecycle/state
Each path moves from unknown to one immutable cached boolean. Clear returns all
paths to unknown; no resource lifecycle participates.
```mermaid
stateDiagram-v2
[*] --> Unknown
Unknown --> RebuildRequired: billboard or referenced UV rotation
Unknown --> RebuildNotRequired: no relevant metadata
RebuildRequired --> RebuildRequired: repeated query
RebuildNotRequired --> RebuildNotRequired: repeated query
RebuildRequired --> Unknown: clear
RebuildNotRequired --> Unknown: clear
```
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Native as M2Loader
participant Finalizer as M2RuntimeMeshFinalizer
participant Classifier as M2RuntimeMeshRebuildClassifier
participant Builder as M2Builder
Loader->>Native: load_m2(normalized path)
Native-->>Loader: raw Dictionary
Loader->>Finalizer: finalize_mesh(path, mesh, data, dir)
Finalizer->>Classifier: needs_runtime_mesh_rebuild(path, data)
Classifier-->>Finalizer: memoized boolean
alt rebuild required
Finalizer->>Builder: rebuild mesh from raw data
else no rebuild required
Finalizer->>Finalizer: stamp material refresh version
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[M2RuntimeMeshFinalizer]
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader --> Native[M2Loader]
Loader --> Builder[M2Builder]
Loader --> Cache[M2 Mesh/Scene caches]
Classifier -. no dependency .-> Native
Classifier -. no dependency .-> Builder
Classifier -. no dependency .-> Cache
```
## Ownership, threading and resources
- The classifier owns only normalized-path boolean entries.
- The finalizer owns classifier lifetime, rebuild decisions and refresh metadata.
- The loader owns raw loading, cache/resource selection and Mesh adoption.
- Raw Dictionaries are borrowed and never retained; only the computed boolean is cached.
- Current calls and clears are serialized on the renderer main thread.
- No Node, Resource, Mesh, material or RID reference crosses into the classifier.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Non-Dictionary batch/transform | Type guard | Skip entry or use identity defaults | Synthetic verifier | Correct parser data on next cleared lifetime |
| Missing transforms/combos | Empty guard | No UV-driven rebuild | Synthetic verifier | Billboard classification still applies |
| Invalid combo/transform index | Bounds guard | Skip stage | Synthetic verifier | Correct raw data and clear |
| Repeated path with changed data | Cache hit | Preserve first decision | Memoization fixture | Map/reset clear recomputes |
| Load/build failure | Outside classifier | Existing mesh fallback remains | Loader diagnostics | Existing reload/fallback path |
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Maximum texture stages inspected | `4` | Existing M2 material path | No | Preserves historical four-stage shader boundary |
| UV rotation-speed epsilon | `0.000001` exclusive | Existing M2 material path | No | Larger absolute speed requires rebuild |
| `MATERIAL_REFRESH_VERSION` | Finalizer constant `2` | All | Code version | Gates whether classifier is called |
## Persistence, cache and migration
Memoized booleans are runtime-only and are cleared at the two historical loader
reset/shutdown sites. No persisted cache version or rebuild instruction changes.
## Diagnostics and observability
- Logs: none.
- Metrics: `cached_path_count` is contract-level only and not added to runtime logs.
- Debug views: detached path-to-boolean snapshot.
- Correlation IDs: normalized M2 relative path.
## Verification
- `verify_m2_runtime_mesh_rebuild_classifier.gd`: billboard flag/count, invalid
variants, identity/non-identity rotation, positive/negative speed, exact
epsilon, four-stage cap, invalid indices, memoization, clear, source boundary
and 100-by-256 timing.
- Existing M2 builder/material verifiers cover downstream shader/cache behavior.
- Fidelity evidence is exact predicate/cache extraction. No new asset-backed or
original-client parity claim is made.
- Performance budget: 25,600 classifications and clears under one second.
## Extension points
- A typed raw M2 material descriptor may replace the Dictionary only with parser
fixtures and a compatibility adapter.
- Mesh request/cache/finalization state can be extracted separately without
changing this rebuild predicate.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Billboard rebuild classification | Implemented extraction | Flag/count fixtures | Asset-backed cache refresh timing pending |
| UV-rotation rebuild classification | Implemented extraction | Combo/stage/threshold fixtures | More build-12340 material fixtures pending |
| Runtime memoization lifetime | Implemented extraction | First-decision/clear/source fixtures | Byte/count runtime metric not exposed |
| Runtime Mesh finalization | Implemented extraction | Finalizer transition/rebuild fixtures | Asset-backed material comparison pending |
## Known gaps and risks
- Raw parser data remains Dictionary-based.
- The first decision wins even if different raw data is passed for the same path;
this intentionally preserves prior cache behavior.
- Only four texture stages are inspected, matching current material support.
- No private asset traversal, descriptor-pressure, p95/p99 or paired visual run is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Predicate, thresholds and boolean memoization |
| `src/scenes/streaming/streaming_world_loader.gd` | Raw loading, Mesh rebuild/adoption, refresh version and clear calls |
| `addons/mpq_extractor/loaders/m2_builder.gd` | Mesh/material rebuild implementation |
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | Synthetic predicate/cache/boundary/timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,224 @@
# M2 Static Batch Materializer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented |
| Target/work package | M03 / `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001` |
| Owners | Static M2 MultiMesh construction, render settings and attachment |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-static-batch-materializer`, 2026-07-18 |
| Profiles/capabilities | Existing static M2 placement path |
## Purpose
Build and attach one static M2 `MultiMeshInstance3D` from an already prepared
Mesh and an ordered transform slice. The service preserves the existing
transform format, Mesh identity, batch name, visibility and shadow behavior.
## Non-goals
- Group placements, size batches or advance build-job cursors.
- Load, refresh, build or cache Mesh resources.
- Consume render permits, rotate queues or decide retries/cancellation.
- Assign Editor owners or release tile roots.
- Materialize animated instances or introduce spatial-cell batching.
## Context and boundaries
```mermaid
flowchart LR
Groups[Ordered transform group] --> Loader[StreamingWorldLoader build adapter]
MeshCache[Prepared Mesh cache] --> Loader
Planner[M2BuildBatchPlanner] --> Slice[Start and count]
Slice --> Loader
Loader --> Materializer[M2StaticBatchMaterializer]
Materializer --> Batch[Attached MultiMeshInstance3D]
Loader --> EditorOwner[Editor owner adapter]
```
Allowed dependencies are Godot `Mesh`, `MultiMesh`, `MultiMeshInstance3D`,
`GeometryInstance3D` and main-thread SceneTree APIs. Files, ResourceLoader,
workers, RenderingServer calls, caches, build jobs, scheduler policy, gameplay,
network and Editor ownership are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `materialize_batch(parent, path, mesh, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows)` | Command/query | Create, configure and attach one static MultiMesh batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input returns null; transform bounds are a caller precondition |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | M2 parent root | Loader build job | Batch attachment | Borrowed Node3D | Tile/job lifetime |
| Input | Relative model path and serial | Loader build adapter | Batch node name | Copied values | One call |
| Input | Prepared static Mesh | M2 Mesh resource cache/finalizer | MultiMesh | Borrowed Resource; exact reference retained by MultiMesh | Cache and batch lifetimes |
| Input | Ordered transforms, start and count | Placement group and batch plan | MultiMesh transform buffer | Borrowed Array/scalars | One call |
| Input | Visibility end/margin and shadow flag | Renderer configuration | MultiMeshInstance3D properties | Copied scalars | One call |
| Output | Attached `MultiMeshInstance3D` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned node/resource | Tile lifetime |
Side effects are MultiMesh allocation, transform-buffer mutation,
MultiMeshInstance3D allocation/configuration and SceneTree attachment. The
service retains no direct reference after return.
## Data flow
```mermaid
flowchart TD
Validate[Validate parent, Mesh, transforms and count] --> MultiMesh[Allocate TRANSFORM_3D MultiMesh]
MultiMesh --> MeshIdentity[Assign exact prepared Mesh]
MeshIdentity --> Count[Set instance count]
Count --> Upload[Upload transforms start plus ordered batch offset]
Upload --> Node[Create named MultiMeshInstance3D]
Node --> Render[Apply shadow and optional visibility end/margin]
Render --> Attach[Attach once to supplied parent]
Attach --> Return[Return attached node]
Validate -->|invalid| Null[Return null without attachment]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Validating
Validating --> Rejected: invalid or empty input
Validating --> Building: valid slice
Building --> Configured: transforms and render properties set
Configured --> Attached: parent add_child
Rejected --> [*]
Attached --> [*]: parent/tile owner releases subtree
```
The `RefCounted` service is stateless; states describe a single call.
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant P as M2BuildBatchPlanner
participant M as M2StaticBatchMaterializer
participant R as M2 parent root
L->>P: plan_batch(transform count, offset, static limits)
P-->>L: start/count and cursor transition
L->>L: resolve prepared Mesh or rotate pending job
L->>M: materialize_batch(parent, Mesh, transform slice, render settings)
M->>M: allocate MultiMesh and upload ordered transforms
M->>R: add_child(MultiMeshInstance3D)
M-->>L: attached node
L->>L: assign optional Editor owner
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Materializer[M2StaticBatchMaterializer]
Planner[M2BuildBatchPlanner] --> Loader
MeshState[M2MeshResourceCacheState] --> Loader
Materializer --> Engine[Mesh / MultiMesh / MultiMeshInstance3D]
Loader --> EditorOwner[Editor ownership]
Loader --> Scheduler[RenderBudgetScheduler]
Materializer -. no dependency .-> ResourceIO[ResourceLoader / files]
Materializer -. no dependency .-> BuildState[Build jobs / queues / caches]
```
## Ownership, threading and resources
- The caller borrows a prepared Mesh and transform Array for the synchronous call.
- The new MultiMesh retains the exact Mesh Resource reference.
- The new MultiMeshInstance3D retains the MultiMesh and transfers to the parent.
- Parent/tile teardown owns node and Resource release through the SceneTree.
- Loader assigns an Editor owner after successful attachment when configured.
- All Resource and SceneTree mutation is main-thread-only.
- No RID is acquired or released directly by this service.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null parent or Mesh | Entry guard | Return null without allocation/attachment | Synthetic contract fixture | Correct build composition/cache state |
| Empty transforms or non-positive count | Entry guard | Return null | Synthetic contract fixture | Planner supplies non-empty slice |
| Invalid transform bounds | Caller precondition | Historical indexed access behavior remains | Build planner/source regression | Repair job cursor/group state |
| Mesh pending | Outside service | Loader rotates job without calling service | Existing build/cache diagnostics | Async completion retries job |
| Tile cancelled | Outside service | Loader cancels before materialization or releases parent later | Shutdown regression | Eligible tile may requeue |
The service does not swallow invalid bounds or silently clamp/reorder transforms,
because doing so would change the accepted build-planner contract.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| Transform format | `MultiMesh.TRANSFORM_3D` | All | No | Preserves full 3D M2 placements |
| Static batch count | Loader `m2_multimesh_batch_size` through planner | Quality preset | Yes, between plans | Controls per-batch transform count |
| Visibility end/margin | Loader M2 range / chunk size | Quality preset | Yes, between calls | Applied only when end is positive |
| Shadow mode | Loader `m2_cast_shadows` | Quality preset | Yes, between calls | Selects ON/OFF casting setting |
## Persistence, cache and migration
No state is serialized and no cache/schema version changes. The service consumes
the accepted prepared Mesh cache contract; no rebake or migration is required.
## Diagnostics and observability
- The service emits no logs, metrics or debug views.
- Existing loader queue/hitch metrics continue to account for `m2build` work.
- Path/serial remain visible through the unchanged batch node name.
- Editor ownership remains observable through the existing loader adapter.
## Verification
- `verify_m2_static_batch_materializer.gd` covers invalid inputs, node naming,
exact Mesh identity, transform format/count, ordered slice source contract,
visibility, shadows, single attachment, ownership boundaries and timing.
- Build planner, Mesh cache/extractor/finalizer, prototype, animated materializer,
shutdown, materials, facade and internal-access regressions protect neighbors.
- The headless dummy renderer does not round-trip per-instance transforms through
`MultiMesh.get_instance_transform`; exact ordered upload is protected by the
source contract and render checkpoints remain the observable regression gate.
- Fidelity evidence is exact extraction only; no new build-12340 parity claim.
- Performance budget: 10,000 transform API writes under one second in headless.
## Extension points
Measured spatial-cell batching may later provide smaller transform slices while
reusing this materializer. A generic static/animated base class, async mutation
and callback framework are intentionally excluded.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static M2 MultiMesh materialization | Implemented extraction | Synthetic node/resource/render/source/timing verifier | Asset-backed traversal pending |
| Transform ordering | Implemented extraction | Exact source contract and existing checkpoints | Headless per-instance getter unavailable |
| Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate |
| Spatial-cell batching | Planned | Renderer roadmap | Culling/performance design and evidence pending |
## Known gaps and risks
- Materialization remains synchronous main-thread work under the existing permit.
- MultiMesh transform upload timing in a headless dummy renderer does not measure GPU cost.
- No private asset traversal, visual comparison, descriptor-pressure, leak or p95/p99 run exists.
- Current model-path batching is not culling-driven spatial-cell batching.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh allocation, transform upload, render setup and attachment |
| `src/render/m2/m2_build_batch_planner.gd` | Static/animated batch count and cursor planning |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh reference ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource readiness, job/budget adapter and Editor ownership |
| `src/tools/verify_m2_static_batch_materializer.gd` | Node/resource/render/source/timing regression |
## Related decisions and references
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -0,0 +1,211 @@
# M2 Static Build Resource Observer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001` |
| Owners | Static build Mesh lookup, request selection and missing transition |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-static-build-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh M2 build path |
## Purpose
Produce the static phase of `M2BuildResourceSnapshot`: reuse a prepared Mesh,
wait for an existing request, start the first eligible cache request, or record
a terminal missing model while preserving historical path and GLB rules.
## Non-goals
- Observe animated/native prototypes or finalize threaded loads.
- Own/free Meshes, Nodes, cache entries or snapshot lifetime.
- Materialize instances, plan batches, rotate queues or consume permits.
- Change cache format, candidate order, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> Observer[M2StaticBuildResourceObserver]
MeshCache[M2MeshResourceCacheState] --> Observer
Missing[M2PrototypeCacheState] --> Observer
Pipeline[M2MeshLoadPipelineState] --> Observer
Observer --> Snapshot[M2BuildResourceSnapshot]
Snapshot --> Dispatch[M2BuildDispatchPlanner]
```
## Public API
| Symbol | Kind | Purpose | Errors |
|---|---|---|---|
| `observe(snapshot, path, cache_dir, mesh_cache, prototype_cache, pipeline)` | Command/query | Adopt cached/pending/requested/missing static result | Invalid inputs return `rejected` |
| `cache_resource_paths(cache_dir, path, extensions)` | Pure query | Return unique historical nested/lowercase/basename candidates | Empty values yield fewer/empty candidates |
| `glb_animation_schema(path)` | Read query | Read OpenWC schema marker from GLB JSON | Invalid/missing GLB returns empty String |
| `OUTCOME_*` | Constants | Stable diagnostic transition identifiers | None |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized M2 path/cache directory | Loader | Observer | Copied Strings; one observation |
| Input | Mesh/missing/request states | M2 state services | Observer | Borrowed services; map/session |
| Input/output | Resource snapshot | Loader | Observer/dispatch/materializer | Borrowed; one build operation |
| Output | Cached Mesh or missing/pending values | Observer | Snapshot | Exact borrowed Mesh/scalars |
| Output | Threaded request record | Observer | Mesh pipeline | Pipeline-owned path record |
| Output | Outcome StringName | Observer | Tests/future diagnostics | Immutable scalar |
## Data flow
```mermaid
flowchart TD
Start[Observe static path] --> Cached{Mesh cached?}
Cached -->|yes| AdoptMesh[Adopt exact Mesh; cached]
Cached -->|no| Missing{Already missing?}
Missing -->|yes| AdoptMissing[Adopt missing]
Missing -->|no| Pending{Request pending?}
Pending -->|yes| AdoptPending[Adopt unresolved; pending]
Pending -->|no| Candidates[Generate tscn then glb candidates]
Candidates --> Eligible{Exists and not pivot-prefix GLB?}
Eligible -->|yes| Request[Threaded request]
Request -->|OK or busy| Remember[Remember request; requested]
Eligible -->|none/error| MarkMissing[Mark/adopt missing]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observing
Observing --> Cached
Observing --> Pending
Observing --> Requested
Observing --> Missing
Observing --> Rejected
Cached --> [*]
Pending --> [*]
Requested --> [*]
Missing --> [*]
Rejected --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant O as StaticBuildResourceObserver
participant C as Mesh/prototype cache state
participant P as MeshLoadPipelineState
participant R as ResourceLoader
participant S as M2BuildResourceSnapshot
L->>O: observe(snapshot, path, cache_dir, states)
O->>C: cached/missing lookup
alt cache hit or missing
O->>S: adopt static observation
else pending
O->>P: has_request
O->>S: adopt unresolved observation
else request candidate
O->>R: exists + load_threaded_request
O->>P: remember_request
O->>S: adopt unresolved observation
end
```
## Dependency diagram
```mermaid
flowchart TB
Observer[M2StaticBuildResourceObserver] --> ResourceLoader
Observer --> FileAccess[GLB header/JSON read]
Observer --> MeshCache[M2MeshResourceCacheState]
Observer --> PrototypeCache[M2PrototypeCacheState]
Observer --> Pipeline[M2MeshLoadPipelineState]
Observer --> Snapshot[M2BuildResourceSnapshot]
Observer -. no dependency .-> SceneTree
Observer -. no dependency .-> Materializers
Observer -. no dependency .-> Scheduler
```
## Ownership, threading and resources
- Observer retains no request, cache, snapshot or engine reference.
- Cache/pipeline services retain their existing state and resource ownership.
- Snapshot borrows the exact cached Mesh; observer never frees it.
- Resource existence/request and GLB inspection run on renderer main thread.
- `M2MeshResourceFinalizer` performs terminal polling, Resource extraction,
Mesh preparation and cache/missing adoption from the loader permit loop.
## Errors, cancellation and recovery
| State | Behavior | Recovery |
|---|---|---|
| Invalid dependency/path | Return `rejected`; no mutation | Correct composition |
| Existing request | Return `pending`; no duplicate request | Retry when queue rotates |
| Pivot-prefix GLB | Skip as static candidate | Continue next candidate |
| Request error | Stop historical search and mark missing | Later reset/new session |
| No candidate | Mark/adopt terminal missing | Cache content becomes available in new session |
| Tile cancellation | Observer retains nothing | Loader/queue cancellation remains authoritative |
## Configuration and capabilities
The observer consumes existing `m2_cache_dir`. It adds no profile, cache version,
batch, visibility, shadow or scheduler setting.
## Persistence, cache and migration
No new persistence is introduced. Existing `.tscn/.glb` layout, pipeline records
and missing-cache lifetime are unchanged; no rebake is required.
## Diagnostics and observability
Returned outcomes distinguish rejected/cached/pending/requested/missing without
logging. Existing queue and hitch metrics remain unchanged.
## Verification
- Dedicated verifier covers invalid, cached identity, missing, pending, path
order/deduplication, absent GLB schema, source ownership and bounded timing.
- Snapshot/dispatch/pipeline/cache/shutdown/facade/internal-access/checkpoint
regressions protect adjacent behavior.
- No original-client visual claim is made; this is bookkeeping/I/O extraction.
## Extension points
Animated/native observation may become a separate producer of the animation
phase. Shared cache candidate/GLB inspection can later be reused without adding
a generic resource framework.
## Capability status
| Capability | Status | Evidence | Gap |
|---|---|---|---|
| Static build lookup/request production | Implemented extraction | Lifecycle/path/source/timing verifier | Asset-backed traversal pending |
| Static finalize/preparation | Implemented extraction | Mesh resource finalizer verifier | Asset-backed traversal pending |
| Animated observation | Existing loader-owned | Animation regressions | Separate producer pending |
## Known gaps and risks
- Successful request behavior is protected by source plus existing pipeline
tests; starting an undrained asynchronous request in the unit fixture would leak.
- GLB schema parser reads synchronously, matching the previous loader behavior.
- Private traversal, p95/p99, leak and visual evidence remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_static_build_resource_observer.gd` | Static cache/request/missing observation |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed observation result |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Terminal polling, preparation and adoption |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, permits and action execution |
| `src/tools/verify_m2_static_build_resource_observer.gd` | Lifecycle/path/boundary/timing regression |
## Related decisions and references
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
+23 -17
View File
@@ -15,7 +15,8 @@
Own the mutually exclusive cached, missing and pending states for normalized
lightweight-WMO render paths outside the monolithic streamer. The state holder
accepts only caller-validated `Resource` references and records cache paths for
threaded requests whose I/O lifecycle remains in `StreamingWorldLoader`.
threaded requests whose terminal I/O lifecycle belongs to
`WmoRenderResourceFinalizer`.
## Non-goals
@@ -34,7 +35,8 @@ flowchart LR
State -->|cached Resource| Loader
Loader -->|cache path| ResourceLoader[Godot ResourceLoader]
ResourceLoader -->|status and loaded Resource| Loader
Loader --> Validate[Script and FORMAT_VERSION validation]
Loader --> Finalizer[WmoRenderResourceFinalizer]
Finalizer --> Validate[Script and FORMAT_VERSION validation]
Validate -->|accepted Resource or missing| State
Loader --> Fallback[Cached scene or live-prototype fallback]
```
@@ -53,8 +55,8 @@ or editor dependency. Cache validation stays at the I/O boundary in the loader.
| `has_request(path)` | Query | Test pending threaded-request state | Renderer main thread | Empty returns false |
| `remember_request(path, cache_path)` | Command/query | Record one loader-started request | Renderer main thread; until terminal/reset | Invalid or occupied state returns false |
| `request_paths_snapshot()` | Query | Copy pending normalized/cache-path mapping | Poll or shutdown drain | Detached Dictionary |
| `complete_request_with_resource(path, resource)` | Command/query | Remove pending request and adopt caller-validated Resource | Terminal loader poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending request and adopt negative state | Terminal loader poll | Unknown returns false |
| `complete_request_with_resource(path, resource)` | Command/query | Remove pending request and adopt caller-validated Resource | Terminal finalizer poll | Unknown/null returns false |
| `complete_request_as_missing(path)` | Command/query | Remove pending request and adopt negative state | Terminal finalizer poll | Unknown returns false |
| `clear_transient_state()` | Command | Clear pending and missing while retaining accepted Resources | Map reset/request drain | Idempotent |
| `clear_all()` | Command | Release Resources, pending and missing | Final runtime cache release | Idempotent |
| `pending_request_count()` | Query | Preserve renderer queue metric contribution | Renderer diagnostics | None |
@@ -81,11 +83,11 @@ flowchart TD
Blocked -->|yes| Null[Return null; loader waits/falls back]
Blocked -->|no| Start[Loader starts threaded request]
Start --> Remember[remember_request]
Remember --> Poll[Loader polls detached request snapshot]
Remember --> Poll[Finalizer polls detached request snapshot]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Poll
Terminal -->|failed| Missing[complete as missing]
Terminal -->|loaded| Validate[Loader validates script/version]
Terminal -->|loaded| Validate[Finalizer validates script/version]
Validate -->|valid| Adopt[complete with Resource]
Validate -->|invalid| Missing
```
@@ -113,28 +115,31 @@ stateDiagram-v2
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as WmoRenderResourceFinalizer
participant State as WmoRenderResourceCacheState
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>RL: exists + load_threaded_request(cache path)
Loader->>State: remember_request(normalized, cache path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(cache path)
Loader->>Finalizer: poll_terminal_requests(State)
Finalizer->>State: request_paths_snapshot()
Finalizer->>RL: load_threaded_get_status(cache path)
end
alt load failed
Loader->>State: complete_request_as_missing(normalized)
Finalizer->>State: complete_request_as_missing(normalized)
else loaded
Loader->>RL: load_threaded_get(cache path)
Loader->>Loader: validate script and FORMAT_VERSION
Loader->>State: complete with Resource or as missing
Finalizer->>RL: load_threaded_get(cache path)
Finalizer->>Finalizer: validate script and FORMAT_VERSION
Finalizer->>State: complete with Resource or as missing
end
```
## Ownership, threading and resources
- The state owns three Dictionaries and strong references to accepted Resources.
- The loader owns normalization, cache paths, ResourceLoader calls and validation.
- The loader owns normalization, cache paths and request admission; the finalizer
owns terminal ResourceLoader calls and validation.
- All mutation is serialized by the renderer main-thread lookup/drain lifecycle.
- No mutex or callback is needed; detached request snapshots permit safe removal.
- The loader/build queue borrow Resources without transferring ownership.
@@ -158,8 +163,8 @@ format version, request scheduling and WMO build budgets remain loader-owned.
## Persistence, cache and migration
The state is runtime-only and serializes nothing. `WMOStreamingResource` script
identity and `FORMAT_VERSION` validation remain unchanged in the loader; no cache
migration or rebuild is introduced by this extraction.
identity and `FORMAT_VERSION` validation remain unchanged in the finalizer; no
cache migration or rebuild is introduced by this extraction.
## Diagnostics and observability
@@ -189,7 +194,7 @@ states; Resource references and cache file paths are not exposed. No logs emit.
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Lightweight WMO render Resource state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Cache script/version validation | Preserved in loader | Source boundary and WMO regressions | Dedicated corrupt-cache fixture could follow |
| Cache script/version validation | Implemented extraction | Finalizer source and synthetic validation verifier | Serialized corrupt-cache fixture could follow |
| Cached WMO scene state | Implemented extraction | Scene-cache lifecycle/source/timing verifier | Asset-backed traversal/leak evidence pending |
| WMO materialization | Partial/loader-owned | Queue/planner regressions | Further safe extraction |
@@ -206,7 +211,8 @@ states; Resource references and cache file paths are not exposed. No logs emit.
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_resource_cache_state.gd` | Resource/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalize, request, poll, validate, fallback and shutdown I/O |
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Terminal polling, script/format validation and publication |
| `src/scenes/streaming/streaming_world_loader.gd` | Normalize, request admission, fallback and shutdown order |
| `src/tools/verify_wmo_render_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final Resource ownership regression |
@@ -0,0 +1,219 @@
# WMO Render Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001` |
| Owners | Lightweight WMO terminal status polling, Resource validation and cache/missing publication |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-render-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Profile-independent lightweight WMO render-cache finalization |
## Purpose
Finalize pending lightweight WMO `.res` cache requests outside
`StreamingWorldLoader`: poll terminal status, retrieve a loaded Resource, enforce
exact script/current-format validation and publish either the exact Resource or
the historical missing outcome.
## Non-goals
- Select normalized WMO/cache paths or start threaded requests.
- Finalize cached WMO PackedScenes or live WMOBuilder prototypes.
- Own cache lifetime, placements, build queues, scheduler permits or Nodes.
- Change `WMOStreamingResource.FORMAT_VERSION`, fallback or visible behavior.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] -->|compose and tick| Finalizer[WmoRenderResourceFinalizer]
Cache[WmoRenderResourceCacheState] -->|pending snapshot| Finalizer
Finalizer --> ResourceLoader
Finalizer -->|validated Resource or missing| Cache
Cache --> Build[WMO render build queue]
```
The service may depend on `ResourceLoader`, the injected expected Script and the
render Resource cache-state API. File selection, FileAccess, SceneTree, WMOBuilder,
placement, scheduler and application layers remain outside it.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `poll_terminal_requests(cache_state)` | Command/query | Poll all pending paths once and publish terminal outcomes | Renderer main thread; stateless between calls | Null state returns zero; non-terminal retained |
| `is_current_render_resource(resource)` | Query | Enforce exact injected Script and minimum format | Renderer main thread; stateless | Null/wrong/stale false |
| `load_threaded_get_status(path)` | Boundary query | Read opaque threaded status | Renderer main thread; injectable in tests | ResourceLoader semantics |
| `load_threaded_get(path)` | Boundary query | Retrieve terminal Resource | Renderer main thread; injectable in tests | Null accepted as failed outcome |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Detached normalized-path to `.res` path Dictionary | Render cache state | Finalizer polling | Caller-owned copy | One poll pass |
| Input | Opaque ResourceLoader status | ResourceLoader | Finalizer | Integer value | One path poll |
| Input | Terminal Resource | ResourceLoader | Validator | Borrowed reference | One completion |
| Input | Expected Script/minimum format | Loader composition | Validator | Borrowed/value | Finalizer lifetime |
| Output | Exact validated Resource | Finalizer | Render cache state | Cache adopts strong reference | Until full clear |
| Output | Missing transition | Finalizer | Render cache state | Path-only state | Until transient clear |
## Data flow
```mermaid
flowchart TD
Snapshot[Detached pending snapshot] --> Next[Next path in insertion order]
Next --> Poll[Poll ResourceLoader status]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Retain[Retain pending]
Terminal -->|failed| Missing[Complete as missing]
Terminal -->|loaded| Get[Get terminal Resource]
Get --> Validate{Exact script and format current?}
Validate -->|yes| Adopt[Complete with exact Resource]
Validate -->|no| Missing
Retain --> Next
Adopt --> Next
Missing --> Next
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: non-terminal status
Polling --> Missing: failed/null/wrong/stale
Polling --> Cached: exact script and current format
Pending --> Polling: later tick
Missing --> [*]
Cached --> [*]
```
The finalizer retains no per-path state. The sibling cache state owns all
Pending/Cached/Missing lifetime and reset transitions.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as WmoRenderResourceFinalizer
participant Cache as WmoRenderResourceCacheState
participant RL as ResourceLoader
Loader->>Finalizer: poll_terminal_requests(Cache)
Finalizer->>Cache: request_paths_snapshot()
loop insertion-ordered paths
Finalizer->>RL: load_threaded_get_status(cache path)
alt non-terminal
Finalizer->>Finalizer: retain request
else failed
Finalizer->>Cache: complete_request_as_missing(path)
else loaded
Finalizer->>RL: load_threaded_get(cache path)
Finalizer->>Finalizer: exact Script and format validation
Finalizer->>Cache: complete with Resource or missing
end
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoRenderResourceFinalizer]
Loader --> Cache[WmoRenderResourceCacheState]
Finalizer --> Cache
Finalizer --> ResourceLoader
Finalizer --> Script[WMOStreamingResource Script contract]
Finalizer -. no dependency .-> FileAccess
Finalizer -. no dependency .-> WMOBuilder
Finalizer -. no dependency .-> Node
Finalizer -. no dependency .-> Scheduler
```
## Ownership, threading and resources
- Renderer main thread serializes polling and cache-state publication.
- Finalizer owns no Resource, request, Node, RID or file lifetime.
- Cache state adopts accepted exact Resource references until its full clear.
- Loader owns request admission, fallback/build orchestration and shutdown order.
- Detached snapshots allow terminal cache-state mutation during iteration.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null cache state | Guard | Return zero | Contract verifier | Correct composition |
| Non-terminal request | Status | Retain pending | Pending count | Poll next tick |
| Failed request | Terminal status | Complete missing | Negative cache | Transient reset/rebuilt cache |
| Null/wrong-script Resource | Validation | Complete missing | Synthetic fixture | Rebuild cache and reset |
| Stale format | Format below injected minimum | Complete missing | Synthetic fixture | Rebuild cache and reset |
| Shutdown | Loader lifecycle | Drain then cache clear | Shutdown verifier | New loader starts absent |
## Configuration and capabilities
The loader injects the exact `WMOStreamingResource` Script and its
`FORMAT_VERSION`. The service introduces no setting, profile, permit or cache
format. WMO cache paths and build budgets remain loader-owned.
## Persistence, cache and migration
The module serializes nothing and changes no cache format. It only enforces the
existing script identity and current-or-newer format rule before runtime cache
adoption, so no migration or rebake is introduced.
## Diagnostics and observability
- `poll_terminal_requests` returns terminal completion count for tests/future metrics.
- Existing cache pending count and renderer `wmobuild` metrics are unchanged.
- The service emits no logs; normalized relative path remains the correlation key.
## Verification
- `verify_wmo_render_resource_finalizer.gd`: null/non-terminal, insertion order,
failed/load boundary, null/wrong/stale rejection, current/newer identity,
source ownership and 1,000 terminal polls under one second.
- Cache-state, WMO queue/planner/registry/resolver, shutdown, facade,
internal-access and baseline regressions protect adjacent behavior.
- Fidelity evidence is exact orchestration extraction; no asset-backed visual or
original-client parity claim is made.
## Extension points
- A legal current/stale `.res` fixture can extend validation evidence unchanged.
- PackedScene finalization remains separate because it instantiates a probe Node
and uses WMOBuilder metadata rather than script/format Resource fields.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Lightweight WMO terminal polling | Implemented extraction | Status/order/source/timing verifier | Asset-backed traversal pending |
| Script/format validation | Implemented extraction | Wrong/stale/current/newer fixtures | Legal serialized fixture pending |
| Resource/missing publication | Implemented extraction | Exact identity/negative-state fixtures | Asset-backed lifetime/leak run pending |
| WMO materialization | Loader-owned | Existing build regressions | Further safe extraction pending |
## Known gaps and risks
- ResourceLoader status/get remains synchronous main-thread boundary polling.
- No proprietary WMO corpus, serialized corrupt-version fixture, leak run,
traversal p95/p99 or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Terminal polling, validation and publication |
| `src/render/wmo/wmo_render_resource_cache_state.gd` | Resource/missing/request ownership and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, request admission, fallback and build orchestration |
| `src/resources/wmo_streaming_resource.gd` | Serialized cache contract and format version |
| `src/tools/verify_wmo_render_resource_finalizer.gd` | Terminal-I/O/validation/source/timing regression |
## Related decisions and references
- [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md)
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+212
View File
@@ -0,0 +1,212 @@
# WMO Runtime Mesh Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-RUNTIME-MESH-FINALIZER-001` |
| Owners | Cached WMO runtime Mesh refresh version, surface iteration and material reconstruction |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-mesh-finalizer`, 2026-07-18 |
| Profiles/capabilities | Profile-independent cached WMO material refresh |
## Purpose
Finalize stale cached WMO runtime Mesh materials outside
`StreamingWorldLoader`: preserve exact Mesh identity, stamp the historical
refresh version and reconstruct eligible surface materials through WMOBuilder.
## Non-goals
- Traverse scene Nodes or own Mesh/MultiMesh attachment and lifetime.
- Select WMO cache paths, poll ResourceLoader or schedule render build jobs.
- Change WMOBuilder shaders, textures, blend rules or cache serialization.
- Share an abstraction with the behaviorally different M2 finalizer.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader traversal/build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer]
Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary]
Builder -->|rebuilt Material| Finalizer
Finalizer -->|same Mesh identity| Loader
```
The loader owns scene traversal and composition. The finalizer owns only the
in-place Resource operation and depends on an injected WMO material builder.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `finalize_mesh(mesh, extracted_directory)` | Command/query | Stamp and refresh a stale Mesh in place | Renderer main thread; stateless between calls | Null returns null; unsupported Mesh returns exact identity |
| `rebuild_cached_material(material, extracted_directory)` | Query/boundary | Reconstruct one metadata-bearing cached WMO Material | Renderer main thread; borrowed input | Null/unmarked/missing builder returns null |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | `Mesh` and refresh metadata | Cached WMO scene/render group | Finalizer | Borrowed Resource | Call duration; identity retained |
| Input | Extracted content directory | Loader configuration | WMOBuilder | Borrowed String value | Call duration |
| Input | Texture paths, WMO flags/shader/blend and cached colors | Surface Material metadata/parameters | Finalizer | Borrowed values | One surface rebuild |
| Output | Compact texture-path array and material definition | Finalizer | WMOBuilder | Detached values | One builder call |
| Output | Rebuilt Material | WMOBuilder | ArrayMesh surface | Surface adopts exact Resource | Mesh lifetime |
| Output | Exact input Mesh | Finalizer | Loader traversal/build step | Caller retains ownership | Existing cache/scene lifetime |
## Data flow
```mermaid
flowchart TD
Mesh[Borrowed Mesh] --> Null{Null?}
Null -->|yes| ReturnNull[Return null]
Null -->|no| Current{Refresh version at least 10?}
Current -->|yes| ReturnSame[Return exact Mesh]
Current -->|no| Stamp[Stamp version 10]
Stamp --> Array{ArrayMesh?}
Array -->|no| ReturnSame
Array -->|yes| Surface[Iterate surfaces]
Surface --> Eligible{Material has texture0 metadata?}
Eligible -->|no| Keep[Keep exact Material]
Eligible -->|yes| Definition[Compact paths and copy metadata/colors]
Definition --> Builder[WMOBuilder build material]
Builder --> Result{Non-null result?}
Result -->|yes| Adopt[Replace surface Material]
Result -->|no| Keep
Keep --> Surface
Adopt --> Surface
Surface -->|done| ReturnSame
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Stale: missing/version below 10
[*] --> Current: version at least 10
Stale --> Current: stamp before optional surface refresh
Current --> Current: subsequent calls are identity-only
```
The version stamp is intentionally applied before type/material eligibility, as
in the extracted loader behavior. The finalizer retains no Mesh or Material.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as WmoRuntimeMeshFinalizer
participant Mesh as ArrayMesh
participant Builder as WMOBuilder
Loader->>Finalizer: finalize_mesh(mesh, extracted_directory)
Finalizer->>Mesh: read/stamp refresh metadata
loop each stale ArrayMesh surface
Finalizer->>Mesh: surface_get_material(index)
alt cached WMO metadata exists
Finalizer->>Builder: _build_material(definition, compact paths, directory)
Builder-->>Finalizer: rebuilt Material or null
Finalizer->>Mesh: surface_set_material when non-null
end
end
Finalizer-->>Loader: exact Mesh identity
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer]
Finalizer --> Engine[Mesh / ArrayMesh / Material]
Finalizer --> Builder[Injected WMO material builder]
Finalizer -. no dependency .-> Nodes[Node traversal/lifetime]
Finalizer -. no dependency .-> RL[ResourceLoader]
Finalizer -. no dependency .-> Queue[Build queue/scheduler]
```
## Ownership, threading and resources
- The renderer main thread serializes Mesh metadata and surface mutation.
- The caller owns Mesh identity, scene traversal, attachment and destruction.
- ArrayMesh surfaces adopt only non-null builder results; otherwise the exact
cached Material remains attached.
- The service retains no Resource, Node, RID, file, queue or per-path state.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null Mesh | Guard | Return null | Synthetic contract | Correct caller input |
| Current Mesh | Version metadata | Return exact identity, no builder call | Identity/version contract | Version bump makes it stale |
| Unsupported Mesh subtype | Type check after stamp | Return exact identity | PrimitiveMesh contract | No surface work required |
| Null/unmarked Material | Metadata guard | Preserve exact surface value | Surface contract | Rebuild cache with metadata |
| Missing builder/null result | Boundary guard/result | Preserve exact Material | Synthetic contract | Correct composition/retry after version bump |
| Shutdown | Loader lifecycle | No retained work to cancel | Shutdown suite | New loader composes a new service |
## Configuration and capabilities
Refresh version `10` and metadata key `wow_wmo_material_refresh_version` are the
existing runtime compatibility boundary. No project setting, profile, permit or
feature flag is introduced.
## Persistence, cache and migration
The module writes only the existing runtime Mesh metadata stamp and serializes
nothing. It does not change WMO scene/render cache formats or trigger rebakes.
A future material-rule change must deliberately bump the refresh version.
## Diagnostics and observability
The finalizer emits no logs and allocates no metrics. Existing `wmobuild` queue
metrics and loader diagnostics remain the operational correlation surface.
## Verification
- `verify_wmo_runtime_mesh_finalizer.gd`: null/current/non-ArrayMesh identity,
version stamp, null/unmarked surface retention, compact path indices,
flags/shader/blend/default and shader colors, exact builder adoption,
missing-builder behavior, source ownership and 1,000 current calls under one second.
- Adjacent WMO queue/cache/finalizer, shutdown, material and baseline regressions
protect orchestration and visible output.
- Fidelity evidence is exact behavior-preserving extraction; no private asset or
original-client visual-parity claim is added.
## Extension points
- Asset-backed WMO fixtures may compare reconstructed surface parameters without
changing the service contract.
- A deliberate material refresh change may bump the owned version with old/new
fixtures and paired visual evidence.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Runtime refresh admission | Implemented extraction | Identity/version/type contracts | Serialized cache fixture pending |
| Cached material definition reconstruction | Implemented extraction | Path/metadata/color contracts | Asset-backed visual comparison pending |
| Scene traversal/materialization | Loader-owned | Existing WMO regressions | Further safe extraction pending |
## Known gaps and risks
- Surface mutation and WMOBuilder material construction remain synchronous
main-thread work.
- No proprietary WMO corpus, long leak run, GPU timing, traversal p95/p99 or
paired build-12340 visual capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Refresh admission, surface iteration and material definition reconstruction |
| `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO shader/material construction semantics |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, Node traversal, build jobs and lifetime |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression |
## Related decisions and references
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md)
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+21 -15
View File
@@ -14,8 +14,9 @@
Own mutually exclusive cached, missing and pending states for normalized WMO
scene paths. The state accepts only caller-validated `PackedScene` references and
records `.tscn` paths for threaded requests whose I/O, size limit and cache
metadata validation remain in `StreamingWorldLoader`.
records `.tscn` paths for threaded requests. Size admission remains in
`StreamingWorldLoader`; terminal I/O/probe validation belongs to
`WmoSceneResourceFinalizer`.
## Non-goals
@@ -33,8 +34,8 @@ flowchart LR
Loader --> State[WmoSceneResourceCacheState]
Loader --> Size[File existence and size limit]
Size --> ResourceLoader[Godot ResourceLoader]
ResourceLoader --> Loader
Loader --> Probe[Instantiate, metadata/version check, free]
ResourceLoader --> Finalizer[WmoSceneResourceFinalizer]
Finalizer --> Probe[Instantiate, metadata/version check, free]
Probe -->|accepted PackedScene or missing| State
Loader --> Live[Live-prototype fallback]
```
@@ -82,7 +83,7 @@ flowchart TD
Check -->|allowed| Request[Start threaded request]
Request -->|error| Mark
Request -->|accepted| Remember[remember_request]
Remember --> Poll[Loader polls snapshot]
Remember --> Poll[Scene Resource finalizer polls snapshot]
Poll -->|failure| CompleteMissing[complete as missing]
Poll -->|loaded| Validate[Instantiate and validate cache metadata]
Validate -->|valid| CompleteScene[complete with scene]
@@ -114,24 +115,27 @@ stateDiagram-v2
sequenceDiagram
participant Loader as StreamingWorldLoader
participant State as WmoSceneResourceCacheState
participant Finalizer as WmoSceneResourceFinalizer
participant RL as ResourceLoader
Loader->>State: cached/missing/pending queries
Loader->>Loader: exists and wmo_max_runtime_scene_mb check
Loader->>RL: load_threaded_request(.tscn)
Loader->>State: remember_request(normalized, path)
loop renderer tick
Loader->>State: request_paths_snapshot()
Loader->>RL: load_threaded_get_status(path)
Loader->>Finalizer: poll_terminal_requests(State)
Finalizer->>State: request_paths_snapshot()
Finalizer->>RL: load_threaded_get_status(path)
end
Loader->>RL: load_threaded_get(path)
Loader->>Loader: instantiate, validate WMO metadata, free probe
Loader->>State: complete with scene or as missing
Finalizer->>RL: load_threaded_get(path)
Finalizer->>Finalizer: instantiate, validate WMO metadata, free probe
Finalizer->>State: complete with scene or as missing
```
## Ownership, threading and resources
- State owns three Dictionaries and strong references to accepted PackedScenes.
- Loader owns paths, file measurement, requests, validation and all Node lifetime.
- Loader owns paths, file measurement, request admission and placed Nodes;
finalizer owns terminal I/O and call-local validation-probe lifetime.
- All mutation is serialized on the renderer main thread; no mutex is required.
- Detached request snapshots allow terminal removal while polling.
- Scene instantiation borrows the PackedScene and does not transfer cache ownership.
@@ -143,7 +147,7 @@ sequenceDiagram
| Missing `.tscn` | Loader ResourceLoader existence check | Direct missing state | Transient reset permits later retry |
| Oversize `.tscn` | Loader byte limit | Missing plus unchanged debug log | Raise limit/rebuild, then reset |
| Request-start/load failure | Loader error/status | Direct or terminal missing | Reset and retry later |
| Wrong type/stale metadata | Loader PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
| Wrong type/stale metadata | Finalizer PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
| Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent |
## Configuration and capabilities
@@ -184,8 +188,9 @@ in the loader; the state emits no logs.
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
| Size and cache metadata validation | Preserved in loader | Source boundary and WMO regressions | Oversize/stale asset fixtures could follow |
| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified |
| Size admission | Preserved in loader | Source boundary and WMO regressions | Oversize asset fixture could follow |
| Terminal I/O and cache validation | Implemented extraction | Finalizer type/lifetime/source/timing verifier | Serialized stale fixture pending |
| Live fallback | Loader-owned | Existing runtime behavior | Separate extraction if justified |
## Known gaps and risks
@@ -199,7 +204,8 @@ in the loader; the state emits no logs.
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, requests, validation, fallback and Node lifetime |
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal I/O, probe validation/lifetime and publication |
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, request admission, fallback and placed Nodes |
| `src/tools/verify_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression |
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression |
@@ -0,0 +1,233 @@
# WMO Scene Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001` |
| Owners | Cached WMO PackedScene terminal polling, validation-probe lifetime and scene/missing publication |
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Profile-independent cached WMO `.tscn` finalization |
## Purpose
Finalize pending cached WMO `.tscn` requests outside `StreamingWorldLoader`:
poll terminal status, retrieve a PackedScene, instantiate one validation probe,
apply existing WMOBuilder cache metadata rules, release the probe and publish
either the exact scene or the historical missing outcome.
## Non-goals
- Select paths, measure file size or start ResourceLoader requests.
- Instantiate placed WMO scenes or execute live WMOBuilder fallback.
- Own cache lifetime, placements, build queues, permits or attached Nodes.
- Change cache metadata/version rules, fallback order or visible behavior.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] -->|compose and tick| Finalizer[WmoSceneResourceFinalizer]
Cache[WmoSceneResourceCacheState] -->|pending snapshot| Finalizer
Finalizer --> ResourceLoader
Finalizer --> Probe[Temporary Node3D probe]
Probe --> Validator[WMOBuilder cache validator]
Finalizer -->|PackedScene or missing| Cache
Cache --> Fallback[Placed cached scene or live fallback]
```
The service may depend on ResourceLoader, PackedScene/Node3D lifetime and the
injected cache validator. FileAccess, size policy, placement, attached SceneTree,
scheduler and application layers remain outside it.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `poll_terminal_requests(cache_state)` | Command/query | Poll every pending scene path once and publish terminal outcomes | Renderer main thread; stateless between calls | Null state returns zero; non-terminal retained |
| `is_scene_cache_current(scene)` | Command/query | Instantiate, validate and release one probe | Renderer main thread; call-local Node | Null/no validator/wrong root/stale false |
| `load_threaded_get_status(path)` | Boundary query | Read threaded status | Renderer main thread; injectable tests | ResourceLoader semantics |
| `load_threaded_get(path)` | Boundary query | Retrieve terminal Resource | Renderer main thread; injectable tests | Null/wrong type rejected |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Detached normalized-path to `.tscn` path Dictionary | Scene cache state | Finalizer polling | Caller-owned copy | One pass |
| Input | Opaque load status/terminal Resource | ResourceLoader | Finalizer | Value/borrowed Resource | One path |
| Input | Cached WMO PackedScene | ResourceLoader | Probe validator | Borrowed reference | One completion |
| Input | Metadata validator | Loader composition | Finalizer | Borrowed Object | Finalizer lifetime |
| Output | Exact validated PackedScene | Finalizer | Scene cache state | Cache adopts strong reference | Until full clear |
| Output | Missing transition | Finalizer | Scene cache state | Path-only state | Until transient clear |
| Side effect | Temporary validation Node | PackedScene/finalizer | Validator | Finalizer-owned | Freed before return |
## Data flow
```mermaid
flowchart TD
Snapshot[Detached pending snapshot] --> Poll[Poll next status in insertion order]
Poll --> Terminal{Loaded or failed?}
Terminal -->|no| Retain[Retain pending]
Terminal -->|failed| Missing[Complete missing]
Terminal -->|loaded| Get[Get Resource]
Get --> Type{PackedScene?}
Type -->|no| Missing
Type -->|yes| Instantiate[Instantiate one probe]
Instantiate --> Root{Node3D root?}
Root -->|no| FreeRejected[Free rejected Node] --> Missing
Root -->|yes| Validate[WMOBuilder metadata validation]
Validate --> Free[Free Node3D probe]
Free --> Current{Current?}
Current -->|yes| Adopt[Adopt exact PackedScene]
Current -->|no| Missing
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: non-terminal
Polling --> Probing: loaded PackedScene
Polling --> Missing: failed/null/wrong type
Probing --> Missing: wrong root or stale metadata
Probing --> Cached: current metadata
Probing --> Released: probe freed before outcome
Released --> Missing
Released --> Cached
Pending --> Polling: later tick
```
The finalizer retains no request or Node. Cache state owns Pending/Cached/Missing
lifetime; every instantiated probe is synchronously released before return.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as StreamingWorldLoader
participant Finalizer as WmoSceneResourceFinalizer
participant Cache as WmoSceneResourceCacheState
participant RL as ResourceLoader
participant Validator as WMOBuilder validator
Loader->>Finalizer: poll_terminal_requests(Cache)
Finalizer->>Cache: request_paths_snapshot()
loop insertion-ordered paths
Finalizer->>RL: load_threaded_get_status(path)
alt failed
Finalizer->>Cache: complete_request_as_missing(path)
else loaded
Finalizer->>RL: load_threaded_get(path)
Finalizer->>Finalizer: instantiate Node3D probe
Finalizer->>Validator: is_scene_cache_current(probe)
Finalizer->>Finalizer: free probe
Finalizer->>Cache: complete with exact scene or missing
else non-terminal
Finalizer->>Finalizer: retain request
end
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Finalizer[WmoSceneResourceFinalizer]
Loader --> Cache[WmoSceneResourceCacheState]
Finalizer --> Cache
Finalizer --> ResourceLoader
Finalizer --> PackedScene
Finalizer --> Node3D
Finalizer --> Validator[WMOBuilder cache validator]
Finalizer -. no dependency .-> FileAccess
Finalizer -. no dependency .-> Placement
Finalizer -. no dependency .-> Scheduler
```
## Ownership, threading and resources
- Renderer main thread serializes polling, instantiation and cache publication.
- Finalizer owns every call-local validation probe and frees it before return.
- Non-Node3D rejected roots are also freed, closing the prior leak edge.
- Cache state adopts accepted exact PackedScene references until full clear.
- Loader owns admission/size policy, live fallback, placed Nodes and shutdown order.
## Errors, cancellation and recovery
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Null cache state | Guard | Return zero | Contract verifier | Correct composition |
| Non-terminal | Status | Retain pending | Pending count | Poll later |
| Failed/null/wrong Resource | Status/type | Complete missing | Synthetic fixture | Reset/rebuild cache |
| Non-Node3D root | Probe type | Free probe; complete missing | Type/lifetime fixture | Rebuild cache |
| Stale metadata | Injected validator | Free probe; complete missing | Validation fixture | Rebuild/reset |
| Shutdown | Loader lifecycle | Drain then clear cache | Shutdown verifier | New loader starts absent |
## Configuration and capabilities
The loader injects the existing WMOBuilder cache validator. `wmo_cache_dir`,
`wmo_max_runtime_scene_mb`, debug logging, request timing and WMO budgets remain
loader-owned. No setting, profile or cache version is added.
## Persistence, cache and migration
No data is serialized. Existing WMOBuilder metadata/version acceptance is called
unchanged before adoption. The rejected-root lifetime fix needs no migration or
rebake and does not alter accepted cache output.
## Diagnostics and observability
- Poll returns terminal completion count for tests/future metrics.
- Existing pending count and renderer `wmobuild` metrics remain unchanged.
- No logs are emitted; oversize debug logging remains at loader admission.
- Normalized WMO relative path remains the correlation key.
## Verification
- `verify_wmo_scene_resource_finalizer.gd`: null/non-terminal, insertion order,
failed/load boundary, null/wrong/non-Node3D/stale rejection, current exact
identity, accepted/rejected probe release, source ownership and timing.
- Scene cache, render finalizer/cache, WMO queue/planner/registry/resolver,
shutdown, facade, internal-access and baseline regressions cover neighbors.
- Fidelity evidence is exact orchestration extraction plus rejected-root leak
repair; no asset-backed visual or original-client parity claim is made.
## Extension points
- Legal current/stale serialized `.tscn` fixtures can extend evidence unchanged.
- Placed-scene preparation stays separate because it mutates an attached WMO
instance rather than a call-local validation probe.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Cached WMO scene terminal polling | Implemented extraction | Status/order/source/timing verifier | Asset-backed traversal pending |
| Probe validation/lifetime | Implemented extraction | Current/stale/type/free fixtures | Serialized fixture pending |
| Scene/missing publication | Implemented extraction | Exact identity/negative fixtures | Asset-backed lifetime/leak run pending |
| File-size admission/live fallback | Loader-owned | Existing scene-cache regressions | Separate extraction if justified |
## Known gaps and risks
- PackedScene instantiation/metadata validation remains synchronous main-thread work.
- No proprietary WMO corpus, serialized stale/oversize fixture, long leak run,
traversal p95/p99 or paired original-client capture is included.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal polling, probe validation/lifetime and publication |
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request ownership and resets |
| `src/scenes/streaming/streaming_world_loader.gd` | Admission/size policy, live fallback and placed Node ownership |
| `addons/mpq_extractor/loaders/wmo_builder.gd` | Existing cache metadata/version validation |
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | Terminal/probe/lifetime/source/timing regression |
## Related decisions and references
- [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md)
- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+170 -12
View File
@@ -7,7 +7,7 @@
| Status | Partial |
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
| Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-scene-finalizer`, 2026-07-17 |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -54,6 +54,14 @@ flowchart LR
M2Transform --> Loader
Loader --> M2Batch[M2BuildBatchPlanner]
M2Batch --> Loader
Loader --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> Loader
Loader --> M2Resources[M2BuildResourceSnapshot]
M2Resources --> M2Dispatch
Loader --> M2Queue[M2BuildQueue]
M2Queue --> Loader
Loader --> M2Static[M2StaticBatchMaterializer]
M2Static --> Scene
Loader --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> Loader
Loader --> WmoRegistry[WmoPlacementRegistry]
@@ -132,12 +140,34 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
| `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits |
| `M2BuildResourceSnapshot` | Internal M2 value contract | Carries per-step normalized path, animated/static references and pending/missing observations | Renderer main thread; one build operation | Values retained exactly; release never frees resources |
| `M2StaticBuildResourceObserver.observe` | Internal M2 I/O service | Produces cached/pending/requested/missing static snapshot phase | Renderer main thread; stateless | Invalid composition rejected; no candidate marks missing |
| `M2CachedAnimationResourceObserver.observe` | Internal M2 I/O service | Produces cached/pending/static-only animated snapshot phase | Renderer main thread; stateless | Invalid composition returns empty snapshot; no safe candidate marks static-only |
| `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs |
| `M2StaticBatchMaterializer.materialize_batch` | Internal M2 scene-materialization service | Builds and attaches one prepared-Mesh MultiMesh transform slice | Renderer main thread; stateless after each call | Invalid/empty input returns null; bounds are caller precondition |
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
| `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
| `M2AnimationResourceFinalizer` | Internal M2 terminal-I/O service | Polls cached animation requests, loads/finalizes scenes and publishes prototype outcome | Renderer main thread; stateless across calls | Invalid/failed candidates mark static-only |
| `M2AnimatedSceneFinalizer` | Internal M2 scene-finalization service | Instantiates candidates, repairs materials and requires AnimationPlayer descendants | Renderer main thread; stateless after each call | Invalid/rejected detached roots are freed |
| `M2AnimationPlaybackController` | Internal M2 playback service | Applies stable phase, default imported animation and native animator startup | Renderer main thread; stateless after each call | Missing inventories/names no-op through historical fallbacks |
| `M2AnimatedInstanceMaterializer` | Internal M2 scene-materialization service | Duplicates ordered animated instances, applies render settings, starts playback and attaches a non-empty batch | Renderer main thread; stateless after each call | Invalid/empty input or all failed duplicates returns empty |
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
| `M2MeshResourceExtractor` | Internal M2 scene/resource service | Selects first Mesh from direct Resource, PackedScene or Node subtree | Renderer main thread; stateless except temporary instance | Invalid/no Mesh returns null; temporary PackedScene root freed |
| `M2MeshResourceFinalizer` | Internal M2 terminal-I/O service | Polls static requests, extracts/prepares terminal Meshes and publishes cache/missing outcomes | Renderer main thread; stateless across calls | Invalid/failed Resources mark missing; one call pops at most one terminal record |
| `M2RuntimeMeshFinalizer` | Internal M2 preparation service | Owns refresh version, rebuild classification, M2Builder rebuild and fallback | Renderer main thread; decisions cached until reset | Null returns null; missing/failed rebuild marks and reuses original Mesh |
| `M2RawModelRepository` | Internal M2 native repository | Reads static/animated raw Dictionaries through exact M2Loader methods | Synchronous; stateless | Invalid/unavailable/non-Dictionary result returns empty Dictionary |
| `M2PrototypeCacheState` | Internal M2 prototype cache | Owns detached static/animated Nodes and missing/static-only outcomes | Renderer main thread; final shutdown | Invalid admission rejected; first valid prototype wins |
| `M2NativeAnimationResourceObserver` | Internal native M2 resource observer | Selects GryphonRoost, reads/builds and publishes prototype/static-only outcome | Synchronous renderer main thread; stateless | Invalid/unavailable candidates return null; failures mark static-only |
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
| `WmoSceneResourceFinalizer` | Internal WMO terminal-I/O service | Polls cached scene requests, validates/frees probes and publishes scene/missing outcomes | Renderer main thread; stateless across calls | Failed/wrong/stale scenes complete missing; probes always released |
| `AdtWaterLoadPipelineState` | Internal liquid async-state service | Owns ADT water FIFO/dedupe, active task IDs and mutex result mailbox | Main-thread state; worker result publication | Invalid/duplicate requests rejected; clear does not interrupt workers |
| `AdtWaterSceneFinalizer.attach_water_scene` | Internal liquid main-thread service | Builds and attaches one existing-format ADT Water subtree | Main thread; stateless, returned Node tile-owned | Empty/invalid/dry input returns null without attachment |
@@ -168,6 +198,10 @@ loader configuration remains transitional composition data, not a caller API.
| Internal transform | Rotation/path/scale | Loader or grouper / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
| Internal M2 dispatch plan | Batch count and observed animation/static resource state | Loader / `M2BuildDispatchPlanner` | Loader queue/materializer/progress adapter | Fresh scalar/action Dictionary | One build operation |
| Internal M2 resource observation | Normalized path, optional prototype/Mesh and pending/missing flags | Loader / M2 observers / `M2BuildResourceSnapshot` | Dispatch planner and loader materializer adapter | Snapshot borrows exact engine references | One build operation |
| Internal M2 pending build | Tile key, M2 root, grouped transforms and cursors | Loader / `M2BuildQueue` | Loader readiness, planner and materializer adapters | Queue-owned job/keys and strong references | Until finish/cancel/clear/replacement |
| Internal static M2 materialization | Parent, prepared Mesh, ordered transform slice and render settings | Loader / `M2StaticBatchMaterializer` | Attached MultiMeshInstance3D | Parent owns node/MultiMesh; exact Mesh reference retained | One main-thread build batch |
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
@@ -175,6 +209,12 @@ loader configuration remains transitional composition data, not a caller API.
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
| Internal raw M2 read | Extracted directory and normalized relative path | Loader/native observer via `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
| Internal animated M2 load | Normalized path, cached GLB path and opaque terminal status | Cached observer / loader / `M2AnimationLoadPipelineState` | Loader poll/finalize adapters | State-owned String/status records; detached pending snapshots | Request through terminal finalize/reset |
| Internal animated M2 candidate | Loaded PackedScene, static material root and detached candidate | Loader / `M2AnimatedSceneFinalizer` | Loader prototype adoption/static fallback | Finalizer owns until rejection or exact-root transfer | One main-thread finalize permit |
| Internal animated M2 playback | Prototype/instance native animators, path/index and player inventory | Materializer/finalizer / `M2AnimationPlaybackController` | Native and imported animation state | Borrowed Nodes; detached optional diagnostics | One duplicated instance startup |
| Internal animated M2 materialization | Parent, prototype, ordered transform slice and render settings | Loader / `M2AnimatedInstanceMaterializer` | Attached animated batch and indexed diagnostics | Parent owns batch; caller owns detached diagnostics | One main-thread build batch |
| Internal M2 prototype state | Normalized path, adopted static/animated Node or negative outcome | Loader / `M2PrototypeCacheState` | Loader reuse/fallback adapters | State-owned strong Node refs and Strings; detached diagnostics | Until final shutdown |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
@@ -222,7 +262,12 @@ flowchart TD
M2Registry --> M2Grouper[M2PlacementGrouper]
M2Transform[M2PlacementTransformResolver] --> M2Grouper
M2Grouper --> M2Batch[M2BuildBatchPlanner]
M2Batch --> M2
M2Grouper --> M2Queue[M2BuildQueue]
M2Queue --> M2Batch
M2Batch --> M2Resources[M2BuildResourceSnapshot]
M2Resources --> M2Dispatch[M2BuildDispatchPlanner]
M2Dispatch --> M2Static[M2StaticBatchMaterializer]
M2Static --> M2
R --> WmoPlacement[WmoPlacementResolver]
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
@@ -346,25 +391,74 @@ sequenceDiagram
grouper owns worker-path final transforms; direct placeholder/instance
transforms and every build/render side effect remain loader-owned.
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
The loader retains tasks, mutex/result queues, stale checks and build state.
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
loader retains queue/resource transitions, cursor adoption and materialization.
The loader retains tasks, mutex/result queues and stale-result checks; accepted
groups enter `M2BuildQueue` as typed pending jobs.
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans.
`M2BuildDispatchPlanner` is stateless and owns only call-local action/transition
plans after the loader observes resource availability. `M2BuildResourceSnapshot`
carries those per-step observations and borrows exact prototype/Mesh references
without controlling engine lifetime.
`M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform
references and group/offset/serial cursors without freeing engine objects.
`M2StaticBuildResourceObserver` owns static Mesh lookup/request admission and
snapshot adoption. `M2CachedAnimationResourceObserver` owns cached animated
prototype lookup, GLB policy/request admission and snapshot production.
`M2NativeAnimationResourceObserver` owns native candidate selection, raw read,
detached build and prototype/static-only cache outcome.
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; the
loader retains action execution, cursor adoption, root cleanup, budgets and
Editor ownership.
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
identity and transform values. `WmoPlacementRegistry` owns only placement-key
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
strong root/resource references without freeing engine objects.
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
and pending cache paths. `WmoSceneResourceCacheState` similarly owns validated
PackedScenes, negative entries and pending `.tscn` paths. The loader retains
ResourceLoader/FileAccess I/O, size and cache-version validation, live fallback,
materialization, permits, validity reactions and every Node lifecycle action.
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
similarly owns validated PackedScenes, negative entries and pending `.tscn`
paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and
validation-probe lifetime. The loader retains request admission, FileAccess
size checks, live fallback, materialization, permits, validity reactions and
every placed-Node lifecycle action.
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
active task IDs and the worker-safe parsed-result mailbox. The loader retains
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits,
stale tile/path/root checks and `water_loaded` timing. `AdtWaterSceneFinalizer`
performs the existing ADTBuilder call, tile attach and optional recursive
Editor ownership on the main thread without retaining the generated subtree.
- `M2RuntimeMeshRebuildClassifier` owns only normalized-path boolean decisions
for billboard/UV-rotation material refresh and is composed by the runtime Mesh
finalizer. The raw repository loads value data; loader retains Mesh adoption.
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
terminal statuses and completion-order finalize FIFO. The static observer owns
cache path selection, request admission and initial snapshot adoption. The
Mesh resource finalizer owns ResourceLoader polling/finalize and terminal
adoption; loader retains permits and composition, while prototype cache state
owns shared missing outcomes.
- `M2AnimationLoadPipelineState` owns animated M2 pending Resource paths, opaque
terminal statuses and completion-order finalize FIFO. The cached animation
observer owns allow/deny/path/GLB selection, request admission and initial
snapshot production. The native observer owns synchronous native build; the
animation resource finalizer owns cached terminal polling/load/finalize and
adoption. Loader retains permits and material-prototype lookup.
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
them at the existing final-shutdown site. Prototype state and materialization
belong to the sibling cache service and loader respectively.
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
PackedScene instance destruction. The static observer admits ResourceLoader
requests and initial cache/missing adoption; the Mesh resource finalizer owns
terminal extraction/adoption, while the loader retains materialization.
- `M2RuntimeMeshFinalizer` owns refresh version `2`, classifier lifetime,
M2Builder rebuild and original-Mesh fallback. `M2MeshResourceFinalizer` loads
raw data only after the runtime finalizer reports that a cached Mesh is stale.
- `M2RawModelRepository` owns FileAccess/ClassDB availability and the exact
`load_m2`/`load_m2_animated` calls. The loader retains normalization, fallback
order and every result consumer; `M2PrototypeCacheState` retains outcomes.
- `M2PrototypeCacheState` owns exact static/animated prototype references,
missing-model and static-animation path sets, duplicate-candidate release and
final-shutdown Node cleanup. ResourceLoader, builders and fallback decisions
remain loader-owned.
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -502,16 +596,33 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|---|---|---|---|
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
| M2 static batch materializer | Implemented extraction | Synthetic node/Mesh/render/source/timing contract | Asset-backed traversal/visual/GPU p95/p99 pending |
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Spatial cells and asset-backed p95/p99 remain pending |
| M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending |
| M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending |
| M2 static build resource observer | Implemented extraction | Cache/pending/path/request/source/timing contract | Asset-backed traversal pending |
| M2 cached animation resource observer | Implemented extraction | Cache/pending/policy/GLB/request/source/timing contract | Asset-backed traversal pending |
| M2 native animation resource observer | Implemented extraction | Candidate/cache/raw/build/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending |
| M2 animation resource finalizer | Implemented extraction | Status/FIFO/load/candidate/repair/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending |
| M2 mesh resource finalizer | Implemented extraction | Status/FIFO/load/extract/prepare/adoption/source/timing contract | Asset-backed traversal/leak/p95/p99 pending |
| M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending |
| M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending |
| M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending |
| M2 animation playback controller | Implemented extraction | Synthetic phase/selection/loop/native-copy/source/timing contract | Asset-backed animation timing/name/native comparison pending |
| M2 animated instance materializer | Implemented extraction | Synthetic order/transform/render/playback/attachment/source/timing contract | Asset-backed traversal/visual/leak/p95/p99 pending |
| M2 prototype cache state | Implemented extraction | Synthetic identity/negative/lifecycle/source/timing plus shutdown contract | Asset-backed traversal/leak/p95/p99 pending |
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | ResourceLoader I/O and asset-backed traversal/leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | ResourceLoader/live-fallback extraction and asset-backed traversal/leak evidence pending |
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
| WMO scene Resource finalizer | Implemented extraction | Status/order/type/probe/lifetime/adoption/source/timing contract | Serialized stale/oversize and asset-backed evidence pending |
| WMO runtime Mesh finalizer | Implemented extraction | Identity/version/material-definition/source/timing contract | Asset-backed visual/leak/GPU/p95/p99 evidence pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
@@ -565,6 +676,30 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Full-quality terrain revisit Mesh retention and LRU ownership |
| `src/render/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox |
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Animated M2 pending Resource paths, terminal statuses and finalize FIFO |
| `src/render/m2/m2_animation_resource_finalizer.gd` | Cached animation terminal polling/load/finalize outcomes |
| `src/render/m2/m2_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation |
| `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
| `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment |
| `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh construction, render setup and attachment |
| `src/render/m2/m2_build_dispatch_planner.gd` | M2 wait/materializer/advance action priority and transition plan |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step animated/static observations and borrowed references |
| `src/render/m2/m2_static_build_resource_observer.gd` | Static Mesh lookup/request/missing snapshot production |
| `src/render/m2/m2_cached_animation_resource_observer.gd` | Cached animated GLB policy/request/snapshot production |
| `src/render/m2/m2_build_job.gd` | M2 root/groups references and group/offset/serial progress |
| `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle |
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Static terminal polling, Mesh preparation and cache/missing adoption |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Runtime refresh version, rebuild classification/build and fallback |
| `src/render/m2/m2_raw_model_repository.gd` | Stateless static/animated native M2 file boundary |
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation |
| `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes |
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication |
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication |
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
@@ -579,6 +714,29 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_terrain_quality_mesh_cache.gd` | Terrain cache admission, LRU, ownership and loader-boundary regression |
| `src/tools/verify_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression |
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Animated M2 request/terminal/FIFO/boundary/timing regression |
| `src/tools/verify_m2_animation_resource_finalizer.gd` | Animated M2 polling/load/finalize/adoption/boundary/timing regression |
| `src/tools/verify_m2_mesh_resource_finalizer.gd` | Static M2 polling/extraction/preparation/adoption/boundary/timing regression |
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression |
| `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression |
| `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression |
| `src/tools/verify_m2_static_batch_materializer.gd` | Static M2 node/Mesh/render/attachment/boundary/timing regression |
| `src/tools/verify_m2_build_dispatch_planner.gd` | M2 dispatch priority/action/transition/boundary/timing regression |
| `src/tools/verify_m2_build_resource_snapshot.gd` | M2 observation identity/adoption/lifetime/boundary/timing regression |
| `src/tools/verify_m2_static_build_resource_observer.gd` | Static lookup/request/path/boundary/timing regression |
| `src/tools/verify_m2_cached_animation_resource_observer.gd` | Cached animation lifecycle/policy/GLB/boundary/timing regression |
| `src/tools/verify_m2_build_queue.gd` | M2 job/FIFO/progress/lifetime/boundary/timing regression |
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression |
| `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/timing regression |
| `src/tools/verify_m2_native_animation_resource_observer.gd` | Native candidate/cache/raw/build/adoption/boundary/timing regression |
| `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression |
| `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression |
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression |
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,120 @@
class_name M2AnimatedInstanceMaterializer
extends RefCounted
## Duplicates one ordered animated M2 batch, applies instance render settings,
## starts playback and attaches a non-empty batch to the supplied parent.
## All calls mutate Godot scene objects and therefore belong on the main thread.
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animated_scene_finalizer.gd"
)
const M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT := preload(
"res://src/render/m2/m2_animation_playback_controller.gd"
)
var _animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
var _animation_playback_controller := M2_ANIMATION_PLAYBACK_CONTROLLER_SCRIPT.new()
## Returns the attached batch root and detached diagnostic entries. Invalid or
## empty input, and a batch whose duplicates all fail, returns an empty result.
## The caller owns the returned/attached batch through `m2_parent_root`.
func materialize_batch(
m2_parent_root: Node3D,
relative_path: String,
prototype: Node3D,
transforms: Array,
start_index: int,
instance_count: int,
batch_serial: int,
visibility_range_end: float,
visibility_range_margin: float,
cast_shadows: bool,
native_animator_script: Script,
collect_native_diagnostics: bool
) -> Dictionary:
if (
m2_parent_root == null
or transforms.is_empty()
or instance_count <= 0
or prototype == null
):
return {}
var model_name := relative_path.get_file().get_basename()
var batch_root := Node3D.new()
batch_root.name = "%s_anim_%d" % [model_name, batch_serial]
var diagnostic_entries: Array[Dictionary] = []
for batch_offset in instance_count:
var instance_index := start_index + batch_offset
var instance := prototype.duplicate(
Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS
) as Node3D
if instance == null:
continue
instance.name = "%s_%d" % [model_name, instance_index]
instance.transform = transforms[instance_index]
_apply_visibility_range_recursive(
instance,
visibility_range_end,
visibility_range_margin
)
_apply_shadow_cast_recursive(instance, cast_shadows)
_animation_playback_controller.copy_native_animator_data(
prototype,
instance,
native_animator_script
)
batch_root.add_child(instance)
var animation_players := _animated_scene_finalizer.animation_players_in_subtree(
instance
)
var native_diagnostics := _animation_playback_controller.start_instance_playback(
instance,
relative_path,
instance_index,
native_animator_script,
animation_players,
collect_native_diagnostics
)
for native_diagnostic in native_diagnostics:
diagnostic_entries.append({
"instance_index": instance_index,
"state": native_diagnostic,
})
if batch_root.get_child_count() <= 0:
batch_root.queue_free()
return {}
m2_parent_root.add_child(batch_root)
return {
"batch_root": batch_root,
"native_diagnostics": diagnostic_entries,
}
func _apply_visibility_range_recursive(
node: Node,
range_end: float,
range_end_margin: float
) -> void:
if range_end <= 0.0:
return
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.visibility_range_end = range_end
geometry.visibility_range_end_margin = range_end_margin
for child in node.get_children():
_apply_visibility_range_recursive(child, range_end, range_end_margin)
func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void:
if node is GeometryInstance3D:
var geometry := node as GeometryInstance3D
geometry.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
for child in node.get_children():
_apply_shadow_cast_recursive(child, cast_shadows)
@@ -0,0 +1 @@
uid://dkgy3rvsmlkvm
@@ -0,0 +1,115 @@
class_name M2AnimatedSceneFinalizer
extends RefCounted
## Instantiates and validates detached animated M2 scene candidates and repairs
## their surface materials from an existing static material prototype.
## The caller owns ResourceLoader I/O, permits and prototype cache adoption.
## Instantiates one PackedScene as a detached Node3D candidate. Unsupported
## Resources return null; a created non-Node3D root is freed before return.
func instantiate_candidate(resource: Resource) -> Node3D:
if not (resource is PackedScene):
return null
var candidate_root := (resource as PackedScene).instantiate()
if candidate_root is Node3D:
return candidate_root as Node3D
if candidate_root != null:
candidate_root.free()
return null
## Applies the historical depth-first source-to-target material mapping. A null
## source or target leaves the candidate unchanged.
func repair_materials(animated_root: Node3D, material_source_root: Node3D) -> void:
if animated_root == null or material_source_root == null:
return
var source_meshes := mesh_instances_in_subtree(material_source_root)
var target_meshes := mesh_instances_in_subtree(animated_root)
if source_meshes.is_empty() or target_meshes.is_empty():
return
var fallback_material := _first_mesh_material(source_meshes)
for target_index in target_meshes.size():
var target := target_meshes[target_index]
if target == null or target.mesh == null:
continue
var source := source_meshes[mini(target_index, source_meshes.size() - 1)]
for surface_index in target.mesh.get_surface_count():
var material := _mesh_instance_surface_material(source, surface_index)
if material == null:
material = fallback_material
if material != null:
target.set_surface_override_material(surface_index, material)
## Accepts a detached candidate only when it contains an AnimationPlayer.
## Rejected candidates are freed synchronously. A successful result transfers
## the exact prototype reference and its AnimationPlayer count to the caller.
func finalize_candidate(candidate_root: Node3D) -> Dictionary:
if candidate_root == null:
return {}
var animation_players := animation_players_in_subtree(candidate_root)
if animation_players.is_empty():
candidate_root.free()
return {}
return {
"prototype": candidate_root,
"animation_player_count": animation_players.size(),
}
## Returns MeshInstance3D descendants in depth-first preorder, including root.
func mesh_instances_in_subtree(root: Node) -> Array[MeshInstance3D]:
var mesh_instances: Array[MeshInstance3D] = []
if root != null:
_collect_mesh_instances(root, mesh_instances)
return mesh_instances
## Returns AnimationPlayer descendants in depth-first preorder, including root.
func animation_players_in_subtree(root: Node) -> Array[AnimationPlayer]:
var animation_players: Array[AnimationPlayer] = []
if root != null:
_collect_animation_players(root, animation_players)
return animation_players
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
if node is MeshInstance3D:
result.append(node as MeshInstance3D)
for child in node.get_children():
_collect_mesh_instances(child, result)
func _collect_animation_players(node: Node, result: Array[AnimationPlayer]) -> void:
if node is AnimationPlayer:
result.append(node as AnimationPlayer)
for child in node.get_children():
_collect_animation_players(child, result)
func _first_mesh_material(meshes: Array[MeshInstance3D]) -> Material:
for mesh_instance in meshes:
if mesh_instance == null or mesh_instance.mesh == null:
continue
for surface_index in mesh_instance.mesh.get_surface_count():
var material := _mesh_instance_surface_material(mesh_instance, surface_index)
if material != null:
return material
return null
func _mesh_instance_surface_material(
mesh_instance: MeshInstance3D,
surface_index: int
) -> Material:
if mesh_instance == null or mesh_instance.mesh == null:
return null
if surface_index < mesh_instance.get_surface_override_material_count():
var override_material := mesh_instance.get_surface_override_material(surface_index)
if override_material != null:
return override_material
if surface_index < mesh_instance.mesh.get_surface_count():
return mesh_instance.mesh.surface_get_material(surface_index)
return null
@@ -0,0 +1 @@
uid://cjjim3v675w66
@@ -0,0 +1,102 @@
class_name M2AnimationLoadPipelineState
extends RefCounted
## Owns animated M2 threaded-load request records and the terminal finalize FIFO.
## The caller owns ResourceLoader I/O, permits, Nodes and prototype outcomes.
var _request_by_normalized_path: Dictionary = {}
var _finalize_records: Array[Dictionary] = []
## Records one normalized M2 path and ResourceLoader path. Empty or duplicate
## values are rejected without mutation.
func remember_request(normalized_relative_path: String, resource_path: String) -> bool:
if normalized_relative_path.is_empty() or resource_path.is_empty():
return false
if _request_by_normalized_path.has(normalized_relative_path):
return false
_request_by_normalized_path[normalized_relative_path] = {
"normalized": normalized_relative_path,
"path": resource_path,
}
return true
## Returns whether one normalized path currently has a pending threaded request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_by_normalized_path.has(normalized_relative_path)
)
## Returns detached pending records in their existing Dictionary insertion order.
func request_records_snapshot() -> Array[Dictionary]:
var records: Array[Dictionary] = []
for request_variant in _request_by_normalized_path.values():
var request: Dictionary = request_variant
records.append(request.duplicate())
return records
## Removes one pending request and appends its terminal status to the finalize
## FIFO. Unknown paths are rejected without mutation.
func complete_request(normalized_relative_path: String, terminal_status: int) -> bool:
if not _request_by_normalized_path.has(normalized_relative_path):
return false
var request: Dictionary = _request_by_normalized_path[normalized_relative_path]
_request_by_normalized_path.erase(normalized_relative_path)
var finalize_record := request.duplicate()
finalize_record["status"] = terminal_status
_finalize_records.append(finalize_record)
return true
## Removes one pending request without enqueuing finalization.
func discard_request(normalized_relative_path: String) -> bool:
return _request_by_normalized_path.erase(normalized_relative_path)
## Returns whether a terminal record is waiting for budgeted finalization.
func has_finalize_record() -> bool:
return not _finalize_records.is_empty()
## Removes and returns the oldest terminal record. Empty means none is ready.
func pop_finalize_record() -> Dictionary:
if _finalize_records.is_empty():
return {}
return _finalize_records.pop_front()
## Returns pending plus terminal-finalize work for existing renderer metrics.
func total_work_count() -> int:
return _request_by_normalized_path.size() + _finalize_records.size()
## Returns the pending request count.
func pending_request_count() -> int:
return _request_by_normalized_path.size()
## Returns the terminal finalize FIFO count.
func finalize_record_count() -> int:
return _finalize_records.size()
## Clears request/finalize bookkeeping. The caller must drain ResourceLoader
## requests before orderly shutdown clear.
func clear() -> void:
_request_by_normalized_path.clear()
_finalize_records.clear()
## Returns detached request and finalize records without loaded Resources/Nodes.
func diagnostic_snapshot() -> Dictionary:
var finalize_records: Array[Dictionary] = []
for record in _finalize_records:
finalize_records.append(record.duplicate())
return {
"requests": request_records_snapshot(),
"finalize_records": finalize_records,
}
@@ -0,0 +1 @@
uid://h5rk6njgaujd
@@ -0,0 +1,114 @@
class_name M2AnimationPlaybackController
extends RefCounted
## Applies deterministic playback to one animated M2 instance and copies the
## runtime fields of exact-script native animators. The materializer owns
## instance duplication/attachment; its caller owns diagnostic log formatting.
## Copies the five historical runtime fields between matching native animators
## in depth-first order. References are intentionally retained exactly.
func copy_native_animator_data(
source_root: Node,
target_root: Node,
native_animator_script: Script
) -> void:
var source_animators := native_animators_in_subtree(source_root, native_animator_script)
var target_animators := native_animators_in_subtree(target_root, native_animator_script)
for animator_index in range(mini(source_animators.size(), target_animators.size())):
var source := source_animators[animator_index]
var target := target_animators[animator_index]
target.mesh_instance_path = source.mesh_instance_path
target.bones = source.bones
target.surfaces = source.surfaces
target.animation_length = source.animation_length
target.playback_speed = source.playback_speed
## Prepares native animators, applies deterministic phase, and starts supplied
## AnimationPlayers with the historical loop/name/seek rules. Runtime debug
## state is sampled only when requested and returned as detached Dictionaries.
func start_instance_playback(
root: Node,
relative_path: String,
instance_index: int,
native_animator_script: Script,
animation_players: Array[AnimationPlayer],
collect_native_diagnostics: bool
) -> Array[Dictionary]:
var phase := phase_for_instance(relative_path, instance_index)
var native_diagnostics: Array[Dictionary] = []
for animator in native_animators_in_subtree(root, native_animator_script):
if animator.has_method("prepare_runtime"):
animator.prepare_runtime()
animator.set_phase(phase)
if collect_native_diagnostics and animator.has_method("runtime_debug_state"):
var diagnostic_variant = animator.runtime_debug_state()
if diagnostic_variant is Dictionary:
native_diagnostics.append((diagnostic_variant as Dictionary).duplicate(true))
for player in animation_players:
if player == null:
continue
var animation_name := choose_default_animation(player, relative_path)
if animation_name.is_empty():
continue
for available_name in player.get_animation_list():
var available_animation := player.get_animation(available_name)
if available_animation != null:
available_animation.loop_mode = Animation.LOOP_LINEAR
player.play(animation_name)
var selected_animation := player.get_animation(animation_name)
if selected_animation != null and selected_animation.length > 0.0:
player.seek(selected_animation.length * phase, true)
return native_diagnostics
## Returns exact-script native animators in depth-first preorder.
func native_animators_in_subtree(
root: Node,
native_animator_script: Script
) -> Array[Node]:
var native_animators: Array[Node] = []
if root != null and native_animator_script != null:
_collect_native_animators(root, native_animator_script, native_animators)
return native_animators
## Returns the existing stable path/index phase in the inclusive-lower,
## exclusive-upper range [0, 1).
func phase_for_instance(relative_path: String, instance_index: int) -> float:
return float(abs(("%s:%d" % [relative_path, instance_index]).hash()) % 1000) / 1000.0
## Chooses the historical default animation name for ordinary, fish and bird
## paths, then case-insensitive substring and first-name fallbacks.
func choose_default_animation(player: AnimationPlayer, relative_path: String = "") -> String:
if player == null:
return ""
var lower_path := relative_path.to_lower()
var candidates: Array[String] = ["Stand", "Idle", "Run", "Walk"]
if lower_path.contains("fish"):
candidates = ["Run", "Walk", "Swim", "Stand", "Idle", "Death"]
elif lower_path.contains("eagle") or lower_path.contains("bird") or lower_path.contains("gull"):
candidates = ["Run", "Walk", "Swim", "Stand", "Idle"]
for candidate in candidates:
if player.has_animation(candidate):
return candidate
var available_names := player.get_animation_list()
for candidate in ["run", "walk", "swim", "stand", "idle", "loop"]:
for available_name in available_names:
if String(available_name).to_lower().contains(candidate):
return String(available_name)
return String(available_names[0]) if not available_names.is_empty() else ""
func _collect_native_animators(
node: Node,
native_animator_script: Script,
result: Array[Node]
) -> void:
if node.get_script() == native_animator_script:
result.append(node)
for child in node.get_children():
_collect_native_animators(child, native_animator_script, result)
@@ -0,0 +1 @@
uid://c6oln217sml80
@@ -0,0 +1,187 @@
class_name M2AnimationResourceFinalizer
extends RefCounted
## Polls cached animated M2 ResourceLoader requests and finalizes terminal scenes.
## The caller owns permits, material-source lookup and SceneTree materialization.
const M2_ANIMATED_SCENE_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_animated_scene_finalizer.gd"
)
var _animated_scene_finalizer: RefCounted
var _resource_loader_adapter: Object
func _init(
animated_scene_finalizer: RefCounted = null,
resource_loader_adapter: Object = null
) -> void:
_animated_scene_finalizer = animated_scene_finalizer
if _animated_scene_finalizer == null:
_animated_scene_finalizer = M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new()
_resource_loader_adapter = resource_loader_adapter
## Moves terminal threaded requests into the pipeline finalize FIFO.
## Empty resource paths retain the historical immediate static-only outcome.
func poll_terminal_requests(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> int:
if load_pipeline_state == null or prototype_cache_state == null:
return 0
var completed_request_count := 0
var request_records: Array = load_pipeline_state.call(
"request_records_snapshot"
)
for request_variant in request_records:
if not (request_variant is Dictionary):
continue
var request: Dictionary = request_variant
var normalized_relative_path := String(request.get("normalized", ""))
var resource_path := String(request.get("path", ""))
if resource_path.is_empty():
load_pipeline_state.call(
"discard_request",
normalized_relative_path
)
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
completed_request_count += 1
continue
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
load_pipeline_state.call(
"complete_request",
normalized_relative_path,
load_status
)
completed_request_count += 1
return completed_request_count
## Pops one terminal record and prepares a detached animated scene candidate.
## An empty result means the record was skipped or resolved as static-only.
func prepare_next_candidate(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> Dictionary:
if load_pipeline_state == null or prototype_cache_state == null:
return {}
var terminal_record: Dictionary = load_pipeline_state.call(
"pop_finalize_record"
)
if terminal_record.is_empty():
return {}
var normalized_relative_path := String(
terminal_record.get("normalized", "")
)
if (
normalized_relative_path.is_empty()
or bool(prototype_cache_state.call(
"has_animated_prototype",
normalized_relative_path
))
or bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
))
):
return {}
if int(terminal_record.get(
"status",
ResourceLoader.THREAD_LOAD_FAILED
)) != ResourceLoader.THREAD_LOAD_LOADED:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return {}
var resource_path := String(terminal_record.get("path", ""))
var loaded_resource := load_threaded_get(resource_path)
var candidate := _animated_scene_finalizer.call(
"instantiate_candidate",
loaded_resource
) as Node3D
if candidate == null:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return {}
return {
"normalized": normalized_relative_path,
"path": resource_path,
"candidate": candidate,
}
## Repairs, validates and adopts one prepared candidate. The preparation must be
## completed synchronously so its detached candidate always receives an owner.
func finalize_prepared_candidate(
preparation: Dictionary,
material_source_root: Node3D,
prototype_cache_state: RefCounted,
debug_logging_enabled: bool = false
) -> Node3D:
if prototype_cache_state == null:
return null
var normalized_relative_path := String(preparation.get("normalized", ""))
var candidate := preparation.get("candidate", null) as Node3D
if normalized_relative_path.is_empty() or candidate == null:
return null
_animated_scene_finalizer.call(
"repair_materials",
candidate,
material_source_root
)
var finalization: Dictionary = _animated_scene_finalizer.call(
"finalize_candidate",
candidate
)
var prototype := finalization.get("prototype", null) as Node3D
if prototype == null:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
var canonical_prototype := prototype_cache_state.call(
"adopt_animated_prototype",
normalized_relative_path,
prototype
) as Node3D
if debug_logging_enabled:
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [
normalized_relative_path,
String(preparation.get("path", "")),
int(finalization.get("animation_player_count", 0)),
])
return canonical_prototype
## Production ResourceLoader status adapter; injectable in synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader terminal-result adapter; injectable in tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://2rnsjucg3dul
@@ -0,0 +1,47 @@
class_name M2BuildDispatchPlanner
extends RefCounted
## Selects the next M2 build action from already observed resource state.
## The planner retains no state or engine-object references.
const ACTION_WAIT_FOR_ANIMATION := &"wait_for_animation"
const ACTION_MATERIALIZE_ANIMATED := &"materialize_animated"
const ACTION_WAIT_FOR_STATIC_MESH := &"wait_for_static_mesh"
const ACTION_MATERIALIZE_STATIC := &"materialize_static"
const ACTION_ADVANCE_WITHOUT_MATERIALIZATION := &"advance_without_materialization"
## Returns a detached action/transition plan for one M2 build operation.
func plan_step(
batch_count: int,
resource_snapshot: RefCounted
) -> Dictionary:
if resource_snapshot == null:
return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH)
if bool(resource_snapshot.call("animation_request_pending")):
return _wait_plan(ACTION_WAIT_FOR_ANIMATION)
if batch_count <= 0:
return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, false)
if bool(resource_snapshot.call("has_animated_prototype")):
return _advance_plan(ACTION_MATERIALIZE_ANIMATED, true)
if bool(resource_snapshot.call("has_static_mesh")):
return _advance_plan(ACTION_MATERIALIZE_STATIC, true)
if bool(resource_snapshot.call("static_model_missing")):
return _advance_plan(ACTION_ADVANCE_WITHOUT_MATERIALIZATION, true)
return _wait_plan(ACTION_WAIT_FOR_STATIC_MESH)
func _wait_plan(action: StringName) -> Dictionary:
return {
"action": action,
"rotate_queue": true,
"increment_batch_serial": false,
}
func _advance_plan(action: StringName, increment_batch_serial: bool) -> Dictionary:
return {
"action": action,
"rotate_queue": false,
"increment_batch_serial": increment_batch_serial,
}
@@ -0,0 +1 @@
uid://df0bv1x2x4j2x
+78
View File
@@ -0,0 +1,78 @@
class_name M2BuildJob
extends RefCounted
## Holds one M2 build cursor, grouped transforms and a strong root reference.
## Releasing this record never frees the referenced Node.
var _tile_key: String
var _root: Node3D
var _groups: Dictionary
var _group_keys: Array
var _group_index: int = 0
var _transform_offset: int = 0
var _batch_serial: int = 0
func _init(tile_key: String, root: Node3D, groups: Dictionary) -> void:
_tile_key = tile_key
_root = root
_groups = groups
_group_keys = groups.keys()
## Returns the immutable tile identity used by the queue.
func tile_key() -> String:
return _tile_key
## Returns the strongly referenced M2 scene root without transferring ownership.
func root() -> Node3D:
return _root
## Returns the exact grouped-transform Dictionary supplied at enqueue time.
func groups() -> Dictionary:
return _groups
## Returns the insertion-order group-key snapshot created at enqueue time.
func group_keys() -> Array:
return _group_keys
## Returns the current model-path group cursor.
func group_index() -> int:
return _group_index
## Returns the current transform offset within the selected model-path group.
func transform_offset() -> int:
return _transform_offset
## Returns the next static/animated batch serial used for node naming.
func batch_serial() -> int:
return _batch_serial
## Atomically adopts all progress values after one planned build operation.
func adopt_progress(
next_group_index: int,
next_transform_offset: int,
next_batch_serial: int
) -> void:
_group_index = next_group_index
_transform_offset = next_transform_offset
_batch_serial = next_batch_serial
## Returns detached scalar/count diagnostics without engine-object references.
func diagnostic_snapshot() -> Dictionary:
return {
"tile_key": _tile_key,
"group_index": _group_index,
"transform_offset": _transform_offset,
"batch_serial": _batch_serial,
"group_count": _group_keys.size(),
"has_root": _root != null,
}
+1
View File
@@ -0,0 +1 @@
uid://drawyd5qmel7j
+169
View File
@@ -0,0 +1,169 @@
class_name M2BuildQueue
extends RefCounted
## Owns pending M2 build-job records and FIFO tile keys. Stale FIFO keys are
## intentional: erasing a job never removes its queued key.
const JOB_SCRIPT := preload("res://src/render/m2/m2_build_job.gd")
var _jobs_by_tile_key: Dictionary = {}
var _queued_tile_keys: Array[String] = []
## Enqueues a typed job. Duplicate tile keys replace the keyed job and append
## another FIFO entry, matching raw Dictionary assignment plus Array append.
func enqueue(tile_key: String, root: Node3D, groups: Dictionary) -> bool:
if tile_key.is_empty() or root == null or groups.is_empty():
return false
_jobs_by_tile_key[tile_key] = JOB_SCRIPT.new(tile_key, root, groups)
_queued_tile_keys.append(tile_key)
return true
## Returns whether at least one FIFO key, including a stale key, is pending.
func has_pending() -> bool:
return not _queued_tile_keys.is_empty()
## Returns the front FIFO tile key, or an empty String when the queue is empty.
func front_key() -> String:
if _queued_tile_keys.is_empty():
return ""
return _queued_tile_keys.front()
## Pops and returns the front FIFO key, or an empty String when already empty.
func pop_front() -> String:
if _queued_tile_keys.is_empty():
return ""
return _queued_tile_keys.pop_front()
## Moves the front FIFO key to the tail and reports whether a key existed.
func rotate_front() -> bool:
if _queued_tile_keys.is_empty():
return false
_queued_tile_keys.append(_queued_tile_keys.pop_front())
return true
## Returns whether a tile key still has a current job record.
func has_job(tile_key: String) -> bool:
return _jobs_by_tile_key.has(tile_key)
## Returns the current job as its engine base type, or null for a stale key.
func job_for(tile_key: String) -> RefCounted:
return _jobs_by_tile_key.get(tile_key, null) as RefCounted
## Returns the borrowed root for a current job, or null for a stale key.
func root_for(tile_key: String) -> Node3D:
var job := job_for(tile_key)
if job == null:
return null
return job.call("root") as Node3D
## Returns the exact grouped-transform Dictionary, or an empty Dictionary.
func groups_for(tile_key: String) -> Dictionary:
var job := job_for(tile_key)
if job == null:
return {}
return job.call("groups") as Dictionary
## Returns the borrowed group-key snapshot, or an empty Array for a stale key.
func group_keys_for(tile_key: String) -> Array:
var job := job_for(tile_key)
if job == null:
return []
return job.call("group_keys") as Array
## Returns the current model-path group cursor, or zero for a stale key.
func group_index_for(tile_key: String) -> int:
var job := job_for(tile_key)
if job == null:
return 0
return int(job.call("group_index"))
## Returns the current in-group transform offset, or zero for a stale key.
func transform_offset_for(tile_key: String) -> int:
var job := job_for(tile_key)
if job == null:
return 0
return int(job.call("transform_offset"))
## Returns the current batch serial, or zero for a stale key.
func batch_serial_for(tile_key: String) -> int:
var job := job_for(tile_key)
if job == null:
return 0
return int(job.call("batch_serial"))
## Atomically adopts all progress values for a current job.
func adopt_progress(
tile_key: String,
next_group_index: int,
next_transform_offset: int,
next_batch_serial: int
) -> bool:
var job := job_for(tile_key)
if job == null:
return false
job.call(
"adopt_progress",
next_group_index,
next_transform_offset,
next_batch_serial
)
return true
## Erases only the keyed job and intentionally leaves every FIFO entry stale.
## Returns whether an active record existed. Engine objects are never freed.
func erase_job(tile_key: String) -> bool:
var had_job := _jobs_by_tile_key.has(tile_key)
_jobs_by_tile_key.erase(tile_key)
return had_job
## Returns a detached snapshot of active job keys for loader-owned cleanup.
func job_keys() -> Array:
return _jobs_by_tile_key.keys()
## Releases all job/key references without freeing referenced engine objects.
func clear() -> void:
_jobs_by_tile_key.clear()
_queued_tile_keys.clear()
## Returns FIFO entry count, including duplicate and stale keys.
func pending_count() -> int:
return _queued_tile_keys.size()
## Returns active keyed job count, excluding stale FIFO keys.
func active_job_count() -> int:
return _jobs_by_tile_key.size()
## Returns detached FIFO order and tile-key-sorted scalar job diagnostics.
func diagnostic_snapshot() -> Dictionary:
var tile_keys: Array = _jobs_by_tile_key.keys()
tile_keys.sort()
var jobs: Array[Dictionary] = []
for tile_key_variant in tile_keys:
var tile_key := String(tile_key_variant)
var job := job_for(tile_key)
if job != null:
jobs.append(job.call("diagnostic_snapshot") as Dictionary)
return {
"queued_tile_keys": _queued_tile_keys.duplicate(),
"jobs": jobs,
}
+1
View File
@@ -0,0 +1 @@
uid://57qrpiqqgpr4
@@ -0,0 +1,72 @@
class_name M2BuildResourceSnapshot
extends RefCounted
## Holds one M2 build-step resource observation without owning engine lifetime.
var _normalized_relative_path: String
var _animated_prototype: Node3D
var _animation_request_pending: bool
var _static_mesh: Mesh = null
var _static_model_missing: bool = false
func _init(
normalized_relative_path: String,
animated_prototype: Node3D,
animation_request_pending: bool
) -> void:
_normalized_relative_path = normalized_relative_path
_animated_prototype = animated_prototype
_animation_request_pending = animation_request_pending
## Returns the normalized M2 path observed by the loader.
func normalized_relative_path() -> String:
return _normalized_relative_path
## Returns the borrowed animated prototype without transferring ownership.
func animated_prototype() -> Node3D:
return _animated_prototype
## Returns whether an animated prototype was observed.
func has_animated_prototype() -> bool:
return _animated_prototype != null
## Returns whether the animation request remains pending.
func animation_request_pending() -> bool:
return _animation_request_pending
## Adopts the optional static Mesh and terminal missing-model observation.
func adopt_static_observation(static_mesh: Mesh, static_model_missing: bool) -> void:
_static_mesh = static_mesh
_static_model_missing = static_model_missing
## Returns the borrowed static Mesh without transferring ownership.
func static_mesh() -> Mesh:
return _static_mesh
## Returns whether a prepared static Mesh was observed.
func has_static_mesh() -> bool:
return _static_mesh != null
## Returns whether the static model lookup reached a terminal missing outcome.
func static_model_missing() -> bool:
return _static_model_missing
## Returns detached path/availability diagnostics without engine references.
func diagnostic_snapshot() -> Dictionary:
return {
"normalized_relative_path": _normalized_relative_path,
"has_animated_prototype": has_animated_prototype(),
"animation_request_pending": _animation_request_pending,
"has_static_mesh": has_static_mesh(),
"static_model_missing": _static_model_missing,
}
@@ -0,0 +1 @@
uid://cgleoy15rtmby
@@ -0,0 +1,238 @@
class_name M2CachedAnimationResourceObserver
extends RefCounted
## Observes or requests the cached-GLB animated phase of one M2 build step.
const M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT := preload(
"res://src/render/m2/m2_build_resource_snapshot.gd"
)
## Returns a snapshot containing the exact cached prototype or pending state.
## Invalid composition and terminal failures produce a non-pending snapshot.
func observe(
normalized_relative_path: String,
cache_directory: String,
maximum_primitive_count: int,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray,
prototype_cache_state: RefCounted,
load_pipeline_state: RefCounted,
debug_logging_enabled: bool = false
) -> RefCounted:
var empty_snapshot: RefCounted = M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
false
)
if (
normalized_relative_path.is_empty()
or prototype_cache_state == null
or load_pipeline_state == null
):
return empty_snapshot
var cached_prototype := prototype_cache_state.call(
"find_animated_prototype",
normalized_relative_path
) as Node3D
if cached_prototype != null:
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
cached_prototype,
false
)
if bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
)):
return empty_snapshot
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
true
)
var cache_resource_path := find_eligible_glb_cache_path(
normalized_relative_path,
cache_directory,
maximum_primitive_count,
allowlist_patterns,
denylist_patterns,
debug_logging_enabled
)
if cache_resource_path.is_empty():
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return empty_snapshot
var request_error := ResourceLoader.load_threaded_request(
cache_resource_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE
)
if request_error == OK or request_error == ERR_BUSY:
load_pipeline_state.call(
"remember_request",
normalized_relative_path,
cache_resource_path
)
return M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(
normalized_relative_path,
null,
true
)
prototype_cache_state.call("mark_animation_static", normalized_relative_path)
return empty_snapshot
## Returns the first eligible historical `.glb` cache candidate.
func find_eligible_glb_cache_path(
normalized_relative_path: String,
cache_directory: String,
maximum_primitive_count: int,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray,
debug_logging_enabled: bool = false
) -> String:
if not is_animation_path_allowed(
normalized_relative_path,
allowlist_patterns,
denylist_patterns
):
return ""
for cache_resource_path in cache_resource_paths(
cache_directory,
normalized_relative_path
):
if not ResourceLoader.exists(cache_resource_path):
continue
if glb_cache_is_safe_for_runtime_animation(
cache_resource_path,
maximum_primitive_count,
debug_logging_enabled
):
return cache_resource_path
return ""
## Applies the historical stripped, case-insensitive allow/deny substring policy.
func is_animation_path_allowed(
normalized_relative_path: String,
allowlist_patterns: PackedStringArray,
denylist_patterns: PackedStringArray
) -> bool:
if normalized_relative_path.is_empty() or allowlist_patterns.is_empty():
return false
var lowercase_path := normalized_relative_path.to_lower()
var allowed := false
for pattern in allowlist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lowercase_path.contains(needle):
allowed = true
break
if not allowed:
return false
for pattern in denylist_patterns:
var needle := String(pattern).strip_edges().to_lower()
if not needle.is_empty() and lowercase_path.contains(needle):
return false
return true
## Returns historical nested/lowercase/basename `.glb` candidates without I/O.
func cache_resource_paths(
cache_directory: String,
relative_path: String
) -> PackedStringArray:
var normalized := relative_path.replace("\\", "/")
var lowercase := normalized.to_lower()
var stems := [
normalized.get_basename(),
lowercase.get_basename(),
normalized.get_file().get_basename(),
lowercase.get_file().get_basename(),
]
var result := PackedStringArray()
for stem in stems:
if stem.is_empty():
continue
var cache_resource_path := cache_directory.path_join(stem + ".glb")
if not result.has(cache_resource_path):
result.append(cache_resource_path)
return result
## Checks animation presence, primitive limit and accepted OpenWC schema.
func glb_cache_is_safe_for_runtime_animation(
cache_resource_path: String,
maximum_primitive_count: int,
debug_logging_enabled: bool = false
) -> bool:
var gltf := read_glb_json(cache_resource_path)
if gltf.is_empty():
return false
var animations: Array = gltf.get("animations", [])
if animations.is_empty():
return false
var primitive_count := glb_primitive_count(gltf)
if maximum_primitive_count > 0 and primitive_count > maximum_primitive_count:
return false
var schema := glb_animation_schema(gltf)
if schema == "pivot_prefix_v1" or schema.is_empty():
return true
if debug_logging_enabled:
print(
"M2_ANIM_REJECT schema=%s primitives=%d cache=%s"
% [schema, primitive_count, cache_resource_path]
)
return false
## Reads the JSON chunk of a version-2 GLB or returns an empty Dictionary.
func read_glb_json(cache_resource_path: String) -> Dictionary:
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
if not FileAccess.file_exists(absolute_path):
return {}
var file := FileAccess.open(absolute_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return {}
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return {}
var json_length := int(file.get_32())
var chunk_type := file.get_32()
if json_length <= 0 or chunk_type != 0x4e4f534a:
return {}
var parsed: Variant = JSON.parse_string(
file.get_buffer(json_length).get_string_from_utf8()
)
return parsed as Dictionary if parsed is Dictionary else {}
## Counts all glTF Mesh primitives using the historical safety rule.
func glb_primitive_count(gltf: Dictionary) -> int:
var count := 0
var meshes: Array = gltf.get("meshes", [])
for mesh_variant in meshes:
if mesh_variant is Dictionary:
var primitives: Array = (mesh_variant as Dictionary).get(
"primitives",
[]
)
count += primitives.size()
return count
## Returns the OpenWC animation schema marker from parsed glTF metadata.
func glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
@@ -0,0 +1 @@
uid://d0ptej41k2smt
@@ -0,0 +1,103 @@
class_name M2MeshLoadPipelineState
extends RefCounted
## Owns static M2 threaded-load request records and the terminal finalize FIFO.
## The caller owns ResourceLoader I/O, permits, Mesh caches and materialization.
var _request_by_normalized_path: Dictionary = {}
var _finalize_records: Array[Dictionary] = []
## Records one normalized M2 path and ResourceLoader path. Empty or duplicate
## values are rejected without mutation.
func remember_request(normalized_relative_path: String, resource_path: String) -> bool:
if normalized_relative_path.is_empty() or resource_path.is_empty():
return false
if _request_by_normalized_path.has(normalized_relative_path):
return false
_request_by_normalized_path[normalized_relative_path] = {
"normalized": normalized_relative_path,
"path": resource_path,
}
return true
## Returns whether one normalized path currently has a pending threaded request.
func has_request(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _request_by_normalized_path.has(normalized_relative_path)
)
## Returns detached pending records in their existing Dictionary insertion order.
func request_records_snapshot() -> Array[Dictionary]:
var records: Array[Dictionary] = []
for request_variant in _request_by_normalized_path.values():
var request: Dictionary = request_variant
records.append(request.duplicate())
return records
## Removes one pending request and appends its terminal status to the finalize
## FIFO. Unknown paths are rejected without mutation.
func complete_request(normalized_relative_path: String, terminal_status: int) -> bool:
if not _request_by_normalized_path.has(normalized_relative_path):
return false
var request: Dictionary = _request_by_normalized_path[normalized_relative_path]
_request_by_normalized_path.erase(normalized_relative_path)
var finalize_record := request.duplicate()
finalize_record["status"] = terminal_status
_finalize_records.append(finalize_record)
return true
## Removes one pending request without enqueuing finalization. Returns whether a
## record was removed.
func discard_request(normalized_relative_path: String) -> bool:
return _request_by_normalized_path.erase(normalized_relative_path)
## Returns whether a terminal record is waiting for budgeted finalization.
func has_finalize_record() -> bool:
return not _finalize_records.is_empty()
## Removes and returns the oldest terminal record. Empty means none is ready.
func pop_finalize_record() -> Dictionary:
if _finalize_records.is_empty():
return {}
return _finalize_records.pop_front()
## Returns pending plus terminal-finalize work for existing renderer metrics.
func total_work_count() -> int:
return _request_by_normalized_path.size() + _finalize_records.size()
## Returns the pending request count.
func pending_request_count() -> int:
return _request_by_normalized_path.size()
## Returns the terminal finalize FIFO count.
func finalize_record_count() -> int:
return _finalize_records.size()
## Clears request/finalize bookkeeping. The caller must drain ResourceLoader
## requests before orderly shutdown clear.
func clear() -> void:
_request_by_normalized_path.clear()
_finalize_records.clear()
## Returns detached request and finalize records without loaded Resources/Meshes.
func diagnostic_snapshot() -> Dictionary:
var finalize_records: Array[Dictionary] = []
for record in _finalize_records:
finalize_records.append(record.duplicate())
return {
"requests": request_records_snapshot(),
"finalize_records": finalize_records,
}
@@ -0,0 +1 @@
uid://dadsxuswq0tnd
@@ -0,0 +1,50 @@
class_name M2MeshResourceCacheState
extends RefCounted
## Owns normalized-path references to prepared static M2 Mesh resources.
## The caller owns loading, preparation, missing-state and materialization.
var _mesh_by_normalized_path: Dictionary = {}
## Stores one prepared Mesh for a non-empty normalized M2 path. A later store
## for the same path replaces the retained reference.
func store_mesh(normalized_relative_path: String, mesh: Mesh) -> bool:
if normalized_relative_path.is_empty() or mesh == null:
return false
_mesh_by_normalized_path[normalized_relative_path] = mesh
return true
## Returns whether one normalized path currently has a retained Mesh.
func has_mesh(normalized_relative_path: String) -> bool:
return (
not normalized_relative_path.is_empty()
and _mesh_by_normalized_path.has(normalized_relative_path)
)
## Returns the exact retained Mesh reference, or null for empty/unknown paths.
func find_mesh(normalized_relative_path: String) -> Mesh:
if not has_mesh(normalized_relative_path):
return null
return _mesh_by_normalized_path[normalized_relative_path] as Mesh
## Returns the number of retained static M2 Mesh resources.
func mesh_count() -> int:
return _mesh_by_normalized_path.size()
## Returns a detached insertion-order list of cached normalized paths.
func normalized_paths_snapshot() -> Array[String]:
var normalized_paths: Array[String] = []
for normalized_path_variant in _mesh_by_normalized_path.keys():
normalized_paths.append(String(normalized_path_variant))
return normalized_paths
## Releases all retained Mesh references. The loader calls this only after
## asynchronous work has completed during final scene shutdown.
func clear() -> void:
_mesh_by_normalized_path.clear()
@@ -0,0 +1 @@
uid://dhkyjcrdblgkd
@@ -0,0 +1,34 @@
class_name M2MeshResourceExtractor
extends RefCounted
## Selects the first Mesh from loaded M2 Resources or existing Node subtrees.
## The caller owns ResourceLoader I/O, cache adoption and material preparation.
## Returns the direct Mesh Resource or the first Mesh in a temporary PackedScene
## instance. The temporary instance is freed before this method returns.
func extract_first_mesh(resource: Resource) -> Mesh:
if resource is Mesh:
return resource as Mesh
if not (resource is PackedScene):
return null
var temporary_root := (resource as PackedScene).instantiate()
if temporary_root == null:
return null
var mesh := find_first_mesh_in_subtree(temporary_root)
temporary_root.free()
return mesh
## Returns the first MeshInstance3D Mesh in depth-first preorder. The subtree
## and Mesh ownership remain unchanged.
func find_first_mesh_in_subtree(root: Node) -> Mesh:
if root == null:
return null
if root is MeshInstance3D:
return (root as MeshInstance3D).mesh
for child in root.get_children():
var mesh := find_first_mesh_in_subtree(child)
if mesh != null:
return mesh
return null
@@ -0,0 +1 @@
uid://dea4gekrxshjf
+182
View File
@@ -0,0 +1,182 @@
class_name M2MeshResourceFinalizer
extends RefCounted
## Polls static M2 ResourceLoader requests and publishes prepared Mesh outcomes.
## The caller owns permits, pipeline/cache lifetime and MultiMesh materialization.
var _mesh_resource_extractor: RefCounted
var _runtime_mesh_finalizer: RefCounted
var _raw_model_repository: RefCounted
var _resource_loader_adapter: Object
func _init(
mesh_resource_extractor: RefCounted,
runtime_mesh_finalizer: RefCounted,
raw_model_repository: RefCounted,
resource_loader_adapter: Object = null
) -> void:
_mesh_resource_extractor = mesh_resource_extractor
_runtime_mesh_finalizer = runtime_mesh_finalizer
_raw_model_repository = raw_model_repository
_resource_loader_adapter = resource_loader_adapter
## Moves terminal threaded requests into the pipeline finalize FIFO.
## Empty resource paths retain the historical immediate missing-model outcome.
func poll_terminal_requests(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> int:
if load_pipeline_state == null or prototype_cache_state == null:
return 0
var completed_request_count := 0
var request_records: Array = load_pipeline_state.call(
"request_records_snapshot"
)
for request_variant in request_records:
if not (request_variant is Dictionary):
continue
var request: Dictionary = request_variant
var normalized_relative_path := String(request.get("normalized", ""))
var resource_path := String(request.get("path", ""))
if resource_path.is_empty():
load_pipeline_state.call(
"discard_request",
normalized_relative_path
)
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
completed_request_count += 1
continue
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
load_pipeline_state.call(
"complete_request",
normalized_relative_path,
load_status
)
completed_request_count += 1
return completed_request_count
## Pops and finalizes one terminal record. A true result means a record was
## consumed, including cached, failed and invalid outcomes.
func finalize_next_resource(
load_pipeline_state: RefCounted,
mesh_resource_cache_state: RefCounted,
prototype_cache_state: RefCounted,
extracted_directory: String
) -> bool:
if (
load_pipeline_state == null
or mesh_resource_cache_state == null
or prototype_cache_state == null
):
return false
var terminal_record: Dictionary = load_pipeline_state.call(
"pop_finalize_record"
)
if terminal_record.is_empty():
return false
var normalized_relative_path := String(
terminal_record.get("normalized", "")
)
if (
normalized_relative_path.is_empty()
or bool(mesh_resource_cache_state.call(
"has_mesh",
normalized_relative_path
))
):
return true
if int(terminal_record.get(
"status",
ResourceLoader.THREAD_LOAD_FAILED
)) != ResourceLoader.THREAD_LOAD_LOADED:
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
return true
var resource_path := String(terminal_record.get("path", ""))
var loaded_resource := load_threaded_get(resource_path)
var mesh := _mesh_resource_extractor.call(
"extract_first_mesh",
loaded_resource
) as Mesh
if mesh == null:
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
return true
mesh_resource_cache_state.call(
"store_mesh",
normalized_relative_path,
prepare_mesh_for_runtime(
normalized_relative_path,
mesh,
extracted_directory
)
)
return true
## Applies the existing refresh-version/raw-data/rebuild path to one Mesh.
func prepare_mesh_for_runtime(
normalized_relative_path: String,
mesh: Mesh,
extracted_directory: String
) -> Mesh:
if mesh == null:
return null
if (
_runtime_mesh_finalizer == null
or not bool(_runtime_mesh_finalizer.call(
"requires_raw_data_for_refresh",
mesh
))
):
return mesh
var raw_model_data: Dictionary = {}
if _raw_model_repository != null:
raw_model_data = _raw_model_repository.call(
"load_static_model_data",
extracted_directory,
normalized_relative_path
)
return _runtime_mesh_finalizer.call(
"finalize_mesh",
normalized_relative_path,
mesh,
raw_model_data,
extracted_directory
) as Mesh
## Production ResourceLoader status adapter; injectable in synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader terminal-result adapter; injectable in tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://de4xl1fywvqfw
@@ -0,0 +1,113 @@
class_name M2NativeAnimationResourceObserver
extends RefCounted
## Resolves the existing native GryphonRoost animated M2 prototype path.
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload(
"res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd"
)
var _native_animated_builder: Object
func _init(native_animated_builder: Object = M2_NATIVE_ANIMATED_BUILDER_SCRIPT) -> void:
_native_animated_builder = native_animated_builder
## Returns whether the normalized path uses the historical native animation path.
func is_native_animation_candidate(normalized_relative_path: String) -> bool:
return normalized_relative_path.to_lower().contains("gryphonroost")
## Returns the exact cached or newly adopted native animated prototype.
## Rejected native candidates are marked static-only for the renderer session.
func observe(
normalized_relative_path: String,
extracted_directory: String,
raw_model_repository: RefCounted,
prototype_cache_state: RefCounted,
debug_logging_enabled: bool = false
) -> Node3D:
if (
normalized_relative_path.is_empty()
or not is_native_animation_candidate(normalized_relative_path)
or raw_model_repository == null
or prototype_cache_state == null
):
return null
var cached_prototype := prototype_cache_state.call(
"find_animated_prototype",
normalized_relative_path
) as Node3D
if cached_prototype != null:
return cached_prototype
if bool(prototype_cache_state.call(
"is_animation_static",
normalized_relative_path
)):
return null
var animated_model_data: Dictionary = raw_model_repository.call(
"load_animated_model_data",
extracted_directory,
normalized_relative_path
)
var animated_surfaces: Array = animated_model_data.get(
"animated_surfaces",
[]
)
if animated_model_data.is_empty() or animated_surfaces.is_empty():
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
var prototype := build_animated_prototype(
animated_model_data,
extracted_directory
)
if prototype == null or prototype.get_child_count() <= 0:
prototype_cache_state.call(
"mark_animation_static",
normalized_relative_path
)
return null
prototype = prototype_cache_state.call(
"adopt_animated_prototype",
normalized_relative_path,
prototype
) as Node3D
if debug_logging_enabled:
print(
(
"M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d "
+ "anim_id=%d seq=%d score=%d length=%.2f"
)
% [
normalized_relative_path,
animated_surfaces.size(),
(animated_model_data.get("bones", []) as Array).size(),
int(animated_model_data.get("animation_id", -1)),
int(animated_model_data.get("animation_sequence_index", -1)),
int(animated_model_data.get("animation_activity_score", 0)),
float(animated_model_data.get("animation_length", 0.0)),
]
)
return prototype
## Builds one prototype through the exact native animated builder dependency.
func build_animated_prototype(
animated_model_data: Dictionary,
extracted_directory: String
) -> Node3D:
if _native_animated_builder == null:
return null
return _native_animated_builder.call(
"build",
animated_model_data,
extracted_directory
) as Node3D
@@ -0,0 +1 @@
uid://bc3kgi23usncx
+157
View File
@@ -0,0 +1,157 @@
class_name M2PrototypeCacheState
extends RefCounted
## Owns detached static/animated M2 prototypes and their negative lookup
## outcomes until final renderer shutdown.
var _static_prototypes_by_path: Dictionary = {}
var _animated_prototypes_by_path: Dictionary = {}
var _missing_model_paths: Dictionary = {}
var _static_animation_paths: Dictionary = {}
## Returns whether a valid static prototype is retained for the normalized path.
func has_static_prototype(normalized_relative_path: String) -> bool:
return find_static_prototype(normalized_relative_path) != null
## Returns the exact retained static prototype or null.
func find_static_prototype(normalized_relative_path: String) -> Node3D:
return _find_valid_prototype(_static_prototypes_by_path, normalized_relative_path)
## Adopts a static prototype and returns the canonical retained reference. The
## first valid prototype wins; a later detached candidate is released.
func adopt_static_prototype(
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
return _adopt_prototype(
_static_prototypes_by_path,
normalized_relative_path,
prototype
)
## Returns whether a valid animated prototype is retained for the normalized path.
func has_animated_prototype(normalized_relative_path: String) -> bool:
return find_animated_prototype(normalized_relative_path) != null
## Returns the exact retained animated prototype or null.
func find_animated_prototype(normalized_relative_path: String) -> Node3D:
return _find_valid_prototype(_animated_prototypes_by_path, normalized_relative_path)
## Adopts an animated prototype and returns the canonical retained reference.
func adopt_animated_prototype(
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
return _adopt_prototype(
_animated_prototypes_by_path,
normalized_relative_path,
prototype
)
## Records that no model prototype can currently be loaded for this path.
func mark_model_missing(normalized_relative_path: String) -> bool:
return _mark_path(_missing_model_paths, normalized_relative_path)
## Returns whether the model path has a retained missing outcome.
func is_model_missing(normalized_relative_path: String) -> bool:
return not normalized_relative_path.is_empty() and _missing_model_paths.has(
normalized_relative_path
)
## Records that the path must use static presentation instead of animation.
func mark_animation_static(normalized_relative_path: String) -> bool:
return _mark_path(_static_animation_paths, normalized_relative_path)
## Returns whether the path has a retained static-animation outcome.
func is_animation_static(normalized_relative_path: String) -> bool:
return not normalized_relative_path.is_empty() and _static_animation_paths.has(
normalized_relative_path
)
## Returns detached path-only cache state for diagnostics and verification.
func diagnostic_snapshot() -> Dictionary:
return {
"static_prototype_paths": _sorted_paths(_static_prototypes_by_path),
"animated_prototype_paths": _sorted_paths(_animated_prototypes_by_path),
"missing_model_paths": _sorted_paths(_missing_model_paths),
"static_animation_paths": _sorted_paths(_static_animation_paths),
}
## Releases every retained prototype and clears positive and negative state.
## Detached Nodes are freed synchronously; in-tree Nodes are queued for deletion.
func clear_and_release() -> void:
_release_prototypes(_static_prototypes_by_path)
_release_prototypes(_animated_prototypes_by_path)
_missing_model_paths.clear()
_static_animation_paths.clear()
func _find_valid_prototype(
prototypes_by_path: Dictionary,
normalized_relative_path: String) -> Node3D:
if normalized_relative_path.is_empty():
return null
var prototype_variant: Variant = prototypes_by_path.get(normalized_relative_path)
if not (prototype_variant is Node3D):
return null
var prototype := prototype_variant as Node3D
return prototype if is_instance_valid(prototype) else null
func _adopt_prototype(
prototypes_by_path: Dictionary,
normalized_relative_path: String,
prototype: Node3D) -> Node3D:
if normalized_relative_path.is_empty() or prototype == null or not is_instance_valid(prototype):
return null
var retained_prototype := _find_valid_prototype(
prototypes_by_path,
normalized_relative_path
)
if retained_prototype != null:
if not is_same(retained_prototype, prototype):
_release_prototype(prototype)
return retained_prototype
prototypes_by_path[normalized_relative_path] = prototype
return prototype
func _mark_path(paths: Dictionary, normalized_relative_path: String) -> bool:
if normalized_relative_path.is_empty():
return false
paths[normalized_relative_path] = true
return true
func _release_prototypes(prototypes_by_path: Dictionary) -> void:
for prototype_variant in prototypes_by_path.values():
if prototype_variant is Node3D:
_release_prototype(prototype_variant as Node3D)
prototypes_by_path.clear()
func _release_prototype(prototype: Node3D) -> void:
if prototype == null or not is_instance_valid(prototype):
return
if prototype.is_inside_tree():
prototype.queue_free()
else:
prototype.free()
func _sorted_paths(paths: Dictionary) -> Array[String]:
var result: Array[String] = []
for path_variant in paths.keys():
result.append(String(path_variant))
result.sort()
return result
@@ -0,0 +1 @@
uid://srnqsus8pddp
+51
View File
@@ -0,0 +1,51 @@
class_name M2RawModelRepository
extends RefCounted
## Reads raw static and animated M2 Dictionaries through the optional native
## M2Loader boundary. The repository is stateless and does not cache failures.
const NATIVE_LOADER_CLASS_NAME := "M2Loader"
## Loads one normalized static M2 path with the native `load_m2` method.
## Invalid input, unavailable native support and parse failures return `{}`.
func load_static_model_data(
extracted_directory: String,
normalized_relative_path: String) -> Dictionary:
return _load_model_data(
extracted_directory,
normalized_relative_path,
"load_m2"
)
## Loads one normalized animated M2 path with the native `load_m2_animated`
## method. Invalid input, unavailable support and parse failures return `{}`.
func load_animated_model_data(
extracted_directory: String,
normalized_relative_path: String) -> Dictionary:
return _load_model_data(
extracted_directory,
normalized_relative_path,
"load_m2_animated"
)
func _load_model_data(
extracted_directory: String,
normalized_relative_path: String,
native_method_name: StringName) -> Dictionary:
if normalized_relative_path.is_empty():
return {}
if not ClassDB.class_exists(NATIVE_LOADER_CLASS_NAME):
return {}
var absolute_path := ProjectSettings.globalize_path(
extracted_directory.path_join(normalized_relative_path)
)
if not FileAccess.file_exists(absolute_path):
return {}
var native_loader: Object = ClassDB.instantiate(NATIVE_LOADER_CLASS_NAME)
if native_loader == null or not native_loader.has_method(native_method_name):
return {}
var raw_model_data: Variant = native_loader.call(native_method_name, absolute_path)
return raw_model_data if raw_model_data is Dictionary else {}
@@ -0,0 +1 @@
uid://bwqtuln7vpcj4
@@ -0,0 +1,78 @@
class_name M2RuntimeMeshFinalizer
extends RefCounted
## Finalizes stale cached M2 Meshes using caller-supplied raw data. The caller
## owns raw-file I/O, cache adoption, missing state and materialization.
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
const M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
)
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
"res://src/render/m2/m2_mesh_resource_extractor.gd"
)
const MATERIAL_REFRESH_VERSION := 2
const MATERIAL_REFRESH_VERSION_META := "wow_m2_material_refresh_version"
var _runtime_mesh_rebuild_classifier := M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
var _mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
## Returns whether one Mesh is stale and the caller should load raw M2 data.
func requires_raw_data_for_refresh(mesh: Mesh) -> bool:
return (
mesh != null
and int(mesh.get_meta(MATERIAL_REFRESH_VERSION_META, 0)) < MATERIAL_REFRESH_VERSION
)
## Marks, rebuilds or falls back one stale Mesh using already-loaded raw M2
## data. Current Meshes return unchanged and never inspect raw data.
func finalize_mesh(
normalized_relative_path: String,
mesh: Mesh,
raw_m2_data: Dictionary,
extracted_directory: String) -> Mesh:
if mesh == null:
return null
if not requires_raw_data_for_refresh(mesh):
return mesh
if raw_m2_data.is_empty() or not _runtime_mesh_rebuild_classifier.needs_runtime_mesh_rebuild(
normalized_relative_path,
raw_m2_data):
_mark_mesh_current(mesh)
return mesh
var rebuilt_mesh := _rebuild_mesh(raw_m2_data, extracted_directory)
if rebuilt_mesh != null:
return rebuilt_mesh
_mark_mesh_current(mesh)
return mesh
## Clears memoized per-path rebuild decisions at the existing map/shutdown sites.
func clear() -> void:
_runtime_mesh_rebuild_classifier.clear()
## Returns the memoized rebuild-decision count for diagnostics and verification.
func cached_rebuild_decision_count() -> int:
return _runtime_mesh_rebuild_classifier.cached_path_count()
func _mark_mesh_current(mesh: Mesh) -> void:
mesh.set_meta(MATERIAL_REFRESH_VERSION_META, MATERIAL_REFRESH_VERSION)
func _rebuild_mesh(raw_m2_data: Dictionary, extracted_directory: String) -> Mesh:
var temporary_prototype: Node3D = M2_BUILDER_SCRIPT.build(
raw_m2_data,
extracted_directory
)
if temporary_prototype == null:
return null
var rebuilt_mesh := _mesh_resource_extractor.find_first_mesh_in_subtree(
temporary_prototype
)
temporary_prototype.free()
return rebuilt_mesh
@@ -0,0 +1 @@
uid://bn6vvtmqo0don
@@ -0,0 +1,93 @@
class_name M2RuntimeMeshRebuildClassifier
extends RefCounted
## Memoizes whether raw M2 billboard or UV-rotation metadata requires replacing
## a stale cached runtime mesh. Owns no parser, Mesh, material, Node or RID.
const UV_ROTATION_SPEED_EPSILON := 0.000001
const MAXIMUM_TEXTURE_STAGES := 4
var _rebuild_required_by_normalized_path: Dictionary = {}
## Returns the first memoized rebuild decision for [param normalized_relative_path].
## [param raw_m2_data] is borrowed and not mutated. The decision remains cached
## until [method clear], matching the loader map/shutdown reset lifetime.
func needs_runtime_mesh_rebuild(
normalized_relative_path: String,
raw_m2_data: Dictionary) -> bool:
if _rebuild_required_by_normalized_path.has(normalized_relative_path):
return bool(_rebuild_required_by_normalized_path[normalized_relative_path])
var rebuild_required := (
_raw_data_has_billboards(raw_m2_data)
or _raw_data_has_uv_rotation(raw_m2_data)
)
_rebuild_required_by_normalized_path[normalized_relative_path] = rebuild_required
return rebuild_required
## Clears every memoized path decision. No Mesh or resource is retained or freed.
func clear() -> void:
_rebuild_required_by_normalized_path.clear()
## Returns the number of memoized normalized paths.
func cached_path_count() -> int:
return _rebuild_required_by_normalized_path.size()
## Returns a detached normalized-path to boolean decision snapshot.
func diagnostic_snapshot() -> Dictionary:
return _rebuild_required_by_normalized_path.duplicate()
func _raw_data_has_billboards(raw_m2_data: Dictionary) -> bool:
for batch_variant in raw_m2_data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
if (
bool(batch.get("has_billboard", false))
or int(batch.get("billboard_vertex_count", 0)) > 0
):
return true
return false
func _raw_data_has_uv_rotation(raw_m2_data: Dictionary) -> bool:
var texture_transforms: Array = raw_m2_data.get("texture_transforms", [])
if texture_transforms.is_empty():
return false
var texture_transform_combos: PackedInt32Array = raw_m2_data.get(
"texture_transform_combos",
PackedInt32Array()
)
if texture_transform_combos.is_empty():
return false
for batch_variant in raw_m2_data.get("batches", []):
if not (batch_variant is Dictionary):
continue
var batch: Dictionary = batch_variant
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
var base_combo_index := int(batch.get("texture_transform_combo_index", -1))
for texture_stage in mini(texture_count, MAXIMUM_TEXTURE_STAGES):
var combo_index := base_combo_index + texture_stage
if combo_index < 0 or combo_index >= texture_transform_combos.size():
continue
var transform_index := int(texture_transform_combos[combo_index])
if transform_index < 0 or transform_index >= texture_transforms.size():
continue
var texture_transform: Dictionary = (
texture_transforms[transform_index]
if texture_transforms[transform_index] is Dictionary
else {}
)
var rotation: Vector4 = texture_transform.get(
"rotation",
Vector4(1.0, 0.0, 0.0, 1.0)
)
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
return true
if absf(float(texture_transform.get("rotation_speed", 0.0))) > UV_ROTATION_SPEED_EPSILON:
return true
return false
@@ -0,0 +1 @@
uid://breu8vpmm88en
@@ -0,0 +1,56 @@
class_name M2StaticBatchMaterializer
extends RefCounted
## Builds and attaches one static M2 MultiMesh batch from an ordered transform
## slice. All Resource and SceneTree mutation belongs on the main thread.
## Returns the attached MultiMeshInstance3D, or null for invalid/empty input.
## The supplied parent owns the returned node and its MultiMesh Resource.
## Transform bounds are a build-planner precondition and are not clamped.
func materialize_batch(
m2_parent_root: Node3D,
relative_path: String,
mesh: Mesh,
transforms: Array,
start_index: int,
instance_count: int,
batch_serial: int,
visibility_range_end: float,
visibility_range_margin: float,
cast_shadows: bool
) -> MultiMeshInstance3D:
if (
m2_parent_root == null
or mesh == null
or transforms.is_empty()
or instance_count <= 0
):
return null
var multimesh := MultiMesh.new()
multimesh.transform_format = MultiMesh.TRANSFORM_3D
multimesh.mesh = mesh
multimesh.instance_count = instance_count
for batch_offset in instance_count:
multimesh.set_instance_transform(
batch_offset,
transforms[start_index + batch_offset]
)
var multimesh_instance := MultiMeshInstance3D.new()
multimesh_instance.name = "%s_%d" % [
relative_path.get_file().get_basename(),
batch_serial,
]
multimesh_instance.multimesh = multimesh
multimesh_instance.cast_shadow = (
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
if cast_shadows
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
)
if visibility_range_end > 0.0:
multimesh_instance.visibility_range_end = visibility_range_end
multimesh_instance.visibility_range_end_margin = visibility_range_margin
m2_parent_root.add_child(multimesh_instance)
return multimesh_instance
@@ -0,0 +1 @@
uid://bgiw27rkh8fbn
@@ -0,0 +1,126 @@
class_name M2StaticBuildResourceObserver
extends RefCounted
## Resolves or requests one static M2 build Mesh and fills a resource snapshot.
const OUTCOME_REJECTED := &"rejected"
const OUTCOME_CACHED := &"cached"
const OUTCOME_PENDING := &"pending"
const OUTCOME_REQUESTED := &"requested"
const OUTCOME_MISSING := &"missing"
## Observes cache/request state and adopts the exact static result into snapshot.
func observe(
resource_snapshot: RefCounted,
normalized_relative_path: String,
cache_directory: String,
mesh_cache_state: RefCounted,
prototype_cache_state: RefCounted,
load_pipeline_state: RefCounted
) -> StringName:
if (
resource_snapshot == null
or normalized_relative_path.is_empty()
or mesh_cache_state == null
or prototype_cache_state == null
or load_pipeline_state == null
):
return OUTCOME_REJECTED
if bool(mesh_cache_state.call("has_mesh", normalized_relative_path)):
resource_snapshot.call(
"adopt_static_observation",
mesh_cache_state.call("find_mesh", normalized_relative_path) as Mesh,
false
)
return OUTCOME_CACHED
if bool(prototype_cache_state.call("is_model_missing", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_PENDING
for cache_resource_path in cache_resource_paths(
cache_directory,
normalized_relative_path,
[".tscn", ".glb"]
):
if not ResourceLoader.exists(cache_resource_path):
continue
if (
cache_resource_path.get_extension().to_lower() == "glb"
and glb_animation_schema(cache_resource_path) == "pivot_prefix_v1"
):
continue
var request_error := ResourceLoader.load_threaded_request(
cache_resource_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE
)
if request_error == OK or request_error == ERR_BUSY:
load_pipeline_state.call(
"remember_request",
normalized_relative_path,
cache_resource_path
)
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_REQUESTED
break
prototype_cache_state.call("mark_model_missing", normalized_relative_path)
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
## Returns historical nested/lowercase/basename cache candidates without I/O.
func cache_resource_paths(
cache_directory: String,
relative_path: String,
extensions: Array[String]
) -> PackedStringArray:
var normalized := relative_path.replace("\\", "/")
var lower := normalized.to_lower()
var stems := [
normalized.get_basename(),
lower.get_basename(),
normalized.get_file().get_basename(),
lower.get_file().get_basename(),
]
var result := PackedStringArray()
for extension in extensions:
for stem in stems:
if stem.is_empty():
continue
var path := cache_directory.path_join(stem + extension)
if not result.has(path):
result.append(path)
return result
## Reads only the OpenWC animation schema marker from a GLB JSON chunk.
func glb_animation_schema(cache_resource_path: String) -> String:
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
if not FileAccess.file_exists(absolute_path):
return ""
var file := FileAccess.open(absolute_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_length := int(file.get_32())
var chunk_type := file.get_32()
if json_length <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed: Variant = JSON.parse_string(
file.get_buffer(json_length).get_string_from_utf8()
)
if not (parsed is Dictionary):
return ""
var asset: Dictionary = (parsed as Dictionary).get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
@@ -0,0 +1 @@
uid://c0e3p80ld1dfb
@@ -0,0 +1,90 @@
class_name WmoRenderResourceFinalizer
extends RefCounted
## Polls lightweight WMO render-cache requests and publishes terminal outcomes.
## Request admission, cache lifetime and WMO materialization remain caller-owned.
var _expected_resource_script: Script
var _minimum_format_version: int
var _resource_loader_adapter: Object
func _init(
expected_resource_script: Script,
minimum_format_version: int,
resource_loader_adapter: Object = null
) -> void:
_expected_resource_script = expected_resource_script
_minimum_format_version = minimum_format_version
_resource_loader_adapter = resource_loader_adapter
## Polls a detached pending snapshot in insertion order. Only terminal requests
## are removed and published as an exact Resource reference or missing outcome.
func poll_terminal_requests(render_resource_cache_state: RefCounted) -> int:
if render_resource_cache_state == null:
return 0
var completed_request_count := 0
var request_paths: Dictionary = render_resource_cache_state.call(
"request_paths_snapshot"
)
for normalized_relative_path_variant in request_paths.keys():
var normalized_relative_path := String(normalized_relative_path_variant)
var resource_path := String(request_paths[normalized_relative_path_variant])
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
if load_status != ResourceLoader.THREAD_LOAD_LOADED:
render_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
continue
var loaded_resource := load_threaded_get(resource_path)
if is_current_render_resource(loaded_resource):
render_resource_cache_state.call(
"complete_request_with_resource",
normalized_relative_path,
loaded_resource
)
else:
render_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
return completed_request_count
## Accepts only the exact configured script and its current-or-newer format.
func is_current_render_resource(resource: Resource) -> bool:
return (
resource != null
and _expected_resource_script != null
and resource.get_script() == _expected_resource_script
and int(resource.get("format_version")) >= _minimum_format_version
)
## Production ResourceLoader status boundary; injectable for synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader result boundary; injectable for synthetic tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://dtvba6g1grttk
@@ -0,0 +1,105 @@
class_name WmoRuntimeMeshFinalizer
extends RefCounted
## Refreshes cached WMO runtime Mesh materials through the configured builder.
## Scene traversal, placement and Node lifetime remain caller-owned.
const MATERIAL_REFRESH_VERSION := 10
const MATERIAL_REFRESH_VERSION_METADATA := "wow_wmo_material_refresh_version"
var _wmo_material_builder: Object
func _init(wmo_material_builder: Object) -> void:
_wmo_material_builder = wmo_material_builder
## Finalizes a stale Mesh in place and returns the exact input Resource identity.
## Null remains null; already-current meshes do not cross the builder boundary.
func finalize_mesh(mesh: Mesh, extracted_directory: String) -> Mesh:
if mesh == null:
return null
if int(mesh.get_meta(MATERIAL_REFRESH_VERSION_METADATA, 0)) >= MATERIAL_REFRESH_VERSION:
return mesh
mesh.set_meta(MATERIAL_REFRESH_VERSION_METADATA, MATERIAL_REFRESH_VERSION)
if not (mesh is ArrayMesh):
return mesh
var array_mesh := mesh as ArrayMesh
for surface_index in range(array_mesh.get_surface_count()):
var rebuilt_material := rebuild_cached_material(
array_mesh.surface_get_material(surface_index),
extracted_directory
)
if rebuilt_material != null:
array_mesh.surface_set_material(surface_index, rebuilt_material)
return mesh
## Rebuilds a material carrying the WMO cache metadata contract. Materials
## without texture0 metadata and failed builder results remain unchanged by the caller.
func rebuild_cached_material(
material: Material,
extracted_directory: String
) -> Material:
if (
material == null
or not material.has_meta("texture0_path")
or _wmo_material_builder == null
):
return null
var texture_paths := PackedStringArray()
var texture0_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture0_path", ""))
)
var texture1_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture1_path", ""))
)
var texture2_index := _append_texture_path(
texture_paths,
String(material.get_meta("texture2_path", ""))
)
var diffuse_color := Color.WHITE
var emissive_color := Color.BLACK
var secondary_color := Color.WHITE
if material is ShaderMaterial:
var shader_material := material as ShaderMaterial
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
if diffuse_value is Color:
diffuse_color = diffuse_value
if emissive_value is Color:
emissive_color = emissive_value
if secondary_value is Color:
secondary_color = secondary_value
var material_definition := {
"texture0": texture0_index,
"texture1": texture1_index,
"texture2": texture2_index,
"flags": int(material.get_meta("wow_flags", 0)),
"shader": int(material.get_meta("wow_shader", 0)),
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
"diffuse_color": diffuse_color,
"emissive_color": emissive_color,
"color2": secondary_color,
}
return _wmo_material_builder.call(
"_build_material",
material_definition,
texture_paths,
extracted_directory
) as Material
func _append_texture_path(texture_paths: PackedStringArray, texture_path: String) -> int:
if texture_path.is_empty():
return -1
var texture_index := texture_paths.size()
texture_paths.append(texture_path)
return texture_index
@@ -0,0 +1 @@
uid://bsxqkjuk77cgf
@@ -0,0 +1,97 @@
class_name WmoSceneResourceFinalizer
extends RefCounted
## Polls cached WMO PackedScene requests and publishes validated outcomes.
## File-size admission, live fallback and attached Node lifetime remain caller-owned.
var _scene_cache_validator: Object
var _resource_loader_adapter: Object
func _init(
scene_cache_validator: Object,
resource_loader_adapter: Object = null
) -> void:
_scene_cache_validator = scene_cache_validator
_resource_loader_adapter = resource_loader_adapter
## Polls a detached pending snapshot in insertion order. Terminal requests are
## published as the exact validated PackedScene or the historical missing state.
func poll_terminal_requests(scene_resource_cache_state: RefCounted) -> int:
if scene_resource_cache_state == null:
return 0
var completed_request_count := 0
var request_paths: Dictionary = scene_resource_cache_state.call(
"request_paths_snapshot"
)
for normalized_relative_path_variant in request_paths.keys():
var normalized_relative_path := String(normalized_relative_path_variant)
var resource_path := String(request_paths[normalized_relative_path_variant])
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
if load_status != ResourceLoader.THREAD_LOAD_LOADED:
scene_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
continue
var loaded_resource := load_threaded_get(resource_path)
var loaded_scene := loaded_resource as PackedScene
if is_scene_cache_current(loaded_scene):
scene_resource_cache_state.call(
"complete_request_with_scene",
normalized_relative_path,
loaded_scene
)
else:
scene_resource_cache_state.call(
"complete_request_as_missing",
normalized_relative_path
)
completed_request_count += 1
return completed_request_count
## Instantiates one Node3D probe, delegates the existing WMOBuilder metadata
## check and synchronously releases the probe before returning.
func is_scene_cache_current(scene: PackedScene) -> bool:
if scene == null or _scene_cache_validator == null:
return false
var instantiated_node := scene.instantiate()
if not (instantiated_node is Node3D):
if instantiated_node != null:
instantiated_node.free()
return false
var instance := instantiated_node as Node3D
var is_current := bool(_scene_cache_validator.call(
"is_scene_cache_current",
instance
))
instance.free()
return is_current
## Production ResourceLoader status boundary; injectable for synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader result boundary; injectable for synthetic tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://cp2r3tadn8l6q

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