Compare commits
10 Commits
f8348cf0cb
...
fe6d0deab9
| Author | SHA1 | Date | |
|---|---|---|---|
| fe6d0deab9 | |||
| 203d40bd0e | |||
| 3bf1c2c657 | |||
| d9d20cb53f | |||
| ef6d324b4f | |||
| f978a16806 | |||
| 541279ed45 | |||
| 8da41dc17c | |||
| 7e97b19095 | |||
| 07521ee6a4 |
@@ -1491,6 +1491,32 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
queues and permits remain loader-owned. Synthetic traversal timing is not
|
||||
private-asset visual, leak, GPU or p95/p99 evidence.
|
||||
|
||||
## 2026-08-01 WMO Scene Instance Factory Extraction
|
||||
|
||||
- `WmoSceneInstanceFactory` now owns cached PackedScene instantiation/currentness
|
||||
validation and live-prototype duplication with shared basename/placement rules.
|
||||
- Cached validation still precedes placement; live duplicates still skip the
|
||||
scene-cache validator. Accepted descendant Resources retain exact identity.
|
||||
- Invalid non-Node3D cached roots are now freed synchronously, closing an
|
||||
error-path lifetime leak that normal scene-cache admission already prevents.
|
||||
- Source lookup, ResourceLoader, runtime preparation, attachment, registry,
|
||||
queues and permits remain loader-owned. Synthetic factory timing is not
|
||||
private-asset visual, leak/GPU or p95/p99 evidence.
|
||||
|
||||
## 2026-08-02 M03 Renderer Closeout
|
||||
|
||||
- M03 preserves the M00 `High` topology and batching while enforcing four
|
||||
CPU-only worker boundaries, fifteen main-thread finalization lanes and seven
|
||||
explicit cache versions through the renderer closeout contract verifier.
|
||||
- Performance acceptance uses exact-cache paired M00/M03 captures plus a second
|
||||
ten-second window. A metric must exceed its unchanged 10% budget in both
|
||||
protocols to be a repeatable regression; the closeout result is `0/84`.
|
||||
- Native M2 startup no longer copies an ArrayMesh that is immediately discarded.
|
||||
It creates an instance-local mesh, reapplies shared Materials and performs one
|
||||
phased rebuild before attachment; `_ready()` is idempotent afterward.
|
||||
- Checkpoint evidence is asset-backed but is not an original-client pixel-parity
|
||||
claim. Long traversal and original-client approval remain release gates.
|
||||
|
||||
## Practical Rule For Future Work
|
||||
|
||||
If something improves quality but creates visible hitch, it is not done. Move it to bake/cache/background work, split finalization over frames, or prewarm it before the player can see it.
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
# M03-QAR-INTEGRATOR-CLOSEOUT-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex-m03-integrator:2026-08-03 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-QAR-INTEGRATOR-CLOSEOUT-001:203d40b -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex-m03-integrator`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex-m03-integrator/m03-closeout`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-closeout`
|
||||
|
||||
## Outcome
|
||||
|
||||
Close M03 only after an overall renderer performance comparison, cache-version
|
||||
and main-thread-finalization contract gates, the complete autonomous verifier
|
||||
suite, documentation/coordination gates and integrator review all pass.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change renderer visuals, placements, materials, animations or quality policy.
|
||||
- Add new renderer features or advance to M04/M08.
|
||||
- Claim original-client visual parity beyond existing M00 evidence.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: M03 closeout verifier/comparator, closeout specification, this claim
|
||||
- Shared: renderer verification runner, renderer/module registries, `RENDER.md`,
|
||||
`targets/03-renderer-facade.md`, `targets/README.md`
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Compare identical checkpoint/pass pairs against the accepted M00 report.
|
||||
- Require matching environment/profile/cache state and exact cache version keys.
|
||||
- Enforce the accepted 10% ceilings for p95, p99, max hitch, load time and memory.
|
||||
- Verify GPU/SceneTree finalization remains main-thread and budget-gated by source.
|
||||
- Keep all generated/private reports outside Git; commit only aggregate evidence.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: all accepted M03 facade/planner/scheduler and terrain/M2/WMO/liquid packages
|
||||
- Blocks: M03 DONE marker and activation of the next user-selected plan
|
||||
|
||||
## Verification
|
||||
|
||||
- Full cold/warm renderer capture; deterministic performance comparison; complete
|
||||
headless verifier suite; project/editor parse; baseline manifest/checkpoint dry
|
||||
run; dependency, documentation and coordination gates.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline comparator/gate docs; closeout module specification with API/I/O,
|
||||
ownership and sequence/dependency diagrams; renderer docs and final M03 Evidence.
|
||||
|
||||
## Status
|
||||
|
||||
- State: integrated
|
||||
- Done: renderer closeout gates, exact-cache performance evidence, full verifier
|
||||
suite, documentation, M03 DONE transition and M04 administrative activation
|
||||
- Next: receive the user's plan correction before beginning M04 work
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-QAR-INTEGRATOR-CLOSEOUT-001:203d40b -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commits: `d9d20cb`, `3bf1c2c`, `203d40b`; fast-forwarded to `master`.
|
||||
- Verification: all `67/67` autonomous Godot verifiers; M2 playback `20`
|
||||
cases; renderer contracts `workers=4 frame_steps=15 cache_versions=7
|
||||
nested_glb=1`; performance stability `84` metrics with `0` repeatable
|
||||
regressions; documentation, coordination and diff gates passed.
|
||||
- Fidelity: exact cache inventory and asset-backed checkpoints cover terrain,
|
||||
ADT boundary, dense M2, large WMO, liquid, native animation and dusk sky.
|
||||
This is extraction/performance evidence, not original-client pixel parity.
|
||||
- Remaining risks: original-client visual approval, long-traversal descriptor
|
||||
pressure, scaling native animation beyond CPU deformation and interruption of
|
||||
already in-flight worker jobs remain later release concerns.
|
||||
- Documentation: added the renderer closeout module specification with data-flow
|
||||
and sequence diagrams; updated testing policy, renderer source maps, M2
|
||||
animation ownership/sequence documentation, `RENDER.md` and M03 Evidence.
|
||||
@@ -0,0 +1,90 @@
|
||||
# M03-RND-WMO-SCENE-INSTANCE-FACTORY-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-INSTANCE-FACTORY-001:sindo-main-codex:2026-08-03 -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex/m03-wmo-scene-instance-factory`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-scene-instance-factory`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move WMO cached-scene instantiation/currentness validation and live-prototype
|
||||
duplication with shared name/placement application into one main-thread factory.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change cache lookup/admission, ResourceLoader or prototype construction.
|
||||
- Change runtime Mesh/material/occluder/shadow preparation.
|
||||
- Change attachment, placement registry, queues, permits or lifetime.
|
||||
- Change scene cache version rules or placement formulas.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_scene_instance_factory.gd`,
|
||||
`src/tools/verify_wmo_scene_instance_factory.gd`,
|
||||
`docs/modules/wmo-scene-instance-factory.md`, this claim
|
||||
- Shared: loader, adjacent WMO verifier/spec, renderer registry/specification,
|
||||
`RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Null scenes/prototypes and non-Node3D instantiation/duplication return null.
|
||||
- Cached instances failing the injected currentness validator are synchronously freed.
|
||||
- Accepted instances retain exact descendant Resource identities.
|
||||
- Both paths apply `relative_path.get_file().get_basename()` and the exact
|
||||
placement-resolver Transform3D.
|
||||
- The factory returns detached roots and retains no Node or Resource.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO placement resolver and runtime scene preparer
|
||||
- Blocks: remaining loader-owned WMO cache/prototype orchestration cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic cached/live type, validation, freeing, exact identity, name,
|
||||
transform, dependency/order, ownership, source and bounded-timing contracts;
|
||||
adjacent WMO/renderer gates and checkpoint dry-run.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API docs; module API/I/O/ownership; data-flow/sequence/dependency
|
||||
diagrams; adjacent renderer docs and M03 Evidence.
|
||||
|
||||
## Status
|
||||
|
||||
- State: ready
|
||||
- Done: implementation, verification and documentation
|
||||
- Next: integrator review and merge
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `7e97b19` (`render: extract WMO scene instance factory`)
|
||||
- Results: factory PASS `cases=41 iterations=1000 elapsed_ms=5.297`;
|
||||
placement resolver dependency PASS `cases=10 iterations=20000
|
||||
elapsed_ms=28.740`; suite `65/66` with no unexpected failures and only the
|
||||
proprietary ADT placement probe unavailable; editor parse had zero script
|
||||
diagnostics; checkpoint dry-run retained `7/7`; documentation passed with
|
||||
`52` module specifications; coordination passed with `77` historical warnings.
|
||||
- Fidelity: valid cached/live cache-validation distinction, basename, exact
|
||||
Transform3D and descendant Resource identity are unchanged. Invalid non-Node3D
|
||||
cached roots are now freed synchronously, fixing an error-path leak without
|
||||
changing valid visible output.
|
||||
- Remaining risks: instantiation/duplication remains synchronous; no private WMO
|
||||
portal/room, visual, long-traversal, leak/GPU or p95/p99 evidence.
|
||||
- Documentation: new full factory module specification; placement resolver
|
||||
consumers/sequence, renderer registry/source map and `RENDER.md` updated.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-SCENE-INSTANCE-FACTORY-001:7e97b19 -->
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-SCENE-INSTANCE-FACTORY-001:541279e -->
|
||||
|
||||
- Merge: `541279e` (`merge: WMO scene instance factory`)
|
||||
- Post-merge: factory PASS `cases=41 iterations=1000 elapsed_ms=5.308`;
|
||||
placement resolver `cases=10`/`31.207ms`; runtime preparation/finalization,
|
||||
adjacent WMO services, shutdown, materials, facade, internal-access `30`,
|
||||
manifest `7/7`, documentation `52` and coordination passed.
|
||||
@@ -54,6 +54,23 @@
|
||||
- dense WMO/M2, water, character equipment и UI scale matrices.
|
||||
- navmesh overlay checkpoints и bake/query budgets для больших tiles/dungeons.
|
||||
|
||||
Для M03 renderer closeout сравнение выполняется на точных M00/M03 revisions с
|
||||
одинаковыми viewport, rendering backend и полным cache inventory. Короткий
|
||||
протокол агрегирует повторные captures медианой каждого показателя; независимый
|
||||
протокол использует увеличенное десятисекундное окно. Регрессия считается
|
||||
воспроизводимой, только если один и тот же checkpoint/pass/metric превышает
|
||||
неизменённый 10% budget в обоих протоколах:
|
||||
|
||||
```powershell
|
||||
tools/compare_render_performance.ps1 -BaselineReport <m00-reports> -CandidateReport <m03-reports> -OutputReport <repeated.json>
|
||||
tools/compare_render_performance.ps1 -BaselineReport <m00-long.json> -CandidateReport <m03-long.json> -OutputReport <long.json>
|
||||
tools/verify_render_performance_stability.ps1 -RepeatedSampleComparison <repeated.json> -LongWindowComparison <long.json> -OutputReport <stability.json>
|
||||
```
|
||||
|
||||
Локальные превышения одного протокола сохраняются как diagnostics; gate падает
|
||||
только на повторяемой регрессии. Полный контракт и схема evidence описаны в
|
||||
[`modules/renderer-closeout-verification.md`](modules/renderer-closeout-verification.md).
|
||||
|
||||
### Navigation compatibility
|
||||
|
||||
- Golden synthetic meshes проверяют slope, climb, radius erosion, holes, tiled seams и off-mesh connections.
|
||||
|
||||
@@ -52,9 +52,11 @@
|
||||
| WMO runtime Mesh finalizer | Implemented extraction | [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md) |
|
||||
| WMO render group materializer | Implemented extraction | [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md) |
|
||||
| WMO runtime scene preparer | Implemented extraction | [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md) |
|
||||
| WMO scene instance factory | Implemented extraction | [`wmo-scene-instance-factory.md`](wmo-scene-instance-factory.md) |
|
||||
| Third-person camera | Implemented | [`third-person-camera.md`](third-person-camera.md) |
|
||||
| Character presentation | Implemented boundary / Partial fidelity | [`character-presentation.md`](character-presentation.md) |
|
||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||
| Renderer closeout verification | Implemented | [`renderer-closeout-verification.md`](renderer-closeout-verification.md) |
|
||||
| World entity presentation | Implemented boundary / Prototype visuals | [`world-entity-presentation.md`](world-entity-presentation.md) |
|
||||
| Streaming target planner | Implemented | [`streaming-target-planner.md`](streaming-target-planner.md) |
|
||||
| Render budget scheduler | Implemented | [`render-budget-scheduler.md`](render-budget-scheduler.md) |
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-M2-ANIMATION-PLAYBACK-001` |
|
||||
| Owners | Per-instance AnimationPlayer/native animator playback mutation |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-m2-animation-playback`, 2026-07-18 |
|
||||
| Last verified | Worktree `work/sindo-main-codex-m03-integrator/m03-closeout`, 2026-08-02 |
|
||||
| Profiles/capabilities | Imported GLB and native experimental animated M2 instances |
|
||||
|
||||
## Purpose
|
||||
@@ -59,7 +59,7 @@ MultiMesh, SceneTree attachment and application layers are forbidden.
|
||||
| Output | Mutated native/imported playback | Controller | Rendered instance | Nodes retain state/resources | Instance lifetime |
|
||||
| Output | Detached native diagnostic records | Controller | Loader log adapter | Caller-owned Dictionaries | Debug call |
|
||||
|
||||
Side effects are native field assignment, prepare/phase calls, animation loop
|
||||
Side effects are native field assignment, phased preparation calls, animation loop
|
||||
mutation, play and seek. The service retains no inputs.
|
||||
|
||||
## Data flow
|
||||
@@ -67,13 +67,13 @@ mutation, play and seek. The service retains no inputs.
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Identity[Path and index] --> Phase[Stable hash phase]
|
||||
NativeInventory[Exact-script native inventory] --> Prepare[prepare runtime if available]
|
||||
Phase --> NativePhase[set native phase]
|
||||
NativeInventory[Exact-script native inventory] --> Prepare[prepare local runtime mesh]
|
||||
Phase --> Prepare
|
||||
Players[AnimationPlayers] --> Select[Choose path-specific default]
|
||||
Select --> Loop[Set every animation LOOP_LINEAR]
|
||||
Loop --> Play[Play selected name]
|
||||
Phase --> Seek[Seek positive-length selection]
|
||||
NativePhase --> Diagnostics{Debug requested?}
|
||||
Prepare --> Diagnostics{Debug requested?}
|
||||
Diagnostics -->|yes| Snapshot[Detached runtime state]
|
||||
```
|
||||
|
||||
@@ -103,7 +103,7 @@ sequenceDiagram
|
||||
M->>F: animation_players_in_subtree(duplicate)
|
||||
F-->>M: ordered players
|
||||
M->>P: start_instance_playback(path, index, players, debug)
|
||||
P->>N: prepare_runtime and set_phase
|
||||
P->>N: prepare_runtime_at_phase
|
||||
P->>A: choose, loop, play and seek
|
||||
P-->>M: optional detached native diagnostics
|
||||
M-->>M: tag states with instance index
|
||||
@@ -132,6 +132,11 @@ flowchart TB
|
||||
- Native arrays are assigned by reference exactly as before extraction.
|
||||
- Diagnostic Dictionaries are deep-duplicated before return.
|
||||
- Main thread performs all engine-object mutation; pure phase math is thread-safe.
|
||||
- A duplicated native animator resolves and duplicates its local Mesh, applies
|
||||
phase and deforms once before attachment. Its later `_ready()` is idempotent.
|
||||
- Preparation allocates an empty instance-local ArrayMesh because deformation
|
||||
immediately rebuilds every surface from retained native arrays. Captured
|
||||
immutable Material resources remain shared and are reapplied.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
@@ -168,7 +173,8 @@ and material versions are unchanged; no rebake is required.
|
||||
|
||||
- `verify_m2_animation_playback_controller.gd` covers exact phase, ordinary/
|
||||
fish/bird priorities, substring/first fallback, loop/play/seek, native exact-
|
||||
script order, five-field copy, phase, detached diagnostics and boundaries.
|
||||
script order, five-field copy, single-rebuild phased preparation, idempotent
|
||||
ready, detached diagnostics and boundaries.
|
||||
- Finalizer/build/prototype/material/shutdown regressions protect adjacent behavior.
|
||||
- Fidelity evidence is exact policy/mutation extraction; no private asset or
|
||||
original-client animation comparison is claimed.
|
||||
@@ -192,7 +198,8 @@ for world doodads and compatibility fixtures.
|
||||
|
||||
- Hash phase intentionally depends on existing Godot String hashing behavior.
|
||||
- Default-name heuristics are not a complete WoW animation-state mapping.
|
||||
- No proprietary traversal, animation timing comparison, p95/p99 or paired-client run exists.
|
||||
- Native CPU deformation remains proportional to vertex count and is unsuitable
|
||||
for large numbers of independently animated instances without a future GPU path.
|
||||
|
||||
## Source map
|
||||
|
||||
|
||||
@@ -0,0 +1,183 @@
|
||||
# Renderer Closeout Verification
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target/work package | M03 / `M03-QAR-INTEGRATOR-CLOSEOUT-001` |
|
||||
| Owner | Renderer structural, cache and performance acceptance gates |
|
||||
| Last verified | Worktree `work/sindo-main-codex-m03-integrator/m03-closeout`, 2026-08-02 |
|
||||
| Profile | `Blizzlike335`; quality preset `High` for checkpoint evidence |
|
||||
|
||||
## Purpose
|
||||
|
||||
Provide reproducible closeout checks for the M03 renderer decomposition. The
|
||||
checks prove that worker boundaries remain CPU-only, main-thread frame steps are
|
||||
budgeted, cache versions remain explicit, and paired M00/M03 reports stay within
|
||||
the agreed 10% performance budgets.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Claim pixel-level parity with the original build-12340 client.
|
||||
- Replace subsystem unit verifiers or long-traversal release tests.
|
||||
- Generate, mutate or migrate production cache payloads.
|
||||
- Hide incompatible environments or cache inventories by normalizing reports.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
M00[M00 commit capture] --> Reports[Render checkpoint reports]
|
||||
M03[M03 commit capture] --> Reports
|
||||
Reports --> Comparator[compare_render_performance.ps1]
|
||||
Sources[Renderer source and cache versions] --> Contracts[verify_renderer_closeout_contracts.gd]
|
||||
Comparator --> Stability[repeatability gate across short and long windows]
|
||||
Stability --> Evidence[JSON comparison and exit code]
|
||||
Contracts --> Evidence
|
||||
Evidence --> Target[M03 Evidence / DONE decision]
|
||||
```
|
||||
|
||||
The capture command owns SceneTree execution and PNG/report writes. The
|
||||
PowerShell comparator is read-only except for its requested JSON output. The
|
||||
GDScript contract verifier reads source files and creates no renderer resources.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Preconditions | Failure |
|
||||
|---|---|---|---|---|
|
||||
| `compare_render_performance.ps1 -BaselineReport <paths> -CandidateReport <paths> [-OutputReport <path>]` | CLI | Compare one report or median of repeated reports | Same schema, profile, environment, cache contract/inventory and result keys | Exit 1 and enumerate incompatible fields or budget regressions |
|
||||
| `verify_render_performance_stability.ps1 -RepeatedSampleComparison <path> -LongWindowComparison <path>` | CLI | Reject only a metric regression reproduced by both independent protocols | Both comparator reports contain the same 84 metrics | Exit 1 with every repeatable result/metric key |
|
||||
| `verify_renderer_closeout_contracts.gd` | Godot CLI | Check worker/main-thread/cache/converter source contracts | Project parses and referenced sources exist | Exit 1 with named contract failure |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | M00/M03 `report.json` paths | `capture_render_checkpoints.gd` | Comparator | Filesystem-owned immutable evidence | One comparison |
|
||||
| Input | Renderer/tool GDScript sources | Repository | Contract verifier | Read-only | One verifier run |
|
||||
| Output | 84 metric comparisons and failures | Comparator | Integrator/CI | Optional JSON plus process output | Evidence retention |
|
||||
| Output | Repeatable and protocol-local regression inventories | Stability gate | Integrator/CI | Optional JSON plus process output | Evidence retention |
|
||||
| Output | Structural pass/fail summary | Contract verifier | Integrator/CI | Process output | One run |
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Read[Read every supplied report] --> Compatible{Metadata and inventory match?}
|
||||
Compatible -->|no| Fail[Exit 1 with exact mismatch]
|
||||
Compatible -->|yes| Median[Median each metric per checkpoint/pass]
|
||||
Median --> Pair[Pair 14 result keys]
|
||||
Pair --> Budget[Compare load, p95, p99, hitch and memory]
|
||||
Budget --> Json[Optional comparison JSON]
|
||||
Budget --> Cohorts[Repeated-sample and long-window comparisons]
|
||||
Cohorts --> Repeated{Same metric fails both?}
|
||||
Repeated -->|no| Pass[Exit 0 with protocol-local diagnostics]
|
||||
Repeated -->|yes| Fail
|
||||
```
|
||||
|
||||
## Lifecycle and sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant I as Integrator
|
||||
participant B as M00 worktree
|
||||
participant C as M03 worktree
|
||||
participant R as Checkpoint capture
|
||||
participant G as Performance comparator
|
||||
I->>B: capture repeated baseline samples
|
||||
B->>R: same viewport, driver and cache inventory
|
||||
I->>C: capture repeated candidate samples
|
||||
C->>R: same viewport, driver and cache inventory
|
||||
I->>G: baseline paths plus candidate paths
|
||||
G->>G: compatibility checks and per-metric medians
|
||||
G-->>I: short-window and long-window JSON evidence
|
||||
I->>G: verify stability across both protocols
|
||||
G-->>I: repeatable-regression pass/fail
|
||||
```
|
||||
|
||||
There is no persistent state machine. Each invocation is read, validate,
|
||||
aggregate, compare and terminate.
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Reports and source files are borrowed read-only for one process.
|
||||
- Median aggregation deep-copies the first report and never rewrites inputs.
|
||||
- Rendering remains owned by the GUI capture process on Godot's main thread.
|
||||
- The contract verifier is headless and does not instantiate the streaming world.
|
||||
- The optional comparison report is wholly owned by the caller-selected path.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure | Behavior | Recovery |
|
||||
|---|---|---|
|
||||
| Missing/malformed report | Terminate with path/shape error | Regenerate that capture |
|
||||
| Environment/cache mismatch | Fail before accepting metrics | Recapture both commits on the same machine and cache |
|
||||
| Missing/duplicate checkpoint | Fail result-key validation | Repair manifest/capture completeness |
|
||||
| Metric over budget | Record baseline, candidate, limit and percentage | Diagnose named checkpoint/lane; rerun only after a code or evidence correction |
|
||||
| Interrupted GUI capture | No complete report is accepted | Remove/ignore partial output and rerun |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The comparator reads thresholds from the baseline report. M03 uses 10% maximum
|
||||
regression for load time, frame p95, frame p99, maximum hitch, static memory and
|
||||
video memory. Repeated input paths are optional; when supplied, each side is
|
||||
reduced independently to the median for every metric.
|
||||
|
||||
Closeout uses two independent protocols: repeated three-second captures and a
|
||||
ten-second measurement window. A regression is accepted as real only when the
|
||||
same checkpoint/pass/metric exceeds its unchanged 10% budget in both protocols.
|
||||
Protocol-local failures remain in the JSON as noise diagnostics rather than
|
||||
being discarded.
|
||||
|
||||
## Persistence, cache and migrations
|
||||
|
||||
The comparison JSON uses schema version 1 and contains source paths, sample
|
||||
counts, revisions, all metric pairs and failures. It is evidence, not a runtime
|
||||
cache. Renderer cache versions are read from the manifest/source contracts; this
|
||||
module performs no migration or invalidation.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Success reports result pairs, comparison count and budget percentage.
|
||||
- Failure output names every checkpoint/pass, metric, values and limit.
|
||||
- The structural verifier reports worker count, frame-step count and cache-version count.
|
||||
- Capture reports retain PNG hashes, queue snapshots, environment and cache inventory.
|
||||
|
||||
## Verification, fidelity and performance
|
||||
|
||||
- `verify_renderer_closeout_contracts.gd` covers four worker boundaries, fifteen
|
||||
frame steps, seven cache versions and the nested M2 GLB output contract.
|
||||
- `compare_render_performance.ps1` compares 14 cold/warm result pairs and 84 metrics.
|
||||
- `verify_render_performance_stability.ps1` requires metric-key agreement and
|
||||
rejects any budget regression reproduced by both sampling protocols.
|
||||
- M00 and M03 must be captured from their exact commits against the same cache
|
||||
inventory; old reports with a different inventory are rejected.
|
||||
- PNG hashes and asset-backed coverage prove that terrain, ADT boundaries, dense
|
||||
M2, large WMO, liquid, animated M2 and sky were rendered. They do not prove
|
||||
original-client pixel parity without human/reference-image approval.
|
||||
|
||||
## Extension points
|
||||
|
||||
CI may retain reports and comparison JSON as artifacts. A future release gate may
|
||||
add driver-version metadata, long-traversal samples or approved visual-diff
|
||||
thresholds without changing runtime renderer contracts.
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Godot reports the rendering API but not the installed NVIDIA driver version.
|
||||
- A 0.5-second historical M00 measurement window requires repeated median samples.
|
||||
- Original-client screenshots are not part of the repository evidence set.
|
||||
- Long-traversal descriptor pressure remains a later quality/release gate.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `tools/compare_render_performance.ps1` | Compatibility, median aggregation and metric budgets |
|
||||
| `tools/verify_render_performance_stability.ps1` | Cross-protocol repeatability acceptance |
|
||||
| `src/tools/verify_renderer_closeout_contracts.gd` | Structural source/cache/converter contracts |
|
||||
| `src/tools/capture_render_checkpoints.gd` | Asset-backed GUI capture and report generation |
|
||||
| `src/tools/render_baseline_manifest.json` | Coverage, viewport, cache contract and budgets |
|
||||
| `targets/00-render-baseline.md` | Accepted M00 measurement protocol |
|
||||
| `targets/03-renderer-facade.md` | M03 acceptance and Evidence |
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Implemented |
|
||||
| Target/work package | M03 / `M03-RND-WMO-PLACEMENT-RESOLVER-001` |
|
||||
| Owners | Pure WMO cache-key, placement-identity and world-transform rules |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-placement-resolver`, 2026-07-17 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
|
||||
| Profiles/capabilities | Existing ADT/WDT WMO placement paths |
|
||||
|
||||
## Purpose
|
||||
@@ -31,14 +31,17 @@ live-prototype instance paths.
|
||||
flowchart LR
|
||||
Parsed[ADT/WDT WMO placement] --> Loader[StreamingWorldLoader adapter]
|
||||
Loader --> Resolver[WmoPlacementResolver]
|
||||
Loader --> Factory[WmoSceneInstanceFactory]
|
||||
Factory --> Resolver
|
||||
Resolver --> CacheKey[Normalized cache key]
|
||||
Resolver --> Identity[Registry unique key]
|
||||
Resolver --> Transform[World Transform3D]
|
||||
CacheKey --> Cache[Loader WMO caches/requests]
|
||||
Identity --> Registry[WmoPlacementRegistry]
|
||||
Transform --> RenderRoot[Lightweight render root]
|
||||
Transform --> Scene[Cached scene instance]
|
||||
Transform --> Live[Live prototype instance]
|
||||
Transform --> Factory
|
||||
Factory --> Scene[Cached scene instance]
|
||||
Factory --> Live[Live prototype instance]
|
||||
```
|
||||
|
||||
Allowed dependencies are Dictionary/String values and Godot `Vector3`, `Basis`
|
||||
@@ -62,7 +65,7 @@ WorkerThreadPool, mutexes, files, gameplay, network and editor UI are forbidden.
|
||||
| Input | Tile key and placement index | Loader build job | Synthetic identity fallback | Copied scalar/String | Registry entry lifetime |
|
||||
| Output | Normalized relative path | Resolver | Render/scene cache and load-request maps | New String value | Request/cache lookup |
|
||||
| Output | `uid:*` or `tile:*:*` key | Resolver | `WmoPlacementRegistry` and loader ref arrays | New String value | Until unregister/reset |
|
||||
| Output | World `Transform3D` | Resolver | Three WMO instance adapters | Value copy | Instance lifetime after assignment |
|
||||
| Output | World `Transform3D` | Resolver | Lightweight render-root adapter and cached/live instance factory | Value copy | Instance lifetime after assignment |
|
||||
|
||||
The resolver retains no source Dictionary, output or engine resource.
|
||||
|
||||
@@ -91,6 +94,7 @@ and shutdown require no resolver operation.
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Factory as WmoSceneInstanceFactory
|
||||
participant Resolver as WmoPlacementResolver
|
||||
participant Registry as WmoPlacementRegistry
|
||||
participant Instance as Render/cached/live instance
|
||||
@@ -98,9 +102,15 @@ sequenceDiagram
|
||||
Resolver-->>Loader: cache key
|
||||
Loader->>Resolver: resolve_unique_key(placement, tile, index)
|
||||
Resolver-->>Registry: identity adopted by loader
|
||||
Loader->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Loader: value Transform3D
|
||||
Loader->>Instance: assign transform and attach/build
|
||||
alt lightweight render root
|
||||
Loader->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Loader: value Transform3D
|
||||
else cached/live instance
|
||||
Loader->>Factory: create with placement
|
||||
Factory->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Factory: value Transform3D
|
||||
end
|
||||
Loader->>Instance: attach/build prepared instance
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
@@ -109,7 +119,9 @@ sequenceDiagram
|
||||
- `WmoPlacementRegistry` owns placement-key reference sets. The loader owns its
|
||||
key-to-Node map, cache/load-request state, jobs/queues, resource fallback and
|
||||
cancellation.
|
||||
- The loader and builders own every Node/Mesh/MultiMesh/material/RID lifecycle.
|
||||
- `WmoSceneInstanceFactory` owns detached cached/live candidate roots until
|
||||
rejection or transfer; the loader/builders own attachment and remaining
|
||||
Node/Mesh/MultiMesh/material/RID lifecycle.
|
||||
- Pure calls are thread-safe; current consumers execute on the main thread.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
# WMO Scene Instance Factory
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target | M03 Renderer Facade and Safe Extraction |
|
||||
| Work package | `M03-RND-WMO-SCENE-INSTANCE-FACTORY-001` |
|
||||
| Owner | Render |
|
||||
| Last verified | 2026-08-01 |
|
||||
|
||||
## Purpose
|
||||
|
||||
`WmoSceneInstanceFactory` creates detached WMO `Node3D` roots from validated
|
||||
cached `PackedScene` resources or live-built prototypes. It owns cache-currentness
|
||||
validation, basename assignment and canonical placement-resolver delegation.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- look up/load/cache PackedScenes or build live WMO prototypes;
|
||||
- apply Mesh/material/occluder/shadow runtime preparation;
|
||||
- attach nodes, assign Editor ownership or manage placement references;
|
||||
- own queues, permits, cache versions or world teardown;
|
||||
- define WMO placement formulas or scene-cache currentness rules.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
The loader selects cached versus live sources. The factory creates a detached
|
||||
instance and applies identity/placement. `WmoRuntimeScenePreparer` then applies
|
||||
path-specific presentation policy before the loader attaches/registers the root.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Cache[WMO PackedScene cache] --> Loader[StreamingWorldLoader]
|
||||
Prototype[Live WMO prototype cache/build] --> Loader
|
||||
Loader --> Factory[WmoSceneInstanceFactory]
|
||||
Validator[WMOBuilder scene-cache validator] --> Factory
|
||||
Resolver[WmoPlacementResolver] --> Factory
|
||||
Factory --> Detached[Detached WMO Node3D]
|
||||
Detached --> Preparer[WmoRuntimeScenePreparer]
|
||||
Preparer --> Loader
|
||||
Loader --> Scene[Attachment and placement registry]
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Role | Thread/lifetime | Failure behavior |
|
||||
|---|---|---|---|
|
||||
| `is_cached_node_current(node)` | Delegate one Node to the injected cache validator | Renderer main thread; no retention | Null/missing validator returns false |
|
||||
| `instantiate_cached_scene(relative_path, scene, placement)` | Instantiate, type-check, validate, name and place a cached scene | Renderer main thread; detached result caller-owned | Invalid input/root/stale/dependency returns null; created rejected roots freed |
|
||||
| `duplicate_live_prototype(relative_path, prototype, placement)` | Duplicate, name and place a live prototype | Renderer main thread; detached result caller-owned | Null/missing resolver/unexpected duplicate returns null |
|
||||
|
||||
The cached path validates before placement. The live path deliberately skips the
|
||||
scene-cache validator. Both paths use `get_file().get_basename()` and the exact
|
||||
`WmoPlacementResolver.resolve_world_transform` result.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Cached `PackedScene` or live prototype `Node3D` | Loader cache/build adapters | Factory | Borrowed Resource/Node | One main-thread call |
|
||||
| Input | Relative WMO path and placement Dictionary | Loader placement job | Factory | Borrowed values | One call |
|
||||
| Internal | Candidate root | PackedScene instantiate/prototype duplicate | Validator/factory | Factory-owned until accepted | One call |
|
||||
| Output | Detached named/placed `Node3D` | Factory | Runtime scene preparer/loader | Ownership transfers to caller | Until attachment/release |
|
||||
| Output | Currentness bool | Validator via factory | Loader cache admission | Value | Immediate |
|
||||
|
||||
Side effects are limited to scene instantiation/duplication, candidate name and
|
||||
transform mutation, and synchronous free of rejected candidates. No attachment,
|
||||
filesystem, ResourceLoader, worker, RID, queue, cache or Editor-owner mutation.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Source[PackedScene or live prototype] --> Create{Cached or live?}
|
||||
Create -->|cached| Instantiate[PackedScene.instantiate]
|
||||
Create -->|live| Duplicate[prototype.duplicate]
|
||||
Instantiate --> Type{Node3D?}
|
||||
Duplicate --> Type
|
||||
Type -->|no| Free[Free created candidate and return null]
|
||||
Type -->|yes cached| Current{Cache current?}
|
||||
Type -->|yes live| Identity[Apply basename]
|
||||
Current -->|no| Free
|
||||
Current -->|yes| Identity
|
||||
Identity --> Resolve[WmoPlacementResolver]
|
||||
Resolve --> Return[Return detached Node3D]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Factory as WmoSceneInstanceFactory
|
||||
participant Validator as WMOBuilder
|
||||
participant Resolver as WmoPlacementResolver
|
||||
alt cached source
|
||||
Loader->>Factory: instantiate_cached_scene(path, scene, placement)
|
||||
Factory->>Factory: instantiate and require Node3D
|
||||
Factory->>Validator: is_scene_cache_current(root)
|
||||
Validator-->>Factory: current/stale
|
||||
else live source
|
||||
Loader->>Factory: duplicate_live_prototype(path, prototype, placement)
|
||||
Factory->>Factory: duplicate and require Node3D
|
||||
end
|
||||
Factory->>Factory: assign basename
|
||||
Factory->>Resolver: resolve_world_transform(placement)
|
||||
Resolver-->>Factory: exact Transform3D
|
||||
Factory-->>Loader: detached Node3D or null
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Factory[WmoSceneInstanceFactory]
|
||||
Factory --> Validator[Injected WMO scene-cache validator]
|
||||
Factory --> Resolver[Injected WmoPlacementResolver]
|
||||
Factory --> Engine[PackedScene / Node3D / Transform3D]
|
||||
Factory -. no dependency .-> Preparation[WmoRuntimeScenePreparer]
|
||||
Factory -. no dependency .-> IO[ResourceLoader / FileAccess]
|
||||
Factory -. no dependency .-> Queue[WMO queues / scheduler]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Calls are renderer-main-thread only because PackedScene/Node APIs mutate.
|
||||
- The source scene/prototype remains caller/cache-owned.
|
||||
- The factory owns a newly created root until rejection or successful return.
|
||||
- Successful return transfers detached-root ownership to the caller.
|
||||
- Descendant Mesh/Material Resources retain engine duplicate/instantiate identity.
|
||||
- The factory retains only injected stateless dependencies, never Nodes/Resources.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Recovery |
|
||||
|---|---|---|---|
|
||||
| Null scene/prototype | Guard | Return null without allocation | Correct caller source |
|
||||
| Missing validator | Currentness guard | Cached candidate rejected/freed | Fix composition |
|
||||
| Missing resolver | Guard before creation | Return null without allocation | Fix composition |
|
||||
| Non-Node3D root | Runtime type check | Free candidate and return null | Rebuild invalid cache/source |
|
||||
| Stale cached root | Injected validator | Free candidate; skip placement | Rebuild cache/current metadata |
|
||||
| Placement cancellation | Loader lifecycle | Detached/attached result released by caller | Existing retry path |
|
||||
| Shutdown | No retained candidates | Nothing to drain | Existing loader teardown |
|
||||
|
||||
The non-Node3D cached rejection now frees the created invalid root synchronously.
|
||||
Normal admitted caches already enforce Node3D through the scene finalizer, so this
|
||||
closes an error-path lifetime leak without changing valid rendered output.
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
No new settings. Cache-currentness rules belong to the injected WMOBuilder
|
||||
boundary; placement formulas belong to `WmoPlacementResolver`.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No format/version change and no rebake. The factory reads no files and writes no
|
||||
metadata. Existing cache validator version policy remains authoritative.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The factory emits no logs or metrics. Loader cache/placement metrics and
|
||||
synthetic rejection contracts remain the diagnostic surfaces.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_wmo_scene_instance_factory.gd` covers cached validation-before-placement,
|
||||
exact accepted root/descendant Resource identity, stale-root free, non-Node3D
|
||||
rejection, live validator suppression, detached ownership, dependencies,
|
||||
basename/Transform3D application, source boundaries and 1,000 duplicates.
|
||||
- Adjacent scene finalizer, placement resolver, runtime preparer, shutdown and
|
||||
checkpoint regressions protect lifecycle and visible output.
|
||||
- Fidelity evidence is behavior-preserving extraction for valid inputs. The
|
||||
invalid non-Node3D free is a lifetime fix, not a visual 3.3.5a change.
|
||||
|
||||
The synthetic budget requires 1,000 simple live duplicates in under one second.
|
||||
Asset-backed CPU/GPU p95/p99 and long-traversal evidence remain pending.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed cached/live instances can compare placement and lifetime without
|
||||
changing the factory API.
|
||||
- New source kinds should be separate explicit methods only when their validation
|
||||
and identity semantics differ materially.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Cached WMO instantiation | Implemented extraction | Type/currentness/name/placement/lifetime contract | Serialized asset-backed cache corpus pending |
|
||||
| Live prototype duplication | Implemented extraction | Identity/name/placement/validator-suppression contract | Asset-backed traversal/leak evidence pending |
|
||||
| Runtime preparation | Separate implemented service | Runtime scene preparer regression | Visual/GPU p95/p99 pending |
|
||||
| Attachment/registry lifetime | Loader-owned | Existing WMO placement/shutdown regressions | Further orchestration extraction pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Scene instantiation/duplication remains synchronous main-thread work.
|
||||
- No private WMO corpus, portal/room behavior, long traversal, leak/GPU timing or
|
||||
paired original-client capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live creation, validation, identity and placement |
|
||||
| `src/render/wmo/wmo_placement_resolver.gd` | Canonical WMO placement Transform3D |
|
||||
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Post-factory cached/live render preparation |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Source selection, cache/prototype lookup, attachment and lifetime |
|
||||
| `src/tools/verify_wmo_scene_instance_factory.gd` | Synthetic type/identity/lifetime/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-placement-resolver.md`](wmo-placement-resolver.md)
|
||||
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
|
||||
- [`wmo-runtime-scene-preparer.md`](wmo-runtime-scene-preparer.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -7,7 +7,7 @@
|
||||
| Status | Partial |
|
||||
| Target/work package | M00 baseline; `M01-RND-STREAMING-FOCUS-001`; `M01-QAR-SERVER-SPAWN-RENDERER-001`; M03 facade/planner/scheduler/internal-access/ground/environment/entity packages; M03 terrain packages; M03 M2 packages; M03 WMO placement package |
|
||||
| Owners | Renderer workstream / milestone integrator |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-scene-preparer`, 2026-08-01 |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-instance-factory`, 2026-08-01 |
|
||||
| Profiles/capabilities | `Performance`, `Balanced`, `High`, `Custom`; Blizzlike fidelity incomplete |
|
||||
|
||||
## Purpose
|
||||
@@ -74,6 +74,8 @@ flowchart LR
|
||||
WmoGroupMaterializer --> Scene
|
||||
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
|
||||
WmoScenePreparer --> Scene
|
||||
Loader --> WmoInstanceFactory[WmoSceneInstanceFactory]
|
||||
WmoInstanceFactory --> WmoScenePreparer
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
@@ -170,6 +172,7 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `WmoRenderBuildQueue` / `WmoRenderBuildJob` | Internal WMO pending-state service | Owns typed root/resource/cursor jobs and FIFO placement keys | Renderer main thread; map session | Invalid enqueue rejected; duplicate/stale behavior preserved |
|
||||
| `WmoRenderGroupMaterializer.materialize_mesh_group/materialize_multimesh_group` | Internal WMO scene-materialization service | Creates, configures and attaches one indexed lightweight render group | Renderer main thread; stateless after each call | Invalid parent/resource/index returns null without attachment |
|
||||
| `WmoRuntimeScenePreparer.prepare_cached_instance/prepare_live_instance` | Internal WMO subtree-preparation service | Preserves cached/live Mesh-finalization distinction, direct occluder policy and recursive shadow enabling | Renderer main thread; stateless after each call | Null/freed root returns false |
|
||||
| `WmoSceneInstanceFactory.instantiate_cached_scene/duplicate_live_prototype` | Internal WMO instance-creation service | Validates/creates detached cached/live roots and applies shared name/placement | Renderer main thread; stateless after each call | Invalid/stale/dependency failure returns null and frees created rejects |
|
||||
| `WmoRenderResourceCacheState` | Internal WMO cache-state service | Owns validated Resources, negative entries and pending cache paths | Renderer main thread; map/cache session | Invalid/occupied request and unknown completion are rejected |
|
||||
| `WmoRenderResourceFinalizer` | Internal WMO terminal-I/O service | Polls lightweight render requests, validates script/format and publishes Resource/missing outcomes | Renderer main thread; stateless across calls | Non-terminal retained; failed/null/wrong/stale complete missing |
|
||||
| `WmoSceneResourceCacheState` | Internal WMO cache-state service | Owns validated PackedScenes, negative entries and pending `.tscn` paths | Renderer main thread; map/cache session | Direct missing and terminal request transitions remain distinct |
|
||||
@@ -214,6 +217,7 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal WMO pending build | Placement key, Node3D root, WMO Resource and cursors | Loader / `WmoRenderBuildQueue` | Loader drain and step planner adapter | Queue-owned job and strong references | Until cancel/clear/replacement |
|
||||
| Internal WMO group materialization | Parent root, exact Mesh/MultiMesh, indexed metadata and render settings | Loader / `WmoRenderGroupMaterializer` | Attached geometry node | Parent owns node and exact Resource reference | One main-thread group operation |
|
||||
| Internal WMO subtree preparation | Cached/live root, extracted directory and render policies | Loader / `WmoRuntimeScenePreparer` | Borrowed subtree and runtime Mesh finalizer | Loader/placement owns subtree; preparer retains nothing | One main-thread instance preparation |
|
||||
| Internal WMO instance creation | Cached PackedScene or live prototype, path and placement | Loader / `WmoSceneInstanceFactory` | Runtime scene preparer and attachment adapter | Factory owns candidate until detached-root transfer | One main-thread creation |
|
||||
| Internal WMO render cache | Normalized path, cache path and validated Resource | Loader / `WmoRenderResourceCacheState` | Loader lookup, ResourceLoader poll and build queue | State-owned Resource/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal WMO scene cache | Normalized path, `.tscn` path and validated PackedScene | Loader / `WmoSceneResourceCacheState` | Loader lookup, request poll and scene instantiation | State-owned PackedScene/path references; detached request snapshots | Until transient/full clear |
|
||||
| Internal ADT water load | Tile key, ADT path, task ID and parsed Dictionary | Loader/worker / `AdtWaterLoadPipelineState` | Loader task start, budgeted drain and finalization | State-owned records; mutex result mailbox | Request through result completion/reset |
|
||||
@@ -425,6 +429,9 @@ sequenceDiagram
|
||||
creation, render settings and attachment without retaining engine objects.
|
||||
`WmoRuntimeScenePreparer` owns cached-only Mesh traversal/finalization plus the
|
||||
shared direct-Occluders and recursive shadow policies for cached/live roots.
|
||||
`WmoSceneInstanceFactory` owns cached/live detached-root creation, cache
|
||||
validation, basename and placement application; loader retains source lookup,
|
||||
runtime preparation, attachment and lifetime.
|
||||
`WmoRenderResourceCacheState` owns validated render Resources, negative entries
|
||||
and pending cache paths; `WmoRenderResourceFinalizer` owns its terminal
|
||||
ResourceLoader polling and script/format validation. `WmoSceneResourceCacheState`
|
||||
@@ -576,6 +583,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
- WMO runtime scene preparer contract: cached/live finalizer distinction,
|
||||
exact Mesh traversal order, direct occluder removal, recursive shadow policy,
|
||||
ownership boundaries and bounded main-thread timing.
|
||||
- WMO scene instance factory contract: cached validation-before-placement,
|
||||
stale/type rejection lifetime, live validator suppression, exact descendant
|
||||
Resource identity, naming/placement and bounded main-thread timing.
|
||||
- WMO render Resource cache contract: invalid/duplicate request rejection,
|
||||
validated/missing terminal transitions, transient/full reset, detached sorted
|
||||
diagnostics, loader-owned version validation and bounded timing.
|
||||
@@ -638,6 +648,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Asset-backed traversal/leak evidence pending |
|
||||
| WMO render group materializer | Implemented extraction | Synthetic Resource/name/transform/render/attachment/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
||||
| WMO runtime scene preparer | Implemented extraction | Synthetic cached/live traversal/occluder/shadow/source/timing contract | Asset-backed visual/leak/GPU p95/p99 pending |
|
||||
| WMO scene instance factory | Implemented extraction | Synthetic cached/live type/identity/lifetime/name/placement/source/timing contract | Serialized/asset-backed traversal/leak evidence pending |
|
||||
| WMO render Resource cache state | Implemented extraction | Scene-free lifecycle/exclusivity/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||
| WMO render Resource finalizer | Implemented extraction | Status/order/script/format/adoption/source/timing contract | Serialized/asset-backed corrupt-cache and leak evidence pending |
|
||||
| WMO scene Resource cache state | Implemented extraction | Scene-free lifecycle/direct-missing/source/timing plus shutdown contract | Asset-backed traversal/leak evidence pending |
|
||||
@@ -722,6 +733,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Cached WMO runtime refresh admission, surface iteration and material reconstruction |
|
||||
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed lightweight WMO geometry-node creation, render setup and attachment |
|
||||
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached/live WMO subtree Mesh traversal and render policy |
|
||||
| `src/render/wmo/wmo_scene_instance_factory.gd` | Cached/live detached-root creation, validation, identity and placement |
|
||||
| `src/render/streaming/streaming_target_planner.gd` | Scene-free wanted/retained ADT target calculation |
|
||||
| `src/render/streaming/streaming_target_policy.gd` | Immutable renderer radius/prefetch policy |
|
||||
| `src/render/streaming/streaming_target_plan.gd` | Immutable planner result with read-only tile-key sets |
|
||||
@@ -761,6 +773,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | WMO Mesh identity/version/material-definition/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_render_group_materializer.gd` | WMO render-group Resource/name/transform/render/attachment/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | WMO cached/live traversal/occluder/shadow/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_scene_instance_factory.gd` | WMO cached/live type/identity/lifetime/name/placement/boundary/timing regression |
|
||||
| `src/tools/verify_streaming_target_planner.gd` | Planner behavior, dependency and bounded timing regression |
|
||||
| `src/tools/verify_render_budget_scheduler.gd` | Scheduler bounds, shared-lane priority, cancellation and timing regression |
|
||||
| `src/tools/verify_renderer_internal_access.gd` | Gameplay/EditorPlugin/registered renderer-tool boundary gate derived from private streamer fields |
|
||||
@@ -772,6 +785,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/native/src/*_loader.cpp` | Native binary parsing |
|
||||
| `src/tools/build_*cache.gd`, `src/tools/bake_*cache.gd` | Offline cache generation |
|
||||
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
|
||||
| `tools/compare_render_performance.ps1` | Exact-environment single/repeated report comparator |
|
||||
| `tools/verify_render_performance_stability.ps1` | Repeated-sample/long-window repeatability gate |
|
||||
| `src/tools/verify_renderer_closeout_contracts.gd` | Worker, main-thread, cache-version and nested-GLB closeout contracts |
|
||||
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
|
||||
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Headless ownership regression for detached runtime prototypes, resource caches and empty liquid roots |
|
||||
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
|
||||
|
||||
@@ -39,9 +39,12 @@ func start_instance_playback(
|
||||
var phase := phase_for_instance(relative_path, instance_index)
|
||||
var native_diagnostics: Array[Dictionary] = []
|
||||
for animator in native_animators_in_subtree(root, native_animator_script):
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if animator.has_method("prepare_runtime_at_phase"):
|
||||
animator.prepare_runtime_at_phase(phase)
|
||||
else:
|
||||
if animator.has_method("prepare_runtime"):
|
||||
animator.prepare_runtime()
|
||||
animator.set_phase(phase)
|
||||
if collect_native_diagnostics and animator.has_method("runtime_debug_state"):
|
||||
var diagnostic_variant = animator.runtime_debug_state()
|
||||
if diagnostic_variant is Dictionary:
|
||||
|
||||
@@ -0,0 +1,73 @@
|
||||
class_name WmoSceneInstanceFactory
|
||||
extends RefCounted
|
||||
|
||||
## Creates detached cached/live WMO Node3D instances with the existing cache
|
||||
## validation, naming and world-placement rules. Runtime preparation is separate.
|
||||
|
||||
var _scene_cache_validator: Object
|
||||
var _placement_resolver: Object
|
||||
|
||||
|
||||
func _init(scene_cache_validator: Object, placement_resolver: Object) -> void:
|
||||
_scene_cache_validator = scene_cache_validator
|
||||
_placement_resolver = placement_resolver
|
||||
|
||||
|
||||
## Returns whether [param node] satisfies the injected WMO scene-cache contract.
|
||||
## Null nodes or missing validators return false without mutation.
|
||||
func is_cached_node_current(node: Node) -> bool:
|
||||
if node == null or _scene_cache_validator == null:
|
||||
return false
|
||||
return bool(_scene_cache_validator.call("is_scene_cache_current", node))
|
||||
|
||||
|
||||
## Instantiates, validates, names and places a cached WMO PackedScene. Rejected
|
||||
## instantiated roots are freed synchronously. The accepted detached Node3D is
|
||||
## caller-owned and retains its exact descendant Resource identities.
|
||||
func instantiate_cached_scene(
|
||||
relative_path: String,
|
||||
scene: PackedScene,
|
||||
placement: Dictionary) -> Node3D:
|
||||
if scene == null or _placement_resolver == null:
|
||||
return null
|
||||
var instantiated_root := scene.instantiate()
|
||||
if not (instantiated_root is Node3D):
|
||||
if instantiated_root != null:
|
||||
instantiated_root.free()
|
||||
return null
|
||||
var instance := instantiated_root as Node3D
|
||||
if not is_cached_node_current(instance):
|
||||
instance.free()
|
||||
return null
|
||||
_apply_identity_and_placement(instance, relative_path, placement)
|
||||
return instance
|
||||
|
||||
|
||||
## Duplicates, names and places a live-built WMO prototype. The detached result
|
||||
## is caller-owned. Null inputs, missing placement composition or an unexpected
|
||||
## non-Node3D duplicate return null; rejected duplicates are freed synchronously.
|
||||
func duplicate_live_prototype(
|
||||
relative_path: String,
|
||||
prototype: Node3D,
|
||||
placement: Dictionary) -> Node3D:
|
||||
if prototype == null or _placement_resolver == null:
|
||||
return null
|
||||
var duplicated_root := prototype.duplicate()
|
||||
if not (duplicated_root is Node3D):
|
||||
if duplicated_root != null:
|
||||
duplicated_root.free()
|
||||
return null
|
||||
var instance := duplicated_root as Node3D
|
||||
_apply_identity_and_placement(instance, relative_path, placement)
|
||||
return instance
|
||||
|
||||
|
||||
func _apply_identity_and_placement(
|
||||
instance: Node3D,
|
||||
relative_path: String,
|
||||
placement: Dictionary) -> void:
|
||||
instance.name = relative_path.get_file().get_basename()
|
||||
instance.transform = _placement_resolver.call(
|
||||
"resolve_world_transform",
|
||||
placement
|
||||
) as Transform3D
|
||||
@@ -0,0 +1 @@
|
||||
uid://c13w66d7uaf2n
|
||||
@@ -26,11 +26,13 @@ func setup(target_mesh_instance: MeshInstance3D, bone_data: Array, surface_data:
|
||||
_capture_materials()
|
||||
_make_mesh_unique()
|
||||
_rebuild_mesh(0.0)
|
||||
set_process(mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0)
|
||||
_prepared = _has_runtime_animation_data()
|
||||
set_process(_prepared)
|
||||
|
||||
|
||||
func _ready() -> void:
|
||||
prepare_runtime()
|
||||
if not _prepared:
|
||||
prepare_runtime()
|
||||
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
@@ -41,24 +43,49 @@ func _process(delta: float) -> void:
|
||||
|
||||
|
||||
func set_phase(phase: float) -> void:
|
||||
if animation_length <= 0.0:
|
||||
_time = 0.0
|
||||
else:
|
||||
_time = fposmod(animation_length * phase, animation_length)
|
||||
_set_phase_time(phase)
|
||||
_rebuild_mesh(_time)
|
||||
|
||||
|
||||
func prepare_runtime() -> bool:
|
||||
return _prepare_runtime(false)
|
||||
|
||||
|
||||
## Rebinds a duplicated animator to its local mesh and applies its deterministic
|
||||
## phase with one deformation rebuild. This must happen before attachment so
|
||||
## _ready() can remain idempotent for already prepared runtime instances.
|
||||
func prepare_runtime_at_phase(phase: float) -> bool:
|
||||
_set_phase_time(phase)
|
||||
return _prepare_runtime(true)
|
||||
|
||||
|
||||
func _prepare_runtime(force_rebuild: bool) -> bool:
|
||||
if _prepared and not force_rebuild:
|
||||
set_process(true)
|
||||
return true
|
||||
_resolve_mesh_instance()
|
||||
if force_rebuild:
|
||||
_materials.clear()
|
||||
_capture_materials()
|
||||
_unique_mesh_ready = false
|
||||
_make_mesh_unique()
|
||||
_rebuild_mesh(_time)
|
||||
_prepared = mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
|
||||
_prepared = _has_runtime_animation_data()
|
||||
set_process(_prepared)
|
||||
return _prepared
|
||||
|
||||
|
||||
func _has_runtime_animation_data() -> bool:
|
||||
return mesh != null and not bones.is_empty() and not surfaces.is_empty() and animation_length > 0.0
|
||||
|
||||
|
||||
func _set_phase_time(phase: float) -> void:
|
||||
if animation_length <= 0.0:
|
||||
_time = 0.0
|
||||
else:
|
||||
_time = fposmod(animation_length * phase, animation_length)
|
||||
|
||||
|
||||
func runtime_debug_state() -> Dictionary:
|
||||
return {
|
||||
"prepared": _prepared,
|
||||
@@ -81,11 +108,12 @@ func _resolve_mesh_instance() -> void:
|
||||
func _make_mesh_unique() -> void:
|
||||
if _unique_mesh_ready or mesh_instance == null or mesh_instance.mesh == null:
|
||||
return
|
||||
var duplicated := mesh_instance.mesh.duplicate(true) as ArrayMesh
|
||||
if duplicated == null:
|
||||
return
|
||||
mesh_instance.mesh = duplicated
|
||||
mesh = duplicated
|
||||
# _rebuild_mesh() replaces every surface from the retained native arrays, so
|
||||
# copying the source ArrayMesh would only duplicate data that is discarded.
|
||||
# Materials were captured before this call and are intentionally shared.
|
||||
var instance_mesh := ArrayMesh.new()
|
||||
mesh_instance.mesh = instance_mesh
|
||||
mesh = instance_mesh
|
||||
_unique_mesh_ready = true
|
||||
|
||||
|
||||
@@ -143,20 +171,20 @@ func _rebuild_mesh(time: float) -> void:
|
||||
continue
|
||||
var transform: Transform3D = bone_matrices[bone_index]
|
||||
skinned_pos += transform * base_vertices[vertex_index] * weight
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
skinned_nrm += (transform.basis * base_normals[vertex_index]) * weight
|
||||
total_weight += weight
|
||||
if total_weight > 0.0:
|
||||
vertices[vertex_index] = skinned_pos / total_weight
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = (skinned_nrm / total_weight).normalized()
|
||||
else:
|
||||
vertices[vertex_index] = base_vertices[vertex_index]
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = base_normals[vertex_index]
|
||||
else:
|
||||
vertices[vertex_index] = base_vertices[vertex_index]
|
||||
if normals.size() == base_normals.size():
|
||||
if not normals.is_empty():
|
||||
normals[vertex_index] = base_normals[vertex_index]
|
||||
|
||||
var arrays := []
|
||||
|
||||
@@ -42,6 +42,9 @@ const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
|
||||
const WMO_RUNTIME_SCENE_PREPARER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
||||
)
|
||||
const WMO_SCENE_INSTANCE_FACTORY_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_instance_factory.gd"
|
||||
)
|
||||
const M2_BUILDER_SCRIPT := preload("res://addons/mpq_extractor/loaders/m2_builder.gd")
|
||||
const M2_NATIVE_ANIMATOR_SCRIPT := preload("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
const STREAMING_FOCUS_SCRIPT := preload("res://src/domain/streaming/streaming_focus.gd")
|
||||
@@ -393,6 +396,10 @@ var _wmo_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new(
|
||||
)
|
||||
var _wmo_missing_cache: Dictionary = {}
|
||||
var _wmo_placement_resolver := WMO_PLACEMENT_RESOLVER_SCRIPT.new()
|
||||
var _wmo_scene_instance_factory := WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new(
|
||||
WMO_BUILDER_SCRIPT,
|
||||
_wmo_placement_resolver
|
||||
)
|
||||
var _world_wmo_root: Node3D
|
||||
var _wmo_placement_registry := WMO_PLACEMENT_REGISTRY_SCRIPT.new()
|
||||
var _wmo_render_build_step_planner := WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.new()
|
||||
@@ -3692,7 +3699,13 @@ func _process_wmo_build_jobs() -> void:
|
||||
)
|
||||
refs.append(unique_key)
|
||||
else:
|
||||
var scene_instance := _instantiate_wmo_scene(rel_path, scene, placement)
|
||||
var scene_instance := (
|
||||
_wmo_scene_instance_factory.instantiate_cached_scene(
|
||||
rel_path,
|
||||
scene,
|
||||
placement
|
||||
)
|
||||
)
|
||||
if scene_instance != null:
|
||||
_prepare_runtime_wmo_instance(scene_instance)
|
||||
_world_wmo_root.add_child(scene_instance)
|
||||
@@ -3920,26 +3933,6 @@ func _get_resource_file_size(path: String) -> int:
|
||||
return int(length)
|
||||
|
||||
|
||||
func _is_wmo_node_cache_current(node: Node) -> bool:
|
||||
if node == null:
|
||||
return false
|
||||
return bool(WMO_BUILDER_SCRIPT.is_scene_cache_current(node))
|
||||
|
||||
|
||||
func _instantiate_wmo_scene(rel_path: String, scene: PackedScene, placement: Dictionary) -> Node3D:
|
||||
if scene == null:
|
||||
return null
|
||||
var instance := scene.instantiate() as Node3D
|
||||
if instance == null:
|
||||
return null
|
||||
if not _is_wmo_node_cache_current(instance):
|
||||
instance.free()
|
||||
return null
|
||||
instance.name = rel_path.get_file().get_basename()
|
||||
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
|
||||
return instance
|
||||
|
||||
|
||||
func _prepare_runtime_wmo_instance(instance: Node3D) -> void:
|
||||
_wmo_runtime_scene_preparer.prepare_cached_instance(
|
||||
instance,
|
||||
@@ -4632,11 +4625,15 @@ func _instantiate_wmo_world(rel_path: String, placement: Dictionary) -> Node3D:
|
||||
if prototype == null:
|
||||
return null
|
||||
|
||||
var instance := prototype.duplicate()
|
||||
instance.name = rel_path.get_file().get_basename()
|
||||
# WMOs are parented to _terrain_root, whose position absorbs the editor
|
||||
# offset — use world-space placement.pos directly (no tile_origin subtraction).
|
||||
instance.transform = _wmo_placement_resolver.resolve_world_transform(placement)
|
||||
# offset — the factory applies world-space placement.pos directly.
|
||||
var instance := _wmo_scene_instance_factory.duplicate_live_prototype(
|
||||
rel_path,
|
||||
prototype,
|
||||
placement
|
||||
)
|
||||
if instance == null:
|
||||
return null
|
||||
_wmo_runtime_scene_preparer.prepare_live_instance(
|
||||
instance,
|
||||
enable_occlusion_culling,
|
||||
@@ -4736,7 +4733,7 @@ func _get_or_load_wmo_prototype(rel_path: String) -> Node3D:
|
||||
var resource: Resource = load(cache_path)
|
||||
if resource is PackedScene:
|
||||
var node = (resource as PackedScene).instantiate()
|
||||
if node is Node3D and _is_wmo_node_cache_current(node):
|
||||
if node is Node3D and _wmo_scene_instance_factory.is_cached_node_current(node):
|
||||
_wmo_prototype_cache[normalized_rel] = node as Node3D
|
||||
return node as Node3D
|
||||
if node is Node:
|
||||
|
||||
@@ -176,14 +176,14 @@ func _bake_glb_animation_cache(
|
||||
if not force and FileAccess.file_exists(abs_out_glb):
|
||||
return true
|
||||
var abs_converter := ProjectSettings.globalize_path(converter)
|
||||
var abs_output := ProjectSettings.globalize_path(output_dir)
|
||||
var converter_output_directory := abs_out_glb.get_base_dir()
|
||||
if not FileAccess.file_exists(abs_converter):
|
||||
push_warning("M2 GLB converter not found: %s" % converter)
|
||||
return false
|
||||
var stdout := []
|
||||
var exit_code := OS.execute(
|
||||
python_exe,
|
||||
[abs_converter, abs_m2, abs_output],
|
||||
[abs_converter, abs_m2, converter_output_directory],
|
||||
stdout,
|
||||
true,
|
||||
false)
|
||||
|
||||
@@ -16,6 +16,7 @@ func _initialize() -> void:
|
||||
_verify_animation_selection_priority(failures)
|
||||
_verify_player_loop_play_and_seek(failures)
|
||||
_verify_native_copy_start_and_diagnostics(failures)
|
||||
_verify_native_single_rebuild_preparation(failures)
|
||||
_verify_ownership_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
@@ -24,7 +25,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=15 iterations=20000 elapsed_ms=%.3f"
|
||||
"M2_ANIMATION_PLAYBACK_CONTROLLER PASS cases=20 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
@@ -142,6 +143,47 @@ func _verify_native_copy_start_and_diagnostics(failures: Array[String]) -> void:
|
||||
target_root.free()
|
||||
|
||||
|
||||
func _verify_native_single_rebuild_preparation(failures: Array[String]) -> void:
|
||||
var fixture_root := Node3D.new()
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "Mesh"
|
||||
var source_mesh := ArrayMesh.new()
|
||||
var source_arrays := []
|
||||
source_arrays.resize(Mesh.ARRAY_MAX)
|
||||
source_arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
|
||||
Vector3.ZERO,
|
||||
Vector3.RIGHT,
|
||||
Vector3.UP,
|
||||
])
|
||||
source_arrays[Mesh.ARRAY_INDEX] = PackedInt32Array([0, 1, 2])
|
||||
source_mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, source_arrays)
|
||||
var shared_material := StandardMaterial3D.new()
|
||||
source_mesh.surface_set_material(0, shared_material)
|
||||
mesh_instance.mesh = source_mesh
|
||||
fixture_root.add_child(mesh_instance)
|
||||
|
||||
var animator: Node = NATIVE_ANIMATOR_SCRIPT.new()
|
||||
fixture_root.add_child(animator)
|
||||
animator.setup(
|
||||
mesh_instance,
|
||||
[{"parent": -1, "pivot": Vector3.ZERO}],
|
||||
[{
|
||||
"vertices": PackedVector3Array([Vector3.ZERO, Vector3.RIGHT, Vector3.UP]),
|
||||
"indices": PackedInt32Array([0, 1, 2]),
|
||||
}],
|
||||
4.0
|
||||
)
|
||||
var prepared := bool(animator.prepare_runtime_at_phase(0.25))
|
||||
var phased_mesh: Mesh = mesh_instance.mesh
|
||||
animator.call("_ready")
|
||||
_expect_true(prepared, "native phased preparation succeeds", failures)
|
||||
_expect_float_equal(float(animator.get("_time")), 1.0, "native phased preparation time", failures)
|
||||
_expect_true(phased_mesh != source_mesh, "native phased Mesh is instance-local", failures)
|
||||
_expect_same(phased_mesh.surface_get_material(0), shared_material, "native Material remains shared", failures)
|
||||
_expect_same(mesh_instance.mesh, phased_mesh, "native ready does not duplicate prepared mesh", failures)
|
||||
fixture_root.free()
|
||||
|
||||
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var controller_source := FileAccess.get_file_as_string(CONTROLLER_PATH)
|
||||
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||
@@ -156,6 +198,17 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
_expect_false(loader_source.contains(removed_loader_function), "legacy helper removed: %s" % removed_loader_function, failures)
|
||||
_expect_equal(materializer_source.count("_animation_playback_controller.copy_native_animator_data("), 1, "native copy delegates once", failures)
|
||||
_expect_equal(materializer_source.count("_animation_playback_controller.start_instance_playback("), 1, "playback delegates once", failures)
|
||||
_expect_true(
|
||||
controller_source.contains("animator.prepare_runtime_at_phase(phase)"),
|
||||
"native phase preparation uses one rebuild",
|
||||
failures
|
||||
)
|
||||
var native_animator_source := FileAccess.get_file_as_string("res://src/scenes/streaming/m2_native_animator.gd")
|
||||
_expect_true(
|
||||
native_animator_source.contains("if not _prepared:\n\t\tprepare_runtime()"),
|
||||
"ready is idempotent after pre-attachment preparation",
|
||||
failures
|
||||
)
|
||||
for retained_materializer_rule in [
|
||||
"Node.DUPLICATE_SIGNALS | Node.DUPLICATE_GROUPS | Node.DUPLICATE_SCRIPTS",
|
||||
"batch_root.add_child(instance)",
|
||||
|
||||
@@ -0,0 +1,173 @@
|
||||
extends SceneTree
|
||||
|
||||
## M03 closeout gate for cache versions and worker/main-thread render boundaries.
|
||||
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||
const M2_CACHE_BAKER_PATH := "res://src/tools/bake_m2_cache.gd"
|
||||
const WORKER_FUNCTIONS: Array[String] = [
|
||||
"_load_tile_task",
|
||||
"_load_terrain_splat_task",
|
||||
"_load_tile_water_task",
|
||||
"_group_tile_m2_task",
|
||||
]
|
||||
const WORKER_FORBIDDEN_TOKENS: Array[String] = [
|
||||
"RenderingServer",
|
||||
".add_child(",
|
||||
".queue_free(",
|
||||
".free(",
|
||||
"Node3D.new(",
|
||||
"MeshInstance3D.new(",
|
||||
"MultiMeshInstance3D.new(",
|
||||
"ArrayMesh.new(",
|
||||
"build_tile_water_scene(",
|
||||
"build_tile_coarse_render_payload(",
|
||||
]
|
||||
const MAIN_THREAD_FRAME_STEPS: Array[String] = [
|
||||
"_drain_tile_load_results()",
|
||||
"_drain_terrain_upgrade_results()",
|
||||
"_drain_terrain_control_splat_cache_results()",
|
||||
"_drain_terrain_splat_cache_results()",
|
||||
"_drain_terrain_splat_results()",
|
||||
"_process_water_load_queue()",
|
||||
"_drain_water_load_results()",
|
||||
"_process_queues()",
|
||||
"_drain_m2_group_results()",
|
||||
"_drain_m2_animation_loads()",
|
||||
"_drain_m2_mesh_loads()",
|
||||
"_process_m2_build_jobs()",
|
||||
"_process_wmo_build_jobs()",
|
||||
"_process_wmo_render_build_jobs()",
|
||||
"_process_detail_asset_queue()",
|
||||
]
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var process_source := _function_source(loader_source, "_process", failures)
|
||||
_verify_frame_entrypoint(process_source, failures)
|
||||
_verify_worker_boundaries(loader_source, failures)
|
||||
_verify_rendering_server_boundary(loader_source, failures)
|
||||
_verify_cache_contract(loader_source, failures)
|
||||
_verify_m2_glb_cache_output_contract(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("RENDERER_CLOSEOUT_CONTRACTS: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("RENDERER_CLOSEOUT_CONTRACTS PASS workers=%d frame_steps=%d cache_versions=7 nested_glb=1" % [
|
||||
WORKER_FUNCTIONS.size(),
|
||||
MAIN_THREAD_FRAME_STEPS.size(),
|
||||
])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_frame_entrypoint(process_source: String, failures: Array[String]) -> void:
|
||||
var scheduler_index := process_source.find("_render_budget_scheduler.begin_frame(")
|
||||
if scheduler_index < 0:
|
||||
failures.append("_process does not begin a render-budget frame")
|
||||
return
|
||||
for step in MAIN_THREAD_FRAME_STEPS:
|
||||
var step_index := process_source.find(step)
|
||||
if step_index < 0:
|
||||
failures.append("_process is missing main-thread step %s" % step)
|
||||
elif step_index < scheduler_index:
|
||||
failures.append("%s runs before render-budget frame admission" % step)
|
||||
|
||||
|
||||
func _verify_worker_boundaries(loader_source: String, failures: Array[String]) -> void:
|
||||
for function_name in WORKER_FUNCTIONS:
|
||||
var function_source := _function_source(loader_source, function_name, failures)
|
||||
for forbidden_token in WORKER_FORBIDDEN_TOKENS:
|
||||
if function_source.contains(forbidden_token):
|
||||
failures.append("worker %s contains main-thread token %s" % [
|
||||
function_name,
|
||||
forbidden_token,
|
||||
])
|
||||
|
||||
|
||||
func _verify_rendering_server_boundary(loader_source: String, failures: Array[String]) -> void:
|
||||
var remaining_source := loader_source
|
||||
for function_name in ["_create_render_instance", "_free_render_instance"]:
|
||||
var function_source := _function_source(loader_source, function_name, failures)
|
||||
remaining_source = remaining_source.replace(function_source, "")
|
||||
if remaining_source.contains("RenderingServer.instance_create("):
|
||||
failures.append("RenderingServer instance creation escaped the owned adapter")
|
||||
if remaining_source.contains("RenderingServer.free_rid("):
|
||||
failures.append("RenderingServer RID release escaped the owned adapter")
|
||||
|
||||
|
||||
func _verify_cache_contract(loader_source: String, failures: Array[String]) -> void:
|
||||
var manifest_source := _read_text(MANIFEST_PATH, failures)
|
||||
var parsed_manifest = JSON.parse_string(manifest_source)
|
||||
if not (parsed_manifest is Dictionary):
|
||||
failures.append("baseline manifest is not a Dictionary")
|
||||
return
|
||||
var cache_contract: Dictionary = parsed_manifest.get("cache_contract", {})
|
||||
var expected_versions := {
|
||||
"baked_terrain": 5,
|
||||
"streaming_terrain": 2,
|
||||
"terrain_splat": 1,
|
||||
"terrain_control_splat": 3,
|
||||
"wmo_streaming": 2,
|
||||
"wmo_builder": 2,
|
||||
"m2_material": 2,
|
||||
}
|
||||
if cache_contract.size() != expected_versions.size():
|
||||
failures.append("cache contract key count changed: expected=%d actual=%d" % [
|
||||
expected_versions.size(),
|
||||
cache_contract.size(),
|
||||
])
|
||||
for cache_name in expected_versions:
|
||||
if not cache_contract.has(cache_name):
|
||||
failures.append("cache contract is missing %s" % cache_name)
|
||||
continue
|
||||
var cache_record: Dictionary = cache_contract[cache_name]
|
||||
if int(cache_record.get("version", -1)) != int(expected_versions[cache_name]):
|
||||
failures.append("cache version changed for %s" % cache_name)
|
||||
|
||||
var required_loader_declarations := [
|
||||
"const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5",
|
||||
"const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1",
|
||||
"const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3",
|
||||
"STREAMING_TILE_SCRIPT.FORMAT_VERSION",
|
||||
"WMO_STREAMING_SCRIPT.FORMAT_VERSION",
|
||||
]
|
||||
for declaration in required_loader_declarations:
|
||||
if not loader_source.contains(declaration):
|
||||
failures.append("loader cache-version boundary is missing %s" % declaration)
|
||||
|
||||
|
||||
func _verify_m2_glb_cache_output_contract(failures: Array[String]) -> void:
|
||||
var baker_source := _read_text(M2_CACHE_BAKER_PATH, failures)
|
||||
if not baker_source.contains(
|
||||
"var converter_output_directory := abs_out_glb.get_base_dir()"):
|
||||
failures.append("M2 GLB converter output is not derived from the nested cache path")
|
||||
if not baker_source.contains(
|
||||
"[abs_converter, abs_m2, converter_output_directory]"):
|
||||
failures.append("M2 GLB converter does not receive the nested output directory")
|
||||
|
||||
|
||||
func _function_source(source: String, function_name: String, failures: Array[String]) -> String:
|
||||
var signature := "func %s(" % function_name
|
||||
var start_index := source.find(signature)
|
||||
if start_index < 0:
|
||||
failures.append("missing function %s" % function_name)
|
||||
return ""
|
||||
var next_function_index := source.find("\nfunc ", start_index + signature.length())
|
||||
if next_function_index < 0:
|
||||
return source.substr(start_index)
|
||||
return source.substr(start_index, next_function_index - start_index)
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot read %s" % path)
|
||||
return ""
|
||||
var content := file.get_as_text()
|
||||
file.close()
|
||||
return content
|
||||
@@ -0,0 +1 @@
|
||||
uid://nqj0mu6r8omf
|
||||
@@ -4,6 +4,7 @@ extends SceneTree
|
||||
|
||||
const RESOLVER_SCRIPT := preload("res://src/render/wmo/wmo_placement_resolver.gd")
|
||||
const RESOLVER_PATH := "res://src/render/wmo/wmo_placement_resolver.gd"
|
||||
const INSTANCE_FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -25,7 +26,7 @@ func _initialize() -> void:
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_PLACEMENT_RESOLVER PASS cases=9 iterations=20000 elapsed_ms=%.3f"
|
||||
"WMO_PLACEMENT_RESOLVER PASS cases=10 iterations=20000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
@@ -138,6 +139,7 @@ func _verify_node_property_equivalence(failures: Array[String]) -> void:
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var resolver_source := _read_text(RESOLVER_PATH, failures)
|
||||
var instance_factory_source := _read_text(INSTANCE_FACTORY_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_PLACEMENT_RESOLVER_SCRIPT.new()"),
|
||||
"loader composes resolver",
|
||||
@@ -157,8 +159,13 @@ func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
)
|
||||
_expect_equal_int(
|
||||
loader_source.count("_wmo_placement_resolver.resolve_world_transform("),
|
||||
3,
|
||||
"three transform adapters",
|
||||
1,
|
||||
"one direct lightweight render-root transform adapter",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
instance_factory_source.contains('"resolve_world_transform"'),
|
||||
"cached/live instance factory delegates transform resolution",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
|
||||
@@ -0,0 +1,294 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free WMO cached/live instance validation, identity, placement,
|
||||
## lifetime, source-boundary and bounded-timing regression.
|
||||
|
||||
const FACTORY_SCRIPT := preload("res://src/render/wmo/wmo_scene_instance_factory.gd")
|
||||
const FACTORY_PATH := "res://src/render/wmo/wmo_scene_instance_factory.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeSceneCacheValidator extends RefCounted:
|
||||
var is_current := true
|
||||
var inspected_nodes: Array[Node] = []
|
||||
var events: Array[String]
|
||||
|
||||
func _init(shared_events: Array[String] = []) -> void:
|
||||
events = shared_events
|
||||
|
||||
func is_scene_cache_current(node: Node) -> bool:
|
||||
inspected_nodes.append(node)
|
||||
events.append("validate")
|
||||
return is_current
|
||||
|
||||
|
||||
class FakePlacementResolver extends RefCounted:
|
||||
var resolved_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0))
|
||||
var placements: Array[Dictionary] = []
|
||||
var events: Array[String]
|
||||
|
||||
func _init(shared_events: Array[String] = []) -> void:
|
||||
events = shared_events
|
||||
|
||||
func resolve_world_transform(placement: Dictionary) -> Transform3D:
|
||||
placements.append(placement.duplicate(true))
|
||||
events.append("resolve")
|
||||
return resolved_transform
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_cached_scene_contract(failures)
|
||||
_verify_stale_and_wrong_root_rejection(failures)
|
||||
_verify_live_prototype_contract(failures)
|
||||
_verify_invalid_composition(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_SCENE_INSTANCE_FACTORY: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_SCENE_INSTANCE_FACTORY PASS cases=41 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_cached_scene_contract(failures: Array[String]) -> void:
|
||||
var events: Array[String] = []
|
||||
var validator := FakeSceneCacheValidator.new(events)
|
||||
var resolver := FakePlacementResolver.new(events)
|
||||
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||
var fixture := _create_packed_wmo_scene()
|
||||
var scene: PackedScene = fixture["scene"]
|
||||
var source_mesh: Mesh = fixture["mesh"]
|
||||
var placement := {"pos": Vector3(1.0, 2.0, 3.0), "rotation": Vector3.ZERO}
|
||||
var instance: Node3D = factory.instantiate_cached_scene(
|
||||
"World/Wmo/Stormwind/Keep.wmo",
|
||||
scene,
|
||||
placement
|
||||
)
|
||||
_expect_true(instance != null, "current cached scene accepted", failures)
|
||||
_expect_true(instance.get_parent() == null, "cached result remains detached", failures)
|
||||
_expect_string_equal(instance.name, "Keep", "cached basename applied", failures)
|
||||
_expect_true(instance.transform == resolver.resolved_transform, "cached transform applied", failures)
|
||||
_expect_equal(validator.inspected_nodes.size(), 1, "cached root validated once", failures)
|
||||
_expect_same(validator.inspected_nodes[0], instance, "accepted exact root validated", failures)
|
||||
_expect_true(resolver.placements == [placement], "cached placement forwarded", failures)
|
||||
_expect_true(events == ["validate", "resolve"], "validate precedes placement", failures)
|
||||
var mesh_child := instance.get_node_or_null("Group") as MeshInstance3D
|
||||
_expect_true(mesh_child != null, "cached Mesh child retained", failures)
|
||||
_expect_same(mesh_child.mesh, source_mesh, "cached descendant Mesh identity retained", failures)
|
||||
_expect_true(factory.is_cached_node_current(instance), "public currentness delegates", failures)
|
||||
instance.free()
|
||||
|
||||
|
||||
func _verify_stale_and_wrong_root_rejection(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
validator.is_current = false
|
||||
var resolver := FakePlacementResolver.new()
|
||||
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||
var fixture := _create_packed_wmo_scene()
|
||||
_expect_true(
|
||||
factory.instantiate_cached_scene("world/stale.wmo", fixture["scene"], {}) == null,
|
||||
"stale cached scene rejected",
|
||||
failures
|
||||
)
|
||||
_expect_equal(validator.inspected_nodes.size(), 1, "stale root validated once", failures)
|
||||
_expect_false(
|
||||
is_instance_valid(validator.inspected_nodes[0]),
|
||||
"stale instantiated root freed",
|
||||
failures
|
||||
)
|
||||
_expect_equal(resolver.placements.size(), 0, "stale root skips placement", failures)
|
||||
|
||||
var control_root := Control.new()
|
||||
var wrong_root_scene := PackedScene.new()
|
||||
_expect_equal(wrong_root_scene.pack(control_root), OK, "wrong-root fixture packed", failures)
|
||||
control_root.free()
|
||||
validator.is_current = true
|
||||
_expect_true(
|
||||
factory.instantiate_cached_scene("world/control.wmo", wrong_root_scene, {}) == null,
|
||||
"non-Node3D cached root rejected",
|
||||
failures
|
||||
)
|
||||
_expect_equal(validator.inspected_nodes.size(), 1, "wrong root skips validator", failures)
|
||||
|
||||
|
||||
func _verify_live_prototype_contract(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var resolver := FakePlacementResolver.new()
|
||||
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||
var prototype := Node3D.new()
|
||||
prototype.name = "Prototype"
|
||||
var source_mesh := ArrayMesh.new()
|
||||
var mesh_child := MeshInstance3D.new()
|
||||
mesh_child.name = "Group"
|
||||
mesh_child.mesh = source_mesh
|
||||
prototype.add_child(mesh_child)
|
||||
var placement := {"pos": Vector3(7.0, 8.0, 9.0)}
|
||||
var instance: Node3D = factory.duplicate_live_prototype(
|
||||
"World/Live/Abbey.wmo",
|
||||
prototype,
|
||||
placement
|
||||
)
|
||||
_expect_true(instance != null, "live prototype duplicated", failures)
|
||||
_expect_false(is_same(instance, prototype), "live root identity isolated", failures)
|
||||
_expect_true(instance.get_parent() == null, "live result remains detached", failures)
|
||||
_expect_string_equal(instance.name, "Abbey", "live basename applied", failures)
|
||||
_expect_true(instance.transform == resolver.resolved_transform, "live transform applied", failures)
|
||||
_expect_same(
|
||||
(instance.get_node("Group") as MeshInstance3D).mesh,
|
||||
source_mesh,
|
||||
"live descendant Mesh identity retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(validator.inspected_nodes.size(), 0, "live path skips cache validator", failures)
|
||||
_expect_true(resolver.placements == [placement], "live placement forwarded", failures)
|
||||
instance.free()
|
||||
prototype.free()
|
||||
|
||||
|
||||
func _verify_invalid_composition(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var resolver := FakePlacementResolver.new()
|
||||
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||
_expect_false(factory.is_cached_node_current(null), "null currentness rejected", failures)
|
||||
_expect_true(
|
||||
factory.instantiate_cached_scene("world/a.wmo", null, {}) == null,
|
||||
"null PackedScene rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
factory.duplicate_live_prototype("world/a.wmo", null, {}) == null,
|
||||
"null prototype rejected",
|
||||
failures
|
||||
)
|
||||
var fixture := _create_packed_wmo_scene()
|
||||
var missing_validator_factory := FACTORY_SCRIPT.new(null, resolver)
|
||||
_expect_true(
|
||||
missing_validator_factory.instantiate_cached_scene(
|
||||
"world/a.wmo", fixture["scene"], {}
|
||||
) == null,
|
||||
"missing validator rejects cached instance",
|
||||
failures
|
||||
)
|
||||
var missing_resolver_factory := FACTORY_SCRIPT.new(validator, null)
|
||||
_expect_true(
|
||||
missing_resolver_factory.instantiate_cached_scene(
|
||||
"world/a.wmo", fixture["scene"], {}
|
||||
) == null,
|
||||
"missing resolver rejects cached instance before allocation",
|
||||
failures
|
||||
)
|
||||
var prototype := Node3D.new()
|
||||
_expect_true(
|
||||
missing_resolver_factory.duplicate_live_prototype("world/a.wmo", prototype, {}) == null,
|
||||
"missing resolver rejects live duplicate",
|
||||
failures
|
||||
)
|
||||
prototype.free()
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var factory_source := FileAccess.get_file_as_string(FACTORY_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_SCENE_INSTANCE_FACTORY_SCRIPT.new("),
|
||||
"loader composes scene instance factory",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_scene_instance_factory.instantiate_cached_scene("),
|
||||
"loader delegates cached instantiation",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_scene_instance_factory.duplicate_live_prototype("),
|
||||
"loader delegates live duplication",
|
||||
failures
|
||||
)
|
||||
for released_token in [
|
||||
"func _is_wmo_node_cache_current(",
|
||||
"func _instantiate_wmo_scene(",
|
||||
]:
|
||||
_expect_false(loader_source.contains(released_token), "loader releases %s" % released_token, failures)
|
||||
for owned_token in [
|
||||
"scene.instantiate()",
|
||||
"prototype.duplicate()",
|
||||
"is_scene_cache_current",
|
||||
"relative_path.get_file().get_basename()",
|
||||
"resolve_world_transform",
|
||||
]:
|
||||
_expect_true(factory_source.contains(owned_token), "factory owns %s" % owned_token, failures)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"RenderingServer.",
|
||||
"add_child(",
|
||||
".owner =",
|
||||
"WmoRuntimeScenePreparer",
|
||||
"_wmo_render_build_queue",
|
||||
]:
|
||||
_expect_false(factory_source.contains(forbidden_dependency), "factory omits %s" % forbidden_dependency, failures)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var resolver := FakePlacementResolver.new()
|
||||
var factory := FACTORY_SCRIPT.new(validator, resolver)
|
||||
var prototype := Node3D.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in 1000:
|
||||
var instance: Node3D = factory.duplicate_live_prototype(
|
||||
"world/timing.wmo", prototype, {}
|
||||
)
|
||||
instance.free()
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_equal(resolver.placements.size(), 1000, "timing duplicates complete", failures)
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "1000 duplicates under one second", failures)
|
||||
prototype.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _create_packed_wmo_scene() -> Dictionary:
|
||||
var source_root := Node3D.new()
|
||||
var mesh := ArrayMesh.new()
|
||||
var mesh_child := MeshInstance3D.new()
|
||||
mesh_child.name = "Group"
|
||||
mesh_child.mesh = mesh
|
||||
source_root.add_child(mesh_child)
|
||||
mesh_child.owner = source_root
|
||||
var scene := PackedScene.new()
|
||||
var pack_error := scene.pack(source_root)
|
||||
if pack_error != OK:
|
||||
push_error("WMO_SCENE_INSTANCE_FACTORY: cannot pack fixture")
|
||||
source_root.free()
|
||||
return {"scene": scene, "mesh": mesh}
|
||||
|
||||
|
||||
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_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://cae6gor0iqjv4
|
||||
@@ -1,6 +1,7 @@
|
||||
# M03 — Renderer Facade and Safe Extraction
|
||||
|
||||
<!-- OPENWC_TARGET:ACTIVE -->
|
||||
<!-- OPENWC_TARGET:DONE -->
|
||||
<!-- OPENWC_TARGET_DONE:M03:3bf1c2c:2026-08-02 -->
|
||||
|
||||
## Outcome
|
||||
|
||||
@@ -12,9 +13,9 @@
|
||||
- [x] Запретить gameplay/editor доступ к внутренним очередям streamer.
|
||||
- [x] Извлечь pure `StreamingTargetPlanner`.
|
||||
- [x] Извлечь `RenderBudgetScheduler` с bounded queues/cancellation.
|
||||
- [ ] Затем извлекать terrain, M2, WMO и liquid services по одному.
|
||||
- [ ] Сохранить cache versioning и main-thread GPU finalization rules.
|
||||
- [ ] Добавить dependency и performance regression checks.
|
||||
- [x] Затем извлекать terrain, M2, WMO и liquid services по одному.
|
||||
- [x] Сохранить cache versioning и main-thread GPU finalization rules.
|
||||
- [x] Добавить dependency и performance regression checks.
|
||||
|
||||
## Fidelity evidence
|
||||
|
||||
@@ -27,7 +28,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-08-01
|
||||
- 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`, `5e82daa`, `1dc013e`, `99a90dd`, `4354834`, `a043c79`, `f79e064`, `d37c799`, `06f6394`, `34b7000`, `1acddab`, `6a0f9bd`, `d65ebee`, `705354d`, `57d0a9f`;
|
||||
- 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`, `5e82daa`, `1dc013e`, `99a90dd`, `4354834`, `a043c79`, `f79e064`, `d37c799`, `06f6394`, `34b7000`, `1acddab`, `6a0f9bd`, `d65ebee`, `705354d`, `57d0a9f`, `541279e`;
|
||||
packages `M03-RND-SCHEDULER-001`, `M03-RND-INTERNAL-ACCESS-GATE-001`,
|
||||
`M03-RND-FACADE-GROUND-QUERY-001`, `M03-RND-FACADE-ENVIRONMENT-001`,
|
||||
`M03-RND-FACADE-ENTITY-001`, `M03-RND-TERRAIN-CACHE-SERVICE-001`,
|
||||
@@ -47,6 +48,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-WMO-RUNTIME-MESH-FINALIZER-001`,
|
||||
`M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001`,
|
||||
`M03-RND-WMO-RUNTIME-SCENE-PREPARER-001`,
|
||||
`M03-RND-WMO-SCENE-INSTANCE-FACTORY-001`,
|
||||
`M03-RND-ADT-WATER-LOAD-PIPELINE-001`,
|
||||
`M03-RND-ADT-WATER-SCENE-FINALIZER-001`,
|
||||
`M03-RND-M2-RUNTIME-REBUILD-CLASSIFIER-001`,
|
||||
@@ -389,6 +391,19 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
passed post-merge preparer (`3.486ms`), Mesh finalizer (`0.199ms`), all nine
|
||||
adjacent WMO services, shutdown, materials, facade, internal-access `30`,
|
||||
manifest `7/7`, documentation `51` and coordination.
|
||||
WMO scene instance factory passed `cases=41 iterations=1000
|
||||
elapsed_ms=5.297`; the updated placement resolver dependency passed
|
||||
`cases=10 iterations=20000 elapsed_ms=28.740`. Cached validation-before-
|
||||
placement, live validator suppression, basename, exact Transform3D and
|
||||
descendant Mesh identity remained unchanged. Invalid non-Node3D cached roots
|
||||
are now freed synchronously, closing an error-path lifetime leak. The suite
|
||||
passed `65/66` with no unexpected failures and only the proprietary ADT probe
|
||||
unavailable; editor parse had zero script diagnostics, documentation covered
|
||||
`52` module specifications and checkpoint dry-run retained `7/7`. Merge
|
||||
`541279e` passed post-merge factory (`5.308ms`), placement resolver
|
||||
(`31.207ms`), runtime preparation/finalization, all adjacent WMO services,
|
||||
shutdown, materials, facade, internal-access `30`, manifest `7/7`,
|
||||
documentation `52` and coordination.
|
||||
M02 terrain-query regression
|
||||
remained green (13 pre-existing expired M00 claim warnings).
|
||||
- Fidelity comparison: all 16 historical operation limits and drain sites were
|
||||
@@ -549,6 +564,7 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
WMO runtime Mesh finalizer and four loader Mesh-delegation adapters,
|
||||
stateless WMO render group materializer and both loader node-attachment adapters,
|
||||
stateless WMO runtime scene preparer and cached/live loader preparation adapters,
|
||||
stateless WMO scene instance factory and cached/live loader creation adapters,
|
||||
ADT water pending/task/result pipeline state and loader enqueue/start/publish/
|
||||
drain/release/reset/shutdown/metrics adapters,
|
||||
stateless ADT water scene finalizer and both loader build/attach adapters,
|
||||
@@ -611,11 +627,46 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
cached-scene WMO FileAccess/size admission and live fallback remain in the
|
||||
loader; both WMO terminal I/O paths, runtime Mesh material refresh and
|
||||
lightweight group materialization plus cached/live subtree preparation are
|
||||
separated; recursive Mesh/shadow mutation and Node creation remain synchronous
|
||||
main-thread work behind existing orchestration/permits;
|
||||
separated; cached/live root creation and placement are also separated, while
|
||||
ResourceLoader/prototype construction remain loader-owned. Recursive Mesh/
|
||||
shadow mutation and Node creation remain synchronous main-thread work behind
|
||||
existing orchestration/permits;
|
||||
asset-backed WMO placement/portal/material/leak/p95/p99 evidence remains pending;
|
||||
ADT water parsing and active worker interruption remain loader-owned;
|
||||
materialization is separated but remains synchronous main-thread work behind
|
||||
the existing permit, and asset-backed MH2O/MCLQ traversal/leak/p95/p99/
|
||||
original-client evidence is pending; M03 still needs further terrain/M2/WMO/
|
||||
liquid service extraction and dependency/performance acceptance.
|
||||
|
||||
### Closeout evidence — 2026-08-02
|
||||
|
||||
- Exact accepted M00 commit `239cb24` and M03 were captured on the same RTX 5070,
|
||||
Godot 4.6.1, `High` preset and exact cache inventory: terrain `687`, WMO
|
||||
render/scene `368/368`, M2 baked/GLB/scene `2009/2009/2009`.
|
||||
- The repeated-sample protocol (five M00 reports and three M03 reports) and the
|
||||
independent ten-second measurement protocol compared all `14` cold/warm
|
||||
result pairs and `84` metrics with the unchanged 10% budgets. The stability
|
||||
gate passed with `repeatable_regressions=0`; five protocol-local exceedances
|
||||
did not reproduce across both sampling windows.
|
||||
- Native GryphonRoost preparation now allocates an empty instance-local
|
||||
`ArrayMesh`, shares captured Materials and performs one phased deformation
|
||||
rebuild. The diagnosed initial build hitch fell from `168.02ms` to no native
|
||||
copy hitch; dense-M2 maximum hitches fell from M00 `171–195ms` to M03
|
||||
`52–66ms` in the paired short-window series.
|
||||
- All `67/67` autonomous Godot verifier scripts passed. Renderer closeout
|
||||
contracts passed `workers=4 frame_steps=15 cache_versions=7 nested_glb=1`;
|
||||
documentation passed `module_specs=53 required_files=7`; coordination and
|
||||
`git diff --check` passed.
|
||||
- Asset-backed PNG/checkpoint evidence covers terrain overview, ADT boundary,
|
||||
dense M2, large WMO, liquid, native animation and dusk sky. This proves the
|
||||
M03 extraction did not introduce a repeatable performance regression; it does
|
||||
not claim pixel-level parity with the original WoW 3.3.5a client.
|
||||
- Superseding the historical pending statements above: the extraction,
|
||||
dependency, cache-version, main-thread finalization and performance criteria
|
||||
required by M03 are accepted. Remaining items are release-level risks:
|
||||
original-client visual approval, long-traversal descriptor pressure, GPU
|
||||
skinning for many native animators and interruption of already in-flight work.
|
||||
- Closeout files: `src/tools/verify_renderer_closeout_contracts.gd`,
|
||||
`tools/compare_render_performance.ps1`,
|
||||
`tools/verify_render_performance_stability.ps1`, renderer closeout/module
|
||||
documentation, nested M2 GLB bake fix and native animator regression coverage.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# M04 — Godot Editor Shell
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
<!-- OPENWC_TARGET:ACTIVE -->
|
||||
|
||||
## Outcome
|
||||
|
||||
|
||||
+3
-3
@@ -10,7 +10,7 @@
|
||||
|
||||
## Current target
|
||||
|
||||
`M03` — [03-renderer-facade.md](03-renderer-facade.md)
|
||||
`M04` — [04-editor-shell.md](04-editor-shell.md)
|
||||
|
||||
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
||||
|
||||
@@ -36,8 +36,8 @@
|
||||
| M00 | [Renderer baseline](00-render-baseline.md) | — | DONE |
|
||||
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | DONE |
|
||||
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | DONE |
|
||||
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | ACTIVE |
|
||||
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
|
||||
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | DONE |
|
||||
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | ACTIVE |
|
||||
| M05 | [Content Project](05-content-project.md) | M04 | OPEN |
|
||||
| M06 | [Server database adapters](06-server-adapters.md) | M05 | OPEN |
|
||||
| M07 | [World Editor vertical slice](07-world-editor-slice.md) | M06 | OPEN |
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$BaselineReport,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string[]]$CandidateReport,
|
||||
|
||||
[string]$OutputReport
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
$failures = [System.Collections.Generic.List[string]]::new()
|
||||
$comparisons = [System.Collections.Generic.List[object]]::new()
|
||||
|
||||
function Read-RenderReport {
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Path,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label report does not exist: $Path"
|
||||
}
|
||||
$report = Get-Content -Raw -Encoding UTF8 -LiteralPath $Path | ConvertFrom-Json
|
||||
if ($null -eq $report -or $null -eq $report.results) {
|
||||
throw "$Label report has no results array: $Path"
|
||||
}
|
||||
return $report
|
||||
}
|
||||
|
||||
function Convert-ToStableJson {
|
||||
param([object]$Value)
|
||||
return $Value | ConvertTo-Json -Depth 20 -Compress
|
||||
}
|
||||
|
||||
function Get-MedianValue {
|
||||
param([double[]]$Values)
|
||||
|
||||
$ordered = @($Values | Sort-Object)
|
||||
if ($ordered.Count -eq 0) {
|
||||
throw 'Cannot calculate a median from an empty value set'
|
||||
}
|
||||
$middle = [math]::Floor($ordered.Count / 2)
|
||||
if ($ordered.Count % 2 -eq 1) {
|
||||
return [double]$ordered[$middle]
|
||||
}
|
||||
return ([double]$ordered[$middle - 1] + [double]$ordered[$middle]) / 2.0
|
||||
}
|
||||
|
||||
function Assert-EqualValue {
|
||||
param(
|
||||
[string]$Name,
|
||||
[object]$BaselineValue,
|
||||
[object]$CandidateValue
|
||||
)
|
||||
|
||||
$baselineJson = Convert-ToStableJson $BaselineValue
|
||||
$candidateJson = Convert-ToStableJson $CandidateValue
|
||||
if ($baselineJson -cne $candidateJson) {
|
||||
$failures.Add("$Name differs: baseline=$baselineJson candidate=$candidateJson")
|
||||
}
|
||||
}
|
||||
|
||||
function Get-ResultKey {
|
||||
param([object]$Result)
|
||||
return "$($Result.name)|$($Result.pass)"
|
||||
}
|
||||
|
||||
function Assert-CompatibleCacheInventory {
|
||||
param(
|
||||
[object]$BaselineInventory,
|
||||
[object]$CandidateInventory
|
||||
)
|
||||
|
||||
$baselineNames = @($BaselineInventory.PSObject.Properties.Name | Sort-Object)
|
||||
$candidateNames = @($CandidateInventory.PSObject.Properties.Name | Sort-Object)
|
||||
Assert-EqualValue 'cache inventory key set' $baselineNames $candidateNames
|
||||
foreach ($cacheName in $baselineNames) {
|
||||
$baselineRecord = $BaselineInventory.$cacheName
|
||||
$candidateRecord = $CandidateInventory.$cacheName
|
||||
if ($null -eq $candidateRecord) {
|
||||
continue
|
||||
}
|
||||
Assert-EqualValue "cache inventory presence for $cacheName" `
|
||||
$baselineRecord.present `
|
||||
$candidateRecord.present
|
||||
Assert-EqualValue "cache inventory file count for $cacheName" `
|
||||
$baselineRecord.file_count `
|
||||
$candidateRecord.file_count
|
||||
if ([bool]$baselineRecord.present -and [int64]$candidateRecord.file_count -le 0) {
|
||||
$failures.Add("cache inventory is empty for $cacheName")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Build-ResultIndex {
|
||||
param(
|
||||
[object[]]$Results,
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
$index = @{}
|
||||
foreach ($result in $Results) {
|
||||
$key = Get-ResultKey $result
|
||||
if ($index.ContainsKey($key)) {
|
||||
$failures.Add("$Label report contains duplicate result: $key")
|
||||
continue
|
||||
}
|
||||
$index[$key] = $result
|
||||
}
|
||||
return $index
|
||||
}
|
||||
|
||||
function Merge-RenderReports {
|
||||
param(
|
||||
[object[]]$Reports,
|
||||
[string]$Label
|
||||
)
|
||||
|
||||
$merged = Convert-ToStableJson $Reports[0] | ConvertFrom-Json
|
||||
if ($Reports.Count -eq 1) {
|
||||
return $merged
|
||||
}
|
||||
$resultIndexes = @(
|
||||
foreach ($sample in $Reports) {
|
||||
Build-ResultIndex -Results @($sample.results) -Label "$Label sample"
|
||||
}
|
||||
)
|
||||
$referenceResultKeys = @($resultIndexes[0].Keys | Sort-Object)
|
||||
for ($sampleNumber = 1; $sampleNumber -lt $Reports.Count; $sampleNumber++) {
|
||||
$sample = $Reports[$sampleNumber]
|
||||
Assert-EqualValue "$Label[$sampleNumber] schema_version" $merged.schema_version $sample.schema_version
|
||||
Assert-EqualValue "$Label[$sampleNumber] profile" $merged.profile $sample.profile
|
||||
Assert-EqualValue "$Label[$sampleNumber] cache_state" $merged.cache_state $sample.cache_state
|
||||
Assert-EqualValue "$Label[$sampleNumber] viewport" $merged.environment.viewport $sample.environment.viewport
|
||||
Assert-EqualValue "$Label[$sampleNumber] Godot version" $merged.environment.godot_version.string $sample.environment.godot_version.string
|
||||
Assert-EqualValue "$Label[$sampleNumber] rendering driver" $merged.environment.rendering_driver $sample.environment.rendering_driver
|
||||
Assert-EqualValue "$Label[$sampleNumber] rendering method" $merged.environment.rendering_method $sample.environment.rendering_method
|
||||
Assert-EqualValue "$Label[$sampleNumber] video adapter" $merged.environment.video_adapter $sample.environment.video_adapter
|
||||
Assert-EqualValue "$Label[$sampleNumber] CPU" $merged.environment.cpu $sample.environment.cpu
|
||||
Assert-EqualValue "$Label[$sampleNumber] cache contract" $merged.cache_contract $sample.cache_contract
|
||||
Assert-EqualValue "$Label[$sampleNumber] cache inventory" $merged.cache_inventory $sample.cache_inventory
|
||||
Assert-EqualValue `
|
||||
"$Label[$sampleNumber] result key set" `
|
||||
$referenceResultKeys `
|
||||
@($resultIndexes[$sampleNumber].Keys | Sort-Object)
|
||||
}
|
||||
|
||||
$metricDefinitions = @(
|
||||
@('load_time_ms', 'root'),
|
||||
@('frame_ms_p95', 'metrics'),
|
||||
@('frame_ms_p99', 'metrics'),
|
||||
@('max_hitch_ms', 'metrics'),
|
||||
@('memory_static_bytes', 'metrics'),
|
||||
@('video_memory_bytes', 'metrics')
|
||||
)
|
||||
$mergedResultIndex = Build-ResultIndex -Results @($merged.results) -Label "Median $Label"
|
||||
foreach ($resultKey in $referenceResultKeys) {
|
||||
$mergedResult = $mergedResultIndex[$resultKey]
|
||||
foreach ($definition in $metricDefinitions) {
|
||||
$metricName = $definition[0]
|
||||
$location = $definition[1]
|
||||
$values = @(
|
||||
foreach ($sampleIndex in $resultIndexes) {
|
||||
if ($location -eq 'root') {
|
||||
[double]$sampleIndex[$resultKey].$metricName
|
||||
} else {
|
||||
[double]$sampleIndex[$resultKey].metrics.$metricName
|
||||
}
|
||||
}
|
||||
)
|
||||
$medianValue = Get-MedianValue -Values $values
|
||||
if ($location -eq 'root') {
|
||||
$mergedResult.$metricName = $medianValue
|
||||
} else {
|
||||
$mergedResult.metrics.$metricName = $medianValue
|
||||
}
|
||||
}
|
||||
}
|
||||
return $merged
|
||||
}
|
||||
|
||||
function Compare-Metric {
|
||||
param(
|
||||
[string]$ResultKey,
|
||||
[string]$MetricName,
|
||||
[double]$BaselineValue,
|
||||
[double]$CandidateValue,
|
||||
[double]$AllowedRegressionPercent
|
||||
)
|
||||
|
||||
$limit = if ($BaselineValue -eq 0.0) {
|
||||
0.0
|
||||
} else {
|
||||
$BaselineValue * (1.0 + $AllowedRegressionPercent / 100.0)
|
||||
}
|
||||
$regressionPercent = if ($BaselineValue -eq 0.0) {
|
||||
if ($CandidateValue -eq 0.0) { 0.0 } else { [double]::PositiveInfinity }
|
||||
} else {
|
||||
(($CandidateValue - $BaselineValue) / $BaselineValue) * 100.0
|
||||
}
|
||||
$passed = $CandidateValue -le $limit
|
||||
$comparisons.Add([pscustomobject]@{
|
||||
result = $ResultKey
|
||||
metric = $MetricName
|
||||
baseline = $BaselineValue
|
||||
candidate = $CandidateValue
|
||||
limit = $limit
|
||||
regression_percent = $regressionPercent
|
||||
passed = $passed
|
||||
})
|
||||
if (-not $passed) {
|
||||
$failures.Add(
|
||||
("{0} {1} regressed by {2:N3}%: baseline={3:N3} candidate={4:N3} limit={5:N3}" -f
|
||||
$ResultKey,
|
||||
$MetricName,
|
||||
$regressionPercent,
|
||||
$BaselineValue,
|
||||
$CandidateValue,
|
||||
$limit)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
$baselineReports = @(
|
||||
for ($baselineIndex = 0; $baselineIndex -lt $BaselineReport.Count; $baselineIndex++) {
|
||||
Read-RenderReport `
|
||||
-Path $BaselineReport[$baselineIndex] `
|
||||
-Label "Baseline[$baselineIndex]"
|
||||
}
|
||||
)
|
||||
$candidateReports = @(
|
||||
for ($candidateIndex = 0; $candidateIndex -lt $CandidateReport.Count; $candidateIndex++) {
|
||||
Read-RenderReport `
|
||||
-Path $CandidateReport[$candidateIndex] `
|
||||
-Label "Candidate[$candidateIndex]"
|
||||
}
|
||||
)
|
||||
$baseline = Merge-RenderReports -Reports $baselineReports -Label 'baseline'
|
||||
$candidate = Merge-RenderReports -Reports $candidateReports -Label 'candidate'
|
||||
|
||||
Assert-EqualValue 'schema_version' $baseline.schema_version $candidate.schema_version
|
||||
Assert-EqualValue 'profile' $baseline.profile $candidate.profile
|
||||
Assert-EqualValue 'cache_state' $baseline.cache_state $candidate.cache_state
|
||||
Assert-EqualValue 'viewport' $baseline.environment.viewport $candidate.environment.viewport
|
||||
Assert-EqualValue 'Godot version' $baseline.environment.godot_version.string $candidate.environment.godot_version.string
|
||||
Assert-EqualValue 'rendering driver' $baseline.environment.rendering_driver $candidate.environment.rendering_driver
|
||||
Assert-EqualValue 'rendering method' $baseline.environment.rendering_method $candidate.environment.rendering_method
|
||||
Assert-EqualValue 'video adapter' $baseline.environment.video_adapter $candidate.environment.video_adapter
|
||||
Assert-EqualValue 'CPU' $baseline.environment.cpu $candidate.environment.cpu
|
||||
Assert-EqualValue 'cache contract' $baseline.cache_contract $candidate.cache_contract
|
||||
Assert-CompatibleCacheInventory $baseline.cache_inventory $candidate.cache_inventory
|
||||
|
||||
$baselineIndex = Build-ResultIndex -Results @($baseline.results) -Label 'Baseline'
|
||||
$candidateIndex = Build-ResultIndex -Results @($candidate.results) -Label 'Candidate'
|
||||
Assert-EqualValue 'result key set' @($baselineIndex.Keys | Sort-Object) @($candidateIndex.Keys | Sort-Object)
|
||||
|
||||
$budgets = $baseline.comparison_budgets
|
||||
$metricDefinitions = @(
|
||||
@('load_time_ms', 'load_time_ms_max_regression_percent', 'root'),
|
||||
@('frame_ms_p95', 'frame_ms_p95_max_regression_percent', 'metrics'),
|
||||
@('frame_ms_p99', 'frame_ms_p99_max_regression_percent', 'metrics'),
|
||||
@('max_hitch_ms', 'max_hitch_ms_max_regression_percent', 'metrics'),
|
||||
@('memory_static_bytes', 'memory_bytes_max_regression_percent', 'metrics'),
|
||||
@('video_memory_bytes', 'memory_bytes_max_regression_percent', 'metrics')
|
||||
)
|
||||
|
||||
foreach ($key in @($baselineIndex.Keys | Sort-Object)) {
|
||||
if (-not $candidateIndex.ContainsKey($key)) {
|
||||
continue
|
||||
}
|
||||
$baselineResult = $baselineIndex[$key]
|
||||
$candidateResult = $candidateIndex[$key]
|
||||
foreach ($definition in $metricDefinitions) {
|
||||
$metricName = $definition[0]
|
||||
$budgetName = $definition[1]
|
||||
$location = $definition[2]
|
||||
$baselineValue = if ($location -eq 'root') {
|
||||
[double]$baselineResult.$metricName
|
||||
} else {
|
||||
[double]$baselineResult.metrics.$metricName
|
||||
}
|
||||
$candidateValue = if ($location -eq 'root') {
|
||||
[double]$candidateResult.$metricName
|
||||
} else {
|
||||
[double]$candidateResult.metrics.$metricName
|
||||
}
|
||||
Compare-Metric `
|
||||
-ResultKey $key `
|
||||
-MetricName $metricName `
|
||||
-BaselineValue $baselineValue `
|
||||
-CandidateValue $candidateValue `
|
||||
-AllowedRegressionPercent ([double]$budgets.$budgetName)
|
||||
}
|
||||
}
|
||||
|
||||
$summary = [pscustomobject]@{
|
||||
schema_version = 1
|
||||
baseline_revision = $baseline.revision
|
||||
candidate_revision = $candidate.revision
|
||||
baseline_created_utc = $baseline.created_utc
|
||||
candidate_created_utc = $candidate.created_utc
|
||||
baseline_samples = $baselineReports.Count
|
||||
baseline_reports = @($BaselineReport)
|
||||
candidate_samples = $candidateReports.Count
|
||||
candidate_reports = @($CandidateReport)
|
||||
result_pairs = $baselineIndex.Count
|
||||
metric_comparisons = $comparisons.Count
|
||||
passed = $failures.Count -eq 0
|
||||
failures = @($failures)
|
||||
comparisons = @($comparisons)
|
||||
}
|
||||
|
||||
if ($OutputReport) {
|
||||
$parent = Split-Path -Parent $OutputReport
|
||||
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
|
||||
New-Item -ItemType Directory -Path $parent | Out-Null
|
||||
}
|
||||
$summary | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 -LiteralPath $OutputReport
|
||||
}
|
||||
|
||||
if ($failures.Count -gt 0) {
|
||||
foreach ($failure in $failures) {
|
||||
Write-Error "RENDER_PERFORMANCE: $failure" -ErrorAction Continue
|
||||
}
|
||||
Write-Host "RENDER_PERFORMANCE FAIL result_pairs=$($baselineIndex.Count) comparisons=$($comparisons.Count) failures=$($failures.Count)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "RENDER_PERFORMANCE PASS result_pairs=$($baselineIndex.Count) comparisons=$($comparisons.Count) max_regression_percent=10"
|
||||
exit 0
|
||||
@@ -0,0 +1,106 @@
|
||||
[CmdletBinding()]
|
||||
param(
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$RepeatedSampleComparison,
|
||||
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$LongWindowComparison,
|
||||
|
||||
[string]$OutputReport
|
||||
)
|
||||
|
||||
$ErrorActionPreference = 'Stop'
|
||||
|
||||
function Read-ComparisonReport {
|
||||
param([string]$Path, [string]$Label)
|
||||
|
||||
if (-not (Test-Path -LiteralPath $Path -PathType Leaf)) {
|
||||
throw "$Label comparison does not exist: $Path"
|
||||
}
|
||||
$report = Get-Content -Raw -Encoding UTF8 -LiteralPath $Path | ConvertFrom-Json
|
||||
if ($null -eq $report -or $null -eq $report.comparisons) {
|
||||
throw "$Label comparison has no comparisons array: $Path"
|
||||
}
|
||||
return $report
|
||||
}
|
||||
|
||||
function Build-ComparisonIndex {
|
||||
param([object[]]$Comparisons, [string]$Label)
|
||||
|
||||
$index = @{}
|
||||
foreach ($comparison in $Comparisons) {
|
||||
$key = "$($comparison.result)|$($comparison.metric)"
|
||||
if ($index.ContainsKey($key)) {
|
||||
throw "$Label comparison contains duplicate metric: $key"
|
||||
}
|
||||
$index[$key] = $comparison
|
||||
}
|
||||
return $index
|
||||
}
|
||||
|
||||
$repeatedReport = Read-ComparisonReport `
|
||||
-Path $RepeatedSampleComparison `
|
||||
-Label 'Repeated-sample'
|
||||
$longWindowReport = Read-ComparisonReport `
|
||||
-Path $LongWindowComparison `
|
||||
-Label 'Long-window'
|
||||
$repeatedIndex = Build-ComparisonIndex `
|
||||
-Comparisons @($repeatedReport.comparisons) `
|
||||
-Label 'Repeated-sample'
|
||||
$longWindowIndex = Build-ComparisonIndex `
|
||||
-Comparisons @($longWindowReport.comparisons) `
|
||||
-Label 'Long-window'
|
||||
$repeatedKeys = @($repeatedIndex.Keys | Sort-Object)
|
||||
$longWindowKeys = @($longWindowIndex.Keys | Sort-Object)
|
||||
if (($repeatedKeys | ConvertTo-Json -Compress) -cne ($longWindowKeys | ConvertTo-Json -Compress)) {
|
||||
throw 'Comparison metric inventories differ between repeated and long-window protocols'
|
||||
}
|
||||
|
||||
$repeatableRegressions = [System.Collections.Generic.List[object]]::new()
|
||||
$nonRepeatableRegressions = [System.Collections.Generic.List[object]]::new()
|
||||
foreach ($key in $repeatedKeys) {
|
||||
$repeatedMetricComparison = $repeatedIndex[$key]
|
||||
$longWindowMetricComparison = $longWindowIndex[$key]
|
||||
$repeatedFailed = ([bool]$repeatedMetricComparison.passed) -eq $false
|
||||
$longWindowFailed = ([bool]$longWindowMetricComparison.passed) -eq $false
|
||||
if ($repeatedFailed -and $longWindowFailed) {
|
||||
$repeatableRegressions.Add([pscustomobject]@{
|
||||
key = $key
|
||||
repeated_sample = $repeatedMetricComparison
|
||||
long_window = $longWindowMetricComparison
|
||||
})
|
||||
} elseif ($repeatedFailed -or $longWindowFailed) {
|
||||
$nonRepeatableRegressions.Add([pscustomobject]@{
|
||||
key = $key
|
||||
failed_protocol = if ($repeatedFailed) { 'repeated_sample' } else { 'long_window' }
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
$summary = [pscustomobject]@{
|
||||
schema_version = 1
|
||||
repeated_sample_comparison = $RepeatedSampleComparison
|
||||
long_window_comparison = $LongWindowComparison
|
||||
metric_comparisons = $repeatedKeys.Count
|
||||
passed = $repeatableRegressions.Count -eq 0
|
||||
repeatable_regressions = @($repeatableRegressions)
|
||||
non_repeatable_regressions = @($nonRepeatableRegressions)
|
||||
}
|
||||
if ($OutputReport) {
|
||||
$parent = Split-Path -Parent $OutputReport
|
||||
if ($parent -and -not (Test-Path -LiteralPath $parent)) {
|
||||
New-Item -ItemType Directory -Path $parent | Out-Null
|
||||
}
|
||||
$summary | ConvertTo-Json -Depth 20 | Set-Content -Encoding UTF8 -LiteralPath $OutputReport
|
||||
}
|
||||
|
||||
if ($repeatableRegressions.Count -gt 0) {
|
||||
foreach ($regression in $repeatableRegressions) {
|
||||
Write-Error "RENDER_PERFORMANCE_STABILITY: repeatable regression $($regression.key)" -ErrorAction Continue
|
||||
}
|
||||
Write-Host "RENDER_PERFORMANCE_STABILITY FAIL repeated=$($repeatableRegressions.Count) metrics=$($repeatedKeys.Count)"
|
||||
exit 1
|
||||
}
|
||||
|
||||
Write-Host "RENDER_PERFORMANCE_STABILITY PASS metrics=$($repeatedKeys.Count) non_repeatable=$($nonRepeatableRegressions.Count)"
|
||||
exit 0
|
||||
Reference in New Issue
Block a user