From ddeb708c08ef5476b21abe156caf5dfbf99342a7 Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 18 Jul 2026 13:51:08 +0400 Subject: [PATCH 1/2] render: extract M2 Mesh resource finalizer --- RENDER.md | 29 +- docs/modules/README.md | 1 + docs/modules/m2-mesh-load-pipeline-state.md | 38 +- docs/modules/m2-mesh-resource-cache-state.md | 16 +- docs/modules/m2-mesh-resource-extractor.md | 19 +- docs/modules/m2-mesh-resource-finalizer.md | 251 +++++++++ docs/modules/m2-prototype-cache-state.md | 4 +- docs/modules/m2-raw-model-repository.md | 12 +- docs/modules/m2-runtime-mesh-finalizer.md | 35 +- .../m2-static-build-resource-observer.md | 8 +- docs/modules/world-renderer.md | 19 +- src/render/m2/m2_mesh_resource_finalizer.gd | 182 +++++++ .../m2/m2_mesh_resource_finalizer.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 79 +-- .../verify_m2_mesh_load_pipeline_state.gd | 20 +- .../verify_m2_mesh_resource_cache_state.gd | 23 +- .../verify_m2_mesh_resource_extractor.gd | 22 +- .../verify_m2_mesh_resource_finalizer.gd | 499 ++++++++++++++++++ .../verify_m2_mesh_resource_finalizer.gd.uid | 1 + src/tools/verify_m2_raw_model_repository.gd | 5 +- targets/03-renderer-facade.md | 33 +- 21 files changed, 1147 insertions(+), 150 deletions(-) create mode 100644 docs/modules/m2-mesh-resource-finalizer.md create mode 100644 src/render/m2/m2_mesh_resource_finalizer.gd create mode 100644 src/render/m2/m2_mesh_resource_finalizer.gd.uid create mode 100644 src/tools/verify_m2_mesh_resource_finalizer.gd create mode 100644 src/tools/verify_m2_mesh_resource_finalizer.gd.uid diff --git a/RENDER.md b/RENDER.md index 061214e..ec58666 100644 --- a/RENDER.md +++ b/RENDER.md @@ -47,6 +47,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g - `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. @@ -1169,6 +1170,22 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - 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 @@ -1244,12 +1261,14 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe' - `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` now owns - FileAccess/ClassDB M2Loader I/O and supplies raw data through the loader only - when the finalizer reports a stale Mesh; both historical clear sites persist. +- 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. Cache adoption decisions, permits and MultiMesh - materialization remain loader-owned; negative outcomes belong to prototype state. + 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. diff --git a/docs/modules/README.md b/docs/modules/README.md index a0ef71d..c77f1a5 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -37,6 +37,7 @@ | 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) | diff --git a/docs/modules/m2-mesh-load-pipeline-state.md b/docs/modules/m2-mesh-load-pipeline-state.md index acd6ea7..1bf0f48 100644 --- a/docs/modules/m2-mesh-load-pipeline-state.md +++ b/docs/modules/m2-mesh-load-pipeline-state.md @@ -78,7 +78,7 @@ Side effects are limited to collection mutation and retaining String/integer val flowchart TD Start[Successful ResourceLoader request] --> Remember[remember request] Remember --> Snapshot[request records snapshot] - Snapshot --> Poll[Loader polls status] + Snapshot --> Poll[Mesh resource finalizer polls status] Poll --> Active{In progress?} Active -->|yes| Snapshot Active -->|no| Complete[complete request with terminal status] @@ -86,7 +86,7 @@ flowchart TD FIFO --> Permit{Loader permit available?} Permit -->|no| FIFO Permit -->|yes| Pop[Pop oldest terminal record] - Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction] + 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] ``` @@ -113,6 +113,7 @@ again only if loader cache/missing rules permit it. ```mermaid sequenceDiagram participant Loader as StreamingWorldLoader + participant Finalizer as M2MeshResourceFinalizer participant Resource as ResourceLoader participant State as M2MeshLoadPipelineState participant Budget as RenderBudgetScheduler @@ -120,14 +121,16 @@ sequenceDiagram Resource-->>Loader: OK or ERR_BUSY Loader->>State: remember_request(normalized, cache path) loop frames - Loader->>State: request_records_snapshot() - Loader->>Resource: load_threaded_get_status(path) + Loader->>Finalizer: poll_terminal_requests(state, prototype cache) + Finalizer->>State: request_records_snapshot() + Finalizer->>Resource: load_threaded_get_status(path) end - Loader->>State: complete_request(normalized, terminal status) + Finalizer->>State: complete_request(normalized, terminal status) Loader->>Budget: try_consume_permit(M2_MESH_FINALIZE) - Loader->>State: pop_finalize_record() - Loader->>Resource: load_threaded_get(path) - Loader->>Loader: extract/refresh/adopt Mesh or mark missing + 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 @@ -135,7 +138,9 @@ sequenceDiagram ```mermaid flowchart TB Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState] - Loader --> Resource[ResourceLoader] + Loader --> MeshFinalizer[M2MeshResourceFinalizer] + MeshFinalizer --> State + MeshFinalizer --> Resource[ResourceLoader] Loader --> Budget[RenderBudgetScheduler] Loader --> MeshCache[M2 Mesh resource and prototype outcome cache states] Loader --> Finalizer[M2RuntimeMeshFinalizer] @@ -148,7 +153,9 @@ flowchart TB - Main thread serializes all state mutation and snapshots. - State owns only request/finalize Dictionaries containing Strings and status integers. -- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear. +- `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 @@ -161,9 +168,9 @@ flowchart TB |---|---|---|---|---| | 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 | Loader poll | Keep pending | Existing queue metric | Poll next frame | -| Terminal load failure | Status in popped record | Loader marks missing | Existing missing behavior | World/cache reload | -| Empty defensive path | Loader before poll | Discard and mark missing | Source regression | Correct request producer | +| 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 @@ -207,7 +214,7 @@ rebuild policy are unchanged; no migration or rebake is required. |---|---|---|---| | 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 | Existing loader-owned | Shutdown/material regressions | Finalize extraction optional | +| 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 | @@ -225,11 +232,12 @@ rebuild policy are unchanged; no migration or rebake is required. | 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, I/O polling, permits and adoption decisions | +| `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 | diff --git a/docs/modules/m2-mesh-resource-cache-state.md b/docs/modules/m2-mesh-resource-cache-state.md index 660163a..1c043ac 100644 --- a/docs/modules/m2-mesh-resource-cache-state.md +++ b/docs/modules/m2-mesh-resource-cache-state.md @@ -103,9 +103,9 @@ sequenceDiagram participant Pipeline as M2MeshLoadPipelineState Loader->>Cache: find/has normalized path alt cache miss - Loader->>Pipeline: request/poll/finalize record - Loader->>Loader: ResourceLoader get; delegate extract + prepare - Loader->>Cache: store_mesh(path, prepared Mesh) + 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 @@ -133,8 +133,9 @@ flowchart TB - 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; the loader owns - resource adoption and build-job decisions. + 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 @@ -186,7 +187,7 @@ the historical Mesh cache had no queue contribution or log site. | 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 state verifier | Finalize polling remains loader-owned | +| 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 | @@ -203,8 +204,9 @@ the historical Mesh cache had no queue contribution or log site. |---|---| | `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, raw/resource I/O and materialization | +| `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 | diff --git a/docs/modules/m2-mesh-resource-extractor.md b/docs/modules/m2-mesh-resource-extractor.md index 84d5768..accf260 100644 --- a/docs/modules/m2-mesh-resource-extractor.md +++ b/docs/modules/m2-mesh-resource-extractor.md @@ -92,27 +92,27 @@ No state or Resource reference is retained between calls. ```mermaid sequenceDiagram - participant Loader as StreamingWorldLoader + participant Finalizer as M2MeshResourceFinalizer participant Extractor as M2MeshResourceExtractor participant Scene as PackedScene - Loader->>Extractor: extract_first_mesh(loaded Resource) + Finalizer->>Extractor: extract_first_mesh(loaded Resource) alt direct Mesh - Extractor-->>Loader: same Mesh reference + 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-->>Loader: Mesh or null + Extractor-->>Finalizer: Mesh or null end - Loader->>Loader: prepare and cache Mesh or mark missing + Finalizer->>Finalizer: prepare and cache Mesh or mark missing ``` ## Dependency diagram ```mermaid flowchart TB - Loader[StreamingWorldLoader] --> Extractor[M2MeshResourceExtractor] + Finalizer[M2MeshResourceFinalizer] --> Extractor[M2MeshResourceExtractor] Extractor --> Types[Resource / PackedScene / Node / Mesh] Loader --> Pipeline[M2MeshLoadPipelineState] Loader --> Cache[M2MeshResourceCacheState] @@ -168,8 +168,8 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun ## Extension points -- `M2RuntimeMeshFinalizer` consumes the Mesh returned here and the loader stores - its result in `M2MeshResourceCacheState`. +- `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. @@ -194,8 +194,9 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun | 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` | ResourceLoader/raw I/O, cache decisions and materialization | +| `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 | diff --git a/docs/modules/m2-mesh-resource-finalizer.md b/docs/modules/m2-mesh-resource-finalizer.md new file mode 100644 index 0000000..c04ff5a --- /dev/null +++ b/docs/modules/m2-mesh-resource-finalizer.md @@ -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) diff --git a/docs/modules/m2-prototype-cache-state.md b/docs/modules/m2-prototype-cache-state.md index 805fd71..5295b9c 100644 --- a/docs/modules/m2-prototype-cache-state.md +++ b/docs/modules/m2-prototype-cache-state.md @@ -193,7 +193,8 @@ and native M2 formats are unchanged; no migration or rebake is introduced. - `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. + 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. @@ -240,6 +241,7 @@ this state without moving ResourceLoader or builder ownership into cache state. | `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 | diff --git a/docs/modules/m2-raw-model-repository.md b/docs/modules/m2-raw-model-repository.md index fc6db00..6707132 100644 --- a/docs/modules/m2-raw-model-repository.md +++ b/docs/modules/m2-raw-model-repository.md @@ -27,11 +27,13 @@ static or animated Dictionary consumed by existing builders and classifiers. ```mermaid flowchart LR - Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository] + 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] @@ -55,7 +57,7 @@ renderer policy and other application layers are forbidden. | 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` | Loader/finalizer/M2Builder | Fresh native result | One caller operation | +| 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 | @@ -136,8 +138,9 @@ flowchart TB ## 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 native observer - owns the animated raw-read/build decision; +- 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 @@ -215,6 +218,7 @@ measured work packages rather than expansion of this repository. | 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 | diff --git a/docs/modules/m2-runtime-mesh-finalizer.md b/docs/modules/m2-runtime-mesh-finalizer.md index a7484c9..b871d63 100644 --- a/docs/modules/m2-runtime-mesh-finalizer.md +++ b/docs/modules/m2-runtime-mesh-finalizer.md @@ -27,9 +27,8 @@ UV-rotation cases and returning the historical original-Mesh fallback. ```mermaid flowchart LR - Extractor[M2MeshResourceExtractor] --> Loader[StreamingWorldLoader] - Loader -->|is raw data required?| Finalizer[M2RuntimeMeshFinalizer] - Loader --> Raw[M2RawModelRepository] + ResourceFinalizer[M2MeshResourceFinalizer] -->|is raw data required?| Finalizer[M2RuntimeMeshFinalizer] + ResourceFinalizer --> Raw[M2RawModelRepository] Raw --> Finalizer Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier] Finalizer --> Builder[M2Builder] @@ -54,8 +53,8 @@ Nodes outside temporary rebuild roots and other application layers are forbidden | Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime | |---|---|---|---|---|---| -| Input | Extracted Mesh and normalized M2 path | Loader/extractor adapter | Finalizer | Borrowed Mesh reference/String copy | One call | -| Input | Already-loaded raw M2 Dictionary | `M2RawModelRepository` through loader adapter | Classifier/M2Builder | Caller-owned value container | One call | +| 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 | @@ -101,24 +100,24 @@ map/reset clear site calls `clear()`. ```mermaid sequenceDiagram - participant Loader as StreamingWorldLoader + participant ResourceFinalizer as M2MeshResourceFinalizer participant Finalizer as M2RuntimeMeshFinalizer participant Raw as M2Loader boundary participant Classifier as RebuildClassifier participant Builder as M2Builder - Loader->>Finalizer: requires_raw_data_for_refresh(mesh) + ResourceFinalizer->>Finalizer: requires_raw_data_for_refresh(mesh) alt current Mesh - Finalizer-->>Loader: false; reuse Mesh + Finalizer-->>ResourceFinalizer: false; reuse Mesh else stale Mesh - Finalizer-->>Loader: true - Loader->>Raw: load static raw Dictionary - Loader->>Finalizer: finalize_mesh(path, mesh, raw, extracted dir) + 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-->>Loader: rebuilt or marked fallback Mesh + Finalizer-->>ResourceFinalizer: rebuilt or marked fallback Mesh end ``` @@ -126,12 +125,12 @@ sequenceDiagram ```mermaid flowchart TB - Loader[StreamingWorldLoader] --> Finalizer[M2RuntimeMeshFinalizer] + ResourceFinalizer[M2MeshResourceFinalizer] --> Finalizer[M2RuntimeMeshFinalizer] Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier] Finalizer --> Extractor[M2MeshResourceExtractor] Finalizer --> Builder[M2Builder] - Loader --> Raw[M2RawModelRepository] - Loader --> Cache[M2MeshResourceCacheState] + ResourceFinalizer --> Raw[M2RawModelRepository] + ResourceFinalizer --> Cache[M2MeshResourceCacheState] Finalizer -. no dependency .-> Raw Finalizer -. no dependency .-> Cache ``` @@ -139,7 +138,8 @@ flowchart TB ## Ownership, threading and resources - Renderer main thread executes metadata changes and M2Builder work. -- `M2RawModelRepository` owns raw M2 file/native calls; loader supplies its value result. +- `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. @@ -208,7 +208,8 @@ logs/queue metrics are unchanged. Normalized M2 path remains the correlation key |---|---| | `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/scenes/streaming/streaming_world_loader.gd` | Repository call and returned-Mesh adoption | +| `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 | diff --git a/docs/modules/m2-static-build-resource-observer.md b/docs/modules/m2-static-build-resource-observer.md index 5df0d0d..6ace2da 100644 --- a/docs/modules/m2-static-build-resource-observer.md +++ b/docs/modules/m2-static-build-resource-observer.md @@ -134,7 +134,8 @@ flowchart TB - 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. -- Finalization and Mesh preparation remain in the loader drain path. +- `M2MeshResourceFinalizer` performs terminal polling, Resource extraction, + Mesh preparation and cache/missing adoption from the loader permit loop. ## Errors, cancellation and recovery @@ -181,7 +182,7 @@ a generic resource framework. | Capability | Status | Evidence | Gap | |---|---|---|---| | Static build lookup/request production | Implemented extraction | Lifecycle/path/source/timing verifier | Asset-backed traversal pending | -| Static finalize/preparation | Existing loader-owned | Pipeline/finalizer regressions | Later extraction | +| 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 @@ -197,7 +198,8 @@ a generic resource framework. |---|---| | `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/scenes/streaming/streaming_world_loader.gd` | Composition, finalize drain and action execution | +| `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 diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index bd63a7d..fc45b05 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -155,6 +155,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state | `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 | @@ -427,8 +428,9 @@ sequenceDiagram - `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 - loader retains ResourceLoader polling/finalize, permits and terminal adoption; - prototype cache state owns shared missing outcomes. + 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 @@ -440,11 +442,11 @@ sequenceDiagram 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 loader retains polling, - terminal adoption and materialization. + 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. The loader loads raw data only - after the finalizer reports that a cached Mesh is stale. + 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. @@ -596,10 +598,11 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | 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 and finalize extraction 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 | @@ -681,6 +684,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `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 | @@ -702,6 +706,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m | `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 | diff --git a/src/render/m2/m2_mesh_resource_finalizer.gd b/src/render/m2/m2_mesh_resource_finalizer.gd new file mode 100644 index 0000000..93eec24 --- /dev/null +++ b/src/render/m2/m2_mesh_resource_finalizer.gd @@ -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) diff --git a/src/render/m2/m2_mesh_resource_finalizer.gd.uid b/src/render/m2/m2_mesh_resource_finalizer.gd.uid new file mode 100644 index 0000000..bce7a8d --- /dev/null +++ b/src/render/m2/m2_mesh_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://de4xl1fywvqfw diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 49c920c..c273d0b 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -108,6 +108,9 @@ const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload( const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload( "res://src/render/m2/m2_mesh_resource_extractor.gd" ) +const M2_MESH_RESOURCE_FINALIZER_SCRIPT := preload( + "res://src/render/m2/m2_mesh_resource_finalizer.gd" +) const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd") const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd") const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd") @@ -309,6 +312,11 @@ var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new() var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_SCRIPT.new() var _m2_mesh_resource_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new() var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new() +var _m2_mesh_resource_finalizer := M2_MESH_RESOURCE_FINALIZER_SCRIPT.new( + _m2_mesh_resource_extractor, + _m2_runtime_mesh_finalizer, + _m2_raw_model_repository +) var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new() var _m2_animation_load_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new() var _m2_animated_scene_finalizer := M2_ANIMATED_SCENE_FINALIZER_SCRIPT.new() @@ -4268,41 +4276,19 @@ func _drain_m2_animation_loads() -> void: func _drain_m2_mesh_loads() -> void: - for pending in _m2_mesh_load_pipeline_state.request_records_snapshot(): - var normalized_rel := String(pending.get("normalized", "")) - var path: String = String(pending.get("path", "")) - if path.is_empty(): - _m2_mesh_load_pipeline_state.discard_request(normalized_rel) - _m2_prototype_cache_state.mark_model_missing(normalized_rel) - continue - var status := ResourceLoader.load_threaded_get_status(path) - if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED: - continue - _m2_mesh_load_pipeline_state.complete_request(normalized_rel, status) + _m2_mesh_resource_finalizer.poll_terminal_requests( + _m2_mesh_load_pipeline_state, + _m2_prototype_cache_state + ) while _m2_mesh_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit( RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE): - var pending: Dictionary = _m2_mesh_load_pipeline_state.pop_finalize_record() - var normalized_rel := String(pending.get("normalized", "")) - if ( - normalized_rel.is_empty() - or _m2_mesh_resource_cache_state.has_mesh(normalized_rel) - ): - continue - if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED: - _m2_prototype_cache_state.mark_model_missing(normalized_rel) - continue - - var path := String(pending.get("path", "")) - var resource: Resource = ResourceLoader.load_threaded_get(path) - var mesh := _m2_mesh_resource_extractor.extract_first_mesh(resource) - if mesh != null: - _m2_mesh_resource_cache_state.store_mesh( - normalized_rel, - _prepare_m2_mesh_for_runtime(normalized_rel, mesh) - ) - else: - _m2_prototype_cache_state.mark_model_missing(normalized_rel) + _m2_mesh_resource_finalizer.finalize_next_resource( + _m2_mesh_load_pipeline_state, + _m2_mesh_resource_cache_state, + _m2_prototype_cache_state, + extracted_dir + ) func _process_m2_build_jobs() -> void: @@ -4606,30 +4592,17 @@ func _normalize_m2_rel_path(rel_path: String) -> String: return normalized_rel -func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh: - if mesh == null: - return null - if not _m2_runtime_mesh_finalizer.requires_raw_data_for_refresh(mesh): - return mesh - var data := _m2_raw_model_repository.load_static_model_data( - extracted_dir, - normalized_rel - ) - return _m2_runtime_mesh_finalizer.finalize_mesh( - normalized_rel, - mesh, - data, - extracted_dir - ) - - func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void: if root == null: return for mesh_instance in _m2_animated_scene_finalizer.mesh_instances_in_subtree(root): if mesh_instance == null or mesh_instance.mesh == null: continue - var prepared := _prepare_m2_mesh_for_runtime(normalized_rel, mesh_instance.mesh) + var prepared := _m2_mesh_resource_finalizer.prepare_mesh_for_runtime( + normalized_rel, + mesh_instance.mesh, + extracted_dir + ) if prepared != null: mesh_instance.mesh = prepared @@ -4643,7 +4616,11 @@ func _get_or_load_m2_mesh(rel_path: String) -> Mesh: return null var mesh := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype) if mesh != null and not normalized_rel.is_empty(): - mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh) + mesh = _m2_mesh_resource_finalizer.prepare_mesh_for_runtime( + normalized_rel, + mesh, + extracted_dir + ) _m2_mesh_resource_cache_state.store_mesh(normalized_rel, mesh) return mesh diff --git a/src/tools/verify_m2_mesh_load_pipeline_state.gd b/src/tools/verify_m2_mesh_load_pipeline_state.gd index 82ba54c..38fdf32 100644 --- a/src/tools/verify_m2_mesh_load_pipeline_state.gd +++ b/src/tools/verify_m2_mesh_load_pipeline_state.gd @@ -6,6 +6,7 @@ extends SceneTree const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd") const PIPELINE_PATH := "res://src/render/m2/m2_mesh_load_pipeline_state.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" +const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd" func _initialize() -> void: @@ -86,6 +87,9 @@ func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> v func _verify_ownership_boundaries(failures: Array[String]) -> void: var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + var resource_finalizer_source := FileAccess.get_file_as_string( + RESOURCE_FINALIZER_PATH + ) _expect_true( loader_source.contains("M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()"), "loader composes pipeline state", @@ -105,15 +109,19 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: "two existing clear sites delegate", failures ) - for retained_loader_rule in [ + for retained_renderer_rule in [ "ResourceLoader.load_threaded_request(", - "ResourceLoader.load_threaded_get_status(path)", - "ResourceLoader.load_threaded_get(path)", + "ResourceLoader.load_threaded_get_status(resource_path)", + "ResourceLoader.load_threaded_get(resource_path)", "RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE", - "_m2_mesh_resource_cache_state.store_mesh(", - "_m2_prototype_cache_state.mark_model_missing(normalized_rel)", + "\"store_mesh\"", + "\"mark_model_missing\"", ]: - _expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) + _expect_true( + (loader_source + resource_finalizer_source).contains(retained_renderer_rule), + "renderer retains %s" % retained_renderer_rule, + failures + ) for forbidden_dependency in [ "ResourceLoader.", "WorkerThreadPool.", diff --git a/src/tools/verify_m2_mesh_resource_cache_state.gd b/src/tools/verify_m2_mesh_resource_cache_state.gd index 0887e2a..ffba1c0 100644 --- a/src/tools/verify_m2_mesh_resource_cache_state.gd +++ b/src/tools/verify_m2_mesh_resource_cache_state.gd @@ -7,6 +7,7 @@ const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state. const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd" +const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd" func _initialize() -> void: @@ -70,6 +71,9 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: var cache_source := FileAccess.get_file_as_string(CACHE_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH) + var resource_finalizer_source := FileAccess.get_file_as_string( + RESOURCE_FINALIZER_PATH + ) _expect_true( loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"), "loader composes Mesh cache state", @@ -77,7 +81,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: ) _expect_false(loader_source.contains("var _m2_mesh_cache:"), "legacy Mesh cache removed", failures) _expect_equal( - loader_source.count("_m2_mesh_resource_cache_state.store_mesh("), + loader_source.count("_m2_mesh_resource_cache_state.store_mesh(") + + resource_finalizer_source.count("\"store_mesh\""), 2, "two existing stores delegate", failures @@ -95,14 +100,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: "final shutdown clear delegates", failures ) - for retained_loader_rule in [ - "ResourceLoader.load_threaded_get(path)", - "_m2_mesh_resource_extractor.extract_first_mesh(resource)", - "_prepare_m2_mesh_for_runtime(normalized_rel, mesh)", - "_m2_prototype_cache_state.mark_model_missing(normalized_rel)", + for retained_renderer_rule in [ + "ResourceLoader.load_threaded_get(resource_path)", + "\"extract_first_mesh\"", + "prepare_mesh_for_runtime(", + "\"mark_model_missing\"", "_m2_prototype_cache_state.adopt_static_prototype(", ]: - _expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) + _expect_true( + (loader_source + resource_finalizer_source).contains(retained_renderer_rule), + "renderer retains %s" % retained_renderer_rule, + failures + ) for forbidden_dependency in [ "ResourceLoader.", "FileAccess.", diff --git a/src/tools/verify_m2_mesh_resource_extractor.gd b/src/tools/verify_m2_mesh_resource_extractor.gd index 2cf7863..d5c77e9 100644 --- a/src/tools/verify_m2_mesh_resource_extractor.gd +++ b/src/tools/verify_m2_mesh_resource_extractor.gd @@ -6,6 +6,7 @@ extends SceneTree const EXTRACTOR_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_extractor.gd") const EXTRACTOR_PATH := "res://src/render/m2/m2_mesh_resource_extractor.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" +const RESOURCE_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd" func _initialize() -> void: @@ -94,6 +95,9 @@ func _verify_packed_scene_mesh_and_temporary_lifetime(failures: Array[String]) - func _verify_ownership_boundaries(failures: Array[String]) -> void: var extractor_source := FileAccess.get_file_as_string(EXTRACTOR_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + var resource_finalizer_source := FileAccess.get_file_as_string( + RESOURCE_FINALIZER_PATH + ) _expect_true( loader_source.contains("M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()"), "loader composes Mesh resource extractor", @@ -110,7 +114,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: failures ) _expect_equal( - loader_source.count("_m2_mesh_resource_extractor.extract_first_mesh("), + resource_finalizer_source.count("\"extract_first_mesh\""), 1, "threaded Resource finalization delegates", failures @@ -121,14 +125,18 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: "synchronous fallback delegates", failures ) - for retained_loader_rule in [ - "ResourceLoader.load_threaded_get(path)", - "_prepare_m2_mesh_for_runtime(normalized_rel, mesh)", - "_m2_mesh_resource_cache_state.store_mesh(", + for retained_renderer_rule in [ + "ResourceLoader.load_threaded_get(resource_path)", + "prepare_mesh_for_runtime(", + "\"store_mesh\"", "M2_BUILDER_SCRIPT.build(data, extracted_dir)", - "_m2_prototype_cache_state.mark_model_missing(normalized_rel)", + "\"mark_model_missing\"", ]: - _expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures) + _expect_true( + (loader_source + resource_finalizer_source).contains(retained_renderer_rule), + "renderer retains %s" % retained_renderer_rule, + failures + ) for forbidden_dependency in [ "ResourceLoader.", "FileAccess.", diff --git a/src/tools/verify_m2_mesh_resource_finalizer.gd b/src/tools/verify_m2_mesh_resource_finalizer.gd new file mode 100644 index 0000000..16db37a --- /dev/null +++ b/src/tools/verify_m2_mesh_resource_finalizer.gd @@ -0,0 +1,499 @@ +extends SceneTree + +const FINALIZER_SCRIPT := preload( + "res://src/render/m2/m2_mesh_resource_finalizer.gd" +) +const PIPELINE_SCRIPT := preload( + "res://src/render/m2/m2_mesh_load_pipeline_state.gd" +) +const MESH_CACHE_SCRIPT := preload( + "res://src/render/m2/m2_mesh_resource_cache_state.gd" +) +const PROTOTYPE_CACHE_SCRIPT := preload( + "res://src/render/m2/m2_prototype_cache_state.gd" +) +const FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd" +const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" + + +class FakeResourceLoaderAdapter extends RefCounted: + var statuses: Dictionary = {} + var resources: Dictionary = {} + var status_paths: Array[String] = [] + var load_paths: Array[String] = [] + + func load_threaded_get_status(resource_path: String) -> int: + status_paths.append(resource_path) + return int(statuses.get( + resource_path, + ResourceLoader.THREAD_LOAD_IN_PROGRESS + )) + + func load_threaded_get(resource_path: String) -> Resource: + load_paths.append(resource_path) + return resources.get(resource_path, null) as Resource + + +class FakeMeshResourceExtractor extends RefCounted: + var mesh_by_resource: Dictionary = {} + var resources: Array[Resource] = [] + + func extract_first_mesh(resource: Resource) -> Mesh: + resources.append(resource) + return mesh_by_resource.get(resource, null) as Mesh + + +class FakeRuntimeMeshFinalizer extends RefCounted: + var raw_data_required: bool = false + var finalized_mesh: Mesh = null + var requirement_meshes: Array[Mesh] = [] + var finalize_calls: Array[Dictionary] = [] + + func requires_raw_data_for_refresh(mesh: Mesh) -> bool: + requirement_meshes.append(mesh) + return raw_data_required + + func finalize_mesh( + normalized_relative_path: String, + mesh: Mesh, + raw_model_data: Dictionary, + extracted_directory: String + ) -> Mesh: + finalize_calls.append({ + "normalized": normalized_relative_path, + "mesh": mesh, + "raw_model_data": raw_model_data, + "extracted_directory": extracted_directory, + }) + return finalized_mesh + + +class FakeRawModelRepository extends RefCounted: + var result: Dictionary = {} + var calls: Array[Dictionary] = [] + + func load_static_model_data( + extracted_directory: String, + normalized_relative_path: String + ) -> Dictionary: + calls.append({ + "extracted_directory": extracted_directory, + "normalized": normalized_relative_path, + }) + return result + + +class EmptyPathPipeline extends RefCounted: + var discarded_path: String = "" + + func request_records_snapshot() -> Array[Dictionary]: + return [{"normalized": "world/empty.m2", "path": ""}] + + func discard_request(normalized_relative_path: String) -> bool: + discarded_path = normalized_relative_path + return true + + +func _initialize() -> void: + var failures: Array[String] = [] + _verify_polling(failures) + _verify_empty_path_polling(failures) + _verify_terminal_rejections(failures) + _verify_current_mesh_adoption(failures) + _verify_stale_mesh_preparation(failures) + _verify_source_boundaries(failures) + var elapsed_milliseconds := _verify_bounded_timing(failures) + if not failures.is_empty(): + for failure in failures: + push_error("M2_MESH_RESOURCE_FINALIZER: %s" % failure) + quit(1) + return + print( + "M2_MESH_RESOURCE_FINALIZER PASS " + + "cases=31 iterations=1000 elapsed_ms=%.3f" % elapsed_milliseconds + ) + quit(0) + + +func _verify_polling(failures: Array[String]) -> void: + var dependencies := _dependencies() + var service: RefCounted = dependencies.service + var resource_loader: FakeResourceLoaderAdapter = dependencies.resource_loader + var pipeline: RefCounted = PIPELINE_SCRIPT.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + for request in [ + ["world/pending.m2", "res://pending.tres"], + ["world/loaded.m2", "res://loaded.tres"], + ["world/failed.m2", "res://failed.tres"], + ]: + pipeline.call("remember_request", request[0], request[1]) + resource_loader.statuses = { + "res://pending.tres": ResourceLoader.THREAD_LOAD_IN_PROGRESS, + "res://loaded.tres": ResourceLoader.THREAD_LOAD_LOADED, + "res://failed.tres": ResourceLoader.THREAD_LOAD_FAILED, + } + _expect_equal( + int(service.call("poll_terminal_requests", pipeline, prototype_cache)), + 2, + "two terminal requests completed", + failures + ) + _expect_equal(int(pipeline.call("pending_request_count")), 1, "pending retained", failures) + _expect_equal(int(pipeline.call("finalize_record_count")), 2, "two finalize records", failures) + _expect_string_array( + resource_loader.status_paths, + ["res://pending.tres", "res://loaded.tres", "res://failed.tres"], + "status insertion order", + failures + ) + prototype_cache.call("clear_and_release") + + +func _verify_empty_path_polling(failures: Array[String]) -> void: + var dependencies := _dependencies() + var pipeline := EmptyPathPipeline.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + _expect_equal( + int(dependencies.service.call( + "poll_terminal_requests", + pipeline, + prototype_cache + )), + 1, + "empty resource path completed", + failures + ) + _expect_string_equal( + pipeline.discarded_path, + "world/empty.m2", + "empty resource path discarded", + failures + ) + _expect_true( + bool(prototype_cache.call("is_model_missing", "world/empty.m2")), + "empty resource path marks missing", + failures + ) + _expect_equal( + dependencies.resource_loader.status_paths.size(), + 0, + "empty path skips I/O", + failures + ) + prototype_cache.call("clear_and_release") + + +func _verify_terminal_rejections(failures: Array[String]) -> void: + var dependencies := _dependencies() + var service: RefCounted = dependencies.service + var pipeline: RefCounted = PIPELINE_SCRIPT.new() + var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + _expect_false( + bool(service.call( + "finalize_next_resource", + pipeline, + mesh_cache, + prototype_cache, + "res://data/extracted" + )), + "empty finalize FIFO", + failures + ) + _enqueue_terminal( + pipeline, + "world/failed.m2", + "res://failed.tres", + ResourceLoader.THREAD_LOAD_FAILED + ) + _expect_true( + bool(service.call( + "finalize_next_resource", pipeline, mesh_cache, + prototype_cache, "res://data/extracted" + )), + "failed record consumed", + failures + ) + _expect_true( + bool(prototype_cache.call("is_model_missing", "world/failed.m2")), + "failed record marks missing", + failures + ) + var cached_mesh := ArrayMesh.new() + mesh_cache.call("store_mesh", "world/cached.m2", cached_mesh) + _enqueue_terminal( + pipeline, + "world/cached.m2", + "res://cached.tres", + ResourceLoader.THREAD_LOAD_LOADED + ) + service.call( + "finalize_next_resource", pipeline, mesh_cache, + prototype_cache, "res://data/extracted" + ) + _enqueue_terminal( + pipeline, + "world/null.m2", + "res://null.tres", + ResourceLoader.THREAD_LOAD_LOADED + ) + service.call( + "finalize_next_resource", pipeline, mesh_cache, + prototype_cache, "res://data/extracted" + ) + _expect_true( + bool(prototype_cache.call("is_model_missing", "world/null.m2")), + "null Resource marks missing", + failures + ) + _expect_string_array( + dependencies.resource_loader.load_paths, + ["res://null.tres"], + "cached record skips terminal get", + failures + ) + mesh_cache.call("clear") + prototype_cache.call("clear_and_release") + + +func _verify_current_mesh_adoption(failures: Array[String]) -> void: + var dependencies := _dependencies() + var resource := Resource.new() + var mesh := ArrayMesh.new() + dependencies.resource_loader.resources["res://current.tres"] = resource + dependencies.extractor.mesh_by_resource[resource] = mesh + var pipeline: RefCounted = PIPELINE_SCRIPT.new() + var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + _enqueue_terminal( + pipeline, + "world/current.m2", + "res://current.tres", + ResourceLoader.THREAD_LOAD_LOADED + ) + _expect_true( + bool(dependencies.service.call( + "finalize_next_resource", pipeline, mesh_cache, + prototype_cache, "res://custom/extracted" + )), + "current Mesh record consumed", + failures + ) + _expect_same( + mesh_cache.call("find_mesh", "world/current.m2"), + mesh, + "current Mesh exact adoption", + failures + ) + _expect_equal(dependencies.raw_repository.calls.size(), 0, "current Mesh skips raw", failures) + _expect_equal( + dependencies.runtime_finalizer.finalize_calls.size(), + 0, + "current Mesh skips finalize", + failures + ) + mesh_cache.call("clear") + prototype_cache.call("clear_and_release") + + +func _verify_stale_mesh_preparation(failures: Array[String]) -> void: + var dependencies := _dependencies() + dependencies.runtime_finalizer.raw_data_required = true + dependencies.raw_repository.result = {"vertices": [1]} + var resource := Resource.new() + var stale_mesh := ArrayMesh.new() + var prepared_mesh := ArrayMesh.new() + dependencies.runtime_finalizer.finalized_mesh = prepared_mesh + dependencies.resource_loader.resources["res://stale.tres"] = resource + dependencies.extractor.mesh_by_resource[resource] = stale_mesh + var pipeline: RefCounted = PIPELINE_SCRIPT.new() + var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + _enqueue_terminal( + pipeline, + "world/stale.m2", + "res://stale.tres", + ResourceLoader.THREAD_LOAD_LOADED + ) + dependencies.service.call( + "finalize_next_resource", pipeline, mesh_cache, + prototype_cache, "res://custom/extracted" + ) + _expect_same( + mesh_cache.call("find_mesh", "world/stale.m2"), + prepared_mesh, + "prepared Mesh adoption", + failures + ) + _expect_equal(dependencies.raw_repository.calls.size(), 1, "one raw read", failures) + _expect_string_equal( + String(dependencies.raw_repository.calls[0].normalized), + "world/stale.m2", + "raw normalized path", + failures + ) + _expect_string_equal( + String(dependencies.raw_repository.calls[0].extracted_directory), + "res://custom/extracted", + "raw extracted directory", + failures + ) + _expect_same( + dependencies.runtime_finalizer.finalize_calls[0].mesh, + stale_mesh, + "runtime finalizer source Mesh", + failures + ) + _expect_true( + dependencies.runtime_finalizer.finalize_calls[0].raw_model_data + == dependencies.raw_repository.result, + "runtime finalizer raw data", + failures + ) + mesh_cache.call("clear") + prototype_cache.call("clear_and_release") + + +func _verify_source_boundaries(failures: Array[String]) -> void: + var loader_source := FileAccess.get_file_as_string(LOADER_PATH) + var service_source := FileAccess.get_file_as_string(FINALIZER_PATH) + var drain_start := loader_source.find("func _drain_m2_mesh_loads()") + var drain_end := loader_source.find("func _process_m2_build_jobs()", drain_start) + var drain_source := loader_source.substr(drain_start, drain_end - drain_start) + _expect_true( + drain_source.contains("_m2_mesh_resource_finalizer.poll_terminal_requests("), + "loader delegates polling", + failures + ) + _expect_true( + drain_source.contains("_m2_mesh_resource_finalizer.finalize_next_resource("), + "loader delegates terminal finalization", + failures + ) + for removed_token in [ + "ResourceLoader.load_threaded_get_status", + "ResourceLoader.load_threaded_get(", + "_m2_mesh_resource_extractor.extract_first_mesh", + "_m2_mesh_resource_cache_state.store_mesh", + "_m2_prototype_cache_state.mark_model_missing", + ]: + _expect_false( + drain_source.contains(removed_token), + "loader drain omits %s" % removed_token, + failures + ) + _expect_false( + loader_source.contains("func _prepare_m2_mesh_for_runtime("), + "legacy loader preparation removed", + failures + ) + for required_token in [ + "ResourceLoader.load_threaded_get_status", + "ResourceLoader.load_threaded_get(", + "\"extract_first_mesh\"", + "\"store_mesh\"", + "\"mark_model_missing\"", + "\"load_static_model_data\"", + "\"finalize_mesh\"", + ]: + _expect_true( + service_source.contains(required_token), + "service owns %s" % required_token, + failures + ) + + +func _verify_bounded_timing(failures: Array[String]) -> float: + var dependencies := _dependencies() + var pipeline: RefCounted = PIPELINE_SCRIPT.new() + var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new() + var started_microseconds := Time.get_ticks_usec() + for iteration in range(1000): + dependencies.service.call( + "poll_terminal_requests", + pipeline, + prototype_cache + ) + var elapsed_milliseconds := float( + Time.get_ticks_usec() - started_microseconds + ) / 1000.0 + _expect_true(elapsed_milliseconds < 1000.0, "empty polling bounded", failures) + prototype_cache.call("clear_and_release") + return elapsed_milliseconds + + +func _dependencies() -> Dictionary: + var resource_loader := FakeResourceLoaderAdapter.new() + var extractor := FakeMeshResourceExtractor.new() + var runtime_finalizer := FakeRuntimeMeshFinalizer.new() + var raw_repository := FakeRawModelRepository.new() + return { + "resource_loader": resource_loader, + "extractor": extractor, + "runtime_finalizer": runtime_finalizer, + "raw_repository": raw_repository, + "service": FINALIZER_SCRIPT.new( + extractor, + runtime_finalizer, + raw_repository, + resource_loader + ), + } + + +func _enqueue_terminal( + pipeline: RefCounted, + normalized_relative_path: String, + resource_path: String, + status: int +) -> void: + pipeline.call("remember_request", normalized_relative_path, resource_path) + pipeline.call("complete_request", normalized_relative_path, status) + + +func _expect_true(condition: bool, label: String, failures: Array[String]) -> void: + if not condition: + failures.append(label) + + +func _expect_false(condition: bool, label: String, failures: Array[String]) -> void: + _expect_true(not condition, label, failures) + + +func _expect_same( + actual: Variant, + expected: Variant, + label: String, + failures: Array[String] +) -> void: + _expect_true(is_same(actual, expected), label, failures) + + +func _expect_equal( + actual: int, + expected: int, + label: String, + failures: Array[String] +) -> void: + if actual != expected: + failures.append("%s expected=%d actual=%d" % [label, expected, actual]) + + +func _expect_string_equal( + actual: String, + expected: String, + label: String, + failures: Array[String] +) -> void: + if actual != expected: + failures.append("%s expected=%s actual=%s" % [label, expected, actual]) + + +func _expect_string_array( + actual: Array[String], + expected: Array[String], + label: String, + failures: Array[String] +) -> void: + if actual != expected: + failures.append("%s expected=%s actual=%s" % [label, expected, actual]) diff --git a/src/tools/verify_m2_mesh_resource_finalizer.gd.uid b/src/tools/verify_m2_mesh_resource_finalizer.gd.uid new file mode 100644 index 0000000..ee3935c --- /dev/null +++ b/src/tools/verify_m2_mesh_resource_finalizer.gd.uid @@ -0,0 +1 @@ +uid://by0rlnw5g8hjr diff --git a/src/tools/verify_m2_raw_model_repository.gd b/src/tools/verify_m2_raw_model_repository.gd index 2010ba9..f918269 100644 --- a/src/tools/verify_m2_raw_model_repository.gd +++ b/src/tools/verify_m2_raw_model_repository.gd @@ -7,6 +7,7 @@ const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd" const NATIVE_OBSERVER_PATH := ( "res://src/render/m2/m2_native_animation_resource_observer.gd" ) +const MESH_FINALIZER_PATH := "res://src/render/m2/m2_mesh_resource_finalizer.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" @@ -60,6 +61,7 @@ func _verify_rejected_inputs(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void: var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH) var native_observer_source := FileAccess.get_file_as_string(NATIVE_OBSERVER_PATH) + var mesh_finalizer_source := FileAccess.get_file_as_string(MESH_FINALIZER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH) _expect_true( loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"), @@ -67,7 +69,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void: failures ) _expect_equal( - loader_source.count("_m2_raw_model_repository.load_static_model_data("), + loader_source.count("_m2_raw_model_repository.load_static_model_data(") + + mesh_finalizer_source.count("\"load_static_model_data\""), 2, "two static callers delegate", failures diff --git a/targets/03-renderer-facade.md b/targets/03-renderer-facade.md index ef9e230..4268700 100644 --- a/targets/03-renderer-facade.md +++ b/targets/03-renderer-facade.md @@ -62,7 +62,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру `M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001`, `M03-RND-M2-CACHED-ANIMATION-RESOURCE-OBSERVER-001`, `M03-RND-M2-NATIVE-ANIMATION-RESOURCE-OBSERVER-001`, - `M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001` + `M03-RND-M2-ANIMATION-RESOURCE-FINALIZER-001`, + `M03-RND-M2-MESH-RESOURCE-FINALIZER-001` - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. - Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner @@ -317,6 +318,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру All 58 autonomous headless regressions passed; internal-access remained `30`, documentation covered `45` module specifications, coordination retained `34` historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans. + M2 Mesh resource finalizer passed `cases=31 iterations=1000 + elapsed_ms=0.767` with insertion-order status polling, loaded/failed FIFO, + empty-path discard, cached/null/current/stale Mesh outcomes, exact dependency + arguments, raw-read suppression and loader source boundaries. All 59 autonomous + headless regressions passed; internal-access remained `30`, documentation + covered `46` module specifications, coordination retained `34` historical + expired-claim warnings and checkpoint dry-run kept all `7/7` plans. M02 terrain-query regression remained green (13 pre-existing expired M00 claim warnings). - Fidelity comparison: all 16 historical operation limits and drain sites were @@ -400,14 +408,16 @@ Runtime и Editor используют facade; planner/scheduler тестиру M2 stale-cache rebuild selection retains the same positive billboard flag/ vertex-count tests, referenced non-identity UV rotation, exclusive `0.000001` speed threshold, four-stage cap and first-decision-per-path cache - lifetime. Raw loading, material refresh version, M2Builder rebuild/fallback and - Mesh adoption remain loader-owned. + lifetime. Material refresh version and M2Builder rebuild/fallback remain in + the runtime finalizer; static raw loading and Mesh adoption now belong to the + Mesh resource finalizer. Static M2 mesh pending requests preserve normalized-path dedupe and Dictionary insertion polling order; loaded/failed transitions preserve completion-order finalization FIFO and opaque statuses. All three pending-plus-finalize metrics, - both clear sites and shutdown ResourceLoader drain order are unchanged; cache - path selection, I/O, permits and adoption decisions remain loader-owned; - missing outcomes now belong to the prototype cache state. + both clear sites and shutdown ResourceLoader drain order are unchanged. Cache + path selection/request admission and permits remain loader-owned; terminal + I/O and Mesh/missing adoption belong to the Mesh resource finalizer, with + missing outcomes retained by prototype cache state. Animated M2 pending requests preserve normalized-path dedupe and Dictionary insertion polling order; loaded/failed transitions preserve completion-order finalization FIFO and opaque statuses. All three pending-plus-finalize metrics, @@ -489,6 +499,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру adapters plus synthetic lifecycle and exact-dependency regression fixtures, cached M2 animation resource finalizer and loader/pipeline/scene-finalizer/ prototype-cache adapters plus synthetic terminal-I/O regression fixtures, + static M2 Mesh resource finalizer and loader/pipeline/extractor/runtime-finalizer/ + raw-repository/cache adapters plus synthetic terminal-I/O regression fixtures, expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence. - Remaining risks: worker concurrency and stale-result validation remain streamer-owned; cancellation stops new permits but does not interrupt @@ -505,8 +517,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру culling-driven spatial-cell grouping; M2 rebuild classification and static request/finalize bookkeeping, prepared Mesh cache ownership and first-Mesh traversal plus refresh/rebuild/fallback, raw native M2 I/O and prototype/ - negative cache state are separated, but synchronous native parsing/build and - Mesh adoption remain in the loader call path; cached animation eligibility/ + negative cache state are separated; static Mesh terminal polling/adoption is + also separated, but synchronous native parsing/build remains in its + main-thread service call path; cached animation eligibility/ request admission is extracted while terminal ResourceLoader polling remains; raw animated-instance materialization is now separated but remains synchronous @@ -518,8 +531,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру batching still needs culling/performance evidence; typed M2 pending state is separated and dispatch decisions are explicit; static and cached-animation observers produce typed per-operation state, including native observation; - animation terminal ResourceLoader drain is extracted, while action execution, - material-prototype lookup, root cleanup and static Mesh finalize drain remain loader-owned; + animation and static Mesh terminal ResourceLoader drains are extracted, while + action execution, material-prototype lookup and root cleanup remain loader-owned; WMO ResourceLoader/FileAccess I/O, live fallback and group materialization remain in the loader; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; From 71a10127799f49af08b822bbc7c56e8b72562ba9 Mon Sep 17 00:00:00 2001 From: sindoring Date: Sat, 18 Jul 2026 13:51:21 +0400 Subject: [PATCH 2/2] coordination: hand off M2 Mesh resource finalizer --- .../M03-RND-M2-MESH-RESOURCE-FINALIZER-001.md | 22 +++++++++++++------ 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/coordination/claims/M03-RND-M2-MESH-RESOURCE-FINALIZER-001.md b/coordination/claims/M03-RND-M2-MESH-RESOURCE-FINALIZER-001.md index 0e52bd8..443435a 100644 --- a/coordination/claims/M03-RND-M2-MESH-RESOURCE-FINALIZER-001.md +++ b/coordination/claims/M03-RND-M2-MESH-RESOURCE-FINALIZER-001.md @@ -58,14 +58,22 @@ loader into one dedicated finalization service. ## Status -- State: active -- Done: ownership/compatibility contract -- Next: implementation, verification and documentation +- State: ready +- Done: implementation, verification and documentation +- Next: merge and post-merge acceptance - Blocked by: ## Handoff -- Commit: -- Results: -- Remaining risks: -- Documentation updated: +- 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.