Compare commits

...

8 Commits

15 changed files with 594 additions and 162 deletions
+19 -3
View File
@@ -929,11 +929,27 @@ $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.
- Its factories load the predeclared Script path instead of relying on a warm
global-class cache; a dedicated cold-start verifier covers this bootstrap path.
- `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.
@@ -0,0 +1,115 @@
# M03-RND-FACADE-GROUND-QUERY-001 — Rendered ground query facade
<!-- OPENWC_CLAIM:M03-RND-FACADE-GROUND-QUERY-001:sindo-main-codex:2026-07-18 -->
## Ownership
- Target: M03
- Program: RND
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m03-facade-ground-query`
- Lease expires UTC: 2026-07-18
- Integrator: M03 milestone integrator
## Outcome
Expose typed ground-height sampling and a detached readiness diagnostic through
`WorldRenderFacade`, using already loaded render terrain meshes, and migrate the
terrain-height probe away from direct streamer state/queue access.
## Non-goals
- Replace the M02 gameplay-owned `TerrainQuery`/`AdtTerrainQuery` composition.
- Make rendered mesh sampling authoritative for movement, collision or server state.
- Add slopes, holes, liquid, WMO/M2 collision or async query callbacks.
- Change terrain meshes, streaming policy, queue order, cache formats or visuals.
- Complete environment/entity-visual facade contracts or mark M03 complete.
## Paths
- Exclusive: this claim
- Shared/hotspots: `src/render/world_render_facade.gd`,
`src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/probe_render_terrain_height.gd`,
`src/tools/verify_world_render_facade.gd`,
`src/tools/verify_renderer_internal_access.gd`, `docs/modules/world-renderer.md`,
`docs/modules/terrain-query.md`, `RENDER.md`, `targets/03-renderer-facade.md`
- Generated/ignored: `.godot`, native DLL, generated ADT resource scripts,
extracted renderer corpus and probe reports
## Contracts and data
- Input: immutable `GodotWorldPosition` in Godot world units
- Output: renderer-owned immutable `RenderedGroundSample` plus optional detached diagnostic Dictionary
- Sampling source: loaded quality or tile-LOD Mesh and its live tile transform
- M02 ADT query cache, renderer cache versions and coordinate contracts: unchanged
## Dependencies
- Requires: M02 terrain query contract and integrated M03 facade/access gate `53d84cc`
- Blocks: removal of renderer diagnostic internal-access exception and later terrain service extraction
- External state: asset-backed probe is optional; contract tests use a facade double
## Verification
- Commands: facade query/snapshot isolation contract, internal-access gate,
terrain-query regression, focus/coordinate/manifest/shutdown regressions and repository gates
- Fixtures: available/unavailable sample, detached diagnostic, probe source migration
- Fidelity evidence: mesh ray/nearest fallback algorithm moves unchanged from the probe
- Performance budget: on-demand diagnostic query only; no per-frame query added
## Documentation deliverables
- Inline facade/streamer API docs
- World-renderer API/data-flow/sequence/ownership/recovery/source-map updates
- Terrain-query backend status/ownership clarification and renderer implementation note
## Simplicity and naming
- Important names: `sample_ground_height`, `renderer_ground_query_snapshot`
- Simplest approach: one typed facade query over one existing diagnostic read model
- Rejected complexity: duplicate ADT cache, physics service, callback bus, query node,
generic spatial interface or new dependency
- Unavoidable complexity: triangle-mesh ray query retains the existing bounded
nearby fallback for seam/edge diagnostic behavior
## Status
- State: ready-for-review
- Done: added renderer-owned typed sample, facade query/snapshot, migrated terrain
probe, expanded internal-access/coordinate gates and updated module docs
- Next: M03 integrator reviews and merges `b697a89` plus cold-start fix `9408d88`
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-FACADE-GROUND-QUERY-001:9408d88 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-FACADE-GROUND-QUERY-001:d6e5b53 -->
## Handoff
- Commits: implementation `b697a89`; cold-start fix `9408d88`
- Results: `WorldRenderFacade` delegates loaded-mesh ground sampling and returns
renderer-owned `RenderedGroundSample`; detached diagnostic snapshots preserve
the probe report while hiding queue/tile/Mesh/Node references. Gameplay remains
on the independent M02 `TerrainQuery` composition.
- Verification: facade `delegation=5 ground_contract=2 runtime_scenes=2 tools=3`;
cold rendered-ground value `contract=3` from an empty Godot class cache;
internal access `private_symbols=43 gameplay=7 editor_sources=9 renderer_tools=1`;
terrain query contracts passed; coordinate boundary `files=113 consumers=5`;
renderer manifest `7/7`; shutdown ownership, scheduler/planner, coordination,
documentation and diff gates passed.
- Fidelity: the probe's 5000-unit ray height, 3x3 tile search, highest-hit choice,
readiness fields and `2/5/10/20/40` nearby fallback moved unchanged. No terrain
geometry, streaming, cache, material, placement or visual rule changed. No
build-12340 parity claim is added.
- External/local inputs: ignored native DLL and generated ADT resource scripts
were copied from master only for editor/streamer parse and then removed.
Proprietary extracted world data was unavailable, producing expected degraded diagnostics.
- Remaining risks: triangle-mesh generation is on-demand/main-thread and diagnostic;
it is unsuitable for per-physics-frame gameplay; exact asset-backed probe output
still depends on local corpus readiness; environment/entity facade contracts remain.
- Documentation: inline facade/loader/value API; world-renderer API/data-flow/
sequence/ownership/recovery/capability/source map; terrain-query authority
clarification and renderer implementation notes updated.
- Integration: implementation accepted in master merge `80cb084`; cold-start fix
accepted in merge `d6e5b53`. Post-merge cold/facade/access/terrain/coordinate/
manifest/shutdown/scheduler/planner/focus and repository gates passed.
+11 -3
View File
@@ -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 |
+35 -13
View File
@@ -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,10 @@ 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.
- Rendered-ground value contract: available/non-finite/unavailable invariants pass
from a cold Godot class cache before editor import.
- 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 +320,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 +345,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 +362,11 @@ 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_rendered_ground_sample.gd` | Cold-start renderer ground result invariant 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 +382,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,52 @@
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
const SCRIPT_PATH := "res://src/render/terrain/rendered_ground_sample.gd"
## Creates an available finite rendered-ground sample in Godot world units.
static func available(height_units_value: float) -> RefCounted:
return load(SCRIPT_PATH).new(true, height_units_value, &"")
## Creates an unavailable rendered-ground sample with a stable failure code.
static func unavailable(failure_code_value: StringName) -> RefCounted:
return load(SCRIPT_PATH).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
+30
View File
@@ -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
+9 -134
View File
@@ -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)
@@ -0,0 +1,45 @@
extends SceneTree
## Cold-start contract for the renderer-owned ground query result value.
const SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
func _initialize() -> void:
var failures: Array[String] = []
var available_sample: RefCounted = SAMPLE_SCRIPT.available(42.25)
_expect_true(bool(available_sample.get("is_available")), "available sample", failures)
_expect_near(float(available_sample.get("height_units")), 42.25, "available height", failures)
var non_finite_sample: RefCounted = SAMPLE_SCRIPT.available(INF)
_expect_true(not bool(non_finite_sample.get("is_available")), "non-finite unavailable", failures)
_expect_true(
StringName(non_finite_sample.get("failure_code")) == &"render_terrain_height_not_finite",
"non-finite failure code",
failures
)
var unavailable_sample: RefCounted = SAMPLE_SCRIPT.unavailable(&"")
_expect_true(
StringName(unavailable_sample.get("failure_code")) == &"render_terrain_unavailable",
"empty failure code normalization",
failures
)
if not failures.is_empty():
for failure in failures:
push_error("RENDERED_GROUND_SAMPLE: %s" % failure)
quit(1)
return
print("RENDERED_GROUND_SAMPLE PASS contract=3")
quit(0)
func _expect_near(actual_value: float, expected_value: float, label: String, failures: Array[String]) -> void:
if not is_equal_approx(actual_value, expected_value):
failures.append("%s expected %.3f, got %.3f" % [label, expected_value, actual_value])
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://b8juyikyvx1xw
+7 -1
View File
@@ -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)
+88 -1
View File
@@ -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)
+11 -5
View File
@@ -27,8 +27,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence ## Evidence
- Date: 2026-07-16 - Date: 2026-07-16
- Revision/worktree: master merges `7ece2ab`, `7e35de7`; packages - Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`;
`M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001` packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
`M03-RND-FACADE-GROUND-QUERY-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
@@ -36,15 +37,20 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`files=111 consumers=6`; renderer manifest retained `7/7` checkpoints; `files=111 consumers=6`; renderer manifest retained `7/7` checkpoints;
shutdown ownership and repository gates passed. Internal-access gate additionally shutdown ownership and repository gates passed. Internal-access gate additionally
derived `43` private symbols and found no forbidden access across `7` gameplay derived `43` private symbols and found no forbidden access across `7` gameplay
and `9` EditorPlugin-package sources (13 pre-existing expired M00 claim warnings). and `9` EditorPlugin-package sources plus the terrain probe. Ground facade passed
`delegation=5 ground_contract=2`; cold `RenderedGroundSample contract=3`;
M02 terrain-query regression remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - Fidelity comparison: all 16 historical operation limits and drain sites were
preserved; chunk removal/create retain their shared removal-first budget. preserved; chunk removal/create retain their shared removal-first budget.
Defaults, quality presets, cache versions and visual rules are unchanged. Defaults, quality presets, cache versions and visual rules are unchanged.
Rendered-ground diagnostics preserve the prior 5000-unit ray, 3x3 search and
`2/5/10/20/40`-unit nearby fallback while hiding mutable renderer state.
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending. No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
- Changed files: scene-free scheduler and verifier; streamer budget composition/ - Changed files: scene-free scheduler and verifier; streamer budget composition/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes; permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
gameplay/editor internal-access verifier; work-package claims and this evidence. renderer-owned ground sample, facade/loader ground query, migrated terrain probe,
expanded internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: queue storage length and worker concurrency/stale-result logic - Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt remain streamer-owned; cancellation stops new permits but does not interrupt
in-flight work; M03 still needs remaining facade contracts, service extraction in-flight work; M03 still needs environment/entity facade contracts, service extraction
and dependency/performance acceptance. and dependency/performance acceptance.