rnd(M03): add rendered ground query facade
This commit is contained in:
@@ -929,11 +929,25 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
matches those ownership categories.
|
matches those ownership categories.
|
||||||
- Scene composition may still reference the loader script and the facade's
|
- Scene composition may still reference the loader script and the facade's
|
||||||
implementation path; those are composition details, not mutable queue access.
|
implementation path; those are composition details, not mutable queue access.
|
||||||
- Renderer diagnostic probes are outside this gameplay/editor checklist.
|
- Renderer diagnostic probes are added explicitly as their facade contracts land.
|
||||||
`probe_render_terrain_height.gd` remains a documented temporary exception until
|
|
||||||
readiness and ground-query facade contracts replace its internal inspection.
|
|
||||||
- This is a source-only boundary check and changes no renderer behavior or fidelity.
|
- This is a source-only boundary check and changes no renderer behavior or fidelity.
|
||||||
|
|
||||||
|
## 2026-07-16 Rendered Ground Query Facade
|
||||||
|
|
||||||
|
- `WorldRenderFacade.sample_ground_height()` accepts `GodotWorldPosition` and
|
||||||
|
returns renderer-owned immutable `RenderedGroundSample` from already loaded
|
||||||
|
quality/tile-LOD render meshes, avoiding a render-to-gameplay dependency.
|
||||||
|
- `renderer_ground_query_snapshot()` preserves the terrain probe's tile readiness,
|
||||||
|
queue position, mesh bounds and nearby seam-fallback diagnostics as a detached
|
||||||
|
Dictionary; callers receive no queue, tile-state, Mesh or Node reference.
|
||||||
|
- `probe_render_terrain_height.gd` now uses this facade snapshot and no longer reads
|
||||||
|
`_tile_states`, `_tile_load_queue`, `_tile_loading_tasks` or `_available_tiles`.
|
||||||
|
- Gameplay continues using the independently composed M02 `TerrainQuery`/
|
||||||
|
`AdtTerrainQuery`. Loaded render meshes are diagnostic and are not authoritative
|
||||||
|
movement collision.
|
||||||
|
- The ray height, 3x3 tile search and `2/5/10/20/40`-unit nearby fallback are
|
||||||
|
preserved. No terrain geometry, cache, streaming or visual behavior changed.
|
||||||
|
|
||||||
## 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.
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ terrain-height probe away from direct streamer state/queue access.
|
|||||||
## Contracts and data
|
## Contracts and data
|
||||||
|
|
||||||
- Input: immutable `GodotWorldPosition` in Godot world units
|
- Input: immutable `GodotWorldPosition` in Godot world units
|
||||||
- Output: immutable `TerrainGroundSample` plus optional detached diagnostic Dictionary
|
- Output: renderer-owned immutable `RenderedGroundSample` plus optional detached diagnostic Dictionary
|
||||||
- Sampling source: loaded quality or tile-LOD Mesh and its live tile transform
|
- Sampling source: loaded quality or tile-LOD Mesh and its live tile transform
|
||||||
- M02 ADT query cache, renderer cache versions and coordinate contracts: unchanged
|
- M02 ADT query cache, renderer cache versions and coordinate contracts: unchanged
|
||||||
|
|
||||||
|
|||||||
@@ -5,10 +5,10 @@
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Status | Implemented |
|
| Status | Implemented |
|
||||||
| Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001 |
|
| Target/work package | M02 / M02-GMP-TERRAIN-QUERY-001; M03 renderer consumer `M03-RND-FACADE-GROUND-QUERY-001` |
|
||||||
| Owners | Gameplay terrain-query contract; ADT adapter owns parsed tile cache |
|
| Owners | Gameplay terrain-query contract; ADT adapter owns parsed tile cache |
|
||||||
| Last verified | `a45d521`, 2026-07-14 |
|
| Last verified | `a45d521`, 2026-07-14 |
|
||||||
| Profiles/capabilities | Current sandbox ADT ground height; renderer/physics backend planned |
|
| Profiles/capabilities | Current sandbox ADT ground height; loaded-render-mesh diagnostic query; authoritative physics backend planned |
|
||||||
|
|
||||||
## Purpose
|
## Purpose
|
||||||
|
|
||||||
@@ -47,6 +47,8 @@ Allowed dependencies:
|
|||||||
- Existing native `ADTLoader` at the ADT adapter boundary.
|
- Existing native `ADTLoader` at the ADT adapter boundary.
|
||||||
- Dynamic dictionaries only inside the native-format adapter.
|
- Dynamic dictionaries only inside the native-format adapter.
|
||||||
- Player scene composition may inject any `TerrainQuery` implementation.
|
- Player scene composition may inject any `TerrainQuery` implementation.
|
||||||
|
- Renderer diagnostics reuse `GodotWorldPosition` through `WorldRenderFacade`
|
||||||
|
but return renderer-owned `RenderedGroundSample`, preserving the render/gameplay boundary.
|
||||||
|
|
||||||
Forbidden dependencies:
|
Forbidden dependencies:
|
||||||
|
|
||||||
@@ -65,6 +67,7 @@ Forbidden dependencies:
|
|||||||
| `AdtTerrainQuery.new(extracted_directory, map_directory_name, loader_override)` | Adapter constructor | Configures one map and optional test/data-source loader | Query lifetime | Production validates native loader and file presence lazily |
|
| `AdtTerrainQuery.new(extracted_directory, map_directory_name, loader_override)` | Adapter constructor | Configures one map and optional test/data-source loader | Query lifetime | Production validates native loader and file presence lazily |
|
||||||
| `AdtTerrainQuery.sample_ground_height(position)` | Adapter method | Loads/caches tile and interpolates ADT outer heights | Main thread; cache lives with query | Returns stable failure code |
|
| `AdtTerrainQuery.sample_ground_height(position)` | Adapter method | Loads/caches tile and interpolates ADT outer heights | Main thread; cache lives with query | Returns stable failure code |
|
||||||
| `ThirdPersonWowController.set_terrain_query(query)` | Composition method | Replaces terrain backend independently of movement/presentation | Call before scene ready or on main thread | Null is rejected and current query retained |
|
| `ThirdPersonWowController.set_terrain_query(query)` | Composition method | Replaces terrain backend independently of movement/presentation | Call before scene ready or on main thread | Null is rejected and current query retained |
|
||||||
|
| `WorldRenderFacade.sample_ground_height(position)` | Separate renderer diagnostic API | Samples already loaded terrain Mesh and returns `RenderedGroundSample` without exposing streamer state | Main thread; immutable sample | Stable `render_terrain_*` unavailable code; not injected into player by default |
|
||||||
|
|
||||||
## Inputs and outputs
|
## Inputs and outputs
|
||||||
|
|
||||||
@@ -206,6 +209,9 @@ Existing ADT/native/cache format versions remain unchanged.
|
|||||||
## Extension points
|
## Extension points
|
||||||
|
|
||||||
- Streaming renderer or physics backend may implement `TerrainQuery` without changing player/movement.
|
- Streaming renderer or physics backend may implement `TerrainQuery` without changing player/movement.
|
||||||
|
- The current facade intentionally uses a separate renderer-owned result and is
|
||||||
|
not a `TerrainQuery` implementation. A future adapter must explicitly translate
|
||||||
|
readiness/results and define gameplay authority before composition.
|
||||||
- Future result may grow separate slope, surface, liquid or collision contracts;
|
- Future result may grow separate slope, surface, liquid or collision contracts;
|
||||||
incompatible additions require a versioned contract rather than Dictionary fields.
|
incompatible additions require a versioned contract rather than Dictionary fields.
|
||||||
- Test/authoring data sources may use constructor loader override.
|
- Test/authoring data sources may use constructor loader override.
|
||||||
@@ -218,7 +224,8 @@ Existing ADT/native/cache format versions remain unchanged.
|
|||||||
| Typed replaceable ground-height query | Implemented | Contract and injected player regression | Integrate target checklist after review |
|
| Typed replaceable ground-height query | Implemented | Contract and injected player regression | Integrate target checklist after review |
|
||||||
| Existing ADT height interpolation | Implemented | Synthetic fractional-grid fixture | Compare terrain contact with build 12340 |
|
| Existing ADT height interpolation | Implemented | Synthetic fractional-grid fixture | Compare terrain contact with build 12340 |
|
||||||
| Per-tile success/failure cache | Implemented | Loader call-count regression | Add explicit invalidation only with authoring/hot reload |
|
| Per-tile success/failure cache | Implemented | Loader call-count regression | Add explicit invalidation only with authoring/hot reload |
|
||||||
| Renderer/streaming cache backend | Planned | Boundary permits replacement | M03 renderer facade/public terrain backend |
|
| Renderer loaded-mesh diagnostic backend | Implemented | M03 facade typed sample and detached snapshot contract | Not composed into gameplay; triangle Mesh ray is diagnostic only |
|
||||||
|
| Authoritative renderer/physics backend | Planned | Boundary permits replacement | Define holes/slopes/collision/readiness semantics before gameplay composition |
|
||||||
| Holes/slopes/collision | Planned | Outside height-only contract | Later movement/physics package |
|
| Holes/slopes/collision | Planned | Outside height-only contract | Later movement/physics package |
|
||||||
| Liquid/swim query | Planned | Outside contract | M09/M12 world gameplay |
|
| Liquid/swim query | Planned | Outside contract | M09/M12 world gameplay |
|
||||||
|
|
||||||
@@ -240,6 +247,7 @@ Existing ADT/native/cache format versions remain unchanged.
|
|||||||
| `src/gameplay/terrain/terrain_query.gd` | Replaceable typed query boundary |
|
| `src/gameplay/terrain/terrain_query.gd` | Replaceable typed query boundary |
|
||||||
| `src/gameplay/terrain/adt_terrain_query.gd` | Native ADT adapter, cache, chunk selection and interpolation |
|
| `src/gameplay/terrain/adt_terrain_query.gd` | Native ADT adapter, cache, chunk selection and interpolation |
|
||||||
| `src/scenes/player/third_person_wow_controller.gd` | Query composition and existing ground-snap consumer |
|
| `src/scenes/player/third_person_wow_controller.gd` | Query composition and existing ground-snap consumer |
|
||||||
|
| `src/render/world_render_facade.gd` | Read-only loaded-render-mesh diagnostic consumer of typed position/sample contracts |
|
||||||
| `src/tools/verify_terrain_query.gd` | Synthetic contract/cache/failure/injection regression |
|
| `src/tools/verify_terrain_query.gd` | Synthetic contract/cache/failure/injection regression |
|
||||||
| `src/tests/scenes/player_input_regression.tscn` | Asset-free player composition fixture |
|
| `src/tests/scenes/player_input_regression.tscn` | Asset-free player composition fixture |
|
||||||
|
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
| Field | Value |
|
| Field | Value |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Status | Partial |
|
| Status | Partial |
|
||||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; `M03-RND-FACADE-001`; `M03-RND-STREAMING-PLANNER-001`; `M03-RND-SCHEDULER-001` |
|
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; `M03-RND-FACADE-001`; `M03-RND-STREAMING-PLANNER-001`; `M03-RND-SCHEDULER-001`; `M03-RND-INTERNAL-ACCESS-GATE-001`; `M03-RND-FACADE-GROUND-QUERY-001` |
|
||||||
| Owners | Renderer workstream / milestone integrator |
|
| Owners | Renderer workstream / milestone integrator |
|
||||||
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
|
| Last verified | Worktree `work/sindo-main-codex/m03-render-budget-scheduler`, 2026-07-16 |
|
||||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||||
@@ -65,9 +65,9 @@ Forbidden dependencies:
|
|||||||
|
|
||||||
## Public API
|
## Public API
|
||||||
|
|
||||||
Runtime, capture and probe callers use `WorldRenderFacade` for focus and metrics.
|
Runtime, capture and probe callers use `WorldRenderFacade` for focus, metrics and
|
||||||
`StreamingWorldLoader` remains the internal scene implementation while later M03
|
rendered-ground diagnostics. `StreamingWorldLoader` remains the internal scene
|
||||||
packages add environment, entity-visual and ground-query contracts.
|
implementation while later M03 packages add environment and entity-visual contracts.
|
||||||
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
|
Gameplay modules and Godot `EditorPlugin` package sources are repository-gated
|
||||||
from externally reading/writing loader-private queue, task, cache and tile-state fields.
|
from externally reading/writing loader-private queue, task, cache and tile-state fields.
|
||||||
|
|
||||||
@@ -80,6 +80,8 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `WorldRenderFacade.set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Missing internal renderer is diagnosed; no state is duplicated |
|
| `WorldRenderFacade.set_streaming_focus` | Public method | Accepts focus from app/editor/tool composition | Main thread/session | Missing internal renderer is diagnosed; no state is duplicated |
|
||||||
| `WorldRenderFacade.refresh_streaming_focus` | Public method | Samples configured source and delegates existing refresh thresholds | Main thread/session | Returns `false` when dependency/focus is unavailable |
|
| `WorldRenderFacade.refresh_streaming_focus` | Public method | Samples configured source and delegates existing refresh thresholds | Main thread/session | Returns `false` when dependency/focus is unavailable |
|
||||||
| `WorldRenderFacade.renderer_metrics_snapshot` | Public method | Returns a deep-detached queue/cache/activity snapshot | Main thread/caller-owned result | Missing/invalid implementation result returns empty snapshot with diagnostic |
|
| `WorldRenderFacade.renderer_metrics_snapshot` | Public method | Returns a deep-detached queue/cache/activity snapshot | Main thread/caller-owned result | Missing/invalid implementation result returns empty snapshot with diagnostic |
|
||||||
|
| `WorldRenderFacade.sample_ground_height` | Public read-only query | Samples already loaded render terrain at a typed Godot world position | Main thread; immutable caller-held result | Stable unavailable code for missing mesh/intersection/facade; not gameplay authority |
|
||||||
|
| `WorldRenderFacade.renderer_ground_query_snapshot` | Public diagnostic query | Returns ground height plus detached tile/readiness context without exposing mutable state | Main thread; caller-owned deep copy | Missing/invalid renderer returns empty snapshot with diagnostic |
|
||||||
| `WorldRenderFacade.streaming_world_loader_path` | Internal adapter property | Resolves the existing implementation without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
|
| `WorldRenderFacade.streaming_world_loader_path` | Internal adapter property | Resolves the existing implementation without exposing it to consumers | Main thread, scene lifetime | Missing method/path produces one diagnostic until recovery |
|
||||||
| `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning |
|
| `camera_path` | Exported property | Camera used only by optional automatic overview positioning | Main thread, scene lifetime | Missing camera skips automatic positioning |
|
||||||
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
|
| `quality_preset` | Exported property | Applies renderer budgets/radii | Before/at runtime depending property | Invalid combination should be diagnosed |
|
||||||
@@ -96,6 +98,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
|---|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `WorldRenderFacade` | Immutable caller value | Main thread/session |
|
| Input | `StreamingFocus` | Player/spectator/editor/capture adapter | `WorldRenderFacade` | Immutable caller value | Main thread/session |
|
||||||
| Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
|
| Input | Focus-source `NodePath` | Runtime/test scene composition | `WorldRenderFacade` source adapter | Scene-owned node, sampled position | Main thread/scene lifetime |
|
||||||
|
| Input | Ground-query `GodotWorldPosition` | Renderer probe/future adapter | `WorldRenderFacade` | Immutable caller-held value | One main-thread query |
|
||||||
| Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
|
| Input | Map/configuration | Scene/app/editor | `StreamingWorldLoader` | Caller config, copied/read | Main thread/session |
|
||||||
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
|
| Input | WDT/ADT/M2/WMO/BLP bytes | Extracted asset repository | Native loaders/builders | Loader result owns parsed data | Worker or controlled load |
|
||||||
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
|
| Input | Baked resources | Cache build tools | Streamer/builders | Shared immutable resource/cache entry | Session/cache lifetime |
|
||||||
@@ -105,6 +108,8 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| 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 |
|
||||||
|
| Output | `RenderedGroundSample` | `WorldRenderFacade`/loaded terrain | Renderer diagnostics | Immutable caller-held value | One query |
|
||||||
|
| Output | Detached rendered-ground snapshot | `WorldRenderFacade` | Terrain-height probe | Caller-owned deep copy | One query |
|
||||||
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
|
| Output | Logs/diagnostics | Hitch/stat instrumentation | Logs/baseline tools | Immutable records | Periodic/frame |
|
||||||
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
|
| Test output | Diagnostic spawn marker and cold/warm PNGs | Checkpoint capture tool | Human/integrator review | SceneTree marker; `user://` files | One capture process |
|
||||||
|
|
||||||
@@ -140,6 +145,8 @@ flowchart TD
|
|||||||
B --> M2[M2 group/MultiMesh attach]
|
B --> M2[M2 group/MultiMesh attach]
|
||||||
B --> WMO[WMO instance/group attach]
|
B --> WMO[WMO instance/group attach]
|
||||||
B --> Liquid[Liquid attach]
|
B --> Liquid[Liquid attach]
|
||||||
|
Loaded[Loaded terrain Mesh and Transform] --> Ground[Facade ground query]
|
||||||
|
Ground --> GroundSample[RenderedGroundSample / detached diagnostics]
|
||||||
Terrain --> World[Godot world]
|
Terrain --> World[Godot world]
|
||||||
M2 --> World
|
M2 --> World
|
||||||
WMO --> World
|
WMO --> World
|
||||||
@@ -192,6 +199,9 @@ sequenceDiagram
|
|||||||
Budget-->>Stream: true while lane remains bounded
|
Budget-->>Stream: true while lane remains bounded
|
||||||
Stream->>Render: attach permitted terrain/M2/WMO/liquid work
|
Stream->>Render: attach permitted terrain/M2/WMO/liquid work
|
||||||
Stream->>Render: evict outside retention range
|
Stream->>Render: evict outside retention range
|
||||||
|
Source->>Facade: sample_ground_height(typed position)
|
||||||
|
Facade->>Stream: sample loaded render terrain
|
||||||
|
Stream-->>Facade: RenderedGroundSample / diagnostic snapshot
|
||||||
Stream->>Budget: shutdown: cancel permit issuance
|
Stream->>Budget: shutdown: cancel permit issuance
|
||||||
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
|
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
|
||||||
Stream->>Stream: shutdown: finish registered ResourceLoader requests
|
Stream->>Stream: shutdown: finish registered ResourceLoader requests
|
||||||
@@ -204,6 +214,10 @@ sequenceDiagram
|
|||||||
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
|
- `StreamingWorldLoader` владеет tile states, queues, task registries, active world instances и render caches session scope.
|
||||||
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
|
- `WorldRenderFacade` owns only scene-relative adapter paths. It does not own the
|
||||||
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
|
streamer, focus source, returned metrics, queues, caches, nodes or RIDs.
|
||||||
|
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||||
|
the facade never returns Mesh, Node, tile-state or queue references.
|
||||||
|
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||||
|
collision. M02 `TerrainQuery` composition and ADT cache ownership remain unchanged.
|
||||||
- `StreamingTargetPlanner` is stateless and owns only call-local calculations;
|
- `StreamingTargetPlanner` is stateless and owns only call-local calculations;
|
||||||
its immutable plan is consumed synchronously by the streamer.
|
its immutable plan is consumed synchronously by the streamer.
|
||||||
- `RenderBudgetScheduler` owns only per-frame lane counters and a terminal
|
- `RenderBudgetScheduler` owns only per-frame lane counters and a terminal
|
||||||
@@ -230,6 +244,7 @@ sequenceDiagram
|
|||||||
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
|
| Cache version mismatch | Required format constant | Reject stale cache/fallback | Version diagnostic | Rebuild matching cache |
|
||||||
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
|
| Worker failure | Task result/empty data | Skip/degrade affected asset | Queue/task warning | Retry/rebuild source |
|
||||||
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
|
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
|
||||||
|
| Render terrain unavailable | Facade ground query | Immutable unavailable sample | Stable `render_terrain_*` failure code and optional snapshot | Wait for streaming or use gameplay-owned query backend |
|
||||||
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
|
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
|
||||||
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
|
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
|
||||||
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report + cache shutdown verifier | Drain requests, preserve tree ownership, then free detached prototypes and clear resource caches |
|
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report + cache shutdown verifier | Drain requests, preserve tree ownership, then free detached prototypes and clear resource caches |
|
||||||
@@ -276,6 +291,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- Internal-access contract: derives the current streamer's private queue/task/
|
- Internal-access contract: derives the current streamer's private queue/task/
|
||||||
cache/state field inventory and rejects external member/reflection access from
|
cache/state field inventory and rejects external member/reflection access from
|
||||||
gameplay or EditorPlugin package code.
|
gameplay or EditorPlugin package code.
|
||||||
|
- Ground-query facade contract: available/unavailable typed result delegation,
|
||||||
|
detached nested diagnostics and terrain-probe source migration without private access.
|
||||||
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
|
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes, seven M00
|
||||||
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
|
cold/warm checkpoints and one dedicated mapped server-spawn checkpoint.
|
||||||
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
||||||
@@ -301,10 +318,11 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
|
| Character presentation | Prototype | Character creator/player experiment | Full customization/equipment states |
|
||||||
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
|
| Camera-independent streaming focus | Implemented | Typed contract, two player-focused runtime scenes and three explicitly camera-focused diagnostic tools | Multi-focus/direction/velocity belong to planner extraction |
|
||||||
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
|
| Golden server-spawn visualization | Implemented | Pinned AzerothCore fixture, validated renderer manifest and diagnostic origin-marker capture at ADT `(32,48)`/MCNK `(3,12)` | No TrinityCore row or original-client visual-parity claim |
|
||||||
| Stable renderer facade | Partial | `M03-RND-FACADE-001` contract/runtime/tool regressions | Focus and metrics migrated; environment/entity visuals/ground query remain |
|
| Stable renderer facade | Partial | Facade contracts cover focus, metrics and loaded-mesh ground query | Environment and entity visuals remain |
|
||||||
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
|
| Pure streaming target planner | Implemented | `M03-RND-STREAMING-PLANNER-001` asset-free contract/performance regression | Queue scheduling and loaded-tile LOD application remain in streamer |
|
||||||
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
|
| Bounded render budget scheduler | Implemented | `M03-RND-SCHEDULER-001` asset-free permit/cancellation/performance regression | Queue storage, worker concurrency and operation execution remain in streamer/services |
|
||||||
| Gameplay/editor internal-access boundary | Implemented | `M03-RND-INTERNAL-ACCESS-GATE-001` scans actual private streamer declarations against gameplay and EditorPlugin sources | Renderer diagnostic probes remain a scoped exception pending ground-query/readiness facade contracts |
|
| Gameplay/editor/tool internal-access boundary | Implemented | Access gate scans gameplay, EditorPlugin packages and terrain probe against actual private streamer declarations | Add future renderer diagnostic tools explicitly to the protected list |
|
||||||
|
| Rendered terrain ground query | Implemented | `M03-RND-FACADE-GROUND-QUERY-001` typed result/snapshot isolation and probe migration | Diagnostic Mesh ray only; not movement/collision authority |
|
||||||
|
|
||||||
## Known gaps and risks
|
## Known gaps and risks
|
||||||
|
|
||||||
@@ -325,15 +343,16 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- D3D12 descriptor issues remain. Explicit shutdown ownership removes the GUI capture's Node/resource/RID leaks in focused and seven-checkpoint runs; a timing-dependent ObjectDB warning for anonymous zero-reference `RefCounted` objects remains as a separate engine-teardown diagnostic.
|
- D3D12 descriptor issues remain. Explicit shutdown ownership removes the GUI capture's Node/resource/RID leaks in focused and seven-checkpoint runs; a timing-dependent ObjectDB warning for anonymous zero-reference `RefCounted` objects remains as a separate engine-teardown diagnostic.
|
||||||
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
||||||
- Public API is mostly exported configuration rather than stable contracts.
|
- Public API is mostly exported configuration rather than stable contracts.
|
||||||
- `probe_render_terrain_height.gd` remains a renderer-only diagnostic exception
|
- Rendered-ground sampling generates a triangle mesh on demand and retains the
|
||||||
that reads tile readiness/terrain internals; it is excluded from the gameplay/
|
historical nearby seam fallback. It is appropriate for diagnostics, not a
|
||||||
EditorPlugin gate until equivalent ground-query and readiness facade contracts exist.
|
per-physics-frame gameplay query or a claim of exact terrain collision parity.
|
||||||
|
|
||||||
## Source map
|
## Source map
|
||||||
|
|
||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus and detached renderer metrics |
|
| `src/render/world_render_facade.gd` | Stable caller boundary for typed focus, detached metrics and rendered-ground queries |
|
||||||
|
| `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result |
|
||||||
| `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 |
|
||||||
@@ -341,10 +360,10 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
|
| `src/scenes/streaming/streaming_world_loader.gd` | Current runtime orchestration, queues, caches and attachment |
|
||||||
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
|
| `src/domain/streaming/streaming_focus.gd` | Immutable camera-independent streaming position contract |
|
||||||
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
|
| `src/tools/verify_streaming_focus.gd` | Headless contract, dependency and runtime/tool wiring regression |
|
||||||
| `src/tools/verify_world_render_facade.gd` | Facade delegation, snapshot isolation and consumer wiring regression |
|
| `src/tools/verify_world_render_facade.gd` | Focus/metrics/ground delegation, snapshot isolation and consumer wiring 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 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 |
|
||||||
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
|
| `addons/mpq_extractor/loaders/adt_builder.gd` | Terrain/material/liquid construction |
|
||||||
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
|
| `addons/mpq_extractor/loaders/m2_builder.gd` | Static M2 mesh/material construction |
|
||||||
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
|
| `addons/mpq_extractor/loaders/m2_native_animated_builder.gd` | Native animated M2 experiment |
|
||||||
@@ -360,7 +379,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer checkpoint contract verification |
|
| `src/tools/verify_server_spawn_renderer.gd` | Fixture-to-renderer checkpoint contract verification |
|
||||||
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
|
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
|
||||||
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
|
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
|
||||||
| `src/tools/probe_render_terrain_height.gd` | Offline active-mesh terrain height and camera-clearance report |
|
| `src/tools/probe_render_terrain_height.gd` | Offline facade-backed active-mesh terrain height and camera-clearance report |
|
||||||
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
|
| `src/tools/probe_render_camera_occluders.gd` | Scene-tree placement containment and camera-to-target AABB intersection report |
|
||||||
|
|
||||||
## Related decisions and references
|
## Related decisions and references
|
||||||
|
|||||||
@@ -0,0 +1,50 @@
|
|||||||
|
class_name RenderedGroundSample
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable result of sampling currently loaded render terrain in Godot world units.
|
||||||
|
## This diagnostic value is renderer-owned and is not authoritative gameplay collision.
|
||||||
|
|
||||||
|
var is_available: bool:
|
||||||
|
get:
|
||||||
|
return _is_available
|
||||||
|
|
||||||
|
var height_units: float:
|
||||||
|
get:
|
||||||
|
return _height_units
|
||||||
|
|
||||||
|
var failure_code: StringName:
|
||||||
|
get:
|
||||||
|
return _failure_code
|
||||||
|
|
||||||
|
var _is_available: bool
|
||||||
|
var _height_units: float
|
||||||
|
var _failure_code: StringName
|
||||||
|
|
||||||
|
|
||||||
|
## Creates an available finite rendered-ground sample in Godot world units.
|
||||||
|
static func available(height_units_value: float) -> RenderedGroundSample:
|
||||||
|
return RenderedGroundSample.new(true, height_units_value, &"")
|
||||||
|
|
||||||
|
|
||||||
|
## Creates an unavailable rendered-ground sample with a stable failure code.
|
||||||
|
static func unavailable(failure_code_value: StringName) -> RenderedGroundSample:
|
||||||
|
return RenderedGroundSample.new(false, 0.0, failure_code_value)
|
||||||
|
|
||||||
|
|
||||||
|
func _init(
|
||||||
|
is_available_value: bool,
|
||||||
|
height_units_value: float,
|
||||||
|
failure_code_value: StringName
|
||||||
|
) -> void:
|
||||||
|
_is_available = is_available_value and is_finite(height_units_value)
|
||||||
|
_height_units = height_units_value if _is_available else 0.0
|
||||||
|
if _is_available:
|
||||||
|
_failure_code = &""
|
||||||
|
elif not is_finite(height_units_value):
|
||||||
|
_failure_code = &"render_terrain_height_not_finite"
|
||||||
|
else:
|
||||||
|
_failure_code = (
|
||||||
|
failure_code_value
|
||||||
|
if not failure_code_value.is_empty()
|
||||||
|
else &"render_terrain_unavailable"
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c744ldnnn87a8
|
||||||
@@ -7,6 +7,7 @@ extends Node
|
|||||||
|
|
||||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.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")
|
||||||
|
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||||
|
|
||||||
## Internal renderer implementation resolved relative to this node.
|
## Internal renderer implementation resolved relative to this node.
|
||||||
## The referenced node remains owned by its scene parent.
|
## The referenced node remains owned by its scene parent.
|
||||||
@@ -66,6 +67,33 @@ func renderer_metrics_snapshot() -> Dictionary:
|
|||||||
return (metrics_variant as Dictionary).duplicate(true)
|
return (metrics_variant as Dictionary).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
|
## Samples the currently loaded render terrain at a typed Godot world position.
|
||||||
|
## This read-only renderer view is diagnostic and does not replace authoritative
|
||||||
|
## gameplay collision or the independently composed [TerrainQuery].
|
||||||
|
func sample_ground_height(godot_world_position: GodotWorldPosition) -> RefCounted:
|
||||||
|
var loader := _resolve_streaming_world_loader()
|
||||||
|
if loader == null:
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_facade_unavailable")
|
||||||
|
var sample_variant = loader.call("sample_rendered_ground_height", godot_world_position)
|
||||||
|
if sample_variant is RefCounted and sample_variant.get_script() == RENDERED_GROUND_SAMPLE_SCRIPT:
|
||||||
|
return sample_variant
|
||||||
|
push_error("WorldRenderFacade received an invalid rendered ground sample.")
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_ground_sample_invalid")
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a detached diagnostic snapshot for one rendered-ground query.
|
||||||
|
## Mutating the result cannot mutate streamer queues, tile state or resources.
|
||||||
|
func renderer_ground_query_snapshot(godot_world_position: GodotWorldPosition) -> Dictionary:
|
||||||
|
var loader := _resolve_streaming_world_loader()
|
||||||
|
if loader == null:
|
||||||
|
return {}
|
||||||
|
var snapshot_variant = loader.call("render_ground_query_snapshot", godot_world_position)
|
||||||
|
if not (snapshot_variant is Dictionary):
|
||||||
|
push_error("WorldRenderFacade received a non-Dictionary ground query snapshot.")
|
||||||
|
return {}
|
||||||
|
return (snapshot_variant as Dictionary).duplicate(true)
|
||||||
|
|
||||||
|
|
||||||
func _capture_streaming_focus_from_source() -> void:
|
func _capture_streaming_focus_from_source() -> void:
|
||||||
if streaming_focus_source_path == NodePath():
|
if streaming_focus_source_path == NodePath():
|
||||||
return
|
return
|
||||||
@@ -99,6 +127,8 @@ func _resolve_streaming_world_loader() -> Node:
|
|||||||
&"set_streaming_focus",
|
&"set_streaming_focus",
|
||||||
&"refresh_streaming_focus",
|
&"refresh_streaming_focus",
|
||||||
&"render_baseline_snapshot",
|
&"render_baseline_snapshot",
|
||||||
|
&"sample_rendered_ground_height",
|
||||||
|
&"render_ground_query_snapshot",
|
||||||
]:
|
]:
|
||||||
if not candidate.has_method(required_method):
|
if not candidate.has_method(required_method):
|
||||||
if not _missing_loader_reported:
|
if not _missing_loader_reported:
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot
|
|||||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||||
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
const ADT_TILE_COORDINATE_SCRIPT := preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||||
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
const ADT_TILE_LOCAL_POSITION_SCRIPT := preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||||
|
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||||
@@ -25,6 +26,7 @@ const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
|||||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||||
const M2_MATERIAL_REFRESH_VERSION := 2
|
const M2_MATERIAL_REFRESH_VERSION := 2
|
||||||
const WMO_MATERIAL_REFRESH_VERSION := 10
|
const WMO_MATERIAL_REFRESH_VERSION := 10
|
||||||
|
const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||||
|
|
||||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||||
@@ -1038,6 +1040,173 @@ func render_baseline_snapshot() -> Dictionary:
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
## Samples already loaded render terrain at one typed Godot world position.
|
||||||
|
## This diagnostic rendering view is not authoritative gameplay collision.
|
||||||
|
func sample_rendered_ground_height(godot_world_position: GodotWorldPosition) -> RefCounted:
|
||||||
|
var ground_query_snapshot := render_ground_query_snapshot(godot_world_position)
|
||||||
|
if ground_query_snapshot.has("terrain_height"):
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.available(float(ground_query_snapshot["terrain_height"]))
|
||||||
|
var query_status := StringName(ground_query_snapshot.get("status", "render_terrain_unavailable"))
|
||||||
|
match query_status:
|
||||||
|
&"mesh_not_ready":
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_terrain_mesh_not_ready")
|
||||||
|
&"no_intersection":
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_terrain_no_intersection")
|
||||||
|
_:
|
||||||
|
return RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_terrain_unavailable")
|
||||||
|
|
||||||
|
|
||||||
|
## Returns a detached diagnostic read model for the loaded terrain around one
|
||||||
|
## typed position. Queue/cache ownership and mutable tile state stay internal.
|
||||||
|
func render_ground_query_snapshot(godot_world_position: GodotWorldPosition) -> Dictionary:
|
||||||
|
var world_position := Vector3(
|
||||||
|
godot_world_position.x_units,
|
||||||
|
godot_world_position.y_units,
|
||||||
|
godot_world_position.z_units
|
||||||
|
)
|
||||||
|
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(godot_world_position)
|
||||||
|
var highest_terrain_height := -INF
|
||||||
|
var intersected_tile_key := ""
|
||||||
|
var ready_mesh_count := 0
|
||||||
|
for tile_y in range(tile_coordinate.tile_y - 1, tile_coordinate.tile_y + 2):
|
||||||
|
for tile_x in range(tile_coordinate.tile_x - 1, tile_coordinate.tile_x + 2):
|
||||||
|
var tile_key := _tile_key(tile_x, tile_y)
|
||||||
|
if not _tile_states.has(tile_key):
|
||||||
|
continue
|
||||||
|
var terrain_state: Dictionary = _tile_states[tile_key]
|
||||||
|
var terrain_height_variant = _intersect_rendered_terrain_state(terrain_state, world_position)
|
||||||
|
if terrain_height_variant == null:
|
||||||
|
continue
|
||||||
|
ready_mesh_count += 1
|
||||||
|
var terrain_height := float(terrain_height_variant)
|
||||||
|
if terrain_height > highest_terrain_height:
|
||||||
|
highest_terrain_height = terrain_height
|
||||||
|
intersected_tile_key = tile_key
|
||||||
|
|
||||||
|
if is_finite(highest_terrain_height):
|
||||||
|
return {
|
||||||
|
"status": "sampled",
|
||||||
|
"tile": intersected_tile_key,
|
||||||
|
"terrain_height": highest_terrain_height,
|
||||||
|
}
|
||||||
|
|
||||||
|
var focus_tile_key := _tile_key(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
||||||
|
var missing_snapshot := {
|
||||||
|
"status": "no_intersection" if ready_mesh_count > 0 else "mesh_not_ready",
|
||||||
|
"tile": focus_tile_key,
|
||||||
|
}
|
||||||
|
missing_snapshot.merge(
|
||||||
|
_render_ground_tile_diagnostic(focus_tile_key, world_position),
|
||||||
|
true
|
||||||
|
)
|
||||||
|
if bool(missing_snapshot.get("quality_mesh_present", false)) or bool(
|
||||||
|
missing_snapshot.get("tile_lod_mesh_present", false)):
|
||||||
|
missing_snapshot["status"] = "no_intersection"
|
||||||
|
missing_snapshot.merge(_nearest_rendered_terrain_sample(world_position), true)
|
||||||
|
if missing_snapshot.get("nearest_sample_distance", null) != null:
|
||||||
|
missing_snapshot["status"] = "sampled_nearby"
|
||||||
|
missing_snapshot["terrain_height"] = float(missing_snapshot["nearest_sample_height"])
|
||||||
|
return missing_snapshot
|
||||||
|
|
||||||
|
|
||||||
|
func _intersect_rendered_terrain_state(terrain_state: Dictionary, world_position: Vector3):
|
||||||
|
var terrain_mesh: Mesh = terrain_state.get("quality_terrain_mesh", null)
|
||||||
|
if terrain_mesh == null:
|
||||||
|
terrain_mesh = terrain_state.get("tile_lod_mesh", null)
|
||||||
|
if terrain_mesh == null:
|
||||||
|
return null
|
||||||
|
var triangle_mesh := terrain_mesh.generate_triangle_mesh()
|
||||||
|
if triangle_mesh == null:
|
||||||
|
return null
|
||||||
|
var tile_root := terrain_state.get("root", null) as Node3D
|
||||||
|
if tile_root == null or not is_instance_valid(tile_root):
|
||||||
|
return null
|
||||||
|
var inverse_transform := tile_root.global_transform.affine_inverse()
|
||||||
|
var local_ray_origin := inverse_transform * Vector3(
|
||||||
|
world_position.x,
|
||||||
|
RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS,
|
||||||
|
world_position.z
|
||||||
|
)
|
||||||
|
var local_ray_direction := inverse_transform.basis * Vector3.DOWN
|
||||||
|
var intersection: Dictionary = triangle_mesh.intersect_ray(local_ray_origin, local_ray_direction)
|
||||||
|
if intersection.is_empty():
|
||||||
|
return null
|
||||||
|
var local_hit: Vector3 = intersection.get("position", Vector3.ZERO)
|
||||||
|
return (tile_root.global_transform * local_hit).y
|
||||||
|
|
||||||
|
|
||||||
|
func _render_ground_tile_diagnostic(tile_key: String, world_position: Vector3) -> Dictionary:
|
||||||
|
var queued_index := -1
|
||||||
|
for request_index in _tile_load_queue.size():
|
||||||
|
var tile_request: Dictionary = _tile_load_queue[request_index]
|
||||||
|
if String(tile_request.get("key", "")) == tile_key:
|
||||||
|
queued_index = request_index
|
||||||
|
break
|
||||||
|
var diagnostic_snapshot := {
|
||||||
|
"available": _available_tiles.has(tile_key),
|
||||||
|
"queued_index": queued_index,
|
||||||
|
"loading": _tile_loading_tasks.has(tile_key),
|
||||||
|
"state_present": _tile_states.has(tile_key),
|
||||||
|
"load_queue_size": _tile_load_queue.size(),
|
||||||
|
}
|
||||||
|
if not _tile_states.has(tile_key):
|
||||||
|
return diagnostic_snapshot
|
||||||
|
|
||||||
|
var terrain_state: Dictionary = _tile_states[tile_key]
|
||||||
|
var quality_mesh: Mesh = terrain_state.get("quality_terrain_mesh", null)
|
||||||
|
var tile_lod_mesh: Mesh = terrain_state.get("tile_lod_mesh", null)
|
||||||
|
diagnostic_snapshot["quality_mesh_present"] = quality_mesh != null
|
||||||
|
diagnostic_snapshot["tile_lod_mesh_present"] = tile_lod_mesh != null
|
||||||
|
diagnostic_snapshot["quality_source"] = String(terrain_state.get("quality_terrain_source", ""))
|
||||||
|
diagnostic_snapshot["tile_lod"] = int(terrain_state.get("tile_lod", -1))
|
||||||
|
var tile_root := terrain_state.get("root", null) as Node3D
|
||||||
|
if tile_root != null and is_instance_valid(tile_root):
|
||||||
|
var local_position := tile_root.global_transform.affine_inverse() * world_position
|
||||||
|
diagnostic_snapshot["tile_root_position"] = _render_ground_vector3_array(tile_root.global_position)
|
||||||
|
diagnostic_snapshot["probe_local_position"] = _render_ground_vector3_array(local_position)
|
||||||
|
var diagnostic_mesh := quality_mesh if quality_mesh != null else tile_lod_mesh
|
||||||
|
if diagnostic_mesh != null:
|
||||||
|
var mesh_bounds := diagnostic_mesh.get_aabb()
|
||||||
|
diagnostic_snapshot["mesh_aabb_position"] = _render_ground_vector3_array(mesh_bounds.position)
|
||||||
|
diagnostic_snapshot["mesh_aabb_size"] = _render_ground_vector3_array(mesh_bounds.size)
|
||||||
|
return diagnostic_snapshot
|
||||||
|
|
||||||
|
|
||||||
|
func _nearest_rendered_terrain_sample(world_position: Vector3) -> Dictionary:
|
||||||
|
for radius_units in [2.0, 5.0, 10.0, 20.0, 40.0]:
|
||||||
|
for offset in [
|
||||||
|
Vector2(radius_units, 0.0),
|
||||||
|
Vector2(-radius_units, 0.0),
|
||||||
|
Vector2(0.0, radius_units),
|
||||||
|
Vector2(0.0, -radius_units),
|
||||||
|
]:
|
||||||
|
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
|
||||||
|
var typed_sample_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||||
|
sample_position.x,
|
||||||
|
sample_position.y,
|
||||||
|
sample_position.z
|
||||||
|
)
|
||||||
|
var sample_tile = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_sample_position)
|
||||||
|
var sample_tile_key := _tile_key(sample_tile.tile_x, sample_tile.tile_y)
|
||||||
|
if not _tile_states.has(sample_tile_key):
|
||||||
|
continue
|
||||||
|
var terrain_height_variant = _intersect_rendered_terrain_state(
|
||||||
|
_tile_states[sample_tile_key],
|
||||||
|
sample_position
|
||||||
|
)
|
||||||
|
if terrain_height_variant != null:
|
||||||
|
return {
|
||||||
|
"nearest_sample_distance": radius_units,
|
||||||
|
"nearest_sample_height": float(terrain_height_variant),
|
||||||
|
"nearest_sample_tile": sample_tile_key,
|
||||||
|
}
|
||||||
|
return {"nearest_sample_distance": null}
|
||||||
|
|
||||||
|
|
||||||
|
func _render_ground_vector3_array(vector: Vector3) -> Array[float]:
|
||||||
|
return [vector.x, vector.y, vector.z]
|
||||||
|
|
||||||
|
|
||||||
func _tick_runtime_stats(delta: float) -> void:
|
func _tick_runtime_stats(delta: float) -> void:
|
||||||
if not runtime_stats_enabled or Engine.is_editor_hint():
|
if not runtime_stats_enabled or Engine.is_editor_hint():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -5,9 +5,7 @@ extends SceneTree
|
|||||||
## [--wait 3.0] [--output user://render_terrain_height/report.json]
|
## [--wait 3.0] [--output user://render_terrain_height/report.json]
|
||||||
|
|
||||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||||
const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
|
||||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
||||||
const RAY_HEIGHT := 5000.0
|
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
@@ -61,7 +59,15 @@ func _run_async() -> void:
|
|||||||
camera.global_position = camera_position
|
camera.global_position = camera_position
|
||||||
render_facade.call("refresh_streaming_focus", true)
|
render_facade.call("refresh_streaming_focus", true)
|
||||||
await create_timer(maxf(0.1, wait_seconds)).timeout
|
await create_timer(maxf(0.1, wait_seconds)).timeout
|
||||||
var terrain_sample := _sample_terrain(world, camera_position)
|
var typed_camera_position = GODOT_WORLD_POSITION_SCRIPT.new(
|
||||||
|
camera_position.x,
|
||||||
|
camera_position.y,
|
||||||
|
camera_position.z
|
||||||
|
)
|
||||||
|
var terrain_sample: Dictionary = render_facade.call(
|
||||||
|
"renderer_ground_query_snapshot",
|
||||||
|
typed_camera_position
|
||||||
|
)
|
||||||
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
|
terrain_sample["name"] = checkpoint.get("name", "checkpoint")
|
||||||
terrain_sample["camera_y"] = camera_position.y
|
terrain_sample["camera_y"] = camera_position.y
|
||||||
if terrain_sample.has("terrain_height"):
|
if terrain_sample.has("terrain_height"):
|
||||||
@@ -94,137 +100,6 @@ func _run_async() -> void:
|
|||||||
quit(1 if failed else 0)
|
quit(1 if failed else 0)
|
||||||
|
|
||||||
|
|
||||||
func _sample_terrain(world: Node3D, world_position: Vector3) -> Dictionary:
|
|
||||||
var tile_coordinate := _adt_tile_vector2i(world_position)
|
|
||||||
var tile_states: Dictionary = world.get("_tile_states")
|
|
||||||
var highest_terrain_height := -INF
|
|
||||||
var intersected_tile_key := ""
|
|
||||||
var ready_mesh_count := 0
|
|
||||||
for tile_y in range(tile_coordinate.y - 1, tile_coordinate.y + 2):
|
|
||||||
for tile_x in range(tile_coordinate.x - 1, tile_coordinate.x + 2):
|
|
||||||
var tile_key := "%d_%d" % [tile_x, tile_y]
|
|
||||||
if not tile_states.has(tile_key):
|
|
||||||
continue
|
|
||||||
var state: Dictionary = tile_states[tile_key]
|
|
||||||
var terrain_height_variant = _intersect_terrain_state(state, world_position)
|
|
||||||
if terrain_height_variant == null:
|
|
||||||
continue
|
|
||||||
ready_mesh_count += 1
|
|
||||||
var terrain_height := float(terrain_height_variant)
|
|
||||||
if terrain_height > highest_terrain_height:
|
|
||||||
highest_terrain_height = terrain_height
|
|
||||||
intersected_tile_key = tile_key
|
|
||||||
if not is_finite(highest_terrain_height):
|
|
||||||
var missing_result := {
|
|
||||||
"status": "no_intersection" if ready_mesh_count > 0 else "mesh_not_ready",
|
|
||||||
"tile": "%d_%d" % [tile_coordinate.x, tile_coordinate.y],
|
|
||||||
}
|
|
||||||
missing_result.merge(_tile_runtime_diagnostic(world, String(missing_result.tile), world_position), true)
|
|
||||||
if bool(missing_result.get("quality_mesh_present", false)) or bool(missing_result.get("tile_lod_mesh_present", false)):
|
|
||||||
missing_result["status"] = "no_intersection"
|
|
||||||
missing_result.merge(_nearest_terrain_sample(world, world_position), true)
|
|
||||||
if missing_result.get("nearest_sample_distance", null) != null:
|
|
||||||
missing_result["status"] = "sampled_nearby"
|
|
||||||
missing_result["terrain_height"] = float(missing_result.nearest_sample_height)
|
|
||||||
return missing_result
|
|
||||||
return {
|
|
||||||
"status": "sampled",
|
|
||||||
"tile": intersected_tile_key,
|
|
||||||
"terrain_height": highest_terrain_height,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
func _intersect_terrain_state(state: Dictionary, world_position: Vector3):
|
|
||||||
var terrain_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
|
||||||
if terrain_mesh == null:
|
|
||||||
terrain_mesh = state.get("tile_lod_mesh", null)
|
|
||||||
if terrain_mesh == null:
|
|
||||||
return null
|
|
||||||
var triangle_mesh := terrain_mesh.generate_triangle_mesh()
|
|
||||||
if triangle_mesh == null:
|
|
||||||
return null
|
|
||||||
var tile_root := state.get("root", null) as Node3D
|
|
||||||
if tile_root == null:
|
|
||||||
return null
|
|
||||||
var inverse_transform := tile_root.global_transform.affine_inverse()
|
|
||||||
var local_ray_origin := inverse_transform * Vector3(world_position.x, RAY_HEIGHT, world_position.z)
|
|
||||||
var local_ray_direction := inverse_transform.basis * Vector3.DOWN
|
|
||||||
var intersection: Dictionary = triangle_mesh.intersect_ray(local_ray_origin, local_ray_direction)
|
|
||||||
if intersection.is_empty():
|
|
||||||
return null
|
|
||||||
var local_hit: Vector3 = intersection.get("position", Vector3.ZERO)
|
|
||||||
var world_hit := tile_root.global_transform * local_hit
|
|
||||||
return world_hit.y
|
|
||||||
|
|
||||||
|
|
||||||
func _tile_runtime_diagnostic(world: Node3D, tile_key: String, world_position: Vector3) -> Dictionary:
|
|
||||||
var available_tiles: Dictionary = world.get("_available_tiles")
|
|
||||||
var loading_tasks: Dictionary = world.get("_tile_loading_tasks")
|
|
||||||
var tile_states: Dictionary = world.get("_tile_states")
|
|
||||||
var load_queue: Array = world.get("_tile_load_queue")
|
|
||||||
var queued_index := -1
|
|
||||||
for index in load_queue.size():
|
|
||||||
var request: Dictionary = load_queue[index]
|
|
||||||
if String(request.get("key", "")) == tile_key:
|
|
||||||
queued_index = index
|
|
||||||
break
|
|
||||||
var diagnostic := {
|
|
||||||
"available": available_tiles.has(tile_key),
|
|
||||||
"queued_index": queued_index,
|
|
||||||
"loading": loading_tasks.has(tile_key),
|
|
||||||
"state_present": tile_states.has(tile_key),
|
|
||||||
"load_queue_size": load_queue.size(),
|
|
||||||
}
|
|
||||||
if tile_states.has(tile_key):
|
|
||||||
var state: Dictionary = tile_states[tile_key]
|
|
||||||
var quality_mesh: Mesh = state.get("quality_terrain_mesh", null)
|
|
||||||
var tile_lod_mesh: Mesh = state.get("tile_lod_mesh", null)
|
|
||||||
diagnostic["quality_mesh_present"] = quality_mesh != null
|
|
||||||
diagnostic["tile_lod_mesh_present"] = tile_lod_mesh != null
|
|
||||||
diagnostic["quality_source"] = String(state.get("quality_terrain_source", ""))
|
|
||||||
diagnostic["tile_lod"] = int(state.get("tile_lod", -1))
|
|
||||||
var tile_root := state.get("root", null) as Node3D
|
|
||||||
if tile_root != null:
|
|
||||||
var local_position := tile_root.global_transform.affine_inverse() * world_position
|
|
||||||
diagnostic["tile_root_position"] = _vector3_array(tile_root.global_position)
|
|
||||||
diagnostic["probe_local_position"] = _vector3_array(local_position)
|
|
||||||
var diagnostic_mesh := quality_mesh if quality_mesh != null else tile_lod_mesh
|
|
||||||
if diagnostic_mesh != null:
|
|
||||||
var mesh_aabb := diagnostic_mesh.get_aabb()
|
|
||||||
diagnostic["mesh_aabb_position"] = _vector3_array(mesh_aabb.position)
|
|
||||||
diagnostic["mesh_aabb_size"] = _vector3_array(mesh_aabb.size)
|
|
||||||
return diagnostic
|
|
||||||
|
|
||||||
|
|
||||||
func _nearest_terrain_sample(world: Node3D, world_position: Vector3) -> Dictionary:
|
|
||||||
var tile_states: Dictionary = world.get("_tile_states")
|
|
||||||
for radius in [2.0, 5.0, 10.0, 20.0, 40.0]:
|
|
||||||
for offset in [Vector2(radius, 0.0), Vector2(-radius, 0.0), Vector2(0.0, radius), Vector2(0.0, -radius)]:
|
|
||||||
var sample_position := world_position + Vector3(offset.x, 0.0, offset.y)
|
|
||||||
var sample_tile := _adt_tile_vector2i(sample_position)
|
|
||||||
var sample_key := "%d_%d" % [sample_tile.x, sample_tile.y]
|
|
||||||
if not tile_states.has(sample_key):
|
|
||||||
continue
|
|
||||||
var height_variant = _intersect_terrain_state(tile_states[sample_key], sample_position)
|
|
||||||
if height_variant != null:
|
|
||||||
return {
|
|
||||||
"nearest_sample_distance": radius,
|
|
||||||
"nearest_sample_height": float(height_variant),
|
|
||||||
"nearest_sample_tile": sample_key,
|
|
||||||
}
|
|
||||||
return {"nearest_sample_distance": null}
|
|
||||||
|
|
||||||
|
|
||||||
func _adt_tile_vector2i(world_position: Vector3) -> Vector2i:
|
|
||||||
var typed_world_position = GODOT_WORLD_POSITION_SCRIPT.new(world_position.x, world_position.y, world_position.z)
|
|
||||||
var tile_coordinate = COORDINATE_MAPPER_SCRIPT.godot_to_adt_tile(typed_world_position)
|
|
||||||
return Vector2i(tile_coordinate.tile_x, tile_coordinate.tile_y)
|
|
||||||
|
|
||||||
|
|
||||||
func _vector3_array(value: Vector3) -> Array[float]:
|
|
||||||
return [value.x, value.y, value.z]
|
|
||||||
|
|
||||||
|
|
||||||
func _vector3(value_variant) -> Vector3:
|
func _vector3(value_variant) -> Vector3:
|
||||||
if not (value_variant is Array) or value_variant.size() != 3:
|
if not (value_variant is Array) or value_variant.size() != 3:
|
||||||
return Vector3.ZERO
|
return Vector3.ZERO
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ const REQUIRED_MAPPER_CALLS := {
|
|||||||
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
|
"res://src/render/streaming/streaming_target_planner.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local"],
|
||||||
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
"res://src/scenes/player/third_person_wow_controller.gd": ["adt_tile_local_to_godot"],
|
||||||
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
"res://src/gameplay/terrain/adt_terrain_query.gd": ["godot_to_adt_tile", "godot_to_adt_tile_local", "adt_tile_local_to_chunk"],
|
||||||
"res://src/tools/probe_render_terrain_height.gd": ["godot_to_adt_tile"],
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -61,7 +60,7 @@ func _initialize() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=6 classifier_cases=6" % source_paths.size())
|
print("COORDINATE_CONVERSION_BOUNDARIES PASS files=%d native_boundary=1 oracle=1 consumers=5 classifier_cases=6" % source_paths.size())
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,9 @@ extends SceneTree
|
|||||||
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
const GAMEPLAY_ROOT := "res://src/gameplay"
|
const GAMEPLAY_ROOT := "res://src/gameplay"
|
||||||
const SOURCE_ROOTS: Array[String] = ["res://src", "res://addons"]
|
const SOURCE_ROOTS: Array[String] = ["res://src", "res://addons"]
|
||||||
|
const RENDERER_DIAGNOSTIC_PATHS: Array[String] = [
|
||||||
|
"res://src/tools/probe_render_terrain_height.gd",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
@@ -20,6 +23,8 @@ func _initialize() -> void:
|
|||||||
consumer_paths_by_path[gameplay_path] = true
|
consumer_paths_by_path[gameplay_path] = true
|
||||||
for editor_source_path in editor_source_paths:
|
for editor_source_path in editor_source_paths:
|
||||||
consumer_paths_by_path[editor_source_path] = true
|
consumer_paths_by_path[editor_source_path] = true
|
||||||
|
for renderer_diagnostic_path in RENDERER_DIAGNOSTIC_PATHS:
|
||||||
|
consumer_paths_by_path[renderer_diagnostic_path] = true
|
||||||
|
|
||||||
for consumer_path_variant in consumer_paths_by_path:
|
for consumer_path_variant in consumer_paths_by_path:
|
||||||
var consumer_path := String(consumer_path_variant)
|
var consumer_path := String(consumer_path_variant)
|
||||||
@@ -33,10 +38,11 @@ func _initialize() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("RENDERER_INTERNAL_ACCESS PASS private_symbols=%d gameplay=%d editor_sources=%d" % [
|
print("RENDERER_INTERNAL_ACCESS PASS private_symbols=%d gameplay=%d editor_sources=%d renderer_tools=%d" % [
|
||||||
private_renderer_symbols.size(),
|
private_renderer_symbols.size(),
|
||||||
gameplay_paths.size(),
|
gameplay_paths.size(),
|
||||||
editor_source_paths.size(),
|
editor_source_paths.size(),
|
||||||
|
RENDERER_DIAGNOSTIC_PATHS.size(),
|
||||||
])
|
])
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ extends SceneTree
|
|||||||
const WORLD_RENDER_FACADE_SCRIPT := preload("res://src/render/world_render_facade.gd")
|
const WORLD_RENDER_FACADE_SCRIPT := preload("res://src/render/world_render_facade.gd")
|
||||||
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.gd")
|
const GODOT_WORLD_POSITION_SCRIPT := preload("res://src/domain/coordinates/godot_world_position.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")
|
||||||
|
const RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
|
||||||
|
|
||||||
const RUNTIME_SCENE_PATHS: Array[String] = [
|
const RUNTIME_SCENE_PATHS: Array[String] = [
|
||||||
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
"res://src/scenes/streaming/eastern_kingdoms_streaming.tscn",
|
||||||
@@ -15,12 +16,19 @@ const TOOL_PATHS: Array[String] = [
|
|||||||
"res://src/tools/probe_render_camera_occluders.gd",
|
"res://src/tools/probe_render_camera_occluders.gd",
|
||||||
"res://src/tools/probe_render_terrain_height.gd",
|
"res://src/tools/probe_render_terrain_height.gd",
|
||||||
]
|
]
|
||||||
|
const STREAMING_WORLD_LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
class StreamingWorldLoaderDouble extends Node:
|
class StreamingWorldLoaderDouble extends Node:
|
||||||
var current_focus: StreamingFocus
|
var current_focus: StreamingFocus
|
||||||
var refresh_count := 0
|
var refresh_count := 0
|
||||||
var metrics := {"tiles": 2, "queues": {"tile": 1}}
|
var metrics := {"tiles": 2, "queues": {"tile": 1}}
|
||||||
|
var ground_sample: RefCounted = RENDERED_GROUND_SAMPLE_SCRIPT.available(42.25)
|
||||||
|
var ground_snapshot := {
|
||||||
|
"status": "sampled",
|
||||||
|
"terrain_height": 42.25,
|
||||||
|
"diagnostic": {"state_present": true},
|
||||||
|
}
|
||||||
|
|
||||||
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
func set_streaming_focus(streaming_focus: StreamingFocus) -> void:
|
||||||
current_focus = streaming_focus
|
current_focus = streaming_focus
|
||||||
@@ -32,9 +40,16 @@ class StreamingWorldLoaderDouble extends Node:
|
|||||||
func render_baseline_snapshot() -> Dictionary:
|
func render_baseline_snapshot() -> Dictionary:
|
||||||
return metrics
|
return metrics
|
||||||
|
|
||||||
|
func sample_rendered_ground_height(_godot_world_position: GodotWorldPosition) -> RefCounted:
|
||||||
|
return ground_sample
|
||||||
|
|
||||||
|
func render_ground_query_snapshot(_godot_world_position: GodotWorldPosition) -> Dictionary:
|
||||||
|
return ground_snapshot
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
var failures: Array[String] = []
|
var failures: Array[String] = []
|
||||||
|
_verify_rendered_ground_sample_contract(failures)
|
||||||
await _verify_delegation_and_snapshot_isolation(failures)
|
await _verify_delegation_and_snapshot_isolation(failures)
|
||||||
_verify_repository_wiring(failures)
|
_verify_repository_wiring(failures)
|
||||||
|
|
||||||
@@ -44,10 +59,26 @@ func _initialize() -> void:
|
|||||||
quit(1)
|
quit(1)
|
||||||
return
|
return
|
||||||
|
|
||||||
print("WORLD_RENDER_FACADE PASS delegation=3 runtime_scenes=2 tools=3")
|
print("WORLD_RENDER_FACADE PASS delegation=5 ground_contract=2 runtime_scenes=2 tools=3")
|
||||||
quit(0)
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_rendered_ground_sample_contract(failures: Array[String]) -> void:
|
||||||
|
var non_finite_sample = RENDERED_GROUND_SAMPLE_SCRIPT.available(INF)
|
||||||
|
_expect_true(not non_finite_sample.is_available, "non-finite rendered ground is unavailable", failures)
|
||||||
|
_expect_true(
|
||||||
|
non_finite_sample.failure_code == &"render_terrain_height_not_finite",
|
||||||
|
"non-finite rendered ground failure code",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var default_unavailable_sample = RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"")
|
||||||
|
_expect_true(
|
||||||
|
default_unavailable_sample.failure_code == &"render_terrain_unavailable",
|
||||||
|
"empty rendered ground failure code normalizes",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
||||||
var test_root := Node.new()
|
var test_root := Node.new()
|
||||||
var loader := StreamingWorldLoaderDouble.new()
|
var loader := StreamingWorldLoaderDouble.new()
|
||||||
@@ -82,6 +113,24 @@ func _verify_delegation_and_snapshot_isolation(failures: Array[String]) -> void:
|
|||||||
(snapshot["queues"] as Dictionary)["tile"] = 999
|
(snapshot["queues"] as Dictionary)["tile"] = 999
|
||||||
_expect_true(int(loader.metrics.tiles) == 2, "top-level metrics are detached", failures)
|
_expect_true(int(loader.metrics.tiles) == 2, "top-level metrics are detached", failures)
|
||||||
_expect_true(int(loader.metrics.queues.tile) == 1, "nested metrics are detached", failures)
|
_expect_true(int(loader.metrics.queues.tile) == 1, "nested metrics are detached", failures)
|
||||||
|
|
||||||
|
var query_position = GODOT_WORLD_POSITION_SCRIPT.new(100.0, 200.0, 300.0)
|
||||||
|
var ground_sample: RefCounted = facade.sample_ground_height(query_position)
|
||||||
|
_expect_true(ground_sample.is_available, "rendered ground sample delegates", failures)
|
||||||
|
_expect_near(ground_sample.height_units, 42.25, "rendered ground height", failures)
|
||||||
|
loader.ground_sample = RENDERED_GROUND_SAMPLE_SCRIPT.unavailable(&"render_terrain_mesh_not_ready")
|
||||||
|
var unavailable_ground_sample: RefCounted = facade.sample_ground_height(query_position)
|
||||||
|
_expect_true(not unavailable_ground_sample.is_available, "unavailable ground sample delegates", failures)
|
||||||
|
_expect_true(
|
||||||
|
unavailable_ground_sample.failure_code == &"render_terrain_mesh_not_ready",
|
||||||
|
"ground failure code delegates",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
var ground_snapshot: Dictionary = facade.renderer_ground_query_snapshot(query_position)
|
||||||
|
ground_snapshot["terrain_height"] = 999.0
|
||||||
|
(ground_snapshot["diagnostic"] as Dictionary)["state_present"] = false
|
||||||
|
_expect_near(float(loader.ground_snapshot.terrain_height), 42.25, "ground snapshot top-level detached", failures)
|
||||||
|
_expect_true(bool(loader.ground_snapshot.diagnostic.state_present), "ground snapshot nested detached", failures)
|
||||||
test_root.queue_free()
|
test_root.queue_free()
|
||||||
|
|
||||||
|
|
||||||
@@ -98,6 +147,44 @@ func _verify_repository_wiring(failures: Array[String]) -> void:
|
|||||||
_expect_true(not tool_source.contains('world.call("refresh_streaming_focus"'), "%s avoids direct streamer refresh" % tool_path, failures)
|
_expect_true(not tool_source.contains('world.call("refresh_streaming_focus"'), "%s avoids direct streamer refresh" % tool_path, failures)
|
||||||
_expect_true(not tool_source.contains('world.set("streaming_focus_source_path"'), "%s avoids direct streamer focus config" % tool_path, failures)
|
_expect_true(not tool_source.contains('world.set("streaming_focus_source_path"'), "%s avoids direct streamer focus config" % tool_path, failures)
|
||||||
|
|
||||||
|
var terrain_probe_source := _read_text("res://src/tools/probe_render_terrain_height.gd", failures)
|
||||||
|
_expect_true(
|
||||||
|
terrain_probe_source.contains('"renderer_ground_query_snapshot"'),
|
||||||
|
"terrain probe delegates ground diagnostics",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
not terrain_probe_source.contains('world.get("_'),
|
||||||
|
"terrain probe avoids private streamer state",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for forbidden_probe_text in [
|
||||||
|
"_tile_states",
|
||||||
|
"_tile_load_queue",
|
||||||
|
"_tile_loading_tasks",
|
||||||
|
"_available_tiles",
|
||||||
|
"generate_triangle_mesh",
|
||||||
|
]:
|
||||||
|
_expect_true(
|
||||||
|
not terrain_probe_source.contains(forbidden_probe_text),
|
||||||
|
"terrain probe omits %s" % forbidden_probe_text,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
var loader_source := _read_text(STREAMING_WORLD_LOADER_PATH, failures)
|
||||||
|
for required_loader_text in [
|
||||||
|
"func sample_rendered_ground_height(godot_world_position: GodotWorldPosition)",
|
||||||
|
"func render_ground_query_snapshot(godot_world_position: GodotWorldPosition)",
|
||||||
|
"RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0",
|
||||||
|
"for radius_units in [2.0, 5.0, 10.0, 20.0, 40.0]",
|
||||||
|
"generate_triangle_mesh()",
|
||||||
|
]:
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains(required_loader_text),
|
||||||
|
"streamer ground query retains %s" % required_loader_text,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
func _read_text(path: String, failures: Array[String]) -> String:
|
func _read_text(path: String, failures: Array[String]) -> String:
|
||||||
var file := FileAccess.open(path, FileAccess.READ)
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
|||||||
Reference in New Issue
Block a user