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/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_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/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. - `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/adt_builder.gd` - сборка ADT terrain, control splat материалов и MH2O liquids.
- `addons/mpq_extractor/loaders/m2_builder.gd` - сборка статического M2 mesh/material. - `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. - Cache formats, quality profiles, batching output and visible rules are unchanged.
Asset-backed p95/p99 and spatial-cell batching evidence remain pending. 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 ## 2026-07-17 WMO Placement Resolver Extraction
- `WmoPlacementResolver` now owns lowercase/slash cache-key normalization, - `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 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 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 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 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 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) | | 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 | | 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 | | 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 | | 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 | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## 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 | | `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 | | `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 | | `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 | | `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 | | `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 | | `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` stale tile/path/root checks and `water_loaded` timing. `AdtWaterSceneFinalizer`
performs the existing ADTBuilder call, tile attach and optional recursive performs the existing ADTBuilder call, tile attach and optional recursive
Editor ownership on the main thread without retaining the generated subtree. 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; - Rendered-ground query results and diagnostic snapshots are caller-owned values;
the facade never returns Mesh, Node, tile-state or queue references. the facade never returns Mesh, Node, tile-state or queue references.
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay - Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
@@ -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/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_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/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_planner.gd` | Scene-free wanted/retained ADT target calculation |
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy | | `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets | | `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
@@ -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_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_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_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_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression | | `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields | | `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
@@ -0,0 +1,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( const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
"res://src/render/m2/m2_build_batch_planner.gd" "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_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd") const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd") const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
@@ -247,10 +250,12 @@ var _m2_unique_placement_registry := (
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new() var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new() var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_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_cache: Dictionary = {}
var _m2_mesh_load_requests: Dictionary = {} var _m2_mesh_load_requests: Dictionary = {}
var _m2_mesh_finalize_queue: Array = [] var _m2_mesh_finalize_queue: Array = []
var _m2_runtime_rebuild_required_cache: Dictionary = {}
var _m2_animation_load_requests: Dictionary = {} var _m2_animation_load_requests: Dictionary = {}
var _m2_animation_finalize_queue: Array = [] var _m2_animation_finalize_queue: Array = []
var _wmo_build_jobs: Dictionary = {} var _wmo_build_jobs: Dictionary = {}
@@ -2342,7 +2347,7 @@ func _wait_for_tile_tasks() -> void:
ResourceLoader.load_threaded_get(path) ResourceLoader.load_threaded_get(path)
_m2_mesh_load_requests.clear() _m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.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(): for pending in _m2_animation_load_requests.values():
var path: String = String(pending.get("path", "")) var path: String = String(pending.get("path", ""))
@@ -2993,7 +2998,7 @@ func _clear_streamed_world() -> void:
_m2_unique_placement_registry.clear() _m2_unique_placement_registry.clear()
_m2_mesh_load_requests.clear() _m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.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_load_requests.clear()
_m2_animation_finalize_queue.clear() _m2_animation_finalize_queue.clear()
_wmo_build_jobs.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: if int(mesh.get_meta("wow_m2_material_refresh_version", 0)) >= M2_MATERIAL_REFRESH_VERSION:
return mesh return mesh
var data := _load_m2_raw_data_for_refresh(normalized_rel) 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) mesh.set_meta("wow_m2_material_refresh_version", M2_MATERIAL_REFRESH_VERSION)
return mesh return mesh
var rebuilt := _rebuild_m2_mesh_from_data(data) 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 {} 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: func _rebuild_m2_mesh_from_data(data: Dictionary) -> Mesh:
if data.is_empty(): if data.is_empty():
return null 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 ## Evidence
- Date: 2026-07-17 - 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`, 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-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-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-RENDER-RESOURCE-CACHE-001`,
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`, `M03-RND-WMO-SCENE-RESOURCE-CACHE-001`,
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`, `M03-RND-ADT-WATER-LOAD-PIPELINE-001`,
`M03-RND-ADT-WATER-SCENE-FINALIZER-001` `M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown - Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates. headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner - 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 retained `7/7` plans. Post-merge finalizer (`0.025ms`), water pipeline
(`98.398ms`), shutdown, materials, facade, internal-access `36`, `7/7` (`98.398ms`), shutdown, materials, facade, internal-access `36`, `7/7`
manifest, documentation and coordination gates remained green. 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 M02 terrain-query regression
remained green (13 pre-existing expired M00 claim warnings). remained green (13 pre-existing expired M00 claim warnings).
- Fidelity comparison: all 16 historical operation limits and drain sites were - 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`, finalizer. It invokes the unchanged `ADTBuilder.build_tile_water_scene`,
attaches the returned root once and applies the existing optional recursive attaches the returned root once and applies the existing optional recursive
Editor ownership; tile subtree lifetime and dry-root cleanup are unchanged. 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. 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/ - Changed files: scene-free scheduler and verifier; streamer budget composition/
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes; 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/ ADT water pending/task/result pipeline state and loader enqueue/start/publish/
drain/release/reset/shutdown/metrics adapters, drain/release/reset/shutdown/metrics adapters,
stateless ADT water scene finalizer and both loader build/attach 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. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: worker concurrency and stale-result validation remain - Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt 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 private Azeroth data was absent for the asset-backed ADT placement probe; M2
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
and boundary traversal p95/p99 remain pending; path grouping is not yet 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 WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
remain in the loader; remain in the loader;
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending; asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;