refactor(M03): extract WMO placement registry
This commit is contained in:
@@ -27,6 +27,8 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
|||||||
|
|
||||||
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
|
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
|
||||||
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
|
- `src/scenes/streaming/streaming_world_loader.gd` - внутренний runtime streamer мира.
|
||||||
|
- `src/render/wmo/wmo_placement_resolver.gd` - value-only WMO path, identity and transform rules.
|
||||||
|
- `src/render/wmo/wmo_placement_registry.gd` - WMO placement-key to tile/global reference sets; Nodes and render jobs remain in the streamer.
|
||||||
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
||||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||||
@@ -394,6 +396,8 @@ Known limits:
|
|||||||
- `WMOBuilder` строит group meshes;
|
- `WMOBuilder` строит group meshes;
|
||||||
- runtime registry дедуплицирует WMO по `unique_id`;
|
- runtime registry дедуплицирует WMO по `unique_id`;
|
||||||
- WMO на границе ADT не дублируются;
|
- WMO на границе ADT не дублируются;
|
||||||
|
- `WmoPlacementRegistry` owns only placement reference sets; the streamer keeps
|
||||||
|
the parallel key-to-Node map and performs final-release cancellation/free;
|
||||||
- большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения;
|
- большие WMO не должны инстанцироваться как тяжелые `.tscn` во время движения;
|
||||||
- добавлен lightweight render-cache path для WMO groups;
|
- добавлен lightweight render-cache path для WMO groups;
|
||||||
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
|
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
|
||||||
|
|||||||
@@ -20,6 +20,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) |
|
||||||
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
||||||
|
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
||||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||||
|
|||||||
@@ -0,0 +1,207 @@
|
|||||||
|
# WMO Placement Registry
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented extraction |
|
||||||
|
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-REGISTRY-001` |
|
||||||
|
| Owners | WMO placement-key to tile/global reference-set ownership |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-registry`, 2026-07-17 |
|
||||||
|
| Profiles/capabilities | Existing ADT/WDT WMO placement paths; profile-independent |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Own the reference sets that keep one resolved WMO placement active across one
|
||||||
|
or more ADT tiles, or across the global WDT placement lifetime. This separates
|
||||||
|
value-only placement ownership from scene and render-resource ownership.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Resolve WMO paths, placement identity or world transforms.
|
||||||
|
- Own Nodes, Resources, RIDs, roots, jobs, queues, tasks or caches.
|
||||||
|
- Load, instantiate, build or free WMO content.
|
||||||
|
- Change duplicate order, budgets, retries or global-WMO behavior.
|
||||||
|
- Claim original-client WMO visual, portal, room or material parity.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Parsed[ADT/WDT placement] --> Resolver[WmoPlacementResolver]
|
||||||
|
Resolver --> Key[Placement key]
|
||||||
|
Tile[Tile/global reference key] --> Loader[StreamingWorldLoader]
|
||||||
|
Key --> Loader
|
||||||
|
Loader --> Registry[WmoPlacementRegistry]
|
||||||
|
Registry --> Decision[Existing or final-release decision]
|
||||||
|
Decision --> Loader
|
||||||
|
Loader --> NodeMap[Loader key-to-Node map]
|
||||||
|
Loader --> Jobs[Loader jobs/queues/resources]
|
||||||
|
```
|
||||||
|
|
||||||
|
The registry depends only on `RefCounted`, `Dictionary`, `Array` and `String`.
|
||||||
|
SceneTree, Node, Resource, RID, files, loaders, threads, locks, network,
|
||||||
|
gameplay and editor UI are forbidden dependencies.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `add_reference(placement_key, reference_key)` | Command/query | Idempotently add a reference; return true only when creating the placement key | Renderer main thread; map session | Empty key returns false and changes nothing |
|
||||||
|
| `release_reference(placement_key, reference_key)` | Command/query | Remove an owned reference; return true only on final removal | Renderer main thread; map session | Empty, unknown or non-owner input returns false |
|
||||||
|
| `contains(placement_key)` | Query | Test whether any reference keeps a placement active | Renderer main thread; map session | Empty key returns false |
|
||||||
|
| `active_count()` | Query | Count active placement keys, not references | Renderer main thread; map session | None |
|
||||||
|
| `diagnostic_snapshot()` | Query | Return detached, placement/reference-sorted records | Renderer main thread; caller-owned result | None |
|
||||||
|
| `clear()` | Command | Remove all ownership records idempotently | Renderer main thread; reset/shutdown | None |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Resolved `uid:*`, `tile:*:*` or `global` placement key | `WmoPlacementResolver` / loader global adapter | Registry | Copied String dictionary key | Until final release/clear |
|
||||||
|
| Input | ADT tile key or `__global__` reference key | Loader | Registry | Copied String set key | Until release/clear |
|
||||||
|
| Output | First-placement Boolean | Registry | Loader creation path | Value | One call |
|
||||||
|
| Output | Final-release Boolean | Registry | Loader cancellation/free path | Value | One call |
|
||||||
|
| Output | Active count and sorted diagnostic records | Registry | Renderer diagnostics/verifier | Detached values | Caller lifetime |
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Add[add placement + reference] --> Valid{Both non-empty?}
|
||||||
|
Valid -->|no| Reject[false; unchanged]
|
||||||
|
Valid -->|yes| Existing{Placement exists?}
|
||||||
|
Existing -->|no| Create[Create reference set; true]
|
||||||
|
Existing -->|yes| Retain[Idempotently add reference; false]
|
||||||
|
Release[release placement + reference] --> Owner{Known owner reference?}
|
||||||
|
Owner -->|no| Reject
|
||||||
|
Owner -->|yes| Erase[Erase reference]
|
||||||
|
Erase --> Empty{Set empty?}
|
||||||
|
Empty -->|no| Retained[false; placement remains]
|
||||||
|
Empty -->|yes| Final[Erase placement; true]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle and state
|
||||||
|
|
||||||
|
The states for one placement are `Absent`, `Referenced` and back to `Absent`.
|
||||||
|
The first valid add enters `Referenced`; further tile/global adds remain there.
|
||||||
|
Only release of the final owned reference returns to `Absent`. Map reset and
|
||||||
|
shutdown call `clear`, which is safe from either state.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Absent
|
||||||
|
Absent --> Referenced: first valid add / true
|
||||||
|
Referenced --> Referenced: idempotent/shared add / false
|
||||||
|
Referenced --> Referenced: non-owner/non-final release / false
|
||||||
|
Referenced --> Absent: final owned release / true
|
||||||
|
Referenced --> Absent: clear
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Registry as WmoPlacementRegistry
|
||||||
|
participant Nodes as Loader Node map
|
||||||
|
Loader->>Registry: contains(placement key)
|
||||||
|
alt existing placement
|
||||||
|
Loader->>Registry: add_reference(key, tile)
|
||||||
|
else new placement built successfully
|
||||||
|
Loader->>Nodes: store key-to-Node
|
||||||
|
Loader->>Registry: add_reference(key, tile/global)
|
||||||
|
end
|
||||||
|
Loader->>Registry: release_reference(key, tile)
|
||||||
|
alt final reference
|
||||||
|
Registry-->>Loader: true
|
||||||
|
Loader->>Loader: cancel render build
|
||||||
|
Loader->>Nodes: queue-free and erase Node
|
||||||
|
else shared/unknown reference
|
||||||
|
Registry-->>Loader: false
|
||||||
|
end
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The registry owns only placement-key Dictionaries and reference-key sets.
|
||||||
|
- The loader owns the parallel placement-key-to-Node map and every Node action.
|
||||||
|
- The loader also owns WMO roots, tile state, caches, requests, jobs, queues,
|
||||||
|
retries, cancellation and render-resource lifetime.
|
||||||
|
- Current calls are serialized on the renderer main thread. The registry has no
|
||||||
|
lock and must not be mutated concurrently.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Empty placement/reference key | API guard | Reject without mutation | Contract verifier | Repair caller input |
|
||||||
|
| Duplicate add | Existing set member | Idempotent retain | Sorted snapshot | No recovery required |
|
||||||
|
| Unknown/non-owner release | Missing placement/reference | Ignore without mutation | Contract verifier | Release from actual owner or reset |
|
||||||
|
| Tile build cancellation | Loader-owned condition | Loader cancels job, then releases recorded refs | Existing queue metrics | Retry when tile becomes wanted |
|
||||||
|
| Final release | Empty set after erase | Return true and erase registry record | Active count | Loader cancels/free Node exactly once |
|
||||||
|
| Map reset/shutdown | Loader lifecycle | Loader frees Nodes/jobs and clears registry | Shutdown regression | Next map begins empty |
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
There is no setting, profile branch or capacity knob. WMO tile radius, cache
|
||||||
|
limits, render budgets and visibility settings remain loader-owned.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
No data is serialized and no cache/schema/version changes. Placement and
|
||||||
|
reference keys exist only for the current renderer map session.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
`active_count` backs existing WMO instance metrics. `diagnostic_snapshot`
|
||||||
|
returns deterministic detached records for tests and future read-only
|
||||||
|
diagnostics; mutating the returned arrays cannot alter registry state.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_wmo_placement_registry.gd`: invalid/first/idempotent/shared/non-owner/
|
||||||
|
final/global/clear behavior, deterministic detached diagnostics, source
|
||||||
|
ownership boundaries and bounded 100-by-256 shared-reference cycles.
|
||||||
|
- WMO resolver, material, runtime-cache shutdown and the renderer regression
|
||||||
|
suite verify the unchanged loader adapters and surrounding lifecycle.
|
||||||
|
- Fidelity evidence is exact preservation of the repository's prior shared-ref
|
||||||
|
and final-release semantics. Asset-backed 3.3.5a comparison is still pending.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- WMO build/resource state can be extracted separately without moving Nodes
|
||||||
|
into this registry.
|
||||||
|
- A future diagnostic adapter may expose the detached snapshot through the
|
||||||
|
facade without exposing mutable loader internals.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| WMO placement reference ownership | Implemented extraction | Contract/source/timing verifier | Asset-backed cross-tile corpus pending |
|
||||||
|
| WMO build/resource scheduling | Remains in loader | Existing runtime regressions | Separate state/cancellation package |
|
||||||
|
| WMO visual/portal/material fidelity | Partial | Renderer baseline/material checks | Original-client paired evidence pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Correctness assumes the loader records each successful placement reference in
|
||||||
|
the tile `wmo_refs` array before unregister.
|
||||||
|
- Old caches without positive UID retain resolver-defined per-tile identity.
|
||||||
|
- No proprietary WMO corpus or original-client paired capture is in this package.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/wmo/wmo_placement_registry.gd` | Placement-key/reference-set ownership |
|
||||||
|
| `src/render/wmo/wmo_placement_resolver.gd` | Stable path, identity and transform rules |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Node map, instances, resources, jobs, queues and lifecycle reactions |
|
||||||
|
| `src/tools/verify_wmo_placement_registry.gd` | Contract, boundary and timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`wmo-placement-resolver.md`](wmo-placement-resolver.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)
|
||||||
@@ -19,7 +19,8 @@ live-prototype instance paths.
|
|||||||
## Non-goals
|
## Non-goals
|
||||||
|
|
||||||
- Parse ADT/WDT or convert canonical WoW coordinates.
|
- Parse ADT/WDT or convert canonical WoW coordinates.
|
||||||
- Own WMO registry references, tile state, jobs, queues or retries.
|
- Own WMO registry references, tile state, jobs, queues or retries; reference
|
||||||
|
ownership belongs to `WmoPlacementRegistry`.
|
||||||
- Load/validate cached scenes or lightweight render resources.
|
- Load/validate cached scenes or lightweight render resources.
|
||||||
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
|
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
|
||||||
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
|
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
|
||||||
@@ -34,7 +35,7 @@ flowchart LR
|
|||||||
Resolver --> Identity[Registry unique key]
|
Resolver --> Identity[Registry unique key]
|
||||||
Resolver --> Transform[World Transform3D]
|
Resolver --> Transform[World Transform3D]
|
||||||
CacheKey --> Cache[Loader WMO caches/requests]
|
CacheKey --> Cache[Loader WMO caches/requests]
|
||||||
Identity --> Registry[Loader WMO registry]
|
Identity --> Registry[WmoPlacementRegistry]
|
||||||
Transform --> RenderRoot[Lightweight render root]
|
Transform --> RenderRoot[Lightweight render root]
|
||||||
Transform --> Scene[Cached scene instance]
|
Transform --> Scene[Cached scene instance]
|
||||||
Transform --> Live[Live prototype instance]
|
Transform --> Live[Live prototype instance]
|
||||||
@@ -60,7 +61,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
|
|||||||
| Input | Raw placement Dictionary | WDT global or ADT MODF parser result | Identity/transform queries | Read-only shallow value | One call |
|
| Input | Raw placement Dictionary | WDT global or ADT MODF parser result | Identity/transform queries | Read-only shallow value | One call |
|
||||||
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
|
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
|
||||||
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
|
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
|
||||||
| Output | `uid:*` or `tile:*:*` key | Resolver | Loader WMO registry/ref arrays | New String value | Until unregister/reset |
|
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
|
||||||
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
|
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
|
||||||
|
|
||||||
The resolver retains no source Dictionary, output or engine resource.
|
The resolver retains no source Dictionary, output or engine resource.
|
||||||
@@ -91,7 +92,7 @@ and shutdown require no resolver operation.
|
|||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Loader as StreamingWorldLoader
|
participant Loader as StreamingWorldLoader
|
||||||
participant Resolver as WmoPlacementResolver
|
participant Resolver as WmoPlacementResolver
|
||||||
participant Registry as Loader WMO registry/cache
|
participant Registry as WmoPlacementRegistry
|
||||||
participant Instance as Render/cached/live instance
|
participant Instance as Render/cached/live instance
|
||||||
Loader->>Resolver: normalize_relative_path(path)
|
Loader->>Resolver: normalize_relative_path(path)
|
||||||
Resolver-->>Loader: cache key
|
Resolver-->>Loader: cache key
|
||||||
@@ -105,8 +106,9 @@ sequenceDiagram
|
|||||||
## Ownership, threading and resources
|
## Ownership, threading and resources
|
||||||
|
|
||||||
- The resolver owns only call-local scalar/value calculations.
|
- The resolver owns only call-local scalar/value calculations.
|
||||||
- The loader owns WMO registry entries/ref sets, cache/load-request state, build
|
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||||
jobs/queues, resource fallback and cancellation.
|
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
|
||||||
|
cancellation.
|
||||||
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
|
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
|
||||||
- Pure calls are thread-safe; current consumers execute on the main thread.
|
- Pure calls are thread-safe; current consumers execute on the main thread.
|
||||||
|
|
||||||
@@ -152,8 +154,8 @@ in the facade/loader diagnostic snapshot.
|
|||||||
|
|
||||||
## Extension points
|
## Extension points
|
||||||
|
|
||||||
- A later package may extract WMO registry ownership/ref release using these
|
- `WmoPlacementRegistry` consumes these stable keys; later packages may extract
|
||||||
stable keys.
|
WMO build/resource state without changing the resolver.
|
||||||
- Render-group build cursor and resource-selection state require separate
|
- Render-group build cursor and resource-selection state require separate
|
||||||
lifecycle/cancellation contracts.
|
lifecycle/cancellation contracts.
|
||||||
|
|
||||||
@@ -162,7 +164,8 @@ in the facade/loader diagnostic snapshot.
|
|||||||
| Capability | Status | Evidence | Gap/next step |
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
|
| Consistent WMO placement identity/transform | Implemented extraction | Contract/source/timing verifier | Asset-backed placement comparison pending |
|
||||||
| WMO registry/build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
|
| WMO placement registry | Implemented extraction | Ownership/source/timing verifier | Asset-backed cross-tile corpus pending |
|
||||||
|
| WMO build services | Remains in loader | Existing runtime regressions | Stateful extraction pending |
|
||||||
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
|
| Portals/rooms/material fidelity | Partial | M00 checkpoint/material evidence | Broader WMO implementation pending |
|
||||||
|
|
||||||
## Known gaps and risks
|
## Known gaps and risks
|
||||||
@@ -177,11 +180,13 @@ in the facade/loader diagnostic snapshot.
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
|
| `src/render/wmo/wmo_placement_resolver.gd` | Path, identity and transform rules |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Registry/cache/build/resource/Node ownership |
|
| `src/render/wmo/wmo_placement_registry.gd` | Placement reference-set ownership |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Node map/cache/build/resource ownership |
|
||||||
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
|
| `src/tools/verify_wmo_placement_resolver.gd` | Contract, source and timing regression |
|
||||||
|
|
||||||
## Related decisions and references
|
## Related decisions and references
|
||||||
|
|
||||||
- [`world-renderer.md`](world-renderer.md)
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
|
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
|
||||||
- [`../../RENDER.md`](../../RENDER.md)
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.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-wmo-placement-resolver`, 2026-07-17 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-registry`, 2026-07-17 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -56,6 +56,8 @@ flowchart LR
|
|||||||
M2Batch --> Loader
|
M2Batch --> Loader
|
||||||
Loader --> WmoPlacement[WmoPlacementResolver]
|
Loader --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> Loader
|
WmoPlacement --> Loader
|
||||||
|
Loader --> WmoRegistry[WmoPlacementRegistry]
|
||||||
|
WmoRegistry --> Loader
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
Parsed --> Loader
|
Parsed --> Loader
|
||||||
Loader --> Scene[SceneTree nodes]
|
Loader --> Scene[SceneTree nodes]
|
||||||
@@ -127,6 +129,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 |
|
||||||
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
||||||
|
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||||
|
|
||||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||||
@@ -156,6 +159,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
|
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
|
||||||
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
|
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
|
||||||
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
|
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
|
||||||
|
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
|
||||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||||
@@ -205,7 +209,8 @@ flowchart TD
|
|||||||
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
||||||
M2Batch --> M2
|
M2Batch --> M2
|
||||||
R --> WmoPlacement[WmoPlacementResolver]
|
R --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> WMO
|
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
|
||||||
|
WmoRegistry --> WMO
|
||||||
R --> B[RenderBudgetScheduler permits]
|
R --> B[RenderBudgetScheduler permits]
|
||||||
B --> Terrain[Terrain attach/upgrade]
|
B --> Terrain[Terrain attach/upgrade]
|
||||||
B --> M2[M2 group/MultiMesh attach]
|
B --> M2[M2 group/MultiMesh attach]
|
||||||
@@ -328,7 +333,9 @@ sequenceDiagram
|
|||||||
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
||||||
loader retains queue/resource transitions, cursor adoption and materialization.
|
loader retains queue/resource transitions, cursor adoption and materialization.
|
||||||
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
||||||
identity and transform values. The loader retains WMO refs/caches/jobs/resources.
|
identity and transform values. `WmoPlacementRegistry` owns only placement-key
|
||||||
|
reference sets. The loader retains the key-to-Node map, caches, jobs, queues,
|
||||||
|
resources, cancellation and every Node lifecycle action.
|
||||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
the facade never returns Mesh, Node, tile-state or queue references.
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
@@ -422,6 +429,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
count, completion/cursor rules, source boundary and bounded timing.
|
count, completion/cursor rules, source boundary and bounded timing.
|
||||||
- WMO placement contract: cache-path normalization, positive/fallback identity,
|
- WMO placement contract: cache-path normalization, positive/fallback identity,
|
||||||
default/rotated/unclamped transforms, three consumers and bounded timing.
|
default/rotated/unclamped transforms, three consumers and bounded timing.
|
||||||
|
- WMO placement registry contract: first/idempotent/shared/non-owner/final/global
|
||||||
|
lifecycle, detached sorted diagnostics, source ownership and bounded timing.
|
||||||
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
||||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||||
dependency boundaries and bounded permit timing without loading a world scene.
|
dependency boundaries and bounded permit timing without loading a world scene.
|
||||||
@@ -455,7 +464,8 @@ 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 |
|
||||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Registry/build extraction and 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 rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
||||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||||
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
||||||
|
|||||||
@@ -0,0 +1,69 @@
|
|||||||
|
class_name WmoPlacementRegistry
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Owns WMO placement-key to tile/global reference-key sets without Nodes.
|
||||||
|
|
||||||
|
var _reference_keys_by_placement_key: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
|
## Adds one idempotent reference and returns true only for a newly registered
|
||||||
|
## placement key. Empty placement/reference keys are rejected.
|
||||||
|
func add_reference(placement_key: String, reference_key: String) -> bool:
|
||||||
|
if placement_key.is_empty() or reference_key.is_empty():
|
||||||
|
return false
|
||||||
|
var is_new_placement := not _reference_keys_by_placement_key.has(placement_key)
|
||||||
|
if is_new_placement:
|
||||||
|
_reference_keys_by_placement_key[placement_key] = {}
|
||||||
|
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
|
||||||
|
reference_keys[reference_key] = true
|
||||||
|
return is_new_placement
|
||||||
|
|
||||||
|
|
||||||
|
## Releases one reference and returns true only when the final reference removes
|
||||||
|
## the placement key. Unknown and non-owner references leave state unchanged.
|
||||||
|
func release_reference(placement_key: String, reference_key: String) -> bool:
|
||||||
|
if placement_key.is_empty() or reference_key.is_empty():
|
||||||
|
return false
|
||||||
|
if not _reference_keys_by_placement_key.has(placement_key):
|
||||||
|
return false
|
||||||
|
var reference_keys: Dictionary = _reference_keys_by_placement_key[placement_key]
|
||||||
|
if not reference_keys.has(reference_key):
|
||||||
|
return false
|
||||||
|
reference_keys.erase(reference_key)
|
||||||
|
if not reference_keys.is_empty():
|
||||||
|
return false
|
||||||
|
_reference_keys_by_placement_key.erase(placement_key)
|
||||||
|
return true
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether the placement key currently has at least one reference.
|
||||||
|
func contains(placement_key: String) -> bool:
|
||||||
|
return not placement_key.is_empty() and _reference_keys_by_placement_key.has(placement_key)
|
||||||
|
|
||||||
|
|
||||||
|
## Removes every placement/reference record. Safe to call repeatedly.
|
||||||
|
func clear() -> void:
|
||||||
|
_reference_keys_by_placement_key.clear()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the number of currently referenced placement keys.
|
||||||
|
func active_count() -> int:
|
||||||
|
return _reference_keys_by_placement_key.size()
|
||||||
|
|
||||||
|
|
||||||
|
## Returns detached, placement/reference-key-sorted diagnostic records.
|
||||||
|
func diagnostic_snapshot() -> Array[Dictionary]:
|
||||||
|
var placement_keys: Array = _reference_keys_by_placement_key.keys()
|
||||||
|
placement_keys.sort()
|
||||||
|
var snapshot: Array[Dictionary] = []
|
||||||
|
for placement_key_variant in placement_keys:
|
||||||
|
var placement_key := String(placement_key_variant)
|
||||||
|
var reference_keys: Array = (
|
||||||
|
_reference_keys_by_placement_key[placement_key] as Dictionary
|
||||||
|
).keys()
|
||||||
|
reference_keys.sort()
|
||||||
|
snapshot.append({
|
||||||
|
"placement_key": placement_key,
|
||||||
|
"reference_keys": reference_keys.duplicate(),
|
||||||
|
})
|
||||||
|
return snapshot
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cfbo5fw33vw4f
|
||||||
@@ -12,6 +12,9 @@ const WMO_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/wmo_buil
|
|||||||
const WMO_PLACEMENT_RESOLVER_SCRIPT := preload(
|
const WMO_PLACEMENT_RESOLVER_SCRIPT := preload(
|
||||||
"res://src/render/wmo/wmo_placement_resolver.gd"
|
"res://src/render/wmo/wmo_placement_resolver.gd"
|
||||||
)
|
)
|
||||||
|
const WMO_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||||
|
"res://src/render/wmo/wmo_placement_registry.gd"
|
||||||
|
)
|
||||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||||
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
const M2_NATIVE_ANIMATED_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||||
@@ -286,7 +289,8 @@ var _m2_animated_scene_cache: Dictionary = {}
|
|||||||
var _m2_static_animation_cache: Dictionary = {}
|
var _m2_static_animation_cache: Dictionary = {}
|
||||||
var _m2_missing_cache: Dictionary = {}
|
var _m2_missing_cache: Dictionary = {}
|
||||||
var _world_wmo_root: Node3D
|
var _world_wmo_root: Node3D
|
||||||
var _wmo_registry: Dictionary = {}
|
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||||
|
var _wmo_instance_nodes_by_unique_key: Dictionary = {}
|
||||||
|
|
||||||
|
|
||||||
func _ready() -> void:
|
func _ready() -> void:
|
||||||
@@ -1031,7 +1035,7 @@ func render_baseline_snapshot() -> Dictionary:
|
|||||||
},
|
},
|
||||||
"cache_entries": _tile_mesh_cache.size(),
|
"cache_entries": _tile_mesh_cache.size(),
|
||||||
"m2_active_unique_ids": _m2_unique_placement_registry.active_count(),
|
"m2_active_unique_ids": _m2_unique_placement_registry.active_count(),
|
||||||
"wmo_instances": _wmo_registry.size(),
|
"wmo_instances": _wmo_placement_registry.active_count(),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -1313,7 +1317,7 @@ func _tick_runtime_stats(delta: float) -> void:
|
|||||||
m2_tiles,
|
m2_tiles,
|
||||||
m2_multimeshes,
|
m2_multimeshes,
|
||||||
m2_instances,
|
m2_instances,
|
||||||
_wmo_registry.size(),
|
_wmo_placement_registry.active_count(),
|
||||||
])
|
])
|
||||||
|
|
||||||
|
|
||||||
@@ -3363,7 +3367,7 @@ func _wdt_has_global_wmo() -> bool:
|
|||||||
func _place_global_wmo() -> void:
|
func _place_global_wmo() -> void:
|
||||||
if not _wdt_has_global_wmo():
|
if not _wdt_has_global_wmo():
|
||||||
return
|
return
|
||||||
if _wmo_registry.has("global"):
|
if _wmo_placement_registry.contains("global"):
|
||||||
return
|
return
|
||||||
|
|
||||||
_ensure_world_wmo_root()
|
_ensure_world_wmo_root()
|
||||||
@@ -3387,10 +3391,7 @@ func _place_global_wmo() -> void:
|
|||||||
|
|
||||||
_world_wmo_root.add_child(instance)
|
_world_wmo_root.add_child(instance)
|
||||||
_set_editor_owner_recursive(instance)
|
_set_editor_owner_recursive(instance)
|
||||||
_wmo_registry["global"] = {
|
_register_wmo_instance_reference("global", "__global__", instance)
|
||||||
"node": instance,
|
|
||||||
"refs": {"__global__": true},
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
|
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
|
||||||
@@ -3566,10 +3567,8 @@ func _process_wmo_build_jobs() -> void:
|
|||||||
tile_key,
|
tile_key,
|
||||||
index
|
index
|
||||||
)
|
)
|
||||||
if _wmo_registry.has(unique_key):
|
if _wmo_placement_registry.contains(unique_key):
|
||||||
var entry: Dictionary = _wmo_registry[unique_key]
|
_wmo_placement_registry.add_reference(unique_key, tile_key)
|
||||||
var entry_refs: Dictionary = entry["refs"]
|
|
||||||
entry_refs[tile_key] = true
|
|
||||||
refs.append(unique_key)
|
refs.append(unique_key)
|
||||||
else:
|
else:
|
||||||
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
|
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
|
||||||
@@ -3579,10 +3578,7 @@ func _process_wmo_build_jobs() -> void:
|
|||||||
if instance != null:
|
if instance != null:
|
||||||
_world_wmo_root.add_child(instance)
|
_world_wmo_root.add_child(instance)
|
||||||
_set_editor_owner_recursive(instance)
|
_set_editor_owner_recursive(instance)
|
||||||
_wmo_registry[unique_key] = {
|
_register_wmo_instance_reference(unique_key, tile_key, instance)
|
||||||
"node": instance,
|
|
||||||
"refs": {tile_key: true},
|
|
||||||
}
|
|
||||||
refs.append(unique_key)
|
refs.append(unique_key)
|
||||||
_start_wmo_render_build(unique_key, instance, render_resource)
|
_start_wmo_render_build(unique_key, instance, render_resource)
|
||||||
else:
|
else:
|
||||||
@@ -3602,10 +3598,11 @@ func _process_wmo_build_jobs() -> void:
|
|||||||
_prepare_runtime_wmo_instance(live_instance)
|
_prepare_runtime_wmo_instance(live_instance)
|
||||||
_world_wmo_root.add_child(live_instance)
|
_world_wmo_root.add_child(live_instance)
|
||||||
_set_editor_owner_recursive(live_instance)
|
_set_editor_owner_recursive(live_instance)
|
||||||
_wmo_registry[unique_key] = {
|
_register_wmo_instance_reference(
|
||||||
"node": live_instance,
|
unique_key,
|
||||||
"refs": {tile_key: true},
|
tile_key,
|
||||||
}
|
live_instance
|
||||||
|
)
|
||||||
refs.append(unique_key)
|
refs.append(unique_key)
|
||||||
else:
|
else:
|
||||||
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
|
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
|
||||||
@@ -3613,10 +3610,11 @@ func _process_wmo_build_jobs() -> void:
|
|||||||
_prepare_runtime_wmo_instance(scene_instance)
|
_prepare_runtime_wmo_instance(scene_instance)
|
||||||
_world_wmo_root.add_child(scene_instance)
|
_world_wmo_root.add_child(scene_instance)
|
||||||
_set_editor_owner_recursive(scene_instance)
|
_set_editor_owner_recursive(scene_instance)
|
||||||
_wmo_registry[unique_key] = {
|
_register_wmo_instance_reference(
|
||||||
"node": scene_instance,
|
unique_key,
|
||||||
"refs": {tile_key: true},
|
tile_key,
|
||||||
}
|
scene_instance
|
||||||
|
)
|
||||||
refs.append(unique_key)
|
refs.append(unique_key)
|
||||||
if advance_job:
|
if advance_job:
|
||||||
job["refs"] = refs
|
job["refs"] = refs
|
||||||
@@ -3994,6 +3992,17 @@ func _cancel_wmo_build_job(tile_key: String) -> void:
|
|||||||
_wmo_build_queue.erase(tile_key)
|
_wmo_build_queue.erase(tile_key)
|
||||||
|
|
||||||
|
|
||||||
|
func _register_wmo_instance_reference(
|
||||||
|
placement_key: String,
|
||||||
|
reference_key: String,
|
||||||
|
instance: Node3D
|
||||||
|
) -> void:
|
||||||
|
if placement_key.is_empty() or reference_key.is_empty() or instance == null:
|
||||||
|
return
|
||||||
|
_wmo_instance_nodes_by_unique_key[placement_key] = instance
|
||||||
|
_wmo_placement_registry.add_reference(placement_key, reference_key)
|
||||||
|
|
||||||
|
|
||||||
func _unregister_tile_wmos(state: Dictionary) -> void:
|
func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||||
var refs: Array = state.get("wmo_refs", [])
|
var refs: Array = state.get("wmo_refs", [])
|
||||||
var tile_key: String = String(state.get("key", ""))
|
var tile_key: String = String(state.get("key", ""))
|
||||||
@@ -4003,18 +4012,16 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
|||||||
state["wmo_building"] = false
|
state["wmo_building"] = false
|
||||||
return
|
return
|
||||||
|
|
||||||
for unique_key in refs:
|
for unique_key_variant in refs:
|
||||||
if not _wmo_registry.has(unique_key):
|
var unique_key := String(unique_key_variant)
|
||||||
|
if not _wmo_placement_registry.contains(unique_key):
|
||||||
continue
|
continue
|
||||||
var entry: Dictionary = _wmo_registry[unique_key]
|
if _wmo_placement_registry.release_reference(unique_key, tile_key):
|
||||||
var entry_refs: Dictionary = entry["refs"]
|
|
||||||
entry_refs.erase(tile_key)
|
|
||||||
if entry_refs.is_empty():
|
|
||||||
_cancel_wmo_render_build_job(String(unique_key))
|
_cancel_wmo_render_build_job(String(unique_key))
|
||||||
var node: Node = entry.get("node", null)
|
var node: Node = _wmo_instance_nodes_by_unique_key.get(unique_key, null)
|
||||||
if node:
|
if node != null and is_instance_valid(node):
|
||||||
node.queue_free()
|
node.queue_free()
|
||||||
_wmo_registry.erase(unique_key)
|
_wmo_instance_nodes_by_unique_key.erase(unique_key)
|
||||||
|
|
||||||
state["wmo_refs"] = []
|
state["wmo_refs"] = []
|
||||||
state["wmo_building"] = false
|
state["wmo_building"] = false
|
||||||
@@ -4023,14 +4030,14 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
|||||||
func _clear_wmo_registry() -> void:
|
func _clear_wmo_registry() -> void:
|
||||||
_wmo_render_build_jobs.clear()
|
_wmo_render_build_jobs.clear()
|
||||||
_wmo_render_build_queue.clear()
|
_wmo_render_build_queue.clear()
|
||||||
for entry_variant in _wmo_registry.values():
|
for node_variant in _wmo_instance_nodes_by_unique_key.values():
|
||||||
if not (entry_variant is Dictionary):
|
if not (node_variant is Node):
|
||||||
continue
|
continue
|
||||||
var entry: Dictionary = entry_variant
|
var node := node_variant as Node
|
||||||
var node: Node = entry.get("node", null)
|
if is_instance_valid(node):
|
||||||
if node and is_instance_valid(node):
|
|
||||||
_queue_free_streamed_node(node)
|
_queue_free_streamed_node(node)
|
||||||
_wmo_registry.clear()
|
_wmo_instance_nodes_by_unique_key.clear()
|
||||||
|
_wmo_placement_registry.clear()
|
||||||
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
||||||
_queue_free_streamed_node(_world_wmo_root)
|
_queue_free_streamed_node(_world_wmo_root)
|
||||||
_world_wmo_root = null
|
_world_wmo_root = null
|
||||||
|
|||||||
@@ -0,0 +1,203 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Asset-free ownership, lifecycle, dependency and timing regression for WMOs.
|
||||||
|
|
||||||
|
const REGISTRY_SCRIPT := preload("res://src/render/wmo/wmo_placement_registry.gd")
|
||||||
|
const REGISTRY_PATH := "res://src/render/wmo/wmo_placement_registry.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_invalid_and_first_reference(failures)
|
||||||
|
_verify_idempotent_and_shared_references(failures)
|
||||||
|
_verify_owner_checked_final_release(failures)
|
||||||
|
_verify_global_reference_and_clear(failures)
|
||||||
|
_verify_detached_sorted_diagnostics(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("WMO_PLACEMENT_REGISTRY: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"WMO_PLACEMENT_REGISTRY PASS cases=10 iterations=100 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_invalid_and_first_reference(failures: Array[String]) -> void:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
_expect_false(registry.call("add_reference", "", "tile-a"), "empty placement rejected", failures)
|
||||||
|
_expect_false(registry.call("add_reference", "uid:7", ""), "empty reference rejected", failures)
|
||||||
|
_expect_true(registry.call("add_reference", "uid:7", "tile-a"), "first reference registers", failures)
|
||||||
|
_expect_true(registry.call("contains", "uid:7"), "registered placement contained", failures)
|
||||||
|
_expect_equal(int(registry.call("active_count")), 1, "first active count", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_idempotent_and_shared_references(failures: Array[String]) -> void:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
registry.call("add_reference", "uid:11", "tile-a")
|
||||||
|
_expect_false(
|
||||||
|
registry.call("add_reference", "uid:11", "tile-a"),
|
||||||
|
"same reference idempotent",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
registry.call("add_reference", "uid:11", "tile-b"),
|
||||||
|
"shared reference retains placement",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(int(registry.call("active_count")), 1, "shared active count", failures)
|
||||||
|
_expect_false(
|
||||||
|
registry.call("release_reference", "uid:11", "tile-a"),
|
||||||
|
"non-final release retains placement",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(registry.call("contains", "uid:11"), "shared placement remains", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_owner_checked_final_release(failures: Array[String]) -> void:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
registry.call("add_reference", "uid:19", "tile-owner")
|
||||||
|
_expect_false(
|
||||||
|
registry.call("release_reference", "uid:19", "tile-other"),
|
||||||
|
"non-owner release ignored",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(
|
||||||
|
registry.call("release_reference", "uid:404", "tile-owner"),
|
||||||
|
"unknown placement release ignored",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
registry.call("release_reference", "uid:19", "tile-owner"),
|
||||||
|
"final owner release removes placement",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(registry.call("contains", "uid:19"), "released placement absent", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_global_reference_and_clear(failures: Array[String]) -> void:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
_expect_true(
|
||||||
|
registry.call("add_reference", "global", "__global__"),
|
||||||
|
"global reference registers",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
registry.call("release_reference", "global", "__global__"),
|
||||||
|
"global final release removes",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
registry.call("add_reference", "uid:3", "tile-a")
|
||||||
|
registry.call("clear")
|
||||||
|
registry.call("clear")
|
||||||
|
_expect_equal(int(registry.call("active_count")), 0, "clear idempotent", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
registry.call("add_reference", "uid:20", "tile-b")
|
||||||
|
registry.call("add_reference", "uid:10", "tile-c")
|
||||||
|
registry.call("add_reference", "uid:10", "tile-a")
|
||||||
|
var snapshot: Array = registry.call("diagnostic_snapshot")
|
||||||
|
_expect_equal(snapshot.size(), 2, "diagnostic record count", failures)
|
||||||
|
_expect_string_equal(String(snapshot[0]["placement_key"]), "uid:10", "placement sort", failures)
|
||||||
|
_expect_string_array(snapshot[0]["reference_keys"], ["tile-a", "tile-c"], "reference sort", failures)
|
||||||
|
(snapshot[0]["reference_keys"] as Array).append("mutated")
|
||||||
|
var fresh_snapshot: Array = registry.call("diagnostic_snapshot")
|
||||||
|
_expect_string_array(
|
||||||
|
fresh_snapshot[0]["reference_keys"],
|
||||||
|
["tile-a", "tile-c"],
|
||||||
|
"diagnostics detached",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var loader_source := _read_text(LOADER_PATH, failures)
|
||||||
|
var registry_source := _read_text(REGISTRY_PATH, failures)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("WMO_PLACEMENT_REGISTRY_SCRIPT.new()"),
|
||||||
|
"loader composes registry",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(loader_source.contains("_wmo_placement_registry.add_reference("), "loader delegates add", failures)
|
||||||
|
_expect_true(loader_source.contains("_wmo_placement_registry.release_reference("), "loader delegates release", failures)
|
||||||
|
_expect_true(loader_source.contains("_wmo_placement_registry.clear()"), "loader delegates clear", failures)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("_wmo_instance_nodes_by_unique_key"),
|
||||||
|
"loader retains node map",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(loader_source.contains("var _wmo_registry"), "legacy mixed registry removed", failures)
|
||||||
|
for forbidden_token in ["extends Node", "Node3D", "Resource", "RID", "Thread", "Mutex", "Semaphore"]:
|
||||||
|
_expect_false(
|
||||||
|
registry_source.contains(forbidden_token),
|
||||||
|
"registry omits %s ownership" % forbidden_token,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var registry: RefCounted = REGISTRY_SCRIPT.new()
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for iteration in range(100):
|
||||||
|
for placement_index in range(256):
|
||||||
|
var placement_key := "uid:%d" % placement_index
|
||||||
|
registry.call("add_reference", placement_key, "tile-a")
|
||||||
|
registry.call("add_reference", placement_key, "tile-b")
|
||||||
|
for placement_index in range(256):
|
||||||
|
var placement_key := "uid:%d" % placement_index
|
||||||
|
registry.call("release_reference", placement_key, "tile-a")
|
||||||
|
registry.call("release_reference", placement_key, "tile-b")
|
||||||
|
_expect_equal(int(registry.call("active_count")), 0, "timing cycle %d clears" % iteration, failures)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "100 shared-reference cycles remain bounded", failures)
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _read_text(path: String, failures: Array[String]) -> String:
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
failures.append("cannot open %s" % path)
|
||||||
|
return ""
|
||||||
|
return file.get_as_text()
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(
|
||||||
|
actual_value: String,
|
||||||
|
expected_value: String,
|
||||||
|
label: String,
|
||||||
|
failures: Array[String]
|
||||||
|
) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_array(
|
||||||
|
actual_values: Array,
|
||||||
|
expected_values: Array,
|
||||||
|
label: String,
|
||||||
|
failures: Array[String]
|
||||||
|
) -> void:
|
||||||
|
if actual_values != expected_values:
|
||||||
|
failures.append("%s expected %s, got %s" % [label, expected_values, actual_values])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not actual_value:
|
||||||
|
failures.append("%s expected true" % label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual_value:
|
||||||
|
failures.append("%s expected false" % label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dprfwayji363v
|
||||||
Reference in New Issue
Block a user