merge(M03): integrate terrain mesh cache service
This commit is contained in:
@@ -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.
|
||||||
|
|||||||
@@ -83,14 +83,33 @@ source-admission rules from `StreamingWorldLoader` into a small scene-free
|
|||||||
|
|
||||||
## Status
|
## Status
|
||||||
|
|
||||||
- State: claimed
|
- State: ready-for-review
|
||||||
- Done: ownership, behavior and non-goals published
|
- Done: scene-free cache ownership/LRU extraction, loader adapter migration,
|
||||||
- Next: implement service, migrate loader wrappers, tests and docs
|
contract regression and module documentation
|
||||||
|
- Next: M03 integrator reviews and merges `97480e0`
|
||||||
- Blocked by:
|
- Blocked by:
|
||||||
|
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M03-RND-TERRAIN-CACHE-SERVICE-001:97480e0 -->
|
||||||
|
|
||||||
## Handoff
|
## Handoff
|
||||||
|
|
||||||
- Commit:
|
- Commit: `97480e0`
|
||||||
- Results:
|
- Results: `TerrainQualityMeshCache` now owns retained full-quality Mesh
|
||||||
- Remaining risks:
|
references, source admission and oldest-to-newest LRU. The loader retains only
|
||||||
- Documentation updated:
|
restore-to-state, store-after-upgrade and clear lifecycle adapters.
|
||||||
|
- Verification: cache `contract_cases=15 loader_calls=3`; facade/entity/ground/
|
||||||
|
terrain regressions; internal access `private_symbols=42 gameplay=7
|
||||||
|
editor_sources=9 renderer_tools=1`; manifest `7/7`; shutdown; scheduler
|
||||||
|
`20000` iterations; planner `250` iterations; focus; editor parse; dry-run;
|
||||||
|
coordination, documentation and diff gates passed.
|
||||||
|
- Fidelity: `baked_full` revisit retention, three excluded splat sources, LRU
|
||||||
|
touch/trim, profile capacities and all call sites remain unchanged. All seven
|
||||||
|
checkpoint plans remain valid. No visible or build-12340 parity claim is added.
|
||||||
|
- External/local inputs: ignored native DLL and generated ADT scripts supported
|
||||||
|
parse/dry-run only; missing proprietary ADT/DBC/character data produced expected diagnostics.
|
||||||
|
- Remaining risks: entry count does not measure Mesh bytes; ordered-array touch
|
||||||
|
remains linear; asset-backed revisit flash, memory and p95/p99 evidence remain pending;
|
||||||
|
terrain build/tasks/state/finalization are still monolithic.
|
||||||
|
- Documentation updated: new terrain cache API/input-output/data-flow/lifecycle/
|
||||||
|
sequence/ownership/failure/configuration/migration/diagnostic/source-map spec;
|
||||||
|
world renderer, registry and `RENDER.md` updated.
|
||||||
|
|||||||
@@ -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) |
|
||||||
|
|||||||
@@ -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)
|
||||||
@@ -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
|
||||||
@@ -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
|
||||||
Reference in New Issue
Block a user