Compare commits
5 Commits
5aa40d6afb
...
1e73e597f8
| Author | SHA1 | Date | |
|---|---|---|---|
| 1e73e597f8 | |||
| f9e5a4f2e0 | |||
| 16053b46ca | |||
| c5900368d4 | |||
| 79bc4e6ffc |
@@ -27,6 +27,8 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
|
||||
- `src/render/world_render_facade.gd` - стабильная граница runtime/tools для focus и metrics.
|
||||
- `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.
|
||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||
@@ -394,6 +396,8 @@ Known limits:
|
||||
- `WMOBuilder` строит group meshes;
|
||||
- runtime registry дедуплицирует WMO по `unique_id`;
|
||||
- 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` во время движения;
|
||||
- добавлен lightweight render-cache path для WMO groups;
|
||||
- `wmo_render_group_ops_per_tick` ограничивает подключение WMO groups по кадрам;
|
||||
|
||||
@@ -0,0 +1,106 @@
|
||||
# M03-RND-WMO-PLACEMENT-REGISTRY-001 — WMO placement registry
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-PLACEMENT-REGISTRY-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-wmo-placement-registry`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract WMO placement-key to tile-reference ownership from the loader while
|
||||
leaving every Node and renderer-resource lifecycle in the loader.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change placement identity rules or WMO transforms.
|
||||
- Move Node maps, world roots, tile state, jobs, queues or cancellation.
|
||||
- Move cache/load requests, material refresh or render-group building.
|
||||
- Change duplicate ordering, global-WMO lifetime or build budgets.
|
||||
- Claim asset-backed WMO fidelity or performance acceptance.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_placement_registry.gd`,
|
||||
`src/tools/verify_wmo_placement_registry.gd`,
|
||||
`docs/modules/wmo-placement-registry.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
|
||||
`docs/modules/README.md`, `RENDER.md`, `targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated ADT/WMO resources, caches
|
||||
and proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Input: resolved WMO placement key and tile/global reference key
|
||||
- `add_reference` creates a key/ref set or idempotently retains an existing ref
|
||||
- `release_reference` erases only the supplied ref and returns true only when the
|
||||
final ref removes the placement key
|
||||
- `contains`, `active_count`, detached sorted diagnostics and idempotent `clear`
|
||||
- Loader retains key→Node mapping and reacts to final release by cancelling the
|
||||
render build and queue-freeing the corresponding Node
|
||||
- Cache/schema/coordinate/profile versions: unchanged
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: current master `5aa40d6`, `WmoPlacementResolver` identity contract
|
||||
- Blocks: WMO build/resource state decomposition
|
||||
- External state: none; tests use synthetic placement/tile keys
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated add/shared/idempotent/non-owner/final/global/clear/
|
||||
diagnostics/source/timing verifier; resolver/material/shutdown plus M2/terrain/
|
||||
facade/internal-access/manifest/scheduler/streaming/coordinate/docs/coord gates
|
||||
- Fidelity evidence: exact shared-ref and final-release behavior preserved
|
||||
- Performance budget: dictionary membership over active WMO keys/refs; no Node,
|
||||
Resource, RID, task, queue, mutex, cache or file access
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline registry API documentation
|
||||
- New module spec with inputs/outputs, data-flow, lifecycle/state, ownership,
|
||||
failure/recovery, performance, fidelity limitations and source map
|
||||
- Placement resolver, renderer/module registry and `RENDER.md` updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name: `WmoPlacementRegistry`
|
||||
- Simplest solution: one private key-to-reference-set Dictionary
|
||||
- Rejected complexity: generic ref-count registry, signals, callbacks, Resources,
|
||||
weak refs or Node ownership inside the service
|
||||
- Unavoidable complexity: loader keeps a parallel key-to-Node map
|
||||
- Measured optimization evidence: bounded synthetic add/release loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: registry extracted; global/tile adapters migrated; Node/resource/job
|
||||
ownership retained in loader; contract, regressions, dry-run and docs passed
|
||||
- Next: WMO build/resource state extraction and asset-backed placement evidence
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-PLACEMENT-REGISTRY-001:c590036 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-PLACEMENT-REGISTRY-001:f9e5a4f -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `c590036`
|
||||
- Results: `verify_wmo_placement_registry.gd` PASS cases=10,
|
||||
iterations=100, elapsed_ms=53.941; 21 adjacent WMO/M2/terrain/facade/
|
||||
scheduler/streaming/coordinate regressions PASS; documentation and
|
||||
coordination gates PASS; render dry-run PASS 7/7.
|
||||
- Remaining risks: no proprietary asset corpus or original-client paired WMO
|
||||
capture; dry-run reports expected missing ADT/DBC/character assets; WMO
|
||||
build/resource state remains loader-owned.
|
||||
- Documentation updated: `docs/modules/wmo-placement-registry.md`,
|
||||
`docs/modules/wmo-placement-resolver.md`, `docs/modules/world-renderer.md`,
|
||||
`docs/modules/README.md`, `RENDER.md`; inline public API comments added.
|
||||
- Integration: accepted in master merge `f9e5a4f`; post-merge registry
|
||||
(`cases=10`, `iterations=100`, `elapsed_ms=54.178`), resolver, runtime-cache
|
||||
shutdown, facade, documentation and coordination gates passed.
|
||||
@@ -20,6 +20,7 @@
|
||||
| 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) |
|
||||
| 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) |
|
||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.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
|
||||
|
||||
- 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.
|
||||
- Create Nodes, Mesh/MultiMesh, materials or RIDs.
|
||||
- Implement portals, rooms, visibility, materials, doodads or WMO parity.
|
||||
@@ -34,7 +35,7 @@ flowchart LR
|
||||
Resolver --> Identity[Registry unique key]
|
||||
Resolver --> Transform[World Transform3D]
|
||||
CacheKey --> Cache[Loader WMO caches/requests]
|
||||
Identity --> Registry[Loader WMO registry]
|
||||
Identity --> Registry[WmoPlacementRegistry]
|
||||
Transform --> RenderRoot[Lightweight render root]
|
||||
Transform --> Scene[Cached scene 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 | 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 | `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 |
|
||||
|
||||
The resolver retains no source Dictionary, output or engine resource.
|
||||
@@ -91,7 +92,7 @@ and shutdown require no resolver operation.
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Resolver as WmoPlacementResolver
|
||||
participant Registry as Loader WMO registry/cache
|
||||
participant Registry as WmoPlacementRegistry
|
||||
participant Instance as Render/cached/live instance
|
||||
Loader->>Resolver: normalize_relative_path(path)
|
||||
Resolver-->>Loader: cache key
|
||||
@@ -105,8 +106,9 @@ sequenceDiagram
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The resolver owns only call-local scalar/value calculations.
|
||||
- The loader owns WMO registry entries/ref sets, cache/load-request state, build
|
||||
jobs/queues, resource fallback and cancellation.
|
||||
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||
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.
|
||||
- Pure calls are thread-safe; current consumers execute on the main thread.
|
||||
|
||||
@@ -152,8 +154,8 @@ in the facade/loader diagnostic snapshot.
|
||||
|
||||
## Extension points
|
||||
|
||||
- A later package may extract WMO registry ownership/ref release using these
|
||||
stable keys.
|
||||
- `WmoPlacementRegistry` consumes these stable keys; later packages may extract
|
||||
WMO build/resource state without changing the resolver.
|
||||
- Render-group build cursor and resource-selection state require separate
|
||||
lifecycle/cancellation contracts.
|
||||
|
||||
@@ -162,7 +164,8 @@ in the facade/loader diagnostic snapshot.
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| 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 |
|
||||
|
||||
## Known gaps and risks
|
||||
@@ -177,11 +180,13 @@ in the facade/loader diagnostic snapshot.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `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 |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`wmo-placement-registry.md`](wmo-placement-registry.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-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 |
|
||||
|
||||
## Purpose
|
||||
@@ -56,6 +56,8 @@ flowchart LR
|
||||
M2Batch --> Loader
|
||||
Loader --> WmoPlacement[WmoPlacementResolver]
|
||||
WmoPlacement --> Loader
|
||||
Loader --> WmoRegistry[WmoPlacementRegistry]
|
||||
WmoRegistry --> Loader
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
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 |
|
||||
| `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 |
|
||||
| `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,
|
||||
внутренние 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 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 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 | 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 |
|
||||
@@ -205,7 +209,8 @@ flowchart TD
|
||||
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
||||
M2Batch --> M2
|
||||
R --> WmoPlacement[WmoPlacementResolver]
|
||||
WmoPlacement --> WMO
|
||||
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
|
||||
WmoRegistry --> WMO
|
||||
R --> B[RenderBudgetScheduler permits]
|
||||
B --> Terrain[Terrain attach/upgrade]
|
||||
B --> M2[M2 group/MultiMesh attach]
|
||||
@@ -328,7 +333,9 @@ sequenceDiagram
|
||||
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
||||
loader retains queue/resource transitions, cursor adoption and materialization.
|
||||
- `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;
|
||||
the facade never returns Mesh, Node, tile-state or queue references.
|
||||
- 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.
|
||||
- WMO placement contract: cache-path normalization, positive/fallback identity,
|
||||
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
|
||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||
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 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 |
|
||||
| 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 |
|
||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||
| 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(
|
||||
"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_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")
|
||||
@@ -286,7 +289,8 @@ var _m2_animated_scene_cache: Dictionary = {}
|
||||
var _m2_static_animation_cache: Dictionary = {}
|
||||
var _m2_missing_cache: Dictionary = {}
|
||||
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:
|
||||
@@ -1031,7 +1035,7 @@ func render_baseline_snapshot() -> Dictionary:
|
||||
},
|
||||
"cache_entries": _tile_mesh_cache.size(),
|
||||
"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_multimeshes,
|
||||
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:
|
||||
if not _wdt_has_global_wmo():
|
||||
return
|
||||
if _wmo_registry.has("global"):
|
||||
if _wmo_placement_registry.contains("global"):
|
||||
return
|
||||
|
||||
_ensure_world_wmo_root()
|
||||
@@ -3387,10 +3391,7 @@ func _place_global_wmo() -> void:
|
||||
|
||||
_world_wmo_root.add_child(instance)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry["global"] = {
|
||||
"node": instance,
|
||||
"refs": {"__global__": true},
|
||||
}
|
||||
_register_wmo_instance_reference("global", "__global__", instance)
|
||||
|
||||
|
||||
func _sync_detail_assets(state: Dictionary, focus_pos: Vector3) -> Dictionary:
|
||||
@@ -3566,10 +3567,8 @@ func _process_wmo_build_jobs() -> void:
|
||||
tile_key,
|
||||
index
|
||||
)
|
||||
if _wmo_registry.has(unique_key):
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs[tile_key] = true
|
||||
if _wmo_placement_registry.contains(unique_key):
|
||||
_wmo_placement_registry.add_reference(unique_key, tile_key)
|
||||
refs.append(unique_key)
|
||||
else:
|
||||
var rel_path: String = str(wmo_names[name_id]).replace("\\", "/")
|
||||
@@ -3579,10 +3578,7 @@ func _process_wmo_build_jobs() -> void:
|
||||
if instance != null:
|
||||
_world_wmo_root.add_child(instance)
|
||||
_set_editor_owner_recursive(instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(unique_key, tile_key, instance)
|
||||
refs.append(unique_key)
|
||||
_start_wmo_render_build(unique_key, instance, render_resource)
|
||||
else:
|
||||
@@ -3602,10 +3598,11 @@ func _process_wmo_build_jobs() -> void:
|
||||
_prepare_runtime_wmo_instance(live_instance)
|
||||
_world_wmo_root.add_child(live_instance)
|
||||
_set_editor_owner_recursive(live_instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": live_instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(
|
||||
unique_key,
|
||||
tile_key,
|
||||
live_instance
|
||||
)
|
||||
refs.append(unique_key)
|
||||
else:
|
||||
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)
|
||||
_world_wmo_root.add_child(scene_instance)
|
||||
_set_editor_owner_recursive(scene_instance)
|
||||
_wmo_registry[unique_key] = {
|
||||
"node": scene_instance,
|
||||
"refs": {tile_key: true},
|
||||
}
|
||||
_register_wmo_instance_reference(
|
||||
unique_key,
|
||||
tile_key,
|
||||
scene_instance
|
||||
)
|
||||
refs.append(unique_key)
|
||||
if advance_job:
|
||||
job["refs"] = refs
|
||||
@@ -3994,6 +3992,17 @@ func _cancel_wmo_build_job(tile_key: String) -> void:
|
||||
_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:
|
||||
var refs: Array = state.get("wmo_refs", [])
|
||||
var tile_key: String = String(state.get("key", ""))
|
||||
@@ -4003,18 +4012,16 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
state["wmo_building"] = false
|
||||
return
|
||||
|
||||
for unique_key in refs:
|
||||
if not _wmo_registry.has(unique_key):
|
||||
for unique_key_variant in refs:
|
||||
var unique_key := String(unique_key_variant)
|
||||
if not _wmo_placement_registry.contains(unique_key):
|
||||
continue
|
||||
var entry: Dictionary = _wmo_registry[unique_key]
|
||||
var entry_refs: Dictionary = entry["refs"]
|
||||
entry_refs.erase(tile_key)
|
||||
if entry_refs.is_empty():
|
||||
if _wmo_placement_registry.release_reference(unique_key, tile_key):
|
||||
_cancel_wmo_render_build_job(String(unique_key))
|
||||
var node: Node = entry.get("node", null)
|
||||
if node:
|
||||
var node: Node = _wmo_instance_nodes_by_unique_key.get(unique_key, null)
|
||||
if node != null and is_instance_valid(node):
|
||||
node.queue_free()
|
||||
_wmo_registry.erase(unique_key)
|
||||
_wmo_instance_nodes_by_unique_key.erase(unique_key)
|
||||
|
||||
state["wmo_refs"] = []
|
||||
state["wmo_building"] = false
|
||||
@@ -4023,14 +4030,14 @@ func _unregister_tile_wmos(state: Dictionary) -> void:
|
||||
func _clear_wmo_registry() -> void:
|
||||
_wmo_render_build_jobs.clear()
|
||||
_wmo_render_build_queue.clear()
|
||||
for entry_variant in _wmo_registry.values():
|
||||
if not (entry_variant is Dictionary):
|
||||
for node_variant in _wmo_instance_nodes_by_unique_key.values():
|
||||
if not (node_variant is Node):
|
||||
continue
|
||||
var entry: Dictionary = entry_variant
|
||||
var node: Node = entry.get("node", null)
|
||||
if node and is_instance_valid(node):
|
||||
var node := node_variant as Node
|
||||
if is_instance_valid(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):
|
||||
_queue_free_streamed_node(_world_wmo_root)
|
||||
_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
|
||||
@@ -73,6 +73,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`;
|
||||
post-merge renderer materials, shutdown, internal-access `42`, facade,
|
||||
`7/7` manifest, documentation and coordination gates remained green.
|
||||
WMO placement registry passed `cases=10 iterations=100 elapsed_ms=53.941`;
|
||||
all 21 adjacent WMO/M2/terrain/facade/scheduler/streaming/coordinate
|
||||
regressions and the `7/7` checkpoint dry-run passed; post-merge registry
|
||||
(`54.178ms`), resolver, shutdown, facade, documentation and coordination
|
||||
gates remained green.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -104,6 +109,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
WMO cache-key normalization, positive MODF identity with tile/index fallback
|
||||
and world position/Euler/unclamped-scale transforms are unchanged across
|
||||
lightweight render, cached-scene and live-prototype paths.
|
||||
WMO first/idempotent/shared/non-owner/final/global reference behavior is
|
||||
unchanged; the extracted registry owns only String reference sets while the
|
||||
loader retains the parallel key-to-Node map and final-release cancellation/free.
|
||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||
@@ -118,6 +126,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
three transform consumers, stateless M2 placement grouper and worker adapter,
|
||||
stateless M2 build batch planner and loader cursor adapter,
|
||||
stateless WMO placement resolver and seven cache/identity/transform adapters,
|
||||
stateful WMO placement registry and global/tile add/release/clear adapters,
|
||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||
@@ -132,7 +141,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
||||
and boundary traversal p95/p99 remain pending; path grouping is not yet
|
||||
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
|
||||
build-job Dictionaries remain in the loader; WMO registry/build/resource state
|
||||
and asset-backed placement/portal/material evidence remain pending;
|
||||
build-job Dictionaries remain in the loader; WMO build/resource state and
|
||||
asset-backed placement/portal/material evidence remain pending;
|
||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
||||
and dependency/performance acceptance.
|
||||
|
||||
Reference in New Issue
Block a user