refactor(M03): extract M2 prototype cache state
This commit is contained in:
@@ -40,6 +40,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
|||||||
- `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_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||||
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
|
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
|
||||||
- `src/render/m2/m2_runtime_mesh_finalizer.gd` - refresh version, classifier lifetime, M2Builder rebuild and original-Mesh fallback.
|
- `src/render/m2/m2_runtime_mesh_finalizer.gd` - refresh version, classifier lifetime, M2Builder rebuild and original-Mesh fallback.
|
||||||
|
- `src/render/m2/m2_prototype_cache_state.gd` - shutdown-lifetime static/animated prototype Nodes and missing/static-only path outcomes.
|
||||||
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
||||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||||
@@ -1090,8 +1091,9 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- The cache retains map-refresh lifetime and clears only at the existing final
|
- The cache retains map-refresh lifetime and clears only at the existing final
|
||||||
runtime-cache shutdown site after asynchronous work is drained.
|
runtime-cache shutdown site after asynchronous work is drained.
|
||||||
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
|
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
|
||||||
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, shared missing
|
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, cache adoption
|
||||||
and prototype/animated caches, permits and MultiMesh materialization.
|
decisions, permits and MultiMesh materialization. Prototype/negative state is
|
||||||
|
now a sibling cache service.
|
||||||
- Cache formats, refresh metadata, profiles and visible output are unchanged.
|
- Cache formats, refresh metadata, profiles and visible output are unchanged.
|
||||||
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
|
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
|
||||||
|
|
||||||
@@ -1115,8 +1117,8 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
FileAccess/ClassDB M2Loader I/O and supplies raw data through the loader only
|
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.
|
when the finalizer reports a stale Mesh; both historical clear sites persist.
|
||||||
- Billboard/UV-rotation predicates, rebuild extraction, metadata key and failure
|
- Billboard/UV-rotation predicates, rebuild extraction, metadata key and failure
|
||||||
fallback are unchanged. Cache adoption, missing state, permits and MultiMesh
|
fallback are unchanged. Cache adoption decisions, permits and MultiMesh
|
||||||
materialization remain loader-owned.
|
materialization remain loader-owned; negative outcomes belong to prototype state.
|
||||||
- Synthetic triangle rebuild/fallback timing is not asset-backed material,
|
- Synthetic triangle rebuild/fallback timing is not asset-backed material,
|
||||||
descriptor-pressure/leak or p95/p99 evidence.
|
descriptor-pressure/leak or p95/p99 evidence.
|
||||||
|
|
||||||
@@ -1127,13 +1129,29 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
`load_m2_animated` raw Dictionaries.
|
`load_m2_animated` raw Dictionaries.
|
||||||
- `StreamingWorldLoader` delegates the stale-Mesh refresh, static prototype and
|
- `StreamingWorldLoader` delegates the stale-Mesh refresh, static prototype and
|
||||||
native animated prototype reads. It retains normalization, `.tscn/.glb`
|
native animated prototype reads. It retains normalization, `.tscn/.glb`
|
||||||
fallback order, missing/prototype caches, builders, permits and Node/Mesh use.
|
fallback order, builders, permits and Node/Mesh use; prototype/negative state
|
||||||
|
is now isolated in `M2PrototypeCacheState`.
|
||||||
- The repository retains no path, native object or parsed data. Empty paths,
|
- The repository retains no path, native object or parsed data. Empty paths,
|
||||||
absent files/classes/methods and invalid results produce the same empty-value
|
absent files/classes/methods and invalid results produce the same empty-value
|
||||||
fallback contract; path join/globalization and native method names are exact.
|
fallback contract; path join/globalization and native method names are exact.
|
||||||
- No cache format, parser, material, geometry, animation or visible rule changed.
|
- No cache format, parser, material, geometry, animation or visible rule changed.
|
||||||
Synthetic missing-input timing is not asset-backed I/O/p95/p99 or fidelity evidence.
|
Synthetic missing-input timing is not asset-backed I/O/p95/p99 or fidelity evidence.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Prototype Cache State
|
||||||
|
|
||||||
|
- `M2PrototypeCacheState` now owns the exact static and animated prototype Node
|
||||||
|
references plus missing-model and static-animation negative path outcomes.
|
||||||
|
- Sync static, native animated, threaded GLB animation, Mesh finalize and
|
||||||
|
material-prototype paths delegate lookup/adoption/negative transitions to the
|
||||||
|
service. First valid prototype remains canonical for each normalized path.
|
||||||
|
- Map refresh still retains all four states. Final shutdown still occurs after
|
||||||
|
asynchronous drain and now calls one idempotent release operation that frees
|
||||||
|
detached prototypes, queues defensive in-tree Nodes and clears path outcomes.
|
||||||
|
- `StreamingWorldLoader` retains normalization, ResourceLoader/native I/O,
|
||||||
|
builders, animation policy, fallback order, permits and materialization.
|
||||||
|
- No cache format, visible rule or profile changed. Synthetic ownership/timing
|
||||||
|
evidence is not an asset-backed traversal/leak/p95/p99 or fidelity claim.
|
||||||
|
|
||||||
## 2026-07-17 WMO Placement Resolver Extraction
|
## 2026-07-17 WMO Placement Resolver Extraction
|
||||||
|
|
||||||
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
||||||
|
|||||||
@@ -27,6 +27,7 @@
|
|||||||
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
||||||
| M2 runtime mesh finalizer | Implemented extraction | [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-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 raw model repository | Implemented extraction | [`m2-raw-model-repository.md`](m2-raw-model-repository.md) |
|
||||||
|
| M2 prototype cache state | Implemented extraction | [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md) |
|
||||||
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
||||||
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
||||||
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
|
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ flowchart LR
|
|||||||
State --> FIFO[Finalize FIFO]
|
State --> FIFO[Finalize FIFO]
|
||||||
FIFO --> Loader
|
FIFO --> Loader
|
||||||
Loader --> Budget[M2_MESH_FINALIZE permit]
|
Loader --> Budget[M2_MESH_FINALIZE permit]
|
||||||
Loader --> Cache[M2 Mesh resource cache or shared missing cache]
|
Loader --> Cache[M2 Mesh resource cache or M2 prototype outcome state]
|
||||||
```
|
```
|
||||||
|
|
||||||
Allowed dependencies are value containers, Strings and opaque integer statuses.
|
Allowed dependencies are value containers, Strings and opaque integer statuses.
|
||||||
@@ -88,7 +88,7 @@ flowchart TD
|
|||||||
Permit -->|yes| Pop[Pop oldest terminal record]
|
Permit -->|yes| Pop[Pop oldest terminal record]
|
||||||
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
|
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
|
||||||
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
|
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
|
||||||
Prepare --> Adopt[Loader adopts Mesh/missing state]
|
Prepare --> Adopt[Loader adopts Mesh or marks prototype outcome state]
|
||||||
```
|
```
|
||||||
|
|
||||||
## Lifecycle/state
|
## Lifecycle/state
|
||||||
@@ -137,7 +137,7 @@ flowchart TB
|
|||||||
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
|
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
|
||||||
Loader --> Resource[ResourceLoader]
|
Loader --> Resource[ResourceLoader]
|
||||||
Loader --> Budget[RenderBudgetScheduler]
|
Loader --> Budget[RenderBudgetScheduler]
|
||||||
Loader --> MeshCache[M2 Mesh resource cache and shared missing cache]
|
Loader --> MeshCache[M2 Mesh resource and prototype outcome cache states]
|
||||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||||
State -. no dependency .-> Resource
|
State -. no dependency .-> Resource
|
||||||
State -. no dependency .-> Budget
|
State -. no dependency .-> Budget
|
||||||
@@ -151,7 +151,8 @@ flowchart TB
|
|||||||
- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear.
|
- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear.
|
||||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and
|
- `M2MeshResourceCacheState` owns prepared static Mesh references and
|
||||||
`M2RuntimeMeshFinalizer` owns refresh/rebuild/fallback. The loader owns shared
|
`M2RuntimeMeshFinalizer` owns refresh/rebuild/fallback. The loader owns shared
|
||||||
missing state, raw I/O and remaining engine resources.
|
adoption decisions, raw I/O and remaining engine resources; the prototype
|
||||||
|
cache service owns missing outcomes.
|
||||||
- Snapshot and diagnostic records are detached; caller mutation cannot affect state.
|
- Snapshot and diagnostic records are detached; caller mutation cannot affect state.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## Errors, cancellation and recovery
|
||||||
@@ -226,7 +227,8 @@ rebuild policy are unchanged; no migration or rebake is required.
|
|||||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh references and final clear |
|
| `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_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_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Cache path choice, I/O polling, permits and Mesh/missing adoption |
|
| `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/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Downstream stale Mesh rebuild decision |
|
| `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 |
|
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
|
||||||
|
|
||||||
|
|||||||
@@ -71,13 +71,13 @@ Store and clear mutate only the cache Dictionary and Resource reference counts.
|
|||||||
flowchart TD
|
flowchart TD
|
||||||
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
|
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
|
||||||
Cached -->|yes| Return[Return retained Mesh]
|
Cached -->|yes| Return[Return retained Mesh]
|
||||||
Cached -->|no| Missing{Shared missing state?}
|
Cached -->|no| Missing{Prototype outcome says missing?}
|
||||||
Missing -->|yes| Null[Return null]
|
Missing -->|yes| Null[Return null]
|
||||||
Missing -->|no| Load[Loader request or synchronous fallback]
|
Missing -->|no| Load[Loader request or synchronous fallback]
|
||||||
Load --> Prepare[Extractor and runtime finalizer prepare Mesh]
|
Load --> Prepare[Extractor and runtime finalizer prepare Mesh]
|
||||||
Prepare --> Valid{Prepared Mesh valid?}
|
Prepare --> Valid{Prepared Mesh valid?}
|
||||||
Valid -->|yes| Store[Store or replace cache entry]
|
Valid -->|yes| Store[Store or replace cache entry]
|
||||||
Valid -->|no| MarkMissing[Loader updates shared missing state]
|
Valid -->|no| MarkMissing[Loader updates prototype outcome state]
|
||||||
Store --> Return
|
Store --> Return
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -131,7 +131,8 @@ flowchart TB
|
|||||||
- The cache owns normalized-path Strings and strong Mesh references.
|
- The cache owns normalized-path Strings and strong Mesh references.
|
||||||
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
||||||
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
||||||
instances. The loader owns missing/prototype/animated state and MultiMeshes.
|
instances. `M2PrototypeCacheState` owns missing/prototype/animated state; the
|
||||||
|
loader owns MultiMeshes and adoption decisions.
|
||||||
- The loader drains asynchronous work before the final cache clear.
|
- The loader drains asynchronous work before the final cache clear.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## Errors, cancellation and recovery
|
||||||
@@ -190,8 +191,7 @@ the historical Mesh cache had no queue contribution or log site.
|
|||||||
## Known gaps and risks
|
## Known gaps and risks
|
||||||
|
|
||||||
- Strong Mesh references intentionally remain until final shutdown.
|
- Strong Mesh references intentionally remain until final shutdown.
|
||||||
- Shared missing state cannot move here without also redesigning prototype and
|
- Missing/prototype/animated lookup state is now a separate sibling service.
|
||||||
animated lookup behavior.
|
|
||||||
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
|
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
|
||||||
original-client capture is included.
|
original-client capture is included.
|
||||||
|
|
||||||
@@ -201,7 +201,8 @@ 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_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_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, raw/resource I/O, missing state and materialization |
|
| `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/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
| `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/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 |
|
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Cache ownership/lifetime/boundary/timing regression |
|
||||||
|
|||||||
@@ -194,7 +194,8 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | ResourceLoader/raw I/O, caching, missing state and materialization |
|
| `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/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
| `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/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 |
|
| `src/tools/verify_m2_mesh_resource_extractor.gd` | Resource/order/lifetime/boundary/timing regression |
|
||||||
|
|||||||
@@ -0,0 +1,247 @@
|
|||||||
|
# M2 Prototype Cache State
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-PROTOTYPE-CACHE-001` |
|
||||||
|
| Owners | Detached static/animated prototypes and missing/static-animation outcomes |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-prototype-cache`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Static, GLB animated and native animated M2 prototype reuse |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Own the four shutdown-lifetime M2 prototype lookup states previously stored in
|
||||||
|
`StreamingWorldLoader`: static and animated `Node3D` references, missing model
|
||||||
|
paths and paths whose animation fallback is static-only.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Normalize paths, choose cache files, call ResourceLoader or parse raw M2 data.
|
||||||
|
- Build, repair or instantiate prototypes, Meshes, materials or MultiMeshes.
|
||||||
|
- Own animation/Mesh request queues, permits or tile/build state.
|
||||||
|
- Add eviction, persistence, retries, profile rules or format changes.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
|
||||||
|
Resource[ResourceLoader / raw builders] --> Loader
|
||||||
|
State --> Static[Static prototype Node3D]
|
||||||
|
State --> Animated[Animated prototype Node3D]
|
||||||
|
State --> Missing[Missing-model path set]
|
||||||
|
State --> StaticOnly[Static-animation path set]
|
||||||
|
Loader --> Instances[Duplicate/animated instance materialization]
|
||||||
|
```
|
||||||
|
|
||||||
|
The state may depend on `Node3D` lifetime APIs and value-only path containers.
|
||||||
|
Filesystem/native/ResourceLoader APIs, builders, worker pools, render scheduling,
|
||||||
|
Mesh traversal and other application layers are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `has_static_prototype(path)` | Query | Test for one valid retained static prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
|
||||||
|
| `find_static_prototype(path)` | Query | Return exact retained static Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
|
||||||
|
| `adopt_static_prototype(path, prototype)` | Command/query | Retain first valid static prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
|
||||||
|
| `has_animated_prototype(path)` | Query | Test for one valid retained animated prototype | Renderer main thread; shutdown lifetime | Empty/invalid returns false |
|
||||||
|
| `find_animated_prototype(path)` | Query | Return exact retained animated Node reference | Renderer main thread; borrowed | Empty/unknown/invalid returns null |
|
||||||
|
| `adopt_animated_prototype(path, prototype)` | Command/query | Retain first valid animated prototype and return canonical reference | Renderer main thread; until shutdown | Invalid input returns null; later candidate released |
|
||||||
|
| `mark_model_missing(path)` / `is_model_missing(path)` | Command/query | Store/query a missing-model outcome | Renderer main thread; until shutdown | Empty mark rejected |
|
||||||
|
| `mark_animation_static(path)` / `is_animation_static(path)` | Command/query | Store/query static-only animation outcome | Renderer main thread; until shutdown | Empty mark rejected |
|
||||||
|
| `diagnostic_snapshot()` | Diagnostic query | Return detached sorted path arrays for all four states | Caller-owned result | Never exposes Nodes |
|
||||||
|
| `clear_and_release()` | Lifecycle command | Release prototypes and clear all state | Final shutdown after async drain | Idempotent |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Normalized non-empty M2 path | Loader normalization | Cache state | Copied String key | Shutdown lifetime |
|
||||||
|
| Input | Detached static Node3D | Cached scene/raw builder adapter | Cache state | Adopted on success | Until final shutdown |
|
||||||
|
| Input | Detached animated Node3D | GLB/native animation adapter | Cache state | Adopted on success | Until final shutdown |
|
||||||
|
| Input | Missing/static-only outcome | Loader failure/fallback adapter | Cache state | Boolean set entry | Until final shutdown |
|
||||||
|
| Output | Canonical prototype Node3D | Cache state | Loader instance/material adapter | Borrowed exact reference | One lookup/use |
|
||||||
|
| Output | Detached path-only snapshot | Cache state | Tests/diagnostics | Fresh caller-owned arrays | One query |
|
||||||
|
|
||||||
|
Side effects are strong Node retention, release of rejected duplicate candidates,
|
||||||
|
final Node destruction/queueing and mutation of runtime-only path state.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Request[Normalized prototype request] --> Positive{Valid prototype cached?}
|
||||||
|
Positive -->|yes| Return[Return exact prototype]
|
||||||
|
Positive -->|no| Negative{Missing or static-only outcome?}
|
||||||
|
Negative -->|yes| Fallback[Return caller fallback]
|
||||||
|
Negative -->|no| Load[Loader performs existing I/O/build]
|
||||||
|
Load --> Success{Valid prototype?}
|
||||||
|
Success -->|yes| Adopt[Adopt first candidate]
|
||||||
|
Adopt --> Return
|
||||||
|
Success -->|no| Mark[Mark relevant negative outcome]
|
||||||
|
Mark --> Fallback
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Empty
|
||||||
|
Empty --> Positive: adopt static/animated prototype
|
||||||
|
Empty --> Negative: mark missing/static-only path
|
||||||
|
Positive --> Positive: lookup or same-reference adoption
|
||||||
|
Positive --> Positive: duplicate candidate released; first wins
|
||||||
|
Negative --> Negative: repeated mark/query
|
||||||
|
Positive --> Released: final shutdown
|
||||||
|
Negative --> Released: final shutdown
|
||||||
|
Released --> Released: repeated clear
|
||||||
|
Released --> [*]
|
||||||
|
```
|
||||||
|
|
||||||
|
Map refresh does not transition or clear this service. Final scene shutdown calls
|
||||||
|
`clear_and_release` only after worker and ResourceLoader work is drained.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant State as M2PrototypeCacheState
|
||||||
|
participant Source as ResourceLoader/raw builder
|
||||||
|
Loader->>State: find static/animated(path)
|
||||||
|
alt positive hit
|
||||||
|
State-->>Loader: exact retained Node3D
|
||||||
|
else negative hit
|
||||||
|
Loader->>State: is missing/static-only(path)
|
||||||
|
State-->>Loader: true; use historical fallback
|
||||||
|
else uncached
|
||||||
|
Loader->>Source: existing load/build operation
|
||||||
|
alt valid prototype
|
||||||
|
Loader->>State: adopt(path, prototype)
|
||||||
|
State-->>Loader: canonical Node3D
|
||||||
|
else failure
|
||||||
|
Loader->>State: mark missing/static-only(path)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
Loader->>State: shutdown clear_and_release()
|
||||||
|
State-->>Loader: all paths empty; Nodes released
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> State[M2PrototypeCacheState]
|
||||||
|
State --> Node3D
|
||||||
|
Loader --> Raw[M2RawModelRepository]
|
||||||
|
Loader --> ResourceLoader
|
||||||
|
Loader --> StaticBuilder[M2Builder]
|
||||||
|
Loader --> AnimatedBuilder[M2NativeAnimatedBuilder]
|
||||||
|
State -. no dependency .-> Raw
|
||||||
|
State -. no dependency .-> ResourceLoader
|
||||||
|
State -. no dependency .-> StaticBuilder
|
||||||
|
State -. no dependency .-> MeshCache[M2MeshResourceCacheState]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Renderer main-thread adapters serialize all operations.
|
||||||
|
- The state owns strong references to adopted static and animated prototype Nodes.
|
||||||
|
- A duplicate detached candidate is released immediately; the first valid Node
|
||||||
|
stays canonical for that path.
|
||||||
|
- Lookups borrow exact Node references without transferring ownership.
|
||||||
|
- Detached Nodes are synchronously `free()`d at shutdown. Defensive in-tree
|
||||||
|
Nodes use `queue_free()` to preserve SceneTree rules.
|
||||||
|
- Negative entries own only copied normalized-path Strings.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty path/null/invalid Node | Admission guard | Reject/null/false | Dedicated verifier | Correct caller input |
|
||||||
|
| Unknown positive lookup | Map miss | Return null | Path snapshot | Loader continues existing load/fallback |
|
||||||
|
| Duplicate candidate | Occupied valid path | Release candidate; return first | Identity fixture | None required |
|
||||||
|
| Model source/load failure | Loader result | Mark missing outcome | Existing fallback behavior | New loader session/source repair |
|
||||||
|
| Animation unavailable/unsafe | Loader policy/result | Mark static-only outcome | Existing fallback behavior | New loader session/cache repair |
|
||||||
|
| Shutdown | Loader lifecycle | Release positive and clear all state | Shutdown verifier | New loader begins empty |
|
||||||
|
| Cancellation | Not owned | No state transition inside this service | N/A | Loader drains/cancels before shutdown clear |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Positive cache lifetime | Final loader shutdown | All | No | Reuses detached prototypes across map refresh |
|
||||||
|
| Negative cache lifetime | Final loader shutdown | All | No | Preserves historical fallback suppression |
|
||||||
|
| Eviction capacity | Unbounded historical behavior | All | No | No mid-session Node destruction |
|
||||||
|
|
||||||
|
Animation enablement, candidate/allow/deny rules, cache paths and per-frame
|
||||||
|
permits remain loader configuration.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
This is runtime-only state and serializes nothing. `.tscn/.glb`, material refresh
|
||||||
|
and native M2 formats are unchanged; no migration or rebake is introduced.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- `diagnostic_snapshot` exposes four sorted path arrays without Node references.
|
||||||
|
- Existing `M2_ANIM_CACHE` and native animation logs remain loader-owned.
|
||||||
|
- Existing renderer queue metrics remain unchanged because these tables never
|
||||||
|
contributed work counts.
|
||||||
|
- Normalized relative path remains the correlation key.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_prototype_cache_state.gd`: invalid admission, exact static/animated
|
||||||
|
identity, independent positive/negative states, first-wins duplicate release,
|
||||||
|
detached/in-tree shutdown ownership, idempotent clear, detached diagnostics,
|
||||||
|
loader source boundaries and 100-by-256 bounded path-state timing.
|
||||||
|
- Runtime-cache shutdown regression verifies final release through the real loader.
|
||||||
|
- Raw repository/finalizer/extractor/Mesh cache/pipeline/material/animation/M2
|
||||||
|
build regressions protect adjacent behavior.
|
||||||
|
- Fidelity evidence is exact cache transition and lifetime extraction. No new
|
||||||
|
proprietary asset or original-client visual comparison is claimed.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
Eviction or byte/count budgets require measured memory evidence and explicit
|
||||||
|
prototype-user lifetime rules. Animation request-state extraction can consume
|
||||||
|
this service without moving ResourceLoader or builder ownership into it.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Static prototype retention | Implemented extraction | Identity/source/shutdown verifier | Asset-backed traversal/leak run pending |
|
||||||
|
| Animated prototype retention | Implemented extraction | Independent identity/shutdown verifier | Legal animated success fixture pending |
|
||||||
|
| Missing/static-only outcomes | Implemented extraction | Independent negative-state verifier | Recovery still requires new loader session |
|
||||||
|
| Bounded eviction | Historical unbounded behavior | Documented lifetime | Add only after memory profiling |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Strong Node references intentionally live until final shutdown.
|
||||||
|
- Negative outcomes do not retain reason codes and cannot be individually retried,
|
||||||
|
preserving historical behavior.
|
||||||
|
- No proprietary long traversal, descriptor pressure, leak, p95/p99 or paired
|
||||||
|
original-client evidence is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_prototype_cache_state.gd` | Positive Node ownership, negative path state and shutdown release |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, I/O/build/fallback decisions and cache adapters |
|
||||||
|
| `src/tools/verify_m2_prototype_cache_state.gd` | Admission/identity/lifecycle/source/timing regression |
|
||||||
|
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Integrated final-shutdown release regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
|
||||||
|
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||||
|
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||||
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
@@ -55,7 +55,7 @@ renderer policy and other application layers are forbidden.
|
|||||||
| Input | Extracted `.m2` bytes | Local legal extraction | Native M2Loader | File-owned | Native 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` | Loader/finalizer/M2Builder | Fresh native result | One caller operation |
|
||||||
| Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Loader/animated builder | Fresh native result | One caller operation |
|
| Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Loader/animated builder | Fresh native result | One caller operation |
|
||||||
| Output | Empty Dictionary | Repository guards | Loader fallback/missing state | Fresh value | One failed call |
|
| Output | Empty Dictionary | Repository guards | Loader fallback and prototype outcome adapter | Fresh value | One failed call |
|
||||||
|
|
||||||
Side effects are limited to file-existence inspection, synchronous native file
|
Side effects are limited to file-existence inspection, synchronous native file
|
||||||
parsing and creation of one temporary native loader instance per accepted call.
|
parsing and creation of one temporary native loader instance per accepted call.
|
||||||
@@ -133,7 +133,8 @@ flowchart TB
|
|||||||
## Ownership, threading and resources
|
## Ownership, threading and resources
|
||||||
|
|
||||||
- The repository owns only call-local path, native instance and result values.
|
- The repository owns only call-local path, native instance and result values.
|
||||||
- The loader owns path normalization, cache/fallback selection and adoption.
|
- The loader owns path normalization and fallback selection;
|
||||||
|
`M2PrototypeCacheState` owns prototype/negative adoption.
|
||||||
- Native `M2Loader` owns parsing behavior and returns a new Dictionary value.
|
- Native `M2Loader` owns parsing behavior and returns a new Dictionary value.
|
||||||
- Calls are synchronous on the caller's thread; current renderer callers use the
|
- Calls are synchronous on the caller's thread; current renderer callers use the
|
||||||
main thread. The module starts no tasks and touches no SceneTree objects.
|
main thread. The module starts no tasks and touches no SceneTree objects.
|
||||||
@@ -145,7 +146,7 @@ flowchart TB
|
|||||||
|---|---|---|---|---|
|
|---|---|---|---|---|
|
||||||
| Empty normalized path | String guard | Return `{}` | Dedicated verifier | Correct caller normalization |
|
| Empty normalized path | String guard | Return `{}` | Dedicated verifier | Correct caller normalization |
|
||||||
| Native extension absent | `ClassDB.class_exists` | Return `{}` | Existing renderer fallback | Install/load extension and retry |
|
| Native extension absent | `ClassDB.class_exists` | Return `{}` | Existing renderer fallback | Install/load extension and retry |
|
||||||
| Extracted file absent | `FileAccess.file_exists` | Return `{}` | Loader adopts historical missing state | Restore extraction and clear caller state |
|
| Extracted file absent | `FileAccess.file_exists` | Return `{}` | Prototype cache records historical missing state through loader | Restore extraction and start a new loader session |
|
||||||
| Instance/method absent | Null/`has_method` guard | Return `{}` | Source contract verifier | Repair extension registration |
|
| Instance/method absent | Null/`has_method` guard | Return `{}` | Source contract verifier | Repair extension registration |
|
||||||
| Parse/non-Dictionary result | Result type check | Return `{}` | Existing loader fallback | Repair asset/parser and retry |
|
| Parse/non-Dictionary result | Result type check | Return `{}` | Existing loader fallback | Repair asset/parser and retry |
|
||||||
| Cancellation requested | Not supported inside synchronous native call | Call completes | None | Caller controls whether to start call |
|
| Cancellation requested | Not supported inside synchronous native call | Call completes | None | Caller controls whether to start call |
|
||||||
@@ -210,7 +211,8 @@ measured work packages rather than expansion of this repository.
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
|
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback/cache state and result consumers |
|
| `src/render/m2/m2_prototype_cache_state.gd` | Prototype and negative-result retention |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback decisions and result consumers |
|
||||||
| `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation |
|
| `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation |
|
||||||
| `src/tools/verify_m2_raw_model_repository.gd` | Rejected-input/source/dependency/timing regression |
|
| `src/tools/verify_m2_raw_model_repository.gd` | Rejected-input/source/dependency/timing regression |
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| Status | Partial |
|
| Status | Partial |
|
||||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||||
| Owners | Renderer workstream / milestone integrator |
|
| Owners | Renderer workstream / milestone integrator |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-raw-model-repository`, 2026-07-17 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-prototype-cache`, 2026-07-17 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -138,6 +138,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `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 |
|
| `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 |
|
||||||
| `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 |
|
| `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 |
|
| `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 |
|
||||||
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
||||||
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||||
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
||||||
@@ -182,6 +183,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
||||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||||
| Internal raw M2 read | Extracted directory and normalized relative path | Loader / `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
|
| Internal raw M2 read | Extracted directory and normalized relative path | Loader / `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
|
||||||
|
| Internal M2 prototype state | Normalized path, adopted static/animated Node or negative outcome | Loader / `M2PrototypeCacheState` | Loader reuse/fallback adapters | State-owned strong Node refs and Strings; detached diagnostics | Until final shutdown |
|
||||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||||
@@ -377,10 +379,11 @@ sequenceDiagram
|
|||||||
finalizer. The raw repository loads value data; loader retains Mesh adoption.
|
finalizer. The raw repository loads value data; loader retains Mesh adoption.
|
||||||
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
|
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
|
||||||
terminal statuses and completion-order finalize FIFO. The loader retains cache
|
terminal statuses and completion-order finalize FIFO. The loader retains cache
|
||||||
path selection, ResourceLoader calls, permits, shared missing state and adoption.
|
path selection, ResourceLoader calls, permits and adoption decisions; prototype
|
||||||
|
cache state owns shared missing outcomes.
|
||||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
|
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
|
||||||
them at the existing final-shutdown site. The loader retains missing/prototype
|
them at the existing final-shutdown site. Prototype state and materialization
|
||||||
state and materialization.
|
belong to the sibling cache service and loader respectively.
|
||||||
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
|
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
|
||||||
PackedScene instance destruction. The loader retains ResourceLoader I/O,
|
PackedScene instance destruction. The loader retains ResourceLoader I/O,
|
||||||
cache/missing adoption and materialization.
|
cache/missing adoption and materialization.
|
||||||
@@ -388,8 +391,12 @@ sequenceDiagram
|
|||||||
M2Builder rebuild and original-Mesh fallback. The loader loads raw data only
|
M2Builder rebuild and original-Mesh fallback. The loader loads raw data only
|
||||||
after the finalizer reports that a cached Mesh is stale.
|
after the finalizer reports that a cached Mesh is stale.
|
||||||
- `M2RawModelRepository` owns FileAccess/ClassDB availability and the exact
|
- `M2RawModelRepository` owns FileAccess/ClassDB availability and the exact
|
||||||
`load_m2`/`load_m2_animated` calls. The loader retains normalization,
|
`load_m2`/`load_m2_animated` calls. The loader retains normalization, fallback
|
||||||
fallback order, missing/prototype state and every result consumer.
|
order and every result consumer; `M2PrototypeCacheState` retains outcomes.
|
||||||
|
- `M2PrototypeCacheState` owns exact static/animated prototype references,
|
||||||
|
missing-model and static-animation path sets, duplicate-candidate release and
|
||||||
|
final-shutdown Node cleanup. ResourceLoader, builders and fallback decisions
|
||||||
|
remain loader-owned.
|
||||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
the facade never returns Mesh, Node, tile-state or queue references.
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
@@ -531,6 +538,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
|
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
|
||||||
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
|
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
|
||||||
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
|
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Queue/resource state, spatial cells and asset-backed p95/p99 remain pending |
|
||||||
|
| M2 prototype cache state | Implemented extraction | Synthetic identity/negative/lifecycle/source/timing plus shutdown contract | Asset-backed traversal/leak/p95/p99 pending |
|
||||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||||
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||||
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
||||||
@@ -596,6 +604,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Runtime refresh version, rebuild classification/build and fallback |
|
| `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_raw_model_repository.gd` | Stateless static/animated native M2 file boundary |
|
||||||
|
| `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes |
|
||||||
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||||
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||||
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||||
@@ -616,6 +625,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression |
|
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/timing regression |
|
| `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/timing regression |
|
||||||
|
| `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression |
|
||||||
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||||
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||||
|
|||||||
@@ -0,0 +1,157 @@
|
|||||||
|
class_name M2PrototypeCacheState
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns detached static/animated M2 prototypes and their negative lookup
|
||||||
|
## outcomes until final renderer shutdown.
|
||||||
|
|
||||||
|
var _static_prototypes_by_path: Dictionary = {}
|
||||||
|
var _animated_prototypes_by_path: Dictionary = {}
|
||||||
|
var _missing_model_paths: Dictionary = {}
|
||||||
|
var _static_animation_paths: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether a valid static prototype is retained for the normalized path.
|
||||||
|
func has_static_prototype(normalized_relative_path: String) -> bool:
|
||||||
|
return find_static_prototype(normalized_relative_path) != null
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the exact retained static prototype or null.
|
||||||
|
func find_static_prototype(normalized_relative_path: String) -> Node3D:
|
||||||
|
return _find_valid_prototype(_static_prototypes_by_path, normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Adopts a static prototype and returns the canonical retained reference. The
|
||||||
|
## first valid prototype wins; a later detached candidate is released.
|
||||||
|
func adopt_static_prototype(
|
||||||
|
normalized_relative_path: String,
|
||||||
|
prototype: Node3D) -> Node3D:
|
||||||
|
return _adopt_prototype(
|
||||||
|
_static_prototypes_by_path,
|
||||||
|
normalized_relative_path,
|
||||||
|
prototype
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether a valid animated prototype is retained for the normalized path.
|
||||||
|
func has_animated_prototype(normalized_relative_path: String) -> bool:
|
||||||
|
return find_animated_prototype(normalized_relative_path) != null
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the exact retained animated prototype or null.
|
||||||
|
func find_animated_prototype(normalized_relative_path: String) -> Node3D:
|
||||||
|
return _find_valid_prototype(_animated_prototypes_by_path, normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Adopts an animated prototype and returns the canonical retained reference.
|
||||||
|
func adopt_animated_prototype(
|
||||||
|
normalized_relative_path: String,
|
||||||
|
prototype: Node3D) -> Node3D:
|
||||||
|
return _adopt_prototype(
|
||||||
|
_animated_prototypes_by_path,
|
||||||
|
normalized_relative_path,
|
||||||
|
prototype
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Records that no model prototype can currently be loaded for this path.
|
||||||
|
func mark_model_missing(normalized_relative_path: String) -> bool:
|
||||||
|
return _mark_path(_missing_model_paths, normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether the model path has a retained missing outcome.
|
||||||
|
func is_model_missing(normalized_relative_path: String) -> bool:
|
||||||
|
return not normalized_relative_path.is_empty() and _missing_model_paths.has(
|
||||||
|
normalized_relative_path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Records that the path must use static presentation instead of animation.
|
||||||
|
func mark_animation_static(normalized_relative_path: String) -> bool:
|
||||||
|
return _mark_path(_static_animation_paths, normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether the path has a retained static-animation outcome.
|
||||||
|
func is_animation_static(normalized_relative_path: String) -> bool:
|
||||||
|
return not normalized_relative_path.is_empty() and _static_animation_paths.has(
|
||||||
|
normalized_relative_path
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached path-only cache state for diagnostics and verification.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
return {
|
||||||
|
"static_prototype_paths": _sorted_paths(_static_prototypes_by_path),
|
||||||
|
"animated_prototype_paths": _sorted_paths(_animated_prototypes_by_path),
|
||||||
|
"missing_model_paths": _sorted_paths(_missing_model_paths),
|
||||||
|
"static_animation_paths": _sorted_paths(_static_animation_paths),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Releases every retained prototype and clears positive and negative state.
|
||||||
|
## Detached Nodes are freed synchronously; in-tree Nodes are queued for deletion.
|
||||||
|
func clear_and_release() -> void:
|
||||||
|
_release_prototypes(_static_prototypes_by_path)
|
||||||
|
_release_prototypes(_animated_prototypes_by_path)
|
||||||
|
_missing_model_paths.clear()
|
||||||
|
_static_animation_paths.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _find_valid_prototype(
|
||||||
|
prototypes_by_path: Dictionary,
|
||||||
|
normalized_relative_path: String) -> Node3D:
|
||||||
|
if normalized_relative_path.is_empty():
|
||||||
|
return null
|
||||||
|
var prototype_variant: Variant = prototypes_by_path.get(normalized_relative_path)
|
||||||
|
if not (prototype_variant is Node3D):
|
||||||
|
return null
|
||||||
|
var prototype := prototype_variant as Node3D
|
||||||
|
return prototype if is_instance_valid(prototype) else null
|
||||||
|
|
||||||
|
|
||||||
|
func _adopt_prototype(
|
||||||
|
prototypes_by_path: Dictionary,
|
||||||
|
normalized_relative_path: String,
|
||||||
|
prototype: Node3D) -> Node3D:
|
||||||
|
if normalized_relative_path.is_empty() or prototype == null or not is_instance_valid(prototype):
|
||||||
|
return null
|
||||||
|
var retained_prototype := _find_valid_prototype(
|
||||||
|
prototypes_by_path,
|
||||||
|
normalized_relative_path
|
||||||
|
)
|
||||||
|
if retained_prototype != null:
|
||||||
|
if not is_same(retained_prototype, prototype):
|
||||||
|
_release_prototype(prototype)
|
||||||
|
return retained_prototype
|
||||||
|
prototypes_by_path[normalized_relative_path] = prototype
|
||||||
|
return prototype
|
||||||
|
|
||||||
|
|
||||||
|
func _mark_path(paths: Dictionary, normalized_relative_path: String) -> bool:
|
||||||
|
if normalized_relative_path.is_empty():
|
||||||
|
return false
|
||||||
|
paths[normalized_relative_path] = true
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
func _release_prototypes(prototypes_by_path: Dictionary) -> void:
|
||||||
|
for prototype_variant in prototypes_by_path.values():
|
||||||
|
if prototype_variant is Node3D:
|
||||||
|
_release_prototype(prototype_variant as Node3D)
|
||||||
|
prototypes_by_path.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _release_prototype(prototype: Node3D) -> void:
|
||||||
|
if prototype == null or not is_instance_valid(prototype):
|
||||||
|
return
|
||||||
|
if prototype.is_inside_tree():
|
||||||
|
prototype.queue_free()
|
||||||
|
else:
|
||||||
|
prototype.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _sorted_paths(paths: Dictionary) -> Array[String]:
|
||||||
|
var result: Array[String] = []
|
||||||
|
for path_variant in paths.keys():
|
||||||
|
result.append(String(path_variant))
|
||||||
|
result.sort()
|
||||||
|
return result
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://srnqsus8pddp
|
||||||
@@ -66,6 +66,9 @@ const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
|||||||
const M2_RAW_MODEL_REPOSITORY_SCRIPT := preload(
|
const M2_RAW_MODEL_REPOSITORY_SCRIPT := preload(
|
||||||
"res://src/render/m2/m2_raw_model_repository.gd"
|
"res://src/render/m2/m2_raw_model_repository.gd"
|
||||||
)
|
)
|
||||||
|
const M2_PROTOTYPE_CACHE_STATE_SCRIPT := preload(
|
||||||
|
"res://src/render/m2/m2_prototype_cache_state.gd"
|
||||||
|
)
|
||||||
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||||
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||||
)
|
)
|
||||||
@@ -263,6 +266,7 @@ var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
|
|||||||
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
|
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
|
||||||
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
|
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
|
||||||
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
|
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_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||||
var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
|
var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
|
||||||
var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
|
var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||||
@@ -309,10 +313,6 @@ var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new()
|
|||||||
var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()
|
var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||||
var _wmo_missing_cache: Dictionary = {}
|
var _wmo_missing_cache: Dictionary = {}
|
||||||
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
||||||
var _m2_scene_cache: Dictionary = {}
|
|
||||||
var _m2_animated_scene_cache: Dictionary = {}
|
|
||||||
var _m2_static_animation_cache: Dictionary = {}
|
|
||||||
var _m2_missing_cache: Dictionary = {}
|
|
||||||
var _world_wmo_root: Node3D
|
var _world_wmo_root: Node3D
|
||||||
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||||
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
|
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
|
||||||
@@ -481,12 +481,9 @@ func _render_operation_limits_for_frame() -> Dictionary:
|
|||||||
## loader after all asynchronous work has completed. Map refreshes retain these
|
## loader after all asynchronous work has completed. Map refreshes retain these
|
||||||
## caches; only scene shutdown destroys them.
|
## caches; only scene shutdown destroys them.
|
||||||
func _release_runtime_caches_for_shutdown() -> void:
|
func _release_runtime_caches_for_shutdown() -> void:
|
||||||
_free_detached_node_cache(_m2_scene_cache)
|
_m2_prototype_cache_state.clear_and_release()
|
||||||
_free_detached_node_cache(_m2_animated_scene_cache)
|
|
||||||
_free_detached_node_cache(_wmo_prototype_cache)
|
_free_detached_node_cache(_wmo_prototype_cache)
|
||||||
_m2_mesh_resource_cache_state.clear()
|
_m2_mesh_resource_cache_state.clear()
|
||||||
_m2_static_animation_cache.clear()
|
|
||||||
_m2_missing_cache.clear()
|
|
||||||
_wmo_render_resource_cache_state.clear_all()
|
_wmo_render_resource_cache_state.clear_all()
|
||||||
_wmo_scene_resource_cache_state.clear_all()
|
_wmo_scene_resource_cache_state.clear_all()
|
||||||
_wmo_missing_cache.clear()
|
_wmo_missing_cache.clear()
|
||||||
@@ -4219,7 +4216,7 @@ func _drain_m2_animation_loads() -> void:
|
|||||||
var path := String(pending.get("path", ""))
|
var path := String(pending.get("path", ""))
|
||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
_m2_animation_load_requests.erase(normalized_rel)
|
_m2_animation_load_requests.erase(normalized_rel)
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
var status := ResourceLoader.load_threaded_get_status(path)
|
||||||
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
||||||
@@ -4232,10 +4229,14 @@ func _drain_m2_animation_loads() -> void:
|
|||||||
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
|
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE):
|
||||||
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
|
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
|
||||||
var normalized_rel := String(pending.get("normalized", ""))
|
var normalized_rel := String(pending.get("normalized", ""))
|
||||||
if normalized_rel.is_empty() or _m2_animated_scene_cache.has(normalized_rel) or _m2_static_animation_cache.has(normalized_rel):
|
if (
|
||||||
|
normalized_rel.is_empty()
|
||||||
|
or _m2_prototype_cache_state.has_animated_prototype(normalized_rel)
|
||||||
|
or _m2_prototype_cache_state.is_animation_static(normalized_rel)
|
||||||
|
):
|
||||||
continue
|
continue
|
||||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var path := String(pending.get("path", ""))
|
var path := String(pending.get("path", ""))
|
||||||
@@ -4246,12 +4247,15 @@ func _drain_m2_animation_loads() -> void:
|
|||||||
_repair_m2_animated_materials(normalized_rel, node as Node3D)
|
_repair_m2_animated_materials(normalized_rel, node as Node3D)
|
||||||
var players := _find_animation_players_recursive(node)
|
var players := _find_animation_players_recursive(node)
|
||||||
if not players.is_empty():
|
if not players.is_empty():
|
||||||
_m2_animated_scene_cache[normalized_rel] = node as Node3D
|
_m2_prototype_cache_state.adopt_animated_prototype(
|
||||||
|
normalized_rel,
|
||||||
|
node as Node3D
|
||||||
|
)
|
||||||
if debug_streaming:
|
if debug_streaming:
|
||||||
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
|
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
|
||||||
continue
|
continue
|
||||||
node.free()
|
node.free()
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
|
|
||||||
|
|
||||||
func _drain_m2_mesh_loads() -> void:
|
func _drain_m2_mesh_loads() -> void:
|
||||||
@@ -4260,7 +4264,7 @@ func _drain_m2_mesh_loads() -> void:
|
|||||||
var path: String = String(pending.get("path", ""))
|
var path: String = String(pending.get("path", ""))
|
||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
_m2_mesh_load_pipeline_state.discard_request(normalized_rel)
|
_m2_mesh_load_pipeline_state.discard_request(normalized_rel)
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
var status := ResourceLoader.load_threaded_get_status(path)
|
||||||
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
||||||
@@ -4277,7 +4281,7 @@ func _drain_m2_mesh_loads() -> void:
|
|||||||
):
|
):
|
||||||
continue
|
continue
|
||||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
var path := String(pending.get("path", ""))
|
var path := String(pending.get("path", ""))
|
||||||
@@ -4289,7 +4293,7 @@ func _drain_m2_mesh_loads() -> void:
|
|||||||
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
|
|
||||||
|
|
||||||
func _process_m2_build_jobs() -> void:
|
func _process_m2_build_jobs() -> void:
|
||||||
@@ -4357,7 +4361,7 @@ func _process_m2_build_jobs() -> void:
|
|||||||
else:
|
else:
|
||||||
var mesh := _get_m2_mesh_or_request(rel_path)
|
var mesh := _get_m2_mesh_or_request(rel_path)
|
||||||
if mesh == null:
|
if mesh == null:
|
||||||
if not _m2_missing_cache.has(normalized_rel):
|
if not _m2_prototype_cache_state.is_model_missing(normalized_rel):
|
||||||
_m2_build_queue.pop_front()
|
_m2_build_queue.pop_front()
|
||||||
_m2_build_queue.append(key)
|
_m2_build_queue.append(key)
|
||||||
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
|
_render_budget_scheduler.try_consume_permit(RENDER_BUDGET_SCHEDULER_SCRIPT.M2_BUILD)
|
||||||
@@ -4624,7 +4628,7 @@ func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
|
|||||||
return null
|
return null
|
||||||
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
|
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
|
||||||
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
|
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
|
||||||
if _m2_missing_cache.has(normalized_rel):
|
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
|
||||||
return null
|
return null
|
||||||
_request_m2_mesh_load(normalized_rel)
|
_request_m2_mesh_load(normalized_rel)
|
||||||
return null
|
return null
|
||||||
@@ -4649,7 +4653,7 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
|
|||||||
return
|
return
|
||||||
break
|
break
|
||||||
|
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
|
|
||||||
|
|
||||||
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||||
@@ -4840,11 +4844,11 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
|||||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||||
if normalized_rel.is_empty():
|
if normalized_rel.is_empty():
|
||||||
return null
|
return null
|
||||||
if _m2_scene_cache.has(normalized_rel):
|
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
|
||||||
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
if cached != null:
|
||||||
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||||
return cached
|
return cached
|
||||||
if _m2_missing_cache.has(normalized_rel):
|
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
|
||||||
return null
|
return null
|
||||||
|
|
||||||
# Try pre-baked cache (.tscn first, then legacy .glb)
|
# Try pre-baked cache (.tscn first, then legacy .glb)
|
||||||
@@ -4858,8 +4862,10 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
|||||||
var node = (resource as PackedScene).instantiate()
|
var node = (resource as PackedScene).instantiate()
|
||||||
if node is Node3D:
|
if node is Node3D:
|
||||||
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||||
return node as Node3D
|
normalized_rel,
|
||||||
|
node as Node3D
|
||||||
|
)
|
||||||
break
|
break
|
||||||
|
|
||||||
# Fall back to the raw native M2 repository.
|
# Fall back to the raw native M2 repository.
|
||||||
@@ -4868,25 +4874,30 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
|||||||
normalized_rel
|
normalized_rel
|
||||||
)
|
)
|
||||||
if data.is_empty():
|
if data.is_empty():
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||||
if prototype == null:
|
if prototype == null:
|
||||||
_m2_missing_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
_m2_scene_cache[normalized_rel] = prototype
|
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||||
return prototype
|
normalized_rel,
|
||||||
|
prototype
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
||||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||||
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
|
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
|
||||||
return null
|
return null
|
||||||
if _m2_animated_scene_cache.has(normalized_rel):
|
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
|
||||||
return _m2_animated_scene_cache[normalized_rel]
|
normalized_rel
|
||||||
if _m2_static_animation_cache.has(normalized_rel):
|
)
|
||||||
|
if cached_animated != null:
|
||||||
|
return cached_animated
|
||||||
|
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
||||||
return null
|
return null
|
||||||
var data := _m2_raw_model_repository.load_animated_model_data(
|
var data := _m2_raw_model_repository.load_animated_model_data(
|
||||||
extracted_dir,
|
extracted_dir,
|
||||||
@@ -4894,15 +4905,18 @@ func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
|||||||
)
|
)
|
||||||
var animated_surfaces: Array = data.get("animated_surfaces", [])
|
var animated_surfaces: Array = data.get("animated_surfaces", [])
|
||||||
if data.is_empty() or animated_surfaces.is_empty():
|
if data.is_empty() or animated_surfaces.is_empty():
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
|
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||||
if prototype == null or prototype.get_child_count() <= 0:
|
if prototype == null or prototype.get_child_count() <= 0:
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
_m2_animated_scene_cache[normalized_rel] = prototype
|
prototype = _m2_prototype_cache_state.adopt_animated_prototype(
|
||||||
|
normalized_rel,
|
||||||
|
prototype
|
||||||
|
)
|
||||||
if debug_streaming:
|
if debug_streaming:
|
||||||
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
|
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
|
||||||
normalized_rel,
|
normalized_rel,
|
||||||
@@ -4924,9 +4938,12 @@ func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
|
|||||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||||
if normalized_rel.is_empty():
|
if normalized_rel.is_empty():
|
||||||
return null
|
return null
|
||||||
if _m2_animated_scene_cache.has(normalized_rel):
|
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
|
||||||
return _m2_animated_scene_cache[normalized_rel]
|
normalized_rel
|
||||||
if _m2_static_animation_cache.has(normalized_rel):
|
)
|
||||||
|
if cached_animated != null:
|
||||||
|
return cached_animated
|
||||||
|
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
||||||
return null
|
return null
|
||||||
if _m2_animation_load_requests.has(normalized_rel):
|
if _m2_animation_load_requests.has(normalized_rel):
|
||||||
return null
|
return null
|
||||||
@@ -4940,7 +4957,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
|
|||||||
return
|
return
|
||||||
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
|
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
|
||||||
if cache_res_path.is_empty():
|
if cache_res_path.is_empty():
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
return
|
return
|
||||||
var err := ResourceLoader.load_threaded_request(
|
var err := ResourceLoader.load_threaded_request(
|
||||||
cache_res_path,
|
cache_res_path,
|
||||||
@@ -4953,7 +4970,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
|
|||||||
"path": cache_res_path,
|
"path": cache_res_path,
|
||||||
}
|
}
|
||||||
else:
|
else:
|
||||||
_m2_static_animation_cache[normalized_rel] = true
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
|
|
||||||
|
|
||||||
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
|
func _find_m2_animated_glb_cache_path(normalized_rel: String) -> String:
|
||||||
@@ -5018,8 +5035,8 @@ func _repair_m2_animated_materials(normalized_rel: String, animated_root: Node3D
|
|||||||
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||||
if normalized_rel.is_empty():
|
if normalized_rel.is_empty():
|
||||||
return null
|
return null
|
||||||
if _m2_scene_cache.has(normalized_rel):
|
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
|
||||||
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
if cached != null:
|
||||||
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
@@ -5031,8 +5048,10 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
|||||||
var node = (resource as PackedScene).instantiate()
|
var node = (resource as PackedScene).instantiate()
|
||||||
if node is Node3D:
|
if node is Node3D:
|
||||||
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||||
return node as Node3D
|
normalized_rel,
|
||||||
|
node as Node3D
|
||||||
|
)
|
||||||
return null
|
return null
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -111,7 +111,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"ResourceLoader.load_threaded_get(path)",
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
||||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||||
"_m2_missing_cache[normalized_rel]",
|
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||||
]:
|
]:
|
||||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
for forbidden_dependency in [
|
for forbidden_dependency in [
|
||||||
@@ -122,7 +122,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"RID(",
|
"RID(",
|
||||||
"_m2_mesh_cache",
|
"_m2_mesh_cache",
|
||||||
"_m2_mesh_resource_cache_state",
|
"_m2_mesh_resource_cache_state",
|
||||||
"_m2_missing_cache",
|
"_m2_prototype_cache_state",
|
||||||
]:
|
]:
|
||||||
_expect_false(
|
_expect_false(
|
||||||
pipeline_source.contains(forbidden_dependency),
|
pipeline_source.contains(forbidden_dependency),
|
||||||
|
|||||||
@@ -96,8 +96,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"ResourceLoader.load_threaded_get(path)",
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
||||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||||
"_m2_missing_cache[normalized_rel]",
|
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||||
"_m2_scene_cache[normalized_rel]",
|
"_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.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
for forbidden_dependency in [
|
for forbidden_dependency in [
|
||||||
@@ -107,8 +107,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"PackedScene",
|
"PackedScene",
|
||||||
"Node3D",
|
"Node3D",
|
||||||
"M2Builder",
|
"M2Builder",
|
||||||
"_m2_missing_cache",
|
"_m2_prototype_cache_state",
|
||||||
"_m2_scene_cache",
|
|
||||||
]:
|
]:
|
||||||
_expect_false(
|
_expect_false(
|
||||||
cache_source.contains(forbidden_dependency),
|
cache_source.contains(forbidden_dependency),
|
||||||
|
|||||||
@@ -126,7 +126,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||||
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
|
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
|
||||||
"_m2_missing_cache[normalized_rel]",
|
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||||
]:
|
]:
|
||||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
for forbidden_dependency in [
|
for forbidden_dependency in [
|
||||||
@@ -135,7 +135,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"WorkerThreadPool.",
|
"WorkerThreadPool.",
|
||||||
"M2Builder",
|
"M2Builder",
|
||||||
"_m2_mesh_resource_cache_state",
|
"_m2_mesh_resource_cache_state",
|
||||||
"_m2_missing_cache",
|
"_m2_prototype_cache_state",
|
||||||
"MultiMesh",
|
"MultiMesh",
|
||||||
]:
|
]:
|
||||||
_expect_false(
|
_expect_false(
|
||||||
|
|||||||
@@ -0,0 +1,162 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic admission/identity/negative-state/release/source/timing contract.
|
||||||
|
|
||||||
|
const CACHE_STATE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd")
|
||||||
|
const CACHE_STATE_PATH := "res://src/render/m2/m2_prototype_cache_state.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_admission_identity_and_negative_state(failures)
|
||||||
|
_verify_release_ownership(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_PROTOTYPE_CACHE_STATE: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_PROTOTYPE_CACHE_STATE PASS cases=16 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_admission_identity_and_negative_state(failures: Array[String]) -> void:
|
||||||
|
var state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||||
|
_expect_same(state.call("find_static_prototype", "world/missing.m2"), null, "unknown static miss", failures)
|
||||||
|
_expect_false(state.call("mark_model_missing", ""), "empty missing path rejected", failures)
|
||||||
|
_expect_false(state.call("mark_animation_static", ""), "empty static-animation path rejected", failures)
|
||||||
|
var static_prototype := Node3D.new()
|
||||||
|
var animated_prototype := Node3D.new()
|
||||||
|
_expect_same(
|
||||||
|
state.call("adopt_static_prototype", "world/tree.m2", static_prototype),
|
||||||
|
static_prototype,
|
||||||
|
"static prototype adopted",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_same(
|
||||||
|
state.call("find_static_prototype", "world/tree.m2"),
|
||||||
|
static_prototype,
|
||||||
|
"static identity retained",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_same(
|
||||||
|
state.call("adopt_animated_prototype", "world/tree.m2", animated_prototype),
|
||||||
|
animated_prototype,
|
||||||
|
"animated prototype independent",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var duplicate_static := Node3D.new()
|
||||||
|
_expect_same(
|
||||||
|
state.call("adopt_static_prototype", "world/tree.m2", duplicate_static),
|
||||||
|
static_prototype,
|
||||||
|
"first static prototype wins",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(is_instance_valid(duplicate_static), "duplicate detached prototype released", failures)
|
||||||
|
_expect_true(state.call("mark_model_missing", "world/missing.m2"), "missing path marked", failures)
|
||||||
|
_expect_true(state.call("is_model_missing", "world/missing.m2"), "missing path retained", failures)
|
||||||
|
_expect_true(state.call("mark_animation_static", "world/rock.m2"), "static animation marked", failures)
|
||||||
|
_expect_true(state.call("is_animation_static", "world/rock.m2"), "static animation retained", failures)
|
||||||
|
var snapshot: Dictionary = state.call("diagnostic_snapshot")
|
||||||
|
_expect_equal((snapshot["static_prototype_paths"] as Array).size(), 1, "one static path", failures)
|
||||||
|
_expect_equal((snapshot["animated_prototype_paths"] as Array).size(), 1, "one animated path", failures)
|
||||||
|
state.call("clear_and_release")
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_release_ownership(failures: Array[String]) -> void:
|
||||||
|
var state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||||
|
var detached_static := Node3D.new()
|
||||||
|
var in_tree_animated := Node3D.new()
|
||||||
|
get_root().add_child(in_tree_animated)
|
||||||
|
state.call("adopt_static_prototype", "world/static.m2", detached_static)
|
||||||
|
state.call("adopt_animated_prototype", "world/animated.m2", in_tree_animated)
|
||||||
|
state.call("mark_model_missing", "world/missing.m2")
|
||||||
|
state.call("mark_animation_static", "world/static.m2")
|
||||||
|
state.call("clear_and_release")
|
||||||
|
state.call("clear_and_release")
|
||||||
|
_expect_false(is_instance_valid(detached_static), "detached prototype freed", failures)
|
||||||
|
_expect_true(
|
||||||
|
not is_instance_valid(in_tree_animated) or in_tree_animated.is_queued_for_deletion(),
|
||||||
|
"in-tree prototype queued or released by headless teardown",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var snapshot: Dictionary = state.call("diagnostic_snapshot")
|
||||||
|
for state_key in snapshot.keys():
|
||||||
|
_expect_true((snapshot[state_key] as Array).is_empty(), "%s cleared" % state_key, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var state_source := FileAccess.get_file_as_string(CACHE_STATE_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()"),
|
||||||
|
"loader composes prototype cache state",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for removed_loader_field in [
|
||||||
|
"var _m2_scene_cache:",
|
||||||
|
"var _m2_animated_scene_cache:",
|
||||||
|
"var _m2_static_animation_cache:",
|
||||||
|
"var _m2_missing_cache:",
|
||||||
|
]:
|
||||||
|
_expect_false(loader_source.contains(removed_loader_field), "loader omits %s" % removed_loader_field, failures)
|
||||||
|
for required_loader_rule in [
|
||||||
|
"_m2_prototype_cache_state.find_static_prototype(",
|
||||||
|
"_m2_prototype_cache_state.adopt_static_prototype(",
|
||||||
|
"_m2_prototype_cache_state.find_animated_prototype(",
|
||||||
|
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||||
|
"_m2_prototype_cache_state.mark_model_missing(",
|
||||||
|
"_m2_prototype_cache_state.mark_animation_static(",
|
||||||
|
"_m2_prototype_cache_state.clear_and_release()",
|
||||||
|
]:
|
||||||
|
_expect_true(loader_source.contains(required_loader_rule), "loader delegates %s" % required_loader_rule, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"FileAccess.",
|
||||||
|
"ClassDB.",
|
||||||
|
"M2Builder",
|
||||||
|
"WorkerThreadPool",
|
||||||
|
"MultiMesh",
|
||||||
|
"MeshInstance3D",
|
||||||
|
]:
|
||||||
|
_expect_false(state_source.contains(forbidden_dependency), "state omits %s" % forbidden_dependency, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(100):
|
||||||
|
var state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||||
|
for path_index in range(256):
|
||||||
|
var normalized_path := "world/model_%d.m2" % path_index
|
||||||
|
state.call("mark_model_missing", normalized_path)
|
||||||
|
state.call("mark_animation_static", normalized_path)
|
||||||
|
state.call("is_model_missing", normalized_path)
|
||||||
|
state.call("is_animation_static", normalized_path)
|
||||||
|
state.call("clear_and_release")
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 path-state cycles under 1 second", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
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_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_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_same(actual, expected):
|
||||||
|
failures.append(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dkjneibuiqjss
|
||||||
@@ -96,7 +96,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
for forbidden_dependency in [
|
for forbidden_dependency in [
|
||||||
"ResourceLoader.",
|
"ResourceLoader.",
|
||||||
"M2_BUILDER",
|
"M2_BUILDER",
|
||||||
"_m2_missing_cache",
|
"_m2_prototype_cache_state",
|
||||||
"Mesh",
|
"Mesh",
|
||||||
"Node",
|
"Node",
|
||||||
"WorkerThreadPool",
|
"WorkerThreadPool",
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"M2_RAW_MODEL_REPOSITORY_SCRIPT.new()",
|
"M2_RAW_MODEL_REPOSITORY_SCRIPT.new()",
|
||||||
"_m2_raw_model_repository.load_static_model_data(",
|
"_m2_raw_model_repository.load_static_model_data(",
|
||||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||||
"_m2_missing_cache[normalized_rel]",
|
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||||
]:
|
]:
|
||||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
for raw_repository_rule in [
|
for raw_repository_rule in [
|
||||||
@@ -152,7 +152,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
|||||||
"ClassDB.",
|
"ClassDB.",
|
||||||
"load_m2",
|
"load_m2",
|
||||||
"_m2_mesh_resource_cache_state",
|
"_m2_mesh_resource_cache_state",
|
||||||
"_m2_missing_cache",
|
"_m2_prototype_cache_state",
|
||||||
"MultiMesh",
|
"MultiMesh",
|
||||||
]:
|
]:
|
||||||
_expect_false(
|
_expect_false(
|
||||||
|
|||||||
@@ -32,13 +32,14 @@ func _verify_runtime_cache_ownership() -> void:
|
|||||||
var animated_m2_prototype := Node3D.new()
|
var animated_m2_prototype := Node3D.new()
|
||||||
var wmo_prototype := Node3D.new()
|
var wmo_prototype := Node3D.new()
|
||||||
|
|
||||||
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
var m2_prototype_cache_state: RefCounted = loader.get("_m2_prototype_cache_state")
|
||||||
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
m2_prototype_cache_state.call("adopt_static_prototype", "static", static_m2_prototype)
|
||||||
|
m2_prototype_cache_state.call("adopt_animated_prototype", "animated", animated_m2_prototype)
|
||||||
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
||||||
var m2_mesh_resource_cache_state: RefCounted = loader.get("_m2_mesh_resource_cache_state")
|
var m2_mesh_resource_cache_state: RefCounted = loader.get("_m2_mesh_resource_cache_state")
|
||||||
m2_mesh_resource_cache_state.call("store_mesh", "mesh", ArrayMesh.new())
|
m2_mesh_resource_cache_state.call("store_mesh", "mesh", ArrayMesh.new())
|
||||||
loader.set("_m2_static_animation_cache", {"static": true})
|
m2_prototype_cache_state.call("mark_animation_static", "static")
|
||||||
loader.set("_m2_missing_cache", {"missing": true})
|
m2_prototype_cache_state.call("mark_model_missing", "missing")
|
||||||
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
|
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
|
||||||
wmo_render_cache_state.call("remember_request", "render", "res://render.res")
|
wmo_render_cache_state.call("remember_request", "render", "res://render.res")
|
||||||
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
|
wmo_render_cache_state.call("complete_request_with_resource", "render", Resource.new())
|
||||||
@@ -56,12 +57,16 @@ func _verify_runtime_cache_ownership() -> void:
|
|||||||
_expect(not is_instance_valid(static_m2_prototype), "static M2 prototype should be freed")
|
_expect(not is_instance_valid(static_m2_prototype), "static M2 prototype should be freed")
|
||||||
_expect(not is_instance_valid(animated_m2_prototype), "animated M2 prototype should be freed")
|
_expect(not is_instance_valid(animated_m2_prototype), "animated M2 prototype should be freed")
|
||||||
_expect(not is_instance_valid(wmo_prototype), "WMO prototype should be freed")
|
_expect(not is_instance_valid(wmo_prototype), "WMO prototype should be freed")
|
||||||
|
var m2_prototype_snapshot: Dictionary = m2_prototype_cache_state.call(
|
||||||
|
"diagnostic_snapshot"
|
||||||
|
)
|
||||||
|
for state_key in m2_prototype_snapshot.keys():
|
||||||
|
_expect(
|
||||||
|
(m2_prototype_snapshot[state_key] as Array).is_empty(),
|
||||||
|
"M2 prototype %s should be empty after shutdown" % state_key
|
||||||
|
)
|
||||||
for cache_name in [
|
for cache_name in [
|
||||||
"_m2_scene_cache",
|
|
||||||
"_m2_animated_scene_cache",
|
|
||||||
"_wmo_prototype_cache",
|
"_wmo_prototype_cache",
|
||||||
"_m2_static_animation_cache",
|
|
||||||
"_m2_missing_cache",
|
|
||||||
"_wmo_missing_cache",
|
"_wmo_missing_cache",
|
||||||
"_shared_tex_cache",
|
"_shared_tex_cache",
|
||||||
]:
|
]:
|
||||||
|
|||||||
Reference in New Issue
Block a user