render: extract static M2 batch materializer
This commit is contained in:
@@ -39,6 +39,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
|||||||
- `src/render/m2/m2_animated_scene_finalizer.gd` - animated PackedScene candidate ownership, historical material repair and AnimationPlayer validation.
|
- `src/render/m2/m2_animated_scene_finalizer.gd` - animated PackedScene candidate ownership, historical material repair and AnimationPlayer validation.
|
||||||
- `src/render/m2/m2_animation_playback_controller.gd` - deterministic per-instance phase, default animation selection and native/imported playback mutation.
|
- `src/render/m2/m2_animation_playback_controller.gd` - deterministic per-instance phase, default animation selection and native/imported playback mutation.
|
||||||
- `src/render/m2/m2_animated_instance_materializer.gd` - main-thread animated instance duplication, render settings, playback startup and non-empty batch attachment.
|
- `src/render/m2/m2_animated_instance_materializer.gd` - main-thread animated instance duplication, render settings, playback startup and non-empty batch attachment.
|
||||||
|
- `src/render/m2/m2_static_batch_materializer.gd` - main-thread static M2 MultiMesh construction, render settings and attachment.
|
||||||
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
|
- `src/render/m2/m2_animation_load_pipeline_state.gd` - animated M2 threaded-load request records and completion-order finalize FIFO without I/O or Node ownership.
|
||||||
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
- `src/render/m2/m2_mesh_load_pipeline_state.gd` - static M2 threaded-load request records, terminal statuses and completion-order finalize FIFO without I/O or Mesh ownership.
|
||||||
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
- `src/render/m2/m2_mesh_resource_cache_state.gd` - normalized-path prepared static M2 Mesh references with final-shutdown lifetime.
|
||||||
|
|||||||
@@ -21,6 +21,7 @@
|
|||||||
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
|
| M2 placement transform resolver | Implemented | [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md) |
|
||||||
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
| M2 placement grouper | Implemented extraction | [`m2-placement-grouper.md`](m2-placement-grouper.md) |
|
||||||
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
| M2 build batch planner | Implemented extraction | [`m2-build-batch-planner.md`](m2-build-batch-planner.md) |
|
||||||
|
| M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md) |
|
||||||
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
| M2 runtime mesh rebuild classifier | Implemented extraction | [`m2-runtime-mesh-rebuild-classifier.md`](m2-runtime-mesh-rebuild-classifier.md) |
|
||||||
| M2 animated scene finalizer | Implemented extraction | [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) |
|
| M2 animated scene finalizer | Implemented extraction | [`m2-animated-scene-finalizer.md`](m2-animated-scene-finalizer.md) |
|
||||||
| M2 animation playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) |
|
| M2 animation playback controller | Implemented extraction | [`m2-animation-playback-controller.md`](m2-animation-playback-controller.md) |
|
||||||
|
|||||||
@@ -174,7 +174,8 @@ queue depth, build activity and hitch observability.
|
|||||||
| Path | Responsibility |
|
| Path | Responsibility |
|
||||||
|---|---|
|
|---|---|
|
||||||
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
|
| `src/render/m2/m2_build_batch_planner.gd` | Pure limit/count/cursor planning |
|
||||||
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resources, instance materialization and budgets |
|
| `src/scenes/streaming/streaming_world_loader.gd` | Build job, queue, resource readiness, materializer adapters and budgets |
|
||||||
|
| `src/render/m2/m2_static_batch_materializer.gd` | Planned static-slice MultiMesh construction and attachment |
|
||||||
| `src/render/m2/m2_animation_playback_controller.gd` | Animated-instance phase/selection/native/imported playback |
|
| `src/render/m2/m2_animation_playback_controller.gd` | Animated-instance phase/selection/native/imported playback |
|
||||||
| `src/render/m2/m2_animated_instance_materializer.gd` | Planned animated-slice duplication, playback startup and attachment |
|
| `src/render/m2/m2_animated_instance_materializer.gd` | Planned animated-slice duplication, playback startup and attachment |
|
||||||
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
|
| `src/render/m2/m2_placement_grouper.gd` | Produces grouped transform arrays |
|
||||||
@@ -184,6 +185,7 @@ queue depth, build activity and hitch observability.
|
|||||||
|
|
||||||
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
|
- [`m2-placement-grouper.md`](m2-placement-grouper.md)
|
||||||
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
|
- [`m2-placement-transform-resolver.md`](m2-placement-transform-resolver.md)
|
||||||
|
- [`m2-static-batch-materializer.md`](m2-static-batch-materializer.md)
|
||||||
- [`world-renderer.md`](world-renderer.md)
|
- [`world-renderer.md`](world-renderer.md)
|
||||||
- [`../../RENDER.md`](../../RENDER.md)
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ flowchart LR
|
|||||||
IO --> Extract[M2MeshResourceExtractor]
|
IO --> Extract[M2MeshResourceExtractor]
|
||||||
Extract --> Prepare[M2RuntimeMeshFinalizer]
|
Extract --> Prepare[M2RuntimeMeshFinalizer]
|
||||||
Prepare --> Cache
|
Prepare --> Cache
|
||||||
Loader --> Build[M2 MultiMesh materialization]
|
Loader --> Build[M2StaticBatchMaterializer]
|
||||||
```
|
```
|
||||||
|
|
||||||
The cache may retain `Mesh` resources and copied normalized-path Strings. It has
|
The cache may retain `Mesh` resources and copied normalized-path Strings. It has
|
||||||
@@ -118,7 +118,8 @@ flowchart TB
|
|||||||
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
|
Loader[StreamingWorldLoader] --> Cache[M2MeshResourceCacheState]
|
||||||
Loader --> Pipeline[M2MeshLoadPipelineState]
|
Loader --> Pipeline[M2MeshLoadPipelineState]
|
||||||
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
Loader --> Finalizer[M2RuntimeMeshFinalizer]
|
||||||
Loader --> Builder[M2Builder and MultiMesh]
|
Loader --> Builder[M2Builder]
|
||||||
|
Loader --> Materializer[M2StaticBatchMaterializer]
|
||||||
Cache --> Mesh[Godot Mesh Resource]
|
Cache --> Mesh[Godot Mesh Resource]
|
||||||
Cache -. no dependency .-> Pipeline
|
Cache -. no dependency .-> Pipeline
|
||||||
Cache -. no dependency .-> Classifier
|
Cache -. no dependency .-> Classifier
|
||||||
@@ -132,7 +133,8 @@ flowchart TB
|
|||||||
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
- Borrowed Mesh lookups do not transfer ownership or duplicate resources.
|
||||||
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
- `M2MeshResourceExtractor` owns first-Mesh selection and temporary PackedScene
|
||||||
instances. `M2PrototypeCacheState` owns missing/prototype/animated state; the
|
instances. `M2PrototypeCacheState` owns missing/prototype/animated state; the
|
||||||
loader owns MultiMeshes and adoption decisions.
|
static materializer owns MultiMesh construction/attachment; the loader owns
|
||||||
|
resource adoption and build-job decisions.
|
||||||
- The loader drains asynchronous work before the final cache clear.
|
- The loader drains asynchronous work before the final cache clear.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## Errors, cancellation and recovery
|
||||||
|
|||||||
@@ -0,0 +1,224 @@
|
|||||||
|
# M2 Static Batch Materializer
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented |
|
||||||
|
| Target/work package | M03 / `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001` |
|
||||||
|
| Owners | Static M2 MultiMesh construction, render settings and attachment |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m03-m2-static-batch-materializer`, 2026-07-18 |
|
||||||
|
| Profiles/capabilities | Existing static M2 placement path |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Build and attach one static M2 `MultiMeshInstance3D` from an already prepared
|
||||||
|
Mesh and an ordered transform slice. The service preserves the existing
|
||||||
|
transform format, Mesh identity, batch name, visibility and shadow behavior.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Group placements, size batches or advance build-job cursors.
|
||||||
|
- Load, refresh, build or cache Mesh resources.
|
||||||
|
- Consume render permits, rotate queues or decide retries/cancellation.
|
||||||
|
- Assign Editor owners or release tile roots.
|
||||||
|
- Materialize animated instances or introduce spatial-cell batching.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Groups[Ordered transform group] --> Loader[StreamingWorldLoader build adapter]
|
||||||
|
MeshCache[Prepared Mesh cache] --> Loader
|
||||||
|
Planner[M2BuildBatchPlanner] --> Slice[Start and count]
|
||||||
|
Slice --> Loader
|
||||||
|
Loader --> Materializer[M2StaticBatchMaterializer]
|
||||||
|
Materializer --> Batch[Attached MultiMeshInstance3D]
|
||||||
|
Loader --> EditorOwner[Editor owner adapter]
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies are Godot `Mesh`, `MultiMesh`, `MultiMeshInstance3D`,
|
||||||
|
`GeometryInstance3D` and main-thread SceneTree APIs. Files, ResourceLoader,
|
||||||
|
workers, RenderingServer calls, caches, build jobs, scheduler policy, gameplay,
|
||||||
|
network and Editor ownership are forbidden.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `materialize_batch(parent, path, mesh, transforms, start, count, serial, visibility_end, visibility_margin, cast_shadows)` | Command/query | Create, configure and attach one static MultiMesh batch | Main thread; borrowed inputs, parent owns result | Invalid/empty input returns null; transform bounds are a caller precondition |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | M2 parent root | Loader build job | Batch attachment | Borrowed Node3D | Tile/job lifetime |
|
||||||
|
| Input | Relative model path and serial | Loader build adapter | Batch node name | Copied values | One call |
|
||||||
|
| Input | Prepared static Mesh | M2 Mesh resource cache/finalizer | MultiMesh | Borrowed Resource; exact reference retained by MultiMesh | Cache and batch lifetimes |
|
||||||
|
| Input | Ordered transforms, start and count | Placement group and batch plan | MultiMesh transform buffer | Borrowed Array/scalars | One call |
|
||||||
|
| Input | Visibility end/margin and shadow flag | Renderer configuration | MultiMeshInstance3D properties | Copied scalars | One call |
|
||||||
|
| Output | Attached `MultiMeshInstance3D` | Materializer | Parent SceneTree and loader Editor-owner adapter | Parent-owned node/resource | Tile lifetime |
|
||||||
|
|
||||||
|
Side effects are MultiMesh allocation, transform-buffer mutation,
|
||||||
|
MultiMeshInstance3D allocation/configuration and SceneTree attachment. The
|
||||||
|
service retains no direct reference after return.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TD
|
||||||
|
Validate[Validate parent, Mesh, transforms and count] --> MultiMesh[Allocate TRANSFORM_3D MultiMesh]
|
||||||
|
MultiMesh --> MeshIdentity[Assign exact prepared Mesh]
|
||||||
|
MeshIdentity --> Count[Set instance count]
|
||||||
|
Count --> Upload[Upload transforms start plus ordered batch offset]
|
||||||
|
Upload --> Node[Create named MultiMeshInstance3D]
|
||||||
|
Node --> Render[Apply shadow and optional visibility end/margin]
|
||||||
|
Render --> Attach[Attach once to supplied parent]
|
||||||
|
Attach --> Return[Return attached node]
|
||||||
|
Validate -->|invalid| Null[Return null without attachment]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
stateDiagram-v2
|
||||||
|
[*] --> Validating
|
||||||
|
Validating --> Rejected: invalid or empty input
|
||||||
|
Validating --> Building: valid slice
|
||||||
|
Building --> Configured: transforms and render properties set
|
||||||
|
Configured --> Attached: parent add_child
|
||||||
|
Rejected --> [*]
|
||||||
|
Attached --> [*]: parent/tile owner releases subtree
|
||||||
|
```
|
||||||
|
|
||||||
|
The `RefCounted` service is stateless; states describe a single call.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
sequenceDiagram
|
||||||
|
participant L as StreamingWorldLoader
|
||||||
|
participant P as M2BuildBatchPlanner
|
||||||
|
participant M as M2StaticBatchMaterializer
|
||||||
|
participant R as M2 parent root
|
||||||
|
L->>P: plan_batch(transform count, offset, static limits)
|
||||||
|
P-->>L: start/count and cursor transition
|
||||||
|
L->>L: resolve prepared Mesh or rotate pending job
|
||||||
|
L->>M: materialize_batch(parent, Mesh, transform slice, render settings)
|
||||||
|
M->>M: allocate MultiMesh and upload ordered transforms
|
||||||
|
M->>R: add_child(MultiMeshInstance3D)
|
||||||
|
M-->>L: attached node
|
||||||
|
L->>L: assign optional Editor owner
|
||||||
|
```
|
||||||
|
|
||||||
|
## Dependency diagram
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart TB
|
||||||
|
Loader[StreamingWorldLoader] --> Materializer[M2StaticBatchMaterializer]
|
||||||
|
Planner[M2BuildBatchPlanner] --> Loader
|
||||||
|
MeshState[M2MeshResourceCacheState] --> Loader
|
||||||
|
Materializer --> Engine[Mesh / MultiMesh / MultiMeshInstance3D]
|
||||||
|
Loader --> EditorOwner[Editor ownership]
|
||||||
|
Loader --> Scheduler[RenderBudgetScheduler]
|
||||||
|
Materializer -. no dependency .-> ResourceIO[ResourceLoader / files]
|
||||||
|
Materializer -. no dependency .-> BuildState[Build jobs / queues / caches]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- The caller borrows a prepared Mesh and transform Array for the synchronous call.
|
||||||
|
- The new MultiMesh retains the exact Mesh Resource reference.
|
||||||
|
- The new MultiMeshInstance3D retains the MultiMesh and transfers to the parent.
|
||||||
|
- Parent/tile teardown owns node and Resource release through the SceneTree.
|
||||||
|
- Loader assigns an Editor owner after successful attachment when configured.
|
||||||
|
- All Resource and SceneTree mutation is main-thread-only.
|
||||||
|
- No RID is acquired or released directly by this service.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Null parent or Mesh | Entry guard | Return null without allocation/attachment | Synthetic contract fixture | Correct build composition/cache state |
|
||||||
|
| Empty transforms or non-positive count | Entry guard | Return null | Synthetic contract fixture | Planner supplies non-empty slice |
|
||||||
|
| Invalid transform bounds | Caller precondition | Historical indexed access behavior remains | Build planner/source regression | Repair job cursor/group state |
|
||||||
|
| Mesh pending | Outside service | Loader rotates job without calling service | Existing build/cache diagnostics | Async completion retries job |
|
||||||
|
| Tile cancelled | Outside service | Loader cancels before materialization or releases parent later | Shutdown regression | Eligible tile may requeue |
|
||||||
|
|
||||||
|
The service does not swallow invalid bounds or silently clamp/reorder transforms,
|
||||||
|
because doing so would change the accepted build-planner contract.
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Transform format | `MultiMesh.TRANSFORM_3D` | All | No | Preserves full 3D M2 placements |
|
||||||
|
| Static batch count | Loader `m2_multimesh_batch_size` through planner | Quality preset | Yes, between plans | Controls per-batch transform count |
|
||||||
|
| Visibility end/margin | Loader M2 range / chunk size | Quality preset | Yes, between calls | Applied only when end is positive |
|
||||||
|
| Shadow mode | Loader `m2_cast_shadows` | Quality preset | Yes, between calls | Selects ON/OFF casting setting |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
No state is serialized and no cache/schema version changes. The service consumes
|
||||||
|
the accepted prepared Mesh cache contract; no rebake or migration is required.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- The service emits no logs, metrics or debug views.
|
||||||
|
- Existing loader queue/hitch metrics continue to account for `m2build` work.
|
||||||
|
- Path/serial remain visible through the unchanged batch node name.
|
||||||
|
- Editor ownership remains observable through the existing loader adapter.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- `verify_m2_static_batch_materializer.gd` covers invalid inputs, node naming,
|
||||||
|
exact Mesh identity, transform format/count, ordered slice source contract,
|
||||||
|
visibility, shadows, single attachment, ownership boundaries and timing.
|
||||||
|
- Build planner, Mesh cache/extractor/finalizer, prototype, animated materializer,
|
||||||
|
shutdown, materials, facade and internal-access regressions protect neighbors.
|
||||||
|
- The headless dummy renderer does not round-trip per-instance transforms through
|
||||||
|
`MultiMesh.get_instance_transform`; exact ordered upload is protected by the
|
||||||
|
source contract and render checkpoints remain the observable regression gate.
|
||||||
|
- Fidelity evidence is exact extraction only; no new build-12340 parity claim.
|
||||||
|
- Performance budget: 10,000 transform API writes under one second in headless.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
Measured spatial-cell batching may later provide smaller transform slices while
|
||||||
|
reusing this materializer. A generic static/animated base class, async mutation
|
||||||
|
and callback framework are intentionally excluded.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Static M2 MultiMesh materialization | Implemented extraction | Synthetic node/resource/render/source/timing verifier | Asset-backed traversal pending |
|
||||||
|
| Transform ordering | Implemented extraction | Exact source contract and existing checkpoints | Headless per-instance getter unavailable |
|
||||||
|
| Editor persistence | Existing loader-owned | Source boundary and editor helper | Editor integration fixture remains separate |
|
||||||
|
| Spatial-cell batching | Planned | Renderer roadmap | Culling/performance design and evidence pending |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Materialization remains synchronous main-thread work under the existing permit.
|
||||||
|
- MultiMesh transform upload timing in a headless dummy renderer does not measure GPU cost.
|
||||||
|
- No private asset traversal, visual comparison, descriptor-pressure, leak or p95/p99 run exists.
|
||||||
|
- Current model-path batching is not culling-driven spatial-cell batching.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh allocation, transform upload, render setup and attachment |
|
||||||
|
| `src/render/m2/m2_build_batch_planner.gd` | Static/animated batch count and cursor planning |
|
||||||
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static Mesh reference ownership |
|
||||||
|
| `src/scenes/streaming/streaming_world_loader.gd` | Resource readiness, job/budget adapter and Editor ownership |
|
||||||
|
| `src/tools/verify_m2_static_batch_materializer.gd` | Node/resource/render/source/timing regression |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- [`m2-build-batch-planner.md`](m2-build-batch-planner.md)
|
||||||
|
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
|
||||||
|
- [`m2-animated-instance-materializer.md`](m2-animated-instance-materializer.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)
|
||||||
@@ -54,6 +54,8 @@ flowchart LR
|
|||||||
M2Transform --> Loader
|
M2Transform --> Loader
|
||||||
Loader --> M2Batch[M2BuildBatchPlanner]
|
Loader --> M2Batch[M2BuildBatchPlanner]
|
||||||
M2Batch --> Loader
|
M2Batch --> Loader
|
||||||
|
Loader --> M2Static[M2StaticBatchMaterializer]
|
||||||
|
M2Static --> Scene
|
||||||
Loader --> WmoPlacement[WmoPlacementResolver]
|
Loader --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> Loader
|
WmoPlacement --> Loader
|
||||||
Loader --> WmoRegistry[WmoPlacementRegistry]
|
Loader --> WmoRegistry[WmoPlacementRegistry]
|
||||||
@@ -132,6 +134,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
|||||||
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
|
| `M2PlacementTransformResolver.resolve_basis/resolve_origin_offset` | Internal pure M2 service | Resolves regular and calibrated model-specific ADT placement transforms | Worker/main thread; stateless | Unknown paths use regular basis and zero offset |
|
||||||
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
|
| `M2PlacementGrouper.group_placements` | Internal pure M2 service | Validates and groups ordered tile-local placement transforms by normalized path | Worker/main thread; stateless | Invalid variants/name IDs/empty paths are skipped |
|
||||||
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
| `M2BuildBatchPlanner.plan_batch` | Internal pure M2 service | Selects static/animated batch count and next group cursor | Main/any thread; stateless | Non-positive selected limit clamps to one; empty range completes |
|
||||||
|
| `M2StaticBatchMaterializer.materialize_batch` | Internal M2 scene-materialization service | Builds and attaches one prepared-Mesh MultiMesh transform slice | Renderer main thread; stateless after each call | Invalid/empty input returns null; bounds are caller precondition |
|
||||||
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
| `M2RuntimeMeshRebuildClassifier` | Internal memoized M2 service | Detects billboard/UV-rotation metadata requiring stale cached-mesh rebuild | Renderer main thread; cached until reset | Invalid variants/indices skipped; first path decision wins |
|
||||||
| `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
| `M2AnimationLoadPipelineState` | Internal M2 async-state service | Owns animated scene pending request records and terminal finalize FIFO | Renderer main thread; map/session | Empty/duplicate/unknown transitions rejected |
|
||||||
| `M2AnimatedSceneFinalizer` | Internal M2 scene-finalization service | Instantiates candidates, repairs materials and requires AnimationPlayer descendants | Renderer main thread; stateless after each call | Invalid/rejected detached roots are freed |
|
| `M2AnimatedSceneFinalizer` | Internal M2 scene-finalization service | Instantiates candidates, repairs materials and requires AnimationPlayer descendants | Renderer main thread; stateless after each call | Invalid/rejected detached roots are freed |
|
||||||
@@ -179,6 +182,7 @@ loader configuration remains transitional composition data, not a caller API.
|
|||||||
| Internal transform | Rotation/path/scale | Loader or grouper / `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 |
|
| Internal grouping | Tile origin, M2 names and placements | Loader / `M2PlacementGrouper` | Loader worker result/build job | Fresh Dictionary/Transform3D arrays | One grouping task |
|
||||||
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
|
| Internal batch plan | Transform count/offset, path kind and limits | Loader / `M2BuildBatchPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One build operation |
|
||||||
|
| Internal static M2 materialization | Parent, prepared Mesh, ordered transform slice and render settings | Loader / `M2StaticBatchMaterializer` | Attached MultiMeshInstance3D | Parent owns node/MultiMesh; exact Mesh reference retained | One main-thread build batch |
|
||||||
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
|
| Internal WMO placement | Path, MODF placement, tile/index | Loader / `WmoPlacementResolver` | WMO caches, registry and three instance adapters | Value-only String/Transform3D | Lookup/placement lifetime |
|
||||||
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
|
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
|
||||||
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
|
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
|
||||||
@@ -239,7 +243,8 @@ flowchart TD
|
|||||||
M2Registry --> M2Grouper[M2PlacementGrouper]
|
M2Registry --> M2Grouper[M2PlacementGrouper]
|
||||||
M2Transform[M2PlacementTransformResolver] --> M2Grouper
|
M2Transform[M2PlacementTransformResolver] --> M2Grouper
|
||||||
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
M2Grouper --> M2Batch[M2BuildBatchPlanner]
|
||||||
M2Batch --> M2
|
M2Batch --> M2Static[M2StaticBatchMaterializer]
|
||||||
|
M2Static --> M2
|
||||||
R --> WmoPlacement[WmoPlacementResolver]
|
R --> WmoPlacement[WmoPlacementResolver]
|
||||||
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
|
WmoPlacement --> WmoRegistry[WmoPlacementRegistry]
|
||||||
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
|
WmoRegistry --> WmoBuildQueue[WmoRenderBuildQueue]
|
||||||
@@ -364,8 +369,10 @@ sequenceDiagram
|
|||||||
transforms and every build/render side effect remain loader-owned.
|
transforms and every build/render side effect remain loader-owned.
|
||||||
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
|
- `M2PlacementGrouper` is stateless and owns only call-local grouped transforms.
|
||||||
The loader retains tasks, mutex/result queues, stale checks and build state.
|
The loader retains tasks, mutex/result queues, stale checks and build state.
|
||||||
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans. The
|
- `M2BuildBatchPlanner` is stateless and owns only call-local scalar plans.
|
||||||
loader retains queue/resource transitions, cursor adoption and materialization.
|
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment;
|
||||||
|
the loader retains queue/resource transitions, cursor adoption, animated/static
|
||||||
|
dispatch, budgets and Editor ownership.
|
||||||
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
- `WmoPlacementResolver` is stateless and owns only call-local cache-key,
|
||||||
identity and transform values. `WmoPlacementRegistry` owns only placement-key
|
identity and transform values. `WmoPlacementRegistry` owns only placement-key
|
||||||
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
||||||
@@ -542,6 +549,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 |
|
| ADT streaming/terrain | Partial | M00 checkpoints and current scenes | Fidelity/performance gaps remain |
|
||||||
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
|
| Static M2 placement | Partial | MultiMesh/dedupe probes | Full materials/animation/effects |
|
||||||
|
| M2 static batch materializer | Implemented extraction | Synthetic node/Mesh/render/source/timing contract | Asset-backed traversal/visual/GPU p95/p99 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 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 |
|
| 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 |
|
||||||
@@ -615,6 +623,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/render/m2/m2_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation |
|
| `src/render/m2/m2_animated_scene_finalizer.gd` | Animated scene candidate ownership, material repair and player validation |
|
||||||
| `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
|
| `src/render/m2/m2_animation_playback_controller.gd` | Per-instance phase, selection, native copy/start and imported playback |
|
||||||
| `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment |
|
| `src/render/m2/m2_animated_instance_materializer.gd` | Animated duplicate/render/playback startup and non-empty batch attachment |
|
||||||
|
| `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh construction, render setup and attachment |
|
||||||
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
| `src/render/m2/m2_mesh_load_pipeline_state.gd` | Static M2 pending Resource paths, terminal statuses and finalize FIFO |
|
||||||
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
| `src/render/m2/m2_mesh_resource_cache_state.gd` | Prepared static M2 Mesh references and final-shutdown release |
|
||||||
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
| `src/render/m2/m2_mesh_resource_extractor.gd` | First-Mesh selection and temporary PackedScene instance lifetime |
|
||||||
@@ -640,6 +649,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression |
|
| `src/tools/verify_m2_animated_scene_finalizer.gd` | Animated M2 candidate/material/player/lifetime/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression |
|
| `src/tools/verify_m2_animation_playback_controller.gd` | Animated M2 phase/selection/playback/native/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression |
|
| `src/tools/verify_m2_animated_instance_materializer.gd` | Animated M2 order/render/playback/attachment/boundary/timing regression |
|
||||||
|
| `src/tools/verify_m2_static_batch_materializer.gd` | Static M2 node/Mesh/render/attachment/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_load_pipeline_state.gd` | Static M2 request/terminal/FIFO/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_resource_cache_state.gd` | Static M2 Mesh cache ownership/lifetime/boundary/timing regression |
|
||||||
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
| `src/tools/verify_m2_mesh_resource_extractor.gd` | M2 direct/PackedScene/subtree order/lifetime/boundary/timing regression |
|
||||||
|
|||||||
@@ -0,0 +1,56 @@
|
|||||||
|
class_name M2StaticBatchMaterializer
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Builds and attaches one static M2 MultiMesh batch from an ordered transform
|
||||||
|
## slice. All Resource and SceneTree mutation belongs on the main thread.
|
||||||
|
|
||||||
|
|
||||||
|
## Returns the attached MultiMeshInstance3D, or null for invalid/empty input.
|
||||||
|
## The supplied parent owns the returned node and its MultiMesh Resource.
|
||||||
|
## Transform bounds are a build-planner precondition and are not clamped.
|
||||||
|
func materialize_batch(
|
||||||
|
m2_parent_root: Node3D,
|
||||||
|
relative_path: String,
|
||||||
|
mesh: Mesh,
|
||||||
|
transforms: Array,
|
||||||
|
start_index: int,
|
||||||
|
instance_count: int,
|
||||||
|
batch_serial: int,
|
||||||
|
visibility_range_end: float,
|
||||||
|
visibility_range_margin: float,
|
||||||
|
cast_shadows: bool
|
||||||
|
) -> MultiMeshInstance3D:
|
||||||
|
if (
|
||||||
|
m2_parent_root == null
|
||||||
|
or mesh == null
|
||||||
|
or transforms.is_empty()
|
||||||
|
or instance_count <= 0
|
||||||
|
):
|
||||||
|
return null
|
||||||
|
|
||||||
|
var multimesh := MultiMesh.new()
|
||||||
|
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||||
|
multimesh.mesh = mesh
|
||||||
|
multimesh.instance_count = instance_count
|
||||||
|
for batch_offset in instance_count:
|
||||||
|
multimesh.set_instance_transform(
|
||||||
|
batch_offset,
|
||||||
|
transforms[start_index + batch_offset]
|
||||||
|
)
|
||||||
|
|
||||||
|
var multimesh_instance := MultiMeshInstance3D.new()
|
||||||
|
multimesh_instance.name = "%s_%d" % [
|
||||||
|
relative_path.get_file().get_basename(),
|
||||||
|
batch_serial,
|
||||||
|
]
|
||||||
|
multimesh_instance.multimesh = multimesh
|
||||||
|
multimesh_instance.cast_shadow = (
|
||||||
|
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||||
|
if cast_shadows
|
||||||
|
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
|
)
|
||||||
|
if visibility_range_end > 0.0:
|
||||||
|
multimesh_instance.visibility_range_end = visibility_range_end
|
||||||
|
multimesh_instance.visibility_range_end_margin = visibility_range_margin
|
||||||
|
m2_parent_root.add_child(multimesh_instance)
|
||||||
|
return multimesh_instance
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bgiw27rkh8fbn
|
||||||
@@ -60,6 +60,9 @@ const M2_PLACEMENT_GROUPER_SCRIPT := preload(
|
|||||||
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
|
const M2_BUILD_BATCH_PLANNER_SCRIPT := preload(
|
||||||
"res://src/render/m2/m2_build_batch_planner.gd"
|
"res://src/render/m2/m2_build_batch_planner.gd"
|
||||||
)
|
)
|
||||||
|
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
|
||||||
|
"res://src/render/m2/m2_static_batch_materializer.gd"
|
||||||
|
)
|
||||||
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
const M2_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||||
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
"res://src/render/m2/m2_runtime_mesh_finalizer.gd"
|
||||||
)
|
)
|
||||||
@@ -273,6 +276,7 @@ var _m2_unique_placement_registry := (
|
|||||||
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
var _m2_placement_transform_resolver := M2_PLACEMENT_TRANSFORM_RESOLVER_SCRIPT.new()
|
||||||
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
|
var _m2_placement_grouper := M2_PLACEMENT_GROUPER_SCRIPT.new()
|
||||||
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
|
var _m2_build_batch_planner := M2_BUILD_BATCH_PLANNER_SCRIPT.new()
|
||||||
|
var _m2_static_batch_materializer := M2_STATIC_BATCH_MATERIALIZER_SCRIPT.new()
|
||||||
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
|
var _m2_runtime_mesh_finalizer := M2_RUNTIME_MESH_FINALIZER_SCRIPT.new()
|
||||||
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
|
var _m2_raw_model_repository := M2_RAW_MODEL_REPOSITORY_SCRIPT.new()
|
||||||
var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()
|
var _m2_prototype_cache_state := M2_PROTOTYPE_CACHE_STATE_SCRIPT.new()
|
||||||
@@ -4396,29 +4400,20 @@ func _materialize_m2_group_batch(
|
|||||||
start: int,
|
start: int,
|
||||||
count: int,
|
count: int,
|
||||||
serial: int) -> void:
|
serial: int) -> void:
|
||||||
if transforms.is_empty() or count <= 0 or mesh == null:
|
var multimesh_instance := _m2_static_batch_materializer.materialize_batch(
|
||||||
return
|
m2_root,
|
||||||
|
rel_path,
|
||||||
var mm := MultiMesh.new()
|
mesh,
|
||||||
mm.transform_format = MultiMesh.TRANSFORM_3D
|
transforms,
|
||||||
mm.mesh = mesh
|
start,
|
||||||
mm.instance_count = count
|
count,
|
||||||
for i in count:
|
serial,
|
||||||
mm.set_instance_transform(i, transforms[start + i])
|
m2_visibility_range,
|
||||||
|
CHUNK_SIZE,
|
||||||
var mmi := MultiMeshInstance3D.new()
|
m2_cast_shadows
|
||||||
mmi.name = "%s_%d" % [rel_path.get_file().get_basename(), serial]
|
|
||||||
mmi.multimesh = mm
|
|
||||||
mmi.cast_shadow = (
|
|
||||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
|
||||||
if m2_cast_shadows
|
|
||||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
|
||||||
)
|
)
|
||||||
if m2_visibility_range > 0.0:
|
if multimesh_instance != null:
|
||||||
mmi.visibility_range_end = m2_visibility_range
|
_set_editor_owner_recursive(multimesh_instance)
|
||||||
mmi.visibility_range_end_margin = CHUNK_SIZE
|
|
||||||
m2_root.add_child(mmi)
|
|
||||||
_set_editor_owner_recursive(mmi)
|
|
||||||
|
|
||||||
|
|
||||||
func _materialize_m2_animated_batch(
|
func _materialize_m2_animated_batch(
|
||||||
|
|||||||
@@ -0,0 +1,255 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Synthetic MultiMesh identity, transform order, render settings, ownership
|
||||||
|
## boundary and bounded-timing regression for static M2 batch materialization.
|
||||||
|
|
||||||
|
const MATERIALIZER_SCRIPT := preload(
|
||||||
|
"res://src/render/m2/m2_static_batch_materializer.gd"
|
||||||
|
)
|
||||||
|
const MATERIALIZER_PATH := "res://src/render/m2/m2_static_batch_materializer.gd"
|
||||||
|
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_batch_contract(failures)
|
||||||
|
_verify_zero_visibility_contract(failures)
|
||||||
|
_verify_invalid_inputs(failures)
|
||||||
|
_verify_ownership_boundaries(failures)
|
||||||
|
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("M2_STATIC_BATCH_MATERIALIZER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print(
|
||||||
|
"M2_STATIC_BATCH_MATERIALIZER PASS cases=11 instances=10000 elapsed_ms=%.3f"
|
||||||
|
% elapsed_milliseconds
|
||||||
|
)
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_batch_contract(failures: Array[String]) -> void:
|
||||||
|
var materializer = MATERIALIZER_SCRIPT.new()
|
||||||
|
var parent_root := Node3D.new()
|
||||||
|
get_root().add_child(parent_root)
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
var transforms: Array = [
|
||||||
|
Transform3D(Basis.IDENTITY, Vector3.ZERO),
|
||||||
|
Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0)),
|
||||||
|
Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0)),
|
||||||
|
]
|
||||||
|
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
|
||||||
|
parent_root,
|
||||||
|
"World\\Doodads\\StaticTree.m2",
|
||||||
|
mesh,
|
||||||
|
transforms,
|
||||||
|
1,
|
||||||
|
2,
|
||||||
|
7,
|
||||||
|
345.0,
|
||||||
|
16.0,
|
||||||
|
false
|
||||||
|
)
|
||||||
|
_expect_true(multimesh_instance != null, "valid batch returned", failures)
|
||||||
|
_expect_same(multimesh_instance.get_parent(), parent_root, "batch attached once", failures)
|
||||||
|
_expect_equal(parent_root.get_child_count(), 1, "one parent child", failures)
|
||||||
|
_expect_string_equal(multimesh_instance.name, "StaticTree_7", "batch name", failures)
|
||||||
|
_expect_equal(multimesh_instance.multimesh.transform_format, MultiMesh.TRANSFORM_3D, "3D transform format", failures)
|
||||||
|
_expect_same(multimesh_instance.multimesh.mesh, mesh, "exact Mesh identity", failures)
|
||||||
|
_expect_equal(multimesh_instance.multimesh.instance_count, 2, "instance count", failures)
|
||||||
|
_expect_equal(
|
||||||
|
multimesh_instance.cast_shadow,
|
||||||
|
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
||||||
|
"shadow mode",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_float_equal(multimesh_instance.visibility_range_end, 345.0, "visibility end", failures)
|
||||||
|
_expect_float_equal(multimesh_instance.visibility_range_end_margin, 16.0, "visibility margin", failures)
|
||||||
|
parent_root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_zero_visibility_contract(failures: Array[String]) -> void:
|
||||||
|
var materializer = MATERIALIZER_SCRIPT.new()
|
||||||
|
var parent_root := Node3D.new()
|
||||||
|
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
|
||||||
|
parent_root,
|
||||||
|
"world/static.m2",
|
||||||
|
ArrayMesh.new(),
|
||||||
|
[Transform3D.IDENTITY],
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
0,
|
||||||
|
0.0,
|
||||||
|
99.0,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
_expect_float_equal(multimesh_instance.visibility_range_end, 0.0, "disabled visibility unchanged", failures)
|
||||||
|
_expect_equal(
|
||||||
|
multimesh_instance.cast_shadow,
|
||||||
|
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||||
|
"enabled shadow mode",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
parent_root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_invalid_inputs(failures: Array[String]) -> void:
|
||||||
|
var materializer = MATERIALIZER_SCRIPT.new()
|
||||||
|
var parent_root := Node3D.new()
|
||||||
|
var mesh := ArrayMesh.new()
|
||||||
|
var transforms: Array = [Transform3D.IDENTITY]
|
||||||
|
_expect_true(
|
||||||
|
materializer.materialize_batch(
|
||||||
|
null, "a.m2", mesh, transforms, 0, 1, 0, 0.0, 0.0, true
|
||||||
|
) == null,
|
||||||
|
"null parent rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
materializer.materialize_batch(
|
||||||
|
parent_root, "a.m2", null, transforms, 0, 1, 0, 0.0, 0.0, true
|
||||||
|
) == null,
|
||||||
|
"null Mesh rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
materializer.materialize_batch(
|
||||||
|
parent_root, "a.m2", mesh, [], 0, 1, 0, 0.0, 0.0, true
|
||||||
|
) == null,
|
||||||
|
"empty transforms rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
materializer.materialize_batch(
|
||||||
|
parent_root, "a.m2", mesh, transforms, 0, 0, 0, 0.0, 0.0, true
|
||||||
|
) == null,
|
||||||
|
"zero count rejected",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(parent_root.get_child_count(), 0, "invalid input has no attachment", failures)
|
||||||
|
parent_root.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||||
|
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||||
|
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||||
|
var loader_adapter_source := _source_between(
|
||||||
|
loader_source,
|
||||||
|
"func _materialize_m2_group_batch(",
|
||||||
|
"func _materialize_m2_animated_batch(",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_true(
|
||||||
|
loader_source.contains("M2_STATIC_BATCH_MATERIALIZER_SCRIPT.new()"),
|
||||||
|
"loader composes materializer",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_equal(
|
||||||
|
loader_adapter_source.count("_m2_static_batch_materializer.materialize_batch("),
|
||||||
|
1,
|
||||||
|
"loader delegates once",
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
_expect_false(loader_adapter_source.contains("MultiMesh.new()"), "loader adapter omits MultiMesh construction", failures)
|
||||||
|
_expect_false(loader_adapter_source.contains("set_instance_transform("), "loader adapter omits transform upload", failures)
|
||||||
|
_expect_true(loader_adapter_source.contains("_set_editor_owner_recursive(multimesh_instance)"), "loader retains Editor ownership", failures)
|
||||||
|
for required_materializer_rule in [
|
||||||
|
"MultiMesh.TRANSFORM_3D",
|
||||||
|
"multimesh.mesh = mesh",
|
||||||
|
"multimesh.instance_count = instance_count",
|
||||||
|
"multimesh.set_instance_transform(",
|
||||||
|
"transforms[start_index + batch_offset]",
|
||||||
|
"m2_parent_root.add_child(multimesh_instance)",
|
||||||
|
]:
|
||||||
|
_expect_true(
|
||||||
|
materializer_source.contains(required_materializer_rule),
|
||||||
|
"materializer owns %s" % required_materializer_rule,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
for forbidden_dependency in [
|
||||||
|
"ResourceLoader.",
|
||||||
|
"FileAccess.",
|
||||||
|
"WorkerThreadPool.",
|
||||||
|
"RenderingServer.",
|
||||||
|
".owner =",
|
||||||
|
"_m2_build_jobs",
|
||||||
|
"_render_budget_scheduler",
|
||||||
|
]:
|
||||||
|
_expect_false(
|
||||||
|
materializer_source.contains(forbidden_dependency),
|
||||||
|
"materializer omits %s ownership" % forbidden_dependency,
|
||||||
|
failures
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||||
|
var materializer = MATERIALIZER_SCRIPT.new()
|
||||||
|
var parent_root := Node3D.new()
|
||||||
|
get_root().add_child(parent_root)
|
||||||
|
var transforms: Array = []
|
||||||
|
for instance_index in 10000:
|
||||||
|
transforms.append(
|
||||||
|
Transform3D(Basis.IDENTITY, Vector3(float(instance_index), 0.0, 0.0))
|
||||||
|
)
|
||||||
|
var started_microseconds := Time.get_ticks_usec()
|
||||||
|
var multimesh_instance: MultiMeshInstance3D = materializer.materialize_batch(
|
||||||
|
parent_root,
|
||||||
|
"world/timing.m2",
|
||||||
|
ArrayMesh.new(),
|
||||||
|
transforms,
|
||||||
|
0,
|
||||||
|
10000,
|
||||||
|
1,
|
||||||
|
0.0,
|
||||||
|
0.0,
|
||||||
|
true
|
||||||
|
)
|
||||||
|
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||||
|
_expect_equal(multimesh_instance.multimesh.instance_count, 10000, "timing batch complete", failures)
|
||||||
|
_expect_true(elapsed_milliseconds < 1000.0, "10000 transforms under one second", failures)
|
||||||
|
parent_root.free()
|
||||||
|
return elapsed_milliseconds
|
||||||
|
|
||||||
|
|
||||||
|
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_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not condition:
|
||||||
|
failures.append(label)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_true(not condition, label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_equal_approx(actual, expected):
|
||||||
|
failures.append("%s expected=%.6f actual=%.6f" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual != expected:
|
||||||
|
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||||
|
if not is_same(actual, expected):
|
||||||
|
failures.append(label)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://hdpp6alfpaa3
|
||||||
@@ -27,7 +27,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Date: 2026-07-18
|
- Date: 2026-07-18
|
||||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`, `1de50f9`, `3dccd3e`, `e7a7c67`, `c0fc191`;
|
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`, `1de50f9`, `3dccd3e`, `e7a7c67`, `c0fc191`, `c251985`;
|
||||||
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`,
|
||||||
@@ -53,7 +53,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
`M03-RND-M2-PROTOTYPE-CACHE-001`,
|
`M03-RND-M2-PROTOTYPE-CACHE-001`,
|
||||||
`M03-RND-M2-ANIMATION-LOAD-PIPELINE-001`,
|
`M03-RND-M2-ANIMATION-LOAD-PIPELINE-001`,
|
||||||
`M03-RND-M2-ANIMATED-SCENE-FINALIZER-001`,
|
`M03-RND-M2-ANIMATED-SCENE-FINALIZER-001`,
|
||||||
`M03-RND-M2-ANIMATION-PLAYBACK-001`
|
`M03-RND-M2-ANIMATION-PLAYBACK-001`,
|
||||||
|
`M03-RND-M2-ANIMATED-INSTANCE-MATERIALIZER-001`,
|
||||||
|
`M03-RND-M2-STATIC-BATCH-MATERIALIZER-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
|
||||||
@@ -227,6 +229,14 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
Post-merge materializer (`13.719ms`), playback/finalizer, shutdown, facade,
|
Post-merge materializer (`13.719ms`), playback/finalizer, shutdown, facade,
|
||||||
internal-access `30`, checkpoint dry-run `7/7`, documentation and coordination
|
internal-access `30`, checkpoint dry-run `7/7`, documentation and coordination
|
||||||
gates remained green.
|
gates remained green.
|
||||||
|
M2 static batch materializer passed `cases=11 instances=10000
|
||||||
|
elapsed_ms=0.552` with invalid-input guards, exact Mesh identity, 3D transform
|
||||||
|
format/count, ordered slice source contract, visibility, shadows, single
|
||||||
|
attachment and loader ownership boundaries. All 51 autonomous headless
|
||||||
|
regressions and checkpoint dry-run `7/7` passed; the proprietary ADT probe
|
||||||
|
remained unavailable. Documentation and coordination gates passed; internal-
|
||||||
|
access inventory remains `30` and static resource readiness/cursors/budgets/
|
||||||
|
Editor ownership remain in the loader adapter.
|
||||||
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
|
||||||
@@ -368,6 +378,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
M2 animated scene finalizer and loader candidate/material/player traversal adapters,
|
M2 animated scene finalizer and loader candidate/material/player traversal adapters,
|
||||||
M2 animation playback controller and loader native/imported playback adapters,
|
M2 animation playback controller and loader native/imported playback adapters,
|
||||||
M2 animated instance materializer and loader log/Editor-owner adapter,
|
M2 animated instance materializer and loader log/Editor-owner adapter,
|
||||||
|
M2 static batch materializer and loader readiness/cursor/Editor-owner 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: worker concurrency and stale-result validation remain
|
- Remaining risks: worker concurrency and stale-result validation remain
|
||||||
streamer-owned; cancellation stops new permits but does not interrupt
|
streamer-owned; cancellation stops new permits but does not interrupt
|
||||||
@@ -388,6 +399,9 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
|||||||
animation request state and Mesh adoption remain in the loader call path; raw
|
animation request state and Mesh adoption remain in the loader call path; raw
|
||||||
animated-instance materialization is now separated but remains synchronous
|
animated-instance materialization is now separated but remains synchronous
|
||||||
main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending;
|
main-thread work; asset-backed traversal/visual/leak/p95/p99 evidence is pending;
|
||||||
|
static MultiMesh materialization is also separated but remains synchronous;
|
||||||
|
headless timing does not measure transform upload GPU cost and spatial-cell
|
||||||
|
batching still needs culling/performance evidence;
|
||||||
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
|
WMO ResourceLoader/FileAccess I/O, live fallback and group materialization
|
||||||
remain in the loader;
|
remain in the loader;
|
||||||
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
||||||
|
|||||||
Reference in New Issue
Block a user