Compare commits
5 Commits
1b6c46870d
...
c34e538dcf
| Author | SHA1 | Date | |
|---|---|---|---|
| c34e538dcf | |||
| 396be5e69d | |||
| 1792938567 | |||
| 0a09d7bc1e | |||
| 91f0ba8433 |
@@ -990,6 +990,19 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- M2/ADT cache formats, transforms, profiles and visible rules are unchanged.
|
||||
Asset-backed cross-boundary p95/p99 and visual acceptance remain required.
|
||||
|
||||
## 2026-07-16 M2 Placement Transform Resolver Extraction
|
||||
|
||||
- `M2PlacementTransformResolver` now owns the pure ADT M2 basis and local
|
||||
origin-compensation formulas used by worker grouping, placeholders and direct
|
||||
instance creation.
|
||||
- Regular models retain `Basis.from_euler`; `ElwynnCliffRock01/02` retain their
|
||||
narrow shell correction; `NewWaterfall` and `ElwynnTallWaterfall01` retain
|
||||
calibrated world yaw, local fall-axis twist and tall-sheet anchor compensation.
|
||||
- Path separator/case/MDX handling, constants and caller-specific final scale
|
||||
clamps are unchanged. The resolver owns no Node, task, cache or RID.
|
||||
- Cache formats, placement positions and visible rules are unchanged by the
|
||||
extraction. Asset-backed visual recheck and general placement parity remain gaps.
|
||||
|
||||
## 2026-07-16 World Environment Snapshot Facade
|
||||
|
||||
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
|
||||
|
||||
@@ -0,0 +1,107 @@
|
||||
# M03-RND-M2-PLACEMENT-TRANSFORM-001 — M2 placement transform resolver
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-M2-PLACEMENT-TRANSFORM-001:sindo-main-codex:2026-07-19 -->
|
||||
|
||||
## Ownership
|
||||
|
||||
- Target: M03
|
||||
- Program: RND
|
||||
- Owner/Agent ID: sindo-main-codex
|
||||
- Branch: `work/sindo-main-codex/m03-m2-placement-transform`
|
||||
- Lease expires UTC: 2026-07-19
|
||||
- Integrator: M03 milestone integrator
|
||||
|
||||
## Outcome
|
||||
|
||||
Extract pure ADT M2 placement basis/origin-offset calculation, including the
|
||||
existing waterfall and Elwynn cliff-rock calibrations, into a scene-free service.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change coordinate conversion, placement positions, rotations or scale clamps.
|
||||
- Generalize model-specific corrections or add new allowlisted models.
|
||||
- Move grouping/tasks, MultiMesh/Node/Mesh/material/RID creation or caches.
|
||||
- Change M2 animation/material behavior, cache versions or renderer profiles.
|
||||
- Claim build-12340 placement parity beyond existing fixtures.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/m2/m2_placement_transform_resolver.gd`,
|
||||
`src/tools/verify_m2_placement_transform_resolver.gd`,
|
||||
`docs/modules/m2-placement-transform-resolver.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 resources, caches and
|
||||
proprietary renderer corpus
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Input: ADT-loader Godot Euler rotation, normalized/original M2 relative path,
|
||||
and raw placement scale where origin compensation is requested
|
||||
- Output: `Basis` and local origin-offset `Vector3`; no retained state
|
||||
- Regular models use `Basis.from_euler`; two cliff rocks retain narrow YZX-style
|
||||
shell correction; two waterfall sheets retain yaw/twist/anchor compensation
|
||||
- Scale clamps remain caller-owned for final basis and resolver-owned at `0.0001`
|
||||
only for historical waterfall anchor compensation
|
||||
- Cache/schema/coordinate versions: unchanged
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: current master `1b6c468`
|
||||
- Blocks: pure M2 grouping extraction and broader M2 service decomposition
|
||||
- External state: none; tests use synthetic rotations/paths and pinned constants
|
||||
|
||||
## Verification
|
||||
|
||||
- Commands: dedicated regular/path-normalization/cliffrock/waterfall/twist-anchor/
|
||||
scale and bounded timing verifier; three loader call-site/source boundaries;
|
||||
M2 unique/dedupe plus facade/terrain/internal-access/manifest/shutdown gates
|
||||
- Fidelity evidence: exact formulas/constants and model allowlists moved unchanged
|
||||
- Performance budget: allocation-free scalar/Basis math per placement aside from
|
||||
path normalization; no Node, Resource, RID, task, cache or file access
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline resolver API documentation
|
||||
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
|
||||
performance, fidelity limitations and source map
|
||||
- World renderer/module registry/`RENDER.md` updates
|
||||
|
||||
## Simplicity and naming
|
||||
|
||||
- Important name: `M2PlacementTransformResolver`
|
||||
- Simplest solution: stateless service with basis and origin-offset queries
|
||||
- Rejected complexity: transform strategy hierarchy, generic correction registry,
|
||||
Resources, data-driven override format or callback injection
|
||||
- Unavoidable complexity: four measured model-specific exceptions remain explicit
|
||||
- Measured optimization evidence: bounded synthetic transform loop
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: resolver extracted, three call sites migrated, contracts and docs verified
|
||||
- Next: pure M2 grouping extraction and broader M2 service decomposition
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PLACEMENT-TRANSFORM-001:0a09d7b -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-PLACEMENT-TRANSFORM-001:396be5e -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `0a09d7b` (`refactor(M03): extract M2 placement transform resolver`)
|
||||
- Results: dedicated formula/source/timing contract PASS (`cases=9`,
|
||||
`iterations=10000`, `elapsed_ms=28.732`); cold editor parse PASS;
|
||||
documentation gate PASS (`module_specs=16`, `required_files=7`);
|
||||
coordination gate PASS with 23 pre-existing expired M00/M01 warnings; related
|
||||
M2 unique/dedupe, terrain cache/LOD/queue, facade, internal-access, manifest,
|
||||
shutdown, scheduler, streaming focus/planner and coordinate-boundary suites
|
||||
PASS; renderer checkpoint dry-run PASS `7/7`.
|
||||
- Remaining risks: no private asset-backed visual recheck in this package;
|
||||
general ADT M2 placement parity and asset-backed p95/p99 remain pending.
|
||||
- Documentation updated: inline public API; new
|
||||
`docs/modules/m2-placement-transform-resolver.md`; renderer module registry,
|
||||
`docs/modules/world-renderer.md` and `RENDER.md`.
|
||||
- Integration: accepted in master merge `396be5e`; post-merge resolver
|
||||
(`cases=9`, `iterations=10000`, `elapsed_ms=27.409`), M2 registry,
|
||||
internal-access, facade, manifest, documentation and coordination gates passed.
|
||||
@@ -16,6 +16,7 @@
|
||||
| Terrain chunk LOD planner | Implemented extraction | [`terrain-chunk-lod-planner.md`](terrain-chunk-lod-planner.md) |
|
||||
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-planner.md) |
|
||||
| M2 unique placement registry | Implemented extraction | [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) |
|
||||
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
|
||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
# M2 Placement Transform Resolver
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-TRANSFORM-001` |
|
||||
| Owners | Pure ADT M2 placement basis and model-specific origin compensation |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
|
||||
| Profiles/capabilities | Existing static M2 paths; four calibrated model exceptions |
|
||||
|
||||
## Purpose
|
||||
|
||||
Resolve the unscaled Godot-space orientation and optional local origin offset
|
||||
used by ADT M2 worker grouping, placeholder rendering and instance creation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Convert WoW/server/ADT coordinates or own placement positions.
|
||||
- Apply final scale clamps, build transforms, group placements or create Nodes.
|
||||
- Add, generalize or configure model-specific corrections.
|
||||
- Change M2 materials, animation, caches, visibility or quality profiles.
|
||||
- Claim general build-12340 placement parity.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver]
|
||||
Resolver --> Basis[Unscaled Basis]
|
||||
Resolver --> Offset[Local origin offset]
|
||||
Basis --> Group[Worker grouping]
|
||||
Basis --> Placeholder[Placeholder MultiMesh path]
|
||||
Basis --> Instance[Instance path]
|
||||
Offset --> Group
|
||||
Offset --> Placeholder
|
||||
Offset --> Instance
|
||||
Group --> Loader[StreamingWorldLoader-owned queues/build]
|
||||
```
|
||||
|
||||
Allowed dependencies are `Vector3`, `Basis`, scalar math and path-string
|
||||
normalization. Node, SceneTree, RenderingServer, ResourceLoader, tasks, caches,
|
||||
files, gameplay, network and editor UI are forbidden.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `resolve_basis(rotation_radians, relative_m2_path)` | Pure query | Return unscaled regular or calibrated placement orientation | Worker/main thread; call-local result | Unknown paths use `Basis.from_euler` |
|
||||
| `resolve_origin_offset(rotation_radians, relative_m2_path, scale_value)` | Pure query | Compensate the measured tall-waterfall twist anchor | Worker/main thread; call-local result | Regular/unanchored paths return zero; scale clamps to `0.0001` for compensation |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call |
|
||||
| Input | Relative M2 path | ADT MMDX/name table | Resolver allowlist | Caller string | One call |
|
||||
| Input | Raw placement scale | ADT placement record | Offset compensation | Scalar copy | One call |
|
||||
| Output | Unscaled `Basis` | Resolver | Group/placeholder/instance adapter | Value copy | One call |
|
||||
| Output | Local `Vector3` offset | Resolver | Transform origin adapter | Value copy | One call |
|
||||
|
||||
Callers preserve their historical basis scale minima: grouping/instance use
|
||||
`0.0001`, while placeholders use `0.01`.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
BasisCall[resolve_basis] --> ModelName[Normalize separators/case/basename]
|
||||
ModelName --> Waterfall{Waterfall sheet?}
|
||||
Waterfall -->|yes| WaterfallBasis[+90 degree world yaw * Euler * local-axis twist]
|
||||
Waterfall -->|no| Cliff{Elwynn cliff rock?}
|
||||
Cliff -->|yes| CliffBasis[Corrected Y/BACK/RIGHT composition]
|
||||
Cliff -->|no| Regular[Basis.from_euler]
|
||||
OffsetCall[resolve_origin_offset] --> Anchored{Tall waterfall anchor?}
|
||||
Anchored -->|no| Zero[Vector3.ZERO]
|
||||
Anchored -->|yes| Compensation[(base anchor - twisted anchor) * clamped scale]
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
The resolver is stateless. It may be shared between the main thread and worker
|
||||
calls because each result is computed solely from value inputs. Construction,
|
||||
world reset and shutdown require no clear, cancellation or resource release.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Resolver as M2PlacementTransformResolver
|
||||
participant Consumer as Group/placeholder/instance path
|
||||
Loader->>Resolver: resolve_basis(rotation, path)
|
||||
Resolver-->>Loader: unscaled Basis
|
||||
Loader->>Resolver: resolve_origin_offset(rotation, path, scale)
|
||||
Resolver-->>Loader: local offset
|
||||
Loader->>Consumer: compose historical scaled Transform3D
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The resolver retains no mutable state or engine resources.
|
||||
- The loader owns placement records, final transforms, tasks, queues, caches,
|
||||
MultiMesh/Node/Mesh/material/RID creation and cleanup.
|
||||
- Pure methods are safe for current worker grouping and main-thread consumers.
|
||||
- Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Empty/unknown path | Allowlist miss | Regular Euler basis and zero offset | Contract fixture | Expected fallback |
|
||||
| Mixed case/backslashes/MDX | Normalization | Same calibrated model match | Contract fixture | None required |
|
||||
| Zero/negative scale for anchored offset | Numeric clamp | Use `0.0001` compensation scale | Contract fixture | Caller may supply valid ADT scale later |
|
||||
| World reset/shutdown | No retained state | No action | Existing shutdown verifier | New calls remain independent |
|
||||
|
||||
There is no asynchronous operation or cancellation inside the resolver.
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The resolver introduces no settings. The preserved constants and allowlists are:
|
||||
|
||||
- world yaw `PI` for `ElwynnCliffRock01/02`;
|
||||
- world yaw `PI/2` for `NewWaterfall` and `ElwynnTallWaterfall01`;
|
||||
- measured local axes/twist signs for both waterfall sheets;
|
||||
- measured tall-waterfall anchor `(-2.667799, 89.62273, 0.00129)`.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No persistence, schema or cache format participates. Existing ADT/M2 cache
|
||||
versions and rebuild instructions remain unchanged.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The resolver emits no logs. The contract verifier compares formulas/constants,
|
||||
checks all three loader consumers and records bounded synthetic timing. Visual
|
||||
and asset-backed evidence remains in the renderer checkpoint workflow.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_m2_placement_transform_resolver.gd`: regular fallback, path
|
||||
normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale
|
||||
compensation, three loader adapters, source boundary and 10,000-pair timing.
|
||||
- M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown,
|
||||
scheduler, streaming and coordinate regressions remain required.
|
||||
- Fidelity evidence is exact extraction of existing calibrated formulas. The
|
||||
private asset/camera comparison that motivated them is not reproduced here.
|
||||
|
||||
## Extension points
|
||||
|
||||
- The next package may compose this resolver into a pure placement grouper.
|
||||
- New model corrections require separate measured fidelity evidence; this module
|
||||
is not a generic override registry.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Consistent transform rules across three M2 paths | Implemented | Formula/source/timing contract | Asset-backed visual recheck pending |
|
||||
| General ADT M2 placement parity | Partial | Existing checkpoint notes | Broader fixture coverage required |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Four model-specific exceptions are empirical compatibility rules.
|
||||
- Most placements use raw Godot Euler behavior; independent client-wide
|
||||
transform validation is incomplete.
|
||||
- Callers intentionally retain different final scale clamps.
|
||||
- Grouping, build, cache and render ownership remain in the streamer.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Three adapters and final Transform3D/resource ownership |
|
||||
| `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.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)
|
||||
@@ -5,9 +5,9 @@
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| 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-RND-M2-UNIQUE-REGISTRY-001` |
|
||||
| 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 unique/transform packages |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-unique-registry`, 2026-07-16 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -48,6 +48,8 @@ flowchart LR
|
||||
ChunkQueue --> Loader
|
||||
Loader --> M2Registry[M2UniquePlacementRegistry]
|
||||
M2Registry --> Loader
|
||||
Loader --> M2Transform[M2PlacementTransformResolver]
|
||||
M2Transform --> Loader
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
@@ -115,6 +117,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `TerrainChunkLodPlanner.plan` | Internal pure terrain query | Maps populated parsed chunk indices to desired LOD 0/1/2 | Synchronous per target refresh | Invalid/disabled input returns empty for existing tile-LOD fallback |
|
||||
| `TerrainChunkGeometryQueuePlanner.plan` | Internal pure terrain query | Produces chunk create/remove requests from current and desired state | Synchronous per queue rebuild | Invalid focus returns empty arrays |
|
||||
| `M2UniquePlacementRegistry.reserve/release/clear` | Internal M2 service | Owns positive cross-tile ADT placement-ID reservations | Renderer thread/map session | Invalid/unkeyed/non-owner inputs preserve documented fallback |
|
||||
| `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 |
|
||||
|
||||
Публичным contract не считаются `StreamingWorldLoader` methods/properties,
|
||||
внутренние dictionaries, queues, job records и generated node names. Scene-owned
|
||||
@@ -140,6 +143,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal plan | Parsed chunks, typed focus and immutable chunk-LOD policy | Loader / `TerrainChunkLodPlanner` | Loader desired state | Fresh dictionary; no retained resources | One target refresh |
|
||||
| Internal plan | Current/desired chunk state and typed focus | Loader / `TerrainChunkGeometryQueuePlanner` | Loader chunk queues | Fresh request arrays; no retained resources | One queue rebuild |
|
||||
| Internal registry | Tile key and M2 placement unique IDs | Loader / `M2UniquePlacementRegistry` | Filtered grouping input and tile retry state | Registry-owned strings; fresh result arrays | Map session |
|
||||
| Internal transform | Rotation/path/scale | Loader / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
|
||||
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame |
|
||||
| Output | Terrain/M2/WMO/liquid instances | Loader/builders | Godot world/renderer | Loader/world owner | Main-thread attach |
|
||||
| Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
|
||||
@@ -185,6 +189,7 @@ flowchart TD
|
||||
ChunkQueue --> B
|
||||
R --> M2Registry[M2UniquePlacementRegistry reserve]
|
||||
M2Registry --> M2
|
||||
M2Transform[M2PlacementTransformResolver] --> M2
|
||||
R --> B[RenderBudgetScheduler permits]
|
||||
B --> Terrain[Terrain attach/upgrade]
|
||||
B --> M2[M2 group/MultiMesh attach]
|
||||
@@ -299,6 +304,8 @@ sequenceDiagram
|
||||
- `M2UniquePlacementRegistry` owns only positive unique-key to tile-key strings.
|
||||
The loader retains owned/skipped tile arrays, candidate retry, grouping/build
|
||||
tasks, caches and all MultiMesh/Node/Mesh/material/RID side effects.
|
||||
- `M2PlacementTransformResolver` is stateless and retains no resources. The
|
||||
loader owns final transforms and every grouping/build/render side effect.
|
||||
- Rendered-ground query results and diagnostic snapshots are caller-owned values;
|
||||
the facade never returns Mesh, Node, tile-state or queue references.
|
||||
- Loaded-mesh ground sampling is renderer diagnostics, not authoritative gameplay
|
||||
@@ -384,6 +391,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
- M2 unique placement contract: unkeyed pass-through, within-call/cross-tile
|
||||
dedupe, owner release/retry, clear, detached diagnostics, loader boundary and
|
||||
bounded reserve/release timing without a world scene.
|
||||
- M2 placement transform contract: regular/cliffrock/waterfall formulas, path
|
||||
normalization, scale compensation, all three consumers and bounded timing.
|
||||
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
|
||||
priority, independent lanes, frame reset, invalid limits, terminal cancellation,
|
||||
dependency boundaries and bounded permit timing without loading a world scene.
|
||||
@@ -414,6 +423,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
|
||||
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
|
||||
| M2 unique placement registry | Implemented extraction | Scene-free ownership/lifecycle/timing contract and historical `uid:11785` smoke | Group/build/tasks/finalization and asset-backed p95/p99 remain pending |
|
||||
| M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
|
||||
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
|
||||
|
||||
@@ -0,0 +1,110 @@
|
||||
class_name M2PlacementTransformResolver
|
||||
extends RefCounted
|
||||
|
||||
## Stateless ADT M2 placement orientation and origin-correction rules.
|
||||
|
||||
const CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
|
||||
|
||||
## Resolves the unscaled Godot-space basis for one ADT M2 placement.
|
||||
func resolve_basis(rotation_radians: Vector3, relative_m2_path: String = "") -> Basis:
|
||||
if not _is_waterfall_path(relative_m2_path):
|
||||
return _resolve_regular_basis(rotation_radians, relative_m2_path)
|
||||
|
||||
var placement_basis := Basis.from_euler(rotation_radians)
|
||||
return (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* placement_basis
|
||||
* Basis(
|
||||
_resolve_waterfall_local_fall_axis(relative_m2_path),
|
||||
_resolve_waterfall_sheet_twist(relative_m2_path)
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
## Resolves local origin compensation for a twisted waterfall sheet. Regular
|
||||
## models and waterfall models without a measured anchor return zero.
|
||||
func resolve_origin_offset(
|
||||
rotation_radians: Vector3,
|
||||
relative_m2_path: String,
|
||||
scale_value: float
|
||||
) -> Vector3:
|
||||
if not _is_waterfall_path(relative_m2_path):
|
||||
return Vector3.ZERO
|
||||
var twist_radians := _resolve_waterfall_sheet_twist(relative_m2_path)
|
||||
if is_zero_approx(twist_radians):
|
||||
return Vector3.ZERO
|
||||
var anchor_local_position := _resolve_waterfall_sheet_twist_anchor(relative_m2_path)
|
||||
if anchor_local_position == Vector3.ZERO:
|
||||
return Vector3.ZERO
|
||||
var placement_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* Basis.from_euler(rotation_radians)
|
||||
)
|
||||
var twisted_basis := placement_basis * Basis(
|
||||
_resolve_waterfall_local_fall_axis(relative_m2_path),
|
||||
twist_radians
|
||||
)
|
||||
return (
|
||||
placement_basis * anchor_local_position
|
||||
- twisted_basis * anchor_local_position
|
||||
) * maxf(scale_value, 0.0001)
|
||||
|
||||
|
||||
func _resolve_regular_basis(rotation_radians: Vector3, relative_m2_path: String) -> Basis:
|
||||
if not _is_elwynn_cliffrock_path(relative_m2_path):
|
||||
return Basis.from_euler(rotation_radians)
|
||||
return (
|
||||
Basis(Vector3.UP, rotation_radians.y + CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rotation_radians.x)
|
||||
* Basis(Vector3.RIGHT, -rotation_radians.z)
|
||||
)
|
||||
|
||||
|
||||
func _is_waterfall_path(relative_m2_path: String) -> bool:
|
||||
var model_name := _normalized_model_name(relative_m2_path)
|
||||
return model_name == "newwaterfall" or model_name == "elwynntallwaterfall01"
|
||||
|
||||
|
||||
func _is_elwynn_cliffrock_path(relative_m2_path: String) -> bool:
|
||||
var model_name := _normalized_model_name(relative_m2_path)
|
||||
return model_name == "elwynncliffrock01" or model_name == "elwynncliffrock02"
|
||||
|
||||
|
||||
func _normalized_model_name(relative_m2_path: String) -> String:
|
||||
return relative_m2_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
|
||||
|
||||
func _resolve_waterfall_local_fall_axis(relative_m2_path: String) -> Vector3:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
|
||||
return Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
return Vector3.UP
|
||||
|
||||
|
||||
func _resolve_waterfall_sheet_twist(relative_m2_path: String) -> float:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if normalized_path.ends_with("newwaterfall.m2") or normalized_path.ends_with("newwaterfall.mdx"):
|
||||
return PI * 0.5
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return -PI * 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
func _resolve_waterfall_sheet_twist_anchor(relative_m2_path: String) -> Vector3:
|
||||
var normalized_path := relative_m2_path.replace("\\", "/").to_lower()
|
||||
if (
|
||||
normalized_path.ends_with("elwynntallwaterfall01.m2")
|
||||
or normalized_path.ends_with("elwynntallwaterfall01.mdx")
|
||||
):
|
||||
return Vector3(-2.667799, 89.62273, 0.00129)
|
||||
return Vector3.ZERO
|
||||
@@ -0,0 +1 @@
|
||||
uid://bcnmp7y8ph3qa
|
||||
@@ -27,6 +27,9 @@ const TERRAIN_CHUNK_GEOMETRY_QUEUE_PLANNER_SCRIPT := preload(
|
||||
const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_unique_placement_registry.gd"
|
||||
)
|
||||
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
|
||||
"res://src/render/m2/m2_placement_transform_resolver.gd"
|
||||
)
|
||||
const STREAMING_TARGET_PLANNER_SCRIPT := preload("res://src/render/streaming/streaming_target_planner.gd")
|
||||
const STREAMING_TARGET_POLICY_SCRIPT := preload("res://src/render/streaming/streaming_target_policy.gd")
|
||||
const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
@@ -39,8 +42,6 @@ const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||
|
||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
const CHUNK_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_CHUNK_SIZE_YARDS
|
||||
const ADT_CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
const ADT_WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
const QUALITY_CUSTOM := "Custom"
|
||||
const QUALITY_PERFORMANCE := "Performance"
|
||||
const QUALITY_BALANCED := "Balanced"
|
||||
@@ -213,6 +214,7 @@ var _m2_build_queue: Array = []
|
||||
var _m2_unique_placement_registry := (
|
||||
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
)
|
||||
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
||||
var _m2_mesh_cache: Dictionary = {}
|
||||
var _m2_mesh_load_requests: Dictionary = {}
|
||||
var _m2_mesh_finalize_queue: Array = []
|
||||
@@ -3978,86 +3980,6 @@ func _rebuild_cached_wmo_material(material: Material) -> Material:
|
||||
return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir)
|
||||
|
||||
|
||||
func _adt_m2_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
|
||||
if not _is_waterfall_m2_path(rel_path):
|
||||
return _adt_m2_regular_placement_basis(rot, rel_path)
|
||||
|
||||
# ADTLoader stores MDDF yaw as raw_yaw - 90 degrees for historical Godot
|
||||
# facing alignment. In our tile-positive ADT world, waterfall models need a
|
||||
# world-space +90 degree yaw to anchor their top to the source pool. Twist
|
||||
# the sheet around its own local fall axis afterwards, so the anchor stays in
|
||||
# place but the alpha plane is not rendered edge-on.
|
||||
var basis := Basis.from_euler(rot)
|
||||
return (
|
||||
Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET)
|
||||
* basis
|
||||
* Basis(_waterfall_local_fall_axis(rel_path), _waterfall_sheet_twist(rel_path))
|
||||
)
|
||||
|
||||
|
||||
func _adt_m2_regular_placement_basis(rot: Vector3, rel_path: String = "") -> Basis:
|
||||
if not _is_elwynn_cliffrock_m2_path(rel_path):
|
||||
return Basis.from_euler(rot)
|
||||
|
||||
# Elwynn cliff rock M2s are open-backed shell meshes. Plain Godot Euler
|
||||
# points their hollow side out of the cliff at waterfall placements. Keep the
|
||||
# correction model-specific so normal props retain the native ADT rotation.
|
||||
return (
|
||||
Basis(Vector3.UP, rot.y + ADT_CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rot.x)
|
||||
* Basis(Vector3.RIGHT, -rot.z)
|
||||
)
|
||||
|
||||
|
||||
func _adt_m2_placement_origin_offset(rot: Vector3, rel_path: String, scale_value: float) -> Vector3:
|
||||
if not _is_waterfall_m2_path(rel_path):
|
||||
return Vector3.ZERO
|
||||
var twist := _waterfall_sheet_twist(rel_path)
|
||||
if is_zero_approx(twist):
|
||||
return Vector3.ZERO
|
||||
var anchor := _waterfall_sheet_twist_anchor(rel_path)
|
||||
if anchor == Vector3.ZERO:
|
||||
return Vector3.ZERO
|
||||
var base := Basis(Vector3.UP, ADT_WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rot)
|
||||
var twisted := base * Basis(_waterfall_local_fall_axis(rel_path), twist)
|
||||
return (base * anchor - twisted * anchor) * maxf(scale_value, 0.0001)
|
||||
|
||||
|
||||
func _is_waterfall_m2_path(rel_path: String) -> bool:
|
||||
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
return name == "newwaterfall" or name == "elwynntallwaterfall01"
|
||||
|
||||
|
||||
func _is_elwynn_cliffrock_m2_path(rel_path: String) -> bool:
|
||||
var name := rel_path.replace("\\", "/").to_lower().get_file().get_basename()
|
||||
return name == "elwynncliffrock01" or name == "elwynncliffrock02"
|
||||
|
||||
|
||||
func _waterfall_local_fall_axis(rel_path: String) -> Vector3:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
|
||||
return Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
return Vector3.UP
|
||||
|
||||
|
||||
func _waterfall_sheet_twist(rel_path: String) -> float:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("newwaterfall.m2") or normalized.ends_with("newwaterfall.mdx"):
|
||||
return PI * 0.5
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return -PI * 0.5
|
||||
return 0.0
|
||||
|
||||
|
||||
func _waterfall_sheet_twist_anchor(rel_path: String) -> Vector3:
|
||||
var normalized := rel_path.replace("\\", "/").to_lower()
|
||||
if normalized.ends_with("elwynntallwaterfall01.m2") or normalized.ends_with("elwynntallwaterfall01.mdx"):
|
||||
return Vector3(-2.667799, 89.62273, 0.00129)
|
||||
return Vector3.ZERO
|
||||
|
||||
|
||||
func _cancel_wmo_build_job(tile_key: String) -> void:
|
||||
if _wmo_build_jobs.has(tile_key):
|
||||
_wmo_build_jobs.erase(tile_key)
|
||||
@@ -4216,8 +4138,12 @@ func _group_tile_m2_task(
|
||||
var pos: Vector3 = world_pos - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
var basis := _adt_m2_placement_basis(rot, normalized)
|
||||
var offset := _adt_m2_placement_origin_offset(rot, normalized, scale_value)
|
||||
var basis: Basis = _m2_placement_transform_resolver.resolve_basis(rot, normalized)
|
||||
var offset: Vector3 = _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
normalized,
|
||||
scale_value
|
||||
)
|
||||
var xform := Transform3D(basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)), pos + offset)
|
||||
|
||||
if not groups.has(normalized):
|
||||
@@ -4905,8 +4831,18 @@ func _build_tile_m2_placeholders(tile_root: Node3D, tile_origin: Vector3, m2_nam
|
||||
var local_pos: Vector3 = placement.get("pos", Vector3.ZERO) - tile_origin
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
var xform_basis := _adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.01))
|
||||
var xf := Transform3D(xform_basis, local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
|
||||
var xform_basis := _m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.01))
|
||||
var xf := Transform3D(
|
||||
xform_basis,
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
if is_tree:
|
||||
tree_transforms.append(xf)
|
||||
else:
|
||||
@@ -4956,8 +4892,16 @@ func _instantiate_m2(rel_path: String, placement: Dictionary, tile_origin: Vecto
|
||||
var rot: Vector3 = placement.get("rot", Vector3.ZERO)
|
||||
var scale_value: float = float(placement.get("scale", 1.0))
|
||||
node.transform = Transform3D(
|
||||
_adt_m2_placement_basis(rot, rel_path).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
|
||||
local_pos + _adt_m2_placement_origin_offset(rot, rel_path, scale_value))
|
||||
_m2_placement_transform_resolver.resolve_basis(
|
||||
rot,
|
||||
rel_path
|
||||
).scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
|
||||
local_pos + _m2_placement_transform_resolver.resolve_origin_offset(
|
||||
rot,
|
||||
rel_path,
|
||||
scale_value
|
||||
)
|
||||
)
|
||||
return node
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free formula, dependency and timing regression for ADT M2 transforms.
|
||||
|
||||
const RESOLVER_SCRIPT := preload("res://src/render/m2/m2_placement_transform_resolver.gd")
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
|
||||
const CLIFFROCK_WORLD_YAW_OFFSET := PI
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_regular_basis(failures)
|
||||
_verify_cliffrock_basis(failures)
|
||||
_verify_new_waterfall_basis(failures)
|
||||
_verify_tall_waterfall_basis_and_offset(failures)
|
||||
_verify_path_normalization_and_scale_clamp(failures)
|
||||
_verify_loader_boundary(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("M2_PLACEMENT_TRANSFORM_RESOLVER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_PLACEMENT_TRANSFORM_RESOLVER PASS cases=9 iterations=10000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_regular_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.2, -0.7, 1.1)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "World/Generic/Tree.m2"),
|
||||
Basis.from_euler(rotation),
|
||||
"regular basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, "World/Generic/Tree.m2", 3.0),
|
||||
Vector3.ZERO,
|
||||
"regular offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_cliffrock_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.3, 0.4, -0.2)
|
||||
var expected_basis := (
|
||||
Basis(Vector3.UP, rotation.y + CLIFFROCK_WORLD_YAW_OFFSET)
|
||||
* Basis(Vector3.BACK, rotation.x)
|
||||
* Basis(Vector3.RIGHT, -rotation.z)
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "WORLD\\AZEROTH\\ElwynnCliffRock01.MDX"),
|
||||
expected_basis,
|
||||
"cliffrock basis",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_new_waterfall_basis(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(-0.1, 0.8, 0.2)
|
||||
var fall_axis := Vector3(-0.138742, 0.990329, 0.0).normalized()
|
||||
var expected_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET)
|
||||
* Basis.from_euler(rotation)
|
||||
* Basis(fall_axis, PI * 0.5)
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, "world/x/NewWaterfall.m2"),
|
||||
expected_basis,
|
||||
"new waterfall basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, "world/x/NewWaterfall.m2", 1.0),
|
||||
Vector3.ZERO,
|
||||
"new waterfall offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_tall_waterfall_basis_and_offset(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.05, -0.2, 0.15)
|
||||
var path := "world/x/ElwynnTallWaterfall01.m2"
|
||||
var fall_axis := Vector3(-0.068962, 0.997619, 0.0).normalized()
|
||||
var placement_basis := (
|
||||
Basis(Vector3.UP, WATERFALL_WORLD_YAW_OFFSET) * Basis.from_euler(rotation)
|
||||
)
|
||||
var expected_basis := placement_basis * Basis(fall_axis, -PI * 0.5)
|
||||
var anchor := Vector3(-2.667799, 89.62273, 0.00129)
|
||||
var expected_offset := (placement_basis * anchor - expected_basis * anchor) * 1.5
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", rotation, path),
|
||||
expected_basis,
|
||||
"tall waterfall basis",
|
||||
failures
|
||||
)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", rotation, path, 1.5),
|
||||
expected_offset,
|
||||
"tall waterfall offset",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_path_normalization_and_scale_clamp(failures: Array[String]) -> void:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var path := "WORLD\\X\\ELWYNNTALLWATERFALL01.MDX"
|
||||
var minimum_offset: Vector3 = resolver.call("resolve_origin_offset", Vector3.ZERO, path, 0.0001)
|
||||
_expect_vector_near(
|
||||
resolver.call("resolve_origin_offset", Vector3.ZERO, path, -9.0),
|
||||
minimum_offset,
|
||||
"negative scale clamp",
|
||||
failures
|
||||
)
|
||||
_expect_basis_near(
|
||||
resolver.call("resolve_basis", Vector3.ZERO, "ElwynnCliffRock02.m2"),
|
||||
Basis(Vector3.UP, PI),
|
||||
"second cliffrock",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_loader_boundary(failures: Array[String]) -> void:
|
||||
var source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(
|
||||
source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"),
|
||||
"loader composes resolver",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
source.count("_m2_placement_transform_resolver.resolve_basis("),
|
||||
3,
|
||||
"three basis adapters",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
source.count("_m2_placement_transform_resolver.resolve_origin_offset("),
|
||||
3,
|
||||
"three offset adapters",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not source.contains("func _adt_m2_placement_basis("),
|
||||
"loader omits basis formula",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
not source.contains("func _waterfall_sheet_twist("),
|
||||
"loader omits waterfall formula",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var resolver: RefCounted = RESOLVER_SCRIPT.new()
|
||||
var rotation := Vector3(0.1, 0.2, 0.3)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(10000):
|
||||
var path := (
|
||||
"world/x/ElwynnTallWaterfall01.m2"
|
||||
if iteration % 4 == 0
|
||||
else "world/generic/Tree.m2"
|
||||
)
|
||||
resolver.call("resolve_basis", rotation, path)
|
||||
resolver.call("resolve_origin_offset", rotation, path, 1.0)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "10000 transform pairs bounded", failures)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_basis_near(
|
||||
actual_basis: Basis,
|
||||
expected_basis: Basis,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_basis.is_equal_approx(expected_basis):
|
||||
failures.append("%s basis mismatch" % label)
|
||||
|
||||
|
||||
func _expect_vector_near(
|
||||
actual_value: Vector3,
|
||||
expected_value: Vector3,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if not actual_value.is_equal_approx(expected_value):
|
||||
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_equal(
|
||||
actual_value: int,
|
||||
expected_value: int,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b4a7gtg46jrdn
|
||||
@@ -27,12 +27,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-16
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`;
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`;
|
||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-001`,
|
||||
`M03-RND-M2-UNIQUE-REGISTRY-001`
|
||||
`M03-RND-M2-UNIQUE-REGISTRY-001`,
|
||||
`M03-RND-M2-PLACEMENT-TRANSFORM-001`
|
||||
- Commands: dedicated scheduler/planner/facade/focus/coordinate/manifest/shutdown
|
||||
headless verifiers; Godot cold editor parse; coordination, documentation and diff gates.
|
||||
- Results: scheduler `cases=6 iterations=20000 elapsed_ms=10.216`; planner
|
||||
@@ -57,6 +58,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
M2 unique placement registry passed `cases=10 iterations=100 elapsed_ms=35.513`;
|
||||
the historical `uid:11785` cross-tile smoke, internal-access `42`, facade and
|
||||
`7/7` manifest gates remained green after merge.
|
||||
M2 placement transform resolver passed `cases=9 iterations=10000
|
||||
elapsed_ms=27.409`; post-merge M2 registry, internal-access `42`, facade,
|
||||
`7/7` manifest, documentation and coordination gates remained green.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -77,6 +81,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
loader still drains removals first through the shared `chunk_geometry` budget.
|
||||
M2 positive `MDDF.uniqueId` retains `uid:<decimal>` first-owner semantics,
|
||||
within-call duplicate suppression, cross-tile skip/retry and unkeyed pass-through.
|
||||
M2 regular Euler, Elwynn cliff-rock shell correction, waterfall yaw/local-axis
|
||||
twist, tall-waterfall anchor compensation, path normalization and caller scale
|
||||
clamps are unchanged across worker grouping, placeholders and direct instances.
|
||||
No new build-12340 parity claim is made; asset-backed visual/p95/p99 comparison remains pending.
|
||||
- Changed files: scene-free scheduler and verifier; streamer budget composition/
|
||||
permit delegation; scheduler module spec; world renderer/module registry/RENDER notes;
|
||||
@@ -87,7 +94,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
immutable terrain chunk LOD policy, pure planner and both loader plan adapters,
|
||||
pure terrain chunk geometry queue planner and loader queue-adoption adapter,
|
||||
stateful M2 unique placement registry, loader reserve/release/clear adapters and
|
||||
migrated historical dedupe smoke,
|
||||
migrated historical dedupe smoke, stateless M2 placement transform resolver and
|
||||
three loader transform adapters,
|
||||
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
|
||||
- Remaining risks: queue storage length and worker concurrency/stale-result logic
|
||||
remain streamer-owned; cancellation stops new permits but does not interrupt
|
||||
@@ -99,6 +107,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
chunk queue planning adds a second tile-state pass whose asset-backed p95/p99
|
||||
still needs acceptance, while queue drains and geometry finalization remain monolithic;
|
||||
private Azeroth data was absent for the asset-backed ADT placement probe; M2
|
||||
grouping/build/tasks/caches/finalization and boundary traversal p95/p99 remain pending;
|
||||
asset-backed visual transform recheck, grouping/build/tasks/caches/finalization
|
||||
and boundary traversal p95/p99 remain pending;
|
||||
M03 still needs further terrain/M2/WMO/liquid service extraction
|
||||
and dependency/performance acceptance.
|
||||
|
||||
Reference in New Issue
Block a user