Compare commits

..

5 Commits

12 changed files with 678 additions and 55 deletions
+4
View File
@@ -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 по кадрам;
@@ -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.
+1
View File
@@ -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) |
+207
View File
@@ -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)
+15 -10
View File
@@ -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)
+14 -4
View File
@@ -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 |
+69
View File
@@ -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
+46 -39
View File
@@ -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
+203
View File
@@ -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
+11 -2
View File
@@ -73,6 +73,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`; WMO placement resolver passed `cases=9 iterations=20000 elapsed_ms=27.919`;
post-merge renderer materials, shutdown, internal-access `42`, facade, post-merge renderer materials, shutdown, internal-access `42`, facade,
`7/7` manifest, documentation and coordination gates remained green. `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 M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - 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 WMO cache-key normalization, positive MODF identity with tile/index fallback
and world position/Euler/unclamped-scale transforms are unchanged across and world position/Euler/unclamped-scale transforms are unchanged across
lightweight render, cached-scene and live-prototype paths. 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. 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/ - Changed files: scene-free scheduler and verifier; streamer budget composition/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes; 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, three transform consumers, stateless M2 placement grouper and worker adapter,
stateless M2 build batch planner and loader cursor adapter, stateless M2 build batch planner and loader cursor adapter,
stateless WMO placement resolver and seven cache/identity/transform adapters, 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. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: queue storage length and worker concurrency/stale-result logic - Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt 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 asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
and boundary traversal p95/p99 remain pending; path grouping is not yet and boundary traversal p95/p99 remain pending; path grouping is not yet
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
build-job Dictionaries remain in the loader; WMO registry/build/resource state build-job Dictionaries remain in the loader; WMO build/resource state and
and asset-backed placement/portal/material evidence remain pending; asset-backed placement/portal/material evidence remain pending;
M03 still needs further terrain/M2/WMO/liquid service extraction M03 still needs further terrain/M2/WMO/liquid service extraction
and dependency/performance acceptance. and dependency/performance acceptance.