merge M03 M2 runtime mesh finalizer

This commit is contained in:
2026-07-17 15:02:28 +04:00
16 changed files with 642 additions and 92 deletions
+25 -5
View File
@@ -39,6 +39,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `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.
@@ -807,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.
@@ -817,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
@@ -1053,9 +1060,9 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- 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 loads raw M2 data, checks
`M2_MATERIAL_REFRESH_VERSION`, calls `M2Builder`, adopts/falls back to Meshes
and owns ResourceLoader/finalization budgets.
- `StreamingWorldLoader` still loads 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.
@@ -1100,6 +1107,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- 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. The loader retains FileAccess/
ClassDB M2Loader I/O and supplies raw data only when the finalizer reports a
stale Mesh; both historical decision-cache clear sites are preserved.
- 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 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
@@ -78,14 +78,23 @@ Mesh finalizer while retaining raw-file I/O in the loader.
## Status
- State: claimed
- Done: refresh, raw-data boundary, rebuild/fallback and both clear sites identified
- Next: implement finalizer, migrate adapters, verify and document
- State: ready
- Done: finalizer, loader/test adapters, transition/rebuild verifier and required documentation
- Next: integrator merge and post-merge smoke
- Blocked by:
## Handoff
- Commit:
- Results:
- Remaining risks:
- Documentation updated:
- 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 -->
+1
View File
@@ -25,6 +25,7 @@
| 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) |
| 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) |
+9 -5
View File
@@ -87,7 +87,8 @@ flowchart TD
Permit -->|no| FIFO
Permit -->|yes| Pop[Pop oldest terminal record]
Pop --> Finalize[Loader gets Resource and delegates first-Mesh extraction]
Finalize --> Adopt[Loader prepares and adopts Mesh/missing state]
Finalize --> Prepare[M2RuntimeMeshFinalizer prepares Mesh]
Prepare --> Adopt[Loader adopts Mesh/missing state]
```
## Lifecycle/state
@@ -137,7 +138,7 @@ flowchart TB
Loader --> Resource[ResourceLoader]
Loader --> Budget[RenderBudgetScheduler]
Loader --> MeshCache[M2 Mesh resource cache and shared missing cache]
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
State -. no dependency .-> Resource
State -. no dependency .-> Budget
State -. no dependency .-> MeshCache
@@ -148,8 +149,9 @@ flowchart TB
- 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; the loader
owns shared missing state, material refresh, M2Builder and other engine resources.
- `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
@@ -206,7 +208,8 @@ rebuild policy are unchanged; no migration or rebake is required.
| 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 materialization | Existing loader-owned | Renderer/material tests | Separate preparation finalizer 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
@@ -222,6 +225,7 @@ rebuild policy are unchanged; no migration or rebake is required.
| `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 |
+14 -12
View File
@@ -13,8 +13,8 @@
## Purpose
Own the runtime cache of prepared static M2 `Mesh` references outside
`StreamingWorldLoader`. The loader remains responsible for finding, loading,
extracting, refreshing and materializing each Mesh.
`StreamingWorldLoader`. Dedicated services extract and prepare each Mesh; the
loader remains responsible for finding, loading, adopting and materializing it.
## Non-goals
@@ -34,7 +34,7 @@ flowchart LR
Loader --> Pipeline[M2MeshLoadPipelineState]
Pipeline --> IO[ResourceLoader]
IO --> Extract[M2MeshResourceExtractor]
Extract --> Prepare[Loader runtime preparation]
Extract --> Prepare[M2RuntimeMeshFinalizer]
Prepare --> Cache
Loader --> Build[M2 MultiMesh materialization]
```
@@ -74,7 +74,7 @@ flowchart TD
Cached -->|no| Missing{Shared missing state?}
Missing -->|yes| Null[Return null]
Missing -->|no| Load[Loader request or synchronous fallback]
Load --> Prepare[Loader extracts and prepares Mesh]
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]
@@ -104,7 +104,7 @@ sequenceDiagram
Loader->>Cache: find/has normalized path
alt cache miss
Loader->>Pipeline: request/poll/finalize record
Loader->>Loader: ResourceLoader get + extract + prepare
Loader->>Loader: ResourceLoader get; delegate extract + prepare
Loader->>Cache: store_mesh(path, prepared Mesh)
end
Cache-->>Loader: exact retained Mesh
@@ -117,7 +117,7 @@ sequenceDiagram
flowchart TB
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
Loader --> Pipeline[M2MeshLoadPipelineState]
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
Loader --> Builder[M2Builder and MultiMesh]
Cache --> Mesh[Godot Mesh Resource]
Cache -. no dependency .-> Pipeline
@@ -146,8 +146,9 @@ flowchart TB
## Configuration and capabilities
The module adds no setting, budget or profile branch. Cache path order,
`M2_MATERIAL_REFRESH_VERSION`, finalize permits and batching remain loader-owned.
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
@@ -173,8 +174,8 @@ the historical Mesh cache had no queue contribution or log site.
## Extension points
- A separate M2 Mesh resource finalizer may later return a prepared Mesh for
`store_mesh` without changing cache ownership.
- `M2RuntimeMeshFinalizer` returns a prepared Mesh for `store_mesh` without
changing cache ownership.
- Eviction requires measured memory pressure and explicit lifetime semantics.
## Capability status
@@ -184,7 +185,7 @@ the historical Mesh cache had no queue contribution or log site.
| 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 | Existing loader-owned | Material/classifier regressions | Separate finalizer extraction next |
| M2 Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
## Known gaps and risks
@@ -200,7 +201,8 @@ the historical Mesh cache had no queue contribution or log site.
|---|---|
| `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, I/O, preparation, missing state and materialization |
| `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 |
+9 -7
View File
@@ -32,7 +32,7 @@ flowchart LR
Loader --> Resource[Loaded Mesh or PackedScene Resource]
Resource --> Extractor[M2MeshResourceExtractor]
Extractor -->|first Mesh or null| Loader
Loader --> Prepare[Material refresh/rebuild]
Loader --> Prepare[M2RuntimeMeshFinalizer]
Prepare --> Cache[M2MeshResourceCacheState]
Prototype[Existing M2 Node subtree] --> Extractor
```
@@ -116,7 +116,7 @@ flowchart TB
Extractor --> Types[Resource / PackedScene / Node / Mesh]
Loader --> Pipeline[M2MeshLoadPipelineState]
Loader --> Cache[M2MeshResourceCacheState]
Loader --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader --> Finalizer[M2RuntimeMeshFinalizer]
Extractor -. no dependency .-> Pipeline
Extractor -. no dependency .-> Cache
Extractor -. no dependency .-> Classifier
@@ -128,7 +128,8 @@ flowchart TB
- 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 and Mesh material preparation remain loader/service concerns.
- Cache retention remains a cache-state concern; runtime material preparation
belongs to `M2RuntimeMeshFinalizer`.
## Errors, cancellation and recovery
@@ -167,8 +168,8 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun
## Extension points
- Material refresh/rebuild can become a separate finalizer consuming the Mesh
returned here and storing its result in `M2MeshResourceCacheState`.
- `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.
@@ -178,7 +179,7 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun
|---|---|---|---|
| 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 | Existing loader-owned | Classifier/material regressions | Separate finalizer extraction next |
| Material refresh/rebuild finalization | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
## Known gaps and risks
@@ -193,7 +194,8 @@ normalized M2 path. Its verifier measures traversal time and temporary Node coun
| 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, preparation, caching, missing state and materialization |
| `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 |
+222
View File
@@ -0,0 +1,222 @@
# 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[M2Loader raw Dictionary]
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 | Loader raw-file boundary | 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[Loader supplies raw M2 data]
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 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[FileAccess / ClassDB M2Loader]
Loader --> Cache[M2MeshResourceCacheState]
Finalizer -. no dependency .-> Raw
Finalizer -. no dependency .-> Cache
```
## Ownership, threading and resources
- Renderer main thread executes metadata changes and M2Builder work.
- Loader owns raw M2 file existence/load calls and supplies value data.
- 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
- Raw M2 file loading may later move behind a dedicated repository 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 | Existing loader-owned | Source boundary | Separate repository extraction optional |
## 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/scenes/streaming/streaming_world_loader.gd` | Raw-file I/O 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)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
@@ -29,13 +29,13 @@ runtime-cache lifetime.
```mermaid
flowchart LR
Cache[Stale cached M2 Mesh] --> Loader[StreamingWorldLoader]
Loader --> Raw[M2Loader raw Dictionary]
Cache[Stale cached M2 Mesh] --> Finalizer[M2RuntimeMeshFinalizer]
Loader[StreamingWorldLoader] --> Raw[M2Loader raw Dictionary]
Raw --> Classifier[M2RuntimeMeshRebuildClassifier]
Classifier --> Decision[Memoized rebuild boolean]
Decision --> Loader
Loader -->|true| Builder[M2Builder rebuild]
Loader -->|false| Refresh[Mark material refresh version]
Decision --> Finalizer
Finalizer -->|true| Builder[M2Builder rebuild]
Finalizer -->|false| Refresh[Mark material refresh version]
```
Allowed dependencies are Dictionary/Array, `PackedInt32Array`, `Vector4` and
@@ -57,7 +57,7 @@ WorkerThreadPool, Mesh/Node/RID ownership and other layers are forbidden.
|---|---|---|---|---|---|
| Input | Normalized M2 relative path | Loader cache lookup | Classifier cache key | String value copied | Until clear |
| Input | Raw M2 batches/transforms/combos Dictionary | Native `M2Loader` adapter | Classifier | Borrowed; not mutated | First classification call per path |
| Output | Rebuild-required boolean | Classifier | Loader material-refresh adapter | Scalar | Memoized until clear |
| 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.
@@ -103,16 +103,18 @@ stateDiagram-v2
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->>Classifier: needs_runtime_mesh_rebuild(path, data)
Classifier-->>Loader: memoized boolean
Loader->>Finalizer: finalize_mesh(path, mesh, data, dir)
Finalizer->>Classifier: needs_runtime_mesh_rebuild(path, data)
Classifier-->>Finalizer: memoized boolean
alt rebuild required
Loader->>Builder: rebuild mesh from raw data
Finalizer->>Builder: rebuild mesh from raw data
else no rebuild required
Loader->>Loader: stamp material refresh version
Finalizer->>Finalizer: stamp material refresh version
end
```
@@ -120,7 +122,8 @@ sequenceDiagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader[StreamingWorldLoader] --> Finalizer[M2RuntimeMeshFinalizer]
Finalizer --> Classifier[M2RuntimeMeshRebuildClassifier]
Loader --> Native[M2Loader]
Loader --> Builder[M2Builder]
Loader --> Cache[M2 Mesh/Scene caches]
@@ -132,8 +135,8 @@ flowchart TB
## Ownership, threading and resources
- The classifier owns only normalized-path boolean entries.
- The loader owns raw loading, cache/resource selection, Mesh replacement and
material refresh metadata.
- 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.
@@ -154,7 +157,7 @@ flowchart TB
|---|---|---|---|---|
| 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 |
| `M2_MATERIAL_REFRESH_VERSION` | Loader constant `2` | All | Code version | Gates whether classifier is called |
| `MATERIAL_REFRESH_VERSION` | Finalizer constant `2` | All | Code version | Gates whether classifier is called |
## Persistence, cache and migration
@@ -193,7 +196,7 @@ reset/shutdown sites. No persisted cache version or rebuild instruction changes.
| 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 |
| M2 mesh loading/finalization | Existing loader-owned | Material/shutdown regressions | Separate state extraction pending |
| Runtime Mesh finalization | Implemented extraction | Finalizer transition/rebuild fixtures | Asset-backed material comparison pending |
## Known gaps and risks
+12 -6
View File
@@ -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-m2-mesh-resource-extractor`, 2026-07-17 |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-mesh-finalizer`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -136,6 +136,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 |
| `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 |
@@ -370,17 +371,20 @@ sequenceDiagram
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. The loader retains raw M2 loading,
refresh-version checks, Mesh rebuild/adoption and both historical clear sites.
for billboard/UV-rotation material refresh and is composed by the runtime Mesh
finalizer. The loader retains raw M2 loading and returned-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 material
refresh/rebuild, missing/prototype state and materialization.
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,
runtime preparation, cache/missing adoption and materialization.
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.
- 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
@@ -585,6 +589,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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/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 |
@@ -603,6 +608,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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_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,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
+11 -29
View File
@@ -60,8 +60,8 @@ 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_REBUILD_CLASSIFIER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_rebuild_classifier.gd"
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
)
const M2_MESH_LOAD_PIPELINE_STATE_SCRIPT := preload(
"res://src/render/m2/m2_mesh_load_pipeline_state.gd"
@@ -78,7 +78,6 @@ const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/rend
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
@@ -259,9 +258,7 @@ 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_runtime_mesh_rebuild_classifier := (
M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()
)
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_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()
@@ -2355,7 +2352,7 @@ func _wait_for_tile_tasks() -> void:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_pipeline_state.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
_m2_runtime_mesh_finalizer.clear()
for pending in _m2_animation_load_requests.values():
var path: String = String(pending.get("path", ""))
@@ -3005,7 +3002,7 @@ func _clear_streamed_world() -> void:
_m2_build_queue.clear()
_m2_unique_placement_registry.clear()
_m2_mesh_load_pipeline_state.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
_m2_runtime_mesh_finalizer.clear()
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
_wmo_build_jobs.clear()
@@ -4654,19 +4651,15 @@ 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_runtime_mesh_rebuild_classifier.needs_runtime_mesh_rebuild(
return _m2_runtime_mesh_finalizer.finalize_mesh(
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
mesh,
data,
extracted_dir
)
func _prepare_m2_node_for_runtime(normalized_rel: String, root: Node3D) -> void:
@@ -4693,17 +4686,6 @@ func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
return data if data is Dictionary else {}
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 := _m2_mesh_resource_extractor.find_first_mesh_in_subtree(prototype)
prototype.free()
return rebuilt
func _get_or_load_m2_mesh(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
@@ -117,8 +117,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
)
_expect_equal(
loader_source.count("_m2_mesh_resource_extractor.find_first_mesh_in_subtree("),
2,
"rebuild and synchronous fallback delegate",
1,
"synchronous fallback delegates",
failures
)
for retained_loader_rule in [
@@ -0,0 +1,217 @@
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 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 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 [
"func _load_m2_raw_data_for_refresh(",
"FileAccess.file_exists(abs_path)",
"ClassDB.instantiate(\"M2Loader\")",
"loader.call(\"load_m2\", abs_path)",
"_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 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
@@ -7,6 +7,7 @@ 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"
@@ -152,16 +153,17 @@ func _verify_memoization_clear_and_diagnostics(failures: Array[String]) -> void:
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(
loader_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"loader composes classifier",
finalizer_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"runtime Mesh finalizer composes classifier",
failures
)
_expect_equal(
loader_source.count("_m2_runtime_mesh_rebuild_classifier.clear()"),
loader_source.count("_m2_runtime_mesh_finalizer.clear()"),
2,
"loader retains two classifier clear sites",
"loader retains two finalizer clear sites",
failures
)
_expect_false(