merge M03 M2 animation load pipeline
This commit is contained in:
@@ -36,6 +36,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
|||||||
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
|
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
|
||||||
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
||||||
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
|
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
|
||||||
|
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
|
||||||
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||||
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
- `src/render/m2/m2_mesh_resource_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.
|
||||||
@@ -1083,6 +1084,20 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Cache formats, material refresh, profiles and visible output are unchanged.
|
- Cache formats, material refresh, profiles and visible output are unchanged.
|
||||||
Synthetic state timing is not asset-backed I/O, leak or p95/p99 evidence.
|
Synthetic state timing is not asset-backed I/O, leak or p95/p99 evidence.
|
||||||
|
|
||||||
|
## 2026-07-17 M2 Animation Load Pipeline State Extraction
|
||||||
|
|
||||||
|
- `M2AnimationLoadPipelineState` now owns animated M2 successful threaded-load
|
||||||
|
request records and the FIFO populated at loaded/failed terminal transitions.
|
||||||
|
- Pending snapshots preserve insertion order, while finalization follows completion
|
||||||
|
order. All three historical `m2_animation` metrics still count both stages.
|
||||||
|
- Shutdown still drains pending ResourceLoader paths before clear; map reset and
|
||||||
|
shutdown preserve the same two clear sites.
|
||||||
|
- `StreamingWorldLoader` retains GLB eligibility/cache selection, every
|
||||||
|
ResourceLoader call, `M2_ANIMATION_FINALIZE` permits, scene instantiation,
|
||||||
|
material repair and prototype/static-fallback decisions.
|
||||||
|
- Cache formats, animation behavior and visible output are unchanged. Synthetic
|
||||||
|
timing is not asset-backed I/O, leak, animation-fidelity or p95/p99 evidence.
|
||||||
|
|
||||||
## 2026-07-17 M2 Mesh Resource Cache State Extraction
|
## 2026-07-17 M2 Mesh Resource Cache State Extraction
|
||||||
|
|
||||||
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
|
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
|
||||||
|
|||||||
@@ -79,14 +79,25 @@ finalize FIFO from `StreamingWorldLoader` into an explicit state service.
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
- State: claimed
|
- State: ready for integration
|
||||||
- Done: request/finalize fields, three metrics, two clear sites and shutdown drain identified
|
- Done: state, loader adapters, lifecycle verifier and required documentation
|
||||||
- Next: implement state, migrate adapters, verify and document
|
- Next: integrate on master and run post-merge acceptance gates
|
||||||
- Blocked by:
|
- Blocked by:
|
||||||
|
|
||||||
## Handoff
|
## Handoff
|
||||||
|
|
||||||
- Commit:
|
- Commit: `b8e0f11`
|
||||||
- Results:
|
- Results: animation pipeline passes 11 validation/dedupe/order/transition/FIFO/
|
||||||
- Remaining risks:
|
discard/snapshot/source cases and 100-by-256 cycles in 69.798 ms; all 47
|
||||||
- Documentation updated:
|
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 31 to 30.
|
||||||
|
- Remaining risks: successful animated-GLB traversal, descriptor pressure,
|
||||||
|
leaks, animation fidelity and p95/p99 require proprietary data; no paired
|
||||||
|
original-client visual comparison is claimed.
|
||||||
|
- Documentation updated: inline API; `m2-animation-load-pipeline-state.md` with
|
||||||
|
data-flow, lifecycle, sequence and dependency diagrams; prototype/world-renderer
|
||||||
|
specs, module registry and `RENDER.md`.
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-ANIMATION-LOAD-PIPELINE-001:b8e0f11 -->
|
||||||
|
|||||||
@@ -22,6 +22,7 @@
|
|||||||
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
||||||
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
||||||
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||||
|
| M2 animation load pipeline state | Implemented extraction | [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md) |
|
||||||
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
|
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
|
||||||
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
|
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
|
||||||
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
# M2 Animation Load Pipeline State
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-ANIMATION-LOAD-PIPELINE-001` |
|
||||||
|
| Owners | Animated M2 threaded request records and terminal finalize FIFO |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-load-pipeline`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing optional cached-GLB animated M2 path |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Own cross-frame bookkeeping between a successful animated M2 ResourceLoader
|
||||||
|
request, terminal polling and budgeted main-thread scene finalization. This is
|
||||||
|
an exact state extraction; animation eligibility, loading and Node lifecycle
|
||||||
|
remain in `StreamingWorldLoader` and `M2PrototypeCacheState`.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Select cache paths, interpret ResourceLoader statuses or perform I/O.
|
||||||
|
- Decide animated/static fallback or own prototype Nodes.
|
||||||
|
- Instantiate PackedScenes, repair materials or consume render permits.
|
||||||
|
- Merge the distinct animated and static-Mesh pipelines.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Loader[StreamingWorldLoader] --> IO[ResourceLoader]
|
||||||
|
Loader --> State[M2AnimationLoadPipelineState]
|
||||||
|
State -->|detached pending records| Loader
|
||||||
|
Loader -->|opaque terminal status| State
|
||||||
|
State -->|completion FIFO| Loader
|
||||||
|
Loader --> Budget[M2_ANIMATION_FINALIZE permit]
|
||||||
|
Loader --> Prototype[M2PrototypeCacheState]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are value containers, Strings and opaque integer statuses.
|
||||||
|
ResourceLoader, workers, scheduler, Node/Resource/Mesh ownership and other
|
||||||
|
renderer services are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `remember_request(normalized_relative_path, resource_path)` | Command/query | Insert one pending request | Renderer main thread; until transition/clear | Empty/duplicate false |
|
||||||
|
| `has_request(path)` | Query | Pending-request dedupe | Main thread | Empty/unknown false |
|
||||||
|
| `request_records_snapshot()` | Query | Detached pending records in insertion order | Main thread; caller-owned | None |
|
||||||
|
| `complete_request(path, terminal_status)` | Command/query | Move copied record into completion FIFO | Main thread after poll | Unknown false |
|
||||||
|
| `discard_request(path)` | Command/query | Remove without finalization | Main thread | Unknown false |
|
||||||
|
| `has_finalize_record()` / `pop_finalize_record()` | Query/command | Drain completion-order FIFO | Main-thread budget drain | Empty pop returns `{}` |
|
||||||
|
| `total_work_count()` | Query | Pending plus finalize metric | Main thread | None |
|
||||||
|
| `pending_request_count()` / `finalize_record_count()` | Query | Stage diagnostics | Main thread | None |
|
||||||
|
| `clear()` | Command | Drop bookkeeping after caller I/O drain/reset | Main thread | Idempotent; no I/O drain |
|
||||||
|
| `diagnostic_snapshot()` | Query | Detached paths/status records | Main thread | No Resources exposed |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Normalized M2 path and GLB Resource path | Loader request adapter | Pending map | Copied Strings | Until completion/discard/clear |
|
||||||
|
| Input | Opaque terminal status | Loader polling adapter | Finalize FIFO | Integer value | Until pop/clear |
|
||||||
|
| Output | Detached pending records | State | Loader poll/shutdown adapter | Caller-owned copies | One pass |
|
||||||
|
| Output | Oldest completion record | State | Loader finalizer | Transferred Dictionary | One finalize attempt |
|
||||||
|
| Output | Detached diagnostics | State | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
|
||||||
|
|
||||||
|
Side effects are limited to collection mutation and retaining String/integer values.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Start[Successful threaded request] --> Remember[Remember request]
|
||||||
|
Remember --> Poll[Loader polls detached snapshot]
|
||||||
|
Poll --> Terminal{Loaded or failed?}
|
||||||
|
Terminal -->|no| Poll
|
||||||
|
Terminal -->|yes| Complete[Complete with opaque status]
|
||||||
|
Complete --> FIFO[Finalize FIFO]
|
||||||
|
FIFO --> Permit{Permit available?}
|
||||||
|
Permit -->|no| FIFO
|
||||||
|
Permit -->|yes| Pop[Pop oldest record]
|
||||||
|
Pop --> Finalize[Loader loads/instantiates or marks static]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Absent
|
||||||
|
Absent --> Pending: remember
|
||||||
|
Pending --> TerminalQueued: complete
|
||||||
|
Pending --> Absent: discard or clear
|
||||||
|
TerminalQueued --> Absent: pop or clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant L as StreamingWorldLoader
|
||||||
|
participant R as ResourceLoader
|
||||||
|
participant S as M2AnimationLoadPipelineState
|
||||||
|
participant P as M2PrototypeCacheState
|
||||||
|
L->>R: load_threaded_request(GLB)
|
||||||
|
L->>S: remember_request(path, GLB)
|
||||||
|
loop frames
|
||||||
|
L->>S: request_records_snapshot()
|
||||||
|
L->>R: load_threaded_get_status(GLB)
|
||||||
|
end
|
||||||
|
L->>S: complete_request(path, status)
|
||||||
|
L->>S: pop_finalize_record() after permit
|
||||||
|
L->>R: load_threaded_get(GLB)
|
||||||
|
L->>P: adopt animated prototype or mark static
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Main thread serializes all mutation.
|
||||||
|
- State owns only request/finalize Dictionaries with copied paths and statuses.
|
||||||
|
- Loader drains pending ResourceLoader paths before orderly shutdown clear.
|
||||||
|
- Loader owns PackedScene instantiation and material repair; prototype state owns
|
||||||
|
accepted detached Node references and static-only outcomes.
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> State[M2AnimationLoadPipelineState]
|
||||||
|
Loader --> Resource[ResourceLoader]
|
||||||
|
Loader --> Budget[RenderBudgetScheduler]
|
||||||
|
Loader --> Prototype[M2PrototypeCacheState]
|
||||||
|
State -. no dependency .-> Resource
|
||||||
|
State -. no dependency .-> Budget
|
||||||
|
State -. no dependency .-> Prototype
|
||||||
|
```
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller or request later |
|
||||||
|
| Request start/cache miss | Loader | No insertion; mark static | Existing loader path | Cache correction/reload |
|
||||||
|
| Non-terminal status | Loader | Keep pending | Existing metric | Poll next frame |
|
||||||
|
| Failed terminal load | Popped status | Loader marks static | Existing behavior | Future map/session reload |
|
||||||
|
| Empty defensive path | Loader poll | Discard and mark static | Source contract | Correct producer |
|
||||||
|
| Shutdown | Loader drains pending paths | Clear state | Source/shutdown regressions | New loader starts empty |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `enable_m2_animated_instances` | `true` | Existing renderer profile | Yes | Enables caller request path |
|
||||||
|
| `m2_animation_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds caller FIFO drain |
|
||||||
|
| Animated allow/deny/primitive rules | Existing values | Existing renderer profile | Yes | Filter before state insertion |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
State is not serialized. Cache formats and prototype lifetimes are unchanged;
|
||||||
|
no rebake or migration is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- `total_work_count()` preserves all three historical `m2_animation` metrics.
|
||||||
|
- Snapshots expose only paths and opaque statuses, never Resources or Nodes.
|
||||||
|
- Normalized M2 path is the correlation key; existing loader logs are unchanged.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_animation_load_pipeline_state.gd` covers validation, dedupe,
|
||||||
|
insertion order, completion FIFO, opaque status, discard, detached snapshots,
|
||||||
|
source boundaries and 100-by-256 timing.
|
||||||
|
- Adjacent renderer, scheduler, prototype and shutdown regressions cover callers.
|
||||||
|
- Fidelity evidence is exact state/lifecycle extraction; no visual 3.3.5a parity
|
||||||
|
or proprietary asset-backed claim is made.
|
||||||
|
- Performance budget: 25,600 request/complete/pop transitions under one second.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
ResourceLoader polling or animated-scene finalization may later move behind
|
||||||
|
separate adapters without changing this value-only state contract.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Animated request/finalize state | Implemented extraction | Synthetic contract/source/timing verifier | Asset-backed traversal/leak/p95/p99 pending |
|
||||||
|
| ResourceLoader and GLB selection | Existing loader-owned | Adjacent renderer tests | I/O adapter extraction optional |
|
||||||
|
| Animated prototype outcomes | Implemented extraction | Prototype cache verifier | Asset-backed animation fidelity pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Dictionary records preserve the existing dynamic ResourceLoader boundary.
|
||||||
|
- `clear()` does not drain I/O; caller ordering remains mandatory.
|
||||||
|
- No private asset traversal, leak/descriptor-pressure or paired-client run is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Pending records, completion FIFO and metrics |
|
||||||
|
| `src/render/m2/m2_prototype_cache_state.gd` | Animated prototype/static-only outcomes |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Eligibility, I/O, permits, instantiation and adoption |
|
||||||
|
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-prototype-cache-state.md`](m2-prototype-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)
|
||||||
@@ -240,6 +240,7 @@ this service without moving ResourceLoader or builder ownership into it.
|
|||||||
## Related decisions and references
|
## Related decisions and references
|
||||||
|
|
||||||
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
|
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
|
||||||
|
- [`m2-animation-load-pipeline-state.md`](m2-animation-load-pipeline-state.md)
|
||||||
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.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)
|
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||||
- [`world-renderer.md`](world-renderer.md)
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
|||||||
@@ -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-prototype-cache`, 2026-07-17 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-load-pipeline`, 2026-07-17 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -133,6 +133,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
|
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
|
||||||
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
||||||
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
||||||
|
| `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||||
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||||
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
|
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
|
||||||
| `M2MeshResourceExtractor` | Internal M2 scene/resource service | Selects first Mesh from direct Resource, PackedScene or Node subtree | Renderer main thread; stateless except temporary instance | Invalid/no Mesh returns null; temporary PackedScene root freed |
|
| `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 |
|
||||||
@@ -183,6 +184,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 animated M2 load | Normalized path, cached GLB path and opaque terminal status | Loader / `M2AnimationLoadPipelineState` | Loader poll/finalize adapters | State-owned String/status records; detached pending snapshots | Request through terminal finalize/reset |
|
||||||
| 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 |
|
| 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 |
|
||||||
@@ -538,6 +540,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 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending |
|
||||||
| M2 prototype cache state | Implemented extraction | Synthetic identity/negative/lifecycle/source/timing plus shutdown contract | Asset-backed traversal/leak/p95/p99 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 |
|
||||||
@@ -599,6 +602,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/render/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox |
|
| `src/render/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox |
|
||||||
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
|
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
|
||||||
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
|
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
|
||||||
|
| `src/render/m2/m2_animation_load_pipeline_state.gd` | Animated M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
||||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||||
@@ -620,6 +624,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/verify_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression |
|
| `src/tools/verify_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression |
|
||||||
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
|
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
|
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_animation_load_pipeline_state.gd` | Animated M2 request/terminal/FIFO/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/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_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||||
|
|||||||
@@ -0,0 +1,102 @@
|
|||||||
|
class_name M2AnimationLoadPipelineState
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns animated M2 threaded-load request records and the terminal finalize FIFO.
|
||||||
|
## The caller owns ResourceLoader I/O, permits, Nodes and prototype outcomes.
|
||||||
|
|
||||||
|
var _request_by_normalized_path: Dictionary = {}
|
||||||
|
var _finalize_records: Array[Dictionary] = []
|
||||||
|
|
||||||
|
|
||||||
|
## Records one normalized M2 path and ResourceLoader path. Empty or duplicate
|
||||||
|
## values are rejected without mutation.
|
||||||
|
func remember_request(normalized_relative_path: String, resource_path: String) -> bool:
|
||||||
|
if normalized_relative_path.is_empty() or resource_path.is_empty():
|
||||||
|
return false
|
||||||
|
if _request_by_normalized_path.has(normalized_relative_path):
|
||||||
|
return false
|
||||||
|
_request_by_normalized_path[normalized_relative_path] = {
|
||||||
|
"normalized": normalized_relative_path,
|
||||||
|
"path": resource_path,
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether one normalized path currently has a pending threaded request.
|
||||||
|
func has_request(normalized_relative_path: String) -> bool:
|
||||||
|
return (
|
||||||
|
not normalized_relative_path.is_empty()
|
||||||
|
and _request_by_normalized_path.has(normalized_relative_path)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached pending records in their existing Dictionary insertion order.
|
||||||
|
func request_records_snapshot() -> Array[Dictionary]:
|
||||||
|
var records: Array[Dictionary] = []
|
||||||
|
for request_variant in _request_by_normalized_path.values():
|
||||||
|
var request: Dictionary = request_variant
|
||||||
|
records.append(request.duplicate())
|
||||||
|
return records
|
||||||
|
|
||||||
|
|
||||||
|
## Removes one pending request and appends its terminal status to the finalize
|
||||||
|
## FIFO. Unknown paths are rejected without mutation.
|
||||||
|
func complete_request(normalized_relative_path: String, terminal_status: int) -> bool:
|
||||||
|
if not _request_by_normalized_path.has(normalized_relative_path):
|
||||||
|
return false
|
||||||
|
var request: Dictionary = _request_by_normalized_path[normalized_relative_path]
|
||||||
|
_request_by_normalized_path.erase(normalized_relative_path)
|
||||||
|
var finalize_record := request.duplicate()
|
||||||
|
finalize_record["status"] = terminal_status
|
||||||
|
_finalize_records.append(finalize_record)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Removes one pending request without enqueuing finalization.
|
||||||
|
func discard_request(normalized_relative_path: String) -> bool:
|
||||||
|
return _request_by_normalized_path.erase(normalized_relative_path)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether a terminal record is waiting for budgeted finalization.
|
||||||
|
func has_finalize_record() -> bool:
|
||||||
|
return not _finalize_records.is_empty()
|
||||||
|
|
||||||
|
|
||||||
|
## Removes and returns the oldest terminal record. Empty means none is ready.
|
||||||
|
func pop_finalize_record() -> Dictionary:
|
||||||
|
if _finalize_records.is_empty():
|
||||||
|
return {}
|
||||||
|
return _finalize_records.pop_front()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns pending plus terminal-finalize work for existing renderer metrics.
|
||||||
|
func total_work_count() -> int:
|
||||||
|
return _request_by_normalized_path.size() + _finalize_records.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the pending request count.
|
||||||
|
func pending_request_count() -> int:
|
||||||
|
return _request_by_normalized_path.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the terminal finalize FIFO count.
|
||||||
|
func finalize_record_count() -> int:
|
||||||
|
return _finalize_records.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Clears request/finalize bookkeeping. The caller must drain ResourceLoader
|
||||||
|
## requests before orderly shutdown clear.
|
||||||
|
func clear() -> void:
|
||||||
|
_request_by_normalized_path.clear()
|
||||||
|
_finalize_records.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached request and finalize records without loaded Resources/Nodes.
|
||||||
|
func diagnostic_snapshot() -> Dictionary:
|
||||||
|
var finalize_records: Array[Dictionary] = []
|
||||||
|
for record in _finalize_records:
|
||||||
|
finalize_records.append(record.duplicate())
|
||||||
|
return {
|
||||||
|
"requests": request_records_snapshot(),
|
||||||
|
"finalize_records": finalize_records,
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://h5rk6njgaujd
|
||||||
@@ -72,6 +72,9 @@ const M2_PROTOTYPE_CACHE_STATE_SCRIPT := preload(
|
|||||||
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"
|
||||||
)
|
)
|
||||||
|
const M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||||
|
"res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||||
|
)
|
||||||
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||||
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||||
)
|
)
|
||||||
@@ -270,8 +273,7 @@ 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()
|
||||||
var _m2_animation_load_requests: Dictionary = {}
|
var _m2_animation_load_pipeline_state := M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||||
var _m2_animation_finalize_queue: Array = []
|
|
||||||
var _wmo_build_jobs: Dictionary = {}
|
var _wmo_build_jobs: Dictionary = {}
|
||||||
var _wmo_build_queue: Array = []
|
var _wmo_build_queue: Array = []
|
||||||
var _tile_loading_tasks: Dictionary = {}
|
var _tile_loading_tasks: Dictionary = {}
|
||||||
@@ -1017,7 +1019,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
|
|||||||
_adt_water_load_pipeline_state.total_load_count(),
|
_adt_water_load_pipeline_state.total_load_count(),
|
||||||
_detail_asset_queue.size(),
|
_detail_asset_queue.size(),
|
||||||
_m2_group_tasks.size(),
|
_m2_group_tasks.size(),
|
||||||
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
_m2_animation_load_pipeline_state.total_work_count(),
|
||||||
_m2_mesh_load_pipeline_state.total_work_count(),
|
_m2_mesh_load_pipeline_state.total_work_count(),
|
||||||
_m2_build_queue.size(),
|
_m2_build_queue.size(),
|
||||||
(
|
(
|
||||||
@@ -1048,7 +1050,7 @@ func render_baseline_snapshot() -> Dictionary:
|
|||||||
"water": _adt_water_load_pipeline_state.total_load_count(),
|
"water": _adt_water_load_pipeline_state.total_load_count(),
|
||||||
"detail": _detail_asset_queue.size(),
|
"detail": _detail_asset_queue.size(),
|
||||||
"m2_task": _m2_group_tasks.size(),
|
"m2_task": _m2_group_tasks.size(),
|
||||||
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
"m2_animation": _m2_animation_load_pipeline_state.total_work_count(),
|
||||||
"m2_mesh": _m2_mesh_load_pipeline_state.total_work_count(),
|
"m2_mesh": _m2_mesh_load_pipeline_state.total_work_count(),
|
||||||
"m2_build": _m2_build_queue.size(),
|
"m2_build": _m2_build_queue.size(),
|
||||||
"wmo_build": (
|
"wmo_build": (
|
||||||
@@ -1334,7 +1336,7 @@ func _tick_runtime_stats(delta: float) -> void:
|
|||||||
_tile_load_queue.size(),
|
_tile_load_queue.size(),
|
||||||
_detail_asset_queue.size(),
|
_detail_asset_queue.size(),
|
||||||
_m2_group_tasks.size(),
|
_m2_group_tasks.size(),
|
||||||
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
_m2_animation_load_pipeline_state.total_work_count(),
|
||||||
_m2_mesh_load_pipeline_state.total_work_count(),
|
_m2_mesh_load_pipeline_state.total_work_count(),
|
||||||
_m2_build_queue.size(),
|
_m2_build_queue.size(),
|
||||||
(
|
(
|
||||||
@@ -2355,15 +2357,14 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
_m2_mesh_load_pipeline_state.clear()
|
_m2_mesh_load_pipeline_state.clear()
|
||||||
_m2_runtime_mesh_finalizer.clear()
|
_m2_runtime_mesh_finalizer.clear()
|
||||||
|
|
||||||
for pending in _m2_animation_load_requests.values():
|
for pending in _m2_animation_load_pipeline_state.request_records_snapshot():
|
||||||
var path: String = String(pending.get("path", ""))
|
var path: String = String(pending.get("path", ""))
|
||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
var status := ResourceLoader.load_threaded_get_status(path)
|
||||||
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
|
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
|
||||||
ResourceLoader.load_threaded_get(path)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_m2_animation_load_requests.clear()
|
_m2_animation_load_pipeline_state.clear()
|
||||||
_m2_animation_finalize_queue.clear()
|
|
||||||
|
|
||||||
var wmo_scene_request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot()
|
var wmo_scene_request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot()
|
||||||
for path_variant in wmo_scene_request_paths.values():
|
for path_variant in wmo_scene_request_paths.values():
|
||||||
@@ -3004,8 +3005,7 @@ func _clear_streamed_world() -> void:
|
|||||||
_m2_unique_placement_registry.clear()
|
_m2_unique_placement_registry.clear()
|
||||||
_m2_mesh_load_pipeline_state.clear()
|
_m2_mesh_load_pipeline_state.clear()
|
||||||
_m2_runtime_mesh_finalizer.clear()
|
_m2_runtime_mesh_finalizer.clear()
|
||||||
_m2_animation_load_requests.clear()
|
_m2_animation_load_pipeline_state.clear()
|
||||||
_m2_animation_finalize_queue.clear()
|
|
||||||
_wmo_build_jobs.clear()
|
_wmo_build_jobs.clear()
|
||||||
_wmo_build_queue.clear()
|
_wmo_build_queue.clear()
|
||||||
_wmo_scene_resource_cache_state.clear_transient_state()
|
_wmo_scene_resource_cache_state.clear_transient_state()
|
||||||
@@ -4210,24 +4210,21 @@ func _drain_m2_group_results() -> void:
|
|||||||
|
|
||||||
|
|
||||||
func _drain_m2_animation_loads() -> void:
|
func _drain_m2_animation_loads() -> void:
|
||||||
for normalized_rel_variant in _m2_animation_load_requests.keys():
|
for pending in _m2_animation_load_pipeline_state.request_records_snapshot():
|
||||||
var normalized_rel := String(normalized_rel_variant)
|
var normalized_rel := String(pending.get("normalized", ""))
|
||||||
var pending: Dictionary = _m2_animation_load_requests[normalized_rel]
|
|
||||||
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_pipeline_state.discard_request(normalized_rel)
|
||||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
_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:
|
||||||
continue
|
continue
|
||||||
pending["status"] = status
|
_m2_animation_load_pipeline_state.complete_request(normalized_rel, status)
|
||||||
_m2_animation_load_requests.erase(normalized_rel)
|
|
||||||
_m2_animation_finalize_queue.append(pending)
|
|
||||||
|
|
||||||
while not _m2_animation_finalize_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
|
while _m2_animation_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
|
||||||
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_load_pipeline_state.pop_finalize_record()
|
||||||
var normalized_rel := String(pending.get("normalized", ""))
|
var normalized_rel := String(pending.get("normalized", ""))
|
||||||
if (
|
if (
|
||||||
normalized_rel.is_empty()
|
normalized_rel.is_empty()
|
||||||
@@ -4341,7 +4338,7 @@ func _process_m2_build_jobs() -> void:
|
|||||||
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
|
animated_prototype = _get_or_load_m2_native_animated_prototype(rel_path)
|
||||||
if animated_prototype == null:
|
if animated_prototype == null:
|
||||||
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
|
animated_prototype = _get_or_load_m2_animated_prototype(rel_path)
|
||||||
if enable_m2_animated_instances and _m2_animation_load_requests.has(normalized_rel):
|
if enable_m2_animated_instances and _m2_animation_load_pipeline_state.has_request(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)
|
||||||
@@ -4945,7 +4942,7 @@ func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
|
|||||||
return cached_animated
|
return cached_animated
|
||||||
if _m2_prototype_cache_state.is_animation_static(normalized_rel):
|
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_pipeline_state.has_request(normalized_rel):
|
||||||
return null
|
return null
|
||||||
|
|
||||||
_request_m2_animation_load(normalized_rel)
|
_request_m2_animation_load(normalized_rel)
|
||||||
@@ -4953,7 +4950,7 @@ func _get_or_load_m2_animated_prototype(rel_path: String) -> Node3D:
|
|||||||
|
|
||||||
|
|
||||||
func _request_m2_animation_load(normalized_rel: String) -> void:
|
func _request_m2_animation_load(normalized_rel: String) -> void:
|
||||||
if normalized_rel.is_empty() or _m2_animation_load_requests.has(normalized_rel):
|
if normalized_rel.is_empty() or _m2_animation_load_pipeline_state.has_request(normalized_rel):
|
||||||
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():
|
||||||
@@ -4965,10 +4962,7 @@ func _request_m2_animation_load(normalized_rel: String) -> void:
|
|||||||
false,
|
false,
|
||||||
ResourceLoader.CACHE_MODE_REUSE)
|
ResourceLoader.CACHE_MODE_REUSE)
|
||||||
if err == OK or err == ERR_BUSY:
|
if err == OK or err == ERR_BUSY:
|
||||||
_m2_animation_load_requests[normalized_rel] = {
|
_m2_animation_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
|
||||||
"normalized": normalized_rel,
|
|
||||||
"path": cache_res_path,
|
|
||||||
}
|
|
||||||
else:
|
else:
|
||||||
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
_m2_prototype_cache_state.mark_animation_static(normalized_rel)
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,142 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic lifecycle, FIFO, ownership and timing regression for animated M2
|
||||||
|
## threaded scene loading.
|
||||||
|
|
||||||
|
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_animation_load_pipeline_state.gd")
|
||||||
|
const PIPELINE_PATH := "res://src/render/m2/m2_animation_load_pipeline_state.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_validation_dedupe_and_order(failures)
|
||||||
|
_verify_terminal_transition_and_fifo(failures)
|
||||||
|
_verify_discard_metrics_clear_and_diagnostics(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_ANIMATION_LOAD_PIPELINE_STATE: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_ANIMATION_LOAD_PIPELINE_STATE PASS cases=11 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_validation_dedupe_and_order(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
_expect_false(pipeline.call("remember_request", "", "res://a.glb"), "empty normalized path rejected", failures)
|
||||||
|
_expect_false(pipeline.call("remember_request", "world/a.m2", ""), "empty resource path rejected", failures)
|
||||||
|
_expect_true(pipeline.call("remember_request", "world/a.m2", "res://a.glb"), "first request accepted", failures)
|
||||||
|
_expect_false(pipeline.call("remember_request", "world/a.m2", "res://duplicate.glb"), "duplicate request rejected", failures)
|
||||||
|
_expect_true(pipeline.call("remember_request", "world/b.m2", "res://b.glb"), "second request accepted", failures)
|
||||||
|
_expect_true(pipeline.call("has_request", "world/a.m2"), "request observable", failures)
|
||||||
|
var records: Array[Dictionary] = pipeline.call("request_records_snapshot")
|
||||||
|
_expect_equal(records.size(), 2, "request record count", failures)
|
||||||
|
_expect_string_equal(String(records[0]["normalized"]), "world/a.m2", "request insertion order first", failures)
|
||||||
|
_expect_string_equal(String(records[1]["normalized"]), "world/b.m2", "request insertion order second", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_terminal_transition_and_fifo(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
pipeline.call("remember_request", "world/a.m2", "res://a.glb")
|
||||||
|
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||||
|
_expect_false(pipeline.call("complete_request", "world/missing.m2", 3), "unknown completion rejected", failures)
|
||||||
|
_expect_true(pipeline.call("complete_request", "world/b.m2", 4), "second request completes first", failures)
|
||||||
|
_expect_true(pipeline.call("complete_request", "world/a.m2", 3), "first request completes second", failures)
|
||||||
|
_expect_false(pipeline.call("has_request", "world/a.m2"), "completed request removed", failures)
|
||||||
|
_expect_equal(int(pipeline.call("pending_request_count")), 0, "all requests completed", failures)
|
||||||
|
_expect_equal(int(pipeline.call("finalize_record_count")), 2, "two terminal records", failures)
|
||||||
|
var first: Dictionary = pipeline.call("pop_finalize_record")
|
||||||
|
var second: Dictionary = pipeline.call("pop_finalize_record")
|
||||||
|
_expect_string_equal(String(first["normalized"]), "world/b.m2", "completion FIFO first", failures)
|
||||||
|
_expect_equal(int(first["status"]), 4, "first opaque status retained", failures)
|
||||||
|
_expect_string_equal(String(second["normalized"]), "world/a.m2", "completion FIFO second", failures)
|
||||||
|
_expect_equal(int(second["status"]), 3, "second opaque status retained", failures)
|
||||||
|
_expect_false(pipeline.call("has_finalize_record"), "finalize FIFO drained", failures)
|
||||||
|
_expect_true((pipeline.call("pop_finalize_record") as Dictionary).is_empty(), "empty pop returns Dictionary", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
pipeline.call("remember_request", "world/a.m2", "res://a.glb")
|
||||||
|
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||||
|
_expect_true(pipeline.call("discard_request", "world/a.m2"), "pending request discarded", failures)
|
||||||
|
_expect_false(pipeline.call("discard_request", "world/a.m2"), "discard is idempotent", failures)
|
||||||
|
pipeline.call("complete_request", "world/b.m2", 3)
|
||||||
|
_expect_equal(int(pipeline.call("total_work_count")), 1, "total includes finalize only", failures)
|
||||||
|
var snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||||
|
(snapshot["finalize_records"] as Array)[0]["path"] = "mutated"
|
||||||
|
var fresh_snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||||
|
_expect_string_equal(String((fresh_snapshot["finalize_records"] as Array)[0]["path"]), "res://b.glb", "diagnostics detached", failures)
|
||||||
|
pipeline.call("clear")
|
||||||
|
pipeline.call("clear")
|
||||||
|
_expect_equal(int(pipeline.call("total_work_count")), 0, "clear removes all work", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(loader_source.contains("M2_ANIMATION_LOAD_PIPELINE_STATE_SCRIPT.new()"), "loader composes pipeline state", failures)
|
||||||
|
_expect_false(loader_source.contains("var _m2_animation_load_requests:"), "legacy request field removed", failures)
|
||||||
|
_expect_false(loader_source.contains("var _m2_animation_finalize_queue:"), "legacy finalize field removed", failures)
|
||||||
|
_expect_equal(loader_source.count("_m2_animation_load_pipeline_state.total_work_count()"), 3, "three existing metrics delegate", failures)
|
||||||
|
_expect_equal(loader_source.count("_m2_animation_load_pipeline_state.clear()"), 2, "two existing clear sites delegate", failures)
|
||||||
|
for retained_loader_rule in [
|
||||||
|
"ResourceLoader.load_threaded_request(",
|
||||||
|
"ResourceLoader.load_threaded_get_status(path)",
|
||||||
|
"ResourceLoader.load_threaded_get(path)",
|
||||||
|
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_ANIMATION_FINALIZE",
|
||||||
|
"_m2_prototype_cache_state.adopt_animated_prototype(",
|
||||||
|
"_m2_prototype_cache_state.mark_animation_static(",
|
||||||
|
]:
|
||||||
|
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"Node3D",
|
||||||
|
"PackedScene",
|
||||||
|
": Mesh",
|
||||||
|
"_m2_prototype_cache_state",
|
||||||
|
"_render_budget_scheduler",
|
||||||
|
]:
|
||||||
|
_expect_false(pipeline_source.contains(forbidden_dependency), "pipeline omits %s ownership" % forbidden_dependency, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for _iteration in range(100):
|
||||||
|
for path_index in range(256):
|
||||||
|
pipeline.call("remember_request", "world/model_%d.m2" % path_index, "res://model_%d.glb" % path_index)
|
||||||
|
for path_index in range(256):
|
||||||
|
pipeline.call("complete_request", "world/model_%d.m2" % path_index, 3)
|
||||||
|
while pipeline.call("has_finalize_record"):
|
||||||
|
pipeline.call("pop_finalize_record")
|
||||||
|
pipeline.call("clear")
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 transitions 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_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dx682en5374nh
|
||||||
Reference in New Issue
Block a user