refactor(M03): extract terrain quality mesh cache

This commit is contained in:
2026-07-16 09:29:38 +04:00
parent 6617dfe74e
commit 97480e06bb
9 changed files with 379 additions and 39 deletions
+12
View File
@@ -979,6 +979,18 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- PackedScene loading is currently synchronous on create/replace and is a documented - PackedScene loading is currently synchronous on create/replace and is a documented
prototype limitation, not an accepted network-scale frame-critical path. prototype limitation, not an accepted network-scale frame-critical path.
## 2026-07-16 Terrain Quality Mesh Cache Extraction
- `TerrainQualityMeshCache` now owns full-quality terrain revisit Mesh references,
tile-key LRU order and source admission outside `StreamingWorldLoader`.
- The historical rules are unchanged: successful restore touches newest; store
trims oldest to `terrain_quality_mesh_cache_limit`; zero/negative capacity keeps
no entries; control-splat/splat sources are never retained in this revisit cache.
- The loader keeps three narrow adapters: restore into tile state, store after a
full-quality result and clear during existing world reset/shutdown.
- ADT parsing, quality tasks/results, tile state, cache format versions, material/
Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
## 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.
+1
View File
@@ -12,6 +12,7 @@
| Player input | Partial | [`player-input.md`](player-input.md) | | Player input | Partial | [`player-input.md`](player-input.md) |
| Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.md) | | Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.md) |
| Terrain query | Implemented | [`terrain-query.md`](terrain-query.md) | | Terrain query | Implemented | [`terrain-query.md`](terrain-query.md) |
| Terrain quality Mesh cache | Implemented extraction | [`terrain-quality-mesh-cache.md`](terrain-quality-mesh-cache.md) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) | | Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
+198
View File
@@ -0,0 +1,198 @@
# Terrain Quality Mesh Cache
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-TERRAIN-CACHE-SERVICE-001` |
| Owners | Full-quality terrain revisit Mesh retention and LRU policy |
| Last verified | Worktree `work/sindo-main-codex/m03-terrain-quality-cache`, 2026-07-16 |
| Profiles/capabilities | Performance/Balanced/High/Custom capacity; session-memory only |
## Purpose
Retain eligible full-quality terrain Mesh references after ADT tile state unload,
so a revisit can restore quality without flashing to a lower-quality fallback,
while bounding retained references with the existing least-recently-used policy.
## Non-goals
- Parse/build terrain, own tile state or schedule quality-upgrade tasks.
- Create/finalize Nodes or RIDs, mutate materials or choose terrain LOD.
- Persist cache entries or change baked/control-splat/splat format versions.
- Cache control-splat/splat sources whose active radius has separate ownership.
- Provide a generic cache framework or change profile capacity defaults.
## Context and boundaries
```mermaid
flowchart LR
Upgrade[Full-quality baked upgrade result] --> Loader[StreamingWorldLoader adapter]
Loader --> Cache[TerrainQualityMeshCache]
Cache -->|retained Mesh reference| Revisit[Reloaded tile state]
Revisit --> Finalize[Existing tile LOD/finalization path]
Splat[Control/splat quality sources] -. rejected .-> Cache
```
Allowed dependencies: `Mesh`, Dictionary/Array scalar containers and the loader
adapter. Forbidden dependencies: Node/SceneTree/RID, tasks/mutexes, ADT parsing,
gameplay/network/editor UI, files and persistent Resource cache ownership.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `TerrainQualityMeshCache.store(tile_key, mesh, source, capacity)` | Command | Admit/touch a Mesh and trim oldest entries | Owning renderer thread/session | Returns false for rejected input or immediate zero-capacity eviction |
| `TerrainQualityMeshCache.restore(tile_key)` | Query/touch | Return a fresh Mesh/source record and mark newest | Owning renderer thread | Empty on miss; invalid record is removed |
| `TerrainQualityMeshCache.clear()` | Lifecycle command | Release all retained Mesh references and LRU keys | Reset/shutdown | Idempotent |
| `TerrainQualityMeshCache.diagnostic_snapshot()` | Read model | Detached count and oldest-to-newest key/source list | Caller-owned result | Never exposes Mesh references |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Tile key | Streamer tile catalog/state | Cache index | Copied string | Map session |
| Input | Full-quality Mesh | Baked upgrade/fallback path | Cache | Retained shared reference | Until eviction/clear |
| Input | Quality source | Streamer quality adapter | Admission/restore | Copied string | Entry lifetime |
| Input | Current capacity | Active renderer profile | Store trim | Scalar copy | One store |
| Output | Fresh `{mesh, source}` record | Cache | Loader state adapter | Caller dictionary; shared Mesh reference | One restore |
| Output | Detached diagnostics | Cache | Tests/future metrics | Caller-owned scalars | One query |
Side effects are limited to retained reference and LRU container mutation. There
is no scene, GPU, task, file, database, network or persistent-cache mutation.
## Data flow
```mermaid
flowchart TD
Store[store tile/mesh/source/capacity] --> Admit{Valid key + Mesh + source?}
Admit -->|no| Reject[Return false]
Admit -->|yes| Touch[Insert/replace and touch newest]
Touch --> Trim[Evict oldest until count <= capacity]
Restore[restore tile] --> Hit{Valid admitted entry?}
Hit -->|no| Miss[Remove invalid / return empty]
Hit -->|yes| TouchRestore[Touch newest and return fresh record]
Clear[world reset/shutdown] --> Empty[Release all references and keys]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Empty
Empty --> Populated: admitted store with capacity > 0
Populated --> Populated: store / restore touch / bounded eviction
Populated --> Empty: capacity zero store evicts all
Populated --> Empty: clear on reset/shutdown
Empty --> Empty: rejected store / miss / clear
Empty --> [*]
Populated --> [*]: owner releases cache
```
## Main sequence
```mermaid
sequenceDiagram
participant Upgrade as Terrain upgrade result
participant Loader as StreamingWorldLoader
participant Cache as TerrainQualityMeshCache
Upgrade->>Loader: full Mesh + baked_full source
Loader->>Cache: store(key, mesh, source, profile capacity)
Note over Loader,Cache: tile state may later unload
Loader->>Cache: restore(key) on tile creation/revisit
Cache-->>Loader: fresh record or empty
Loader->>Loader: apply Mesh/source to existing tile state
Loader->>Cache: clear() on world reset/shutdown
```
## Ownership, threading and resources
- The cache owns its dictionary, ordered tile keys and retained Mesh references.
- The loader retains tile state, async tasks/results, quality selection and all
Node/RID/material finalization.
- Returned records are new dictionaries; their Mesh is a shared reference.
- Calls currently occur on the main renderer thread. The service has no lock and
must not be mutated concurrently.
- `clear` deterministically drops references during the existing world reset path.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Empty key/null Mesh | Store guard | Reject unchanged | Boolean/test | Fix caller result |
| Splat/control source | Admission list | Reject unchanged | Boolean/test | Keep source in active-radius ownership |
| Zero/negative capacity | Clamp and trim | Entry immediately evicted; cache empty | Snapshot/count | Increase profile capacity |
| Miss/invalid entry | Restore validation | Remove invalid and return empty | Empty record | Existing loader fallback/rebuild |
| World reset/shutdown | Loader lifecycle | Clear every reference/key | Shutdown regression | Refill during next session |
There is no asynchronous work or cancellation token inside this service.
## Configuration and capabilities
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|---|---|---|---|---|
| `terrain_quality_mesh_cache_limit` | Existing loader value | `0/24/48` in current presets | Read on every store | Maximum retained entries |
| Admitted source | Non-splat (currently `baked_full`) | All | No hidden override | Revisit retention |
| Excluded sources | `control_splat_cache`, `splat_cache`, `splat` | All | No | Preserve active quality ownership |
## Persistence, cache and migration
- This is an in-memory reference cache, not a serialized cache format.
- `REQUIRED_BAKED_TILE_FORMAT_VERSION=5`, control splat v3 and splat v1 remain
loader/resource validation contracts and are unchanged.
- No migration or rebuild is required. Entry keys retain the existing tile-key form.
## Diagnostics and observability
- Detached diagnostics report count and oldest-to-newest tile key/source entries.
- Mesh, material, Node and RID references are deliberately absent.
- Performance is bounded dictionary lookup plus one ordered-array erase/append;
no new optimization or scale claim is made.
## Verification
- `verify_terrain_quality_mesh_cache.gd`: admission, exclusions, Mesh identity,
source, LRU touch/eviction, detached diagnostics, negative capacity and clear.
- Loader source boundary proves cache composition/delegation and removal of its
former LRU array/touch/trim implementation.
- Existing shutdown, facade, internal-access, manifest, planner and scheduler
regressions remain required.
- Fidelity: source rules, capacities and three loader adapter sites are unchanged;
no visible terrain or build-12340 parity change is claimed.
## Extension points
- Expose scalar cache counts through facade metrics when needed.
- Move additional terrain responsibilities only as separate bounded services.
- Replace ordered-array LRU only after measured scale evidence.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Bounded quality Mesh retention | Implemented extraction | Contract/LRU fixtures | Asset-backed memory/revisit timing pending |
| Deterministic reset ownership | Implemented | Clear path plus shutdown regression | Full terrain service still monolithic |
| Persistent terrain cache formats | Unchanged | Loader version constants | Separate acceptance audit remains |
## Known gaps and risks
- Ordered-array touch is linear in entry count, matching prior behavior.
- Mesh memory size is not measured; capacity remains entry-count based.
- Only revisit Mesh ownership moved; build/tasks/state/finalization remain loader-owned.
- Asset-backed flash/revisit timing and p95/p99 evidence remain pending.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Session cache entries, LRU and admission |
| `src/scenes/streaming/streaming_world_loader.gd` | Store/restore state adapter and reset lifecycle |
| `src/tools/verify_terrain_quality_mesh_cache.gd` | Scene-free cache and loader-boundary regression |
## Related decisions and references
- [`world-renderer.md`](world-renderer.md)
- [`terrain-query.md`](terrain-query.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
+14 -2
View File
@@ -5,9 +5,9 @@
| 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`; `M03-RND-INTERNAL-ACCESS-GATE-001`; `M03-RND-FACADE-GROUND-QUERY-001`; `M03-RND-FACADE-ENVIRONMENT-001`; `M03-RND-FACADE-ENTITY-001` | | Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; `M03-RND-TERRAIN-CACHE-SERVICE-001` |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-facade-entity`, 2026-07-16 | | Last verified | Worktree `work/sindo-main-codex/m03-terrain-quality-cache`, 2026-07-16 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -40,6 +40,8 @@ flowchart LR
Loader --> Budget[RenderBudgetScheduler] Loader --> Budget[RenderBudgetScheduler]
Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders] Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders]
Cache[Baked terrain/M2/WMO caches] --> Loader Cache[Baked terrain/M2/WMO caches] --> Loader
Loader --> TerrainCache[TerrainQualityMeshCache]
TerrainCache --> Loader
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
Loader --> Scene[SceneTree nodes] Loader --> Scene[SceneTree nodes]
@@ -103,6 +105,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 |
| `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only | | `runtime_stats_enabled` | Exported property | Enables periodic performance snapshot | Runtime mutable | Logging overhead only |
| `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only | | `hitch_profiler_enabled` | Exported property | Enables named hitch sections | Runtime mutable | Logging overhead only |
| `TerrainQualityMeshCache.store/restore/clear` | Internal terrain service | Owns admitted full-quality revisit Mesh references and LRU | Renderer thread/map session | Rejected/missed entry leaves loader fallback unchanged |
Публичным contract не считаются `StreamingWorldLoader` methods/properties, Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -124,6 +127,7 @@ loader configuration remains transitional composition data, not a caller API.
| Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime | | Test input | Server-spawn render manifest | Coordinate fixture/verifier | Checkpoint capture tool | Repository-owned diagnostic data | Test-process lifetime |
| Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh | | Output | `StreamingTargetPlan` | `StreamingTargetPlanner` | Streamer queue/state apply | Immutable ephemeral value | One refresh |
| Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame | | Internal control | Per-frame lane limits and boolean permits | Streamer configuration / `RenderBudgetScheduler` | Ordered streamer drains | Scheduler-owned counters | Main thread/frame |
| Internal cache | Full-quality terrain Mesh/source | Loader terrain upgrade / `TerrainQualityMeshCache` | Revisited tile state | Cache-retained Mesh reference | Map session until eviction/reset |
| 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 |
@@ -161,6 +165,8 @@ flowchart TD
T --> Q[Tile load queue] T --> Q[Tile load queue]
Q --> Parse[Worker parse/cache load] Q --> Parse[Worker parse/cache load]
Parse --> R[Result queues] Parse --> R[Result queues]
R --> TerrainCache[TerrainQualityMeshCache store]
TerrainCache --> Revisit[Revisited tile state restore]
R --> B[RenderBudgetScheduler permits] R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade] B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach] B --> M2[M2 group/MultiMesh attach]
@@ -263,6 +269,9 @@ sequenceDiagram
`EntityId`; it owns no authoritative entity state and excludes the local player. `EntityId`; it owns no authoritative entity state and excludes the local player.
- Entity presentation snapshots remain caller-owned inputs. Detached diagnostics - Entity presentation snapshots remain caller-owned inputs. Detached diagnostics
expose only identity debug keys, paths and visibility, never Node/Resource/RID. expose only identity debug keys, paths and visibility, never Node/Resource/RID.
- `TerrainQualityMeshCache` owns only full-quality revisit Mesh references and
LRU keys. The loader retains terrain tasks/results, tile state, source choice,
cache versions and every material/Node/RID finalization side effect.
- Rendered-ground query results and diagnostic snapshots are caller-owned values; - Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references. the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay - Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -377,6 +386,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Stable renderer facade | Implemented boundary / Partial fidelity | Facade contracts cover focus, environment, entity visuals, metrics and loaded-mesh ground query | Production consumers and fidelity remain | | Stable renderer facade | Implemented boundary / Partial fidelity | Facade contracts cover focus, environment, entity visuals, metrics and loaded-mesh ground query | Production consumers and fidelity remain |
| Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain | | Environment snapshot input | Implemented boundary / Partial fidelity | Immutable time contract, facade/controller delegation and checkpoint migration | Weather/indoor state and paired zone-transition fidelity remain |
| Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain | | Entity presentation commands | Implemented boundary / Prototype visuals | Versioned typed snapshot plus present/remove lifecycle and runtime service composition | Display/equipment/animation/effects and async scale path remain |
| Terrain quality Mesh cache service | Implemented extraction | Scene-free admission/LRU/clear contract and loader delegation | Remaining terrain build/state/finalization is monolithic |
| 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/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 | | 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 |
@@ -414,6 +424,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/entities/entity_presentation_snapshot.gd` | Immutable contract-v1 entity visual input and validation | | `src/render/entities/entity_presentation_snapshot.gd` | Immutable contract-v1 entity visual input and validation |
| `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual subtree ownership and lifecycle | | `src/render/entities/world_entity_presenter.gd` | Main-thread entity visual subtree ownership and lifecycle |
| `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result | | `src/render/terrain/rendered_ground_sample.gd` | Immutable renderer-owned available/unavailable ground query result |
| `src/render/terrain/terrain_quality_mesh_cache.gd` | Full-quality terrain revisit Mesh retention and LRU ownership |
| `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 |
@@ -425,6 +436,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_world_environment_snapshot.gd` | Cold-start environment value normalization and invalid-input regression | | `src/tools/verify_world_environment_snapshot.gd` | Cold-start environment value normalization and invalid-input regression |
| `src/tools/verify_world_entity_presentation.gd` | Entity snapshot, create/update/replace/remove and ownership regression | | `src/tools/verify_world_entity_presentation.gd` | Entity snapshot, create/update/replace/remove and ownership regression |
| `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression | | `src/tools/verify_rendered_ground_sample.gd` | Cold-start renderer ground result invariant regression |
| `src/tools/verify_terrain_quality_mesh_cache.gd` | Terrain cache admission, LRU, ownership and loader-boundary regression |
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression | | `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,73 @@
class_name TerrainQualityMeshCache
extends RefCounted
## Session-memory LRU owner for full-quality terrain Mesh references retained
## across tile unload/revisit. It owns no tile state, Node, RID, task or file.
const EXCLUDED_QUALITY_SOURCES: Array[String] = [
"control_splat_cache",
"splat_cache",
"splat",
]
var _entries_by_tile_key: Dictionary = {}
var _tile_keys_oldest_to_newest: Array[String] = []
## Stores an admitted Mesh reference, touches it as newest and trims oldest
## entries to the supplied non-negative capacity. Returns false when rejected.
func store(tile_key: String, mesh: Mesh, source: String, capacity: int) -> bool:
if tile_key.is_empty() or mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
return false
_entries_by_tile_key[tile_key] = {"mesh": mesh, "source": source}
_touch(tile_key)
_trim(maxi(0, capacity))
return _entries_by_tile_key.has(tile_key)
## Returns a fresh record containing the retained Mesh/source and touches the key
## as newest. Missing or invalid records return empty and are removed.
func restore(tile_key: String) -> Dictionary:
if not _entries_by_tile_key.has(tile_key):
return {}
var entry: Dictionary = _entries_by_tile_key[tile_key]
var source := String(entry.get("source", "baked_full"))
var mesh := entry.get("mesh") as Mesh
if mesh == null or EXCLUDED_QUALITY_SOURCES.has(source):
_remove(tile_key)
return {}
_touch(tile_key)
return {"mesh": mesh, "source": source}
## Releases every retained Mesh reference and resets LRU order.
func clear() -> void:
_entries_by_tile_key.clear()
_tile_keys_oldest_to_newest.clear()
## Returns detached scalar diagnostics without exposing retained Mesh references.
func diagnostic_snapshot() -> Dictionary:
var entries: Array[Dictionary] = []
for tile_key in _tile_keys_oldest_to_newest:
var entry: Dictionary = _entries_by_tile_key.get(tile_key, {})
entries.append({"tile_key": tile_key, "source": String(entry.get("source", ""))})
return {
"entry_count": entries.size(),
"entries_oldest_to_newest": entries,
}
func _touch(tile_key: String) -> void:
_tile_keys_oldest_to_newest.erase(tile_key)
_tile_keys_oldest_to_newest.append(tile_key)
func _trim(capacity: int) -> void:
while _tile_keys_oldest_to_newest.size() > capacity:
_remove(_tile_keys_oldest_to_newest.front())
func _remove(tile_key: String) -> void:
_entries_by_tile_key.erase(tile_key)
_tile_keys_oldest_to_newest.erase(tile_key)
@@ -0,0 +1 @@
uid://y576l2omqwox
+12 -37
View File
@@ -18,6 +18,7 @@ const COORDINATE_MAPPER_SCRIPT := preload("res://src/domain/coordinates/coordina
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 RENDERED_GROUND_SAMPLE_SCRIPT := preload("res://src/render/terrain/rendered_ground_sample.gd")
const TERRAIN_QUALITY_MESH_CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.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")
@@ -241,8 +242,7 @@ var _last_refresh_focus_pos := Vector3.ZERO
var _has_refresh_focus := false var _has_refresh_focus := false
var _tile_mesh_cache: Dictionary = {} var _tile_mesh_cache: Dictionary = {}
var _tile_mesh_cache_order: Array = [] var _tile_mesh_cache_order: Array = []
var _terrain_quality_mesh_cache: Dictionary = {} var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()
var _terrain_quality_mesh_cache_order: Array = []
var _wmo_prototype_cache: Dictionary = {} var _wmo_prototype_cache: Dictionary = {}
var _wmo_render_cache: Dictionary = {} var _wmo_render_cache: Dictionary = {}
var _wmo_render_missing_cache: Dictionary = {} var _wmo_render_missing_cache: Dictionary = {}
@@ -5500,53 +5500,28 @@ func _clear_tile_mesh_cache() -> void:
func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary: func _restore_quality_terrain_mesh(tile_key: String, state: Dictionary) -> Dictionary:
if not _terrain_quality_mesh_cache.has(tile_key): var cached: Dictionary = _terrain_quality_mesh_cache.call("restore", tile_key)
if cached.is_empty():
return state return state
var cached: Dictionary = _terrain_quality_mesh_cache[tile_key]
var source := String(cached.get("source", "baked_full")) var source := String(cached.get("source", "baked_full"))
if source == "control_splat_cache" or source == "splat_cache" or source == "splat":
_terrain_quality_mesh_cache.erase(tile_key)
_terrain_quality_mesh_cache_order.erase(tile_key)
return state
var mesh: Mesh = cached.get("mesh", null) var mesh: Mesh = cached.get("mesh", null)
if mesh == null:
_terrain_quality_mesh_cache.erase(tile_key)
_terrain_quality_mesh_cache_order.erase(tile_key)
return state
state["quality_terrain_mesh"] = mesh state["quality_terrain_mesh"] = mesh
state["quality_terrain_source"] = source state["quality_terrain_source"] = source
_touch_quality_terrain_mesh_cache(tile_key)
return state return state
func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void: func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void:
if tile_key.is_empty() or mesh == null: _terrain_quality_mesh_cache.call(
return "store",
if source == "control_splat_cache" or source == "splat_cache" or source == "splat": tile_key,
return mesh,
_terrain_quality_mesh_cache[tile_key] = { source,
"mesh": mesh, terrain_quality_mesh_cache_limit
"source": source, )
}
_touch_quality_terrain_mesh_cache(tile_key)
_trim_quality_terrain_mesh_cache()
func _touch_quality_terrain_mesh_cache(tile_key: String) -> void:
_terrain_quality_mesh_cache_order.erase(tile_key)
_terrain_quality_mesh_cache_order.append(tile_key)
func _trim_quality_terrain_mesh_cache() -> void:
var limit := maxi(0, terrain_quality_mesh_cache_limit)
while _terrain_quality_mesh_cache_order.size() > limit:
var oldest: String = _terrain_quality_mesh_cache_order.pop_front()
_terrain_quality_mesh_cache.erase(oldest)
func _clear_quality_terrain_mesh_cache() -> void: func _clear_quality_terrain_mesh_cache() -> void:
_terrain_quality_mesh_cache.clear() _terrain_quality_mesh_cache.call("clear")
_terrain_quality_mesh_cache_order.clear()
func _position_camera_over_world() -> void: func _position_camera_over_world() -> void:
@@ -0,0 +1,67 @@
extends SceneTree
## Scene-free contract, admission and LRU regression for terrain revisit meshes.
const CACHE_SCRIPT := preload("res://src/render/terrain/terrain_quality_mesh_cache.gd")
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_cache_contract(failures)
_verify_loader_boundary(failures)
if not failures.is_empty():
for failure in failures:
push_error("TERRAIN_QUALITY_MESH_CACHE: %s" % failure)
quit(1)
return
print("TERRAIN_QUALITY_MESH_CACHE PASS contract_cases=15 loader_calls=3")
quit(0)
func _verify_cache_contract(failures: Array[String]) -> void:
var cache: RefCounted = CACHE_SCRIPT.new()
var mesh_a := ArrayMesh.new()
var mesh_b := ArrayMesh.new()
var mesh_c := ArrayMesh.new()
_expect_true(not bool(cache.call("store", "", mesh_a, "baked_full", 2)), "empty key rejected", failures)
_expect_true(not bool(cache.call("store", "a", null, "baked_full", 2)), "null mesh rejected", failures)
for excluded_source in ["control_splat_cache", "splat_cache", "splat"]:
_expect_true(not bool(cache.call("store", "a", mesh_a, excluded_source, 2)), "%s rejected" % excluded_source, failures)
_expect_true(bool(cache.call("store", "a", mesh_a, "baked_full", 2)), "first store", failures)
_expect_true(bool(cache.call("store", "b", mesh_b, "baked_full", 2)), "second store", failures)
var restored_a: Dictionary = cache.call("restore", "a")
_expect_true(restored_a.get("mesh") == mesh_a, "restore retains mesh reference", failures)
_expect_true(String(restored_a.get("source", "")) == "baked_full", "restore retains source", failures)
_expect_true(bool(cache.call("store", "c", mesh_c, "baked_full", 2)), "third store", failures)
_expect_true((cache.call("restore", "b") as Dictionary).is_empty(), "oldest evicted after touch", failures)
_expect_true(not (cache.call("restore", "a") as Dictionary).is_empty(), "touched entry retained", failures)
var diagnostics: Dictionary = cache.call("diagnostic_snapshot")
_expect_true(int(diagnostics.get("entry_count", 0)) == 2, "bounded count", failures)
(diagnostics.get("entries_oldest_to_newest") as Array).clear()
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", 0)) == 2, "diagnostics detached", failures)
_expect_true(not bool(cache.call("store", "zero", ArrayMesh.new(), "baked_full", -1)), "negative capacity clamps to zero", failures)
cache.call("clear")
_expect_true(int((cache.call("diagnostic_snapshot") as Dictionary).get("entry_count", -1)) == 0, "clear releases entries", failures)
func _verify_loader_boundary(failures: Array[String]) -> void:
var source := _read_text(LOADER_PATH, failures)
_expect_true(source.contains("TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new()"), "loader composes cache", failures)
_expect_true(source.contains('_terrain_quality_mesh_cache.call("restore", tile_key)'), "loader delegates restore", failures)
_expect_true(source.contains('_terrain_quality_mesh_cache.call("clear")'), "loader delegates clear", failures)
for removed_symbol in ["_terrain_quality_mesh_cache_order", "func _touch_quality_terrain_mesh_cache", "func _trim_quality_terrain_mesh_cache"]:
_expect_true(not source.contains(removed_symbol), "loader omits %s" % removed_symbol, failures)
func _read_text(path: String, failures: Array[String]) -> String:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
failures.append("cannot open %s" % path)
return ""
return file.get_as_text()
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
if not actual_value:
failures.append("%s expected true" % label)
@@ -0,0 +1 @@
uid://khd1ths2b4ya