merge: static M2 build resource observer

This commit is contained in:
2026-07-18 03:02:06 +04:00
17 changed files with 555 additions and 91 deletions
+10
View File
@@ -43,6 +43,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
- `src/render/m2/m2_build_queue.gd` / `m2_build_job.gd` - typed pending M2 jobs, FIFO/stale tile keys, grouped-transform references and progress cursors without engine destruction. - `src/render/m2/m2_build_queue.gd` / `m2_build_job.gd` - typed pending M2 jobs, FIFO/stale tile keys, grouped-transform references and progress cursors without engine destruction.
- `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision. - `src/render/m2/m2_build_dispatch_planner.gd` - pure animation/static wait, materialization and missing-model advance decision.
- `src/render/m2/m2_build_resource_snapshot.gd` - typed per-step animated/static resource observations without engine destruction. - `src/render/m2/m2_build_resource_snapshot.gd` - typed per-step animated/static resource observations without engine destruction.
- `src/render/m2/m2_static_build_resource_observer.gd` - static Mesh cache lookup, threaded request selection and missing transition.
- `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.
@@ -1084,6 +1085,15 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
- ResourceLoader/cache requests, permit/cursor transitions, engine lifetime, - ResourceLoader/cache requests, permit/cursor transitions, engine lifetime,
cache formats, profiles and visible rules remain unchanged. cache formats, profiles and visible rules remain unchanged.
## 2026-07-18 M2 Static Build Resource Observer Extraction
- `M2StaticBuildResourceObserver` now owns prepared-Mesh lookup, existing-request
detection, `.tscn`-before-`.glb` candidate selection, pivot-prefix GLB rejection,
threaded request admission and terminal missing transition for build jobs.
- The observer fills `M2BuildResourceSnapshot`; the loader retains animation
observation, finalize drains, materialization, permits and engine lifetime.
- Candidate order, request errors, cache formats, profiles and visuals are unchanged.
## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction ## 2026-07-17 M2 Runtime Mesh Rebuild Classifier Extraction
- `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a - `M2RuntimeMeshRebuildClassifier` now owns the memoized decision used when a
@@ -55,14 +55,22 @@ missing transition from the loader into a service that fills
## Status ## Status
- State: active - State: ready for integration
- Done: ownership/compatibility contract - Done: implementation, verification and documentation
- Next: implementation, verification and documentation - Next: integrator merge and post-merge acceptance
- Blocked by: - Blocked by:
## Handoff ## Handoff
- Commit: - Commit: `7cb3e34` (`render: extract static M2 build resource observer`)
- Results: - Results: observer PASS (`11` cases, `20,000` iterations, `64.654ms`);
- Remaining risks: full autonomous suite PASS (`55/55`); renderer checkpoint dry-run PASS (`7/7`);
- Documentation updated: internal-access PASS (`30` private symbols); documentation PASS (`42` module
specs); coordination PASS with `30` historical warnings.
- Remaining risks: successful asynchronous admission is covered by source and
existing pipeline regressions because an undrained unit request leaks; private
asset traversal, p95/p99, leak and visual evidence remain unavailable;
animated observation and static finalize polling remain loader-owned.
- Documentation updated: new observer module API/I/O/data-flow/state/sequence/
dependency/ownership specification; M2 snapshot, pipeline/cache, world-renderer,
module registry, `RENDER.md` and M03 Evidence updated.
+1
View File
@@ -23,6 +23,7 @@
| 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 build dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) | | M2 build dispatch planner | Implemented extraction | [`m2-build-dispatch-planner.md`](m2-build-dispatch-planner.md) |
| M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) | | M2 build resource snapshot | Implemented extraction | [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md) |
| M2 static build resource observer | Implemented extraction | [`m2-static-build-resource-observer.md`](m2-static-build-resource-observer.md) |
| M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) | | M2 build queue | Implemented extraction | [`m2-build-queue.md`](m2-build-queue.md) |
| M2 static batch materializer | Implemented extraction | [`m2-static-batch-materializer.md`](m2-static-batch-materializer.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) |
+7 -5
View File
@@ -187,9 +187,9 @@ queue/hitch metrics remain unchanged.
## Extension points ## Extension points
A later resource-observation service may produce this snapshot while moving `M2StaticBuildResourceObserver` now produces the static phase. A later animated
ResourceLoader/cache request ownership out of the loader. The value contract observer may produce the first phase. The value contract remains independent of
must remain independent of that service and of generic callback frameworks. both producers and of generic callback frameworks.
## Capability status ## Capability status
@@ -197,7 +197,8 @@ must remain independent of that service and of generic callback frameworks.
|---|---|---|---| |---|---|---|---|
| Typed per-step M2 resource observation | Implemented extraction | Identity/adoption/lifetime/source/timing verifier | Asset-backed traversal pending | | Typed per-step M2 resource observation | Implemented extraction | Identity/adoption/lifetime/source/timing verifier | Asset-backed traversal pending |
| Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending | | Dispatch decision | Implemented separately | Dispatch planner verifier | Asset-backed traversal pending |
| Resource lookup/request execution | Loader-owned | Cache/pipeline regressions | Service extraction pending | | Static lookup/request execution | Implemented separately | Static observer verifier | Asset-backed traversal pending |
| Animated lookup/request execution | Loader-owned | Animation regressions | Service extraction pending |
| Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending | | Engine lifetime/materialization | Loader/materializer-owned | Lifetime/materializer regressions | GPU/p95/p99 evidence pending |
## Known gaps and risks ## Known gaps and risks
@@ -213,7 +214,8 @@ must remain independent of that service and of generic callback frameworks.
|---|---| |---|---|
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observations and diagnostics | | `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step resource observations and diagnostics |
| `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning | | `src/render/m2/m2_build_dispatch_planner.gd` | Snapshot-to-action planning |
| `src/scenes/streaming/streaming_world_loader.gd` | Resource observation/request execution and materializer borrowing | | `src/render/m2/m2_static_build_resource_observer.gd` | Static snapshot production and requests |
| `src/scenes/streaming/streaming_world_loader.gd` | Animated observation/request execution and materializer borrowing |
| `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression | | `src/tools/verify_m2_build_resource_snapshot.gd` | Identity/adoption/lifetime/boundary/timing regression |
## Related decisions and references ## Related decisions and references
+2 -1
View File
@@ -206,7 +206,8 @@ rebuild policy are unchanged; no migration or rebake is required.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| Static M2 request/finalize state | Implemented extraction | Contract/source/timing verifier | Asset-backed long traversal pending | | Static M2 request/finalize state | Implemented extraction | Contract/source/timing verifier | Asset-backed long traversal pending |
| ResourceLoader I/O | Existing loader-owned | Shutdown/material regressions | I/O adapter extraction optional | | Static request admission I/O | Implemented in observer | Observer/shutdown regressions | Asset-backed traversal pending |
| Static request polling/finalize I/O | Existing loader-owned | Shutdown/material regressions | Finalize extraction optional |
| Mesh cache | Implemented extraction | Mesh resource cache state verifier | Asset-backed memory/leak run pending | | Mesh cache | Implemented extraction | Mesh resource cache state verifier | Asset-backed memory/leak run pending |
| First-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending | | First-Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
| Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending | | Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
+1 -1
View File
@@ -186,7 +186,7 @@ the historical Mesh cache had no queue contribution or log site.
| Capability | Status | Evidence | Gap/next step | | Capability | Status | Evidence | Gap/next step |
|---|---|---|---| |---|---|---|---|
| Prepared static M2 Mesh cache | Implemented extraction | Contract/source/timing verifier | Asset-backed memory/leak run pending | | Prepared static M2 Mesh cache | Implemented extraction | Contract/source/timing verifier | Asset-backed memory/leak run pending |
| M2 Mesh request lifecycle | Implemented extraction | Pipeline state verifier | ResourceLoader I/O remains loader-owned | | M2 Mesh request lifecycle | Implemented extraction | Pipeline state verifier | Finalize polling remains loader-owned |
| M2 Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending | | M2 Mesh extraction | Implemented extraction | Resource/order/lifetime verifier | Asset-backed corrupt-scene fixture pending |
| M2 Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending | | M2 Mesh preparation | Implemented extraction | Runtime finalizer transition/rebuild verifier | Asset-backed material comparison pending |
@@ -0,0 +1,209 @@
# M2 Static Build Resource Observer
## Metadata
| Field | Value |
|---|---|
| Status | Implemented extraction |
| Target/work package | M03 / `M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-001` |
| Owners | Static build Mesh lookup, request selection and missing transition |
| Last verified | Worktree `work/sindo-main-codex/m03-m2-static-build-resource-observer`, 2026-07-18 |
| Profiles/capabilities | Existing static MultiMesh M2 build path |
## Purpose
Produce the static phase of `M2BuildResourceSnapshot`: reuse a prepared Mesh,
wait for an existing request, start the first eligible cache request, or record
a terminal missing model while preserving historical path and GLB rules.
## Non-goals
- Observe animated/native prototypes or finalize threaded loads.
- Own/free Meshes, Nodes, cache entries or snapshot lifetime.
- Materialize instances, plan batches, rotate queues or consume permits.
- Change cache format, candidate order, profiles or visible output.
## Context and boundaries
```mermaid
flowchart LR
Loader[StreamingWorldLoader] --> Observer[M2StaticBuildResourceObserver]
MeshCache[M2MeshResourceCacheState] --> Observer
Missing[M2PrototypeCacheState] --> Observer
Pipeline[M2MeshLoadPipelineState] --> Observer
Observer --> Snapshot[M2BuildResourceSnapshot]
Snapshot --> Dispatch[M2BuildDispatchPlanner]
```
## Public API
| Symbol | Kind | Purpose | Errors |
|---|---|---|---|
| `observe(snapshot, path, cache_dir, mesh_cache, prototype_cache, pipeline)` | Command/query | Adopt cached/pending/requested/missing static result | Invalid inputs return `rejected` |
| `cache_resource_paths(cache_dir, path, extensions)` | Pure query | Return unique historical nested/lowercase/basename candidates | Empty values yield fewer/empty candidates |
| `glb_animation_schema(path)` | Read query | Read OpenWC schema marker from GLB JSON | Invalid/missing GLB returns empty String |
| `OUTCOME_*` | Constants | Stable diagnostic transition identifiers | None |
## Inputs and outputs
| Direction | Data | Producer | Consumer | Ownership/lifetime |
|---|---|---|---|---|
| Input | Normalized M2 path/cache directory | Loader | Observer | Copied Strings; one observation |
| Input | Mesh/missing/request states | M2 state services | Observer | Borrowed services; map/session |
| Input/output | Resource snapshot | Loader | Observer/dispatch/materializer | Borrowed; one build operation |
| Output | Cached Mesh or missing/pending values | Observer | Snapshot | Exact borrowed Mesh/scalars |
| Output | Threaded request record | Observer | Mesh pipeline | Pipeline-owned path record |
| Output | Outcome StringName | Observer | Tests/future diagnostics | Immutable scalar |
## Data flow
```mermaid
flowchart TD
Start[Observe static path] --> Cached{Mesh cached?}
Cached -->|yes| AdoptMesh[Adopt exact Mesh; cached]
Cached -->|no| Missing{Already missing?}
Missing -->|yes| AdoptMissing[Adopt missing]
Missing -->|no| Pending{Request pending?}
Pending -->|yes| AdoptPending[Adopt unresolved; pending]
Pending -->|no| Candidates[Generate tscn then glb candidates]
Candidates --> Eligible{Exists and not pivot-prefix GLB?}
Eligible -->|yes| Request[Threaded request]
Request -->|OK or busy| Remember[Remember request; requested]
Eligible -->|none/error| MarkMissing[Mark/adopt missing]
```
## Lifecycle/state
```mermaid
stateDiagram-v2
[*] --> Observing
Observing --> Cached
Observing --> Pending
Observing --> Requested
Observing --> Missing
Observing --> Rejected
Cached --> [*]
Pending --> [*]
Requested --> [*]
Missing --> [*]
Rejected --> [*]
```
## Main sequence
```mermaid
sequenceDiagram
participant L as StreamingWorldLoader
participant O as StaticBuildResourceObserver
participant C as Mesh/prototype cache state
participant P as MeshLoadPipelineState
participant R as ResourceLoader
participant S as M2BuildResourceSnapshot
L->>O: observe(snapshot, path, cache_dir, states)
O->>C: cached/missing lookup
alt cache hit or missing
O->>S: adopt static observation
else pending
O->>P: has_request
O->>S: adopt unresolved observation
else request candidate
O->>R: exists + load_threaded_request
O->>P: remember_request
O->>S: adopt unresolved observation
end
```
## Dependency diagram
```mermaid
flowchart TB
Observer[M2StaticBuildResourceObserver] --> ResourceLoader
Observer --> FileAccess[GLB header/JSON read]
Observer --> MeshCache[M2MeshResourceCacheState]
Observer --> PrototypeCache[M2PrototypeCacheState]
Observer --> Pipeline[M2MeshLoadPipelineState]
Observer --> Snapshot[M2BuildResourceSnapshot]
Observer -. no dependency .-> SceneTree
Observer -. no dependency .-> Materializers
Observer -. no dependency .-> Scheduler
```
## Ownership, threading and resources
- Observer retains no request, cache, snapshot or engine reference.
- Cache/pipeline services retain their existing state and resource ownership.
- Snapshot borrows the exact cached Mesh; observer never frees it.
- Resource existence/request and GLB inspection run on renderer main thread.
- Finalization and Mesh preparation remain in the loader drain path.
## Errors, cancellation and recovery
| State | Behavior | Recovery |
|---|---|---|
| Invalid dependency/path | Return `rejected`; no mutation | Correct composition |
| Existing request | Return `pending`; no duplicate request | Retry when queue rotates |
| Pivot-prefix GLB | Skip as static candidate | Continue next candidate |
| Request error | Stop historical search and mark missing | Later reset/new session |
| No candidate | Mark/adopt terminal missing | Cache content becomes available in new session |
| Tile cancellation | Observer retains nothing | Loader/queue cancellation remains authoritative |
## Configuration and capabilities
The observer consumes existing `m2_cache_dir`. It adds no profile, cache version,
batch, visibility, shadow or scheduler setting.
## Persistence, cache and migration
No new persistence is introduced. Existing `.tscn/.glb` layout, pipeline records
and missing-cache lifetime are unchanged; no rebake is required.
## Diagnostics and observability
Returned outcomes distinguish rejected/cached/pending/requested/missing without
logging. Existing queue and hitch metrics remain unchanged.
## Verification
- Dedicated verifier covers invalid, cached identity, missing, pending, path
order/deduplication, absent GLB schema, source ownership and bounded timing.
- Snapshot/dispatch/pipeline/cache/shutdown/facade/internal-access/checkpoint
regressions protect adjacent behavior.
- No original-client visual claim is made; this is bookkeeping/I/O extraction.
## Extension points
Animated/native observation may become a separate producer of the animation
phase. Shared cache candidate/GLB inspection can later be reused without adding
a generic resource framework.
## Capability status
| Capability | Status | Evidence | Gap |
|---|---|---|---|
| Static build lookup/request production | Implemented extraction | Lifecycle/path/source/timing verifier | Asset-backed traversal pending |
| Static finalize/preparation | Existing loader-owned | Pipeline/finalizer regressions | Later extraction |
| Animated observation | Existing loader-owned | Animation regressions | Separate producer pending |
## Known gaps and risks
- Successful request behavior is protected by source plus existing pipeline
tests; starting an undrained asynchronous request in the unit fixture would leak.
- GLB schema parser reads synchronously, matching the previous loader behavior.
- Private traversal, p95/p99, leak and visual evidence remain unavailable.
## Source map
| Path | Responsibility |
|---|---|
| `src/render/m2/m2_static_build_resource_observer.gd` | Static cache/request/missing observation |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed observation result |
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, finalize drain and action execution |
| `src/tools/verify_m2_static_build_resource_observer.gd` | Lifecycle/path/boundary/timing regression |
## Related decisions and references
- [`m2-build-resource-snapshot.md`](m2-build-resource-snapshot.md)
- [`m2-mesh-load-pipeline-state.md`](m2-mesh-load-pipeline-state.md)
- [`m2-mesh-resource-cache-state.md`](m2-mesh-resource-cache-state.md)
- [`world-renderer.md`](world-renderer.md)
- [`../../RENDER.md`](../../RENDER.md)
+15 -8
View File
@@ -142,6 +142,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
| `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 |
| `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits | | `M2BuildDispatchPlanner.plan_step` | Internal pure M2 service | Selects wait, animated/static materialization or no-Node advancement from observed resource state | Main/any thread; stateless | Pending animation has priority; unresolved static Mesh waits |
| `M2BuildResourceSnapshot` | Internal M2 value contract | Carries per-step normalized path, animated/static references and pending/missing observations | Renderer main thread; one build operation | Values retained exactly; release never frees resources | | `M2BuildResourceSnapshot` | Internal M2 value contract | Carries per-step normalized path, animated/static references and pending/missing observations | Renderer main thread; one build operation | Values retained exactly; release never frees resources |
| `M2StaticBuildResourceObserver.observe` | Internal M2 I/O service | Produces cached/pending/requested/missing static snapshot phase | Renderer main thread; stateless | Invalid composition rejected; no candidate marks missing |
| `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs | | `M2BuildQueue` / `M2BuildJob` | Internal M2 pending-state service | Own typed root/groups/cursor jobs and FIFO/stale tile keys | Renderer main thread; map session | Invalid enqueue rejected; stale keys drain independently of jobs |
| `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 | | `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 |
@@ -393,9 +394,10 @@ sequenceDiagram
without controlling engine lifetime. without controlling engine lifetime.
`M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform `M2BuildQueue` owns typed pending jobs, FIFO/stale keys, grouped-transform
references and group/offset/serial cursors without freeing engine objects. references and group/offset/serial cursors without freeing engine objects.
`M2StaticBatchMaterializer` owns static MultiMesh construction/attachment; `M2StaticBuildResourceObserver` owns static Mesh lookup/request admission and
the loader retains resource observation/requests, action execution, snapshot adoption. `M2StaticBatchMaterializer` owns static MultiMesh
cursor adoption, root cleanup, budgets and Editor ownership. construction/attachment; the loader retains animated observation/requests,
action execution, cursor adoption, root cleanup, 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
@@ -416,15 +418,17 @@ sequenceDiagram
for billboard/UV-rotation material refresh and is composed by the runtime Mesh for billboard/UV-rotation material refresh and is composed by the runtime Mesh
finalizer. The raw repository loads value data; loader retains Mesh adoption. finalizer. The raw repository loads value data; loader retains Mesh adoption.
- `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque - `M2MeshLoadPipelineState` owns static M2 pending Resource paths, opaque
terminal statuses and completion-order finalize FIFO. The loader retains cache terminal statuses and completion-order finalize FIFO. The static observer owns
path selection, ResourceLoader calls, permits and adoption decisions; prototype cache path selection, request admission and initial snapshot adoption. The
cache state owns shared missing outcomes. loader retains ResourceLoader polling/finalize, permits and terminal adoption;
prototype cache state owns shared missing outcomes.
- `M2MeshResourceCacheState` owns prepared static Mesh references and releases - `M2MeshResourceCacheState` owns prepared static Mesh references and releases
them at the existing final-shutdown site. Prototype state and materialization them at the existing final-shutdown site. Prototype state and materialization
belong to the sibling cache service and loader respectively. belong to the sibling cache service and loader respectively.
- `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary - `M2MeshResourceExtractor` owns depth-first first-Mesh selection and temporary
PackedScene instance destruction. The loader retains ResourceLoader I/O, PackedScene instance destruction. The static observer admits ResourceLoader
cache/missing adoption and materialization. requests and initial cache/missing adoption; the loader retains polling,
terminal adoption and materialization.
- `M2RuntimeMeshFinalizer` owns refresh version `2`, classifier lifetime, - `M2RuntimeMeshFinalizer` owns refresh version `2`, classifier lifetime,
M2Builder rebuild and original-Mesh fallback. The loader loads raw data only M2Builder rebuild and original-Mesh fallback. The loader loads raw data only
after the finalizer reports that a cached Mesh is stale. after the finalizer reports that a cached Mesh is stale.
@@ -579,6 +583,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Spatial cells and asset-backed p95/p99 remain pending | | M2 build batch planner | Implemented extraction | Scene-free limit/count/cursor/source/timing contract | Spatial cells and asset-backed p95/p99 remain pending |
| M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending | | M2 build dispatch planner | Implemented extraction | Scene-free priority/action/transition/source/timing contract | Resource observation/orchestration and asset-backed traversal remain pending |
| M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending | | M2 build resource snapshot | Implemented extraction | Typed identity/adoption/lifetime/source/timing contract | Resource observation service and asset-backed traversal remain pending |
| M2 static build resource observer | Implemented extraction | Cache/pending/path/request/source/timing contract | Asset-backed traversal and finalize extraction pending |
| M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending | | M2 build queue | Implemented extraction | Typed lifecycle/FIFO/progress/lifetime/source/timing contract | Resource dispatch and asset-backed traversal remain pending |
| M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending | | M2 animation load pipeline state | Implemented extraction | Synthetic lifecycle/FIFO/source/timing contract | Asset-backed traversal/leak/animation-fidelity/p95/p99 pending |
| M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending | | M2 animated scene finalizer | Implemented extraction | Synthetic type/lifetime/material/player/source/timing contract | Asset-backed GLB traversal/material/animation comparison pending |
@@ -652,6 +657,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh construction, render setup and attachment | | `src/render/m2/m2_static_batch_materializer.gd` | Static MultiMesh construction, render setup and attachment |
| `src/render/m2/m2_build_dispatch_planner.gd` | M2 wait/materializer/advance action priority and transition plan | | `src/render/m2/m2_build_dispatch_planner.gd` | M2 wait/materializer/advance action priority and transition plan |
| `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step animated/static observations and borrowed references | | `src/render/m2/m2_build_resource_snapshot.gd` | Typed per-step animated/static observations and borrowed references |
| `src/render/m2/m2_static_build_resource_observer.gd` | Static Mesh lookup/request/missing snapshot production |
| `src/render/m2/m2_build_job.gd` | M2 root/groups references and group/offset/serial progress | | `src/render/m2/m2_build_job.gd` | M2 root/groups references and group/offset/serial progress |
| `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle | | `src/render/m2/m2_build_queue.gd` | Keyed pending jobs and FIFO/stale tile-key lifecycle |
| `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 |
@@ -682,6 +688,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
| `src/tools/verify_m2_static_batch_materializer.gd` | Static M2 node/Mesh/render/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_build_dispatch_planner.gd` | M2 dispatch priority/action/transition/boundary/timing regression | | `src/tools/verify_m2_build_dispatch_planner.gd` | M2 dispatch priority/action/transition/boundary/timing regression |
| `src/tools/verify_m2_build_resource_snapshot.gd` | M2 observation identity/adoption/lifetime/boundary/timing regression | | `src/tools/verify_m2_build_resource_snapshot.gd` | M2 observation identity/adoption/lifetime/boundary/timing regression |
| `src/tools/verify_m2_static_build_resource_observer.gd` | Static lookup/request/path/boundary/timing regression |
| `src/tools/verify_m2_build_queue.gd` | M2 job/FIFO/progress/lifetime/boundary/timing regression | | `src/tools/verify_m2_build_queue.gd` | M2 job/FIFO/progress/lifetime/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 |
@@ -0,0 +1,126 @@
class_name M2StaticBuildResourceObserver
extends RefCounted
## Resolves or requests one static M2 build Mesh and fills a resource snapshot.
const OUTCOME_REJECTED := &"rejected"
const OUTCOME_CACHED := &"cached"
const OUTCOME_PENDING := &"pending"
const OUTCOME_REQUESTED := &"requested"
const OUTCOME_MISSING := &"missing"
## Observes cache/request state and adopts the exact static result into snapshot.
func observe(
resource_snapshot: RefCounted,
normalized_relative_path: String,
cache_directory: String,
mesh_cache_state: RefCounted,
prototype_cache_state: RefCounted,
load_pipeline_state: RefCounted
) -> StringName:
if (
resource_snapshot == null
or normalized_relative_path.is_empty()
or mesh_cache_state == null
or prototype_cache_state == null
or load_pipeline_state == null
):
return OUTCOME_REJECTED
if bool(mesh_cache_state.call("has_mesh", normalized_relative_path)):
resource_snapshot.call(
"adopt_static_observation",
mesh_cache_state.call("find_mesh", normalized_relative_path) as Mesh,
false
)
return OUTCOME_CACHED
if bool(prototype_cache_state.call("is_model_missing", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
if bool(load_pipeline_state.call("has_request", normalized_relative_path)):
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_PENDING
for cache_resource_path in cache_resource_paths(
cache_directory,
normalized_relative_path,
[".tscn", ".glb"]
):
if not ResourceLoader.exists(cache_resource_path):
continue
if (
cache_resource_path.get_extension().to_lower() == "glb"
and glb_animation_schema(cache_resource_path) == "pivot_prefix_v1"
):
continue
var request_error := ResourceLoader.load_threaded_request(
cache_resource_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE
)
if request_error == OK or request_error == ERR_BUSY:
load_pipeline_state.call(
"remember_request",
normalized_relative_path,
cache_resource_path
)
resource_snapshot.call("adopt_static_observation", null, false)
return OUTCOME_REQUESTED
break
prototype_cache_state.call("mark_model_missing", normalized_relative_path)
resource_snapshot.call("adopt_static_observation", null, true)
return OUTCOME_MISSING
## Returns historical nested/lowercase/basename cache candidates without I/O.
func cache_resource_paths(
cache_directory: String,
relative_path: String,
extensions: Array[String]
) -> PackedStringArray:
var normalized := relative_path.replace("\\", "/")
var lower := normalized.to_lower()
var stems := [
normalized.get_basename(),
lower.get_basename(),
normalized.get_file().get_basename(),
lower.get_file().get_basename(),
]
var result := PackedStringArray()
for extension in extensions:
for stem in stems:
if stem.is_empty():
continue
var path := cache_directory.path_join(stem + extension)
if not result.has(path):
result.append(path)
return result
## Reads only the OpenWC animation schema marker from a GLB JSON chunk.
func glb_animation_schema(cache_resource_path: String) -> String:
var absolute_path := ProjectSettings.globalize_path(cache_resource_path)
if not FileAccess.file_exists(absolute_path):
return ""
var file := FileAccess.open(absolute_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_length := int(file.get_32())
var chunk_type := file.get_32()
if json_length <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed: Variant = JSON.parse_string(
file.get_buffer(json_length).get_string_from_utf8()
)
if not (parsed is Dictionary):
return ""
var asset: Dictionary = (parsed as Dictionary).get("asset", {})
var extras: Dictionary = asset.get("extras", {})
return String(extras.get("openwc_m2_anim_schema", ""))
@@ -0,0 +1 @@
uid://c0e3p80ld1dfb
+19 -65
View File
@@ -70,6 +70,9 @@ const M2_BUILD_QUEUE_SCRIPT := preload("res://src/render/m2/m2_build_queue.gd")
const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload( const M2_STATIC_BATCH_MATERIALIZER_SCRIPT := preload(
"res://src/render/m2/m2_static_batch_materializer.gd" "res://src/render/m2/m2_static_batch_materializer.gd"
) )
const M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT := preload(
"res://src/render/m2/m2_static_build_resource_observer.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"
) )
@@ -277,6 +280,9 @@ var _m2_group_result_mutex := Mutex.new()
var _m2_group_result_queue: Array = [] var _m2_group_result_queue: Array = []
var _m2_build_queue_state := M2_BUILD_QUEUE_SCRIPT.new() var _m2_build_queue_state := M2_BUILD_QUEUE_SCRIPT.new()
var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new() var _m2_build_dispatch_planner := M2_BUILD_DISPATCH_PLANNER_SCRIPT.new()
var _m2_static_build_resource_observer := (
M2_STATIC_BUILD_RESOURCE_OBSERVER_SCRIPT.new()
)
var _m2_unique_placement_registry := ( var _m2_unique_placement_registry := (
M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new() M2_UNIQUE_PLACEMENT_REGISTRY_SCRIPT.new()
) )
@@ -4373,14 +4379,13 @@ func _process_m2_build_jobs() -> void:
batch_count > 0 batch_count > 0
and not bool(resource_snapshot.call("has_animated_prototype")) and not bool(resource_snapshot.call("has_animated_prototype"))
): ):
var static_mesh := _get_m2_mesh_or_request(rel_path) _m2_static_build_resource_observer.observe(
resource_snapshot.call( resource_snapshot,
"adopt_static_observation", normalized_rel,
static_mesh, m2_cache_dir,
( _m2_mesh_resource_cache_state,
static_mesh == null _m2_prototype_cache_state,
and _m2_prototype_cache_state.is_model_missing(normalized_rel) _m2_mesh_load_pipeline_state
)
) )
var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step( var dispatch_plan: Dictionary = _m2_build_dispatch_planner.plan_step(
batch_count, batch_count,
@@ -4587,40 +4592,6 @@ func _normalize_m2_rel_path(rel_path: String) -> String:
return normalized_rel return normalized_rel
func _get_m2_mesh_or_request(rel_path: String) -> Mesh:
var normalized_rel := _normalize_m2_rel_path(rel_path)
if normalized_rel.is_empty():
return null
if _m2_mesh_resource_cache_state.has_mesh(normalized_rel):
return _m2_mesh_resource_cache_state.find_mesh(normalized_rel)
if _m2_prototype_cache_state.is_model_missing(normalized_rel):
return null
_request_m2_mesh_load(normalized_rel)
return null
func _request_m2_mesh_load(normalized_rel: String) -> void:
if normalized_rel.is_empty() or _m2_mesh_load_pipeline_state.has_request(normalized_rel):
return
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path):
continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1":
continue
var err := ResourceLoader.load_threaded_request(
cache_res_path,
"",
false,
ResourceLoader.CACHE_MODE_REUSE)
if err == OK or err == ERR_BUSY:
_m2_mesh_load_pipeline_state.remember_request(normalized_rel, cache_res_path)
return
break
_m2_prototype_cache_state.mark_model_missing(normalized_rel)
func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh: func _prepare_m2_mesh_for_runtime(normalized_rel: String, mesh: Mesh) -> Mesh:
if mesh == null: if mesh == null:
return null return null
@@ -4820,7 +4791,12 @@ func _get_or_load_m2_prototype(rel_path: String) -> Node3D:
for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]): for cache_res_path in _get_m2_cache_resource_paths(normalized_rel, [".tscn", ".glb"]):
if not ResourceLoader.exists(cache_res_path): if not ResourceLoader.exists(cache_res_path):
continue continue
if cache_res_path.get_extension().to_lower() == "glb" and _glb_cache_animation_schema(cache_res_path) == "pivot_prefix_v1": if (
cache_res_path.get_extension().to_lower() == "glb"
and _m2_static_build_resource_observer.glb_animation_schema(
cache_res_path
) == "pivot_prefix_v1"
):
continue continue
var resource: Resource = load(cache_res_path) var resource: Resource = load(cache_res_path)
if resource is PackedScene: if resource is PackedScene:
@@ -5041,28 +5017,6 @@ func _glb_primitive_count(gltf: Dictionary) -> int:
return count return count
func _glb_cache_animation_schema(cache_res_path: String) -> String:
var abs_path := ProjectSettings.globalize_path(cache_res_path)
if not FileAccess.file_exists(abs_path):
return ""
var file := FileAccess.open(abs_path, FileAccess.READ)
if file == null or file.get_length() < 20:
return ""
var magic := file.get_32()
var version := file.get_32()
file.get_32()
if magic != 0x46546c67 or version != 2:
return ""
var json_len := int(file.get_32())
var chunk_type := file.get_32()
if json_len <= 0 or chunk_type != 0x4e4f534a:
return ""
var parsed = JSON.parse_string(file.get_buffer(json_len).get_string_from_utf8())
if parsed is Dictionary:
return _glb_animation_schema(parsed as Dictionary)
return ""
func _glb_animation_schema(gltf: Dictionary) -> String: func _glb_animation_schema(gltf: Dictionary) -> String:
var asset: Dictionary = gltf.get("asset", {}) var asset: Dictionary = gltf.get("asset", {})
var extras: Dictionary = asset.get("extras", {}) var extras: Dictionary = asset.get("extras", {})
@@ -160,7 +160,7 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
failures failures
) )
_expect_true( _expect_true(
loader_source.contains("_get_m2_mesh_or_request(rel_path)") loader_source.contains("_m2_static_build_resource_observer.observe(")
and loader_source.contains("_materialize_m2_animated_batch(") and loader_source.contains("_materialize_m2_animated_batch(")
and loader_source.contains("_materialize_m2_group_batch(") and loader_source.contains("_materialize_m2_group_batch(")
and loader_source.contains("try_consume_permit("), and loader_source.contains("try_consume_permit("),
@@ -8,6 +8,7 @@ const SNAPSHOT_SCRIPT := preload(
) )
const SNAPSHOT_PATH := "res://src/render/m2/m2_build_resource_snapshot.gd" const SNAPSHOT_PATH := "res://src/render/m2/m2_build_resource_snapshot.gd"
const DISPATCH_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd" const DISPATCH_PATH := "res://src/render/m2/m2_build_dispatch_planner.gd"
const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
@@ -134,10 +135,10 @@ func _verify_engine_lifetime(failures: Array[String]) -> void:
func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void: func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var dispatch_source := FileAccess.get_file_as_string(DISPATCH_PATH) var dispatch_source := FileAccess.get_file_as_string(DISPATCH_PATH)
var static_observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH)
var snapshot_source := FileAccess.get_file_as_string(SNAPSHOT_PATH) var snapshot_source := FileAccess.get_file_as_string(SNAPSHOT_PATH)
for delegated_token in [ for delegated_token in [
"M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(", "M2_BUILD_RESOURCE_SNAPSHOT_SCRIPT.new(",
"\"adopt_static_observation\"",
"resource_snapshot.call(\"animated_prototype\")", "resource_snapshot.call(\"animated_prototype\")",
"resource_snapshot.call(\"static_mesh\")", "resource_snapshot.call(\"static_mesh\")",
]: ]:
@@ -146,6 +147,11 @@ func _verify_loader_and_dependency_boundaries(failures: Array[String]) -> void:
"loader uses %s" % delegated_token, "loader uses %s" % delegated_token,
failures failures
) )
_expect_true(
static_observer_source.contains("\"adopt_static_observation\""),
"static observer adopts snapshot",
failures
)
_expect_true( _expect_true(
dispatch_source.contains("resource_snapshot.call(\"animation_request_pending\")") dispatch_source.contains("resource_snapshot.call(\"animation_request_pending\")")
and dispatch_source.contains("resource_snapshot.call(\"has_static_mesh\")"), and dispatch_source.contains("resource_snapshot.call(\"has_static_mesh\")"),
@@ -6,6 +6,7 @@ extends SceneTree
const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd") const CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd")
const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd" const CACHE_PATH := "res://src/render/m2/m2_mesh_resource_cache_state.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd" const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
const STATIC_OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
func _initialize() -> void: func _initialize() -> void:
@@ -68,6 +69,7 @@ func _verify_clear_and_diagnostics(failures: Array[String]) -> void:
func _verify_ownership_boundaries(failures: Array[String]) -> void: func _verify_ownership_boundaries(failures: Array[String]) -> void:
var cache_source := FileAccess.get_file_as_string(CACHE_PATH) var cache_source := FileAccess.get_file_as_string(CACHE_PATH)
var loader_source := FileAccess.get_file_as_string(LOADER_PATH) var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
var observer_source := FileAccess.get_file_as_string(STATIC_OBSERVER_PATH)
_expect_true( _expect_true(
loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"), loader_source.contains("M2_MESH_RESOURCE_CACHE_STATE_SCRIPT.new()"),
"loader composes Mesh cache state", "loader composes Mesh cache state",
@@ -81,7 +83,8 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
failures failures
) )
_expect_equal( _expect_equal(
loader_source.count("_m2_mesh_resource_cache_state.find_mesh("), loader_source.count("_m2_mesh_resource_cache_state.find_mesh(")
+ observer_source.count("\"find_mesh\""),
2, 2,
"two existing lookups delegate", "two existing lookups delegate",
failures failures
@@ -0,0 +1,127 @@
extends SceneTree
const OBSERVER_SCRIPT := preload("res://src/render/m2/m2_static_build_resource_observer.gd")
const SNAPSHOT_SCRIPT := preload("res://src/render/m2/m2_build_resource_snapshot.gd")
const MESH_CACHE_SCRIPT := preload("res://src/render/m2/m2_mesh_resource_cache_state.gd")
const PROTOTYPE_CACHE_SCRIPT := preload("res://src/render/m2/m2_prototype_cache_state.gd")
const PIPELINE_SCRIPT := preload("res://src/render/m2/m2_mesh_load_pipeline_state.gd")
const OBSERVER_PATH := "res://src/render/m2/m2_static_build_resource_observer.gd"
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
func _initialize() -> void:
var failures: Array[String] = []
_verify_invalid_cached_missing_pending(failures)
_verify_request_and_path_order(failures)
_verify_source_boundaries(failures)
var elapsed := _verify_timing(failures)
if not failures.is_empty():
for failure in failures:
push_error("M2_STATIC_BUILD_RESOURCE_OBSERVER: %s" % failure)
quit(1)
return
print("M2_STATIC_BUILD_RESOURCE_OBSERVER PASS cases=11 iterations=20000 elapsed_ms=%.3f" % elapsed)
quit(0)
func _verify_invalid_cached_missing_pending(failures: Array[String]) -> void:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var mesh_cache: RefCounted = MESH_CACHE_SCRIPT.new()
var prototype_cache: RefCounted = PROTOTYPE_CACHE_SCRIPT.new()
var pipeline: RefCounted = PIPELINE_SCRIPT.new()
var snapshot: RefCounted = SNAPSHOT_SCRIPT.new("world/a.m2", null, false)
_expect(
observer.call(
"observe", null, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_REJECTED,
"null snapshot rejected", failures
)
var mesh := ArrayMesh.new()
mesh_cache.call("store_mesh", "world/a.m2", mesh)
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_CACHED, "cached outcome", failures
)
_expect(is_same(snapshot.call("static_mesh"), mesh), "cached Mesh identity", failures)
mesh_cache.call("clear")
prototype_cache.call("mark_model_missing", "world/a.m2")
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_MISSING, "missing outcome", failures
)
_expect(bool(snapshot.call("static_model_missing")), "missing adopted", failures)
prototype_cache = PROTOTYPE_CACHE_SCRIPT.new()
pipeline.call("remember_request", "world/a.m2", "res://pending.tscn")
_expect(
observer.call(
"observe", snapshot, "world/a.m2", "res://none",
mesh_cache, prototype_cache, pipeline
)
== OBSERVER_SCRIPT.OUTCOME_PENDING, "pending outcome", failures
)
_expect(not bool(snapshot.call("static_model_missing")), "pending is not missing", failures)
pipeline.call("clear")
prototype_cache.call("clear_and_release")
func _verify_request_and_path_order(failures: Array[String]) -> void:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var extensions: Array[String] = [".tscn", ".glb"]
var paths: PackedStringArray = observer.call(
"cache_resource_paths", "res://cache", "World/Foo/Bar.M2", extensions
)
var expected := PackedStringArray([
"res://cache/World/Foo/Bar.tscn", "res://cache/world/foo/bar.tscn",
"res://cache/Bar.tscn", "res://cache/bar.tscn",
"res://cache/World/Foo/Bar.glb", "res://cache/world/foo/bar.glb",
"res://cache/Bar.glb", "res://cache/bar.glb",
])
_expect(paths == expected, "candidate order", failures)
_expect(
observer.call("glb_animation_schema", "res://missing.glb") == "",
"missing GLB schema empty", failures
)
func _verify_source_boundaries(failures: Array[String]) -> void:
var loader := FileAccess.get_file_as_string(LOADER_PATH)
var observer := FileAccess.get_file_as_string(OBSERVER_PATH)
_expect(
loader.contains("_m2_static_build_resource_observer.observe("),
"loader delegates observer", failures
)
_expect(not loader.contains("func _get_m2_mesh_or_request"), "legacy lookup removed", failures)
_expect(not loader.contains("func _request_m2_mesh_load"), "legacy request removed", failures)
_expect(
observer.contains("ResourceLoader.load_threaded_request("),
"observer owns request", failures
)
for forbidden in ["queue_free", "RenderingServer", "WorkerThreadPool", "Mutex"]:
_expect(not observer.contains(forbidden), "observer omits %s" % forbidden, failures)
func _verify_timing(failures: Array[String]) -> float:
var observer: RefCounted = OBSERVER_SCRIPT.new()
var extensions: Array[String] = [".tscn", ".glb"]
var started := Time.get_ticks_usec()
for index in range(20000):
observer.call(
"cache_resource_paths", "res://cache",
"world/%d/model.m2" % (index % 64), extensions
)
var elapsed := float(Time.get_ticks_usec() - started) / 1000.0
_expect(elapsed < 1000.0, "path planning bounded", failures)
return elapsed
func _expect(condition: bool, label: String, failures: Array[String]) -> void:
if not condition:
failures.append(label)
@@ -0,0 +1 @@
uid://b8bxs4swxsntd
+9 -1
View File
@@ -58,7 +58,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
`M03-RND-M2-STATIC-BATCH-MATERIALIZER-001`, `M03-RND-M2-STATIC-BATCH-MATERIALIZER-001`,
`M03-RND-M2-BUILD-QUEUE-001`, `M03-RND-M2-BUILD-QUEUE-001`,
`M03-RND-M2-BUILD-DISPATCH-PLANNER-001`, `M03-RND-M2-BUILD-DISPATCH-PLANNER-001`,
`M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001` `M03-RND-M2-BUILD-RESOURCE-SNAPSHOT-001`,
`M03-RND-M2-STATIC-BUILD-RESOURCE-OBSERVER-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
@@ -275,6 +276,10 @@ Runtime и Editor используют facade; planner/scheduler тестиру
materializers, animation/Mesh pipelines, Mesh/prototype state, shutdown, facade, materializers, animation/Mesh pipelines, Mesh/prototype state, shutdown, facade,
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation and internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation and
coordination remained green. coordination remained green.
M2 static build resource observer passed `cases=11 iterations=20000
elapsed_ms=64.158` with invalid/cached/Mesh-identity/missing/pending outcomes,
exact candidate order, GLB schema fallback and source ownership. All 55
autonomous headless regressions passed; internal-access remains `30`.
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
@@ -429,6 +434,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
typed M2 build job/queue and loader enqueue/drain/progress/cancel/clear adapters, typed M2 build job/queue and loader enqueue/drain/progress/cancel/clear adapters,
stateless M2 build dispatch planner and loader resource-observation/action adapter, stateless M2 build dispatch planner and loader resource-observation/action adapter,
typed M2 build resource snapshot and loader/dispatch/materializer adapters, typed M2 build resource snapshot and loader/dispatch/materializer adapters,
static M2 build resource observer and loader/cache/pipeline/snapshot adapters,
expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence. expanded facade/internal-access/coordinate verifiers; work-package claims and this evidence.
- Remaining risks: worker concurrency and stale-result validation remain - Remaining risks: worker concurrency and stale-result validation remain
streamer-owned; cancellation stops new permits but does not interrupt streamer-owned; cancellation stops new permits but does not interrupt
@@ -455,6 +461,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
separated and dispatch decisions are explicit, while resource observation/ separated and dispatch decisions are explicit, while resource observation/
requests, action execution and root cleanup remain in loader; resource state is requests, action execution and root cleanup remain in loader; resource state is
typed per operation but its producing service is not yet extracted; typed per operation but its producing service is not yet extracted;
static lookup/request production is extracted, while animated observation and
both ResourceLoader finalize drains remain loader-owned;
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;