Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 8ec13d6018 | |||
| 38f831ea2f | |||
| 0544cf22df | |||
| 19ca8f48e8 | |||
| 324e64dd6e |
@@ -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 -->
|
||||||
@@ -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,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
|
||||||
@@ -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
|
||||||
@@ -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;
|
||||||
|
|||||||
Reference in New Issue
Block a user