Compare commits

..

5 Commits

13 changed files with 758 additions and 63 deletions
+11
View File
@@ -1003,6 +1003,17 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- Cache formats, placement positions and visible rules are unchanged by the - Cache formats, placement positions and visible rules are unchanged by the
extraction. Asset-backed visual recheck and general placement parity remain gaps. extraction. Asset-backed visual recheck and general placement parity remain gaps.
## 2026-07-16 M2 Placement Grouper Extraction
- `M2PlacementGrouper` now owns pure worker-side validation, historical path
normalization and ordered tile-local transform grouping of ADT M2 placements.
- Invalid records, backslash replacement, lowercase `.mdx/.mdl` conversion,
first-seen ordering and the `0.0001` scale clamp are unchanged.
- The loader retains tasks, mutex/result queues, stale-result checks, tile/build
state, caches and all Node/MultiMesh/Mesh/material/RID finalization.
- Cache formats, batching, profiles and visible rules are unchanged. Asset-backed
traversal p95/p99, visual recheck and spatial-cell grouping remain pending.
## 2026-07-16 World Environment Snapshot Facade ## 2026-07-16 World Environment Snapshot Facade
- `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value, - `WorldEnvironmentSnapshot` carries one immutable finite time-of-day value,
@@ -0,0 +1,114 @@
# M03-RND-M2-PLACEMENT-GROUPER-001 — M2 placement grouper
<!-- OPENWC_CLAIM:M03-RND-M2-PLACEMENT-GROUPER-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-grouper`
- Lease expires UTC: 2026-07-19
- Integrator: M03 milestone integrator
## Outcome
Extract pure ADT M2 placement validation, path normalization and local-transform
grouping from the worker callback into a scene-free service.
## Non-goals
- Change placement ordering, coordinate conversion, transform formulas or clamps.
- Move WorkerThreadPool tasks, mutex/result queues, tile state or retry behavior.
- Move build jobs, animation/mesh loading, caches, Nodes, MultiMesh or RIDs.
- Generalize grouping policy, change batching or add a dependency.
- Claim build-12340 placement or visual parity beyond preserved formulas.
## Paths
- Exclusive: `src/render/m2/m2_placement_grouper.gd`,
`src/tools/verify_m2_placement_grouper.gd`,
`docs/modules/m2-placement-grouper.md`, this claim
- Shared/hotspots: `src/scenes/streaming/streaming_world_loader.gd`,
`src/tools/verify_m2_placement_transform_resolver.gd`,
`docs/modules/m2-placement-transform-resolver.md`,
`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: tile-local origin, MMDX name table and raw ADT M2 placement array
- Output: fresh Dictionary keyed by historically normalized relative M2 path;
each value is a fresh ordered Array of tile-local `Transform3D` values
- Invalid variants, out-of-range name IDs and empty paths are skipped
- Backslashes become slashes; lowercase `.mdx/.mdl` suffixes become `.m2`
- Transform basis/origin delegates to `M2PlacementTransformResolver`; final basis
scale retains the grouping-path clamp `max(scale, 0.0001)`
- Cache/schema/coordinate versions: unchanged
## Dependencies
- Requires: current master `c34e538`, `M2PlacementTransformResolver`
- Blocks: broader M2 worker/build decomposition
- External state: none; tests use synthetic names and placement dictionaries
## Verification
- Commands: dedicated invalid/name/path/group-order/default/transform/scale and
bounded timing verifier; loader source boundary; resolver/registry/dedupe plus
facade/internal-access/manifest/shutdown/documentation/coordination gates
- Fidelity evidence: exact loop rules and transform composition moved unchanged
- Performance budget: linear over placement count with fresh output containers;
no Node, Resource, RID, task, mutex, cache or file access
## Documentation deliverables
- Inline grouper API documentation
- New module spec with inputs/outputs, data-flow, lifecycle, ownership, failure,
performance, fidelity limitations and source map
- Transform resolver, renderer/module registry and `RENDER.md` updates
## Simplicity and naming
- Important name: `M2PlacementGrouper`
- Simplest solution: one stateless `group_placements` query
- Rejected complexity: group/result classes, strategy interfaces, callbacks,
Resources, policies or generic spatial batching framework
- Unavoidable complexity: raw ADT dictionaries remain at the parser adapter
- Measured optimization evidence: bounded synthetic grouping loop
## Status
- State: accepted
- Done: pure grouper, worker adapter, regressions and documentation verified
- Next: M2 worker/build state extraction and spatial-cell evidence
- Blocked by:
<!-- OPENWC_HANDOFF:READY:M03-RND-M2-PLACEMENT-GROUPER-001:9df32d3 -->
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-M2-PLACEMENT-GROUPER-001:f88bf97 -->
## Handoff
- Commit: `9df32d3` (`refactor(M03): extract M2 placement grouper`)
- Results: dedicated grouper PASS (`cases=9`, `iterations=100`,
`placements=256`, `elapsed_ms=67.736`); transform resolver PASS (`cases=9`,
`iterations=10000`, `elapsed_ms=28.571`); M2 registry/dedupe, terrain
cache/LOD/queue, facade, internal-access, manifest, shutdown, scheduler,
streaming planner/focus, terrain-query and coordinate-boundary suites PASS;
editor import parsed the project with only expected missing private corpus
diagnostics; documentation PASS (`module_specs=17`, `required_files=7`);
coordination PASS with 23 pre-existing expired M00/M01 warnings; checkpoint
dry-run planned `7/7`.
- Remaining risks: no private asset-backed traversal/visual comparison;
uppercase extension preservation is historical but inconsistent with the
resolver's case-insensitive exception matching; grouping remains model-path
based rather than spatial-cell based; worker/build state and p95/p99 remain.
- Documentation updated: inline public API; new
`docs/modules/m2-placement-grouper.md`; transform resolver module spec,
renderer module registry, `docs/modules/world-renderer.md` and `RENDER.md`.
- Integration: accepted in master merge `f88bf97`; post-merge grouper
(`cases=9`, `iterations=100`, `placements=256`, `elapsed_ms=67.104`),
transform resolver, registry, internal-access, facade, manifest,
documentation and coordination gates passed.
+1
View File
@@ -17,6 +17,7 @@
| Terrain chunk geometry queue planner | Implemented extraction | [`terrain-chunk-geometry-queue-planner.md`](terrain-chunk-geometry-queue-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 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) | | 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) |
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.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) | | Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) | | Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
+191
View File
@@ -0,0 +1,191 @@
# M2 Placement Grouper
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-PLACEMENT-GROUPER-001` |
| Owners | Pure ADT M2 placement validation, normalization and transform grouping |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
| Profiles/capabilities | Existing static M2 worker-grouping path |
## Purpose
Convert valid raw ADT M2 placement records into ordered tile-local transforms
grouped by the historically normalized relative model path.
## Non-goals
- Reserve cross-tile unique IDs or choose placement ownership.
- Own WorkerThreadPool tasks, mutexes, result queues or tile lifecycle.
- Load models, choose animated/static rendering or build MultiMesh batches.
- Create Node, Mesh, material, Resource or RID objects.
- Change path case, transform rules, batching or visual fidelity.
## Context and boundaries
```mermaid
flowchart LR
ADT[ADT names + placements] --> Registry[M2UniquePlacementRegistry]
Registry --> Grouper[M2PlacementGrouper]
Grouper --> Resolver[M2PlacementTransformResolver]
Resolver --> Grouper
Grouper --> Groups[Path to ordered Transform3D arrays]
Groups --> Worker[Loader worker result adapter]
Worker --> Build[Loader-owned build queue]
```
Allowed dependencies are value containers, `Vector3`, `Basis`, `Transform3D`
and `M2PlacementTransformResolver`. SceneTree, Node, RenderingServer,
WorkerThreadPool, mutexes, ResourceLoader, files, gameplay, network and editor UI
are forbidden.
## Public API
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|---|---|---|---|---|
| `group_placements(tile_origin, m2_names, m2_placements)` | Pure query | Return valid placements grouped as ordered tile-local transforms | Worker/main thread; call-local output | Invalid variants/name IDs/empty paths are skipped |
## Inputs and outputs
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|---|---|---|---|---|---|
| Input | Godot-space tile origin | Loader tile state | Local-position calculation | Value copy | One call |
| Input | ADT MMDX name table | Parsed ADT tile | Path lookup | Read-only PackedStringArray | One call |
| Input | Raw placement dictionaries | Unique registry result | Validation/transforms | Read-only shallow values | One call |
| Output | Relative path to ordered `Transform3D` arrays | Grouper | Loader worker/build adapter | Fresh Dictionary and arrays | One call/build job |
Output retains first-seen group-key order through Godot Dictionary insertion
order and placement order inside each array. Inputs are not retained or returned.
## Data flow
```mermaid
flowchart TD
Call[group_placements] --> Next[Read placement]
Next --> Dict{Dictionary?}
Dict -->|no| Next
Dict -->|yes| Name{Valid name_id?}
Name -->|no| Next
Name -->|yes| Normalize[Backslash to slash; lowercase MDX/MDL to M2]
Normalize --> Empty{Path empty?}
Empty -->|yes| Next
Empty -->|no| Local[world position - tile origin]
Local --> Resolve[Resolve basis and origin offset]
Resolve --> Scale[Apply max scale 0.0001]
Scale --> Append[Append Transform3D to path group]
Append --> Next
Next -->|complete| Return[Return fresh groups]
```
## Lifecycle/state
The grouper retains no state. Every call constructs one stateless transform
resolver and fresh output containers. Reset, cancellation and shutdown require
no grouper action.
## Main sequence
```mermaid
sequenceDiagram
participant Loader as Loader worker callback
participant Grouper as M2PlacementGrouper
participant Resolver as M2PlacementTransformResolver
Loader->>Grouper: group_placements(origin, names, placements)
loop valid placement
Grouper->>Resolver: resolve basis and origin offset
Resolver-->>Grouper: value-only basis and offset
Grouper->>Grouper: append local Transform3D
end
Grouper-->>Loader: fresh path groups
Loader->>Loader: mutex-protected result enqueue
```
## Ownership, threading and resources
- The grouper owns only call-local containers and transforms.
- The loader owns tasks, mutex/result queue, tile/build state, caches, model
loading, Nodes, MultiMesh, Mesh, materials and RIDs.
- Worker use is safe because the grouper and resolver mutate no shared state.
- The caller owns returned containers; mutation cannot affect later calls.
## Errors, cancellation and recovery
| Failure | Detection | Behavior | Diagnostic | Recovery |
|---|---|---|---|---|
| Non-Dictionary placement | Type guard | Skip | Contract fixture | Repair parser input |
| Invalid name ID | Bounds guard | Skip | Contract fixture | Repair name table/placement |
| Empty path | Empty guard | Skip | Contract fixture | Supply valid MMDX entry |
| Missing position/rotation/scale | Defaults | Zero position/rotation and scale 1 | Contract fixture | Expected fallback |
| Zero/negative scale | Historical clamp | Basis scale uses `0.0001` | Contract fixture | Supply valid ADT scale |
| Cancellation after grouping | Loader state check | Discard stale result | Loader regressions | Re-request eligible detail |
Cancellation stays loader-owned because grouping has no task or partial state.
## Configuration and capabilities
No setting or profile branch is introduced. Preserved behavior includes
backslash replacement without lowercasing, case-sensitive lowercase `.mdx/.mdl`
conversion, first-seen ordering, tile-local subtraction and scale clamp `0.0001`.
## Persistence, cache and migration
No persistence or cache format participates. ADT and M2 cache versions are
unchanged, so no rebuild or migration is required.
## Diagnostics and observability
The service emits no logs. Its verifier records contract results, source
boundaries and bounded synthetic 100-by-256 placement timing. Loader metrics
continue to own task/build/cache observability.
## Verification
- `verify_m2_placement_grouper.gd`: invalid input, path behavior, ordering,
defaults, local origin, regular/waterfall transforms, scale clamp, fresh output,
loader/source boundary and timing.
- `verify_m2_placement_transform_resolver.gd`: three transform consumers remain
covered as two direct loader adapters plus the grouper adapter.
- Registry/dedupe, facade, internal-access, manifest, shutdown, documentation and
coordination regressions remain required.
- Fidelity evidence is exact extraction; no new asset-backed parity claim is made.
## Extension points
- A later package may extract the M2 build-job state machine behind explicit
budgets while retaining this pure grouping input.
- Spatial cells require measured culling evidence and are outside this contract.
## Capability status
| Capability | Status | Evidence | Gap/next step |
|---|---|---|---|
| Pure path/transform grouping | Implemented extraction | Contract/source/timing verifier | Asset-backed traversal timing pending |
| Worker/build orchestration | Remains in loader | Existing regressions | Separate stateful extraction required |
| Spatial MultiMesh cells | Partial | Current path batches | Culling-driven design/evidence pending |
## Known gaps and risks
- Raw placement dictionaries remain the ADT parser boundary.
- Uppercase extensions remain unchanged even though transform exception matching
is case-insensitive.
- Grouping is by model path, not spatial culling cell.
- Asset-backed p95/p99 and visual comparison remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_placement_grouper.gd` | Validation, normalization and grouping |
| `src/render/m2/m2_placement_transform_resolver.gd` | Basis and origin rules |
| `src/scenes/streaming/streaming_world_loader.gd` | Worker/result/build lifecycle and resources |
| `src/tools/verify_m2_placement_grouper.gd` | Contract, dependency and timing regression |
## Related decisions and references
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
- [`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)
+21 -18
View File
@@ -13,7 +13,7 @@
## Purpose ## Purpose
Resolve the unscaled Godot-space orientation and optional local origin offset Resolve the unscaled Godot-space orientation and optional local origin offset
used by ADT M2 worker grouping, placeholder rendering and instance creation. used by the pure placement grouper, placeholder rendering and instance creation.
## Non-goals ## Non-goals
@@ -30,7 +30,7 @@ flowchart LR
ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver] ADT[ADT placement rotation/path/scale] --> Resolver[M2PlacementTransformResolver]
Resolver --> Basis[Unscaled Basis] Resolver --> Basis[Unscaled Basis]
Resolver --> Offset[Local origin offset] Resolver --> Offset[Local origin offset]
Basis --> Group[Worker grouping] Basis --> Group[M2PlacementGrouper]
Basis --> Placeholder[Placeholder MultiMesh path] Basis --> Placeholder[Placeholder MultiMesh path]
Basis --> Instance[Instance path] Basis --> Instance[Instance path]
Offset --> Group Offset --> Group
@@ -57,8 +57,8 @@ files, gameplay, network and editor UI are forbidden.
| Input | Godot Euler rotation in radians | ADT loader placement record | Resolver | Scalar copy | One call | | 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 | 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 | | 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 | Unscaled `Basis` | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
| Output | Local `Vector3` offset | Resolver | Transform origin adapter | Value copy | One call | | Output | Local `Vector3` offset | Resolver | Grouper/placeholder/instance adapter | Value copy | One call |
Callers preserve their historical basis scale minima: grouping/instance use Callers preserve their historical basis scale minima: grouping/instance use
`0.0001`, while placeholders use `0.01`. `0.0001`, while placeholders use `0.01`.
@@ -88,22 +88,22 @@ world reset and shutdown require no clear, cancellation or resource release.
```mermaid ```mermaid
sequenceDiagram sequenceDiagram
participant Loader as StreamingWorldLoader participant Caller as Grouper or StreamingWorldLoader
participant Resolver as M2PlacementTransformResolver participant Resolver as M2PlacementTransformResolver
participant Consumer as Group/placeholder/instance path participant Consumer as Grouper/placeholder/instance path
Loader->>Resolver: resolve_basis(rotation, path) Caller->>Resolver: resolve_basis(rotation, path)
Resolver-->>Loader: unscaled Basis Resolver-->>Caller: unscaled Basis
Loader->>Resolver: resolve_origin_offset(rotation, path, scale) Caller->>Resolver: resolve_origin_offset(rotation, path, scale)
Resolver-->>Loader: local offset Resolver-->>Caller: local offset
Loader->>Consumer: compose historical scaled Transform3D Caller->>Consumer: compose historical scaled Transform3D
``` ```
## Ownership, threading and resources ## Ownership, threading and resources
- The resolver retains no mutable state or engine resources. - The resolver retains no mutable state or engine resources.
- The loader owns placement records, final transforms, tasks, queues, caches, - The grouper owns worker-path final transforms; the loader owns placement records,
MultiMesh/Node/Mesh/material/RID creation and cleanup. tasks, queues, caches and MultiMesh/Node/Mesh/material/RID creation and cleanup.
- Pure methods are safe for current worker grouping and main-thread consumers. - Pure methods are safe for grouper worker use and main-thread consumers.
- Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver. - Returned `Basis`/`Vector3` values have no lifetime coupling to the resolver.
## Errors, cancellation and recovery ## Errors, cancellation and recovery
@@ -134,14 +134,15 @@ versions and rebuild instructions remain unchanged.
## Diagnostics and observability ## Diagnostics and observability
The resolver emits no logs. The contract verifier compares formulas/constants, The resolver emits no logs. The contract verifier compares formulas/constants,
checks all three loader consumers and records bounded synthetic timing. Visual checks two direct loader consumers plus the grouper consumer and records bounded
synthetic timing. Visual
and asset-backed evidence remains in the renderer checkpoint workflow. and asset-backed evidence remains in the renderer checkpoint workflow.
## Verification ## Verification
- `verify_m2_placement_transform_resolver.gd`: regular fallback, path - `verify_m2_placement_transform_resolver.gd`: regular fallback, path
normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale normalization, both cliff rocks, both waterfalls, twist anchor, negative-scale
compensation, three loader adapters, source boundary and 10,000-pair timing. compensation, three consumers, source boundary and 10,000-pair timing.
- M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown, - M2 unique/dedupe, terrain, facade, internal-access, manifest, shutdown,
scheduler, streaming and coordinate regressions remain required. scheduler, streaming and coordinate regressions remain required.
- Fidelity evidence is exact extraction of existing calibrated formulas. The - Fidelity evidence is exact extraction of existing calibrated formulas. The
@@ -149,7 +150,7 @@ and asset-backed evidence remains in the renderer checkpoint workflow.
## Extension points ## Extension points
- The next package may compose this resolver into a pure placement grouper. - `M2PlacementGrouper` composes this resolver for the worker grouping path.
- New model corrections require separate measured fidelity evidence; this module - New model corrections require separate measured fidelity evidence; this module
is not a generic override registry. is not a generic override registry.
@@ -173,11 +174,13 @@ and asset-backed evidence remains in the renderer checkpoint workflow.
| Path | Responsibility | | Path | Responsibility |
|---|---| |---|---|
| `src/render/m2/m2_placement_transform_resolver.gd` | Pure basis/offset rules and calibrated allowlists | | `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/render/m2/m2_placement_grouper.gd` | Grouping adapter and local Transform3D ownership |
| `src/scenes/streaming/streaming_world_loader.gd` | Two direct adapters and renderer resource ownership |
| `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression | | `src/tools/verify_m2_placement_transform_resolver.gd` | Formula, source and performance regression |
## Related decisions and references ## Related decisions and references
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
- [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md) - [`m2-unique-placement-registry.md`](m2-unique-placement-registry.md)
- [`world-renderer.md`](world-renderer.md) - [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md) - [`../../RENDER.md`](../../RENDER.md)
+18 -7
View File
@@ -5,9 +5,9 @@
| Field | Value | | Field | Value |
|---|---| |---|---|
| 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 unique/transform packages | | 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/grouping packages |
| Owners | Renderer workstream / milestone integrator | | Owners | Renderer workstream / milestone integrator |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-transform`, 2026-07-16 | | Last verified | Worktree `work/sindo-main-codex/m03-m2-placement-grouper`, 2026-07-16 |
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete | | Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
## Purpose ## Purpose
@@ -48,7 +48,9 @@ flowchart LR
ChunkQueue --> Loader ChunkQueue --> Loader
Loader --> M2Registry[M2UniquePlacementRegistry] Loader --> M2Registry[M2UniquePlacementRegistry]
M2Registry --> Loader M2Registry --> Loader
Loader --> M2Transform[M2PlacementTransformResolver] Loader --> M2Grouper[M2PlacementGrouper]
M2Grouper --> M2Transform[M2PlacementTransformResolver]
M2Transform --> M2Grouper
M2Transform --> Loader M2Transform --> Loader
Native --> Parsed[Parsed tile/model data] Native --> Parsed[Parsed tile/model data]
Parsed --> Loader Parsed --> Loader
@@ -118,6 +120,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 | | `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 | | `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 | | `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 |
Публичным contract не считаются `StreamingWorldLoader` methods/properties, Публичным contract не считаются `StreamingWorldLoader` methods/properties,
внутренние dictionaries, queues, job records и generated node names. Scene-owned внутренние dictionaries, queues, job records и generated node names. Scene-owned
@@ -143,7 +146,8 @@ 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 | 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 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 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 | | Internal transform | Rotation/path/scale | Loader or grouper / `M2PlacementTransformResolver` | Group/placeholder/instance transforms | Value-only Basis/Vector3 | One placement |
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
| Output | Desired tile/detail operations | Streamer plan application | Finalize queues | Loader-owned | Cross-frame | | 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 | 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 | | Output | Detached renderer metrics | `WorldRenderFacade` | Capture/baseline tools | Caller-owned deep copy | On demand |
@@ -188,8 +192,9 @@ flowchart TD
DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner] DesiredChunkLod --> ChunkQueue[TerrainChunkGeometryQueuePlanner]
ChunkQueue --> B ChunkQueue --> B
R --> M2Registry[M2UniquePlacementRegistry reserve] R --> M2Registry[M2UniquePlacementRegistry reserve]
M2Registry --> M2 M2Registry --> M2Grouper[M2PlacementGrouper]
M2Transform[M2PlacementTransformResolver] --> M2 M2Transform[M2PlacementTransformResolver] --> M2Grouper
M2Grouper --> M2
R --> B[RenderBudgetScheduler permits] R --> B[RenderBudgetScheduler permits]
B --> Terrain[Terrain attach/upgrade] B --> Terrain[Terrain attach/upgrade]
B --> M2[M2 group/MultiMesh attach] B --> M2[M2 group/MultiMesh attach]
@@ -305,7 +310,10 @@ sequenceDiagram
The loader retains owned/skipped tile arrays, candidate retry, grouping/build The loader retains owned/skipped tile arrays, candidate retry, grouping/build
tasks, caches and all MultiMesh/Node/Mesh/material/RID side effects. tasks, caches and all MultiMesh/Node/Mesh/material/RID side effects.
- `M2PlacementTransformResolver` is stateless and retains no resources. The - `M2PlacementTransformResolver` is stateless and retains no resources. The
loader owns final transforms and every grouping/build/render side effect. grouper owns worker-path final transforms; direct placeholder/instance
transforms and every build/render side effect remain loader-owned.
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
The loader retains tasks, mutex/result queues, stale checks and build state.
- 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
@@ -393,6 +401,8 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
bounded reserve/release timing without a world scene. bounded reserve/release timing without a world scene.
- M2 placement transform contract: regular/cliffrock/waterfall formulas, path - M2 placement transform contract: regular/cliffrock/waterfall formulas, path
normalization, scale compensation, all three consumers and bounded timing. normalization, scale compensation, all three consumers and bounded timing.
- M2 placement grouper contract: invalid/default records, historical path rules,
group order, local transforms, fresh output, source boundary and bounded timing.
- Budget scheduler contract: exact lane exhaustion, shared chunk removal/create - Budget scheduler contract: exact lane exhaustion, shared chunk removal/create
priority, independent lanes, frame reset, invalid limits, terminal cancellation, priority, independent lanes, frame reset, invalid limits, terminal cancellation,
dependency boundaries and bounded permit timing without loading a world scene. dependency boundaries and bounded permit timing without loading a world scene.
@@ -424,6 +434,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects | | 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 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 | | M2 placement transform resolver | Implemented extraction | Scene-free formula/source/timing contract across three consumers | Asset-backed visual recheck and general placement parity pending |
| M2 placement grouper | Implemented extraction | Scene-free validation/order/transform/source/timing contract | Worker/build state, spatial cells and asset-backed p95/p99 remain pending |
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity | | WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity | | Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
| Sky/light | Partial | DBC controller/logs | Zone transition paired validation | | Sky/light | Partial | DBC controller/logs | Zone transition paired validation |
+65
View File
@@ -0,0 +1,65 @@
class_name M2PlacementGrouper
extends RefCounted
## Stateless ADT M2 placement validation, normalization and transform grouping.
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd"
)
## Groups valid placements by normalized relative M2 path while preserving
## placement order inside each group. Returned containers are caller-owned.
func group_placements(
tile_origin: Vector3,
m2_names: PackedStringArray,
m2_placements: Array
) -> Dictionary:
var groups: Dictionary = {}
var placement_transform_resolver := (
M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
)
for placement_variant in m2_placements:
if not (placement_variant is Dictionary):
continue
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id < 0 or name_id >= m2_names.size():
continue
var normalized_relative_path := _normalize_relative_path(m2_names[name_id])
if normalized_relative_path.is_empty():
continue
var world_position: Vector3 = placement.get("pos", Vector3.ZERO)
var local_position := world_position - tile_origin
var rotation_radians: Vector3 = placement.get("rot", Vector3.ZERO)
var scale_value := float(placement.get("scale", 1.0))
var placement_basis: Basis = placement_transform_resolver.resolve_basis(
rotation_radians,
normalized_relative_path
)
var origin_offset: Vector3 = placement_transform_resolver.resolve_origin_offset(
rotation_radians,
normalized_relative_path,
scale_value
)
var local_transform := Transform3D(
placement_basis.scaled(Vector3.ONE * maxf(scale_value, 0.0001)),
local_position + origin_offset
)
if not groups.has(normalized_relative_path):
groups[normalized_relative_path] = []
(groups[normalized_relative_path] as Array).append(local_transform)
return groups
func _normalize_relative_path(relative_path: String) -> String:
var normalized_relative_path := relative_path.replace("\\", "/")
if (
normalized_relative_path.ends_with(".mdx")
or normalized_relative_path.ends_with(".mdl")
):
normalized_relative_path = normalized_relative_path.get_basename() + ".m2"
return normalized_relative_path
@@ -0,0 +1 @@
uid://dkktxa5essfn
+8 -29
View File
@@ -30,6 +30,9 @@ const M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT := preload(
const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload( const M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd" "res://src/render/m2/m2_placement_transform_resolver.gd"
) )
const M2_PLACEMENT_GROUPER_SCRIPT := preload(
"res://src/render/m2/m2_placement_grouper.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")
@@ -215,6 +218,7 @@ var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new() M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
) )
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_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 = []
@@ -4119,36 +4123,11 @@ func _group_tile_m2_task(
tile_origin: Vector3, tile_origin: Vector3,
m2_names: PackedStringArray, m2_names: PackedStringArray,
m2_placements: Array) -> void: m2_placements: Array) -> void:
var groups: Dictionary = {} var groups: Dictionary = _m2_placement_grouper.group_placements(
for placement_variant in m2_placements: tile_origin,
if not (placement_variant is Dictionary): m2_names,
continue m2_placements
var placement: Dictionary = placement_variant
var name_id: int = int(placement.get("name_id", -1))
if name_id < 0 or name_id >= m2_names.size():
continue
var normalized: String = str(m2_names[name_id]).replace("\\", "/")
if normalized.ends_with(".mdx") or normalized.ends_with(".mdl"):
normalized = normalized.get_basename() + ".m2"
if normalized.is_empty():
continue
var world_pos: Vector3 = placement.get("pos", Vector3.ZERO)
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: 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):
groups[normalized] = []
(groups[normalized] as Array).append(xform)
_m2_group_result_mutex.lock() _m2_group_result_mutex.lock()
_m2_group_result_queue.append({ _m2_group_result_queue.append({
+296
View File
@@ -0,0 +1,296 @@
extends SceneTree
## Asset-free contract, dependency and timing regression for ADT M2 grouping.
const GROUPER_SCRIPT := preload("res://src/render/m2/m2_placement_grouper.gd")
const TRANSFORM_RESOLVER_SCRIPT := preload(
"res://src/render/m2/m2_placement_transform_resolver.gd"
)
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_empty_and_invalid_inputs(failures)
_verify_historical_path_normalization(failures)
_verify_group_order(failures)
_verify_tile_local_defaults(failures)
_verify_regular_transform(failures)
_verify_waterfall_transform(failures)
_verify_scale_clamp(failures)
_verify_fresh_output(failures)
_verify_source_boundaries(failures)
var elapsed_milliseconds := _verify_bounded_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_PLACEMENT_GROUPER: %s" % failure)
quit(1)
return
print(
"M2_PLACEMENT_GROUPER PASS cases=9 iterations=100 placements=256 elapsed_ms=%.3f"
% elapsed_milliseconds
)
quit(0)
func _verify_empty_and_invalid_inputs(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
_expect_equal(
grouper.group_placements(Vector3.ZERO, PackedStringArray(), []).size(),
0,
"empty",
failures
)
var placements: Array = [
"invalid",
{"name_id": -1},
{"name_id": 2},
{"name_id": 0},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray([""]),
placements
)
_expect_equal(groups.size(), 0, "invalid placements", failures)
func _verify_historical_path_normalization(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray([
"World\\Generic\\Tree.mdx",
"World/Generic/Rock.mdl",
"World/Generic/Upper.MDX",
])
var placements: Array = [{"name_id": 0}, {"name_id": 1}, {"name_id": 2}]
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_true(groups.has("World/Generic/Tree.m2"), "backslash and mdx normalization", failures)
_expect_true(groups.has("World/Generic/Rock.m2"), "mdl normalization", failures)
_expect_true(groups.has("World/Generic/Upper.MDX"), "uppercase suffix preservation", failures)
func _verify_group_order(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var placements: Array = [
{"name_id": 0, "pos": Vector3(3.0, 0.0, 0.0)},
{"name_id": 1, "pos": Vector3(8.0, 0.0, 0.0)},
{"name_id": 0, "pos": Vector3(5.0, 0.0, 0.0)},
]
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2", "Rock.m2"]),
placements
)
_expect_equal(groups.size(), 2, "group count", failures)
var tree_transforms: Array = groups.get("Tree.m2", [])
_expect_equal(tree_transforms.size(), 2, "tree transform count", failures)
if tree_transforms.size() == 2:
_expect_vector_near(tree_transforms[0].origin, Vector3(3.0, 0.0, 0.0), "first tree", failures)
_expect_vector_near(tree_transforms[1].origin, Vector3(5.0, 0.0, 0.0), "second tree", failures)
func _verify_tile_local_defaults(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3(10.0, 20.0, 30.0),
PackedStringArray(["Tree.m2"]),
[{"name_id": 0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_vector_near(transform.origin, Vector3(-10.0, -20.0, -30.0), "default local position", failures)
_expect_basis_near(transform.basis, Basis.IDENTITY, "default basis and scale", failures)
func _verify_regular_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var rotation := Vector3(0.2, -0.4, 0.6)
var groups: Dictionary = grouper.group_placements(
Vector3(1.0, 2.0, 3.0),
PackedStringArray(["Tree.m2"]),
[{
"name_id": 0,
"pos": Vector3(5.0, 8.0, 13.0),
"rot": rotation,
"scale": 2.0,
}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(transform.basis, Basis.from_euler(rotation).scaled(Vector3.ONE * 2.0), "regular basis", failures)
_expect_vector_near(transform.origin, Vector3(4.0, 6.0, 10.0), "regular origin", failures)
func _verify_waterfall_transform(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var resolver := TRANSFORM_RESOLVER_SCRIPT.new()
var path := "World/X/ElwynnTallWaterfall01.m2"
var rotation := Vector3(0.05, -0.2, 0.15)
var scale_value := 1.5
var position := Vector3(20.0, 30.0, 40.0)
var tile_origin := Vector3(2.0, 3.0, 4.0)
var groups: Dictionary = grouper.group_placements(
tile_origin,
PackedStringArray([path]),
[{
"name_id": 0,
"pos": position,
"rot": rotation,
"scale": scale_value,
}]
)
var transform: Transform3D = (groups[path] as Array)[0]
var expected_basis: Basis = resolver.resolve_basis(rotation, path).scaled(
Vector3.ONE * scale_value
)
var expected_origin: Vector3 = (
position
- tile_origin
+ resolver.resolve_origin_offset(rotation, path, scale_value)
)
_expect_basis_near(transform.basis, expected_basis, "waterfall basis", failures)
_expect_vector_near(transform.origin, expected_origin, "waterfall origin", failures)
func _verify_scale_clamp(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var groups: Dictionary = grouper.group_placements(
Vector3.ZERO,
PackedStringArray(["Tree.m2"]),
[{"name_id": 0, "scale": -5.0}]
)
var transform: Transform3D = (groups["Tree.m2"] as Array)[0]
_expect_basis_near(
transform.basis,
Basis.IDENTITY.scaled(Vector3.ONE * 0.0001),
"negative scale clamp",
failures
)
func _verify_fresh_output(failures: Array[String]) -> void:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2"])
var placements: Array = [{"name_id": 0}]
var first: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
(first["Tree.m2"] as Array).clear()
var second: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
_expect_equal((second["Tree.m2"] as Array).size(), 1, "fresh output", failures)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader_source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
var grouping_task_source := _source_between(
loader_source,
"func _group_tile_m2_task(",
"func _drain_m2_group_results()",
failures
)
_expect_equal(
loader_source.count("_m2_placement_grouper.group_placements("),
1,
"one loader grouping adapter",
failures
)
_expect_true(
not grouping_task_source.contains("for placement_variant in m2_placements"),
"loader omits grouping loop",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouper basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouper offset adapter",
failures
)
for forbidden_dependency in ["WorkerThreadPool", "Mutex", "RenderingServer", "ResourceLoader"]:
_expect_true(
not grouper_source.contains(forbidden_dependency),
"grouper omits %s" % forbidden_dependency,
failures
)
func _verify_bounded_timing(failures: Array[String]) -> float:
var grouper := GROUPER_SCRIPT.new()
var names := PackedStringArray(["Tree.m2", "Rock.mdx"])
var placements: Array = []
for placement_index in range(256):
placements.append({
"name_id": placement_index % 2,
"pos": Vector3(float(placement_index), 0.0, float(-placement_index)),
"rot": Vector3(0.1, 0.2, 0.3),
"scale": 1.0,
})
var started_microseconds := Time.get_ticks_usec()
for iteration in range(100):
var groups: Dictionary = grouper.group_placements(Vector3.ZERO, names, placements)
if groups.size() != 2:
failures.append("timing iteration %d produced wrong group count" % iteration)
break
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
_expect_true(elapsed_milliseconds < 1000.0, "100 x 256 grouping 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 _source_between(
source: String,
start_marker: String,
end_marker: String,
failures: Array[String]
) -> String:
var start_offset := source.find(start_marker)
var end_offset := source.find(end_marker, start_offset + start_marker.length())
if start_offset < 0 or end_offset <= start_offset:
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
return ""
return source.substr(start_offset, end_offset - start_offset)
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://bbx57qy0lp11w
@@ -4,6 +4,7 @@ extends SceneTree
const RESOLVER_SCRIPT := preload("res://src/render/m2/m2_placement_transform_resolver.gd") 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 LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const GROUPER_PATH := "res://src/render/m2/m2_placement_grouper.gd"
const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5 const WATERFALL_WORLD_YAW_OFFSET := PI * 0.5
const CLIFFROCK_WORLD_YAW_OFFSET := PI const CLIFFROCK_WORLD_YAW_OFFSET := PI
@@ -130,6 +131,7 @@ func _verify_path_normalization_and_scale_clamp(failures: Array[String]) -> void
func _verify_loader_boundary(failures: Array[String]) -> void: func _verify_loader_boundary(failures: Array[String]) -> void:
var source := _read_text(LOADER_PATH, failures) var source := _read_text(LOADER_PATH, failures)
var grouper_source := _read_text(GROUPER_PATH, failures)
_expect_true( _expect_true(
source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"), source.contains("M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()"),
"loader composes resolver", "loader composes resolver",
@@ -137,14 +139,26 @@ func _verify_loader_boundary(failures: Array[String]) -> void:
) )
_expect_equal( _expect_equal(
source.count("_m2_placement_transform_resolver.resolve_basis("), source.count("_m2_placement_transform_resolver.resolve_basis("),
3, 2,
"three basis adapters", "two direct loader basis adapters",
failures failures
) )
_expect_equal( _expect_equal(
source.count("_m2_placement_transform_resolver.resolve_origin_offset("), source.count("_m2_placement_transform_resolver.resolve_origin_offset("),
3, 2,
"three offset adapters", "two direct loader offset adapters",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_basis("),
1,
"one grouped basis adapter",
failures
)
_expect_equal(
grouper_source.count("placement_transform_resolver.resolve_origin_offset("),
1,
"one grouped offset adapter",
failures failures
) )
_expect_true( _expect_true(
+12 -4
View File
@@ -27,13 +27,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
## Evidence ## Evidence
- Date: 2026-07-16 - Date: 2026-07-16
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`; - Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`;
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`,
`M03-RND-TERRAIN-LOD-PLANNER-001`, `M03-RND-TERRAIN-CHUNK-QUEUE-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` `M03-RND-M2-PLACEMENT-TRANSFORM-001`,
`M03-RND-M2-PLACEMENT-GROUPER-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
@@ -61,6 +62,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
M2 placement transform resolver passed `cases=9 iterations=10000 M2 placement transform resolver passed `cases=9 iterations=10000
elapsed_ms=27.409`; post-merge M2 registry, internal-access `42`, facade, elapsed_ms=27.409`; post-merge M2 registry, internal-access `42`, facade,
`7/7` manifest, documentation and coordination gates remained green. `7/7` manifest, documentation and coordination gates remained green.
M2 placement grouper passed `cases=9 iterations=100 placements=256
elapsed_ms=67.104`; post-merge transform resolver, registry, internal-access
`42`, facade, `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
@@ -84,6 +88,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
M2 regular Euler, Elwynn cliff-rock shell correction, waterfall yaw/local-axis M2 regular Euler, Elwynn cliff-rock shell correction, waterfall yaw/local-axis
twist, tall-waterfall anchor compensation, path normalization and caller scale twist, tall-waterfall anchor compensation, path normalization and caller scale
clamps are unchanged across worker grouping, placeholders and direct instances. clamps are unchanged across worker grouping, placeholders and direct instances.
M2 worker grouping retains invalid-record filtering, case-sensitive lowercase
`.mdx/.mdl` conversion, first-seen group/per-group placement order, tile-local
origin subtraction and the `0.0001` scale clamp.
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;
@@ -95,7 +102,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
pure terrain chunk geometry queue planner and loader queue-adoption adapter, pure terrain chunk geometry queue planner and loader queue-adoption adapter,
stateful M2 unique placement registry, loader reserve/release/clear adapters and stateful M2 unique placement registry, loader reserve/release/clear adapters and
migrated historical dedupe smoke, stateless M2 placement transform resolver and migrated historical dedupe smoke, stateless M2 placement transform resolver and
three loader transform adapters, three transform consumers, stateless M2 placement grouper and worker adapter,
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: queue storage length and worker concurrency/stale-result logic - Remaining risks: queue storage length and worker concurrency/stale-result logic
remain streamer-owned; cancellation stops new permits but does not interrupt remain streamer-owned; cancellation stops new permits but does not interrupt
@@ -108,6 +115,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
still needs acceptance, while queue drains and geometry finalization remain monolithic; still needs acceptance, while queue drains and geometry finalization remain monolithic;
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; and boundary traversal p95/p99 remain pending; path grouping is not yet
culling-driven spatial-cell grouping;
M03 still needs further terrain/M2/WMO/liquid service extraction M03 still needs further terrain/M2/WMO/liquid service extraction
and dependency/performance acceptance. and dependency/performance acceptance.