Compare commits

..

5 Commits

11 changed files with 735 additions and 55 deletions
+17
View File
@@ -35,6 +35,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `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/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.
@@ -1039,6 +1040,22 @@ $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 loads raw M2 data, checks
`M2_MATERIAL_REFRESH_VERSION`, calls `M2Builder`, adopts/falls back to Meshes
and owns ResourceLoader/finalization budgets.
- 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 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization,
@@ -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 -->
+1
View File
@@ -21,6 +21,7 @@
| 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) |
| 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) |
@@ -0,0 +1,221 @@
# 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] --> Loader[StreamingWorldLoader]
Loader --> 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]
```
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 | 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 | 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 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
alt rebuild required
Loader->>Builder: rebuild mesh from raw data
else no rebuild required
Loader->>Loader: stamp material refresh version
end
```
## Dependency diagram
```mermaid
flowchart TB
Loader[StreamingWorldLoader] --> 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 loader owns raw loading, cache/resource selection, Mesh replacement and
material refresh metadata.
- 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 |
| `M2_MATERIAL_REFRESH_VERSION` | Loader 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 |
| M2 mesh loading/finalization | Existing loader-owned | Material/shutdown regressions | Separate state extraction 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 -1
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-adt-water-scene-finalizer`, 2026-07-17 |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-runtime-rebuild-classifier`, 2026-07-17 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose
@@ -132,6 +132,7 @@ 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 |
| `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 |
@@ -365,6 +366,9 @@ sequenceDiagram
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. The loader retains raw M2 loading,
refresh-version checks, Mesh rebuild/adoption and both historical clear sites.
- 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
@@ -565,6 +569,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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/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 |
@@ -579,6 +584,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `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_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,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
+11 -51
View File
@@ -60,6 +60,9 @@ 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 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")
@@ -247,10 +250,12 @@ 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_mesh_cache: Dictionary = {}
var _m2_mesh_load_requests: Dictionary = {}
var _m2_mesh_finalize_queue: Array = []
var _m2_runtime_rebuild_required_cache: Dictionary = {}
var _m2_animation_load_requests: Dictionary = {}
var _m2_animation_finalize_queue: Array = []
var _wmo_build_jobs: Dictionary = {}
@@ -2342,7 +2347,7 @@ func _wait_for_tile_tasks() -> void:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
_m2_runtime_rebuild_required_cache.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
for pending in _m2_animation_load_requests.values():
var path: String = String(pending.get("path", ""))
@@ -2993,7 +2998,7 @@ func _clear_streamed_world() -> void:
_m2_unique_placement_registry.clear()
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
_m2_runtime_rebuild_required_cache.clear()
_m2_runtime_mesh_rebuild_classifier.clear()
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
_wmo_build_jobs.clear()
@@ -4645,7 +4650,9 @@ func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
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):
if data.is_empty() or not _m2_runtime_mesh_rebuild_classifier.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)
@@ -4679,53 +4686,6 @@ func _load_m2_raw_data_for_refresh(normalized_rel: String) -> Dictionary:
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
@@ -0,0 +1,248 @@
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 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 loader_source := FileAccess.get_file_as_string(LOADER_PATH)
_expect_true(
loader_source.contains("M2_RUNTIME_MESH_REBUILD_CLASSIFIER_SCRIPT.new()"),
"loader composes classifier",
failures
)
_expect_equal(
loader_source.count("_m2_runtime_mesh_rebuild_classifier.clear()"),
2,
"loader retains two classifier 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
+20 -3
View File
@@ -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`, `874fd0f`;
- 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`;
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`,
@@ -43,7 +43,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`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-SCENE-FINALIZER-001`
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-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
@@ -126,6 +127,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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.
M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were
@@ -186,6 +195,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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.
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;
@@ -210,6 +224,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
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,
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
@@ -223,7 +238,9 @@ 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 is separated,
but raw loading, resource/queue transitions and mesh finalization remain in
the loader; 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;