Compare commits
36 Commits
e1b2886657
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 87bf067493 | |||
| ca77d32be1 | |||
| 7cd07dda4f | |||
| 8605b43b1f | |||
| b608a81766 | |||
| e4bb10cee7 | |||
| 1d94eace68 | |||
| 1fafddecb6 | |||
| 0007a69769 | |||
| ece7724a28 | |||
| 951bd54fff | |||
| dd8cfbf79c | |||
| fa85985ec5 | |||
| f68e971cce | |||
| 7b0e1eac5f | |||
| 6607100686 | |||
| 5e2afbc400 | |||
| 30945615b9 | |||
| c5ecaba0ab | |||
| e990a6503d | |||
| e2bb501695 | |||
| ef782d1f2b | |||
| 83b3618c50 | |||
| ccbb27a9fd | |||
| 82df7a555d | |||
| 5f6287905c | |||
| 8ec13d6018 | |||
| 38f831ea2f | |||
| 0544cf22df | |||
| 19ca8f48e8 | |||
| 324e64dd6e | |||
| 90b1dac56e | |||
| 874fd0f3f2 | |||
| 9cb8bdb137 | |||
| 235be3a36c | |||
| 4f8fc3b3b8 |
@@ -34,6 +34,12 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
- `src/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.
|
||||
- `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership.
|
||||
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
|
||||
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
||||
- `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` - memoized billboard/UV-rotation decision for stale cached M2 runtime mesh refresh.
|
||||
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||
- `src/render/m2/m2_mesh_resource_extractor.gd` - first-Mesh selection from direct/PackedScene/Node inputs with temporary instance cleanup.
|
||||
- `src/render/m2/m2_runtime_mesh_finalizer.gd` - refresh version, classifier lifetime, M2Builder rebuild and original-Mesh fallback.
|
||||
- `src/scenes/streaming/eastern_kingdoms_streaming.tscn` - текущая сцена для проверки Azeroth/Eastern Kingdoms.
|
||||
- `addons/mpq_extractor/loaders/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
|
||||
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material.
|
||||
@@ -802,7 +808,12 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Billboard metadata is resolved per drawn vertex through the skin section bone palette and vertex bone indices. This avoids treating an entire mixed surface as billboard just because the palette contains a billboard bone.
|
||||
- `M2Builder` stores billboard pivot/mode in `Mesh.ARRAY_CUSTOM0` as RGBA float data. `CUSTOM0.xyz` is the Godot-space bone pivot; `CUSTOM0.w` is the billboard mode: spherical, lock X, lock Y, or lock Z.
|
||||
- `WowM2Material` applies camera-facing billboarding in the vertex shader for marked vertices. This works with grouped ADT M2 `MultiMesh` batches because the shader uses each instance transform.
|
||||
- Runtime M2 cache refresh now has `M2_MATERIAL_REFRESH_VERSION`. Old cached `.tscn/.glb` meshes are replaced in memory from raw `.m2` only when the raw model actually needs custom billboard attributes or UV-rotation data. Do not rebuild every stale cached M2 during streaming: creating fresh materials for ordinary props can exhaust the D3D12 resource descriptor heap.
|
||||
- Runtime M2 cache refresh uses material refresh version `2` (now
|
||||
`M2RuntimeMeshFinalizer.MATERIAL_REFRESH_VERSION`). Old cached `.tscn/.glb`
|
||||
meshes are replaced in memory from raw `.m2` only when the raw model actually
|
||||
needs custom billboard attributes or UV-rotation data. Do not rebuild every
|
||||
stale cached M2 during streaming: creating fresh materials for ordinary props
|
||||
can exhaust the D3D12 resource descriptor heap.
|
||||
- Verified with `ElwynnGrass1.m2`: native data exposes a billboard batch, the built mesh contains `CUSTOM0`, and the M2 shader compiles with the billboard uniform enabled.
|
||||
- Remaining parity work: many Elwynn tree canopy M2s do not carry billboard bone flags, so their flat-looking leaves are a separate material/geometry parity issue, not this bone-billboard path.
|
||||
|
||||
@@ -812,7 +823,8 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Texture transform dictionaries now expose static UV rotation matrices and approximate Z-rotation speeds, in addition to existing translation, translation speed and scale.
|
||||
- `WowM2Material` applies UV rotation and scale around WoW's texture center `(0.5, 0.5)`, then applies translation/scroll. This is closer to the client path shown by `CM2Model::AnimateTextureTransformsMT`.
|
||||
- All four M2 texture stages receive UV rotation and rotation-speed uniforms, matching the existing four-stage texture binding path.
|
||||
- `M2_MATERIAL_REFRESH_VERSION` and `M2Builder.MATERIAL_FORMAT_VERSION` are bumped to `2`, so cached M2 meshes are rebuilt in memory when raw `.m2` data is available.
|
||||
- Runtime M2 material refresh and `M2Builder.MATERIAL_FORMAT_VERSION` are `2`,
|
||||
so stale cached M2 meshes can be rebuilt in memory when raw `.m2` data is available.
|
||||
- Verified with `Spells/ArcaneForceShield_Blue.m2`: native data exposes a non-identity UV rotation, the built material receives the stage rotation uniform, and the shader compiles.
|
||||
|
||||
## 2026-07-08 Renderer Material Regression Smoke
|
||||
@@ -1038,6 +1050,90 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Cache formats, quality profiles, batching output and visible rules are unchanged.
|
||||
Asset-backed p95/p99 and spatial-cell batching evidence remain pending.
|
||||
|
||||
## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
|
||||
|
||||
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
|
||||
stale cached M2 mesh may need an in-memory rebuild from raw data.
|
||||
- Existing triggers are unchanged: any billboard flag/positive billboard vertex
|
||||
count, or a referenced non-identity UV rotation/absolute rotation speed above
|
||||
`0.000001` across at most four texture stages.
|
||||
- Invalid batch variants and combo/transform indices remain skipped. The first
|
||||
decision for a normalized path remains cached until the same two historical
|
||||
map/shutdown clear sites.
|
||||
- `StreamingWorldLoader` still requests raw M2 data and owns ResourceLoader,
|
||||
returned-Mesh adoption and finalization budgets. Refresh-version checks,
|
||||
M2Builder rebuild and fallback now belong to `M2RuntimeMeshFinalizer`.
|
||||
- No cache version, shader, material, geometry or visible rule changed. Synthetic
|
||||
predicate timing is not asset-backed descriptor-pressure/p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Mesh Load Pipeline State Extraction
|
||||
|
||||
- `M2MeshLoadPipelineState` now owns static M2 successful threaded-load request
|
||||
records and the FIFO populated when ResourceLoader polling reaches loaded or
|
||||
failed terminal status.
|
||||
- Pending polling snapshots preserve Dictionary insertion order; terminal FIFO
|
||||
order follows completion transition order. Existing `m2_mesh` metrics still
|
||||
count pending plus finalize records at all three sites.
|
||||
- Shutdown still drains every pending ResourceLoader path before state clear;
|
||||
map reset and shutdown retain the same two clear sites.
|
||||
- `StreamingWorldLoader` still selects `.tscn`/`.glb` paths, excludes animated
|
||||
pivot-prefix GLBs, performs all ResourceLoader calls, consumes
|
||||
`M2_MESH_FINALIZE`, extracts/prepares Meshes and updates Mesh/missing caches.
|
||||
- Cache formats, material refresh, profiles and visible output are unchanged.
|
||||
Synthetic state timing is not asset-backed I/O, leak or p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Mesh Resource Cache State Extraction
|
||||
|
||||
- `M2MeshResourceCacheState` now owns normalized-path references to prepared
|
||||
static M2 Mesh resources. Lookup returns the exact retained reference and a
|
||||
later store for one path keeps the historical replacement behavior.
|
||||
- The cache retains map-refresh lifetime and clears only at the existing final
|
||||
runtime-cache shutdown site after asynchronous work is drained.
|
||||
- `StreamingWorldLoader` still owns normalization, `.tscn`/`.glb` selection,
|
||||
ResourceLoader I/O, Mesh extraction, material refresh/rebuild, shared missing
|
||||
and prototype/animated caches, permits and MultiMesh materialization.
|
||||
- Cache formats, refresh metadata, profiles and visible output are unchanged.
|
||||
Synthetic cache timing is not asset-backed memory/leak or p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Mesh Resource Extractor
|
||||
|
||||
- `M2MeshResourceExtractor` now selects the first Mesh from direct Mesh
|
||||
Resources, temporary PackedScene instances and existing prototype/rebuild
|
||||
Node subtrees using the prior depth-first child order.
|
||||
- PackedScene extraction still instantiates exactly once and synchronously frees
|
||||
the temporary root before returning the borrowed Mesh reference.
|
||||
- `StreamingWorldLoader` still owns ResourceLoader I/O and raw M2 consumers,
|
||||
material refresh/rebuild, cache/missing adoption, permits and MultiMesh builds.
|
||||
- Cache formats, traversal rules, profiles and visible output are unchanged.
|
||||
Synthetic traversal timing is not asset-backed leak or p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Runtime Mesh Finalizer
|
||||
|
||||
- `M2RuntimeMeshFinalizer` now owns material refresh version `2`, stale-Mesh
|
||||
rebuild classification, M2Builder rebuild and original-Mesh fallback.
|
||||
- Current Meshes still skip raw `.m2` loading. `M2RawModelRepository` now owns
|
||||
FileAccess/ClassDB M2Loader I/O and supplies raw data through the loader only
|
||||
when the finalizer reports a stale Mesh; both historical clear sites persist.
|
||||
- Billboard/UV-rotation predicates, rebuild extraction, metadata key and failure
|
||||
fallback are unchanged. Cache adoption, missing state, permits and MultiMesh
|
||||
materialization remain loader-owned.
|
||||
- Synthetic triangle rebuild/fallback timing is not asset-backed material,
|
||||
descriptor-pressure/leak or p95/p99 evidence.
|
||||
|
||||
## 2026-07-17 M2 Raw Model Repository
|
||||
|
||||
- `M2RawModelRepository` now owns the repeated extracted-file and optional
|
||||
native `M2Loader` boundary for static `load_m2` and animated
|
||||
`load_m2_animated` raw Dictionaries.
|
||||
- `StreamingWorldLoader` delegates the stale-Mesh refresh, static prototype and
|
||||
native animated prototype reads. It retains normalization, `.tscn/.glb`
|
||||
fallback order, missing/prototype caches, builders, permits and Node/Mesh use.
|
||||
- The repository retains no path, native object or parsed data. Empty paths,
|
||||
absent files/classes/methods and invalid results produce the same empty-value
|
||||
fallback contract; path join/globalization and native method names are exact.
|
||||
- No cache format, parser, material, geometry, animation or visible rule changed.
|
||||
Synthetic missing-input timing is not asset-backed I/O/p95/p99 or fidelity evidence.
|
||||
|
||||
## 2026-07-17 WMO Placement Resolver Extraction
|
||||
|
||||
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
|
||||
@@ -1084,7 +1180,8 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
active WorkerThreadPool task IDs and the mutex-protected parsed-result mailbox.
|
||||
- `StreamingWorldLoader` still selects concurrency, starts/waits tasks, invokes
|
||||
`ADTLoader`, consumes `WATER_FINALIZE` permits, rejects stale tile/path/root
|
||||
results and builds/attaches water Nodes through `ADTBuilder` on the main thread.
|
||||
results and owns `water_loaded` timing. Main-thread Water subtree construction
|
||||
and attachment are delegated separately as described below.
|
||||
- Tile release still cancels only pending water requests; active work completes
|
||||
and its result is discarded when tile ownership is stale. Shutdown waits task
|
||||
IDs before clearing state; reset does not interrupt workers.
|
||||
@@ -1093,6 +1190,23 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Asset-backed MH2O/MCLQ fidelity, long-traversal leak/p95/p99 and paired capture
|
||||
evidence remain pending.
|
||||
|
||||
## 2026-07-17 ADT Water Scene Finalizer Extraction
|
||||
|
||||
- `AdtWaterSceneFinalizer` now owns the two main-thread operations that turn
|
||||
validated parsed ADT data into a Water subtree: the existing
|
||||
`ADTBuilder.build_tile_water_scene` call and attachment to the tile root.
|
||||
- Both raw tile finalization and delayed cached-tile water results use the same
|
||||
service. Optional persisted Editor ownership is assigned recursively only
|
||||
when the loader supplies its existing edited-scene owner.
|
||||
- The service is stateless and retains no Node, Mesh, material or RID. The tile
|
||||
SceneTree subtree remains the lifetime owner; dry data still causes ADTBuilder
|
||||
to free its temporary empty Water root.
|
||||
- Parsing, task/result state, concurrency, stale validation, `WATER_FINALIZE`
|
||||
permits, `enable_water` and `water_loaded` timing remain loader-owned.
|
||||
- Geometry, materials, coordinates, cache formats, profiles and visible output
|
||||
are unchanged. Synthetic geometry/ownership evidence is not an MH2O/MCLQ or
|
||||
original-client parity claim; asset-backed traversal/p95/p99 remains pending.
|
||||
|
||||
## 2026-07-16 World Environment Snapshot Facade
|
||||
|
||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
||||
|
||||
@@ -0,0 +1,116 @@
|
||||
# M03-RND-ADT-WATER-SCENE-FINALIZER-001 — ADT water scene finalizer
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-ADT-WATER-SCENE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-adt-water-scene-finalizer`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract main-thread ADT water subtree construction, tile-root attachment and
|
||||
optional recursive editor ownership from `StreamingWorldLoader` while retaining
|
||||
the existing `ADTBuilder` geometry/material implementation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Parse or reinterpret MH2O/MCLQ data.
|
||||
- Own pending requests, WorkerThreadPool tasks or parsed-result delivery.
|
||||
- Consume `WATER_FINALIZE` permits or validate tile/path/result staleness.
|
||||
- Mark tile state `water_loaded` or choose whether water is enabled.
|
||||
- Change liquid geometry, materials, coordinates, cache formats or profiles.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/liquid/adt_water_scene_finalizer.gd`,
|
||||
`src/tools/verify_adt_water_scene_finalizer.gd`,
|
||||
`docs/modules/adt-water-scene-finalizer.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
`src/tools/verify_adt_water_load_pipeline_state.gd`,
|
||||
`docs/modules/adt-water-load-pipeline-state.md`,
|
||||
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated ADT resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Empty water data or invalid tile root produces no Node and no scene mutation.
|
||||
- Existing `ADTBuilder.build_tile_water_scene` remains the only geometry/material source.
|
||||
- A non-empty built Water root is attached exactly once to the supplied tile root.
|
||||
- Optional editor ownership is assigned recursively only when explicitly enabled
|
||||
and an edited scene root is available.
|
||||
- The returned Node is borrowed and remains owned by the tile SceneTree subtree.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted water pipeline package on current master `e1b2886`
|
||||
- Blocks: further liquid finalization/material responsibility extraction
|
||||
- External state: none; verification uses synthetic liquid dictionaries
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated empty/invalid/build/attach/editor-owner/source/timing
|
||||
verifier; water pipeline, materials, shutdown, facade, internal-access,
|
||||
manifest, documentation and coordination gates
|
||||
- Fixtures: synthetic one-cell liquid mask/heights and temporary Node roots
|
||||
- Fidelity evidence: exact existing ADTBuilder output and two attachment paths preserved
|
||||
- Performance budget: 100 empty finalization attempts under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, main-thread sequence and dependency diagrams
|
||||
- Water-pipeline, world-renderer, module registry and RENDER status updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `AdtWaterSceneFinalizer`
|
||||
- Simplest considered solution: one RefCounted with one explicit attach operation
|
||||
- Rejected complexity/abstractions: generic scene finalizer, signals, callbacks,
|
||||
builder interface, queue ownership or alternate liquid renderer
|
||||
- Unavoidable complexity and justification: editor-persisted generated subtrees
|
||||
require recursive owner assignment after SceneTree attachment
|
||||
- Measured optimization evidence: bounded synthetic no-water loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: stateless finalizer implemented; both loader water paths migrated;
|
||||
synthetic scene/owner regression and required documentation added
|
||||
- Next: continue liquid parsing/task execution extraction or another remaining M03 boundary
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-ADT-WATER-SCENE-FINALIZER-001:874fd0f -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `235be3a`
|
||||
- Results: scene finalizer PASS (`cases=8`, `iterations=100`,
|
||||
`elapsed_ms=0.042`; initial `0.024ms`); synthetic liquid produced one
|
||||
four-vertex/six-index surface with tile attachment and recursive owner; water
|
||||
pipeline (`119.648ms`), WMO/M2/terrain/facade/scheduler/streaming/coordinate,
|
||||
materials, shutdown, internal-access `36` and 32 headless contract regressions
|
||||
PASS; documentation and coordination gates PASS; checkpoint dry-run PASS `7/7`.
|
||||
- Remaining risks: main-thread mesh/material construction remains synchronous
|
||||
behind one existing permit; the Dictionary builder boundary remains untyped;
|
||||
proprietary MH2O/MCLQ traversal/leak/p95/p99/original-client evidence is absent.
|
||||
A verifier launched concurrently with the initial cold editor import observed
|
||||
the pre-existing global-class cache race; serialized editor parse and shutdown
|
||||
rerun passed, with only expected missing private asset diagnostics.
|
||||
- Documentation updated: inline public API; new
|
||||
`docs/modules/adt-water-scene-finalizer.md`; water-pipeline spec, module registry,
|
||||
world renderer and `RENDER.md`; data-flow/state/sequence/dependency diagrams,
|
||||
ownership, main-thread effects, failure/recovery, migration and source map included.
|
||||
- Integration: merged as `874fd0f`; post-merge finalizer (`cases=8`,
|
||||
`iterations=100`, `elapsed_ms=0.025`), water pipeline (`98.398ms`), shutdown,
|
||||
renderer materials, facade, internal-access (`private_symbols=36`), `7/7`
|
||||
manifest, documentation and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-ADT-WATER-SCENE-FINALIZER-001:235be3a -->
|
||||
@@ -0,0 +1,109 @@
|
||||
# M03-RND-M2-MESH-LOAD-PIPELINE-001 — M2 mesh load pipeline state
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-LOAD-PIPELINE-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-mesh-load-pipeline`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract static M2 threaded ResourceLoader request records and their terminal
|
||||
finalize FIFO from `StreamingWorldLoader`, preserving request order, status
|
||||
transition, metrics and reset/shutdown behavior.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Call ResourceLoader or choose `.tscn`/`.glb` cache paths.
|
||||
- Own M2 Mesh, scene, missing or material-refresh caches.
|
||||
- Extract Mesh from Resources or rebuild/adopt runtime Meshes.
|
||||
- Consume `M2_MESH_FINALIZE` permits.
|
||||
- Change cache formats, animation loading or visible M2 behavior.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_mesh_load_pipeline_state.gd`,
|
||||
`src/tools/verify_m2_mesh_load_pipeline_state.gd`,
|
||||
`docs/modules/m2-mesh-load-pipeline-state.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- One normalized path owns at most one pending request.
|
||||
- Pending records preserve Dictionary insertion order for polling snapshots.
|
||||
- Terminal completion removes the request and appends its record/status to FIFO.
|
||||
- Finalize pop remains oldest-first; duplicate/unknown transitions are rejected.
|
||||
- Total work count remains pending plus finalize records.
|
||||
- Clear drops bookkeeping only; loader drains ResourceLoader requests first.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted classifier package on current master `8ec13d6`
|
||||
- Blocks: further M2 Mesh cache/finalization service extraction
|
||||
- External state: none; verification uses synthetic paths and opaque statuses
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated validation/order/transition/FIFO/clear/source/timing
|
||||
verifier; M2 classifier/builder/grouper/transform/registry, materials, shutdown,
|
||||
facade, internal-access, manifest, documentation and coordination gates
|
||||
- Fixtures: synthetic normalized paths, resource paths and integer terminal statuses
|
||||
- Fidelity evidence: exact request/finalize transitions and metric/reset sites preserved
|
||||
- Performance budget: 100 cycles over 256 request/completion/pop records under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- World-renderer, module registry and RENDER status updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `M2MeshLoadPipelineState`
|
||||
- Simplest considered solution: one RefCounted with request map and finalize FIFO
|
||||
- Rejected complexity/abstractions: generic ResourceLoader queue, signals,
|
||||
callbacks, Mesh cache ownership or animation/static pipeline merger
|
||||
- Unavoidable complexity and justification: polling and budgeted finalization are
|
||||
distinct lifecycle stages across frames
|
||||
- Measured optimization evidence: bounded synthetic transition loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: state holder, loader adapters, contract verifier and required documentation
|
||||
- Next: further M2 Mesh cache/finalizer extraction
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `82df7a5`
|
||||
- Results: 34 headless contract regressions pass; dedicated pipeline state
|
||||
verifier passes 100-by-256 transitions in 100.273 ms; documentation and
|
||||
coordination gates pass; checkpoint dry-run passes 7/7.
|
||||
- Remaining risks: no private asset traversal, leak/descriptor-pressure or
|
||||
original-client visual comparison; ResourceLoader I/O and Mesh cache/finalizer
|
||||
ownership remain in `StreamingWorldLoader` by design.
|
||||
- Documentation updated: inline API; `m2-mesh-load-pipeline-state.md` with
|
||||
data-flow, state, sequence and dependency diagrams; module registry,
|
||||
`world-renderer.md` and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-LOAD-PIPELINE-001:82df7a5 -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `83b3618`
|
||||
- Post-merge results: pipeline state `cases=11 iterations=100
|
||||
elapsed_ms=69.196`; classifier, materials, shutdown, facade, internal-access
|
||||
`34`, manifest `7/7`, documentation and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-LOAD-PIPELINE-001:83b3618 -->
|
||||
@@ -0,0 +1,108 @@
|
||||
# M03-RND-M2-MESH-RESOURCE-CACHE-001 — M2 mesh resource cache state
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-CACHE-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-mesh-resource-cache`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract the normalized-path to static M2 Mesh resource cache from
|
||||
`StreamingWorldLoader`, preserving lookup, replacement, shutdown lifetime and
|
||||
all existing cache consumers.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Own the shared M2 missing-path cache or animated/prototype node caches.
|
||||
- Call ResourceLoader, extract Meshes from Resources or select cache paths.
|
||||
- Prepare, rebuild or materialize M2 Meshes.
|
||||
- Change cache formats, material refresh, animation or visible behavior.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_mesh_resource_cache_state.gd`,
|
||||
`src/tools/verify_m2_mesh_resource_cache_state.gd`,
|
||||
`docs/modules/m2-mesh-resource-cache-state.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
`src/tools/verify_m2_mesh_load_pipeline_state.gd`,
|
||||
`src/tools/verify_render_runtime_cache_shutdown.gd`,
|
||||
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Non-empty normalized M2 paths map to at most one Mesh resource reference.
|
||||
- Store replaces the previous reference for the same path.
|
||||
- Lookup returns the exact retained Mesh reference; unknown/empty paths return null.
|
||||
- Clear releases all cache references only at the existing final-shutdown site.
|
||||
- Shared missing/prototype/animated caches remain loader-owned and unchanged.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted mesh load pipeline package on master `ef782d1`
|
||||
- Blocks: isolated M2 Mesh resource finalizer extraction
|
||||
- External state: none; verification uses synthetic Mesh resources
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated validation/store/replace/lookup/clear/source/timing
|
||||
verifier; mesh pipeline/classifier/material/shutdown/M2 build/facade/internal-
|
||||
access/manifest, documentation and coordination gates
|
||||
- Fixtures: synthetic ArrayMesh resources and normalized paths
|
||||
- Fidelity evidence: exact cache call-site and shutdown lifetime preservation
|
||||
- Performance budget: 100 cycles over 256 store/lookup entries under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state and dependency diagrams
|
||||
- World-renderer, module registry and RENDER status updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2MeshResourceCacheState`
|
||||
- Simplest solution: one RefCounted with one typed-purpose Dictionary
|
||||
- Rejected complexity: generic resource cache, missing-state merger, eviction,
|
||||
signals, callbacks or materialization ownership
|
||||
- Unavoidable complexity: none beyond retaining engine Resource references
|
||||
- Measured optimization evidence: bounded synthetic store/lookup loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: cache state, loader/test adapters, contract verifier and required documentation
|
||||
- Next: isolated M2 Mesh resource finalizer extraction
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `e990a65`
|
||||
- Results: dedicated cache verifier passes 100-by-256 store/lookups in
|
||||
38.974 ms; 35 headless renderer/coordinate regressions pass; checkpoint
|
||||
dry-run passes 7/7; documentation and coordination gates pass.
|
||||
- Remaining risks: no private M2 traversal, Mesh memory/leak/descriptor-pressure
|
||||
run or original-client visual comparison; extraction/preparation, shared
|
||||
missing state and materialization remain loader-owned by design.
|
||||
- Documentation updated: inline API; `m2-mesh-resource-cache-state.md` with
|
||||
data-flow, lifecycle, sequence and dependency diagrams; pipeline spec, module
|
||||
registry, `world-renderer.md` and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-CACHE-001:e990a65 -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `3094561`
|
||||
- Post-merge results: cache state `cases=9 iterations=100
|
||||
elapsed_ms=38.686`; pipeline, classifier, materials, shutdown, facade,
|
||||
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-CACHE-001:3094561 -->
|
||||
@@ -0,0 +1,109 @@
|
||||
# M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001 — M2 mesh resource extractor
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-mesh-resource-extractor`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract first-Mesh selection from loaded M2 Resources and Node subtrees,
|
||||
including temporary PackedScene instance cleanup, from `StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Call ResourceLoader or select/request cache paths.
|
||||
- Refresh/rebuild materials or load raw M2 data.
|
||||
- Own the Mesh cache, missing state, prototypes or MultiMesh materialization.
|
||||
- Change traversal order, cache formats, profiles or visible behavior.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_mesh_resource_extractor.gd`,
|
||||
`src/tools/verify_m2_mesh_resource_extractor.gd`,
|
||||
`docs/modules/m2-mesh-resource-extractor.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
M2 pipeline/cache/material/shutdown verifiers, `docs/modules/world-renderer.md`,
|
||||
`docs/modules/README.md`, M2 module specs, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- A direct Mesh Resource returns the same Mesh reference.
|
||||
- A PackedScene is instantiated once, searched depth-first in child order and
|
||||
its temporary root is freed before return.
|
||||
- A Node subtree search returns the first MeshInstance3D Mesh in historical
|
||||
depth-first preorder without changing the tree.
|
||||
- Null, unsupported Resources and subtrees without a Mesh return null.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted Mesh resource cache package on master `5e2afbc`
|
||||
- Blocks: isolated material refresh/rebuild finalizer extraction
|
||||
- External state: none; verification uses synthetic Mesh/Node/PackedScene fixtures
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated direct/PackedScene/subtree/order/lifetime/source/timing
|
||||
verifier; M2 pipeline/cache/classifier/material/shutdown/build/facade/internal-
|
||||
access/manifest, documentation and coordination gates
|
||||
- Fixtures: ArrayMesh, MeshInstance3D subtrees and synthetic PackedScene
|
||||
- Fidelity evidence: exact traversal order and temporary-node destruction
|
||||
- Performance budget: 10,000 synthetic subtree lookups under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- M2 cache/pipeline, world-renderer, registry and RENDER updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2MeshResourceExtractor`
|
||||
- Simplest solution: one stateless RefCounted with two explicit extraction APIs
|
||||
- Rejected complexity: callback-driven finalizer, generic scene utility,
|
||||
cache ownership, signals or visitor framework
|
||||
- Unavoidable complexity: temporary PackedScene instance must be freed while
|
||||
its referenced Mesh remains borrowed
|
||||
- Measured optimization evidence: bounded synthetic depth-first traversal
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: extractor, three loader/test adapters, contract verifier and required documentation
|
||||
- Next: isolated M2 Mesh material preparation finalizer extraction
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `7b0e1ea`
|
||||
- Results: extractor passes direct/PackedScene/subtree/order/lifetime contracts
|
||||
and 10,000 traversals in 39.243 ms; 36 headless renderer/coordinate
|
||||
regressions pass; checkpoint dry-run passes 7/7; documentation and
|
||||
coordination gates pass.
|
||||
- Remaining risks: synchronous PackedScene instantiation remains behind the
|
||||
existing permit; no private M2 corrupt-scene/traversal/leak/p95/p99 or
|
||||
original-client comparison; material preparation remains loader-owned.
|
||||
- Documentation updated: inline API; `m2-mesh-resource-extractor.md` with
|
||||
data-flow, lifecycle, sequence and dependency diagrams; cache/pipeline specs,
|
||||
module registry, `world-renderer.md` and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:7b0e1ea -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `fa85985`
|
||||
- Post-merge results: extractor `cases=10 iterations=10000
|
||||
elapsed_ms=38.172`; cache, pipeline, classifier, materials, shutdown, facade,
|
||||
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001:fa85985 -->
|
||||
@@ -0,0 +1,91 @@
|
||||
# M03-RND-M2-PROTOTYPE-CACHE-001 — M2 prototype cache state
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-PROTOTYPE-CACHE-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-prototype-cache`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract static/animated detached M2 prototype references plus missing-model and
|
||||
static-animation negative outcomes from `StreamingWorldLoader` into one explicit
|
||||
shutdown-lifetime cache-state service.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Choose `.tscn/.glb` paths, call ResourceLoader/native loaders or build Nodes.
|
||||
- Own animation/Mesh request queues, Mesh resources, materials or MultiMeshes.
|
||||
- Change candidate/allowlist/denylist rules, fallback order or visible behavior.
|
||||
- Add persistence, eviction, async work, dependencies or cache-format changes.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_prototype_cache_state.gd`,
|
||||
`src/tools/verify_m2_prototype_cache_state.gd`,
|
||||
`docs/modules/m2-prototype-cache-state.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, runtime
|
||||
cache shutdown/M2 raw/finalizer/extractor/pipeline/material/animation verifiers
|
||||
and module specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
|
||||
`RENDER.md`, `targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Normalized non-empty paths key static and animated detached `Node3D` prototypes.
|
||||
- Missing-model and static-animation outcomes are independent path sets.
|
||||
- Exact cached Node reference is returned; first adoption for an occupied path wins.
|
||||
- Shutdown frees valid detached nodes synchronously, queues in-tree nodes for free,
|
||||
clears all four states and remains idempotent.
|
||||
- Map refresh retains the state exactly as before; only final shutdown clears it.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted raw model repository package on master `ca77d32`
|
||||
- Blocks: remaining M2 animation request/materialization extraction
|
||||
- External state: none; verification uses synthetic Node3D instances
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated admission/identity/independence/duplicate/free/source/timing
|
||||
verifier; runtime shutdown/raw/finalizer/extractor/cache/pipeline/material/
|
||||
animation/M2 build/facade/internal-access/manifest, docs and coordination gates
|
||||
- Fixtures: detached and in-tree synthetic Node3D prototypes
|
||||
- Fidelity evidence: all existing lookup/store/negative/clear caller transitions
|
||||
- Performance budget: 100 cycles over 256 path-state operations under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- Adjacent M2/world-renderer/module-registry/RENDER updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2PrototypeCacheState`
|
||||
- Simplest solution: one main-thread RefCounted with two Node maps and two path sets
|
||||
- Rejected complexity: generic Node cache, signals, callbacks, eviction policy,
|
||||
weak references, async cleanup or dependency injection
|
||||
- Unavoidable complexity: detached Nodes require explicit free at final shutdown
|
||||
- Measured optimization evidence: bounded synthetic cache-cycle loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: claimed
|
||||
- Done: four cache tables and all caller/lifecycle sites identified
|
||||
- Next: implement state service, migrate adapters, verify and document
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit:
|
||||
- Results:
|
||||
- Remaining risks:
|
||||
- Documentation updated:
|
||||
@@ -0,0 +1,109 @@
|
||||
# M03-RND-M2-RAW-MODEL-REPOSITORY-001 — raw M2 model repository
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-RAW-MODEL-REPOSITORY-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-raw-model-repository`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract the repeated FileAccess/ClassDB native `M2Loader` boundary for static
|
||||
and animated raw model Dictionaries from `StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Cache raw data, Meshes, prototypes, missing paths or animation decisions.
|
||||
- Build Nodes/Meshes, refresh materials or choose `.tscn/.glb` caches.
|
||||
- Change static/animated parser methods, path resolution or fallback order.
|
||||
- Add dependencies, persistence, retries or visible behavior.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_raw_model_repository.gd`,
|
||||
`src/tools/verify_m2_raw_model_repository.gd`,
|
||||
`docs/modules/m2-raw-model-repository.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
|
||||
finalizer/extractor/cache/pipeline/material/animation/shutdown verifiers and
|
||||
module specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`,
|
||||
`RENDER.md`, `targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Static load calls native `load_m2`; animated load calls `load_m2_animated`.
|
||||
- Paths remain `globalize_path(extracted_directory.path_join(normalized_path))`.
|
||||
- Empty path, missing native class/file/instance/method, parse failure or
|
||||
non-Dictionary result returns an empty Dictionary without retained state.
|
||||
- Each successful request creates one native loader instance as before.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted runtime Mesh finalizer package on master `1d94eac`
|
||||
- Blocks: M2 prototype/cache state extraction
|
||||
- External state: native M2Loader registration and local extracted files
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated invalid/missing/class/source/timing verifier; finalizer/
|
||||
classifier/extractor/cache/pipeline/material/animation/shutdown/M2 build/facade/
|
||||
internal-access/manifest, documentation and coordination gates
|
||||
- Fixtures: empty/missing normalized paths plus source contract; proprietary
|
||||
success corpus remains external
|
||||
- Fidelity evidence: exact native method names, path expression and three caller transitions
|
||||
- Performance budget: 10,000 rejected empty-path calls under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- Finalizer/extractor/cache/pipeline, world-renderer, registry and RENDER updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2RawModelRepository`
|
||||
- Simplest solution: one stateless RefCounted with static and animated load methods
|
||||
- Rejected complexity: generic native repository, caching, dependency injection,
|
||||
retries, diagnostics framework or async wrapper
|
||||
- Unavoidable complexity: animated native method availability is a distinct guard
|
||||
- Measured optimization evidence: bounded rejected-input loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: repository, three loader adapters, contract verifier and required documentation
|
||||
- Next: continue M2 prototype/cache state extraction
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `b608a81`
|
||||
- Results: repository passes invalid/missing/stateless/native-boundary contracts
|
||||
and 10,000 empty-path rejects in 3.275 ms; 45 repository-independent
|
||||
headless verifiers pass, while the proprietary ADT placement probe is
|
||||
unavailable without `data/extracted`; checkpoint dry-run passes 7/7;
|
||||
documentation and coordination gates pass.
|
||||
- Remaining risks: successful/corrupt native parsing, descriptor pressure,
|
||||
leaks and p95/p99 remain asset-backed work; native calls are synchronous and
|
||||
cannot be cancelled mid-parse; no original-client visual comparison is claimed.
|
||||
- Documentation updated: inline API; `m2-raw-model-repository.md` with data-flow,
|
||||
lifecycle, sequence and dependency diagrams; finalizer/classifier/world-renderer
|
||||
specs, module registry and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RAW-MODEL-REPOSITORY-001:b608a81 -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `7cd07dd`
|
||||
- Post-merge results: repository `cases=10 iterations=10000
|
||||
elapsed_ms=3.200`; finalizer, extractor, cache, pipeline, shutdown, facade,
|
||||
internal-access `34`, manifest `7/7`, documentation and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RAW-MODEL-REPOSITORY-001:7cd07dd -->
|
||||
@@ -0,0 +1,110 @@
|
||||
# M03-RND-M2-RUNTIME-MESH-FINALIZER-001 — M2 runtime mesh finalizer
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-runtime-mesh-finalizer`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract stale cached-Mesh refresh classification, metadata adoption,
|
||||
M2Builder rebuild and fallback from `StreamingWorldLoader` into one runtime M2
|
||||
Mesh finalizer while retaining raw-file I/O in the loader.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Load raw `.m2` files, call FileAccess/ClassDB M2Loader or choose cache paths.
|
||||
- Own ResourceLoader requests, Mesh cache/missing state or MultiMesh builds.
|
||||
- Change material refresh version `2`, classifier predicates or rebuild behavior.
|
||||
- Change cache formats, profiles or visible output.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_runtime_mesh_finalizer.gd`,
|
||||
`src/tools/verify_m2_runtime_mesh_finalizer.gd`,
|
||||
`docs/modules/m2-runtime-mesh-finalizer.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`, M2
|
||||
classifier/extractor/cache/pipeline/material/shutdown verifiers and module
|
||||
specs, `docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated M2 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Refresh version remains `2`; already-current Meshes return unchanged without raw data.
|
||||
- Stale Meshes require caller-supplied raw data only when refresh is necessary.
|
||||
- Empty/ordinary raw data marks the original Mesh current and returns it.
|
||||
- Billboard/UV-rotation data attempts M2Builder rebuild and returns the rebuilt
|
||||
first Mesh; rebuild failure marks and returns the original Mesh.
|
||||
- Classifier memoization retains both existing clear sites through finalizer clear.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted Mesh resource extractor package on master `dd8cfbf`
|
||||
- Blocks: further M2 raw-data I/O/prototype state extraction
|
||||
- External state: none; verification uses synthetic Mesh/raw-data fixtures
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated current/stale/ordinary/rebuild/fallback/clear/source/timing
|
||||
verifier; classifier/extractor/cache/pipeline/material/shutdown/M2 build/facade/
|
||||
internal-access/manifest, documentation and coordination gates
|
||||
- Fixtures: ArrayMesh and one synthetic triangle/billboard M2 Dictionary
|
||||
- Fidelity evidence: exact refresh version, predicate, rebuild and fallback transitions
|
||||
- Performance budget: 100 cycles over 256 ordinary finalizations under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- Classifier/extractor/cache/pipeline, world-renderer, registry and RENDER updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name introduced: `M2RuntimeMeshFinalizer`
|
||||
- Simplest solution: one service composing existing classifier/extractor and M2Builder
|
||||
- Rejected complexity: callbacks for loader I/O, generic material pipeline,
|
||||
cache ownership, signals or strategy framework
|
||||
- Unavoidable complexity: two-phase raw-data request avoids loading raw files for current Meshes
|
||||
- Measured optimization evidence: bounded synthetic ordinary finalization loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: finalizer, loader/test adapters, transition/rebuild verifier and required documentation
|
||||
- Next: further M2 raw-data I/O/prototype state extraction
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `ece7724`
|
||||
- Results: finalizer passes current/stale/ordinary/synthetic-rebuild/fallback/
|
||||
clear contracts and 100-by-256 finalizations in 53.719 ms; 37 headless
|
||||
renderer/coordinate regressions pass; checkpoint dry-run passes 7/7;
|
||||
documentation and coordination gates pass.
|
||||
- Remaining risks: rebuild remains synchronous behind the existing permit; no
|
||||
private M2 material/traversal/descriptor-pressure/leak/p95/p99 or original-
|
||||
client comparison; raw `.m2` I/O remains loader-owned by design.
|
||||
- Documentation updated: inline API; `m2-runtime-mesh-finalizer.md` with
|
||||
data-flow, lifecycle, sequence and dependency diagrams; classifier/extractor/
|
||||
cache/pipeline specs, module registry, `world-renderer.md` and `RENDER.md`.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:ece7724 -->
|
||||
|
||||
## Integration
|
||||
|
||||
- Merge commit: `1fafdde`
|
||||
- Post-merge results: finalizer `cases=11 iterations=100
|
||||
elapsed_ms=54.349`; classifier, extractor, cache, pipeline, materials,
|
||||
shutdown, facade, internal-access `34`, manifest `7/7`, documentation and
|
||||
coordination gates passed.
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RUNTIME-MESH-FINALIZER-001:1fafdde -->
|
||||
@@ -0,0 +1,115 @@
|
||||
# M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001 — M2 runtime rebuild classifier
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`
|
||||
- Lease expires UTC: 2026-07-20
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract the memoized decision that stale cached M2 meshes require an in-memory
|
||||
runtime rebuild because raw batches contain billboard vertices or referenced UV
|
||||
rotation transforms.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Load raw M2 files or cache Resources.
|
||||
- Build, replace or mutate Meshes, materials, Nodes or RIDs.
|
||||
- Change `M2_MATERIAL_REFRESH_VERSION` or cache formats.
|
||||
- Interpret animation, particles, ribbons or general M2 shader fidelity.
|
||||
- Change path normalization, ResourceLoader transitions or finalize budgets.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd`,
|
||||
`src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd`,
|
||||
`docs/modules/m2-runtime-mesh-rebuild-classifier.md`, this claim
|
||||
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
|
||||
`docs/modules/world-renderer.md`, `docs/modules/README.md`, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`
|
||||
- Generated/ignored: `.godot`, native DLL, generated ADT/M2 resources, caches
|
||||
and proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Billboard metadata requires rebuild when `has_billboard` is true or
|
||||
`billboard_vertex_count` is positive in any Dictionary batch.
|
||||
- UV rotation requires non-empty transforms and combos plus a referenced
|
||||
non-identity `Vector4` rotation or absolute rotation speed above `0.000001`.
|
||||
- Only up to four texture stages per batch are inspected; invalid variants and
|
||||
out-of-range combo/transform indices are skipped.
|
||||
- The first decision for a normalized path is memoized until clear, matching the
|
||||
existing loader cache lifetime.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: current accepted master `90b1dac`
|
||||
- Blocks: further M2 mesh request/cache/finalization extraction
|
||||
- External state: none; verification uses synthetic raw M2 Dictionaries
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated billboard/UV/index/stage/cache/clear/source/timing verifier;
|
||||
M2 builder/grouper/transform/registry, materials, shutdown, facade,
|
||||
internal-access, manifest, documentation and coordination gates
|
||||
- Fixtures: synthetic batches, transform combos and rotation dictionaries
|
||||
- Fidelity evidence: exact existing predicate thresholds, stage cap and cache lifetime preserved
|
||||
- Performance budget: 100 classifications over 256 paths under 1 second
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline public API docs
|
||||
- Module specification with inputs/outputs, ownership and source map
|
||||
- Data-flow, lifecycle/state, sequence and dependency diagrams
|
||||
- World-renderer, module registry and RENDER status updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important names introduced: `M2RuntimeMeshRebuildClassifier`
|
||||
- Simplest considered solution: one RefCounted with one memoized query and clear
|
||||
- Rejected complexity/abstractions: generic predicate cache, strategy interface,
|
||||
ResourceLoader wrapper, signals or typed rewrite of raw parser Dictionaries
|
||||
- Unavoidable complexity and justification: four-stage combo indirection is the
|
||||
existing build-12340 M2 material boundary
|
||||
- Measured optimization evidence: bounded synthetic classification loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: classifier/cache implemented; loader query and both clear sites migrated;
|
||||
synthetic predicate/cache verifier and required documentation added
|
||||
- Next: continue M2 mesh request/cache/finalization extraction
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:38f831e -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `19ca8f4`
|
||||
- Results: classifier PASS (`cases=12`, `iterations=100`,
|
||||
`elapsed_ms=64.585`; initial `35.461ms`); billboard flag/count, invalid
|
||||
variants/indices, UV rotation/speed/epsilon, four-stage cap, memoization,
|
||||
detached diagnostics and clear contracts PASS; 33 M2/WMO/liquid/terrain/
|
||||
facade/scheduler/streaming/coordinate/material/shutdown regressions PASS;
|
||||
internal-access decreased to `35`; documentation and coordination gates PASS;
|
||||
checkpoint dry-run PASS `7/7`.
|
||||
- Remaining risks: raw parser data remains Dictionary-based; the first decision
|
||||
for a path remains authoritative until clear; classification covers the current
|
||||
four-stage material implementation only; private asset descriptor-pressure,
|
||||
traversal, p95/p99 and paired original-client evidence remain absent.
|
||||
- Documentation updated: inline public API; new
|
||||
`docs/modules/m2-runtime-mesh-rebuild-classifier.md`; module registry, world
|
||||
renderer and `RENDER.md`; data-flow/state/sequence/dependency diagrams,
|
||||
ownership, failure/recovery, thresholds, cache lifetime and source map included.
|
||||
- Integration: merged as `38f831e`; post-merge classifier (`cases=12`,
|
||||
`iterations=100`, `elapsed_ms=45.439`), materials, shutdown, M2 batch planner,
|
||||
facade, internal-access (`private_symbols=35`), `7/7` manifest, documentation
|
||||
and coordination gates passed.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001:19ca8f4 -->
|
||||
@@ -16,10 +16,17 @@
|
||||
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
|
||||
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
|
||||
| ADT water load pipeline state | Implemented extraction | [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md) |
|
||||
| ADT water scene finalizer | Implemented extraction | [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.md) |
|
||||
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
|
||||
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
|
||||
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
||||
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
||||
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||
| M2 mesh load pipeline state | Implemented extraction | [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md) |
|
||||
| M2 mesh resource cache state | Implemented extraction | [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md) |
|
||||
| M2 mesh resource extractor | Implemented extraction | [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md) |
|
||||
| M2 runtime mesh finalizer | Implemented extraction | [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md) |
|
||||
| M2 raw model repository | Implemented extraction | [`m2-raw-model-repository.md`](m2-raw-model-repository.md) |
|
||||
| WMO placement resolver | Implemented extraction | [`wmo-placement-resolver.md`](wmo-placement-resolver.md) |
|
||||
| WMO placement registry | Implemented extraction | [`wmo-placement-registry.md`](wmo-placement-registry.md) |
|
||||
| WMO render build step planner | Implemented extraction | [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md) |
|
||||
|
||||
@@ -38,8 +38,9 @@ flowchart LR
|
||||
State --> Result[Parsed ADT result]
|
||||
Result --> Loader
|
||||
Loader --> Budget[WATER_FINALIZE permit]
|
||||
Loader --> Builder[ADTBuilder water scene]
|
||||
Builder --> Scene[Main-thread tile Node attach]
|
||||
Loader --> Finalizer[AdtWaterSceneFinalizer]
|
||||
Finalizer --> Builder[ADTBuilder water scene]
|
||||
Finalizer --> Scene[Main-thread tile Node attach]
|
||||
```
|
||||
|
||||
Allowed dependencies are RefCounted, Dictionary/Array/String and one Godot
|
||||
@@ -127,7 +128,7 @@ sequenceDiagram
|
||||
participant State as AdtWaterLoadPipelineState
|
||||
participant Worker as WorkerThreadPool task
|
||||
participant Budget as RenderBudgetScheduler
|
||||
participant Builder as ADTBuilder
|
||||
participant Finalizer as AdtWaterSceneFinalizer
|
||||
Loader->>State: enqueue_request(tile key, ADT path)
|
||||
Loader->>State: take_next_request()
|
||||
Loader->>Worker: add_task(parse binding)
|
||||
@@ -140,8 +141,8 @@ sequenceDiagram
|
||||
Loader->>Worker: wait_for_task_completion(task ID)
|
||||
Loader->>State: complete_active_task(key)
|
||||
Loader->>Loader: reject stale tile/path/root
|
||||
Loader->>Builder: build_tile_water_scene(data, origin)
|
||||
Loader->>Loader: attach Node and mark water_loaded
|
||||
Loader->>Finalizer: attach_water_scene(data, origin, tile root, editor owner)
|
||||
Loader->>Loader: mark water_loaded
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
@@ -151,10 +152,11 @@ flowchart TB
|
||||
Loader[StreamingWorldLoader] --> State[AdtWaterLoadPipelineState]
|
||||
Loader --> Scheduler[RenderBudgetScheduler]
|
||||
Loader --> Worker[WorkerThreadPool / ADTLoader]
|
||||
Loader --> Builder[ADTBuilder / SceneTree]
|
||||
Loader --> Finalizer[AdtWaterSceneFinalizer]
|
||||
Finalizer --> Builder[ADTBuilder / SceneTree]
|
||||
Worker -->|publish only| State
|
||||
State -. no dependency .-> Scheduler
|
||||
State -. no dependency .-> Builder
|
||||
State -. no dependency .-> Finalizer
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
@@ -163,7 +165,8 @@ flowchart TB
|
||||
- Worker threads may call only `publish_result`; result observe/pop/clear also lock.
|
||||
- Parsed water Dictionary identity is retained until main-thread pop.
|
||||
- Loader owns WorkerThreadPool task lifetime and must wait before orderly shutdown clear.
|
||||
- Loader owns tile states, ADTBuilder, water Nodes and every render resource.
|
||||
- Loader owns tile states and the tile subtree; `AdtWaterSceneFinalizer` performs
|
||||
the main-thread ADTBuilder call and attachment without retaining Nodes.
|
||||
- `clear` cannot cancel a worker; post-reset publication becomes a stale result
|
||||
and is discarded because no active task/tile ownership remains.
|
||||
|
||||
@@ -211,8 +214,8 @@ versions are unchanged; no migration or rebuild is required.
|
||||
## Extension points
|
||||
|
||||
- ADT parse execution can later move behind a parser/job adapter without changing state.
|
||||
- Water scene materialization can be extracted separately because it owns Nodes
|
||||
and main-thread effects rather than async bookkeeping.
|
||||
- Water scene materialization is now delegated to `AdtWaterSceneFinalizer`,
|
||||
independently of this async bookkeeping service.
|
||||
|
||||
## Capability status
|
||||
|
||||
@@ -220,7 +223,7 @@ versions are unchanged; no migration or rebuild is required.
|
||||
|---|---|---|---|
|
||||
| ADT water load pipeline state | Implemented extraction | FIFO/task/thread/source/timing verifier | Asset-backed traversal/leak evidence pending |
|
||||
| ADT liquid parsing | Existing/loader-owned adapter | Native ADTLoader and dry-run paths | MH2O/MCLQ fidelity fixtures pending |
|
||||
| Water scene finalization | Existing/loader-owned | Scheduler/material/shutdown regressions | Separate materialization extraction possible |
|
||||
| Water scene finalization | Implemented extraction | Dedicated synthetic scene/ownership verifier | Asset-backed traversal/hitch evidence pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
@@ -235,13 +238,15 @@ versions are unchanged; no migration or rebuild is required.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/liquid/adt_water_load_pipeline_state.gd` | Request/task/result bookkeeping and result mutex |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and Node finalization |
|
||||
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADTBuilder call, attach and optional editor ownership |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Parse task execution, permits, stale checks and tile loaded timing |
|
||||
| `addons/mpq_extractor/loaders/adt_builder.gd` | ADT liquid geometry/material construction |
|
||||
| `src/tools/verify_adt_water_load_pipeline_state.gd` | Lifecycle/thread/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`adt-water-scene-finalizer.md`](adt-water-scene-finalizer.md)
|
||||
- [`render-budget-scheduler.md`](render-budget-scheduler.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
|
||||
@@ -0,0 +1,232 @@
|
||||
# ADT Water Scene Finalizer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-ADT-WATER-SCENE-FINALIZER-001` |
|
||||
| Owners | Main-thread ADT water subtree build, attach and optional editor ownership |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-scene-finalizer`, 2026-07-17 |
|
||||
| Profiles/capabilities | Existing ADT MH2O/MCLQ scene output; profile-independent adapter |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide one liquid-specific main-thread boundary for converting parsed ADT
|
||||
water data through the existing `ADTBuilder`, attaching the resulting `Water`
|
||||
subtree to a caller-owned tile root and assigning optional persisted Editor
|
||||
ownership. The service is stateless and retains no Node or Resource.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Parse or validate MH2O/MCLQ binary structures.
|
||||
- Replace ADTBuilder geometry, material or coordinate calculations.
|
||||
- Own water request/task/result state, permits or concurrency.
|
||||
- Validate tile/path/root freshness beyond receiving a valid root.
|
||||
- Mark `water_loaded`, enable water or remove water subtrees.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Parse[Parsed ADT Dictionary] --> Loader[StreamingWorldLoader]
|
||||
Loader -->|validated data, origin, tile root| Finalizer[AdtWaterSceneFinalizer]
|
||||
Finalizer --> Builder[ADTBuilder]
|
||||
Builder --> Water[Water Node3D or null]
|
||||
Water --> Finalizer
|
||||
Finalizer -->|main-thread add_child| Tile[Caller-owned tile root]
|
||||
Loader -->|after return| Loaded[water_loaded timing]
|
||||
```
|
||||
|
||||
Allowed dependencies are `ADTBuilder` and Godot Node/mesh types needed to adopt
|
||||
its output. WorkerThreadPool, scheduler, tile-state dictionaries, facade,
|
||||
gameplay, network and editor plugin packages are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `attach_water_scene(water_data, tile_origin_godot_units, tile_root, editor_owner)` | Command/query | Build and attach one existing-format water subtree | Renderer main thread; returned Node is borrowed for tile lifetime | Empty data, invalid root or dry build returns null without attachment |
|
||||
|
||||
`editor_owner` is optional. When supplied it must be an ancestor suitable for
|
||||
Godot `Node.owner`; ownership is recursively assigned to the generated subtree.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Parsed ADT water-containing Dictionary | ADTLoader/raw tile path | Finalizer/ADTBuilder | Caller retains Dictionary; not mutated | One main-thread call |
|
||||
| Input | Tile origin in Godot world units | Loader tile state/ADTBuilder | Finalizer | Value copy | One call |
|
||||
| Input | Valid tile root | StreamingWorldLoader | Finalizer | Caller/SceneTree-owned | Must outlive call and attached subtree |
|
||||
| Input | Optional edited scene root | Loader Editor policy | Finalizer | SceneTree-owned borrowed Node | One call |
|
||||
| Output | Attached `Water` Node3D or null | Finalizer | Loader/SceneTree | Tile root owns attached Node | Tile subtree lifetime |
|
||||
|
||||
Side effects are limited to ADTBuilder mesh/material creation, `add_child` and
|
||||
optional recursive `Node.owner` assignment. No cache, filesystem, task or tile
|
||||
state is mutated.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Input[water data + Godot origin + tile root] --> Guard{Data and root valid?}
|
||||
Guard -->|no| None[Return null; unchanged]
|
||||
Guard -->|yes| Build[ADTBuilder.build_tile_water_scene]
|
||||
Build --> Dry{Water root exists?}
|
||||
Dry -->|no| None
|
||||
Dry -->|yes| Attach[tile root add_child]
|
||||
Attach --> Owner{Editor owner supplied?}
|
||||
Owner -->|yes| Assign[Assign owner recursively]
|
||||
Owner -->|no| Return[Return borrowed root]
|
||||
Assign --> Return
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
The service has no retained lifecycle. Each call moves one temporary builder
|
||||
result through the following call-local states.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Validating
|
||||
Validating --> Rejected: empty data or invalid root
|
||||
Validating --> Building: valid input
|
||||
Building --> Dry: no geometry
|
||||
Building --> Attached: Water root built
|
||||
Attached --> EditorOwned: owner supplied
|
||||
Attached --> Complete: no owner
|
||||
EditorOwned --> Complete
|
||||
Rejected --> [*]
|
||||
Dry --> [*]
|
||||
Complete --> [*]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader main thread
|
||||
participant Finalizer as AdtWaterSceneFinalizer
|
||||
participant Builder as ADTBuilder
|
||||
participant Tile as Tile root
|
||||
Loader->>Finalizer: attach_water_scene(data, origin, tile, editor owner)
|
||||
Finalizer->>Builder: build_tile_water_scene(data, origin)
|
||||
Builder-->>Finalizer: Water root or null
|
||||
alt Water root exists
|
||||
Finalizer->>Tile: add_child(Water)
|
||||
opt editor owner supplied
|
||||
Finalizer->>Finalizer: assign owner recursively
|
||||
end
|
||||
Finalizer-->>Loader: borrowed Water root
|
||||
else dry/invalid
|
||||
Finalizer-->>Loader: null
|
||||
end
|
||||
Loader->>Loader: preserve existing water_loaded timing
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[AdtWaterSceneFinalizer]
|
||||
Finalizer --> Builder[ADTBuilder]
|
||||
Builder --> Material[WowLiquidMaterial]
|
||||
Finalizer --> Scene[Godot SceneTree]
|
||||
Pipeline[AdtWaterLoadPipelineState] --> Loader
|
||||
Scheduler[RenderBudgetScheduler] --> Loader
|
||||
Finalizer -. no dependency .-> Pipeline
|
||||
Finalizer -. no dependency .-> Scheduler
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The method is main-thread only because it creates Mesh/Material/Node objects
|
||||
and mutates the SceneTree.
|
||||
- `ADTBuilder` owns temporary construction. It frees an empty `Water` root and
|
||||
returns null for dry/invalid liquid geometry.
|
||||
- After `add_child`, the caller's tile subtree owns the returned Water root and
|
||||
all MeshInstance children; the finalizer retains no references.
|
||||
- Optional editor ownership affects serialization ownership only and does not
|
||||
replace SceneTree lifetime ownership.
|
||||
- Parsed Dictionaries are borrowed and not mutated.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty parsed data | Input guard | Return null, no builder call | Contract verifier | Loader preserves existing loaded timing |
|
||||
| Missing/freed tile root | Input guard | Return null, no scene mutation | Contract verifier | Stale-result validation normally rejects earlier |
|
||||
| Dry or malformed liquid arrays | ADTBuilder returns null | No attachment | Builder/shutdown regression | Reload after corrected source |
|
||||
| Tile/path stale | Loader before call | Finalizer is not invoked | Existing loader behavior | Current tile may enqueue again |
|
||||
| Shutdown/cancellation | Loader drains/cancels before call | No retained finalizer work | Shutdown verifier | New loader starts stateless |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| `enable_water` | Loader setting | Quality/custom | Yes | Gates both caller paths; not owned here |
|
||||
| `water_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Budgets async calls before finalizer |
|
||||
| `editor_persist_generated_nodes` | `false` | Editor only | Yes | Supplies or omits editor owner |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The service writes no files and changes no cache/resource schema. Existing ADT,
|
||||
terrain cache and liquid material versions remain valid; no rebuild or migration
|
||||
is required. Persisted Editor ownership follows the loader's existing opt-in.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs: none; existing loader/builder diagnostics remain authoritative.
|
||||
- Metrics: no independent counters; existing water queue/finalize metrics remain.
|
||||
- Debug views: generated `Water/Liquid_<id>` names remain unchanged.
|
||||
- Correlation IDs: tile/path correlation stays in loader pipeline state.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_adt_water_scene_finalizer.gd`: empty/invalid/dry guards, one-cell
|
||||
geometry, attachment, recursive owner, loader delegation and bounded timing.
|
||||
- `verify_adt_water_load_pipeline_state.gd`: adjacent async ownership and loaded timing.
|
||||
- Renderer material and shutdown verifiers cover material construction and dry-root cleanup.
|
||||
- Fidelity evidence is the unchanged ADTBuilder implementation and unchanged two
|
||||
loader call positions; no new original-client liquid parity claim is made.
|
||||
- Performance budget: 100 empty calls under one second.
|
||||
|
||||
## Extension points
|
||||
|
||||
- A future liquid renderer may replace the internal builder only after paired
|
||||
MH2O/MCLQ fixtures establish equivalent geometry/material behavior.
|
||||
- Removal/eviction remains ordinary tile subtree ownership and needs no parallel API.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| ADT water scene finalization seam | Implemented extraction | Synthetic geometry/attach/owner/source verifier | Asset-backed traversal and hitch metrics pending |
|
||||
| ADT liquid geometry/materials | Existing implementation | ADTBuilder/material/checkpoint evidence | MH2O/MCLQ and original-client parity incomplete |
|
||||
| Async water scheduling | Existing extracted state | Water pipeline verifier | Active worker interruption remains loader-owned |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- The service intentionally wraps the existing Dictionary-based ADTBuilder boundary.
|
||||
- It does not verify that `editor_owner` is an ancestor; the loader supplies the
|
||||
edited scene root under the existing policy.
|
||||
- Mesh/material creation remains synchronous main-thread work behind the existing permit.
|
||||
- No proprietary liquid corpus, long traversal leak run, p95/p99 or paired capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread build, attach and optional recursive editor ownership |
|
||||
| `addons/mpq_extractor/loaders/adt_builder.gd` | Existing ADT liquid geometry/material creation and dry-root cleanup |
|
||||
| `src/render/liquid/adt_water_load_pipeline_state.gd` | Pending/task/result state before finalization |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Permits, stale checks, tile state and finalizer calls |
|
||||
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic scene/ownership/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`adt-water-load-pipeline-state.md`](adt-water-load-pipeline-state.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`render-budget-scheduler.md`](render-budget-scheduler.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,239 @@
|
||||
# M2 Mesh Load Pipeline State
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-MESH-LOAD-PIPELINE-001` |
|
||||
| Owners | Static M2 threaded request records and terminal finalize FIFO |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-load-pipeline`, 2026-07-17 |
|
||||
| Profiles/capabilities | Existing static M2 cached `.tscn`/`.glb` load path |
|
||||
|
||||
## Purpose
|
||||
|
||||
Own cross-frame bookkeeping between successful static M2 ResourceLoader request
|
||||
start, terminal loaded/failed polling and budgeted main-thread finalization. The
|
||||
loader retains every I/O and engine-resource operation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Call ResourceLoader or select cache paths/formats.
|
||||
- Own M2 scene, missing or material-refresh caches, or prepared Mesh references.
|
||||
- Extract Meshes from PackedScene/GLB Resources.
|
||||
- Consume finalize permits or rebuild/adopt runtime Meshes.
|
||||
- Merge static and animated M2 pipelines.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Need[Static M2 mesh needed] --> Loader[StreamingWorldLoader]
|
||||
Loader --> IO[ResourceLoader request]
|
||||
Loader --> State[M2MeshLoadPipelineState]
|
||||
State --> Poll[Detached pending records]
|
||||
Poll --> Loader
|
||||
Loader --> IO
|
||||
Loader -->|terminal status| State
|
||||
State --> FIFO[Finalize FIFO]
|
||||
FIFO --> Loader
|
||||
Loader --> Budget[M2_MESH_FINALIZE permit]
|
||||
Loader --> Cache[M2 Mesh resource cache or shared missing cache]
|
||||
```
|
||||
|
||||
Allowed dependencies are value containers, Strings and opaque integer statuses.
|
||||
ResourceLoader, filesystem, WorkerThreadPool, Mesh/Resource/Node/RID ownership,
|
||||
scheduler and other application layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `remember_request(normalized_relative_path, resource_path)` | Command/query | Insert one pending request | Renderer main thread; until terminal/discard/clear | Empty/duplicate rejected |
|
||||
| `has_request(path)` | Query | Test pending dedupe | Main thread | Empty/unknown false |
|
||||
| `request_records_snapshot()` | Query | Copy pending records in insertion order | Main thread; caller-owned | None |
|
||||
| `complete_request(path, terminal_status)` | Command/query | Move request to terminal FIFO | Main thread after poll | Unknown rejected |
|
||||
| `discard_request(path)` | Command/query | Remove request without finalization | Main thread | Unknown false |
|
||||
| `has_finalize_record()` / `pop_finalize_record()` | Query/command | Observe/pop completion-order FIFO | Main-thread budget drain | Empty pop returns `{}` |
|
||||
| `total_work_count()` | Query | Preserve pending-plus-finalize metric | Main thread | None |
|
||||
| `pending_request_count()` / `finalize_record_count()` | Query | Stage diagnostics | Main thread | None |
|
||||
| `clear()` | Command | Drop bookkeeping after I/O drain/reset | Main thread | Idempotent; does not drain I/O |
|
||||
| `diagnostic_snapshot()` | Query | Detached request/finalize records | Main thread | No Resources exposed |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Normalized M2 path and cache Resource path | Loader request adapter | Pending map | Copied Strings | Until terminal/discard/clear |
|
||||
| Input | Opaque terminal ResourceLoader status | Loader poll adapter | Finalize record | Integer value | Until pop/clear |
|
||||
| Output | Detached pending records | Pipeline state | Loader poll/shutdown adapter | Caller-owned copies | One poll/wait pass |
|
||||
| Output | Oldest terminal record | Pipeline state | Loader finalizer | Record ownership transferred | One finalize attempt |
|
||||
| Output | Detached diagnostics | Pipeline state | Verifier/future metrics | Caller-owned copies | Snapshot lifetime |
|
||||
|
||||
Side effects are limited to collection mutation and retaining String/integer values.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Start[Successful ResourceLoader request] --> Remember[remember request]
|
||||
Remember --> Snapshot[request records snapshot]
|
||||
Snapshot --> Poll[Loader polls status]
|
||||
Poll --> Active{In progress?}
|
||||
Active -->|yes| Snapshot
|
||||
Active -->|no| Complete[complete request with terminal status]
|
||||
Complete --> FIFO[Append finalize FIFO]
|
||||
FIFO --> Permit{Loader permit available?}
|
||||
Permit -->|no| FIFO
|
||||
Permit -->|yes| Pop[Pop oldest terminal record]
|
||||
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
|
||||
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
|
||||
Prepare --> Adopt[Loader adopts Mesh/missing state]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Absent
|
||||
Absent --> Pending: remember request
|
||||
Pending --> Pending: non-terminal poll
|
||||
Pending --> TerminalQueued: complete request
|
||||
Pending --> Absent: discard
|
||||
TerminalQueued --> Absent: pop finalize
|
||||
Pending --> Absent: clear
|
||||
TerminalQueued --> Absent: clear
|
||||
```
|
||||
|
||||
One path has at most one pending request. After completion it may be requested
|
||||
again only if loader cache/missing rules permit it.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Resource as ResourceLoader
|
||||
participant State as M2MeshLoadPipelineState
|
||||
participant Budget as RenderBudgetScheduler
|
||||
Loader->>Resource: load_threaded_request(cache path)
|
||||
Resource-->>Loader: OK or ERR_BUSY
|
||||
Loader->>State: remember_request(normalized, cache path)
|
||||
loop frames
|
||||
Loader->>State: request_records_snapshot()
|
||||
Loader->>Resource: load_threaded_get_status(path)
|
||||
end
|
||||
Loader->>State: complete_request(normalized, terminal status)
|
||||
Loader->>Budget: try_consume_permit(M2_MESH_FINALIZE)
|
||||
Loader->>State: pop_finalize_record()
|
||||
Loader->>Resource: load_threaded_get(path)
|
||||
Loader->>Loader: extract/refresh/adopt Mesh or mark missing
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> State[M2MeshLoadPipelineState]
|
||||
Loader --> Resource[ResourceLoader]
|
||||
Loader --> Budget[RenderBudgetScheduler]
|
||||
Loader --> MeshCache[M2 Mesh resource cache and shared missing cache]
|
||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
State -. no dependency .-> Resource
|
||||
State -. no dependency .-> Budget
|
||||
State -. no dependency .-> MeshCache
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Main thread serializes all state mutation and snapshots.
|
||||
- State owns only request/finalize Dictionaries containing Strings and status integers.
|
||||
- Loader owns ResourceLoader request lifetime and drains active paths before shutdown clear.
|
||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and
|
||||
`M2RuntimeMeshFinalizer` owns refresh/rebuild/fallback. The loader owns shared
|
||||
missing state, raw I/O and remaining engine resources.
|
||||
- Snapshot and diagnostic records are detached; caller mutation cannot affect state.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty/duplicate request | State guard | Reject unchanged | Contract verifier | Correct caller/request later |
|
||||
| Request start failure | Loader return code | No state insert; mark missing | Existing loader behavior | Cache/source correction |
|
||||
| Non-terminal status | Loader poll | Keep pending | Existing queue metric | Poll next frame |
|
||||
| Terminal load failure | Status in popped record | Loader marks missing | Existing missing behavior | World/cache reload |
|
||||
| Empty defensive path | Loader before poll | Discard and mark missing | Source regression | Correct request producer |
|
||||
| Shutdown | Loader drains pending Resource paths | Clear state | Shutdown verifier | New loader starts empty |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| `m2_mesh_finalize_ops_per_tick` | `1` | Quality/custom | Yes | Bounds terminal FIFO pops outside state |
|
||||
| Cache extension order | `.tscn`, `.glb` | Existing M2 path | No | Loader selects request path |
|
||||
| Animated pivot-prefix GLB exclusion | Existing rule | All | No | Loader filters before request |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No pipeline state is serialized. M2 cache formats, material refresh version and
|
||||
rebuild policy are unchanged; no migration or rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- `total_work_count` preserves all three historical `m2_mesh` metrics.
|
||||
- Diagnostic snapshots expose only paths/statuses, never loaded Resources or Meshes.
|
||||
- Existing loader logs and hitch sections remain unchanged.
|
||||
- Normalized M2 path is the correlation key.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_mesh_load_pipeline_state.gd`: validation, dedupe, insertion-order
|
||||
polling, terminal transition, completion FIFO, opaque status, discard, metrics,
|
||||
detached diagnostics, loader ownership and 100-by-256 timing.
|
||||
- Classifier/material/shutdown/M2 placement/build regressions cover adjacent behavior.
|
||||
- Fidelity evidence is exact state/lifecycle extraction; no asset-backed or
|
||||
original-client visual parity claim is made.
|
||||
- Performance budget: 25,600 request/complete/pop transitions under one second.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Mesh/missing cache adoption can move to a separate finalizer without changing this state.
|
||||
- Animated M2 has distinct candidate/static fallback semantics and remains separate.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Static M2 request/finalize state | Implemented extraction | Contract/source/timing verifier | Asset-backed long traversal pending |
|
||||
| ResourceLoader I/O | Existing loader-owned | Shutdown/material regressions | I/O adapter extraction optional |
|
||||
| Mesh cache | Implemented extraction | Mesh resource cache state verifier | Asset-backed memory/leak run pending |
|
||||
| First-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||
| Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
|
||||
| Mesh materialization | Existing loader-owned | Renderer/material tests | Further safe extraction pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Records remain Dictionaries because ResourceLoader polling is a dynamic Godot boundary.
|
||||
- Clear does not drain ResourceLoader; caller ordering is mandatory.
|
||||
- Status integers are intentionally opaque inside state.
|
||||
- No private asset traversal, leak/descriptor-pressure or p95/p99 run is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending records, terminal FIFO and metrics |
|
||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh references and final clear |
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Cache path choice, I/O polling, permits and Mesh/missing adoption |
|
||||
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Downstream stale Mesh rebuild decision |
|
||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Lifecycle/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
|
||||
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,215 @@
|
||||
# M2 Mesh Resource Cache State
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-CACHE-001` |
|
||||
| Owners | Prepared static M2 Mesh resource references by normalized path |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-cache`, 2026-07-17 |
|
||||
| Profiles/capabilities | Existing static M2 cached and raw fallback paths |
|
||||
|
||||
## Purpose
|
||||
|
||||
Own the runtime cache of prepared static M2 `Mesh` references outside
|
||||
`StreamingWorldLoader`. Dedicated services extract and prepare each Mesh; the
|
||||
loader remains responsible for finding, loading, adopting and materializing it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Own the shared M2 missing-path, prototype or animated-scene caches.
|
||||
- Call ResourceLoader/FileAccess or select `.tscn`/`.glb` paths.
|
||||
- Extract a Mesh from a Resource or Node.
|
||||
- Refresh materials, rebuild Meshes or create MultiMesh instances.
|
||||
- Add eviction, persistence, cache versions or quality-profile behavior.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Need[Static M2 mesh lookup] --> Loader[StreamingWorldLoader]
|
||||
Loader --> Cache[M2MeshResourceCacheState]
|
||||
Cache -->|cached Mesh or null| Loader
|
||||
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||
Pipeline --> IO[ResourceLoader]
|
||||
IO --> Extract[M2MeshResourceExtractor]
|
||||
Extract --> Prepare[M2RuntimeMeshFinalizer]
|
||||
Prepare --> Cache
|
||||
Loader --> Build[M2 MultiMesh materialization]
|
||||
```
|
||||
|
||||
The cache may retain `Mesh` resources and copied normalized-path Strings. It has
|
||||
no Node, ResourceLoader, filesystem, worker, scheduler, missing-state, builder,
|
||||
gameplay, network or editor dependency.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `store_mesh(path, mesh)` | Command/query | Retain or replace one prepared Mesh | Renderer main thread; final shutdown | Empty path/null Mesh rejected |
|
||||
| `has_mesh(path)` | Query | Test whether a Mesh is retained | Renderer main thread | Empty/unknown false |
|
||||
| `find_mesh(path)` | Query | Borrow exact retained Mesh reference | Renderer main thread | Empty/unknown null |
|
||||
| `mesh_count()` | Query | Return retained entry count | Diagnostics/tests | None |
|
||||
| `normalized_paths_snapshot()` | Query | Copy insertion-order path keys | Diagnostics/tests | Never exposes Mesh references |
|
||||
| `clear()` | Command | Release every retained Mesh reference | Final shutdown after async drain | Idempotent |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Normalized relative M2 path | Loader normalization adapter | Cache key | Copied String | Cache lifetime |
|
||||
| Input | Prepared static Mesh | Loader finalizer or synchronous fallback | Cache value | Strong reference retained | Until replacement/clear |
|
||||
| Output | Borrowed exact Mesh | Cache lookup | Static M2 batch builder | Cache retains reference | One build/materialization pass |
|
||||
| Output | Detached normalized paths | Cache diagnostics | Verifier/future metrics | Caller-owned Strings | Snapshot lifetime |
|
||||
|
||||
Store and clear mutate only the cache Dictionary and Resource reference counts.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Lookup[Lookup normalized M2 path] --> Cached{Mesh cached?}
|
||||
Cached -->|yes| Return[Return retained Mesh]
|
||||
Cached -->|no| Missing{Shared missing state?}
|
||||
Missing -->|yes| Null[Return null]
|
||||
Missing -->|no| Load[Loader request or synchronous fallback]
|
||||
Load --> Prepare[Extractor and runtime finalizer prepare Mesh]
|
||||
Prepare --> Valid{Prepared Mesh valid?}
|
||||
Valid -->|yes| Store[Store or replace cache entry]
|
||||
Valid -->|no| MarkMissing[Loader updates shared missing state]
|
||||
Store --> Return
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Absent
|
||||
Absent --> Cached: store valid Mesh
|
||||
Cached --> Cached: replace Mesh reference
|
||||
Cached --> Absent: final shutdown clear
|
||||
```
|
||||
|
||||
Map/world refresh retains cached Meshes. Only the existing final runtime-cache
|
||||
release clears them after asynchronous shutdown drain.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Cache as M2MeshResourceCacheState
|
||||
participant Pipeline as M2MeshLoadPipelineState
|
||||
Loader->>Cache: find/has normalized path
|
||||
alt cache miss
|
||||
Loader->>Pipeline: request/poll/finalize record
|
||||
Loader->>Loader: ResourceLoader get; delegate extract + prepare
|
||||
Loader->>Cache: store_mesh(path, prepared Mesh)
|
||||
end
|
||||
Cache-->>Loader: exact retained Mesh
|
||||
Loader->>Loader: materialize static M2 batch
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
|
||||
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Loader --> Builder[M2Builder and MultiMesh]
|
||||
Cache --> Mesh[Godot Mesh Resource]
|
||||
Cache -. no dependency .-> Pipeline
|
||||
Cache -. no dependency .-> Classifier
|
||||
Cache -. no dependency .-> Builder
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Renderer main-thread adapters serialize every cache operation.
|
||||
- The cache owns normalized-path Strings and strong Mesh references.
|
||||
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
||||
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
||||
instances. The loader owns missing/prototype/animated state and MultiMeshes.
|
||||
- The loader drains asynchronous work before the final cache clear.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty path or null Mesh | Cache guard | Reject unchanged | Contract verifier | Correct caller input |
|
||||
| Unknown lookup | Membership query | Return null | Existing loader path | Request/load or missing decision |
|
||||
| Replacement | Existing key | Retain new exact reference | Contract verifier | Normal subsequent lookup |
|
||||
| Load/preparation failure | Loader boundary | Do not store; loader marks missing | Existing renderer behavior | Cache/source correction and reload |
|
||||
| Final shutdown | Loader lifecycle | Clear releases references | Shutdown/source verifier | New loader starts empty |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The module adds no setting, budget or profile branch. Cache path order, finalize
|
||||
permits and batching remain loader-owned; refresh version `2` belongs to
|
||||
`M2RuntimeMeshFinalizer`.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The state is runtime-only and serializes nothing. Existing `.tscn`/`.glb`
|
||||
formats, import caches and material refresh metadata are unchanged; no migration
|
||||
or rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
`mesh_count` and `normalized_paths_snapshot` expose scalar ownership diagnostics
|
||||
without Mesh references. Existing renderer metrics/logging are unchanged because
|
||||
the historical Mesh cache had no queue contribution or log site.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_mesh_resource_cache_state.gd`: invalid input, exact reference,
|
||||
replacement, lookup, detached path snapshot, idempotent clear, loader source
|
||||
ownership and 100-by-256 bounded timing.
|
||||
- Mesh pipeline/classifier/material/shutdown/M2 build/facade regressions cover
|
||||
adjacent behavior.
|
||||
- Fidelity evidence is exact cache/lifetime extraction; no original-client
|
||||
visual-parity or asset-backed performance claim is made.
|
||||
|
||||
## Extension points
|
||||
|
||||
- `M2RuntimeMeshFinalizer` returns a prepared Mesh for `store_mesh` without
|
||||
changing cache ownership.
|
||||
- Eviction requires measured memory pressure and explicit lifetime semantics.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Prepared static M2 Mesh cache | Implemented extraction | Contract/source/timing verifier | Asset-backed memory/leak run pending |
|
||||
| M2 Mesh request lifecycle | Implemented extraction | Pipeline state verifier | ResourceLoader I/O remains loader-owned |
|
||||
| M2 Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||
| M2 Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Strong Mesh references intentionally remain until final shutdown.
|
||||
- Shared missing state cannot move here without also redesigning prototype and
|
||||
animated lookup behavior.
|
||||
- No proprietary M2 traversal, leak/descriptor-pressure, p95/p99 or paired
|
||||
original-client capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh references and final clear |
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene lifetime |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Normalization, raw/resource I/O, missing state and materialization |
|
||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Pending request and terminal finalize records |
|
||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Cache ownership/lifetime/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,209 @@
|
||||
# M2 Mesh Resource Extractor
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001` |
|
||||
| Owners | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-mesh-resource-extractor`, 2026-07-17 |
|
||||
| Profiles/capabilities | Static M2 threaded cache and synchronous raw/prototype fallback paths |
|
||||
|
||||
## Purpose
|
||||
|
||||
Select the first M2 Mesh from a direct Resource, PackedScene or existing Node
|
||||
subtree while keeping traversal and temporary-node lifetime outside
|
||||
`StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Request/load Resources or choose cache paths.
|
||||
- Load raw M2 data, refresh materials or rebuild Meshes.
|
||||
- Own Mesh/prototype/missing caches or pending/finalize queues.
|
||||
- Create MultiMeshes, attach Nodes or consume render permits.
|
||||
- Change first-Mesh selection order or visible output.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Pipeline[M2 load pipeline] --> Loader[StreamingWorldLoader]
|
||||
Loader --> Resource[Loaded Mesh or PackedScene Resource]
|
||||
Resource --> Extractor[M2MeshResourceExtractor]
|
||||
Extractor -->|first Mesh or null| Loader
|
||||
Loader --> Prepare[M2RuntimeMeshFinalizer]
|
||||
Prepare --> Cache[M2MeshResourceCacheState]
|
||||
Prototype[Existing M2 Node subtree] --> Extractor
|
||||
```
|
||||
|
||||
The extractor may inspect `Resource`, `PackedScene`, `Node`, `MeshInstance3D`
|
||||
and `Mesh` engine types. It has no ResourceLoader, filesystem, worker, cache,
|
||||
scheduler, builder, gameplay, network or editor dependency.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `extract_first_mesh(resource)` | Query with temporary ownership | Return direct Mesh or first Mesh from one PackedScene instance | Renderer main thread; temporary root freed before return | Null/unsupported/empty returns null |
|
||||
| `find_first_mesh_in_subtree(root)` | Query | Return first Mesh in historical depth-first preorder | Renderer main thread; caller-owned subtree unchanged | Null/no Mesh returns null |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Direct Mesh Resource | Loader threaded finalizer | Extractor | Borrowed Resource | One call |
|
||||
| Input | PackedScene Resource | Loader threaded finalizer | Extractor | Borrowed scene; extractor owns temporary instance | Temporary root until return |
|
||||
| Input | Existing prototype/rebuild Node subtree | Loader builder/fallback | Extractor | Borrowed Node tree | One traversal |
|
||||
| Output | First Mesh reference | Extractor | Loader preparation/cache adapter | Borrowed exact reference | Cache may retain later |
|
||||
|
||||
The only side effect is synchronous creation and immediate `free()` of a
|
||||
temporary PackedScene instance.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Input[Resource or Node subtree] --> Direct{Direct Mesh?}
|
||||
Direct -->|yes| Return[Return exact Mesh]
|
||||
Direct -->|no| Packed{PackedScene?}
|
||||
Packed -->|yes| Instantiate[Instantiate temporary root]
|
||||
Packed -->|no| Traverse[Traverse caller-owned subtree]
|
||||
Instantiate --> TraverseTemp[Depth-first preorder traversal]
|
||||
TraverseTemp --> Free[Free temporary root]
|
||||
Free --> ReturnOrNull[Return first Mesh or null]
|
||||
Traverse --> ReturnOrNull
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Stateless
|
||||
Stateless --> TemporaryInstance: extract PackedScene
|
||||
TemporaryInstance --> Traversing: instantiate succeeded
|
||||
Traversing --> Stateless: free temporary root before return
|
||||
Stateless --> Stateless: direct Mesh/subtree/invalid input
|
||||
```
|
||||
|
||||
No state or Resource reference is retained between calls.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Extractor as M2MeshResourceExtractor
|
||||
participant Scene as PackedScene
|
||||
Loader->>Extractor: extract_first_mesh(loaded Resource)
|
||||
alt direct Mesh
|
||||
Extractor-->>Loader: same Mesh reference
|
||||
else PackedScene
|
||||
Extractor->>Scene: instantiate()
|
||||
Scene-->>Extractor: temporary root
|
||||
Extractor->>Extractor: depth-first first-Mesh search
|
||||
Extractor->>Extractor: temporary_root.free()
|
||||
Extractor-->>Loader: Mesh or null
|
||||
end
|
||||
Loader->>Loader: prepare and cache Mesh or mark missing
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Extractor[M2MeshResourceExtractor]
|
||||
Extractor --> Types[Resource / PackedScene / Node / Mesh]
|
||||
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||
Loader --> Cache[M2MeshResourceCacheState]
|
||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Extractor -. no dependency .-> Pipeline
|
||||
Extractor -. no dependency .-> Cache
|
||||
Extractor -. no dependency .-> Classifier
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The renderer main thread performs all traversal and PackedScene instantiation.
|
||||
- Direct and subtree Mesh references are borrowed without duplication.
|
||||
- A PackedScene temporary root is extractor-owned and synchronously freed.
|
||||
- Existing prototype/rebuild subtrees remain caller-owned and are never mutated.
|
||||
- Cache retention remains a cache-state concern; runtime material preparation
|
||||
belongs to `M2RuntimeMeshFinalizer`.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Null/unsupported Resource | Type guard | Return null | Contract verifier | Loader marks missing/falls back |
|
||||
| PackedScene instantiate failure | Null root | Return null | Existing loader behavior | Rebuild cache/source |
|
||||
| No Mesh in subtree | Traversal exhaustion | Return null | Contract verifier | Loader marks missing/falls back |
|
||||
| Null MeshInstance Mesh | Null traversal result | Parent continues with later siblings | Order verifier | Normal sibling search |
|
||||
| Shutdown/cancel | No retained state | No action | Ownership docs | Caller owns surrounding job |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The module adds no settings or profile branches. Cache format order, material
|
||||
refresh version, render permits and batching remain unchanged outside it.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The extractor is stateless and serializes nothing. Existing `.tscn`/`.glb`
|
||||
formats, cache versions and rebuild metadata are unchanged; no migration or
|
||||
rebake is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The module emits no logs and retains no counters. Loader correlation remains the
|
||||
normalized M2 path. Its verifier measures traversal time and temporary Node count.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_mesh_resource_extractor.gd`: direct/invalid Resources, depth-first
|
||||
child order, null-Mesh sibling continuation, subtree ownership, PackedScene
|
||||
exact Mesh identity, temporary Node release, source boundaries and 10,000
|
||||
bounded traversals.
|
||||
- M2 pipeline/cache/classifier/material/shutdown/build regressions cover adjacent
|
||||
behavior. No asset-backed visual parity claim is made.
|
||||
|
||||
## Extension points
|
||||
|
||||
- `M2RuntimeMeshFinalizer` consumes the Mesh returned here and the loader stores
|
||||
its result in `M2MeshResourceCacheState`.
|
||||
- Broader generic scene traversal is intentionally excluded until another real
|
||||
consumer requires the same exact contract.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Direct/PackedScene first-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
|
||||
| Prototype/rebuild subtree selection | Implemented extraction | Synthetic preorder verifier | Asset-backed M2 traversal pending |
|
||||
| Material refresh/rebuild finalization | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- PackedScene instantiation remains synchronous main-thread work behind the
|
||||
existing finalize permit.
|
||||
- The first Mesh rule intentionally ignores additional MeshInstances.
|
||||
- No proprietary M2 corpus, corrupt cache, leak/descriptor-pressure, p95/p99 or
|
||||
paired original-client capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh traversal and temporary PackedScene lifetime |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | ResourceLoader/raw I/O, caching, missing state and materialization |
|
||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh/rebuild/fallback preparation |
|
||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared Mesh retention |
|
||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | Resource/order/lifetime/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,224 @@
|
||||
# M2 Raw Model Repository
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-RAW-MODEL-REPOSITORY-001` |
|
||||
| Owners | Native M2Loader availability, file existence and raw Dictionary loading |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-raw-model-repository`, 2026-07-17 |
|
||||
| Profiles/capabilities | Static and native-animated raw M2 reads |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide one stateless boundary between renderer orchestration and the optional
|
||||
native `M2Loader`. The repository resolves an extracted path and returns the raw
|
||||
static or animated Dictionary consumed by existing builders and classifiers.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Normalize relative paths or choose `.tscn`/`.glb` cache fallbacks.
|
||||
- Cache successful data, missing paths, Meshes, Nodes or animation decisions.
|
||||
- Build geometry/materials, own queues or attach renderer objects.
|
||||
- Change native parsing, cache formats, retry policy or visible behavior.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository]
|
||||
Repository --> File[Extracted M2 file]
|
||||
Repository --> Native[ClassDB M2Loader]
|
||||
Native --> Raw[Raw Dictionary]
|
||||
Raw --> Loader
|
||||
Loader --> Builder[Existing M2 builders/finalizer]
|
||||
```
|
||||
|
||||
Allowed dependencies are `ProjectSettings`, `FileAccess` and `ClassDB` for the
|
||||
native file boundary. Resource caches, builders, SceneTree types, worker pools,
|
||||
renderer policy and other application layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `load_static_model_data(extracted_directory, normalized_relative_path)` | Query | Invoke native `load_m2` for one existing extracted file | Synchronous; stateless | Returns `{}` for rejected/unavailable/invalid result |
|
||||
| `load_animated_model_data(extracted_directory, normalized_relative_path)` | Query | Invoke native `load_m2_animated` for one existing extracted file | Synchronous; stateless | Returns `{}` for rejected/unavailable/invalid result |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Extracted directory String | Loader configuration | Repository path resolution | Copied value | One call |
|
||||
| Input | Already-normalized relative M2 path String | Loader normalization | Repository | Copied value | One call |
|
||||
| Input | Extracted `.m2` bytes | Local legal extraction | Native M2Loader | File-owned | Native call |
|
||||
| Output | Static raw M2 Dictionary | Native `load_m2` | Loader/finalizer/M2Builder | Fresh native result | One caller operation |
|
||||
| Output | Animated raw M2 Dictionary | Native `load_m2_animated` | Loader/animated builder | Fresh native result | One caller operation |
|
||||
| Output | Empty Dictionary | Repository guards | Loader fallback/missing state | Fresh value | One failed call |
|
||||
|
||||
Side effects are limited to file-existence inspection, synchronous native file
|
||||
parsing and creation of one temporary native loader instance per accepted call.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Request[Directory plus normalized path] --> Empty{Path empty?}
|
||||
Empty -->|yes| Reject[Return empty Dictionary]
|
||||
Empty -->|no| Class{M2Loader registered?}
|
||||
Class -->|no| Reject
|
||||
Class -->|yes| Resolve[globalize directory/path join]
|
||||
Resolve --> Exists{File exists?}
|
||||
Exists -->|no| Reject
|
||||
Exists -->|yes| Instance[Instantiate M2Loader]
|
||||
Instance --> Method{Instance and method available?}
|
||||
Method -->|no| Reject
|
||||
Method -->|yes| Parse[Call static or animated method]
|
||||
Parse --> Type{Dictionary result?}
|
||||
Type -->|no| Reject
|
||||
Type -->|yes| Return[Return raw Dictionary]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Ready: construct repository
|
||||
Ready --> Resolving: load request
|
||||
Resolving --> Ready: rejected input/unavailable dependency
|
||||
Resolving --> Parsing: file and native method available
|
||||
Parsing --> Ready: Dictionary or empty result returned
|
||||
Ready --> [*]: caller releases reference
|
||||
```
|
||||
|
||||
No request, result or failure state survives a call.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Repo as M2RawModelRepository
|
||||
participant File as FileAccess
|
||||
participant Native as M2Loader
|
||||
Loader->>Repo: load static/animated(directory, path)
|
||||
Repo->>File: file_exists(globalized joined path)
|
||||
alt dependency or file unavailable
|
||||
Repo-->>Loader: empty Dictionary
|
||||
else available
|
||||
Repo->>Native: instantiate and call exact native method
|
||||
Native-->>Repo: Variant
|
||||
Repo-->>Loader: Dictionary or empty Dictionary
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Repository[M2RawModelRepository]
|
||||
Repository --> ProjectSettings
|
||||
Repository --> FileAccess
|
||||
Repository --> ClassDB
|
||||
ClassDB --> Native[M2Loader extension]
|
||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Loader --> StaticBuilder[M2Builder]
|
||||
Loader --> AnimatedBuilder[M2NativeAnimatedBuilder]
|
||||
Repository -. no dependency .-> Finalizer
|
||||
Repository -. no dependency .-> StaticBuilder
|
||||
Repository -. no dependency .-> Cache[Renderer caches/queues]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The repository owns only call-local path, native instance and result values.
|
||||
- The loader owns path normalization, cache/fallback selection and adoption.
|
||||
- Native `M2Loader` owns parsing behavior and returns a new Dictionary value.
|
||||
- Calls are synchronous on the caller's thread; current renderer callers use the
|
||||
main thread. The module starts no tasks and touches no SceneTree objects.
|
||||
- No file handle, native instance, Dictionary or engine Resource is retained.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty normalized path | String guard | Return `{}` | Dedicated verifier | Correct caller normalization |
|
||||
| Native extension absent | `ClassDB.class_exists` | Return `{}` | Existing renderer fallback | Install/load extension and retry |
|
||||
| Extracted file absent | `FileAccess.file_exists` | Return `{}` | Loader adopts historical missing state | Restore extraction and clear caller state |
|
||||
| Instance/method absent | Null/`has_method` guard | Return `{}` | Source contract verifier | Repair extension registration |
|
||||
| Parse/non-Dictionary result | Result type check | Return `{}` | Existing loader fallback | Repair asset/parser and retry |
|
||||
| Cancellation requested | Not supported inside synchronous native call | Call completes | None | Caller controls whether to start call |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Native class name | `M2Loader` | All | No | Exact GDExtension registration |
|
||||
| Static method | `load_m2` | All | No | Existing static parser path |
|
||||
| Animated method | `load_m2_animated` | Experimental animated path | No | Existing native animation parser |
|
||||
| Extracted directory | Loader `extracted_dir` | Scene configuration | Yes, caller-owned | Root used for path join |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The module adds no persistence or cache. Native methods, normalized relative
|
||||
paths and `.tscn/.glb`/material versions are unchanged, so no migration or rebake
|
||||
is required.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs: none; loader retains existing fallback and debug output.
|
||||
- Metrics: none; rejected-input timing is verifier-only.
|
||||
- Debug views: none.
|
||||
- Correlation ID: normalized relative M2 path in the caller.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_raw_model_repository.gd` covers empty/missing/repeated failures,
|
||||
exact static/animated method ownership, three loader delegations, forbidden
|
||||
dependencies and 10,000 empty-path rejects under one second.
|
||||
- Adjacent finalizer/extractor/cache/pipeline/animation/material/shutdown tests
|
||||
protect caller behavior and ownership.
|
||||
- Fidelity evidence is structural: exact path join/globalization and native
|
||||
method names are preserved. No proprietary M2 or original-client visual claim
|
||||
is made by this extraction.
|
||||
|
||||
## Extension points
|
||||
|
||||
Future asset-backed verification may add a legal fixture around these value
|
||||
contracts. Caching, asynchronous I/O and parser replacement require separate
|
||||
measured work packages rather than expansion of this repository.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Static raw M2 read | Implemented extraction | Source/invalid/missing verifier | Legal success fixture pending |
|
||||
| Native animated raw M2 read | Implemented extraction | Exact method/source contract | Legal animated fixture pending |
|
||||
| Stateless failure behavior | Verified synthetic | Repeated missing call and source boundary | Asset-backed corrupt input pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Native parsing remains synchronous and has no mid-call cancellation.
|
||||
- The repository deliberately does not distinguish missing dependency, missing
|
||||
file and parser failure; loader fallback behavior historically treats them alike.
|
||||
- Proprietary asset traversal, descriptor pressure, p95/p99 and visual comparison
|
||||
remain outside this structural extraction.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_raw_model_repository.gd` | Stateless native class/file/method boundary |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Path normalization, fallback/cache state and result consumers |
|
||||
| `src/native/src/m2_loader.cpp` | Native static/animated parsing implementation |
|
||||
| `src/tools/verify_m2_raw_model_repository.gd` | Rejected-input/source/dependency/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-runtime-mesh-finalizer.md`](m2-runtime-mesh-finalizer.md)
|
||||
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,224 @@
|
||||
# M2 Runtime Mesh Finalizer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-RUNTIME-MESH-FINALIZER-001` |
|
||||
| Owners | Stale Mesh refresh version, rebuild classification, M2Builder rebuild and fallback |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-mesh-finalizer`, 2026-07-17 |
|
||||
| Profiles/capabilities | Static cached and prototype M2 runtime material preparation |
|
||||
|
||||
## Purpose
|
||||
|
||||
Finalize an extracted static M2 Mesh for runtime use by accepting already-loaded
|
||||
raw M2 data, preserving material refresh version `2`, rebuilding only billboard/
|
||||
UV-rotation cases and returning the historical original-Mesh fallback.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Read raw `.m2` files or call ClassDB/FileAccess/ResourceLoader.
|
||||
- Own request/finalize queues, Mesh/missing/prototype caches or MultiMeshes.
|
||||
- Select cache paths or extract a Mesh from the originally loaded Resource.
|
||||
- Change classifier predicates, builder materials, refresh version or profiles.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Extractor[M2MeshResourceExtractor] --> Loader[StreamingWorldLoader]
|
||||
Loader -->|is raw data required?| Finalizer[M2RuntimeMeshFinalizer]
|
||||
Loader --> Raw[M2RawModelRepository]
|
||||
Raw --> Finalizer
|
||||
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||
Finalizer --> Builder[M2Builder]
|
||||
Builder --> Subtree[M2MeshResourceExtractor]
|
||||
Finalizer -->|prepared Mesh| Cache[M2MeshResourceCacheState]
|
||||
```
|
||||
|
||||
The finalizer may use Mesh metadata, raw value Dictionaries, M2Builder and the
|
||||
two existing scene-free M2 services. Raw-file I/O, cache state, render permits,
|
||||
Nodes outside temporary rebuild roots and other application layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `requires_raw_data_for_refresh(mesh)` | Query | Tell loader whether stale Mesh needs raw-file lookup | Renderer main thread | Null/current false |
|
||||
| `finalize_mesh(path, mesh, raw_data, extracted_directory)` | Command/query | Return current, marked, rebuilt or fallback Mesh | Renderer main thread; one finalize operation | Null Mesh returns null; invalid rebuild falls back |
|
||||
| `clear()` | Command | Clear memoized classifier decisions | Map reset/final shutdown | Idempotent |
|
||||
| `cached_rebuild_decision_count()` | Diagnostic query | Expose memoized path-decision count | Tests/diagnostics | None |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Extracted Mesh and normalized M2 path | Loader/extractor adapter | Finalizer | Borrowed Mesh reference/String copy | One call |
|
||||
| Input | Already-loaded raw M2 Dictionary | `M2RawModelRepository` through loader adapter | Classifier/M2Builder | Caller-owned value container | One call |
|
||||
| Input | Extracted directory path | Loader configuration | M2Builder texture resolution | Copied String | One rebuild |
|
||||
| Output | Original or rebuilt current Mesh | Finalizer | Cache/prototype adapter | Borrowed/new Resource reference | Cache may retain |
|
||||
|
||||
Side effects are Mesh refresh metadata, classifier memoization and temporary
|
||||
M2Builder prototype creation/destruction during required rebuilds.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Mesh[Extracted Mesh] --> Current{Refresh version >= 2?}
|
||||
Current -->|yes| ReturnOriginal[Return original unchanged]
|
||||
Current -->|no| NeedRaw[Repository supplies raw M2 data through loader]
|
||||
NeedRaw --> HasData{Raw data available?}
|
||||
HasData -->|no| Mark[Mark original version 2]
|
||||
HasData -->|yes| Classify{Billboard or UV rotation?}
|
||||
Classify -->|no| Mark
|
||||
Classify -->|yes| Build[M2Builder temporary prototype]
|
||||
Build --> Extract[M2MeshResourceExtractor first Mesh]
|
||||
Extract --> Rebuilt{Rebuilt Mesh exists?}
|
||||
Rebuilt -->|yes| ReturnRebuilt[Return rebuilt Mesh]
|
||||
Rebuilt -->|no| Mark
|
||||
Mark --> ReturnOriginal
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Current: Mesh metadata >= 2
|
||||
[*] --> Stale: Mesh metadata < 2
|
||||
Stale --> Current: empty/ordinary data marks original
|
||||
Stale --> Rebuilding: classifier requires rebuild
|
||||
Rebuilding --> Current: rebuilt Mesh returned
|
||||
Rebuilding --> Current: failure marks original fallback
|
||||
Current --> Current: later preparation is no-op
|
||||
```
|
||||
|
||||
Classifier decisions are memoized per normalized path until either existing
|
||||
map/reset clear site calls `clear()`.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Finalizer as M2RuntimeMeshFinalizer
|
||||
participant Raw as M2Loader boundary
|
||||
participant Classifier as RebuildClassifier
|
||||
participant Builder as M2Builder
|
||||
Loader->>Finalizer: requires_raw_data_for_refresh(mesh)
|
||||
alt current Mesh
|
||||
Finalizer-->>Loader: false; reuse Mesh
|
||||
else stale Mesh
|
||||
Finalizer-->>Loader: true
|
||||
Loader->>Raw: load static raw Dictionary
|
||||
Loader->>Finalizer: finalize_mesh(path, mesh, raw, extracted dir)
|
||||
Finalizer->>Classifier: needs_runtime_mesh_rebuild(path, raw)
|
||||
opt rebuild required
|
||||
Finalizer->>Builder: build(raw, extracted dir)
|
||||
Finalizer->>Finalizer: extract first Mesh; free prototype
|
||||
end
|
||||
Finalizer-->>Loader: rebuilt or marked fallback Mesh
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||
Finalizer --> Extractor[M2MeshResourceExtractor]
|
||||
Finalizer --> Builder[M2Builder]
|
||||
Loader --> Raw[M2RawModelRepository]
|
||||
Loader --> Cache[M2MeshResourceCacheState]
|
||||
Finalizer -. no dependency .-> Raw
|
||||
Finalizer -. no dependency .-> Cache
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Renderer main thread executes metadata changes and M2Builder work.
|
||||
- `M2RawModelRepository` owns raw M2 file/native calls; loader supplies its value result.
|
||||
- Finalizer owns classifier memoization and temporary rebuild prototype lifetime.
|
||||
- M2Builder owns construction rules; extractor selects the first rebuilt Mesh.
|
||||
- Cache/prototype adapters decide where the returned Mesh reference is retained.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Null Mesh | Guard | Return null | Contract verifier | Caller marks missing |
|
||||
| Current Mesh | Metadata query | Return exact reference; no raw load | Contract verifier | None needed |
|
||||
| Raw file/parse unavailable | Empty Dictionary from loader | Mark/reuse original | Existing renderer behavior | Source/cache repair |
|
||||
| Ordinary stale Mesh | Classifier false | Mark/reuse original | Classifier verifier | None needed |
|
||||
| Builder produces no Mesh | Extractor null | Mark/reuse original | Finalizer fallback fixture | Source/builder repair |
|
||||
| Reset/shutdown | Loader lifecycle | Clear classifier decisions | Source/clear verifier | Recompute next session |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
`MATERIAL_REFRESH_VERSION` remains `2` and is now owned by this finalizer.
|
||||
Finalize permits, cache paths, extracted directory configuration and quality
|
||||
profiles remain unchanged in the loader.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No new format is serialized. Existing Mesh metadata key
|
||||
`wow_m2_material_refresh_version`, M2Builder `MATERIAL_FORMAT_VERSION == 2` and
|
||||
`.tscn/.glb` caches are unchanged; no migration or rebake is introduced.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
`cached_rebuild_decision_count` exposes only a scalar count. Existing renderer
|
||||
logs/queue metrics are unchanged. Normalized M2 path remains the correlation key.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_runtime_mesh_finalizer.gd`: null/current/raw requirement, empty and
|
||||
ordinary marking, synthetic triangle billboard rebuild, rebuild failure
|
||||
fallback, refresh metadata, classifier clear, source boundary and 100-by-256
|
||||
bounded finalization.
|
||||
- Classifier/extractor/cache/pipeline/material/shutdown/M2 build regressions cover
|
||||
adjacent behavior. No asset-backed or original-client visual claim is made.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed fixture coverage can be added without changing the finalizer's
|
||||
value-data contract.
|
||||
- Refresh versions above `2` require explicit cache/fidelity evidence and docs.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Runtime Mesh refresh/fallback | Implemented extraction | Synthetic transition/rebuild verifier | Asset-backed corrupt/stale cache pending |
|
||||
| Rebuild predicate | Implemented extraction dependency | Classifier fixtures | Original-client material comparison pending |
|
||||
| Raw M2 refresh I/O | Implemented repository dependency | Repository source/invalid verifier | Legal success fixture pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Required rebuild remains synchronous main-thread work behind existing permits.
|
||||
- Synthetic geometry proves transition/build behavior, not full asset materials.
|
||||
- No proprietary M2 traversal, descriptor-pressure/leak, p95/p99 or paired
|
||||
original-client capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Refresh version, classification, rebuild and fallback |
|
||||
| `src/render/m2/m2_raw_model_repository.gd` | Raw-file/native I/O and Dictionary result |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Repository call and returned-Mesh adoption |
|
||||
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Memoized billboard/UV-rotation predicate |
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First rebuilt-Mesh selection |
|
||||
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | Transition/rebuild/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md)
|
||||
- [`m2-mesh-resource-extractor.md`](m2-mesh-resource-extractor.md)
|
||||
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||
- [`m2-raw-model-repository.md`](m2-raw-model-repository.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -0,0 +1,224 @@
|
||||
# M2 Runtime Mesh Rebuild Classifier
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001` |
|
||||
| Owners | Billboard/UV-rotation rebuild decision and per-path memoization |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`, 2026-07-17 |
|
||||
| Profiles/capabilities | Existing M2 material-refresh path; profile-independent |
|
||||
|
||||
## Purpose
|
||||
|
||||
Determine whether a stale cached M2 mesh must be rebuilt in memory from raw M2
|
||||
data to restore billboard custom attributes or UV-rotation material inputs. The
|
||||
first decision is memoized by normalized relative path for the current map/
|
||||
runtime-cache lifetime.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Read M2 files or invoke `M2Loader`/`M2Builder`.
|
||||
- Own cached Meshes, scenes, materials, Nodes or RIDs.
|
||||
- Select cache paths, perform ResourceLoader transitions or consume permits.
|
||||
- Change refresh/cache format versions or shader behavior.
|
||||
- Classify animation, particles, ribbons or general visual fidelity.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Cache[Stale cached M2 Mesh] --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Loader[StreamingWorldLoader] --> Raw[M2Loader raw Dictionary]
|
||||
Raw --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||
Classifier --> Decision[Memoized rebuild boolean]
|
||||
Decision --> Finalizer
|
||||
Finalizer -->|true| Builder[M2Builder rebuild]
|
||||
Finalizer -->|false| Refresh[Mark material refresh version]
|
||||
```
|
||||
|
||||
Allowed dependencies are Dictionary/Array, `PackedInt32Array`, `Vector4` and
|
||||
scalar math. Files, ResourceLoader, parser/builder classes, SceneTree,
|
||||
WorkerThreadPool, Mesh/Node/RID ownership and other layers are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `needs_runtime_mesh_rebuild(normalized_relative_path, raw_m2_data)` | Memoized query | Detect billboard or referenced UV rotation requirements | Renderer main thread; cached until clear | Invalid variants/indices are skipped; first path decision wins |
|
||||
| `clear()` | Command | Drop all memoized path decisions | Main thread; map reset/shutdown | Idempotent |
|
||||
| `cached_path_count()` | Query | Return memoized path count | Main thread | None |
|
||||
| `diagnostic_snapshot()` | Query | Return detached path/decision map | Main thread; caller-owned | None |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Normalized M2 relative path | Loader cache lookup | Classifier cache key | String value copied | Until clear |
|
||||
| Input | Raw M2 batches/transforms/combos Dictionary | `M2RawModelRepository` through loader adapter | Classifier | Borrowed; not mutated | First classification call per path |
|
||||
| Output | Rebuild-required boolean | Classifier | Runtime Mesh finalizer | Scalar | Memoized until clear |
|
||||
| Output | Detached decision Dictionary | Classifier | Verifier/future diagnostics | Caller-owned copy | Snapshot lifetime |
|
||||
|
||||
Side effects are limited to inserting and clearing boolean memoization entries.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Query[path + raw M2 data] --> Cached{Path cached?}
|
||||
Cached -->|yes| Return[Return first decision]
|
||||
Cached -->|no| Billboard{Any billboard flag or positive vertex count?}
|
||||
Billboard -->|yes| StoreTrue[Store true]
|
||||
Billboard -->|no| Inputs{Transforms and combos present?}
|
||||
Inputs -->|no| StoreFalse[Store false]
|
||||
Inputs -->|yes| Batch[For each Dictionary batch]
|
||||
Batch --> Stage[Inspect at most four referenced stages]
|
||||
Stage --> Rotation{Non-identity rotation or speed above epsilon?}
|
||||
Rotation -->|yes| StoreTrue
|
||||
Rotation -->|no| StoreFalse
|
||||
StoreTrue --> Return
|
||||
StoreFalse --> Return
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
Each path moves from unknown to one immutable cached boolean. Clear returns all
|
||||
paths to unknown; no resource lifecycle participates.
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Unknown
|
||||
Unknown --> RebuildRequired: billboard or referenced UV rotation
|
||||
Unknown --> RebuildNotRequired: no relevant metadata
|
||||
RebuildRequired --> RebuildRequired: repeated query
|
||||
RebuildNotRequired --> RebuildNotRequired: repeated query
|
||||
RebuildRequired --> Unknown: clear
|
||||
RebuildNotRequired --> Unknown: clear
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Native as M2Loader
|
||||
participant Finalizer as M2RuntimeMeshFinalizer
|
||||
participant Classifier as M2RuntimeMeshRebuildClassifier
|
||||
participant Builder as M2Builder
|
||||
Loader->>Native: load_m2(normalized path)
|
||||
Native-->>Loader: raw Dictionary
|
||||
Loader->>Finalizer: finalize_mesh(path, mesh, data, dir)
|
||||
Finalizer->>Classifier: needs_runtime_mesh_rebuild(path, data)
|
||||
Classifier-->>Finalizer: memoized boolean
|
||||
alt rebuild required
|
||||
Finalizer->>Builder: rebuild mesh from raw data
|
||||
else no rebuild required
|
||||
Finalizer->>Finalizer: stamp material refresh version
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[M2RuntimeMeshFinalizer]
|
||||
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
|
||||
Loader --> Native[M2Loader]
|
||||
Loader --> Builder[M2Builder]
|
||||
Loader --> Cache[M2 Mesh/Scene caches]
|
||||
Classifier -. no dependency .-> Native
|
||||
Classifier -. no dependency .-> Builder
|
||||
Classifier -. no dependency .-> Cache
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The classifier owns only normalized-path boolean entries.
|
||||
- The finalizer owns classifier lifetime, rebuild decisions and refresh metadata.
|
||||
- The loader owns raw loading, cache/resource selection and Mesh adoption.
|
||||
- Raw Dictionaries are borrowed and never retained; only the computed boolean is cached.
|
||||
- Current calls and clears are serialized on the renderer main thread.
|
||||
- No Node, Resource, Mesh, material or RID reference crosses into the classifier.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Non-Dictionary batch/transform | Type guard | Skip entry or use identity defaults | Synthetic verifier | Correct parser data on next cleared lifetime |
|
||||
| Missing transforms/combos | Empty guard | No UV-driven rebuild | Synthetic verifier | Billboard classification still applies |
|
||||
| Invalid combo/transform index | Bounds guard | Skip stage | Synthetic verifier | Correct raw data and clear |
|
||||
| Repeated path with changed data | Cache hit | Preserve first decision | Memoization fixture | Map/reset clear recomputes |
|
||||
| Load/build failure | Outside classifier | Existing mesh fallback remains | Loader diagnostics | Existing reload/fallback path |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||
|---|---|---|---|---|
|
||||
| Maximum texture stages inspected | `4` | Existing M2 material path | No | Preserves historical four-stage shader boundary |
|
||||
| UV rotation-speed epsilon | `0.000001` exclusive | Existing M2 material path | No | Larger absolute speed requires rebuild |
|
||||
| `MATERIAL_REFRESH_VERSION` | Finalizer constant `2` | All | Code version | Gates whether classifier is called |
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
Memoized booleans are runtime-only and are cleared at the two historical loader
|
||||
reset/shutdown sites. No persisted cache version or rebuild instruction changes.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Logs: none.
|
||||
- Metrics: `cached_path_count` is contract-level only and not added to runtime logs.
|
||||
- Debug views: detached path-to-boolean snapshot.
|
||||
- Correlation IDs: normalized M2 relative path.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_runtime_mesh_rebuild_classifier.gd`: billboard flag/count, invalid
|
||||
variants, identity/non-identity rotation, positive/negative speed, exact
|
||||
epsilon, four-stage cap, invalid indices, memoization, clear, source boundary
|
||||
and 100-by-256 timing.
|
||||
- Existing M2 builder/material verifiers cover downstream shader/cache behavior.
|
||||
- Fidelity evidence is exact predicate/cache extraction. No new asset-backed or
|
||||
original-client parity claim is made.
|
||||
- Performance budget: 25,600 classifications and clears under one second.
|
||||
|
||||
## Extension points
|
||||
|
||||
- A typed raw M2 material descriptor may replace the Dictionary only with parser
|
||||
fixtures and a compatibility adapter.
|
||||
- Mesh request/cache/finalization state can be extracted separately without
|
||||
changing this rebuild predicate.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Billboard rebuild classification | Implemented extraction | Flag/count fixtures | Asset-backed cache refresh timing pending |
|
||||
| UV-rotation rebuild classification | Implemented extraction | Combo/stage/threshold fixtures | More build-12340 material fixtures pending |
|
||||
| Runtime memoization lifetime | Implemented extraction | First-decision/clear/source fixtures | Byte/count runtime metric not exposed |
|
||||
| Runtime Mesh finalization | Implemented extraction | Finalizer transition/rebuild fixtures | Asset-backed material comparison pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Raw parser data remains Dictionary-based.
|
||||
- The first decision wins even if different raw data is passed for the same path;
|
||||
this intentionally preserves prior cache behavior.
|
||||
- Only four texture stages are inspected, matching current material support.
|
||||
- No private asset traversal, descriptor-pressure, p95/p99 or paired visual run is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Predicate, thresholds and boolean memoization |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Raw loading, Mesh rebuild/adoption, refresh version and clear calls |
|
||||
| `addons/mpq_extractor/loaders/m2_builder.gd` | Mesh/material rebuild implementation |
|
||||
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | Synthetic predicate/cache/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
|
||||
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| 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 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-adt-water-load-pipeline`, 2026-07-17 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-raw-model-repository`, 2026-07-17 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -132,6 +132,12 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
|
||||
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
|
||||
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
||||
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
||||
| `M2MeshLoadPipelineState` | Internal M2 async-state service | Owns static Mesh pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||
| `M2MeshResourceCacheState` | Internal M2 Resource cache | Owns prepared static Mesh references by normalized path | Renderer main thread; final shutdown | Empty path/null Mesh rejected; same path replaces |
|
||||
| `M2MeshResourceExtractor` | Internal M2 scene/resource service | Selects first Mesh from direct Resource, PackedScene or Node subtree | Renderer main thread; stateless except temporary instance | Invalid/no Mesh returns null; temporary PackedScene root freed |
|
||||
| `M2RuntimeMeshFinalizer` | Internal M2 preparation service | Owns refresh version, rebuild classification, M2Builder rebuild and fallback | Renderer main thread; decisions cached until reset | Null returns null; missing/failed rebuild marks and reuses original Mesh |
|
||||
| `M2RawModelRepository` | Internal M2 native repository | Reads static/animated raw Dictionaries through exact M2Loader methods | Synchronous; stateless | Invalid/unavailable/non-Dictionary result returns empty Dictionary |
|
||||
| `WmoPlacementResolver.normalize_relative_path/resolve_unique_key/resolve_world_transform` | Internal pure WMO service | Resolves cache key, registry identity and world transform | Main/any thread; stateless | Missing UID uses tile/index fallback; transform fields use historical defaults |
|
||||
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
||||
@@ -139,6 +145,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
|
||||
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
|
||||
| `AdtWaterLoadPipelineState` | Internal liquid async-state service | Owns ADT water FIFO/dedupe, active task IDs and mutex result mailbox | Main-thread state; worker result publication | Invalid/duplicate requests rejected; clear does not interrupt workers |
|
||||
| `AdtWaterSceneFinalizer.attach_water_scene` | Internal liquid main-thread service | Builds and attaches one existing-format ADT Water subtree | Main thread; stateless, returned Node tile-owned | Empty/invalid/dry input returns null without attachment |
|
||||
|
||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||
@@ -174,6 +181,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||
| Internal raw M2 read | Extracted directory and normalized relative path | Loader / `M2RawModelRepository` | Finalizer, static or animated builder adapters | Fresh Dictionary; repository retains nothing | One synchronous native call |
|
||||
| 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 |
|
||||
@@ -361,7 +369,27 @@ sequenceDiagram
|
||||
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
|
||||
active task IDs and the worker-safe parsed-result mailbox. The loader retains
|
||||
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits,
|
||||
stale tile/path/root checks and ADTBuilder/Node finalization.
|
||||
stale tile/path/root checks and `water_loaded` timing. `AdtWaterSceneFinalizer`
|
||||
performs the existing ADTBuilder call, tile attach and optional recursive
|
||||
Editor ownership on the main thread without retaining the generated subtree.
|
||||
- `M2RuntimeMeshRebuildClassifier` owns only normalized-path boolean decisions
|
||||
for billboard/UV-rotation material refresh and is composed by the runtime Mesh
|
||||
finalizer. The raw repository loads value data; loader retains Mesh adoption.
|
||||
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
|
||||
terminal statuses and completion-order finalize FIFO. The loader retains cache
|
||||
path selection, ResourceLoader calls, permits, shared missing state and adoption.
|
||||
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases
|
||||
them at the existing final-shutdown site. The loader retains missing/prototype
|
||||
state and materialization.
|
||||
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
|
||||
PackedScene instance destruction. The loader retains ResourceLoader I/O,
|
||||
cache/missing adoption and materialization.
|
||||
- `M2RuntimeMeshFinalizer` owns refresh version `2`, classifier lifetime,
|
||||
M2Builder rebuild and original-Mesh fallback. The loader loads raw data only
|
||||
after the finalizer reports that a cached Mesh is stale.
|
||||
- `M2RawModelRepository` owns FileAccess/ClassDB availability and the exact
|
||||
`load_m2`/`load_m2_animated` calls. The loader retains normalization,
|
||||
fallback order, missing/prototype state and every result consumer.
|
||||
- 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
|
||||
@@ -561,6 +589,13 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `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/liquid/adt_water_load_pipeline_state.gd` | ADT water request/task/result bookkeeping and worker-safe mailbox |
|
||||
| `src/render/liquid/adt_water_scene_finalizer.gd` | Main-thread ADT water build, tile attachment and optional editor ownership |
|
||||
| `src/render/m2/m2_runtime_mesh_rebuild_classifier.gd` | Billboard/UV-rotation stale cached-mesh rebuild decision and memoization |
|
||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||
| `src/render/m2/m2_runtime_mesh_finalizer.gd` | Runtime refresh version, rebuild classification/build and fallback |
|
||||
| `src/render/m2/m2_raw_model_repository.gd` | Stateless static/animated native M2 file boundary |
|
||||
| `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 |
|
||||
@@ -574,6 +609,13 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `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_adt_water_load_pipeline_state.gd` | ADT water FIFO/task/thread/boundary/timing regression |
|
||||
| `src/tools/verify_adt_water_scene_finalizer.gd` | Synthetic ADT water scene/ownership/boundary/timing regression |
|
||||
| `src/tools/verify_m2_runtime_mesh_rebuild_classifier.gd` | M2 rebuild predicate/cache/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||
| `src/tools/verify_m2_runtime_mesh_finalizer.gd` | M2 current/stale/rebuild/fallback/boundary/timing regression |
|
||||
| `src/tools/verify_m2_raw_model_repository.gd` | M2 invalid/missing/native-boundary/dependency/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_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||
|
||||
@@ -0,0 +1,40 @@
|
||||
class_name AdtWaterSceneFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Builds and attaches one ADT water subtree on the renderer main thread. The
|
||||
## supplied tile root owns the returned subtree; this service retains no state.
|
||||
|
||||
const ADT_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||
|
||||
|
||||
## Builds water from parsed ADT data and attaches it once to [param tile_root].
|
||||
## [param tile_origin_godot_units] is the existing Godot-world tile origin used
|
||||
## by [ADTBuilder]. When [param editor_owner] is non-null and is an ancestor,
|
||||
## ownership is assigned recursively for persisted Editor previews. Returns the
|
||||
## attached borrowed root, or null for empty/invalid input or dry ADT data.
|
||||
## This method mutates the SceneTree and must run on the main thread.
|
||||
func attach_water_scene(
|
||||
water_data: Dictionary,
|
||||
tile_origin_godot_units: Vector3,
|
||||
tile_root: Node,
|
||||
editor_owner: Node = null) -> Node3D:
|
||||
if water_data.is_empty() or tile_root == null or not is_instance_valid(tile_root):
|
||||
return null
|
||||
|
||||
var water_root: Node3D = ADT_BUILDER_SCRIPT.build_tile_water_scene(
|
||||
water_data,
|
||||
tile_origin_godot_units
|
||||
)
|
||||
if water_root == null:
|
||||
return null
|
||||
|
||||
tile_root.add_child(water_root)
|
||||
if editor_owner != null and is_instance_valid(editor_owner):
|
||||
_assign_owner_recursive(water_root, editor_owner)
|
||||
return water_root
|
||||
|
||||
|
||||
func _assign_owner_recursive(generated_node: Node, editor_owner: Node) -> void:
|
||||
generated_node.owner = editor_owner
|
||||
for child in generated_node.get_children():
|
||||
_assign_owner_recursive(child, editor_owner)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ck4sdc8a3kg00
|
||||
@@ -0,0 +1,103 @@
|
||||
class_name M2MeshLoadPipelineState
|
||||
extends RefCounted
|
||||
|
||||
## Owns static M2 threaded-load request records and the terminal finalize FIFO.
|
||||
## The caller owns ResourceLoader I/O, permits, Mesh caches and materialization.
|
||||
|
||||
var _request_by_normalized_path: Dictionary = {}
|
||||
var _finalize_records: Array[Dictionary] = []
|
||||
|
||||
|
||||
## Records one normalized M2 path and ResourceLoader path. Empty or duplicate
|
||||
## values are rejected without mutation.
|
||||
func remember_request(normalized_relative_path: String, resource_path: String) -> bool:
|
||||
if normalized_relative_path.is_empty() or resource_path.is_empty():
|
||||
return false
|
||||
if _request_by_normalized_path.has(normalized_relative_path):
|
||||
return false
|
||||
_request_by_normalized_path[normalized_relative_path] = {
|
||||
"normalized": normalized_relative_path,
|
||||
"path": resource_path,
|
||||
}
|
||||
return true
|
||||
|
||||
|
||||
## Returns whether one normalized path currently has a pending threaded request.
|
||||
func has_request(normalized_relative_path: String) -> bool:
|
||||
return (
|
||||
not normalized_relative_path.is_empty()
|
||||
and _request_by_normalized_path.has(normalized_relative_path)
|
||||
)
|
||||
|
||||
|
||||
## Returns detached pending records in their existing Dictionary insertion order.
|
||||
func request_records_snapshot() -> Array[Dictionary]:
|
||||
var records: Array[Dictionary] = []
|
||||
for request_variant in _request_by_normalized_path.values():
|
||||
var request: Dictionary = request_variant
|
||||
records.append(request.duplicate())
|
||||
return records
|
||||
|
||||
|
||||
## Removes one pending request and appends its terminal status to the finalize
|
||||
## FIFO. Unknown paths are rejected without mutation.
|
||||
func complete_request(normalized_relative_path: String, terminal_status: int) -> bool:
|
||||
if not _request_by_normalized_path.has(normalized_relative_path):
|
||||
return false
|
||||
var request: Dictionary = _request_by_normalized_path[normalized_relative_path]
|
||||
_request_by_normalized_path.erase(normalized_relative_path)
|
||||
var finalize_record := request.duplicate()
|
||||
finalize_record["status"] = terminal_status
|
||||
_finalize_records.append(finalize_record)
|
||||
return true
|
||||
|
||||
|
||||
## Removes one pending request without enqueuing finalization. Returns whether a
|
||||
## record was removed.
|
||||
func discard_request(normalized_relative_path: String) -> bool:
|
||||
return _request_by_normalized_path.erase(normalized_relative_path)
|
||||
|
||||
|
||||
## Returns whether a terminal record is waiting for budgeted finalization.
|
||||
func has_finalize_record() -> bool:
|
||||
return not _finalize_records.is_empty()
|
||||
|
||||
|
||||
## Removes and returns the oldest terminal record. Empty means none is ready.
|
||||
func pop_finalize_record() -> Dictionary:
|
||||
if _finalize_records.is_empty():
|
||||
return {}
|
||||
return _finalize_records.pop_front()
|
||||
|
||||
|
||||
## Returns pending plus terminal-finalize work for existing renderer metrics.
|
||||
func total_work_count() -> int:
|
||||
return _request_by_normalized_path.size() + _finalize_records.size()
|
||||
|
||||
|
||||
## Returns the pending request count.
|
||||
func pending_request_count() -> int:
|
||||
return _request_by_normalized_path.size()
|
||||
|
||||
|
||||
## Returns the terminal finalize FIFO count.
|
||||
func finalize_record_count() -> int:
|
||||
return _finalize_records.size()
|
||||
|
||||
|
||||
## Clears request/finalize bookkeeping. The caller must drain ResourceLoader
|
||||
## requests before orderly shutdown clear.
|
||||
func clear() -> void:
|
||||
_request_by_normalized_path.clear()
|
||||
_finalize_records.clear()
|
||||
|
||||
|
||||
## Returns detached request and finalize records without loaded Resources/Meshes.
|
||||
func diagnostic_snapshot() -> Dictionary:
|
||||
var finalize_records: Array[Dictionary] = []
|
||||
for record in _finalize_records:
|
||||
finalize_records.append(record.duplicate())
|
||||
return {
|
||||
"requests": request_records_snapshot(),
|
||||
"finalize_records": finalize_records,
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
uid://dadsxuswq0tnd
|
||||
@@ -0,0 +1,50 @@
|
||||
class_name M2MeshResourceCacheState
|
||||
extends RefCounted
|
||||
|
||||
## Owns normalized-path references to prepared static M2 Mesh resources.
|
||||
## The caller owns loading, preparation, missing-state and materialization.
|
||||
|
||||
var _mesh_by_normalized_path: Dictionary = {}
|
||||
|
||||
|
||||
## Stores one prepared Mesh for a non-empty normalized M2 path. A later store
|
||||
## for the same path replaces the retained reference.
|
||||
func store_mesh(normalized_relative_path: String, mesh: Mesh) -> bool:
|
||||
if normalized_relative_path.is_empty() or mesh == null:
|
||||
return false
|
||||
_mesh_by_normalized_path[normalized_relative_path] = mesh
|
||||
return true
|
||||
|
||||
|
||||
## Returns whether one normalized path currently has a retained Mesh.
|
||||
func has_mesh(normalized_relative_path: String) -> bool:
|
||||
return (
|
||||
not normalized_relative_path.is_empty()
|
||||
and _mesh_by_normalized_path.has(normalized_relative_path)
|
||||
)
|
||||
|
||||
|
||||
## Returns the exact retained Mesh reference, or null for empty/unknown paths.
|
||||
func find_mesh(normalized_relative_path: String) -> Mesh:
|
||||
if not has_mesh(normalized_relative_path):
|
||||
return null
|
||||
return _mesh_by_normalized_path[normalized_relative_path] as Mesh
|
||||
|
||||
|
||||
## Returns the number of retained static M2 Mesh resources.
|
||||
func mesh_count() -> int:
|
||||
return _mesh_by_normalized_path.size()
|
||||
|
||||
|
||||
## Returns a detached insertion-order list of cached normalized paths.
|
||||
func normalized_paths_snapshot() -> Array[String]:
|
||||
var normalized_paths: Array[String] = []
|
||||
for normalized_path_variant in _mesh_by_normalized_path.keys():
|
||||
normalized_paths.append(String(normalized_path_variant))
|
||||
return normalized_paths
|
||||
|
||||
|
||||
## Releases all retained Mesh references. The loader calls this only after
|
||||
## asynchronous work has completed during final scene shutdown.
|
||||
func clear() -> void:
|
||||
_mesh_by_normalized_path.clear()
|
||||
@@ -0,0 +1 @@
|
||||
uid://dhkyjcrdblgkd
|
||||
@@ -0,0 +1,34 @@
|
||||
class_name M2MeshResourceExtractor
|
||||
extends RefCounted
|
||||
|
||||
## Selects the first Mesh from loaded M2 Resources or existing Node subtrees.
|
||||
## The caller owns ResourceLoader I/O, cache adoption and material preparation.
|
||||
|
||||
|
||||
## Returns the direct Mesh Resource or the first Mesh in a temporary PackedScene
|
||||
## instance. The temporary instance is freed before this method returns.
|
||||
func extract_first_mesh(resource: Resource) -> Mesh:
|
||||
if resource is Mesh:
|
||||
return resource as Mesh
|
||||
if not (resource is PackedScene):
|
||||
return null
|
||||
var temporary_root := (resource as PackedScene).instantiate()
|
||||
if temporary_root == null:
|
||||
return null
|
||||
var mesh := find_first_mesh_in_subtree(temporary_root)
|
||||
temporary_root.free()
|
||||
return mesh
|
||||
|
||||
|
||||
## Returns the first MeshInstance3D Mesh in depth-first preorder. The subtree
|
||||
## and Mesh ownership remain unchanged.
|
||||
func find_first_mesh_in_subtree(root: Node) -> Mesh:
|
||||
if root == null:
|
||||
return null
|
||||
if root is MeshInstance3D:
|
||||
return (root as MeshInstance3D).mesh
|
||||
for child in root.get_children():
|
||||
var mesh := find_first_mesh_in_subtree(child)
|
||||
if mesh != null:
|
||||
return mesh
|
||||
return null
|
||||
@@ -0,0 +1 @@
|
||||
uid://dea4gekrxshjf
|
||||
@@ -0,0 +1,51 @@
|
||||
class_name M2RawModelRepository
|
||||
extends RefCounted
|
||||
|
||||
## Reads raw static and animated M2 Dictionaries through the optional native
|
||||
## M2Loader boundary. The repository is stateless and does not cache failures.
|
||||
|
||||
const NATIVE_LOADER_CLASS_NAME := "M2Loader"
|
||||
|
||||
|
||||
## Loads one normalized static M2 path with the native `load_m2` method.
|
||||
## Invalid input, unavailable native support and parse failures return `{}`.
|
||||
func load_static_model_data(
|
||||
extracted_directory: String,
|
||||
normalized_relative_path: String) -> Dictionary:
|
||||
return _load_model_data(
|
||||
extracted_directory,
|
||||
normalized_relative_path,
|
||||
"load_m2"
|
||||
)
|
||||
|
||||
|
||||
## Loads one normalized animated M2 path with the native `load_m2_animated`
|
||||
## method. Invalid input, unavailable support and parse failures return `{}`.
|
||||
func load_animated_model_data(
|
||||
extracted_directory: String,
|
||||
normalized_relative_path: String) -> Dictionary:
|
||||
return _load_model_data(
|
||||
extracted_directory,
|
||||
normalized_relative_path,
|
||||
"load_m2_animated"
|
||||
)
|
||||
|
||||
|
||||
func _load_model_data(
|
||||
extracted_directory: String,
|
||||
normalized_relative_path: String,
|
||||
native_method_name: StringName) -> Dictionary:
|
||||
if normalized_relative_path.is_empty():
|
||||
return {}
|
||||
if not ClassDB.class_exists(NATIVE_LOADER_CLASS_NAME):
|
||||
return {}
|
||||
var absolute_path := ProjectSettings.globalize_path(
|
||||
extracted_directory.path_join(normalized_relative_path)
|
||||
)
|
||||
if not FileAccess.file_exists(absolute_path):
|
||||
return {}
|
||||
var native_loader: Object = ClassDB.instantiate(NATIVE_LOADER_CLASS_NAME)
|
||||
if native_loader == null or not native_loader.has_method(native_method_name):
|
||||
return {}
|
||||
var raw_model_data: Variant = native_loader.call(native_method_name, absolute_path)
|
||||
return raw_model_data if raw_model_data is Dictionary else {}
|
||||
@@ -0,0 +1 @@
|
||||
uid://bwqtuln7vpcj4
|
||||
@@ -0,0 +1,78 @@
|
||||
class_name M2RuntimeMeshFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Finalizes stale cached M2 Meshes using caller-supplied raw data. The caller
|
||||
## owns raw-file I/O, cache adoption, missing state and materialization.
|
||||
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_extractor.gd"
|
||||
)
|
||||
|
||||
const MATERIAL_REFRESH_VERSION := 2
|
||||
const MATERIAL_REFRESH_VERSION_META := "wow_m2_material_refresh_version"
|
||||
|
||||
var _runtime_mesh_rebuild_classifier := M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
|
||||
var _mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
|
||||
|
||||
|
||||
## Returns whether one Mesh is stale and the caller should load raw M2 data.
|
||||
func requires_raw_data_for_refresh(mesh: Mesh) -> bool:
|
||||
return (
|
||||
mesh != null
|
||||
and int(mesh.get_meta(MATERIAL_REFRESH_VERSION_META, 0)) < MATERIAL_REFRESH_VERSION
|
||||
)
|
||||
|
||||
|
||||
## Marks, rebuilds or falls back one stale Mesh using already-loaded raw M2
|
||||
## data. Current Meshes return unchanged and never inspect raw data.
|
||||
func finalize_mesh(
|
||||
normalized_relative_path: String,
|
||||
mesh: Mesh,
|
||||
raw_m2_data: Dictionary,
|
||||
extracted_directory: String) -> Mesh:
|
||||
if mesh == null:
|
||||
return null
|
||||
if not requires_raw_data_for_refresh(mesh):
|
||||
return mesh
|
||||
if raw_m2_data.is_empty() or not _runtime_mesh_rebuild_classifier.needs_runtime_mesh_rebuild(
|
||||
normalized_relative_path,
|
||||
raw_m2_data):
|
||||
_mark_mesh_current(mesh)
|
||||
return mesh
|
||||
var rebuilt_mesh := _rebuild_mesh(raw_m2_data, extracted_directory)
|
||||
if rebuilt_mesh != null:
|
||||
return rebuilt_mesh
|
||||
_mark_mesh_current(mesh)
|
||||
return mesh
|
||||
|
||||
|
||||
## Clears memoized per-path rebuild decisions at the existing map/shutdown sites.
|
||||
func clear() -> void:
|
||||
_runtime_mesh_rebuild_classifier.clear()
|
||||
|
||||
|
||||
## Returns the memoized rebuild-decision count for diagnostics and verification.
|
||||
func cached_rebuild_decision_count() -> int:
|
||||
return _runtime_mesh_rebuild_classifier.cached_path_count()
|
||||
|
||||
|
||||
func _mark_mesh_current(mesh: Mesh) -> void:
|
||||
mesh.set_meta(MATERIAL_REFRESH_VERSION_META, MATERIAL_REFRESH_VERSION)
|
||||
|
||||
|
||||
func _rebuild_mesh(raw_m2_data: Dictionary, extracted_directory: String) -> Mesh:
|
||||
var temporary_prototype: Node3D = M2_BUILDER_SCRIPT.build(
|
||||
raw_m2_data,
|
||||
extracted_directory
|
||||
)
|
||||
if temporary_prototype == null:
|
||||
return null
|
||||
var rebuilt_mesh := _mesh_resource_extractor.find_first_mesh_in_subtree(
|
||||
temporary_prototype
|
||||
)
|
||||
temporary_prototype.free()
|
||||
return rebuilt_mesh
|
||||
@@ -0,0 +1 @@
|
||||
uid://bn6vvtmqo0don
|
||||
@@ -0,0 +1,93 @@
|
||||
class_name M2RuntimeMeshRebuildClassifier
|
||||
extends RefCounted
|
||||
|
||||
## Memoizes whether raw M2 billboard or UV-rotation metadata requires replacing
|
||||
## a stale cached runtime mesh. Owns no parser, Mesh, material, Node or RID.
|
||||
|
||||
const UV_ROTATION_SPEED_EPSILON := 0.000001
|
||||
const MAXIMUM_TEXTURE_STAGES := 4
|
||||
|
||||
var _rebuild_required_by_normalized_path: Dictionary = {}
|
||||
|
||||
|
||||
## Returns the first memoized rebuild decision for [param normalized_relative_path].
|
||||
## [param raw_m2_data] is borrowed and not mutated. The decision remains cached
|
||||
## until [method clear], matching the loader map/shutdown reset lifetime.
|
||||
func needs_runtime_mesh_rebuild(
|
||||
normalized_relative_path: String,
|
||||
raw_m2_data: Dictionary) -> bool:
|
||||
if _rebuild_required_by_normalized_path.has(normalized_relative_path):
|
||||
return bool(_rebuild_required_by_normalized_path[normalized_relative_path])
|
||||
var rebuild_required := (
|
||||
_raw_data_has_billboards(raw_m2_data)
|
||||
or _raw_data_has_uv_rotation(raw_m2_data)
|
||||
)
|
||||
_rebuild_required_by_normalized_path[normalized_relative_path] = rebuild_required
|
||||
return rebuild_required
|
||||
|
||||
|
||||
## Clears every memoized path decision. No Mesh or resource is retained or freed.
|
||||
func clear() -> void:
|
||||
_rebuild_required_by_normalized_path.clear()
|
||||
|
||||
|
||||
## Returns the number of memoized normalized paths.
|
||||
func cached_path_count() -> int:
|
||||
return _rebuild_required_by_normalized_path.size()
|
||||
|
||||
|
||||
## Returns a detached normalized-path to boolean decision snapshot.
|
||||
func diagnostic_snapshot() -> Dictionary:
|
||||
return _rebuild_required_by_normalized_path.duplicate()
|
||||
|
||||
|
||||
func _raw_data_has_billboards(raw_m2_data: Dictionary) -> bool:
|
||||
for batch_variant in raw_m2_data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
if (
|
||||
bool(batch.get("has_billboard", false))
|
||||
or int(batch.get("billboard_vertex_count", 0)) > 0
|
||||
):
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _raw_data_has_uv_rotation(raw_m2_data: Dictionary) -> bool:
|
||||
var texture_transforms: Array = raw_m2_data.get("texture_transforms", [])
|
||||
if texture_transforms.is_empty():
|
||||
return false
|
||||
var texture_transform_combos: PackedInt32Array = raw_m2_data.get(
|
||||
"texture_transform_combos",
|
||||
PackedInt32Array()
|
||||
)
|
||||
if texture_transform_combos.is_empty():
|
||||
return false
|
||||
for batch_variant in raw_m2_data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
|
||||
var base_combo_index := int(batch.get("texture_transform_combo_index", -1))
|
||||
for texture_stage in mini(texture_count, MAXIMUM_TEXTURE_STAGES):
|
||||
var combo_index := base_combo_index + texture_stage
|
||||
if combo_index < 0 or combo_index >= texture_transform_combos.size():
|
||||
continue
|
||||
var transform_index := int(texture_transform_combos[combo_index])
|
||||
if transform_index < 0 or transform_index >= texture_transforms.size():
|
||||
continue
|
||||
var texture_transform: Dictionary = (
|
||||
texture_transforms[transform_index]
|
||||
if texture_transforms[transform_index] is Dictionary
|
||||
else {}
|
||||
)
|
||||
var rotation: Vector4 = texture_transform.get(
|
||||
"rotation",
|
||||
Vector4(1.0, 0.0, 0.0, 1.0)
|
||||
)
|
||||
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
|
||||
return true
|
||||
if absf(float(texture_transform.get("rotation_speed", 0.0))) > UV_ROTATION_SPEED_EPSILON:
|
||||
return true
|
||||
return false
|
||||
@@ -0,0 +1 @@
|
||||
uid://breu8vpmm88en
|
||||
@@ -45,6 +45,9 @@ const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
const ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_load_pipeline_state.gd"
|
||||
)
|
||||
const ADT_WATER_SCENE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/liquid/adt_water_scene_finalizer.gd"
|
||||
)
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
@@ -57,13 +60,27 @@ const M2_PLACEMENT_GROUPER_SCRIPT := preload(
|
||||
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_build_batch_planner.gd"
|
||||
)
|
||||
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
const M2_RAW_MODEL_REPOSITORY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_raw_model_repository.gd"
|
||||
)
|
||||
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
)
|
||||
const M2_MESH_RESOURCE_EXTRACTOR_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_mesh_resource_extractor.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")
|
||||
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||
const M2_MATERIAL_REFRESH_VERSION := 2
|
||||
const WMO_MATERIAL_REFRESH_VERSION := 10
|
||||
const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||
|
||||
@@ -244,10 +261,11 @@ var _m2_unique_placement_registry := (
|
||||
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
||||
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
|
||||
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
|
||||
var _m2_mesh_cache: Dictionary = {}
|
||||
var _m2_mesh_load_requests: Dictionary = {}
|
||||
var _m2_mesh_finalize_queue: Array = []
|
||||
var _m2_runtime_rebuild_required_cache: Dictionary = {}
|
||||
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
|
||||
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
|
||||
var _m2_mesh_resource_cache_state := M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||
var _m2_mesh_resource_extractor := M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()
|
||||
var _m2_mesh_load_pipeline_state := M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||
var _m2_animation_load_requests: Dictionary = {}
|
||||
var _m2_animation_finalize_queue: Array = []
|
||||
var _wmo_build_jobs: Dictionary = {}
|
||||
@@ -260,6 +278,7 @@ var _terrain_splat_tasks: Dictionary = {}
|
||||
var _terrain_splat_result_mutex := Mutex.new()
|
||||
var _terrain_splat_result_queue: Array = []
|
||||
var _adt_water_load_pipeline_state := ADT_WATER_LOAD_PIPELINE_STATE_SCRIPT.new()
|
||||
var _adt_water_scene_finalizer := ADT_WATER_SCENE_FINALIZER_SCRIPT.new()
|
||||
var _tile_result_mutex := Mutex.new()
|
||||
var _tile_result_queue: Array = []
|
||||
var _shared_tex_cache: Dictionary = {}
|
||||
@@ -465,7 +484,7 @@ func _release_runtime_caches_for_shutdown() -> void:
|
||||
_free_detached_node_cache(_m2_scene_cache)
|
||||
_free_detached_node_cache(_m2_animated_scene_cache)
|
||||
_free_detached_node_cache(_wmo_prototype_cache)
|
||||
_m2_mesh_cache.clear()
|
||||
_m2_mesh_resource_cache_state.clear()
|
||||
_m2_static_animation_cache.clear()
|
||||
_m2_missing_cache.clear()
|
||||
_wmo_render_resource_cache_state.clear_all()
|
||||
@@ -1002,7 +1021,7 @@ func _log_hitch_profile(start_usec: int, timings: Array[String], did_refresh: bo
|
||||
_detail_asset_queue.size(),
|
||||
_m2_group_tasks.size(),
|
||||
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
|
||||
_m2_mesh_load_pipeline_state.total_work_count(),
|
||||
_m2_build_queue.size(),
|
||||
(
|
||||
_wmo_build_queue.size()
|
||||
@@ -1033,7 +1052,7 @@ func render_baseline_snapshot() -> Dictionary:
|
||||
"detail": _detail_asset_queue.size(),
|
||||
"m2_task": _m2_group_tasks.size(),
|
||||
"m2_animation": _m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
"m2_mesh": _m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
|
||||
"m2_mesh": _m2_mesh_load_pipeline_state.total_work_count(),
|
||||
"m2_build": _m2_build_queue.size(),
|
||||
"wmo_build": (
|
||||
_wmo_build_queue.size()
|
||||
@@ -1319,7 +1338,7 @@ func _tick_runtime_stats(delta: float) -> void:
|
||||
_detail_asset_queue.size(),
|
||||
_m2_group_tasks.size(),
|
||||
_m2_animation_finalize_queue.size() + _m2_animation_load_requests.size(),
|
||||
_m2_mesh_finalize_queue.size() + _m2_mesh_load_requests.size(),
|
||||
_m2_mesh_load_pipeline_state.total_work_count(),
|
||||
_m2_build_queue.size(),
|
||||
(
|
||||
_wmo_build_queue.size()
|
||||
@@ -2294,10 +2313,12 @@ func _drain_water_load_results() -> void:
|
||||
var data: Dictionary = result.get("data", {})
|
||||
if not data.is_empty():
|
||||
var origin: Vector3 = state.get("origin", Vector3.ZERO)
|
||||
var water_root: Node3D = _builder.build_tile_water_scene(data, origin)
|
||||
if water_root:
|
||||
tile_root.add_child(water_root)
|
||||
_set_editor_owner_recursive(water_root)
|
||||
_adt_water_scene_finalizer.attach_water_scene(
|
||||
data,
|
||||
origin,
|
||||
tile_root,
|
||||
_editor_owner_for_generated_nodes()
|
||||
)
|
||||
|
||||
state["water_loaded"] = true
|
||||
_tile_states[key] = state
|
||||
@@ -2327,16 +2348,15 @@ func _wait_for_tile_tasks() -> void:
|
||||
_m2_group_result_queue.clear()
|
||||
_m2_group_result_mutex.unlock()
|
||||
|
||||
for pending in _m2_mesh_load_requests.values():
|
||||
for pending in _m2_mesh_load_pipeline_state.request_records_snapshot():
|
||||
var path: String = String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
continue
|
||||
var status := ResourceLoader.load_threaded_get_status(path)
|
||||
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
|
||||
ResourceLoader.load_threaded_get(path)
|
||||
_m2_mesh_load_requests.clear()
|
||||
_m2_mesh_finalize_queue.clear()
|
||||
_m2_runtime_rebuild_required_cache.clear()
|
||||
_m2_mesh_load_pipeline_state.clear()
|
||||
_m2_runtime_mesh_finalizer.clear()
|
||||
|
||||
for pending in _m2_animation_load_requests.values():
|
||||
var path: String = String(pending.get("path", ""))
|
||||
@@ -2448,10 +2468,12 @@ func _finalize_loaded_tile(request: Dictionary, data: Dictionary) -> void:
|
||||
tile_root.position = tile_origin
|
||||
|
||||
if enable_water:
|
||||
var water_root: Node3D = _builder.build_tile_water_scene(data, tile_origin)
|
||||
if water_root:
|
||||
tile_root.add_child(water_root)
|
||||
_set_editor_owner_recursive(water_root)
|
||||
_adt_water_scene_finalizer.attach_water_scene(
|
||||
data,
|
||||
tile_origin,
|
||||
tile_root,
|
||||
_editor_owner_for_generated_nodes()
|
||||
)
|
||||
if enable_m2_placeholders:
|
||||
_build_tile_m2_placeholders(
|
||||
tile_root,
|
||||
@@ -2983,9 +3005,8 @@ func _clear_streamed_world() -> void:
|
||||
_cancel_m2_build_job(String(key))
|
||||
_m2_build_queue.clear()
|
||||
_m2_unique_placement_registry.clear()
|
||||
_m2_mesh_load_requests.clear()
|
||||
_m2_mesh_finalize_queue.clear()
|
||||
_m2_runtime_rebuild_required_cache.clear()
|
||||
_m2_mesh_load_pipeline_state.clear()
|
||||
_m2_runtime_mesh_finalizer.clear()
|
||||
_m2_animation_load_requests.clear()
|
||||
_m2_animation_finalize_queue.clear()
|
||||
_wmo_build_jobs.clear()
|
||||
@@ -4234,26 +4255,26 @@ func _drain_m2_animation_loads() -> void:
|
||||
|
||||
|
||||
func _drain_m2_mesh_loads() -> void:
|
||||
for normalized_rel_variant in _m2_mesh_load_requests.keys():
|
||||
var normalized_rel := String(normalized_rel_variant)
|
||||
var pending: Dictionary = _m2_mesh_load_requests[normalized_rel]
|
||||
for pending in _m2_mesh_load_pipeline_state.request_records_snapshot():
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
var path: String = String(pending.get("path", ""))
|
||||
if path.is_empty():
|
||||
_m2_mesh_load_requests.erase(normalized_rel)
|
||||
_m2_mesh_load_pipeline_state.discard_request(normalized_rel)
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
continue
|
||||
var status := ResourceLoader.load_threaded_get_status(path)
|
||||
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
||||
continue
|
||||
pending["status"] = status
|
||||
_m2_mesh_load_requests.erase(normalized_rel)
|
||||
_m2_mesh_finalize_queue.append(pending)
|
||||
_m2_mesh_load_pipeline_state.complete_request(normalized_rel, status)
|
||||
|
||||
while not _m2_mesh_finalize_queue.is_empty() and _render_budget_scheduler.try_consume_permit(
|
||||
while _m2_mesh_load_pipeline_state.has_finalize_record() and _render_budget_scheduler.try_consume_permit(
|
||||
RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE):
|
||||
var pending: Dictionary = _m2_mesh_finalize_queue.pop_front()
|
||||
var pending: Dictionary = _m2_mesh_load_pipeline_state.pop_finalize_record()
|
||||
var normalized_rel := String(pending.get("normalized", ""))
|
||||
if normalized_rel.is_empty() or _m2_mesh_cache.has(normalized_rel):
|
||||
if (
|
||||
normalized_rel.is_empty()
|
||||
or _m2_mesh_resource_cache_state.has_mesh(normalized_rel)
|
||||
):
|
||||
continue
|
||||
if int(pending.get("status", ResourceLoader.THREAD_LOAD_FAILED)) != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
@@ -4261,9 +4282,12 @@ func _drain_m2_mesh_loads() -> void:
|
||||
|
||||
var path := String(pending.get("path", ""))
|
||||
var resource: Resource = ResourceLoader.load_threaded_get(path)
|
||||
var mesh := _extract_first_mesh_from_m2_resource(resource)
|
||||
var mesh := _m2_mesh_resource_extractor.extract_first_mesh(resource)
|
||||
if mesh != null:
|
||||
_m2_mesh_cache[normalized_rel] = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
_m2_mesh_resource_cache_state.store_mesh(
|
||||
normalized_rel,
|
||||
_prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
)
|
||||
else:
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
|
||||
@@ -4598,8 +4622,8 @@ func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if normalized_rel.is_empty():
|
||||
return null
|
||||
if _m2_mesh_cache.has(normalized_rel):
|
||||
return _m2_mesh_cache[normalized_rel]
|
||||
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
|
||||
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
|
||||
if _m2_missing_cache.has(normalized_rel):
|
||||
return null
|
||||
_request_m2_mesh_load(normalized_rel)
|
||||
@@ -4607,7 +4631,7 @@ func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
|
||||
|
||||
|
||||
func _request_m2_mesh_load(normalized_rel: String) -> void:
|
||||
if normalized_rel.is_empty() or _m2_mesh_load_requests.has(normalized_rel):
|
||||
if normalized_rel.is_empty() or _m2_mesh_load_pipeline_state.has_request(normalized_rel):
|
||||
return
|
||||
|
||||
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
|
||||
@@ -4621,10 +4645,7 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
|
||||
false,
|
||||
ResourceLoader.CACHE_MODE_REUSE)
|
||||
if err == OK or err == ERR_BUSY:
|
||||
_m2_mesh_load_requests[normalized_rel] = {
|
||||
"normalized": normalized_rel,
|
||||
"path": cache_res_path,
|
||||
}
|
||||
_m2_mesh_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
|
||||
return
|
||||
break
|
||||
|
||||
@@ -4634,17 +4655,18 @@ func _request_m2_mesh_load(normalized_rel: String) -> void:
|
||||
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
|
||||
if mesh == null:
|
||||
return null
|
||||
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
|
||||
if not _m2_runtime_mesh_finalizer.requires_raw_data_for_refresh(mesh):
|
||||
return mesh
|
||||
var data := _load_m2_raw_data_for_refresh(normalized_rel)
|
||||
if data.is_empty() or not _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel, data):
|
||||
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
|
||||
return mesh
|
||||
var rebuilt := _rebuild_m2_mesh_from_data(data)
|
||||
if rebuilt != null:
|
||||
return rebuilt
|
||||
mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
|
||||
return mesh
|
||||
var data := _m2_raw_model_repository.load_static_model_data(
|
||||
extracted_dir,
|
||||
normalized_rel
|
||||
)
|
||||
return _m2_runtime_mesh_finalizer.finalize_mesh(
|
||||
normalized_rel,
|
||||
mesh,
|
||||
data,
|
||||
extracted_dir
|
||||
)
|
||||
|
||||
|
||||
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||
@@ -4658,111 +4680,17 @@ func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
|
||||
mesh_instance.mesh = prepared
|
||||
|
||||
|
||||
func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
|
||||
if normalized_rel.is_empty() or not ClassDB.class_exists("M2Loader"):
|
||||
return {}
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
return {}
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
if loader == null:
|
||||
return {}
|
||||
var data: Variant = loader.call("load_m2", abs_path)
|
||||
return data if data is Dictionary else {}
|
||||
|
||||
|
||||
func _m2_raw_data_needs_runtime_mesh_rebuild(normalized_rel: String, data: Dictionary) -> bool:
|
||||
if _m2_runtime_rebuild_required_cache.has(normalized_rel):
|
||||
return bool(_m2_runtime_rebuild_required_cache[normalized_rel])
|
||||
var needs_rebuild := _m2_raw_data_has_billboards(data) or _m2_raw_data_has_uv_rotation(data)
|
||||
_m2_runtime_rebuild_required_cache[normalized_rel] = needs_rebuild
|
||||
return needs_rebuild
|
||||
|
||||
|
||||
func _m2_raw_data_has_billboards(data: Dictionary) -> bool:
|
||||
for batch_variant in data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
if bool(batch.get("has_billboard", false)) or int(batch.get("billboard_vertex_count", 0)) > 0:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _m2_raw_data_has_uv_rotation(data: Dictionary) -> bool:
|
||||
var transforms: Array = data.get("texture_transforms", [])
|
||||
if transforms.is_empty():
|
||||
return false
|
||||
var combos: PackedInt32Array = data.get("texture_transform_combos", PackedInt32Array())
|
||||
if combos.is_empty():
|
||||
return false
|
||||
for batch_variant in data.get("batches", []):
|
||||
if not (batch_variant is Dictionary):
|
||||
continue
|
||||
var batch: Dictionary = batch_variant
|
||||
var texture_count := maxi(1, int(batch.get("texture_count", 1)))
|
||||
var base_combo := int(batch.get("texture_transform_combo_index", -1))
|
||||
for stage in mini(texture_count, 4):
|
||||
var combo_index := base_combo + stage
|
||||
if combo_index < 0 or combo_index >= combos.size():
|
||||
continue
|
||||
var transform_index := int(combos[combo_index])
|
||||
if transform_index < 0 or transform_index >= transforms.size():
|
||||
continue
|
||||
var transform: Dictionary = transforms[transform_index] if transforms[transform_index] is Dictionary else {}
|
||||
var rotation: Vector4 = transform.get("rotation", Vector4(1.0, 0.0, 0.0, 1.0))
|
||||
if not rotation.is_equal_approx(Vector4(1.0, 0.0, 0.0, 1.0)):
|
||||
return true
|
||||
if absf(float(transform.get("rotation_speed", 0.0))) > 0.000001:
|
||||
return true
|
||||
return false
|
||||
|
||||
|
||||
func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
|
||||
if data.is_empty():
|
||||
return null
|
||||
var prototype: Node3D = M2_BUILDER_SCRIPT.build(data, extracted_dir)
|
||||
if prototype == null:
|
||||
return null
|
||||
var rebuilt := _find_first_mesh_recursive(prototype)
|
||||
prototype.free()
|
||||
return rebuilt
|
||||
|
||||
|
||||
func _extract_first_mesh_from_m2_resource(resource: Resource) -> Mesh:
|
||||
if resource is Mesh:
|
||||
return resource as Mesh
|
||||
if resource is PackedScene:
|
||||
var node := (resource as PackedScene).instantiate()
|
||||
if node == null:
|
||||
return null
|
||||
var mesh := _find_first_mesh_recursive(node)
|
||||
node.free()
|
||||
return mesh
|
||||
return null
|
||||
|
||||
|
||||
func _find_first_mesh_recursive(node: Node) -> Mesh:
|
||||
if node is MeshInstance3D:
|
||||
return (node as MeshInstance3D).mesh
|
||||
for child in node.get_children():
|
||||
var mesh := _find_first_mesh_recursive(child)
|
||||
if mesh != null:
|
||||
return mesh
|
||||
return null
|
||||
|
||||
|
||||
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
|
||||
var normalized_rel := _normalize_m2_rel_path(rel_path)
|
||||
if _m2_mesh_cache.has(normalized_rel):
|
||||
return _m2_mesh_cache[normalized_rel]
|
||||
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
|
||||
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
|
||||
var prototype: Node3D = _get_or_load_m2_prototype(rel_path)
|
||||
if prototype == null:
|
||||
return null
|
||||
var mesh := _find_first_mesh_recursive(prototype)
|
||||
var mesh := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
|
||||
if mesh != null and not normalized_rel.is_empty():
|
||||
mesh = _prepare_m2_mesh_for_runtime(normalized_rel, mesh)
|
||||
_m2_mesh_cache[normalized_rel] = mesh
|
||||
_m2_mesh_resource_cache_state.store_mesh(normalized_rel, mesh)
|
||||
return mesh
|
||||
|
||||
|
||||
@@ -4934,18 +4862,11 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
|
||||
return node as Node3D
|
||||
break
|
||||
|
||||
# Fall back to raw M2Loader
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
var data: Dictionary = loader.call("load_m2", abs_path)
|
||||
# Fall back to the raw native M2 repository.
|
||||
var data := _m2_raw_model_repository.load_static_model_data(
|
||||
extracted_dir,
|
||||
normalized_rel
|
||||
)
|
||||
if data.is_empty():
|
||||
_m2_missing_cache[normalized_rel] = true
|
||||
return null
|
||||
@@ -4967,24 +4888,10 @@ func _get_or_load_m2_native_animated_prototype(rel_path: String) -> Node3D:
|
||||
return _m2_animated_scene_cache[normalized_rel]
|
||||
if _m2_static_animation_cache.has(normalized_rel):
|
||||
return null
|
||||
if not ClassDB.class_exists("M2Loader"):
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
return null
|
||||
|
||||
var abs_path := ProjectSettings.globalize_path(extracted_dir.path_join(normalized_rel))
|
||||
if not FileAccess.file_exists(abs_path):
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
return null
|
||||
|
||||
var loader = ClassDB.instantiate("M2Loader")
|
||||
if loader == null:
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
return null
|
||||
if not loader.has_method("load_m2_animated"):
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
return null
|
||||
|
||||
var data: Dictionary = loader.call("load_m2_animated", abs_path)
|
||||
var data := _m2_raw_model_repository.load_animated_model_data(
|
||||
extracted_dir,
|
||||
normalized_rel
|
||||
)
|
||||
var animated_surfaces: Array = data.get("animated_surfaces", [])
|
||||
if data.is_empty() or animated_surfaces.is_empty():
|
||||
_m2_static_animation_cache[normalized_rel] = true
|
||||
@@ -5421,12 +5328,7 @@ func _tile_key(tx: int, ty: int) -> String:
|
||||
|
||||
|
||||
func _set_editor_owner_recursive(node: Node) -> void:
|
||||
if not Engine.is_editor_hint():
|
||||
return
|
||||
if not editor_persist_generated_nodes:
|
||||
return
|
||||
|
||||
var edited_root := get_tree().edited_scene_root
|
||||
var edited_root := _editor_owner_for_generated_nodes()
|
||||
if edited_root == null:
|
||||
return
|
||||
|
||||
@@ -5435,6 +5337,12 @@ func _set_editor_owner_recursive(node: Node) -> void:
|
||||
_set_editor_owner_recursive(child)
|
||||
|
||||
|
||||
func _editor_owner_for_generated_nodes() -> Node:
|
||||
if not Engine.is_editor_hint() or not editor_persist_generated_nodes:
|
||||
return null
|
||||
return get_tree().edited_scene_root
|
||||
|
||||
|
||||
func _make_editor_signature() -> String:
|
||||
var values := [
|
||||
extracted_dir,
|
||||
|
||||
@@ -241,8 +241,7 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
"ClassDB.instantiate(\"ADTLoader\")",
|
||||
"WorkerThreadPool.add_task(_load_tile_water_task.bind(key, path))",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.WATER_FINALIZE",
|
||||
"_builder.build_tile_water_scene(data, origin)",
|
||||
"tile_root.add_child(water_root)",
|
||||
"_adt_water_scene_finalizer.attach_water_scene(",
|
||||
"state[\"water_loaded\"] = true",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
|
||||
@@ -0,0 +1,194 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic main-thread scene, ownership, boundary and timing regression for
|
||||
## ADT water finalization.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/liquid/adt_water_scene_finalizer.gd")
|
||||
const FINALIZER_PATH := "res://src/render/liquid/adt_water_scene_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_invalid_and_dry_input(failures)
|
||||
_verify_build_attach_geometry_and_owner(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("ADT_WATER_SCENE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"ADT_WATER_SCENE_FINALIZER PASS cases=8 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_invalid_and_dry_input(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var tile_root := Node3D.new()
|
||||
_expect_null(
|
||||
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null),
|
||||
"empty water data returns null",
|
||||
failures
|
||||
)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "empty data does not mutate tile", failures)
|
||||
_expect_null(
|
||||
finalizer.call("attach_water_scene", _synthetic_water_data(), Vector3.ZERO, null, null),
|
||||
"missing tile root returns null",
|
||||
failures
|
||||
)
|
||||
_expect_null(
|
||||
finalizer.call(
|
||||
"attach_water_scene",
|
||||
{"chunks": [{"origin": Vector3.ZERO, "liquids": []}]},
|
||||
Vector3.ZERO,
|
||||
tile_root,
|
||||
null
|
||||
),
|
||||
"dry parsed ADT returns null",
|
||||
failures
|
||||
)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "dry data does not mutate tile", failures)
|
||||
tile_root.free()
|
||||
|
||||
|
||||
func _verify_build_attach_geometry_and_owner(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var editor_scene_root := Node3D.new()
|
||||
var tile_root := Node3D.new()
|
||||
get_root().add_child(editor_scene_root)
|
||||
editor_scene_root.add_child(tile_root)
|
||||
|
||||
var tile_origin := Vector3(10.0, 20.0, 30.0)
|
||||
var water_root: Node3D = finalizer.call(
|
||||
"attach_water_scene",
|
||||
_synthetic_water_data(tile_origin),
|
||||
tile_origin,
|
||||
tile_root,
|
||||
editor_scene_root
|
||||
)
|
||||
_expect_not_null(water_root, "synthetic liquid builds a water root", failures)
|
||||
if water_root != null:
|
||||
_expect_true(water_root.get_parent() == tile_root, "water root attached once", failures)
|
||||
_expect_equal(tile_root.get_child_count(), 1, "tile owns one water root", failures)
|
||||
_expect_true(water_root.owner == editor_scene_root, "water root editor owner", failures)
|
||||
_expect_equal(water_root.get_child_count(), 1, "one liquid surface grouped", failures)
|
||||
if water_root.get_child_count() == 1:
|
||||
var liquid_mesh_instance := water_root.get_child(0) as MeshInstance3D
|
||||
_expect_not_null(liquid_mesh_instance, "liquid child is MeshInstance3D", failures)
|
||||
if liquid_mesh_instance != null:
|
||||
_expect_true(
|
||||
liquid_mesh_instance.owner == editor_scene_root,
|
||||
"liquid child editor owner",
|
||||
failures
|
||||
)
|
||||
var liquid_mesh := liquid_mesh_instance.mesh as ArrayMesh
|
||||
_expect_not_null(liquid_mesh, "liquid surface has ArrayMesh", failures)
|
||||
if liquid_mesh != null:
|
||||
_expect_equal(
|
||||
liquid_mesh.surface_get_array_len(0),
|
||||
4,
|
||||
"one enabled liquid cell has four vertices",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
liquid_mesh.surface_get_array_index_len(0),
|
||||
6,
|
||||
"one enabled liquid cell has six indices",
|
||||
failures
|
||||
)
|
||||
editor_scene_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
finalizer_source.contains("ADT_BUILDER_SCRIPT.build_tile_water_scene"),
|
||||
"finalizer retains existing ADTBuilder implementation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
finalizer_source.contains("tile_root.add_child(water_root)"),
|
||||
"finalizer owns water subtree attachment",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_adt_water_scene_finalizer.attach_water_scene("),
|
||||
2,
|
||||
"both loader water paths delegate finalization",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("_builder.build_tile_water_scene"),
|
||||
"loader no longer builds water scenes directly",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("state[\"water_loaded\"] = true"),
|
||||
"loader retains water-loaded timing",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("WATER_FINALIZE"),
|
||||
"loader retains finalization permit ownership",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var tile_root := Node3D.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
finalizer.call("attach_water_scene", {}, Vector3.ZERO, tile_root, null)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 empty attempts under 1 second", failures)
|
||||
_expect_equal(tile_root.get_child_count(), 0, "timing loop creates no nodes", failures)
|
||||
tile_root.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _synthetic_water_data(tile_origin: Vector3 = Vector3.ZERO) -> Dictionary:
|
||||
var liquid_mask := PackedByteArray()
|
||||
liquid_mask.resize(64)
|
||||
liquid_mask.fill(0)
|
||||
liquid_mask[0] = 1
|
||||
var liquid_heights := PackedFloat32Array()
|
||||
liquid_heights.resize(81)
|
||||
liquid_heights.fill(tile_origin.y + 5.0)
|
||||
return {
|
||||
"chunks": [{
|
||||
"origin": tile_origin,
|
||||
"liquids": [{
|
||||
"liquid_id": 7,
|
||||
"mask": liquid_mask,
|
||||
"heights": liquid_heights,
|
||||
}],
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(value == null, label, failures)
|
||||
|
||||
|
||||
func _expect_not_null(value: Variant, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(value != null, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://c1gqfpxg7ajsh
|
||||
@@ -0,0 +1,167 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic request/terminal/FIFO/lifecycle/boundary/timing regression for
|
||||
## static M2 threaded mesh loading.
|
||||
|
||||
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd")
|
||||
const PIPELINE_PATH := "res://src/render/m2/m2_mesh_load_pipeline_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_validation_dedupe_and_order(failures)
|
||||
_verify_terminal_transition_and_fifo(failures)
|
||||
_verify_discard_metrics_clear_and_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_MESH_LOAD_PIPELINE_STATE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_MESH_LOAD_PIPELINE_STATE PASS cases=11 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_validation_dedupe_and_order(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
_expect_false(pipeline.call("remember_request", "", "res://a.tscn"), "empty normalized path rejected", failures)
|
||||
_expect_false(pipeline.call("remember_request", "world/a.m2", ""), "empty resource path rejected", failures)
|
||||
_expect_true(pipeline.call("remember_request", "world/a.m2", "res://a.tscn"), "first request accepted", failures)
|
||||
_expect_false(pipeline.call("remember_request", "world/a.m2", "res://duplicate.glb"), "duplicate request rejected", failures)
|
||||
_expect_true(pipeline.call("remember_request", "world/b.m2", "res://b.glb"), "second request accepted", failures)
|
||||
_expect_true(pipeline.call("has_request", "world/a.m2"), "request observable", failures)
|
||||
var records: Array[Dictionary] = pipeline.call("request_records_snapshot")
|
||||
_expect_equal(records.size(), 2, "request record count", failures)
|
||||
_expect_string_equal(String(records[0]["normalized"]), "world/a.m2", "request insertion order first", failures)
|
||||
_expect_string_equal(String(records[1]["normalized"]), "world/b.m2", "request insertion order second", failures)
|
||||
|
||||
|
||||
func _verify_terminal_transition_and_fifo(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
pipeline.call("remember_request", "world/a.m2", "res://a.tscn")
|
||||
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||
_expect_false(pipeline.call("complete_request", "world/missing.m2", 3), "unknown completion rejected", failures)
|
||||
_expect_true(pipeline.call("complete_request", "world/b.m2", 4), "second request completes first", failures)
|
||||
_expect_true(pipeline.call("complete_request", "world/a.m2", 3), "first request completes second", failures)
|
||||
_expect_false(pipeline.call("has_request", "world/a.m2"), "completed request removed", failures)
|
||||
_expect_equal(int(pipeline.call("pending_request_count")), 0, "all requests completed", failures)
|
||||
_expect_equal(int(pipeline.call("finalize_record_count")), 2, "two terminal records", failures)
|
||||
var first: Dictionary = pipeline.call("pop_finalize_record")
|
||||
var second: Dictionary = pipeline.call("pop_finalize_record")
|
||||
_expect_string_equal(String(first["normalized"]), "world/b.m2", "completion FIFO first", failures)
|
||||
_expect_equal(int(first["status"]), 4, "first opaque status retained", failures)
|
||||
_expect_string_equal(String(second["normalized"]), "world/a.m2", "completion FIFO second", failures)
|
||||
_expect_equal(int(second["status"]), 3, "second opaque status retained", failures)
|
||||
_expect_false(pipeline.call("has_finalize_record"), "finalize FIFO drained", failures)
|
||||
_expect_true((pipeline.call("pop_finalize_record") as Dictionary).is_empty(), "empty pop returns Dictionary", failures)
|
||||
|
||||
|
||||
func _verify_discard_metrics_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
pipeline.call("remember_request", "world/a.m2", "res://a.tscn")
|
||||
pipeline.call("remember_request", "world/b.m2", "res://b.glb")
|
||||
_expect_true(pipeline.call("discard_request", "world/a.m2"), "pending request discarded", failures)
|
||||
_expect_false(pipeline.call("discard_request", "world/a.m2"), "discard is idempotent", failures)
|
||||
pipeline.call("complete_request", "world/b.m2", 3)
|
||||
_expect_equal(int(pipeline.call("total_work_count")), 1, "total includes finalize only", failures)
|
||||
var snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||
(snapshot["finalize_records"] as Array)[0]["path"] = "mutated"
|
||||
var fresh_snapshot: Dictionary = pipeline.call("diagnostic_snapshot")
|
||||
_expect_string_equal(
|
||||
String((fresh_snapshot["finalize_records"] as Array)[0]["path"]),
|
||||
"res://b.glb",
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
pipeline.call("clear")
|
||||
pipeline.call("clear")
|
||||
_expect_equal(int(pipeline.call("total_work_count")), 0, "clear removes all work", failures)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var pipeline_source := FileAccess.get_file_as_string(PIPELINE_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_LOAD_PIPELINE_STATE_SCRIPT.new()"),
|
||||
"loader composes pipeline state",
|
||||
failures
|
||||
)
|
||||
_expect_false(loader_source.contains("var _m2_mesh_load_requests:"), "legacy request field removed", failures)
|
||||
_expect_false(loader_source.contains("var _m2_mesh_finalize_queue:"), "legacy finalize field removed", failures)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_load_pipeline_state.total_work_count()"),
|
||||
3,
|
||||
"three existing metrics delegate",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_load_pipeline_state.clear()"),
|
||||
2,
|
||||
"two existing clear sites delegate",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_request(",
|
||||
"ResourceLoader.load_threaded_get_status(path)",
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"RENDER_BUDGET_SCHEDULER_SCRIPT.M2_MESH_FINALIZE",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
"_m2_missing_cache[normalized_rel]",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"WorkerThreadPool.",
|
||||
": Mesh",
|
||||
"Node3D",
|
||||
"RID(",
|
||||
"_m2_mesh_cache",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
]:
|
||||
_expect_false(
|
||||
pipeline_source.contains(forbidden_dependency),
|
||||
"pipeline omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
for path_index in range(256):
|
||||
var normalized_path := "world/model_%d.m2" % path_index
|
||||
pipeline.call("remember_request", normalized_path, "res://model_%d.tscn" % path_index)
|
||||
for path_index in range(256):
|
||||
pipeline.call("complete_request", "world/model_%d.m2" % path_index, 3)
|
||||
while pipeline.call("has_finalize_record"):
|
||||
pipeline.call("pop_finalize_record")
|
||||
pipeline.call("clear")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 transitions under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bdn3pdtg62ou1
|
||||
@@ -0,0 +1,158 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic ownership/lifetime/boundary/timing regression for the prepared
|
||||
## static M2 Mesh resource cache.
|
||||
|
||||
const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd")
|
||||
const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_validation_store_lookup_and_replace(failures)
|
||||
_verify_clear_and_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_MESH_RESOURCE_CACHE_STATE: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_MESH_RESOURCE_CACHE_STATE PASS cases=9 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_validation_store_lookup_and_replace(failures: Array[String]) -> void:
|
||||
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||
var first_mesh := ArrayMesh.new()
|
||||
var replacement_mesh := ArrayMesh.new()
|
||||
_expect_false(cache.call("store_mesh", "", first_mesh), "empty path rejected", failures)
|
||||
_expect_false(cache.call("store_mesh", "world/a.m2", null), "null Mesh rejected", failures)
|
||||
_expect_true(cache.call("store_mesh", "world/a.m2", first_mesh), "first Mesh stored", failures)
|
||||
_expect_true(cache.call("has_mesh", "world/a.m2"), "stored Mesh observable", failures)
|
||||
_expect_same(cache.call("find_mesh", "world/a.m2"), first_mesh, "exact Mesh retained", failures)
|
||||
_expect_true(
|
||||
cache.call("store_mesh", "world/a.m2", replacement_mesh),
|
||||
"same-path Mesh replaced",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
cache.call("find_mesh", "world/a.m2"),
|
||||
replacement_mesh,
|
||||
"replacement Mesh retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(cache.call("mesh_count")), 1, "replacement keeps one entry", failures)
|
||||
_expect_same(cache.call("find_mesh", "world/missing.m2"), null, "unknown path returns null", failures)
|
||||
|
||||
|
||||
func _verify_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||
cache.call("store_mesh", "world/a.m2", ArrayMesh.new())
|
||||
cache.call("store_mesh", "world/b.m2", ArrayMesh.new())
|
||||
var normalized_paths: Array[String] = cache.call("normalized_paths_snapshot")
|
||||
_expect_equal(normalized_paths.size(), 2, "diagnostic path count", failures)
|
||||
_expect_string_equal(normalized_paths[0], "world/a.m2", "diagnostic insertion order first", failures)
|
||||
_expect_string_equal(normalized_paths[1], "world/b.m2", "diagnostic insertion order second", failures)
|
||||
normalized_paths.clear()
|
||||
_expect_equal(int(cache.call("mesh_count")), 2, "diagnostics detached", failures)
|
||||
cache.call("clear")
|
||||
cache.call("clear")
|
||||
_expect_equal(int(cache.call("mesh_count")), 0, "clear releases all entries", failures)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var cache_source := FileAccess.get_file_as_string(CACHE_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
||||
"loader composes Mesh cache state",
|
||||
failures
|
||||
)
|
||||
_expect_false(loader_source.contains("var _m2_mesh_cache:"), "legacy Mesh cache removed", failures)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_cache_state.store_mesh("),
|
||||
2,
|
||||
"two existing stores delegate",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_cache_state.find_mesh("),
|
||||
2,
|
||||
"two existing lookups delegate",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_cache_state.clear()"),
|
||||
1,
|
||||
"final shutdown clear delegates",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"_m2_mesh_resource_extractor.extract_first_mesh(resource)",
|
||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||
"_m2_missing_cache[normalized_rel]",
|
||||
"_m2_scene_cache[normalized_rel]",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"PackedScene",
|
||||
"Node3D",
|
||||
"M2Builder",
|
||||
"_m2_missing_cache",
|
||||
"_m2_scene_cache",
|
||||
]:
|
||||
_expect_false(
|
||||
cache_source.contains(forbidden_dependency),
|
||||
"cache omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var cache: RefCounted = CACHE_SCRIPT.new()
|
||||
var meshes: Array[Mesh] = []
|
||||
for _path_index in range(256):
|
||||
meshes.append(ArrayMesh.new())
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
for path_index in range(256):
|
||||
cache.call("store_mesh", "world/model_%d.m2" % path_index, meshes[path_index])
|
||||
for path_index in range(256):
|
||||
cache.call("find_mesh", "world/model_%d.m2" % path_index)
|
||||
cache.call("clear")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 store/lookups under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://ld4k7pnaueno
|
||||
@@ -0,0 +1,185 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic direct-Resource/PackedScene/subtree/order/lifetime/boundary/timing
|
||||
## regression for first-Mesh extraction.
|
||||
|
||||
const EXTRACTOR_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_extractor.gd")
|
||||
const EXTRACTOR_PATH := "res://src/render/m2/m2_mesh_resource_extractor.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_direct_and_invalid_resources(failures)
|
||||
_verify_depth_first_subtree_order(failures)
|
||||
_verify_packed_scene_mesh_and_temporary_lifetime(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_MESH_RESOURCE_EXTRACTOR: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_MESH_RESOURCE_EXTRACTOR PASS cases=10 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_direct_and_invalid_resources(failures: Array[String]) -> void:
|
||||
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||
var direct_mesh := ArrayMesh.new()
|
||||
_expect_same(
|
||||
extractor.call("extract_first_mesh", direct_mesh),
|
||||
direct_mesh,
|
||||
"direct Mesh reference retained",
|
||||
failures
|
||||
)
|
||||
_expect_same(extractor.call("extract_first_mesh", Resource.new()), null, "unsupported Resource returns null", failures)
|
||||
_expect_same(extractor.call("extract_first_mesh", null), null, "null Resource returns null", failures)
|
||||
_expect_same(extractor.call("find_first_mesh_in_subtree", null), null, "null subtree returns null", failures)
|
||||
|
||||
|
||||
func _verify_depth_first_subtree_order(failures: Array[String]) -> void:
|
||||
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||
var root := Node3D.new()
|
||||
var first_branch := Node3D.new()
|
||||
var second_branch := Node3D.new()
|
||||
var first_mesh_instance := MeshInstance3D.new()
|
||||
var second_mesh_instance := MeshInstance3D.new()
|
||||
var first_mesh := ArrayMesh.new()
|
||||
var second_mesh := ArrayMesh.new()
|
||||
first_mesh_instance.mesh = first_mesh
|
||||
second_mesh_instance.mesh = second_mesh
|
||||
root.add_child(first_branch)
|
||||
root.add_child(second_branch)
|
||||
first_branch.add_child(first_mesh_instance)
|
||||
second_branch.add_child(second_mesh_instance)
|
||||
_expect_same(
|
||||
extractor.call("find_first_mesh_in_subtree", root),
|
||||
first_mesh,
|
||||
"depth-first child order retained",
|
||||
failures
|
||||
)
|
||||
_expect_same(first_mesh_instance.get_parent(), first_branch, "subtree ownership unchanged", failures)
|
||||
first_mesh_instance.mesh = null
|
||||
_expect_same(
|
||||
extractor.call("find_first_mesh_in_subtree", root),
|
||||
second_mesh,
|
||||
"null MeshInstance Mesh skipped by parent traversal",
|
||||
failures
|
||||
)
|
||||
root.free()
|
||||
|
||||
|
||||
func _verify_packed_scene_mesh_and_temporary_lifetime(failures: Array[String]) -> void:
|
||||
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||
var source_root := Node3D.new()
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
var packed_mesh := ArrayMesh.new()
|
||||
mesh_instance.mesh = packed_mesh
|
||||
source_root.add_child(mesh_instance)
|
||||
mesh_instance.owner = source_root
|
||||
var packed_scene := PackedScene.new()
|
||||
_expect_equal(packed_scene.pack(source_root), OK, "synthetic PackedScene packs", failures)
|
||||
source_root.free()
|
||||
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
var extracted_mesh: Mesh = extractor.call("extract_first_mesh", packed_scene)
|
||||
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||
_expect_same(extracted_mesh, packed_mesh, "PackedScene first Mesh retained", failures)
|
||||
_expect_equal(node_count_after, node_count_before, "temporary PackedScene root freed", failures)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var extractor_source := FileAccess.get_file_as_string(EXTRACTOR_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()"),
|
||||
"loader composes Mesh resource extractor",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _extract_first_mesh_from_m2_resource("),
|
||||
"legacy Resource extractor removed",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _find_first_mesh_recursive("),
|
||||
"legacy subtree extractor removed",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_extractor.extract_first_mesh("),
|
||||
1,
|
||||
"threaded Resource finalization delegates",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_mesh_resource_extractor.find_first_mesh_in_subtree("),
|
||||
1,
|
||||
"synchronous fallback delegates",
|
||||
failures
|
||||
)
|
||||
for retained_loader_rule in [
|
||||
"ResourceLoader.load_threaded_get(path)",
|
||||
"_prepare_m2_mesh_for_runtime(normalized_rel, mesh)",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
"M2_BUILDER_SCRIPT.build(data, extracted_dir)",
|
||||
"_m2_missing_cache[normalized_rel]",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"M2Builder",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
"MultiMesh",
|
||||
]:
|
||||
_expect_false(
|
||||
extractor_source.contains(forbidden_dependency),
|
||||
"extractor omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
_expect_true(extractor_source.contains("temporary_root.free()"), "temporary root free remains explicit", failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var extractor: RefCounted = EXTRACTOR_SCRIPT.new()
|
||||
var root := Node3D.new()
|
||||
var branch := root
|
||||
for _depth_index in range(8):
|
||||
var child_branch := Node3D.new()
|
||||
branch.add_child(child_branch)
|
||||
branch = child_branch
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.mesh = ArrayMesh.new()
|
||||
branch.add_child(mesh_instance)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(10000):
|
||||
extractor.call("find_first_mesh_in_subtree", root)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
root.free()
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 subtree lookups under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bgvidp5u31sud
|
||||
@@ -0,0 +1,133 @@
|
||||
extends SceneTree
|
||||
|
||||
## Invalid/missing/native-boundary/source/performance contract for raw M2 I/O.
|
||||
|
||||
const REPOSITORY_SCRIPT := preload("res://src/render/m2/m2_raw_model_repository.gd")
|
||||
const REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_rejected_inputs(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_RAW_MODEL_REPOSITORY: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_RAW_MODEL_REPOSITORY PASS cases=10 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_rejected_inputs(failures: Array[String]) -> void:
|
||||
var repository: RefCounted = REPOSITORY_SCRIPT.new()
|
||||
_expect_empty(
|
||||
repository.call("load_static_model_data", "res://data/extracted", ""),
|
||||
"empty static path",
|
||||
failures
|
||||
)
|
||||
_expect_empty(
|
||||
repository.call("load_animated_model_data", "res://data/extracted", ""),
|
||||
"empty animated path",
|
||||
failures
|
||||
)
|
||||
var missing_path := "world/__openwc_missing__/repository_fixture.m2"
|
||||
_expect_empty(
|
||||
repository.call("load_static_model_data", "res://data/extracted", missing_path),
|
||||
"missing static file",
|
||||
failures
|
||||
)
|
||||
_expect_empty(
|
||||
repository.call("load_animated_model_data", "res://data/extracted", missing_path),
|
||||
"missing animated file",
|
||||
failures
|
||||
)
|
||||
_expect_empty(
|
||||
repository.call("load_static_model_data", "res://data/extracted", missing_path),
|
||||
"missing failure is not retained",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var repository_source := FileAccess.get_file_as_string(REPOSITORY_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_RAW_MODEL_REPOSITORY_SCRIPT.new()"),
|
||||
"loader composes raw model repository",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_raw_model_repository.load_static_model_data("),
|
||||
2,
|
||||
"two static callers delegate",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_raw_model_repository.load_animated_model_data("),
|
||||
1,
|
||||
"animated caller delegates",
|
||||
failures
|
||||
)
|
||||
for removed_loader_rule in [
|
||||
"func _load_m2_raw_data_for_refresh(",
|
||||
"ClassDB.class_exists(\"M2Loader\")",
|
||||
"ClassDB.instantiate(\"M2Loader\")",
|
||||
"loader.call(\"load_m2\"",
|
||||
"loader.call(\"load_m2_animated\"",
|
||||
]:
|
||||
_expect_false(loader_source.contains(removed_loader_rule), "loader omits %s" % removed_loader_rule, failures)
|
||||
for required_repository_rule in [
|
||||
"ClassDB.class_exists(NATIVE_LOADER_CLASS_NAME)",
|
||||
"ProjectSettings.globalize_path(",
|
||||
"extracted_directory.path_join(normalized_relative_path)",
|
||||
"FileAccess.file_exists(absolute_path)",
|
||||
"ClassDB.instantiate(NATIVE_LOADER_CLASS_NAME)",
|
||||
"native_loader.has_method(native_method_name)",
|
||||
"\"load_m2\"",
|
||||
"\"load_m2_animated\"",
|
||||
]:
|
||||
_expect_true(repository_source.contains(required_repository_rule), "repository owns %s" % required_repository_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"M2_BUILDER",
|
||||
"_m2_missing_cache",
|
||||
"Mesh",
|
||||
"Node",
|
||||
"WorkerThreadPool",
|
||||
]:
|
||||
_expect_false(repository_source.contains(forbidden_dependency), "repository omits %s" % forbidden_dependency, failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var repository: RefCounted = REPOSITORY_SCRIPT.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(10000):
|
||||
repository.call("load_static_model_data", "res://data/extracted", "")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 empty-path rejects under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _expect_empty(actual: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not (actual is Dictionary) or not (actual as Dictionary).is_empty():
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://byteanym3al5l
|
||||
@@ -0,0 +1,223 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic current/stale/ordinary/rebuild/fallback/clear/boundary/timing
|
||||
## regression for runtime M2 Mesh material finalization.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/m2/m2_runtime_mesh_finalizer.gd")
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||
const RAW_REPOSITORY_PATH := "res://src/render/m2/m2_raw_model_repository.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const REFRESH_META := "wow_m2_material_refresh_version"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_current_and_empty_raw_data(failures)
|
||||
_verify_ordinary_and_rebuild_paths(failures)
|
||||
_verify_rebuild_failure_fallback_and_clear(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_RUNTIME_MESH_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_RUNTIME_MESH_FINALIZER PASS cases=11 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_current_and_empty_raw_data(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
_expect_same(finalizer.call("finalize_mesh", "world/null.m2", null, {}, ""), null, "null Mesh returns null", failures)
|
||||
var current_mesh := ArrayMesh.new()
|
||||
current_mesh.set_meta(REFRESH_META, 2)
|
||||
_expect_false(
|
||||
finalizer.call("requires_raw_data_for_refresh", current_mesh),
|
||||
"current Mesh skips raw data",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
finalizer.call("finalize_mesh", "world/current.m2", current_mesh, _rebuild_raw_data(), ""),
|
||||
current_mesh,
|
||||
"current Mesh returns unchanged",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 0, "current Mesh skips classifier", failures)
|
||||
var stale_mesh := ArrayMesh.new()
|
||||
_expect_true(
|
||||
finalizer.call("requires_raw_data_for_refresh", stale_mesh),
|
||||
"stale Mesh requests raw data",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
finalizer.call("finalize_mesh", "world/stale.m2", stale_mesh, {}, ""),
|
||||
stale_mesh,
|
||||
"empty raw data falls back to original",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(stale_mesh.get_meta(REFRESH_META, 0)), 2, "empty raw fallback marks current", failures)
|
||||
|
||||
|
||||
func _verify_ordinary_and_rebuild_paths(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var ordinary_mesh := ArrayMesh.new()
|
||||
var ordinary_raw_data := {"batches": [{"has_billboard": false}]}
|
||||
_expect_same(
|
||||
finalizer.call("finalize_mesh", "world/rock.m2", ordinary_mesh, ordinary_raw_data, ""),
|
||||
ordinary_mesh,
|
||||
"ordinary raw data retains original",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(ordinary_mesh.get_meta(REFRESH_META, 0)), 2, "ordinary Mesh marks current", failures)
|
||||
var stale_billboard_mesh := ArrayMesh.new()
|
||||
var rebuilt_mesh: Mesh = finalizer.call(
|
||||
"finalize_mesh",
|
||||
"world/tree.m2",
|
||||
stale_billboard_mesh,
|
||||
_rebuild_raw_data(),
|
||||
""
|
||||
)
|
||||
_expect_true(rebuilt_mesh != null, "billboard raw data rebuilds Mesh", failures)
|
||||
_expect_false(is_same(rebuilt_mesh, stale_billboard_mesh), "rebuild replaces original Mesh", failures)
|
||||
_expect_equal(int(rebuilt_mesh.get_meta(REFRESH_META, 0)), 2, "rebuilt Mesh carries refresh version", failures)
|
||||
|
||||
|
||||
func _verify_rebuild_failure_fallback_and_clear(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var fallback_mesh := ArrayMesh.new()
|
||||
var incomplete_billboard_data := {"batches": [{"has_billboard": true}]}
|
||||
_expect_same(
|
||||
finalizer.call(
|
||||
"finalize_mesh",
|
||||
"world/incomplete.m2",
|
||||
fallback_mesh,
|
||||
incomplete_billboard_data,
|
||||
""
|
||||
),
|
||||
fallback_mesh,
|
||||
"failed rebuild falls back to original",
|
||||
failures
|
||||
)
|
||||
_expect_equal(int(fallback_mesh.get_meta(REFRESH_META, 0)), 2, "failed rebuild marks fallback current", failures)
|
||||
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 1, "classifier decision retained", failures)
|
||||
finalizer.call("clear")
|
||||
finalizer.call("clear")
|
||||
_expect_equal(int(finalizer.call("cached_rebuild_decision_count")), 0, "clear drops decisions", failures)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var raw_repository_source := FileAccess.get_file_as_string(RAW_REPOSITORY_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()"),
|
||||
"loader composes runtime Mesh finalizer",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
|
||||
2,
|
||||
"two existing clear sites delegate",
|
||||
failures
|
||||
)
|
||||
_expect_false(loader_source.contains("const M2_MATERIAL_REFRESH_VERSION"), "refresh version leaves loader", failures)
|
||||
_expect_false(loader_source.contains("func _rebuild_m2_mesh_from_data("), "legacy rebuild adapter removed", failures)
|
||||
for retained_loader_rule in [
|
||||
"M2_RAW_MODEL_REPOSITORY_SCRIPT.new()",
|
||||
"_m2_raw_model_repository.load_static_model_data(",
|
||||
"_m2_mesh_resource_cache_state.store_mesh(",
|
||||
"_m2_missing_cache[normalized_rel]",
|
||||
]:
|
||||
_expect_true(loader_source.contains(retained_loader_rule), "loader retains %s" % retained_loader_rule, failures)
|
||||
for raw_repository_rule in [
|
||||
"FileAccess.file_exists(absolute_path)",
|
||||
"ClassDB.instantiate(NATIVE_LOADER_CLASS_NAME)",
|
||||
"\"load_m2\"",
|
||||
]:
|
||||
_expect_true(raw_repository_source.contains(raw_repository_rule), "raw repository owns %s" % raw_repository_rule, failures)
|
||||
for required_finalizer_rule in [
|
||||
"const MATERIAL_REFRESH_VERSION := 2",
|
||||
"M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()",
|
||||
"M2_MESH_RESOURCE_EXTRACTOR_SCRIPT.new()",
|
||||
"M2_BUILDER_SCRIPT.build(",
|
||||
"temporary_prototype.free()",
|
||||
]:
|
||||
_expect_true(finalizer_source.contains(required_finalizer_rule), "finalizer owns %s" % required_finalizer_rule, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"ClassDB.",
|
||||
"load_m2",
|
||||
"_m2_mesh_resource_cache_state",
|
||||
"_m2_missing_cache",
|
||||
"MultiMesh",
|
||||
]:
|
||||
_expect_false(
|
||||
finalizer_source.contains(forbidden_dependency),
|
||||
"finalizer omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new()
|
||||
var meshes: Array[Mesh] = []
|
||||
for _path_index in range(256):
|
||||
meshes.append(ArrayMesh.new())
|
||||
var ordinary_raw_data := {"batches": [{"has_billboard": false}]}
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in range(100):
|
||||
for path_index in range(256):
|
||||
var mesh := meshes[path_index]
|
||||
mesh.set_meta(REFRESH_META, 0)
|
||||
finalizer.call(
|
||||
"finalize_mesh",
|
||||
"world/model_%d.m2" % path_index,
|
||||
mesh,
|
||||
ordinary_raw_data,
|
||||
""
|
||||
)
|
||||
finalizer.call("clear")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 ordinary finalizations under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _rebuild_raw_data() -> Dictionary:
|
||||
return {
|
||||
"vertices": PackedVector3Array([
|
||||
Vector3(0.0, 0.0, 0.0),
|
||||
Vector3(1.0, 0.0, 0.0),
|
||||
Vector3(0.0, 1.0, 0.0),
|
||||
]),
|
||||
"indices": PackedInt32Array([0, 1, 2]),
|
||||
"batches": [{
|
||||
"index_start": 0,
|
||||
"index_count": 3,
|
||||
"material_id": -1,
|
||||
"texture_combo_index": -1,
|
||||
"has_billboard": true,
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://r1yv88u0rykp
|
||||
@@ -0,0 +1,250 @@
|
||||
extends SceneTree
|
||||
|
||||
## Synthetic predicate, memoization, boundary and timing regression for stale
|
||||
## cached M2 runtime mesh rebuild decisions.
|
||||
|
||||
const CLASSIFIER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
|
||||
)
|
||||
const CLASSIFIER_PATH := "res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
|
||||
const FINALIZER_PATH := "res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_billboard_decisions(failures)
|
||||
_verify_uv_rotation_decisions(failures)
|
||||
_verify_stage_and_index_boundaries(failures)
|
||||
_verify_memoization_clear_and_diagnostics(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_RUNTIME_MESH_REBUILD_CLASSIFIER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_RUNTIME_MESH_REBUILD_CLASSIFIER PASS cases=12 iterations=100 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_billboard_decisions(failures: Array[String]) -> void:
|
||||
_expect_false(_classify("empty", {}), "empty data does not rebuild", failures)
|
||||
_expect_false(
|
||||
_classify("invalid", {"batches": ["invalid", 7, null]}),
|
||||
"non-Dictionary batches skipped",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
_classify("flag", {"batches": [{"has_billboard": true}]}),
|
||||
"billboard flag rebuilds",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
_classify("vertices", {"batches": [{"billboard_vertex_count": 1}]}),
|
||||
"positive billboard vertex count rebuilds",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
_classify("zero_vertices", {"batches": [{"billboard_vertex_count": 0}]}),
|
||||
"zero billboard vertex count does not rebuild",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_uv_rotation_decisions(failures: Array[String]) -> void:
|
||||
_expect_false(
|
||||
_classify("identity", _uv_data([_rotation_transform()])),
|
||||
"identity UV rotation does not rebuild",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
_classify(
|
||||
"rotation",
|
||||
_uv_data([_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0))])
|
||||
),
|
||||
"non-identity UV rotation rebuilds",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
_classify("positive_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000002)])),
|
||||
"positive UV rotation speed rebuilds",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
_classify("negative_speed", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), -0.000002)])),
|
||||
"negative UV rotation speed rebuilds",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
_classify("epsilon", _uv_data([_rotation_transform(Vector4(1, 0, 0, 1), 0.000001)])),
|
||||
"exact UV rotation epsilon does not rebuild",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_stage_and_index_boundaries(failures: Array[String]) -> void:
|
||||
var fifth_stage_transforms: Array = [
|
||||
_rotation_transform(),
|
||||
_rotation_transform(),
|
||||
_rotation_transform(),
|
||||
_rotation_transform(),
|
||||
_rotation_transform(Vector4(0.0, 1.0, -1.0, 0.0)),
|
||||
]
|
||||
_expect_false(
|
||||
_classify(
|
||||
"fifth_stage",
|
||||
_uv_data(fifth_stage_transforms, PackedInt32Array([0, 1, 2, 3, 4]), 5)
|
||||
),
|
||||
"fifth texture stage remains ignored",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
_classify(
|
||||
"invalid_indices",
|
||||
_uv_data([_rotation_transform(Vector4(0, 1, -1, 0))], PackedInt32Array([-1, 9]), 2)
|
||||
),
|
||||
"invalid transform indices skipped",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
_classify(
|
||||
"invalid_transform",
|
||||
_uv_data(["not-a-transform"], PackedInt32Array([0]), 1)
|
||||
),
|
||||
"non-Dictionary transform uses identity defaults",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
|
||||
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||
var billboard_data := {"batches": [{"has_billboard": true}]}
|
||||
_expect_true(
|
||||
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", billboard_data),
|
||||
"first true decision",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||
"first path decision memoized",
|
||||
failures
|
||||
)
|
||||
classifier.call("needs_runtime_mesh_rebuild", "world/rock.m2", {})
|
||||
_expect_equal(int(classifier.call("cached_path_count")), 2, "cached path count", failures)
|
||||
var snapshot: Dictionary = classifier.call("diagnostic_snapshot")
|
||||
snapshot["world/tree.m2"] = false
|
||||
_expect_true(
|
||||
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||
"diagnostics detached",
|
||||
failures
|
||||
)
|
||||
classifier.call("clear")
|
||||
_expect_equal(int(classifier.call("cached_path_count")), 0, "clear count", failures)
|
||||
_expect_false(
|
||||
classifier.call("needs_runtime_mesh_rebuild", "world/tree.m2", {}),
|
||||
"clear allows recomputation",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var classifier_source := FileAccess.get_file_as_string(CLASSIFIER_PATH)
|
||||
var finalizer_source := FileAccess.get_file_as_string(FINALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
finalizer_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
|
||||
"runtime Mesh finalizer composes classifier",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
|
||||
2,
|
||||
"loader retains two finalizer clear sites",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("_m2_runtime_rebuild_required_cache"),
|
||||
"loader removes legacy rebuild cache",
|
||||
failures
|
||||
)
|
||||
for removed_function in [
|
||||
"func _m2_raw_data_needs_runtime_mesh_rebuild",
|
||||
"func _m2_raw_data_has_billboards",
|
||||
"func _m2_raw_data_has_uv_rotation",
|
||||
]:
|
||||
_expect_false(loader_source.contains(removed_function), "loader removes %s" % removed_function, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"WorkerThreadPool.",
|
||||
"M2_BUILDER_SCRIPT",
|
||||
"ClassDB.instantiate",
|
||||
"FileAccess.",
|
||||
]:
|
||||
_expect_false(
|
||||
classifier_source.contains(forbidden_dependency),
|
||||
"classifier omits %s dependency" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||
var ordinary_data := {"batches": [{"has_billboard": false}]}
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(100):
|
||||
for path_index in range(256):
|
||||
classifier.call(
|
||||
"needs_runtime_mesh_rebuild",
|
||||
"world/model_%d.m2" % path_index,
|
||||
ordinary_data
|
||||
)
|
||||
classifier.call("clear")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "100 by 256 classifications under 1 second", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _classify(normalized_path: String, raw_data: Dictionary) -> bool:
|
||||
var classifier: RefCounted = CLASSIFIER_SCRIPT.new()
|
||||
return bool(classifier.call("needs_runtime_mesh_rebuild", normalized_path, raw_data))
|
||||
|
||||
|
||||
func _uv_data(
|
||||
texture_transforms: Array,
|
||||
texture_transform_combos: PackedInt32Array = PackedInt32Array([0]),
|
||||
texture_count: int = 1) -> Dictionary:
|
||||
return {
|
||||
"texture_transforms": texture_transforms,
|
||||
"texture_transform_combos": texture_transform_combos,
|
||||
"batches": [{
|
||||
"texture_count": texture_count,
|
||||
"texture_transform_combo_index": 0,
|
||||
}],
|
||||
}
|
||||
|
||||
|
||||
func _rotation_transform(
|
||||
rotation: Vector4 = Vector4(1.0, 0.0, 0.0, 1.0),
|
||||
rotation_speed: float = 0.0) -> Dictionary:
|
||||
return {
|
||||
"rotation": rotation,
|
||||
"rotation_speed": rotation_speed,
|
||||
}
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://cc62iwxjrkte6
|
||||
@@ -35,7 +35,8 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
||||
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
||||
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
||||
loader.set("_m2_mesh_cache", {"mesh": ArrayMesh.new()})
|
||||
var m2_mesh_resource_cache_state: RefCounted = loader.get("_m2_mesh_resource_cache_state")
|
||||
m2_mesh_resource_cache_state.call("store_mesh", "mesh", ArrayMesh.new())
|
||||
loader.set("_m2_static_animation_cache", {"static": true})
|
||||
loader.set("_m2_missing_cache", {"missing": true})
|
||||
var wmo_render_cache_state: RefCounted = loader.get("_wmo_render_resource_cache_state")
|
||||
@@ -59,7 +60,6 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
"_m2_scene_cache",
|
||||
"_m2_animated_scene_cache",
|
||||
"_wmo_prototype_cache",
|
||||
"_m2_mesh_cache",
|
||||
"_m2_static_animation_cache",
|
||||
"_m2_missing_cache",
|
||||
"_wmo_missing_cache",
|
||||
@@ -67,6 +67,10 @@ func _verify_runtime_cache_ownership() -> void:
|
||||
]:
|
||||
var cache: Dictionary = loader.get(cache_name)
|
||||
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
|
||||
_expect(
|
||||
int(m2_mesh_resource_cache_state.call("mesh_count")) == 0,
|
||||
"M2 Mesh Resources should be empty after shutdown"
|
||||
)
|
||||
var wmo_render_snapshot: Dictionary = wmo_render_cache_state.call("diagnostic_snapshot")
|
||||
_expect(
|
||||
(wmo_render_snapshot["resource_paths"] as Array).is_empty(),
|
||||
|
||||
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-17
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`;
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`;
|
||||
packages `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`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||
@@ -42,7 +42,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-WMO-RENDER-BUILD-QUEUE-001`,
|
||||
`M03-RND-WMO-RENDER-RESOURCE-CACHE-001`,
|
||||
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`,
|
||||
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`
|
||||
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`,
|
||||
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
|
||||
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`,
|
||||
`M03-RND-M2-MESH-LOAD-PIPELINE-001`,
|
||||
`M03-RND-M2-MESH-RESOURCE-CACHE-001`,
|
||||
`M03-RND-M2-MESH-RESOURCE-EXTRACTOR-001`,
|
||||
`M03-RND-M2-RUNTIME-MESH-FINALIZER-001`,
|
||||
`M03-RND-M2-RAW-MODEL-REPOSITORY-001`
|
||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||
@@ -117,6 +124,60 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
checkpoint dry-run retained `7/7` plans. Post-merge water state (`86.935ms`),
|
||||
shutdown, renderer materials, facade, internal-access `36`, `7/7` manifest,
|
||||
documentation and coordination gates remained green.
|
||||
ADT water scene finalizer passed `cases=8 iterations=100 elapsed_ms=0.042`
|
||||
after an initial `0.024ms` run; synthetic data produced one four-vertex/
|
||||
six-index liquid surface with exact tile attachment and recursive editor owner.
|
||||
Water pipeline, WMO/M2/terrain/facade/scheduler/streaming/coordinate, material,
|
||||
shutdown and 32 headless contract regressions passed; checkpoint dry-run
|
||||
retained `7/7` plans. Post-merge finalizer (`0.025ms`), water pipeline
|
||||
(`98.398ms`), shutdown, materials, facade, internal-access `36`, `7/7`
|
||||
manifest, documentation and coordination gates remained green.
|
||||
M2 runtime mesh rebuild classifier passed `cases=12 iterations=100
|
||||
elapsed_ms=64.585` after an initial `35.461ms` run; billboard flag/count,
|
||||
invalid variants/indices, UV rotation/speed/epsilon, four-stage cap,
|
||||
memoization, detached diagnostics and both clear sites passed. Thirty-three
|
||||
M2/WMO/liquid/terrain/facade/scheduler/streaming/coordinate/material/shutdown
|
||||
regressions and checkpoint dry-run `7/7` passed. Post-merge classifier
|
||||
(`45.439ms`), materials, shutdown, M2 batch planner, facade, internal-access
|
||||
`35`, `7/7` manifest, documentation and coordination gates remained green.
|
||||
M2 mesh load pipeline state passed `cases=11 iterations=100
|
||||
elapsed_ms=100.273` with request validation/dedupe, insertion-order polling,
|
||||
terminal transition/completion FIFO, opaque statuses, discard, detached
|
||||
diagnostics, metrics, clear and 100-by-256 timing. Thirty-four adjacent
|
||||
headless regressions and checkpoint dry-run `7/7` passed. Post-merge pipeline
|
||||
state (`69.196ms`), classifier, materials, shutdown, facade, internal-access
|
||||
`34`, `7/7` manifest, documentation and coordination gates remained green.
|
||||
M2 mesh resource cache state passed `cases=9 iterations=100
|
||||
elapsed_ms=38.974` with invalid-input guards, exact-reference lookup,
|
||||
replacement, detached path diagnostics, idempotent clear and 100-by-256
|
||||
timing. Thirty-five renderer/coordinate regressions and checkpoint dry-run
|
||||
`7/7` passed. Post-merge cache state (`38.686ms`), pipeline, classifier,
|
||||
materials, shutdown, facade, internal-access `34`, `7/7` manifest,
|
||||
documentation and coordination gates remained green.
|
||||
M2 mesh resource extractor passed `cases=10 iterations=10000
|
||||
elapsed_ms=39.243` with direct/invalid Resource, depth-first order,
|
||||
null-Mesh sibling continuation, subtree ownership, PackedScene Mesh identity
|
||||
and temporary-root release contracts. Thirty-six renderer/coordinate
|
||||
regressions and checkpoint dry-run `7/7` passed. Post-merge extractor
|
||||
(`38.172ms`), cache, pipeline, classifier, materials, shutdown, facade,
|
||||
internal-access `34`, `7/7` manifest, documentation and coordination gates
|
||||
remained green.
|
||||
M2 runtime mesh finalizer passed `cases=11 iterations=100
|
||||
elapsed_ms=53.719` with current/stale raw-data gating, empty/ordinary marking,
|
||||
synthetic triangle billboard rebuild, failed-rebuild fallback, version `2`
|
||||
metadata and classifier-clear contracts. Thirty-seven renderer/coordinate
|
||||
regressions and checkpoint dry-run `7/7` passed. Post-merge finalizer
|
||||
(`54.349ms`), classifier, extractor, cache, pipeline, materials, shutdown,
|
||||
facade, internal-access `34`, `7/7` manifest, documentation and coordination
|
||||
gates remained green.
|
||||
M2 raw model repository passed `cases=10 iterations=10000
|
||||
elapsed_ms=3.275` with empty/missing/stateless failure, exact native-method,
|
||||
path and three-caller boundary contracts. Forty-five autonomous headless
|
||||
regressions and checkpoint dry-run `7/7` passed; the proprietary ADT probe
|
||||
remained unavailable without `data/extracted`. Post-merge repository
|
||||
(`3.200ms`), finalizer, extractor, cache, pipeline, shutdown, facade,
|
||||
internal-access `34`, `7/7` manifest, documentation and coordination gates
|
||||
remained green.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -172,7 +233,36 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
result publication order are unchanged, and the mutex remains limited to the
|
||||
worker/main-thread mailbox. The loader still owns ADT parsing, concurrency and
|
||||
profile limits, stale tile/path/root rejection, `WATER_FINALIZE` permits,
|
||||
water Node construction/attachment and `water_loaded` timing.
|
||||
tile subtree lifetime and `water_loaded` timing.
|
||||
Both raw-tile and delayed cached-tile water paths now call the same stateless
|
||||
finalizer. It invokes the unchanged `ADTBuilder.build_tile_water_scene`,
|
||||
attaches the returned root once and applies the existing optional recursive
|
||||
Editor ownership; tile subtree lifetime and dry-root cleanup are unchanged.
|
||||
M2 stale-cache rebuild selection retains the same positive billboard flag/
|
||||
vertex-count tests, referenced non-identity UV rotation, exclusive
|
||||
`0.000001` speed threshold, four-stage cap and first-decision-per-path cache
|
||||
lifetime. Raw loading, material refresh version, M2Builder rebuild/fallback and
|
||||
Mesh adoption remain loader-owned.
|
||||
Static M2 mesh pending requests preserve normalized-path dedupe and Dictionary
|
||||
insertion polling order; loaded/failed transitions preserve completion-order
|
||||
finalization FIFO and opaque statuses. All three pending-plus-finalize metrics,
|
||||
both clear sites and shutdown ResourceLoader drain order are unchanged; cache
|
||||
path selection, I/O, permits, shared missing state and adoption remain loader-owned.
|
||||
Prepared static M2 Mesh lookups retain exact Resource identity, same-path
|
||||
replacement and map-refresh lifetime; the only clear remains final shutdown
|
||||
after asynchronous drain. Shared missing/prototype/animated caches and
|
||||
materialization remain loader-owned.
|
||||
Direct Mesh Resources, temporary PackedScene instances and prototype/rebuild
|
||||
subtrees retain the same depth-first first-Mesh rule. PackedScene still
|
||||
instantiates once and frees its temporary root synchronously before return;
|
||||
the extractor retains no state or Resource reference.
|
||||
Runtime material refresh remains version `2`; current Meshes skip raw loading,
|
||||
empty/ordinary data marks and reuses the original, billboard/UV-rotation data
|
||||
attempts the same M2Builder rebuild, and failure marks/reuses the original.
|
||||
Raw FileAccess/ClassDB M2Loader I/O now belongs to the stateless repository;
|
||||
exact path resolution and `load_m2`/`load_m2_animated` names are unchanged.
|
||||
Loader-owned normalization, fallback order, missing/prototype caches, builders
|
||||
and result adoption remain unchanged.
|
||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||
@@ -196,6 +286,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
validation/reset adapters,
|
||||
ADT water pending/task/result pipeline state and loader enqueue/start/publish/
|
||||
drain/release/reset/shutdown/metrics adapters,
|
||||
stateless ADT water scene finalizer and both loader build/attach adapters,
|
||||
memoized M2 runtime mesh rebuild classifier and loader query/clear adapters,
|
||||
M2 mesh load pipeline state and loader request/poll/finalize/metric/clear adapters,
|
||||
prepared M2 Mesh resource cache state and loader lookup/store/shutdown adapters,
|
||||
stateless M2 Mesh resource extractor and three loader traversal adapters,
|
||||
M2 runtime Mesh finalizer and loader raw-data/finalize/clear adapters,
|
||||
stateless M2 raw model repository and three native-load adapters,
|
||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||
- Remaining risks: worker concurrency and stale-result validation remain
|
||||
streamer-owned; cancellation stops new permits but does not interrupt
|
||||
@@ -209,11 +306,16 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
||||
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
||||
and boundary traversal p95/p99 remain pending; path grouping is not yet
|
||||
culling-driven spatial-cell grouping; M2 resource/queue transitions and raw
|
||||
culling-driven spatial-cell grouping; M2 rebuild classification and static
|
||||
request/finalize bookkeeping, prepared Mesh cache ownership and first-Mesh
|
||||
traversal plus refresh/rebuild/fallback and raw native M2 I/O are separated,
|
||||
but synchronous parsing, ResourceLoader, shared missing state and Mesh adoption
|
||||
remain in the loader or its direct call path; raw
|
||||
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
|
||||
remain in the loader;
|
||||
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
||||
ADT water parsing, main-thread materialization and active worker interruption
|
||||
remain loader-owned, and asset-backed MH2O/MCLQ traversal/leak/p95/p99/
|
||||
ADT water parsing and active worker interruption remain loader-owned;
|
||||
materialization is separated but remains synchronous main-thread work behind
|
||||
the existing permit, and asset-backed MH2O/MCLQ traversal/leak/p95/p99/
|
||||
original-client evidence is pending; M03 still needs further terrain/M2/WMO/
|
||||
liquid service extraction and dependency/performance acceptance.
|
||||
|
||||
Reference in New Issue
Block a user