merge: M2 Mesh resource finalizer

This commit is contained in:
2026-07-18 13:51:25 +04:00
22 changed files with 1162 additions and 157 deletions
+24 -5
View File
@@ -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.
@@ -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.
+1
View File
@@ -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) |
+23 -15
View File
@@ -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 |
+9 -7
View File
@@ -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 |
+10 -9
View File
@@ -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 |
+251
View File
@@ -0,0 +1,251 @@
# M2 Mesh Resource Finalizer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-FINALIZER-001` |
| Owners | Static M2 terminal Resource I/O, Mesh preparation and cache outcome |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-finalizer`, 2026-07-18 |
| Profiles/capabilities | Existing cached/static MultiMesh M2 path |
## Purpose
Drain static M2 ResourceLoader work and publish a prepared Mesh or missing-model
outcome without keeping terminal I/O and refresh logic in `StreamingWorldLoader`.
The same preparation method serves threaded and synchronous Mesh consumers.
## Non-goals
- Select cache candidates or admit threaded ResourceLoader requests.
- Own scheduler permits, prototype Nodes, MultiMeshes or SceneTree roots.
- Change first-Mesh traversal, refresh version, rebuild rules or raw parser.
- Merge static and animated terminal services through callbacks/frameworks.
- Change cache formats, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Observer[M2StaticBuildResourceObserver] --> Pipeline[M2MeshLoadPipelineState]
Loader[StreamingWorldLoader] --> Finalizer[M2MeshResourceFinalizer]
Pipeline --> Finalizer
ResourceLoader --> Finalizer
Finalizer --> Extractor[M2MeshResourceExtractor]
Finalizer --> Runtime[M2RuntimeMeshFinalizer]
Finalizer --> Raw[M2RawModelRepository]
Finalizer --> MeshCache[M2MeshResourceCacheState]
Finalizer --> Prototype[M2PrototypeCacheState]
```
## Public API
| Symbol | Kind | Purpose | Failure behavior |
|---|---|---|---|
| `poll_terminal_requests(pipeline, prototype_cache)` | I/O command | Move LOADED/FAILED records into completion FIFO | Invalid composition returns zero |
| `finalize_next_resource(pipeline, mesh_cache, prototype_cache, extracted_dir)` | I/O command | Pop one record and publish Mesh/missing outcome | Empty/invalid composition returns false |
| `prepare_mesh_for_runtime(path, mesh, extracted_dir)` | I/O preparation | Preserve current Mesh or load raw data and refresh stale Mesh | Null returns null |
| `load_threaded_get_status(path)` | I/O adapter | Production status query with injectable test seam | Returns ResourceLoader status |
| `load_threaded_get(path)` | I/O adapter | Production terminal Resource retrieval with injectable test seam | May return null |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Pending/finalize records | Mesh pipeline | Finalizer | Pipeline-owned copied Dictionaries |
| Input | Terminal Resource | ResourceLoader | Mesh extractor | Borrowed one operation |
| Input | Extracted/cached Mesh | Mesh extractor/loader consumer | Runtime preparation | Borrowed Resource reference |
| Input | Raw M2 Dictionary | Raw repository | Runtime finalizer | Call-local value |
| Output | Prepared Mesh | Runtime finalizer | Mesh cache/loader consumer | Resource reference |
| Output | Missing-model outcome | Finalizer | Prototype cache | Copied path; loader session |
| Output | Processed-record flag | Finalizer | Loader permit loop/tests | Boolean value |
## Data flow
```mermaid
flowchart TD
Poll[Poll pending records in insertion order] --> Path{Resource path empty?}
Path -->|yes| Discard[Discard and mark missing]
Path -->|no| Status{LOADED or FAILED?}
Status -->|no| Pending[Keep pending]
Status -->|yes| Complete[Append completion FIFO]
Complete --> Permit[Loader consumes M2_MESH_FINALIZE permit]
Permit --> Pop[Pop oldest terminal record]
Pop --> Cached{Empty path or Mesh cached?}
Cached -->|yes| Skip[Finish one permit operation]
Cached -->|no| Loaded{Status LOADED?}
Loaded -->|no| Missing[Mark model missing]
Loaded -->|yes| Get[Get terminal Resource]
Get --> Extract[Extract first Mesh]
Extract --> Found{Mesh found?}
Found -->|no| Missing
Found -->|yes| Current{Refresh version current?}
Current -->|yes| Store[Store exact Mesh]
Current -->|no| Raw[Read raw M2 data]
Raw --> Refresh[Finalize/rebuild/fallback Mesh]
Refresh --> Store
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Polling
Polling --> Pending: nonterminal status
Polling --> TerminalQueued: LOADED or FAILED
Polling --> Missing: empty Resource path
TerminalQueued --> Skipped: empty/cached record
TerminalQueued --> Missing: failed load or no Mesh
TerminalQueued --> Extracted: Mesh found
Extracted --> Current: refresh version current
Extracted --> Refreshing: stale Mesh
Refreshing --> Prepared: rebuild or fallback
Current --> Cached
Prepared --> Cached
Pending --> [*]
Skipped --> [*]
Missing --> [*]
Cached --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant F as M2MeshResourceFinalizer
participant P as M2MeshLoadPipelineState
participant R as ResourceLoader
participant E as M2MeshResourceExtractor
participant N as M2RuntimeMeshFinalizer
participant C as M2MeshResourceCacheState
L->>F: poll_terminal_requests(P, prototype cache)
F->>R: load_threaded_get_status(path)
F->>P: complete terminal request
L->>L: consume M2_MESH_FINALIZE permit
L->>F: finalize_next_resource(...)
F->>P: pop oldest record
F->>R: load_threaded_get(path)
F->>E: extract_first_mesh(Resource)
F->>N: requires_raw_data_for_refresh(Mesh)
opt stale Mesh
F->>F: read raw M2 data
F->>N: finalize_mesh(...)
end
F->>C: store prepared Mesh
```
## Dependency diagram
```mermaid
flowchart TB
Finalizer[M2MeshResourceFinalizer] --> ResourceLoader
Finalizer --> Pipeline[M2MeshLoadPipelineState]
Finalizer --> Extractor[M2MeshResourceExtractor]
Finalizer --> Runtime[M2RuntimeMeshFinalizer]
Finalizer --> Raw[M2RawModelRepository]
Finalizer --> MeshCache[M2MeshResourceCacheState]
Finalizer --> Prototype[M2PrototypeCacheState]
Loader[StreamingWorldLoader] --> Finalizer
Loader --> Scheduler[RenderBudgetScheduler]
Finalizer -. no dependency .-> Scheduler
Finalizer -. no dependency .-> MultiMesh
Finalizer -. no dependency .-> SceneTree
```
## Ownership, threading and resources
- All operations run synchronously on the renderer main thread.
- Pipeline owns pending/completion records; one permit pops one terminal record.
- Extractor owns temporary PackedScene instance cleanup and returns a Mesh reference.
- Runtime finalizer owns stale/current/rebuild/fallback rules; raw repository owns
only call-local native I/O.
- Mesh cache retains prepared Mesh references until final shutdown; prototype
cache retains missing-model paths.
- Service retains dependency references but no pending record, Resource, Mesh or
raw Dictionary between calls. Loader retains permits and materialization.
## Errors, cancellation and recovery
| Failure/state | Behavior | Recovery |
|---|---|---|
| Missing composition dependency | Return zero/false; preparation keeps Mesh where possible | Correct composition |
| Empty Resource path | Discard request and mark missing | Repair cache and restart session |
| Nonterminal status | Keep pending | Poll next tick |
| FAILED status | Pop and mark missing | Repair cache and restart session |
| Existing cached Mesh | Pop and skip terminal Resource get | None |
| Null/unsupported/no-Mesh Resource | Mark missing | Repair imported cache |
| Raw read/rebuild unavailable | Runtime finalizer marks/reuses original Mesh | Existing fallback |
| Tile cancellation | Shared cache work continues | Loader queue remains authoritative |
| Shutdown | Loader drains ResourceLoader before pipeline/cache clear | New loader starts empty |
## Configuration and capabilities
The service consumes the existing extracted directory. The scheduler retains
`m2_mesh_finalize_ops_per_tick`; material refresh version remains `2`.
## Persistence, cache and migration
No cache path, schema or version changes are introduced. Existing `.tscn/.glb`
Resources and in-memory refresh metadata remain compatible; no rebake is required.
## Diagnostics and observability
The service adds no log. Existing pending-plus-finalize metrics and refresh
metadata remain unchanged. Normalized relative path remains the correlation key.
## Verification
- Dedicated verifier covers status insertion order, empty-path discard, FIFO,
failed/cached/null outcomes, terminal get, exact Mesh identity, stale-only raw
read, exact finalizer arguments, cache adoption, source ownership and timing.
- Pipeline/cache/extractor/runtime-finalizer/raw-repository/prototype/shutdown,
facade, internal-access and baseline regressions protect adjacent behavior.
- Fidelity evidence is exact lifecycle/I/O extraction only; no private asset,
original-client visual, leak-pressure or p95/p99 claim is made.
## Extension points
Further extraction can move static prototype/material-source construction without
changing this terminal Resource contract. Animated finalization remains separate
because its candidate-before-material ordering and Node ownership differ.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Static terminal polling/load | Implemented extraction | Status/FIFO/load/source verifier | Asset-backed traversal pending |
| Mesh extraction/preparation/cache outcome | Implemented extraction | Identity/raw/finalizer/store verifier | Private leak/p95/p99 pending |
| Static MultiMesh materialization | Separate implemented service | Materializer verifier | GPU/asset-backed evidence pending |
## Known gaps and risks
- Terminal Resource get, extraction and stale rebuild remain synchronous main-thread work.
- Prepared Mesh cache remains unbounded until final shutdown.
- Private asset traversal, descriptor pressure, leak and p95/p99 evidence is absent.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_mesh_resource_finalizer.gd` | Poll/load/extract/prepare/cache/missing service |
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending and completion FIFO ownership |
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh and temporary-instance ownership |
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback rules |
| `src/render/m2/m2_raw_model_repository.gd` | Optional native raw M2 boundary |
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh ownership |
| `src/render/m2/m2_prototype_cache_state.gd` | Missing-model outcome ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Permit loop and materialization composition |
| `src/tools/verify_m2_mesh_resource_finalizer.gd` | Polling/preparation/boundary/timing regression |
## Related decisions and references
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md)
- [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md)
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+3 -1
View File
@@ -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 |
+8 -4
View File
@@ -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 |
+18 -17
View File
@@ -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 |
@@ -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
+12 -7
View File
@@ -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 |
+182
View File
@@ -0,0 +1,182 @@
class_name M2MeshResourceFinalizer
extends RefCounted
## Polls static M2 ResourceLoader requests and publishes prepared Mesh outcomes.
## The caller owns permits, pipeline/cache lifetime and MultiMesh materialization.
var _mesh_resource_extractor: RefCounted
var _runtime_mesh_finalizer: RefCounted
var _raw_model_repository: RefCounted
var _resource_loader_adapter: Object
func _init(
mesh_resource_extractor: RefCounted,
runtime_mesh_finalizer: RefCounted,
raw_model_repository: RefCounted,
resource_loader_adapter: Object = null
) -> void:
_mesh_resource_extractor = mesh_resource_extractor
_runtime_mesh_finalizer = runtime_mesh_finalizer
_raw_model_repository = raw_model_repository
_resource_loader_adapter = resource_loader_adapter
## Moves terminal threaded requests into the pipeline finalize FIFO.
## Empty resource paths retain the historical immediate missing-model outcome.
func poll_terminal_requests(
load_pipeline_state: RefCounted,
prototype_cache_state: RefCounted
) -> int:
if load_pipeline_state == null or prototype_cache_state == null:
return 0
var completed_request_count := 0
var request_records: Array = load_pipeline_state.call(
"request_records_snapshot"
)
for request_variant in request_records:
if not (request_variant is Dictionary):
continue
var request: Dictionary = request_variant
var normalized_relative_path := String(request.get("normalized", ""))
var resource_path := String(request.get("path", ""))
if resource_path.is_empty():
load_pipeline_state.call(
"discard_request",
normalized_relative_path
)
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
completed_request_count += 1
continue
var load_status := load_threaded_get_status(resource_path)
if (
load_status != ResourceLoader.THREAD_LOAD_LOADED
and load_status != ResourceLoader.THREAD_LOAD_FAILED
):
continue
load_pipeline_state.call(
"complete_request",
normalized_relative_path,
load_status
)
completed_request_count += 1
return completed_request_count
## Pops and finalizes one terminal record. A true result means a record was
## consumed, including cached, failed and invalid outcomes.
func finalize_next_resource(
load_pipeline_state: RefCounted,
mesh_resource_cache_state: RefCounted,
prototype_cache_state: RefCounted,
extracted_directory: String
) -> bool:
if (
load_pipeline_state == null
or mesh_resource_cache_state == null
or prototype_cache_state == null
):
return false
var terminal_record: Dictionary = load_pipeline_state.call(
"pop_finalize_record"
)
if terminal_record.is_empty():
return false
var normalized_relative_path := String(
terminal_record.get("normalized", "")
)
if (
normalized_relative_path.is_empty()
or bool(mesh_resource_cache_state.call(
"has_mesh",
normalized_relative_path
))
):
return true
if int(terminal_record.get(
"status",
ResourceLoader.THREAD_LOAD_FAILED
)) != ResourceLoader.THREAD_LOAD_LOADED:
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
return true
var resource_path := String(terminal_record.get("path", ""))
var loaded_resource := load_threaded_get(resource_path)
var mesh := _mesh_resource_extractor.call(
"extract_first_mesh",
loaded_resource
) as Mesh
if mesh == null:
prototype_cache_state.call(
"mark_model_missing",
normalized_relative_path
)
return true
mesh_resource_cache_state.call(
"store_mesh",
normalized_relative_path,
prepare_mesh_for_runtime(
normalized_relative_path,
mesh,
extracted_directory
)
)
return true
## Applies the existing refresh-version/raw-data/rebuild path to one Mesh.
func prepare_mesh_for_runtime(
normalized_relative_path: String,
mesh: Mesh,
extracted_directory: String
) -> Mesh:
if mesh == null:
return null
if (
_runtime_mesh_finalizer == null
or not bool(_runtime_mesh_finalizer.call(
"requires_raw_data_for_refresh",
mesh
))
):
return mesh
var raw_model_data: Dictionary = {}
if _raw_model_repository != null:
raw_model_data = _raw_model_repository.call(
"load_static_model_data",
extracted_directory,
normalized_relative_path
)
return _runtime_mesh_finalizer.call(
"finalize_mesh",
normalized_relative_path,
mesh,
raw_model_data,
extracted_directory
) as Mesh
## Production ResourceLoader status adapter; injectable in synthetic tests.
func load_threaded_get_status(resource_path: String) -> int:
if _resource_loader_adapter != null:
return int(_resource_loader_adapter.call(
"load_threaded_get_status",
resource_path
))
return ResourceLoader.load_threaded_get_status(resource_path)
## Production ResourceLoader terminal-result adapter; injectable in tests.
func load_threaded_get(resource_path: String) -> Resource:
if _resource_loader_adapter != null:
return _resource_loader_adapter.call(
"load_threaded_get",
resource_path
) as Resource
return ResourceLoader.load_threaded_get(resource_path)
@@ -0,0 +1 @@
uid://de4xl1fywvqfw
+28 -51
View File
@@ -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
@@ -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.",
@@ -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.",
+15 -7
View File
@@ -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.",
@@ -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])
@@ -0,0 +1 @@
uid://by0rlnw5g8hjr
+4 -1
View File
@@ -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
+23 -10
View File
@@ -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;