render: extract WMO scene instance factory
Work-Package: M03-RND-WMO-SCENE-INSTANCE-FACTORY-001 Agent: sindo-main-codex Tests: 65/66 headless verifiers; proprietary ADT probe unavailable; checkpoint dry-run 7/7; docs and coordination passed Fidelity: valid cached/live naming, placement, validation and Resource identity preserved; invalid-root leak fixed
This commit is contained in:
@@ -1491,6 +1491,18 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
queues and permits remain loader-owned. Synthetic traversal timing is not
|
queues and permits remain loader-owned. Synthetic traversal timing is not
|
||||||
private-asset visual, leak, GPU or p95/p99 evidence.
|
private-asset visual, leak, GPU or p95/p99 evidence.
|
||||||
|
|
||||||
|
## 2026-08-01 WMO Scene Instance Factory Extraction
|
||||||
|
|
||||||
|
- `WmoSceneInstanceFactory` now owns cached PackedScene instantiation/currentness
|
||||||
|
validation and live-prototype duplication with shared basename/placement rules.
|
||||||
|
- Cached validation still precedes placement; live duplicates still skip the
|
||||||
|
scene-cache validator. Accepted descendant Resources retain exact identity.
|
||||||
|
- Invalid non-Node3D cached roots are now freed synchronously, closing an
|
||||||
|
error-path lifetime leak that normal scene-cache admission already prevents.
|
||||||
|
- Source lookup, ResourceLoader, runtime preparation, attachment, registry,
|
||||||
|
queues and permits remain loader-owned. Synthetic factory timing is not
|
||||||
|
private-asset visual, leak/GPU or p95/p99 evidence.
|
||||||
|
|
||||||
## Practical Rule For Future Work
|
## Practical Rule For Future Work
|
||||||
|
|
||||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||||
|
|||||||
@@ -52,6 +52,7 @@
|
|||||||
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
|
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
|
||||||
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
|
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
|
||||||
| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) |
|
| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) |
|
||||||
|
| WMO scene instance factory | Implemented extraction | [`wmo-scene-instance-factory.md`](wmo-scene-instance-factory.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) |
|
||||||
|
|||||||
@@ -7,7 +7,7 @@
|
|||||||
| Status | Implemented |
|
| Status | Implemented |
|
||||||
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
|
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
|
||||||
| Owners | Pure WMO cache-key, placement-identity and world-transform rules |
|
| Owners | Pure WMO cache-key, placement-identity and world-transform rules |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
|
||||||
| Profiles/capabilities | Existing ADT/WDT WMO placement paths |
|
| Profiles/capabilities | Existing ADT/WDT WMO placement paths |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -31,14 +31,17 @@ live-prototype instance paths.
|
|||||||
flowchart LR
|
flowchart LR
|
||||||
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
|
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
|
||||||
Loader --> Resolver[WmoPlacementResolver]
|
Loader --> Resolver[WmoPlacementResolver]
|
||||||
|
Loader --> Factory[WmoSceneInstanceFactory]
|
||||||
|
Factory --> Resolver
|
||||||
Resolver --> CacheKey[Normalized cache key]
|
Resolver --> CacheKey[Normalized cache key]
|
||||||
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[WmoPlacementRegistry]
|
Identity --> Registry[WmoPlacementRegistry]
|
||||||
Transform --> RenderRoot[Lightweight render root]
|
Transform --> RenderRoot[Lightweight render root]
|
||||||
Transform --> Scene[Cached scene instance]
|
Transform --> Factory
|
||||||
Transform --> Live[Live prototype instance]
|
Factory --> Scene[Cached scene instance]
|
||||||
|
Factory --> Live[Live prototype instance]
|
||||||
```
|
```
|
||||||
|
|
||||||
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
|
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
|
||||||
@@ -62,7 +65,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
|
|||||||
| 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 | `WmoPlacementRegistry` and loader 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 | Lightweight render-root adapter and cached/live instance factory | 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,6 +94,7 @@ and shutdown require no resolver operation.
|
|||||||
```mermaid
|
```mermaid
|
||||||
sequenceDiagram
|
sequenceDiagram
|
||||||
participant Loader as StreamingWorldLoader
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Factory as WmoSceneInstanceFactory
|
||||||
participant Resolver as WmoPlacementResolver
|
participant Resolver as WmoPlacementResolver
|
||||||
participant Registry as WmoPlacementRegistry
|
participant Registry as WmoPlacementRegistry
|
||||||
participant Instance as Render/cached/live instance
|
participant Instance as Render/cached/live instance
|
||||||
@@ -98,9 +102,15 @@ sequenceDiagram
|
|||||||
Resolver-->>Loader: cache key
|
Resolver-->>Loader: cache key
|
||||||
Loader->>Resolver: resolve_unique_key(placement, tile, index)
|
Loader->>Resolver: resolve_unique_key(placement, tile, index)
|
||||||
Resolver-->>Registry: identity adopted by loader
|
Resolver-->>Registry: identity adopted by loader
|
||||||
|
alt lightweight render root
|
||||||
Loader->>Resolver: resolve_world_transform(placement)
|
Loader->>Resolver: resolve_world_transform(placement)
|
||||||
Resolver-->>Loader: value Transform3D
|
Resolver-->>Loader: value Transform3D
|
||||||
Loader->>Instance: assign transform and attach/build
|
else cached/live instance
|
||||||
|
Loader->>Factory: create with placement
|
||||||
|
Factory->>Resolver: resolve_world_transform(placement)
|
||||||
|
Resolver-->>Factory: value Transform3D
|
||||||
|
end
|
||||||
|
Loader->>Instance: attach/build prepared instance
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ownership, threading and resources
|
## Ownership, threading and resources
|
||||||
@@ -109,7 +119,9 @@ sequenceDiagram
|
|||||||
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||||
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
|
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
|
||||||
cancellation.
|
cancellation.
|
||||||
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
|
- `WmoSceneInstanceFactory` owns detached cached/live candidate roots until
|
||||||
|
rejection or transfer; the loader/builders own attachment and remaining
|
||||||
|
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.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## Errors, cancellation and recovery
|
||||||
|
|||||||
@@ -0,0 +1,219 @@
|
|||||||
|
# WMO Scene Instance Factory
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented |
|
||||||
|
| Target | M03 Renderer Facade and Safe Extraction |
|
||||||
|
| Work package | `M03-RND-WMO-SCENE-INSTANCE-FACTORY-001` |
|
||||||
|
| Owner | Render |
|
||||||
|
| Last verified | 2026-08-01 |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
`WmoSceneInstanceFactory` creates detached WMO `Node3D` roots from validated
|
||||||
|
cached `PackedScene` resources or live-built prototypes. It owns cache-currentness
|
||||||
|
validation, basename assignment and canonical placement-resolver delegation.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- look up/load/cache PackedScenes or build live WMO prototypes;
|
||||||
|
- apply Mesh/material/occluder/shadow runtime preparation;
|
||||||
|
- attach nodes, assign Editor ownership or manage placement references;
|
||||||
|
- own queues, permits, cache versions or world teardown;
|
||||||
|
- define WMO placement formulas or scene-cache currentness rules.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
The loader selects cached versus live sources. The factory creates a detached
|
||||||
|
instance and applies identity/placement. `WmoRuntimeScenePreparer` then applies
|
||||||
|
path-specific presentation policy before the loader attaches/registers the root.
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Cache[WMO PackedScene cache] --> Loader[StreamingWorldLoader]
|
||||||
|
Prototype[Live WMO prototype cache/build] --> Loader
|
||||||
|
Loader --> Factory[WmoSceneInstanceFactory]
|
||||||
|
Validator[WMOBuilder scene-cache validator] --> Factory
|
||||||
|
Resolver[WmoPlacementResolver] --> Factory
|
||||||
|
Factory --> Detached[Detached WMO Node3D]
|
||||||
|
Detached --> Preparer[WmoRuntimeScenePreparer]
|
||||||
|
Preparer --> Loader
|
||||||
|
Loader --> Scene[Attachment and placement registry]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Role | Thread/lifetime | Failure behavior |
|
||||||
|
|---|---|---|---|
|
||||||
|
| `is_cached_node_current(node)` | Delegate one Node to the injected cache validator | Renderer main thread; no retention | Null/missing validator returns false |
|
||||||
|
| `instantiate_cached_scene(relative_path, scene, placement)` | Instantiate, type-check, validate, name and place a cached scene | Renderer main thread; detached result caller-owned | Invalid input/root/stale/dependency returns null; created rejected roots freed |
|
||||||
|
| `duplicate_live_prototype(relative_path, prototype, placement)` | Duplicate, name and place a live prototype | Renderer main thread; detached result caller-owned | Null/missing resolver/unexpected duplicate returns null |
|
||||||
|
|
||||||
|
The cached path validates before placement. The live path deliberately skips the
|
||||||
|
scene-cache validator. Both paths use `get_file().get_basename()` and the exact
|
||||||
|
`WmoPlacementResolver.resolve_world_transform` result.
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | Cached `PackedScene` or live prototype `Node3D` | Loader cache/build adapters | Factory | Borrowed Resource/Node | One main-thread call |
|
||||||
|
| Input | Relative WMO path and placement Dictionary | Loader placement job | Factory | Borrowed values | One call |
|
||||||
|
| Internal | Candidate root | PackedScene instantiate/prototype duplicate | Validator/factory | Factory-owned until accepted | One call |
|
||||||
|
| Output | Detached named/placed `Node3D` | Factory | Runtime scene preparer/loader | Ownership transfers to caller | Until attachment/release |
|
||||||
|
| Output | Currentness bool | Validator via factory | Loader cache admission | Value | Immediate |
|
||||||
|
|
||||||
|
Side effects are limited to scene instantiation/duplication, candidate name and
|
||||||
|
transform mutation, and synchronous free of rejected candidates. No attachment,
|
||||||
|
filesystem, ResourceLoader, worker, RID, queue, cache or Editor-owner mutation.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Source[PackedScene or live prototype] --> Create{Cached or live?}
|
||||||
|
Create -->|cached| Instantiate[PackedScene.instantiate]
|
||||||
|
Create -->|live| Duplicate[prototype.duplicate]
|
||||||
|
Instantiate --> Type{Node3D?}
|
||||||
|
Duplicate --> Type
|
||||||
|
Type -->|no| Free[Free created candidate and return null]
|
||||||
|
Type -->|yes cached| Current{Cache current?}
|
||||||
|
Type -->|yes live| Identity[Apply basename]
|
||||||
|
Current -->|no| Free
|
||||||
|
Current -->|yes| Identity
|
||||||
|
Identity --> Resolve[WmoPlacementResolver]
|
||||||
|
Resolve --> Return[Return detached Node3D]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant Loader as StreamingWorldLoader
|
||||||
|
participant Factory as WmoSceneInstanceFactory
|
||||||
|
participant Validator as WMOBuilder
|
||||||
|
participant Resolver as WmoPlacementResolver
|
||||||
|
alt cached source
|
||||||
|
Loader->>Factory: instantiate_cached_scene(path, scene, placement)
|
||||||
|
Factory->>Factory: instantiate and require Node3D
|
||||||
|
Factory->>Validator: is_scene_cache_current(root)
|
||||||
|
Validator-->>Factory: current/stale
|
||||||
|
else live source
|
||||||
|
Loader->>Factory: duplicate_live_prototype(path, prototype, placement)
|
||||||
|
Factory->>Factory: duplicate and require Node3D
|
||||||
|
end
|
||||||
|
Factory->>Factory: assign basename
|
||||||
|
Factory->>Resolver: resolve_world_transform(placement)
|
||||||
|
Resolver-->>Factory: exact Transform3D
|
||||||
|
Factory-->>Loader: detached Node3D or null
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Factory[WmoSceneInstanceFactory]
|
||||||
|
Factory --> Validator[Injected WMO scene-cache validator]
|
||||||
|
Factory --> Resolver[Injected WmoPlacementResolver]
|
||||||
|
Factory --> Engine[PackedScene / Node3D / Transform3D]
|
||||||
|
Factory -. no dependency .-> Preparation[WmoRuntimeScenePreparer]
|
||||||
|
Factory -. no dependency .-> IO[ResourceLoader / FileAccess]
|
||||||
|
Factory -. no dependency .-> Queue[WMO queues / scheduler]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Calls are renderer-main-thread only because PackedScene/Node APIs mutate.
|
||||||
|
- The source scene/prototype remains caller/cache-owned.
|
||||||
|
- The factory owns a newly created root until rejection or successful return.
|
||||||
|
- Successful return transfers detached-root ownership to the caller.
|
||||||
|
- Descendant Mesh/Material Resources retain engine duplicate/instantiate identity.
|
||||||
|
- The factory retains only injected stateless dependencies, never Nodes/Resources.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure/state | Detection | Behavior | Recovery |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Null scene/prototype | Guard | Return null without allocation | Correct caller source |
|
||||||
|
| Missing validator | Currentness guard | Cached candidate rejected/freed | Fix composition |
|
||||||
|
| Missing resolver | Guard before creation | Return null without allocation | Fix composition |
|
||||||
|
| Non-Node3D root | Runtime type check | Free candidate and return null | Rebuild invalid cache/source |
|
||||||
|
| Stale cached root | Injected validator | Free candidate; skip placement | Rebuild cache/current metadata |
|
||||||
|
| Placement cancellation | Loader lifecycle | Detached/attached result released by caller | Existing retry path |
|
||||||
|
| Shutdown | No retained candidates | Nothing to drain | Existing loader teardown |
|
||||||
|
|
||||||
|
The non-Node3D cached rejection now frees the created invalid root synchronously.
|
||||||
|
Normal admitted caches already enforce Node3D through the scene finalizer, so this
|
||||||
|
closes an error-path lifetime leak without changing valid rendered output.
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
No new settings. Cache-currentness rules belong to the injected WMOBuilder
|
||||||
|
boundary; placement formulas belong to `WmoPlacementResolver`.
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
No format/version change and no rebake. The factory reads no files and writes no
|
||||||
|
metadata. Existing cache validator version policy remains authoritative.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
The factory emits no logs or metrics. Loader cache/placement metrics and
|
||||||
|
synthetic rejection contracts remain the diagnostic surfaces.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_wmo_scene_instance_factory.gd` covers cached validation-before-placement,
|
||||||
|
exact accepted root/descendant Resource identity, stale-root free, non-Node3D
|
||||||
|
rejection, live validator suppression, detached ownership, dependencies,
|
||||||
|
basename/Transform3D application, source boundaries and 1,000 duplicates.
|
||||||
|
- Adjacent scene finalizer, placement resolver, runtime preparer, shutdown and
|
||||||
|
checkpoint regressions protect lifecycle and visible output.
|
||||||
|
- Fidelity evidence is behavior-preserving extraction for valid inputs. The
|
||||||
|
invalid non-Node3D free is a lifetime fix, not a visual 3.3.5a change.
|
||||||
|
|
||||||
|
The synthetic budget requires 1,000 simple live duplicates in under one second.
|
||||||
|
Asset-backed CPU/GPU p95/p99 and long-traversal evidence remain pending.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Asset-backed cached/live instances can compare placement and lifetime without
|
||||||
|
changing the factory API.
|
||||||
|
- New source kinds should be separate explicit methods only when their validation
|
||||||
|
and identity semantics differ materially.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Cached WMO instantiation | Implemented extraction | Type/currentness/name/placement/lifetime contract | Serialized asset-backed cache corpus pending |
|
||||||
|
| Live prototype duplication | Implemented extraction | Identity/name/placement/validator-suppression contract | Asset-backed traversal/leak evidence pending |
|
||||||
|
| Runtime preparation | Separate implemented service | Runtime scene preparer regression | Visual/GPU p95/p99 pending |
|
||||||
|
| Attachment/registry lifetime | Loader-owned | Existing WMO placement/shutdown regressions | Further orchestration extraction pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Scene instantiation/duplication remains synchronous main-thread work.
|
||||||
|
- No private WMO corpus, portal/room behavior, long traversal, leak/GPU timing or
|
||||||
|
paired original-client capture is included.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live creation, validation, identity and placement |
|
||||||
|
| `src/render/wmo/wmo_placement_resolver.gd` | Canonical WMO placement Transform3D |
|
||||||
|
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Post-factory cached/live render preparation |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Source selection, cache/prototype lookup, attachment and lifetime |
|
||||||
|
| `src/tools/verify_wmo_scene_instance_factory.gd` | Synthetic type/identity/lifetime/boundary/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
|
||||||
|
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
|
||||||
|
- [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.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)
|
||||||
@@ -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-runtime-scene-preparer`, 2026-08-01 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
@@ -74,6 +74,8 @@ flowchart LR
|
|||||||
WmoGroupMaterializer --> Scene
|
WmoGroupMaterializer --> Scene
|
||||||
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
|
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
|
||||||
WmoScenePreparer --> Scene
|
WmoScenePreparer --> Scene
|
||||||
|
Loader --> WmoInstanceFactory[WmoSceneInstanceFactory]
|
||||||
|
WmoInstanceFactory --> WmoScenePreparer
|
||||||
Native --> Parsed[Parsed tile/model data]
|
Native --> Parsed[Parsed tile/model data]
|
||||||
Parsed --> Loader
|
Parsed --> Loader
|
||||||
Loader --> Scene[SceneTree nodes]
|
Loader --> Scene[SceneTree nodes]
|
||||||
@@ -170,6 +172,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
|
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
|
||||||
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
|
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
|
||||||
| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false |
|
| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false |
|
||||||
|
| `WmoSceneInstanceFactory.instantiate_cached_scene/duplicate_live_prototype` | Internal WMO instance-creation service | Validates/creates detached cached/live roots and applies shared name/placement | Renderer main thread; stateless after each call | Invalid/stale/dependency failure returns null and frees created rejects |
|
||||||
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
|
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
|
||||||
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
|
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
|
||||||
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
|
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
|
||||||
@@ -214,6 +217,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
|
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
|
||||||
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
|
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
|
||||||
| Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation |
|
| Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation |
|
||||||
|
| Internal WMO instance creation | Cached PackedScene or live prototype, path and placement | Loader / `WmoSceneInstanceFactory` | Runtime scene preparer and attachment adapter | Factory owns candidate until detached-root transfer | One main-thread creation |
|
||||||
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
|
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
|
||||||
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
||||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||||
@@ -425,6 +429,9 @@ sequenceDiagram
|
|||||||
creation, render settings and attachment without retaining engine objects.
|
creation, render settings and attachment without retaining engine objects.
|
||||||
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
|
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
|
||||||
shared direct-Occluders and recursive shadow policies for cached/live roots.
|
shared direct-Occluders and recursive shadow policies for cached/live roots.
|
||||||
|
`WmoSceneInstanceFactory` owns cached/live detached-root creation, cache
|
||||||
|
validation, basename and placement application; loader retains source lookup,
|
||||||
|
runtime preparation, attachment and lifetime.
|
||||||
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
|
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
|
||||||
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
|
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
|
||||||
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
|
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
|
||||||
@@ -576,6 +583,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- WMO runtime scene preparer contract: cached/live finalizer distinction,
|
- WMO runtime scene preparer contract: cached/live finalizer distinction,
|
||||||
exact Mesh traversal order, direct occluder removal, recursive shadow policy,
|
exact Mesh traversal order, direct occluder removal, recursive shadow policy,
|
||||||
ownership boundaries and bounded main-thread timing.
|
ownership boundaries and bounded main-thread timing.
|
||||||
|
- WMO scene instance factory contract: cached validation-before-placement,
|
||||||
|
stale/type rejection lifetime, live validator suppression, exact descendant
|
||||||
|
Resource identity, naming/placement and bounded main-thread timing.
|
||||||
- WMO render Resource cache contract: invalid/duplicate request rejection,
|
- WMO render Resource cache contract: invalid/duplicate request rejection,
|
||||||
validated/missing terminal transitions, transient/full reset, detached sorted
|
validated/missing terminal transitions, transient/full reset, detached sorted
|
||||||
diagnostics, loader-owned version validation and bounded timing.
|
diagnostics, loader-owned version validation and bounded timing.
|
||||||
@@ -638,6 +648,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending |
|
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending |
|
||||||
| WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
| WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
||||||
| WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
| WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
||||||
|
| WMO scene instance factory | Implemented extraction | Synthetic cached/live type/identity/lifetime/name/placement/source/timing contract | Serialized/asset-backed traversal/leak evidence pending |
|
||||||
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||||
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
|
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
|
||||||
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||||
@@ -722,6 +733,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
|
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
|
||||||
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
|
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
|
||||||
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy |
|
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy |
|
||||||
|
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live detached-root creation, validation, identity and placement |
|
||||||
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||||
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||||
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||||
@@ -761,6 +773,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
|
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
|
||||||
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
|
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
|
||||||
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression |
|
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression |
|
||||||
|
| `src/tools/verify_wmo_scene_instance_factory.gd` | WMO cached/live type/identity/lifetime/name/placement/boundary/timing regression |
|
||||||
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||||
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
class_name WmoSceneInstanceFactory
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Creates detached cached/live WMO Node3D instances with the existing cache
|
||||||
|
## validation, naming and world-placement rules. Runtime preparation is separate.
|
||||||
|
|
||||||
|
var _scene_cache_validator: Object
|
||||||
|
var _placement_resolver: Object
|
||||||
|
|
||||||
|
|
||||||
|
func _init(scene_cache_validator: Object, placement_resolver: Object) -> void:
|
||||||
|
_scene_cache_validator = scene_cache_validator
|
||||||
|
_placement_resolver = placement_resolver
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether [param node] satisfies the injected WMO scene-cache contract.
|
||||||
|
## Null nodes or missing validators return false without mutation.
|
||||||
|
func is_cached_node_current(node: Node) -> bool:
|
||||||
|
if node == null or _scene_cache_validator == null:
|
||||||
|
return false
|
||||||
|
return bool(_scene_cache_validator.call("is_scene_cache_current", node))
|
||||||
|
|
||||||
|
|
||||||
|
## Instantiates, validates, names and places a cached WMO PackedScene. Rejected
|
||||||
|
## instantiated roots are freed synchronously. The accepted detached Node3D is
|
||||||
|
## caller-owned and retains its exact descendant Resource identities.
|
||||||
|
func instantiate_cached_scene(
|
||||||
|
relative_path: String,
|
||||||
|
scene: PackedScene,
|
||||||
|
placement: Dictionary) -> Node3D:
|
||||||
|
if scene == null or _placement_resolver == null:
|
||||||
|
return null
|
||||||
|
var instantiated_root := scene.instantiate()
|
||||||
|
if not (instantiated_root is Node3D):
|
||||||
|
if instantiated_root != null:
|
||||||
|
instantiated_root.free()
|
||||||
|
return null
|
||||||
|
var instance := instantiated_root as Node3D
|
||||||
|
if not is_cached_node_current(instance):
|
||||||
|
instance.free()
|
||||||
|
return null
|
||||||
|
_apply_identity_and_placement(instance, relative_path, placement)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
## Duplicates, names and places a live-built WMO prototype. The detached result
|
||||||
|
## is caller-owned. Null inputs, missing placement composition or an unexpected
|
||||||
|
## non-Node3D duplicate return null; rejected duplicates are freed synchronously.
|
||||||
|
func duplicate_live_prototype(
|
||||||
|
relative_path: String,
|
||||||
|
prototype: Node3D,
|
||||||
|
placement: Dictionary) -> Node3D:
|
||||||
|
if prototype == null or _placement_resolver == null:
|
||||||
|
return null
|
||||||
|
var duplicated_root := prototype.duplicate()
|
||||||
|
if not (duplicated_root is Node3D):
|
||||||
|
if duplicated_root != null:
|
||||||
|
duplicated_root.free()
|
||||||
|
return null
|
||||||
|
var instance := duplicated_root as Node3D
|
||||||
|
_apply_identity_and_placement(instance, relative_path, placement)
|
||||||
|
return instance
|
||||||
|
|
||||||
|
|
||||||
|
func _apply_identity_and_placement(
|
||||||
|
instance: Node3D,
|
||||||
|
relative_path: String,
|
||||||
|
placement: Dictionary) -> void:
|
||||||
|
instance.name = relative_path.get_file().get_basename()
|
||||||
|
instance.transform = _placement_resolver.call(
|
||||||
|
"resolve_world_transform",
|
||||||
|
placement
|
||||||
|
) as Transform3D
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c13w66d7uaf2n
|
||||||
@@ -42,6 +42,9 @@ const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
|
|||||||
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
|
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
|
||||||
"res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
"res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
||||||
)
|
)
|
||||||
|
const WMO_SCENE_INSTANCE_FACTORY_SCRIPT := preload(
|
||||||
|
"res://src/render/wmo/wmo_scene_instance_factory.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_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")
|
||||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||||
@@ -393,6 +396,10 @@ var _wmo_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new(
|
|||||||
)
|
)
|
||||||
var _wmo_missing_cache: Dictionary = {}
|
var _wmo_missing_cache: Dictionary = {}
|
||||||
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
||||||
|
var _wmo_scene_instance_factory := WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new(
|
||||||
|
WMO_BUILDER_SCRIPT,
|
||||||
|
_wmo_placement_resolver
|
||||||
|
)
|
||||||
var _world_wmo_root: Node3D
|
var _world_wmo_root: Node3D
|
||||||
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||||
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
|
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
|
||||||
@@ -3692,7 +3699,13 @@ func _process_wmo_build_jobs() -> void:
|
|||||||
)
|
)
|
||||||
refs.append(unique_key)
|
refs.append(unique_key)
|
||||||
else:
|
else:
|
||||||
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
|
var scene_instance := (
|
||||||
|
_wmo_scene_instance_factory.instantiate_cached_scene(
|
||||||
|
rel_path,
|
||||||
|
scene,
|
||||||
|
placement
|
||||||
|
)
|
||||||
|
)
|
||||||
if scene_instance != null:
|
if scene_instance != null:
|
||||||
_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)
|
||||||
@@ -3920,26 +3933,6 @@ func _get_resource_file_size(path: String) -> int:
|
|||||||
return int(length)
|
return int(length)
|
||||||
|
|
||||||
|
|
||||||
func _is_wmo_node_cache_current(node: Node) -> bool:
|
|
||||||
if node == null:
|
|
||||||
return false
|
|
||||||
return bool(WMO_BUILDER_SCRIPT.is_scene_cache_current(node))
|
|
||||||
|
|
||||||
|
|
||||||
func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dictionary) -> Node3D:
|
|
||||||
if scene == null:
|
|
||||||
return null
|
|
||||||
var instance := scene.instantiate() as Node3D
|
|
||||||
if instance == null:
|
|
||||||
return null
|
|
||||||
if not _is_wmo_node_cache_current(instance):
|
|
||||||
instance.free()
|
|
||||||
return null
|
|
||||||
instance.name = rel_path.get_file().get_basename()
|
|
||||||
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
|
|
||||||
return instance
|
|
||||||
|
|
||||||
|
|
||||||
func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
|
func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
|
||||||
_wmo_runtime_scene_preparer.prepare_cached_instance(
|
_wmo_runtime_scene_preparer.prepare_cached_instance(
|
||||||
instance,
|
instance,
|
||||||
@@ -4632,11 +4625,15 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
|
|||||||
if prototype == null:
|
if prototype == null:
|
||||||
return null
|
return null
|
||||||
|
|
||||||
var instance := prototype.duplicate()
|
|
||||||
instance.name = rel_path.get_file().get_basename()
|
|
||||||
# WMOs are parented to _terrain_root, whose position absorbs the editor
|
# WMOs are parented to _terrain_root, whose position absorbs the editor
|
||||||
# offset — use world-space placement.pos directly (no tile_origin subtraction).
|
# offset — the factory applies world-space placement.pos directly.
|
||||||
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
|
var instance := _wmo_scene_instance_factory.duplicate_live_prototype(
|
||||||
|
rel_path,
|
||||||
|
prototype,
|
||||||
|
placement
|
||||||
|
)
|
||||||
|
if instance == null:
|
||||||
|
return null
|
||||||
_wmo_runtime_scene_preparer.prepare_live_instance(
|
_wmo_runtime_scene_preparer.prepare_live_instance(
|
||||||
instance,
|
instance,
|
||||||
enable_occlusion_culling,
|
enable_occlusion_culling,
|
||||||
@@ -4736,7 +4733,7 @@ func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
|
|||||||
var resource: Resource = load(cache_path)
|
var resource: Resource = load(cache_path)
|
||||||
if resource is PackedScene:
|
if resource is PackedScene:
|
||||||
var node = (resource as PackedScene).instantiate()
|
var node = (resource as PackedScene).instantiate()
|
||||||
if node is Node3D and _is_wmo_node_cache_current(node):
|
if node is Node3D and _wmo_scene_instance_factory.is_cached_node_current(node):
|
||||||
_wmo_prototype_cache[normalized_rel] = node as Node3D
|
_wmo_prototype_cache[normalized_rel] = node as Node3D
|
||||||
return node as Node3D
|
return node as Node3D
|
||||||
if node is Node:
|
if node is Node:
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ extends SceneTree
|
|||||||
|
|
||||||
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
|
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
|
||||||
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
|
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
|
||||||
|
const INSTANCE_FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
|
||||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
@@ -25,7 +26,7 @@ func _initialize() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
print(
|
print(
|
||||||
"WMO_PLACEMENT_RESOLVER PASS cases=9 iterations=20000 elapsed_ms=%.3f"
|
"WMO_PLACEMENT_RESOLVER PASS cases=10 iterations=20000 elapsed_ms=%.3f"
|
||||||
% elapsed_milliseconds
|
% elapsed_milliseconds
|
||||||
)
|
)
|
||||||
quit(0)
|
quit(0)
|
||||||
@@ -138,6 +139,7 @@ func _verify_node_property_equivalence(failures: Array[String]) -> void:
|
|||||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||||
var loader_source := _read_text(LOADER_PATH, failures)
|
var loader_source := _read_text(LOADER_PATH, failures)
|
||||||
var resolver_source := _read_text(RESOLVER_PATH, failures)
|
var resolver_source := _read_text(RESOLVER_PATH, failures)
|
||||||
|
var instance_factory_source := _read_text(INSTANCE_FACTORY_PATH, failures)
|
||||||
_expect_true(
|
_expect_true(
|
||||||
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
|
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
|
||||||
"loader composes resolver",
|
"loader composes resolver",
|
||||||
@@ -157,8 +159,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
|
|||||||
)
|
)
|
||||||
_expect_equal_int(
|
_expect_equal_int(
|
||||||
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
|
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
|
||||||
3,
|
1,
|
||||||
"three transform adapters",
|
"one direct lightweight render-root transform adapter",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
instance_factory_source.contains('"resolve_world_transform"'),
|
||||||
|
"cached/live instance factory delegates transform resolution",
|
||||||
failures
|
failures
|
||||||
)
|
)
|
||||||
_expect_true(
|
_expect_true(
|
||||||
|
|||||||
@@ -0,0 +1,294 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Asset-free WMO cached/live instance validation, identity, placement,
|
||||||
|
## lifetime, source-boundary and bounded-timing regression.
|
||||||
|
|
||||||
|
const FACTORY_SCRIPT := preload("res://src/render/wmo/wmo_scene_instance_factory.gd")
|
||||||
|
const FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
class FakeSceneCacheValidator extends RefCounted:
|
||||||
|
var is_current := true
|
||||||
|
var inspected_nodes: Array[Node] = []
|
||||||
|
var events: Array[String]
|
||||||
|
|
||||||
|
func _init(shared_events: Array[String] = []) -> void:
|
||||||
|
events = shared_events
|
||||||
|
|
||||||
|
func is_scene_cache_current(node: Node) -> bool:
|
||||||
|
inspected_nodes.append(node)
|
||||||
|
events.append("validate")
|
||||||
|
return is_current
|
||||||
|
|
||||||
|
|
||||||
|
class FakePlacementResolver extends RefCounted:
|
||||||
|
var resolved_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0))
|
||||||
|
var placements: Array[Dictionary] = []
|
||||||
|
var events: Array[String]
|
||||||
|
|
||||||
|
func _init(shared_events: Array[String] = []) -> void:
|
||||||
|
events = shared_events
|
||||||
|
|
||||||
|
func resolve_world_transform(placement: Dictionary) -> Transform3D:
|
||||||
|
placements.append(placement.duplicate(true))
|
||||||
|
events.append("resolve")
|
||||||
|
return resolved_transform
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_cached_scene_contract(failures)
|
||||||
|
_verify_stale_and_wrong_root_rejection(failures)
|
||||||
|
_verify_live_prototype_contract(failures)
|
||||||
|
_verify_invalid_composition(failures)
|
||||||
|
_verify_source_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("WMO_SCENE_INSTANCE_FACTORY: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"WMO_SCENE_INSTANCE_FACTORY PASS cases=41 iterations=1000 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_cached_scene_contract(failures: Array[String]) -> void:
|
||||||
|
var events: Array[String] = []
|
||||||
|
var validator := FakeSceneCacheValidator.new(events)
|
||||||
|
var resolver := FakePlacementResolver.new(events)
|
||||||
|
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||||
|
var fixture := _create_packed_wmo_scene()
|
||||||
|
var scene: PackedScene = fixture["scene"]
|
||||||
|
var source_mesh: Mesh = fixture["mesh"]
|
||||||
|
var placement := {"pos": Vector3(1.0, 2.0, 3.0), "rotation": Vector3.ZERO}
|
||||||
|
var instance: Node3D = factory.instantiate_cached_scene(
|
||||||
|
"World/Wmo/Stormwind/Keep.wmo",
|
||||||
|
scene,
|
||||||
|
placement
|
||||||
|
)
|
||||||
|
_expect_true(instance != null, "current cached scene accepted", failures)
|
||||||
|
_expect_true(instance.get_parent() == null, "cached result remains detached", failures)
|
||||||
|
_expect_string_equal(instance.name, "Keep", "cached basename applied", failures)
|
||||||
|
_expect_true(instance.transform == resolver.resolved_transform, "cached transform applied", failures)
|
||||||
|
_expect_equal(validator.inspected_nodes.size(), 1, "cached root validated once", failures)
|
||||||
|
_expect_same(validator.inspected_nodes[0], instance, "accepted exact root validated", failures)
|
||||||
|
_expect_true(resolver.placements == [placement], "cached placement forwarded", failures)
|
||||||
|
_expect_true(events == ["validate", "resolve"], "validate precedes placement", failures)
|
||||||
|
var mesh_child := instance.get_node_or_null("Group") as MeshInstance3D
|
||||||
|
_expect_true(mesh_child != null, "cached Mesh child retained", failures)
|
||||||
|
_expect_same(mesh_child.mesh, source_mesh, "cached descendant Mesh identity retained", failures)
|
||||||
|
_expect_true(factory.is_cached_node_current(instance), "public currentness delegates", failures)
|
||||||
|
instance.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_stale_and_wrong_root_rejection(failures: Array[String]) -> void:
|
||||||
|
var validator := FakeSceneCacheValidator.new()
|
||||||
|
validator.is_current = false
|
||||||
|
var resolver := FakePlacementResolver.new()
|
||||||
|
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||||
|
var fixture := _create_packed_wmo_scene()
|
||||||
|
_expect_true(
|
||||||
|
factory.instantiate_cached_scene("world/stale.wmo", fixture["scene"], {}) == null,
|
||||||
|
"stale cached scene rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(validator.inspected_nodes.size(), 1, "stale root validated once", failures)
|
||||||
|
_expect_false(
|
||||||
|
is_instance_valid(validator.inspected_nodes[0]),
|
||||||
|
"stale instantiated root freed",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(resolver.placements.size(), 0, "stale root skips placement", failures)
|
||||||
|
|
||||||
|
var control_root := Control.new()
|
||||||
|
var wrong_root_scene := PackedScene.new()
|
||||||
|
_expect_equal(wrong_root_scene.pack(control_root), OK, "wrong-root fixture packed", failures)
|
||||||
|
control_root.free()
|
||||||
|
validator.is_current = true
|
||||||
|
_expect_true(
|
||||||
|
factory.instantiate_cached_scene("world/control.wmo", wrong_root_scene, {}) == null,
|
||||||
|
"non-Node3D cached root rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(validator.inspected_nodes.size(), 1, "wrong root skips validator", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_live_prototype_contract(failures: Array[String]) -> void:
|
||||||
|
var validator := FakeSceneCacheValidator.new()
|
||||||
|
var resolver := FakePlacementResolver.new()
|
||||||
|
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||||
|
var prototype := Node3D.new()
|
||||||
|
prototype.name = "Prototype"
|
||||||
|
var source_mesh := ArrayMesh.new()
|
||||||
|
var mesh_child := MeshInstance3D.new()
|
||||||
|
mesh_child.name = "Group"
|
||||||
|
mesh_child.mesh = source_mesh
|
||||||
|
prototype.add_child(mesh_child)
|
||||||
|
var placement := {"pos": Vector3(7.0, 8.0, 9.0)}
|
||||||
|
var instance: Node3D = factory.duplicate_live_prototype(
|
||||||
|
"World/Live/Abbey.wmo",
|
||||||
|
prototype,
|
||||||
|
placement
|
||||||
|
)
|
||||||
|
_expect_true(instance != null, "live prototype duplicated", failures)
|
||||||
|
_expect_false(is_same(instance, prototype), "live root identity isolated", failures)
|
||||||
|
_expect_true(instance.get_parent() == null, "live result remains detached", failures)
|
||||||
|
_expect_string_equal(instance.name, "Abbey", "live basename applied", failures)
|
||||||
|
_expect_true(instance.transform == resolver.resolved_transform, "live transform applied", failures)
|
||||||
|
_expect_same(
|
||||||
|
(instance.get_node("Group") as MeshInstance3D).mesh,
|
||||||
|
source_mesh,
|
||||||
|
"live descendant Mesh identity retained",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(validator.inspected_nodes.size(), 0, "live path skips cache validator", failures)
|
||||||
|
_expect_true(resolver.placements == [placement], "live placement forwarded", failures)
|
||||||
|
instance.free()
|
||||||
|
prototype.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_invalid_composition(failures: Array[String]) -> void:
|
||||||
|
var validator := FakeSceneCacheValidator.new()
|
||||||
|
var resolver := FakePlacementResolver.new()
|
||||||
|
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||||
|
_expect_false(factory.is_cached_node_current(null), "null currentness rejected", failures)
|
||||||
|
_expect_true(
|
||||||
|
factory.instantiate_cached_scene("world/a.wmo", null, {}) == null,
|
||||||
|
"null PackedScene rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
factory.duplicate_live_prototype("world/a.wmo", null, {}) == null,
|
||||||
|
"null prototype rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var fixture := _create_packed_wmo_scene()
|
||||||
|
var missing_validator_factory := FACTORY_SCRIPT.new(null, resolver)
|
||||||
|
_expect_true(
|
||||||
|
missing_validator_factory.instantiate_cached_scene(
|
||||||
|
"world/a.wmo", fixture["scene"], {}
|
||||||
|
) == null,
|
||||||
|
"missing validator rejects cached instance",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var missing_resolver_factory := FACTORY_SCRIPT.new(validator, null)
|
||||||
|
_expect_true(
|
||||||
|
missing_resolver_factory.instantiate_cached_scene(
|
||||||
|
"world/a.wmo", fixture["scene"], {}
|
||||||
|
) == null,
|
||||||
|
"missing resolver rejects cached instance before allocation",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var prototype := Node3D.new()
|
||||||
|
_expect_true(
|
||||||
|
missing_resolver_factory.duplicate_live_prototype("world/a.wmo", prototype, {}) == null,
|
||||||
|
"missing resolver rejects live duplicate",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
prototype.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||||
|
var factory_source := FileAccess.get_file_as_string(FACTORY_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new("),
|
||||||
|
"loader composes scene instance factory",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("_wmo_scene_instance_factory.instantiate_cached_scene("),
|
||||||
|
"loader delegates cached instantiation",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("_wmo_scene_instance_factory.duplicate_live_prototype("),
|
||||||
|
"loader delegates live duplication",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for released_token in [
|
||||||
|
"func _is_wmo_node_cache_current(",
|
||||||
|
"func _instantiate_wmo_scene(",
|
||||||
|
]:
|
||||||
|
_expect_false(loader_source.contains(released_token), "loader releases %s" % released_token, failures)
|
||||||
|
for owned_token in [
|
||||||
|
"scene.instantiate()",
|
||||||
|
"prototype.duplicate()",
|
||||||
|
"is_scene_cache_current",
|
||||||
|
"relative_path.get_file().get_basename()",
|
||||||
|
"resolve_world_transform",
|
||||||
|
]:
|
||||||
|
_expect_true(factory_source.contains(owned_token), "factory owns %s" % owned_token, failures)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"FileAccess.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"RenderingServer.",
|
||||||
|
"add_child(",
|
||||||
|
".owner =",
|
||||||
|
"WmoRuntimeScenePreparer",
|
||||||
|
"_wmo_render_build_queue",
|
||||||
|
]:
|
||||||
|
_expect_false(factory_source.contains(forbidden_dependency), "factory omits %s" % forbidden_dependency, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var validator := FakeSceneCacheValidator.new()
|
||||||
|
var resolver := FakePlacementResolver.new()
|
||||||
|
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||||
|
var prototype := Node3D.new()
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
for iteration in 1000:
|
||||||
|
var instance: Node3D = factory.duplicate_live_prototype(
|
||||||
|
"world/timing.wmo", prototype, {}
|
||||||
|
)
|
||||||
|
instance.free()
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_equal(resolver.placements.size(), 1000, "timing duplicates complete", failures)
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "1000 duplicates under one second", failures)
|
||||||
|
prototype.free()
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
func _create_packed_wmo_scene() -> Dictionary:
|
||||||
|
var source_root := Node3D.new()
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
var mesh_child := MeshInstance3D.new()
|
||||||
|
mesh_child.name = "Group"
|
||||||
|
mesh_child.mesh = mesh
|
||||||
|
source_root.add_child(mesh_child)
|
||||||
|
mesh_child.owner = source_root
|
||||||
|
var scene := PackedScene.new()
|
||||||
|
var pack_error := scene.pack(source_root)
|
||||||
|
if pack_error != OK:
|
||||||
|
push_error("WMO_SCENE_INSTANCE_FACTORY: cannot pack fixture")
|
||||||
|
source_root.free()
|
||||||
|
return {"scene": scene, "mesh": mesh}
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_same(actual, expected):
|
||||||
|
failures.append(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cae6gor0iqjv4
|
||||||
Reference in New Issue
Block a user