merge M03 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_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
|
||||
- `src/render/m2/m2_runtime_mesh_finalizer.gd` - refresh version, classifier lifetime, M2Builder rebuild and original-Mesh fallback.
|
||||
- `src/render/m2/m2_prototype_cache_state.gd` - shutdown-lifetime static/animated prototype Nodes and missing/static-only path outcomes.
|
||||
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||
@@ -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
|
||||
runtime-cache shutdown site after asynchronous work is drained.
|
||||
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
|
||||
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, shared missing
|
||||
and prototype/animated caches, permits and MultiMesh materialization.
|
||||
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, cache adoption
|
||||
decisions, permits and MultiMesh materialization. Prototype/negative state is
|
||||
now a sibling cache service.
|
||||
- Cache formats, refresh metadata, profiles and visible output are unchanged.
|
||||
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
|
||||
|
||||
@@ -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
|
||||
when the finalizer reports a stale Mesh; both historical clear sites persist.
|
||||
- Billboard/UV-rotation predicates, rebuild extraction, metadata key and failure
|
||||
fallback are unchanged. Cache adoption, missing state, permits and MultiMesh
|
||||
materialization remain loader-owned.
|
||||
fallback are unchanged. Cache adoption decisions, permits and MultiMesh
|
||||
materialization remain loader-owned; negative outcomes belong to prototype state.
|
||||
- Synthetic triangle rebuild/fallback timing is not asset-backed material,
|
||||
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.
|
||||
- `StreamingWorldLoader` delegates the stale-Mesh refresh, static prototype and
|
||||
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,
|
||||
absent files/classes/methods and invalid results produce the same empty-value
|
||||
fallback contract; path join/globalization and native method names are exact.
|
||||
- No cache format, parser, material, geometry, animation or visible rule changed.
|
||||
Synthetic missing-input timing is not asset-backed I/O/p95/p99 or fidelity evidence.
|
||||
|
||||
## 2026-07-17 M2 Prototype Cache State
|
||||
|
||||
- `M2PrototypeCacheState` now owns the exact static and animated prototype Node
|
||||
references plus missing-model and static-animation negative path outcomes.
|
||||
- Sync static, native animated, threaded GLB animation, Mesh finalize and
|
||||
material-prototype paths delegate lookup/adoption/negative transitions to the
|
||||
service. First valid prototype remains canonical for each normalized path.
|
||||
- Map refresh still retains all four states. Final shutdown still occurs after
|
||||
asynchronous drain and now calls one idempotent release operation that frees
|
||||
detached prototypes, queues defensive in-tree Nodes and clears path outcomes.
|
||||
- `StreamingWorldLoader` retains normalization, ResourceLoader/native I/O,
|
||||
builders, animation policy, fallback order, permits and materialization.
|
||||
- No cache format, visible rule or profile changed. Synthetic ownership/timing
|
||||
evidence is not an asset-backed traversal/leak/p95/p99 or fidelity claim.
|
||||
|
||||
## 2026-07-17 WMO Placement Resolver Extraction
|
||||
|
||||
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
||||
|
||||
@@ -78,14 +78,24 @@ shutdown-lifetime cache-state service.
|
||||
|
||||
## Status
|
||||
|
||||
- State: claimed
|
||||
- Done: four cache tables and all caller/lifecycle sites identified
|
||||
- Next: implement state service, migrate adapters, verify and document
|
||||
- State: ready
|
||||
- Done: cache state, loader/test adapters, lifecycle verifier and required documentation
|
||||
- Next: integrate and run post-merge acceptance
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit:
|
||||
- Results:
|
||||
- Remaining risks:
|
||||
- Documentation updated:
|
||||
- Commit: `cd664eb`
|
||||
- Results: prototype state passes 16 admission/identity/negative/lifecycle/source
|
||||
cases and 100-by-256 cycles in 50.266 ms; 46 autonomous headless verifiers
|
||||
pass, while the proprietary ADT placement probe is unavailable without
|
||||
`data/extracted`; checkpoint dry-run passes 7/7; documentation and
|
||||
coordination gates pass; loader-private inventory decreases from 34 to 31.
|
||||
- Remaining risks: strong prototypes remain unbounded until shutdown; successful
|
||||
asset traversal, descriptor pressure, leaks and p95/p99 require proprietary
|
||||
data; no original-client visual comparison is claimed.
|
||||
- Documentation updated: inline API; `m2-prototype-cache-state.md` with data-flow,
|
||||
lifecycle, sequence and dependency diagrams; pipeline/Mesh-cache/extractor/raw
|
||||
repository/world-renderer specs, module registry and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PROTOTYPE-CACHE-001:cd664eb -->
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
| 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 raw model repository | Implemented extraction | [`m2-raw-model-repository.md`](m2-raw-model-repository.md) |
|
||||
| M2 prototype cache state | Implemented extraction | [`m2-prototype-cache-state.md`](m2-prototype-cache-state.md) |
|
||||
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
||||
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
||||
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
|
||||
|
||||
@@ -38,7 +38,7 @@ flowchart LR
|
||||
State --> FIFO[Finalize FIFO]
|
||||
FIFO --> Loader
|
||||
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.
|
||||
@@ -88,7 +88,7 @@ flowchart TD
|
||||
Permit -->|yes| Pop[Pop oldest terminal record]
|
||||
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
|
||||
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
|
||||
Prepare --> Adopt[Loader adopts Mesh/missing state]
|
||||
Prepare --> Adopt[Loader adopts Mesh or marks prototype outcome state]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
@@ -137,7 +137,7 @@ flowchart TB
|
||||
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
|
||||
Loader --> Resource[ResourceLoader]
|
||||
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]
|
||||
State -. no dependency .-> Resource
|
||||
State -. no dependency .-> Budget
|
||||
@@ -151,7 +151,8 @@ flowchart TB
|
||||
- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear.
|
||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and
|
||||
`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.
|
||||
|
||||
## 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_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||
| `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/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
|
||||
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
|
||||
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 -->|no| Load[Loader request or synchronous fallback]
|
||||
Load --> Prepare[Extractor and runtime finalizer prepare Mesh]
|
||||
Prepare --> Valid{Prepared Mesh valid?}
|
||||
Valid -->|yes| Store[Store or replace cache entry]
|
||||
Valid -->|no| MarkMissing[Loader updates shared missing state]
|
||||
Valid -->|no| MarkMissing[Loader updates prototype outcome state]
|
||||
Store --> Return
|
||||
```
|
||||
|
||||
@@ -131,7 +131,8 @@ flowchart TB
|
||||
- The cache owns normalized-path Strings and strong Mesh references.
|
||||
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
||||
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
||||
instances. 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.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
@@ -190,8 +191,7 @@ the historical Mesh cache had no queue contribution or log site.
|
||||
## Known gaps and risks
|
||||
|
||||
- Strong Mesh references intentionally remain until final shutdown.
|
||||
- Shared missing state cannot move here without also redesigning prototype and
|
||||
animated lookup behavior.
|
||||
- Missing/prototype/animated lookup state is now a separate sibling service.
|
||||
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
|
||||
original-client capture is included.
|
||||
|
||||
@@ -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_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_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 |
|
||||
|
||||
@@ -194,7 +194,8 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
|
||||
| `src/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_mesh_resource_cache_state.gd` | Prepared Mesh retention |
|
||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | Resource/order/lifetime/boundary/timing regression |
|
||||
|
||||
@@ -0,0 +1,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 |
|
||||
| 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 | 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
|
||||
parsing and creation of one temporary native loader instance per accepted call.
|
||||
@@ -133,7 +133,8 @@ flowchart TB
|
||||
## Ownership, threading and resources
|
||||
|
||||
- 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.
|
||||
- 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.
|
||||
@@ -145,7 +146,7 @@ flowchart TB
|
||||
|---|---|---|---|---|
|
||||
| Empty normalized path | String guard | Return `{}` | Dedicated verifier | Correct caller normalization |
|
||||
| Native extension absent | `ClassDB.class_exists` | Return `{}` | Existing renderer fallback | Install/load extension and retry |
|
||||
| Extracted file absent | `FileAccess.file_exists` | Return `{}` | 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 |
|
||||
| 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 |
|
||||
@@ -210,7 +211,8 @@ measured work packages rather than expansion of this repository.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
|
||||
| `src/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/tools/verify_m2_raw_model_repository.gd` | Rejected-input/source/dependency/timing regression |
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-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 |
|
||||
|
||||
## 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 |
|
||||
| `M2RuntimeMeshFinalizer` | Internal M2 preparation service | Owns refresh version, rebuild classification, M2Builder rebuild and fallback | Renderer main thread; decisions cached until reset | Null returns null; missing/failed rebuild marks and reuses original Mesh |
|
||||
| `M2RawModelRepository` | Internal M2 native repository | Reads static/animated raw Dictionaries through exact M2Loader methods | Synchronous; stateless | Invalid/unavailable/non-Dictionary result returns empty Dictionary |
|
||||
| `M2PrototypeCacheState` | Internal M2 prototype cache | Owns detached static/animated Nodes and missing/static-only outcomes | Renderer main thread; final shutdown | Invalid admission rejected; first valid prototype wins |
|
||||
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
||||
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
||||
@@ -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 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 M2 prototype state | Normalized path, adopted static/animated Node or negative outcome | Loader / `M2PrototypeCacheState` | Loader reuse/fallback adapters | State-owned strong Node refs and Strings; detached diagnostics | Until final shutdown |
|
||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||
@@ -377,10 +379,11 @@ sequenceDiagram
|
||||
finalizer. The raw repository loads value data; loader retains Mesh adoption.
|
||||
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
|
||||
terminal statuses and completion-order finalize FIFO. The 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
|
||||
them at the existing final-shutdown site. The loader retains missing/prototype
|
||||
state and materialization.
|
||||
them at the existing final-shutdown site. Prototype state and materialization
|
||||
belong to the sibling cache service and loader respectively.
|
||||
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
|
||||
PackedScene instance destruction. The loader retains ResourceLoader I/O,
|
||||
cache/missing adoption and materialization.
|
||||
@@ -388,8 +391,12 @@ sequenceDiagram
|
||||
M2Builder rebuild and original-Mesh fallback. The loader loads raw data only
|
||||
after the finalizer reports that a cached Mesh is stale.
|
||||
- `M2RawModelRepository` owns FileAccess/ClassDB availability and the exact
|
||||
`load_m2`/`load_m2_animated` calls. The loader retains normalization,
|
||||
fallback order, missing/prototype state and every result consumer.
|
||||
`load_m2`/`load_m2_animated` calls. The loader retains normalization, fallback
|
||||
order and every result consumer; `M2PrototypeCacheState` retains outcomes.
|
||||
- `M2PrototypeCacheState` owns exact static/animated prototype references,
|
||||
missing-model and static-animation path sets, duplicate-candidate release and
|
||||
final-shutdown Node cleanup. ResourceLoader, builders and fallback decisions
|
||||
remain loader-owned.
|
||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||
the facade never returns Mesh, Node, tile-state or queue references.
|
||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||
@@ -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 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 prototype cache state | Implemented extraction | Synthetic identity/negative/lifecycle/source/timing plus shutdown contract | Asset-backed traversal/leak/p95/p99 pending |
|
||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
||||
@@ -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_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_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_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||
| `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_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_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_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||
|
||||
@@ -0,0 +1,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(
|
||||
"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(
|
||||
"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_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_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_extractor := M2_MESH_RESOURCE_EXTRACTOR_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_missing_cache: Dictionary = {}
|
||||
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 _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_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
|
||||
## caches; only scene shutdown destroys them.
|
||||
func _release_runtime_caches_for_shutdown() -> void:
|
||||
_free_detached_node_cache(_m2_scene_cache)
|
||||
_free_detached_node_cache(_m2_animated_scene_cache)
|
||||
_m2_prototype_cache_state.clear_and_release()
|
||||
_free_detached_node_cache(_wmo_prototype_cache)
|
||||
_m2_mesh_resource_cache_state.clear()
|
||||
_m2_static_animation_cache.clear()
|
||||
_m2_missing_cache.clear()
|
||||
_wmo_render_resource_cache_state.clear_all()
|
||||
_wmo_scene_resource_cache_state.clear_all()
|
||||
_wmo_missing_cache.clear()
|
||||
@@ -4219,7 +4216,7 @@ func _drain_m2_animation_loads() -> void:
|
||||
var path := String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
_m2_animation_load_requests.erase(normalized_rel)
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
continue
|
||||
var status := ResourceLoader.load_threaded_get_status(path)
|
||||
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):
|
||||
var pending: Dictionary = _m2_animation_finalize_queue.pop_front()
|
||||
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
|
||||
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
|
||||
|
||||
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)
|
||||
var players := _find_animation_players_recursive(node)
|
||||
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:
|
||||
print("M2_ANIM_CACHE path=%s cache=%s players=%d" % [normalized_rel, path, players.size()])
|
||||
continue
|
||||
node.free()
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
|
||||
|
||||
func _drain_m2_mesh_loads() -> void:
|
||||
@@ -4260,7 +4264,7 @@ func _drain_m2_mesh_loads() -> void:
|
||||
var path: String = String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
_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
|
||||
var status := ResourceLoader.load_threaded_get_status(path)
|
||||
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
||||
@@ -4277,7 +4281,7 @@ func _drain_m2_mesh_loads() -> void:
|
||||
):
|
||||
continue
|
||||
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
|
||||
|
||||
var path := String(pending.get("path", ""))
|
||||
@@ -4289,7 +4293,7 @@ func _drain_m2_mesh_loads() -> void:
|
||||
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
)
|
||||
else:
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||
|
||||
|
||||
func _process_m2_build_jobs() -> void:
|
||||
@@ -4357,7 +4361,7 @@ func _process_m2_build_jobs() -> void:
|
||||
else:
|
||||
var mesh := _get_m2_mesh_or_request(rel_path)
|
||||
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.append(key)
|
||||
_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
|
||||
if _m2_mesh_resource_cache_state.has_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
|
||||
_request_m2_mesh_load(normalized_rel)
|
||||
return null
|
||||
@@ -4649,7 +4653,7 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
|
||||
return
|
||||
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:
|
||||
@@ -4840,11 +4844,11 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
if _m2_scene_cache.has(normalized_rel):
|
||||
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
||||
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
|
||||
if cached != null:
|
||||
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||
return cached
|
||||
if _m2_missing_cache.has(normalized_rel):
|
||||
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
|
||||
return null
|
||||
|
||||
# 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()
|
||||
if node is Node3D:
|
||||
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
||||
return node as Node3D
|
||||
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||
normalized_rel,
|
||||
node as Node3D
|
||||
)
|
||||
break
|
||||
|
||||
# Fall back to the raw native M2 repository.
|
||||
@@ -4868,25 +4874,30 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
||||
normalized_rel
|
||||
)
|
||||
if data.is_empty():
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||
return null
|
||||
|
||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||
if prototype == null:
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
|
||||
return null
|
||||
|
||||
_m2_scene_cache[normalized_rel] = prototype
|
||||
return prototype
|
||||
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||
normalized_rel,
|
||||
prototype
|
||||
)
|
||||
|
||||
|
||||
func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if normalized_rel.is_empty() or not _is_m2_native_animation_candidate(normalized_rel):
|
||||
return null
|
||||
if _m2_animated_scene_cache.has(normalized_rel):
|
||||
return _m2_animated_scene_cache[normalized_rel]
|
||||
if _m2_static_animation_cache.has(normalized_rel):
|
||||
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
|
||||
normalized_rel
|
||||
)
|
||||
if cached_animated != null:
|
||||
return cached_animated
|
||||
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
||||
return null
|
||||
var data := _m2_raw_model_repository.load_animated_model_data(
|
||||
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", [])
|
||||
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
|
||||
|
||||
var prototype: Node3D = M2_NATIVE_ANIMATED_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||
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
|
||||
|
||||
_m2_animated_scene_cache[normalized_rel] = prototype
|
||||
prototype = _m2_prototype_cache_state.adopt_animated_prototype(
|
||||
normalized_rel,
|
||||
prototype
|
||||
)
|
||||
if debug_streaming:
|
||||
print("M2_NATIVE_ANIM_CACHE path=%s surfaces=%d bones=%d anim_id=%d seq=%d score=%d length=%.2f" % [
|
||||
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)
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
if _m2_animated_scene_cache.has(normalized_rel):
|
||||
return _m2_animated_scene_cache[normalized_rel]
|
||||
if _m2_static_animation_cache.has(normalized_rel):
|
||||
var cached_animated := _m2_prototype_cache_state.find_animated_prototype(
|
||||
normalized_rel
|
||||
)
|
||||
if cached_animated != null:
|
||||
return cached_animated
|
||||
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
||||
return null
|
||||
if _m2_animation_load_requests.has(normalized_rel):
|
||||
return null
|
||||
@@ -4940,7 +4957,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
|
||||
return
|
||||
var cache_res_path := _find_m2_animated_glb_cache_path(normalized_rel)
|
||||
if cache_res_path.is_empty():
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||
return
|
||||
var err := ResourceLoader.load_threaded_request(
|
||||
cache_res_path,
|
||||
@@ -4953,7 +4970,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
|
||||
"path": cache_res_path,
|
||||
}
|
||||
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:
|
||||
@@ -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:
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
if _m2_scene_cache.has(normalized_rel):
|
||||
var cached: Node3D = _m2_scene_cache[normalized_rel]
|
||||
var cached := _m2_prototype_cache_state.find_static_prototype(normalized_rel)
|
||||
if cached != null:
|
||||
_prepare_m2_node_for_runtime(normalized_rel, cached)
|
||||
return cached
|
||||
|
||||
@@ -5031,8 +5048,10 @@ func _get_or_load_m2_material_prototype(normalized_rel: String) -> Node3D:
|
||||
var node = (resource as PackedScene).instantiate()
|
||||
if node is Node3D:
|
||||
_prepare_m2_node_for_runtime(normalized_rel, node as Node3D)
|
||||
_m2_scene_cache[normalized_rel] = node as Node3D
|
||||
return node as Node3D
|
||||
return _m2_prototype_cache_state.adopt_static_prototype(
|
||||
normalized_rel,
|
||||
node as Node3D
|
||||
)
|
||||
return null
|
||||
|
||||
|
||||
|
||||
@@ -111,7 +111,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
||||
"_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)
|
||||
for forbidden_dependency in [
|
||||
@@ -122,7 +122,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"RID(",
|
||||
"_m2_mesh_cache",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
"_m2_prototype_cache_state",
|
||||
]:
|
||||
_expect_false(
|
||||
pipeline_source.contains(forbidden_dependency),
|
||||
|
||||
@@ -96,8 +96,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||
"_m2_missing_cache[normalized_rel]",
|
||||
"_m2_scene_cache[normalized_rel]",
|
||||
"_m2_prototype_cache_state.mark_model_missing(normalized_rel)",
|
||||
"_m2_prototype_cache_state.adopt_static_prototype(",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
@@ -107,8 +107,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"PackedScene",
|
||||
"Node3D",
|
||||
"M2Builder",
|
||||
"_m2_missing_cache",
|
||||
"_m2_scene_cache",
|
||||
"_m2_prototype_cache_state",
|
||||
]:
|
||||
_expect_false(
|
||||
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)",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
"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)
|
||||
for forbidden_dependency in [
|
||||
@@ -135,7 +135,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"WorkerThreadPool.",
|
||||
"M2Builder",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
"_m2_prototype_cache_state",
|
||||
"MultiMesh",
|
||||
]:
|
||||
_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 [
|
||||
"ResourceLoader.",
|
||||
"M2_BUILDER",
|
||||
"_m2_missing_cache",
|
||||
"_m2_prototype_cache_state",
|
||||
"Mesh",
|
||||
"Node",
|
||||
"WorkerThreadPool",
|
||||
|
||||
@@ -129,7 +129,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"M2_RAW_MODEL_REPOSITORY_SCRIPT.new()",
|
||||
"_m2_raw_model_repository.load_static_model_data(",
|
||||
"_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)
|
||||
for raw_repository_rule in [
|
||||
@@ -152,7 +152,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"ClassDB.",
|
||||
"load_m2",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
"_m2_prototype_cache_state",
|
||||
"MultiMesh",
|
||||
]:
|
||||
_expect_false(
|
||||
|
||||
@@ -32,13 +32,14 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
var animated_m2_prototype := Node3D.new()
|
||||
var wmo_prototype := Node3D.new()
|
||||
|
||||
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
||||
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
||||
var m2_prototype_cache_state: RefCounted = loader.get("_m2_prototype_cache_state")
|
||||
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})
|
||||
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())
|
||||
loader.set("_m2_static_animation_cache", {"static": true})
|
||||
loader.set("_m2_missing_cache", {"missing": true})
|
||||
m2_prototype_cache_state.call("mark_animation_static", "static")
|
||||
m2_prototype_cache_state.call("mark_model_missing", "missing")
|
||||
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("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(animated_m2_prototype), "animated M2 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 [
|
||||
"_m2_scene_cache",
|
||||
"_m2_animated_scene_cache",
|
||||
"_wmo_prototype_cache",
|
||||
"_m2_static_animation_cache",
|
||||
"_m2_missing_cache",
|
||||
"_wmo_missing_cache",
|
||||
"_shared_tex_cache",
|
||||
]:
|
||||
|
||||
Reference in New Issue
Block a user