merge: M2 Mesh resource finalizer
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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) |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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)
|
||||
@@ -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 |
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 |
|
||||
|
||||
@@ -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
|
||||
@@ -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.",
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user