From 97480e06bb81b089ab9da00a1340bcc5628bedf2 Mon Sep 17 00:00:00 2001 From: sindoring Date: Thu, 16 Jul 2026 09:29:38 +0400 Subject: [PATCH] refactor(M03): extract terrain quality mesh cache --- RENDER.md | 12 ++ docs/modules/README.md | 1 + docs/modules/terrain-quality-mesh-cache.md | 198 ++++++++++++++++++ docs/modules/world-renderer.md | 16 +- .../terrain/terrain_quality_mesh_cache.gd | 73 +++++++ .../terrain/terrain_quality_mesh_cache.gd.uid | 1 + .../streaming/streaming_world_loader.gd | 49 ++--- .../verify_terrain_quality_mesh_cache.gd | 67 ++++++ .../verify_terrain_quality_mesh_cache.gd.uid | 1 + 9 files changed, 379 insertions(+), 39 deletions(-) create mode 100644 docs/modules/terrain-quality-mesh-cache.md create mode 100644 src/render/terrain/terrain_quality_mesh_cache.gd create mode 100644 src/render/terrain/terrain_quality_mesh_cache.gd.uid create mode 100644 src/tools/verify_terrain_quality_mesh_cache.gd create mode 100644 src/tools/verify_terrain_quality_mesh_cache.gd.uid diff --git a/RENDER.md b/RENDER.md index 114b5ad..023eead 100644 --- a/RENDER.md +++ b/RENDER.md @@ -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 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 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. diff --git a/docs/modules/README.md b/docs/modules/README.md index ca7d78d..a7d4d30 100644 --- a/docs/modules/README.md +++ b/docs/modules/README.md @@ -12,6 +12,7 @@ | Player input | Partial | [`player-input.md`](player-input.md) | | Local player movement | Implemented | [`local-player-movement.md`](local-player-movement.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) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | diff --git a/docs/modules/terrain-quality-mesh-cache.md b/docs/modules/terrain-quality-mesh-cache.md new file mode 100644 index 0000000..175ae03 --- /dev/null +++ b/docs/modules/terrain-quality-mesh-cache.md @@ -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) diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index 6ecdf45..f63fc0e 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -5,9 +5,9 @@ | Field | Value | |---|---| | 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 | -| 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 | ## Purpose @@ -40,6 +40,8 @@ flowchart LR Loader --> Budget[RenderBudgetScheduler] Assets[Extracted WDT/ADT/M2/WMO/BLP] --> Native[Native loaders] Cache[Baked terrain/M2/WMO caches] --> Loader + Loader --> TerrainCache[TerrainQualityMeshCache] + TerrainCache --> Loader Native --> Parsed[Parsed tile/model data] Parsed --> Loader 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 | | `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 | +| `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, внутренние 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 | | 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 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 | 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 | @@ -161,6 +165,8 @@ flowchart TD T --> Q[Tile load queue] Q --> Parse[Worker parse/cache load] Parse --> R[Result queues] + R --> TerrainCache[TerrainQualityMeshCache store] + TerrainCache --> Revisit[Revisited tile state restore] R --> B[RenderBudgetScheduler permits] B --> Terrain[Terrain attach/upgrade] B --> M2[M2 group/MultiMesh attach] @@ -263,6 +269,9 @@ sequenceDiagram `EntityId`; it owns no authoritative entity state and excludes the local player. - Entity presentation snapshots remain caller-owned inputs. Detached diagnostics 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; the facade never returns Mesh, Node, tile-state or queue references. - 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 | | 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 | +| 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 | | 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 | @@ -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/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/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_policy.gd` | Immutable renderer radius/prefetch policy | | `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_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_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_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 | diff --git a/src/render/terrain/terrain_quality_mesh_cache.gd b/src/render/terrain/terrain_quality_mesh_cache.gd new file mode 100644 index 0000000..7f8c10e --- /dev/null +++ b/src/render/terrain/terrain_quality_mesh_cache.gd @@ -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) diff --git a/src/render/terrain/terrain_quality_mesh_cache.gd.uid b/src/render/terrain/terrain_quality_mesh_cache.gd.uid new file mode 100644 index 0000000..45ecb8e --- /dev/null +++ b/src/render/terrain/terrain_quality_mesh_cache.gd.uid @@ -0,0 +1 @@ +uid://y576l2omqwox diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 086049c..afd234b 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -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_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 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_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") @@ -241,8 +242,7 @@ var _last_refresh_focus_pos := Vector3.ZERO var _has_refresh_focus := false var _tile_mesh_cache: Dictionary = {} var _tile_mesh_cache_order: Array = [] -var _terrain_quality_mesh_cache: Dictionary = {} -var _terrain_quality_mesh_cache_order: Array = [] +var _terrain_quality_mesh_cache: RefCounted = TERRAIN_QUALITY_MESH_CACHE_SCRIPT.new() var _wmo_prototype_cache: Dictionary = {} var _wmo_render_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: - 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 - var cached: Dictionary = _terrain_quality_mesh_cache[tile_key] 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) - 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_source"] = source - _touch_quality_terrain_mesh_cache(tile_key) return state func _store_quality_terrain_mesh(tile_key: String, mesh: Mesh, source: String) -> void: - if tile_key.is_empty() or mesh == null: - return - if source == "control_splat_cache" or source == "splat_cache" or source == "splat": - return - _terrain_quality_mesh_cache[tile_key] = { - "mesh": mesh, - "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) + _terrain_quality_mesh_cache.call( + "store", + tile_key, + mesh, + source, + terrain_quality_mesh_cache_limit + ) func _clear_quality_terrain_mesh_cache() -> void: - _terrain_quality_mesh_cache.clear() - _terrain_quality_mesh_cache_order.clear() + _terrain_quality_mesh_cache.call("clear") func _position_camera_over_world() -> void: diff --git a/src/tools/verify_terrain_quality_mesh_cache.gd b/src/tools/verify_terrain_quality_mesh_cache.gd new file mode 100644 index 0000000..642ad31 --- /dev/null +++ b/src/tools/verify_terrain_quality_mesh_cache.gd @@ -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) diff --git a/src/tools/verify_terrain_quality_mesh_cache.gd.uid b/src/tools/verify_terrain_quality_mesh_cache.gd.uid new file mode 100644 index 0000000..6ea1cf3 --- /dev/null +++ b/src/tools/verify_terrain_quality_mesh_cache.gd.uid @@ -0,0 +1 @@ +uid://khd1ths2b4ya