Compare commits
38 Commits
8c54313f7d
..
master
| Author | SHA1 | Date | |
|---|---|---|---|
| 1fd87b3132 | |||
| bc4440562d | |||
| 5b73ddadc9 | |||
| a11eb50219 | |||
| adc441ffca | |||
| afbc4faab3 | |||
| f77f50c0f9 | |||
| 4369dd3c91 | |||
| fe6d0deab9 | |||
| 203d40bd0e | |||
| 3bf1c2c657 | |||
| d9d20cb53f | |||
| ef6d324b4f | |||
| f978a16806 | |||
| 541279ed45 | |||
| 8da41dc17c | |||
| 7e97b19095 | |||
| 07521ee6a4 | |||
| f8348cf0cb | |||
| 57d0a9f8bd | |||
| 73b30ca699 | |||
| ffed91c364 | |||
| cfa3dc1009 | |||
| 57d3330944 | |||
| 705354dc14 | |||
| 6deb80c0dd | |||
| f4d5e29cc9 | |||
| 543ee1572b | |||
| f35dcf3a09 | |||
| d65ebee57f | |||
| a59086fdf0 | |||
| ae40e3f1d4 | |||
| 49e70260ed | |||
| 406dc464c3 | |||
| 6a0f9bd3ba | |||
| 3525cc0ead | |||
| 7779e9e75f | |||
| 9ba7ca3263 |
+28
@@ -5,3 +5,31 @@
|
||||
path = third_party/godot-cpp
|
||||
url = https://github.com/godotengine/godot-cpp
|
||||
branch = 4.5
|
||||
[submodule "reference/open-realm"]
|
||||
path = reference/open-realm
|
||||
url = https://github.com/corepunch/open-realm.git
|
||||
branch = main
|
||||
[submodule "reference/whoa"]
|
||||
path = reference/whoa
|
||||
url = https://github.com/whoahq/whoa
|
||||
branch = master
|
||||
[submodule "reference/WoWee"]
|
||||
path = reference/WoWee
|
||||
url = https://github.com/Kelsidavis/WoWee.git
|
||||
branch = master
|
||||
[submodule "reference/WowUnreal"]
|
||||
path = reference/WowUnreal
|
||||
url = https://github.com/Clancey/WowUnreal
|
||||
branch = main
|
||||
[submodule "reference/wow.export"]
|
||||
path = reference/wow.export
|
||||
url = https://github.com/Kruithne/wow.export.git
|
||||
branch = main
|
||||
[submodule "reference/blender-wow-studio/pywowlib"]
|
||||
path = reference/blender-wow-studio-3.4-1.1.0_Experimental/io_scene_wmo/pywowlib
|
||||
url = https://github.com/wowdev/pywowlib.git
|
||||
branch = master
|
||||
[submodule "reference/benilla"]
|
||||
path = reference/benilla
|
||||
url = https://github.com/samwhosung/benilla.git
|
||||
branch = main
|
||||
|
||||
@@ -33,6 +33,7 @@ Paired run 2026-07-11 подтвердил крупный coordinate/placement g
|
||||
- `src/render/wmo/wmo_render_build_queue.gd` / `wmo_render_build_job.gd` - typed pending group jobs, FIFO placement keys and strong root/resource references without engine destruction.
|
||||
- `src/render/wmo/wmo_render_resource_cache_state.gd` - validated lightweight WMO render Resources, negative cache and pending cache paths without ResourceLoader I/O.
|
||||
- `src/render/wmo/wmo_render_resource_finalizer.gd` - lightweight WMO terminal polling, script/format validation and Resource/missing publication.
|
||||
- `src/render/wmo/wmo_scene_resource_finalizer.gd` - cached WMO terminal polling, PackedScene probe validation/lifetime and scene/missing publication.
|
||||
- `src/render/wmo/wmo_scene_resource_cache_state.gd` - validated cached-WMO PackedScenes, negative cache and pending `.tscn` paths without file/I/O/Node ownership.
|
||||
- `src/render/liquid/adt_water_load_pipeline_state.gd` - ADT water pending FIFO/dedupe, active task IDs and worker-safe parsed-result mailbox without parser or Node ownership.
|
||||
- `src/render/liquid/adt_water_scene_finalizer.gd` - stateless main-thread ADT water build/attach and optional persisted Editor ownership through the existing ADTBuilder.
|
||||
@@ -685,6 +686,42 @@ SKYBOX_MODEL ...
|
||||
- полноценный liquid rendering там не реализован;
|
||||
- skybox/liquid не стоит напрямую переносить как готовый код.
|
||||
|
||||
По WoWee (reviewed update `626243e937fb93965fa583a6507ed5a1aa7dda4b` →
|
||||
`607ea3b8369851014721416293f8e95dfbe64eec`, 2026-09-05):
|
||||
|
||||
- сильнейший M04 signal — не новый renderer целиком, а узкие regression fixtures:
|
||||
M2 global-sequence/color-alpha sampling, `$FSD` event timing, 8x8 liquid masks,
|
||||
rotated WMO liquid projection и CPU vertex/shader interface checks;
|
||||
- общий M2+skin/external-`.anim` resolver и selective animation loading полезны как
|
||||
decomposition, но должны использовать существующие OpenWC repository/worker/
|
||||
main-thread finalization boundaries;
|
||||
- track sampler пока линейно обрабатывает Hermite/Bezier, а particle/ribbon path
|
||||
содержит flame/smoke и orientation heuristics; это не fidelity oracle. Для
|
||||
effect architecture Benilla остаётся более полным secondary reference;
|
||||
- placement rotation order для наклонённых doodads в WoWee явно не решён.
|
||||
OpenWC не меняет calibrated MDDF/MODF transforms без tilted build-12340 fixture;
|
||||
- real-asset tests, которые превращают отсутствие assets в success, не считаются
|
||||
evidence. OpenWC fixture обязан иметь provenance/hash и явно fail/skip-report;
|
||||
- modified MIT license WoWee запрещает использование как основы/компонента
|
||||
commercial video game без разрешения: используем только независимо проверенные
|
||||
идеи и tests, не копируем/не вендорим код без legal review.
|
||||
|
||||
По Benilla (pinned research commit
|
||||
`bc1a2428dd7e00ca8abbfb8f0bf53750dae7b123`):
|
||||
|
||||
- полезна граница `world renderer -> asset/formats` без зависимости от game/UI;
|
||||
- M2 animation разделяет selection policy и renderer machinery: pose sampling,
|
||||
parent-order composition, global sequences, billboard replacement, attachment
|
||||
anchors и palette upload;
|
||||
- particles и ribbons симулируются на CPU для temporal fidelity, но записывают
|
||||
геометрию в один shared effect vertex/index stream с сортировкой, batching и
|
||||
camera-relative upload вместо отдельных dynamic Mesh/Material на emitter;
|
||||
- WMO portal flood, material pass ordering, fog/blend policy, effect lifecycle и
|
||||
corpus/probe tooling являются полезными sources для M04 fixtures;
|
||||
- это Vanilla 1.12.1/Bevy reference, не build-12340 oracle: record layouts,
|
||||
shader flags, effect timing и performance должны быть повторно проверены в
|
||||
Godot против оригинального WoW 3.3.5a.
|
||||
|
||||
По WoW 3.3.5a:
|
||||
|
||||
- старый клиент не рендерил все как modern physically based renderer;
|
||||
@@ -1349,8 +1386,9 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- `WmoSceneResourceCacheState` now owns validated cached-WMO PackedScenes,
|
||||
negative entries and normalized-path to pending-`.tscn` records.
|
||||
- `StreamingWorldLoader` still checks file existence and
|
||||
`wmo_max_runtime_scene_mb`, calls `ResourceLoader`, instantiates a validation
|
||||
probe, checks WMOBuilder cache metadata and frees the probe before adoption.
|
||||
`wmo_max_runtime_scene_mb` and starts requests. `WmoSceneResourceFinalizer`
|
||||
owns terminal ResourceLoader I/O, validation-probe instantiation, WMOBuilder
|
||||
metadata validation and probe release before adoption.
|
||||
- Missing files, oversize scenes, request errors, load failures and stale scenes
|
||||
retain their prior negative-cache and live-prototype fallback behavior.
|
||||
- Map reset clears pending/negative state while retaining accepted scenes; final
|
||||
@@ -1358,6 +1396,21 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- Asset-backed oversize/stale fixtures, traversal/leak p95/p99 and paired fidelity
|
||||
evidence remain pending.
|
||||
|
||||
## 2026-07-18 WMO Scene Resource Finalizer Extraction
|
||||
|
||||
- `WmoSceneResourceFinalizer` now owns cached WMO `.tscn` terminal status
|
||||
polling, loaded Resource/PackedScene validation, call-local probe lifetime and
|
||||
exact scene/missing publication.
|
||||
- Pending snapshots preserve Dictionary insertion order. Non-terminal requests
|
||||
remain pending; failed, null, wrong-type, wrong-root and stale outcomes keep
|
||||
the existing negative-cache transition.
|
||||
- Current scenes retain exact PackedScene identity. Accepted and rejected probes
|
||||
are freed before return; the rejected non-Node3D root now also releases its
|
||||
temporary Node, closing a leak without changing fallback or visible output.
|
||||
- Loader retains file/size admission, oversize log, request start, live fallback,
|
||||
placed Node materialization and shutdown order. Synthetic fixtures are not
|
||||
serialized private assets, long leak, p95/p99 or visual-fidelity evidence.
|
||||
|
||||
## 2026-07-17 ADT Water Load Pipeline State Extraction
|
||||
|
||||
- `AdtWaterLoadPipelineState` now owns ADT water pending FIFO/deduplication,
|
||||
@@ -1432,6 +1485,74 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
||||
- ADT parsing, quality tasks/results, tile state, cache format versions, material/
|
||||
Node/RID finalization, budgets and visible terrain behavior remain loader-owned.
|
||||
|
||||
## 2026-07-18 WMO Runtime Mesh Finalizer Extraction
|
||||
|
||||
- `WmoRuntimeMeshFinalizer` now owns cached WMO Mesh material refresh version
|
||||
`10`, in-place ArrayMesh surface iteration and WMOBuilder material-definition
|
||||
reconstruction.
|
||||
- `StreamingWorldLoader` retains cached-scene traversal, lightweight build-job
|
||||
traversal, Node/MultiMesh attachment, permits and lifetime, and delegates each
|
||||
borrowed Mesh to the service.
|
||||
- The historical metadata keys, compact texture0/texture1/texture2 ordering,
|
||||
flags/shader/blend values, cached shader colors and exact Mesh identity are
|
||||
unchanged. Null/unmarked surfaces and null builder results retain their prior
|
||||
Material.
|
||||
- Asset-free verification covers `27` identity/version/type/material/source
|
||||
cases and 1,000 current-Mesh calls. This is orchestration extraction, not new
|
||||
build-12340 material or visual parity evidence.
|
||||
|
||||
## 2026-08-01 WMO Render Group Materializer Extraction
|
||||
|
||||
- `WmoRenderGroupMaterializer` now owns creation and single attachment of the
|
||||
lightweight cached WMO `MeshInstance3D` and `MultiMeshInstance3D` groups.
|
||||
- Indexed names/transforms, `Group_N`/`DoodadGroup_N` fallbacks, exact
|
||||
Mesh/MultiMesh identity, shadow mode and positive visibility range/margin are
|
||||
unchanged.
|
||||
- `StreamingWorldLoader` retains build-step selection, runtime Mesh finalization,
|
||||
queue cursors, permits, optional Editor ownership and placement lifetime.
|
||||
- Asset-free verification covers `37` presentation/ownership/source cases and
|
||||
1,000 simple group attachments. This extraction adds no asset-backed GPU,
|
||||
leak, p95/p99 or original-client visual-fidelity evidence.
|
||||
|
||||
## 2026-08-01 WMO Runtime Scene Preparer Extraction
|
||||
|
||||
- `WmoRuntimeScenePreparer` now owns cached WMO parent-before-children Mesh/
|
||||
MultiMesh finalization and the shared cached/live render-policy preparation.
|
||||
- The historical path distinction is unchanged: live-built duplicates do not
|
||||
cross the cached runtime Mesh finalizer boundary.
|
||||
- Disabled occlusion still removes only the direct child named `Occluders`;
|
||||
enabled shadows still set descendant GeometryInstance3D nodes ON, while the
|
||||
disabled shadow branch preserves existing values.
|
||||
- Instantiation, placement, attachment, registry lifetime, Editor ownership,
|
||||
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.
|
||||
|
||||
Binary file not shown.
Binary file not shown.
@@ -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,88 @@
|
||||
# M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-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-render-group-materializer`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-render-group-materializer`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move one-step WMO MeshInstance3D and MultiMeshInstance3D creation, render
|
||||
settings and attachment from the loader into one main-thread materializer.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change WMO placement, resource loading, build-step planning or queue state.
|
||||
- Change runtime Mesh finalization, WMOBuilder material behavior or cache formats.
|
||||
- Change permits, visibility distances, shadow policy or visible output.
|
||||
- Generalize M2 and WMO materialization behind a shared abstraction.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_render_group_materializer.gd`,
|
||||
`src/tools/verify_wmo_render_group_materializer.gd`,
|
||||
`docs/modules/wmo-render-group-materializer.md`, this claim
|
||||
- Shared: loader, renderer module registry/specification, `RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Mesh and MultiMesh resources retain exact identity.
|
||||
- Names and optional transforms preserve existing indexed fallback behavior.
|
||||
- Shadow and positive visibility-range settings are applied unchanged.
|
||||
- The supplied WMO root becomes the sole SceneTree owner of the created node.
|
||||
- Editor persisted ownership remains an explicit loader composition policy.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO render build queue/planner and runtime Mesh finalizer
|
||||
- Blocks: remaining loader-owned WMO runtime traversal cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic Mesh/MultiMesh identity, naming, transform, rendering, attachment,
|
||||
invalid-input, ownership, source and bounded-timing contracts; adjacent WMO and
|
||||
renderer gates; full autonomous suite 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: `f4d5e29` (`render: extract WMO render group materializer`)
|
||||
- Results: materializer PASS `cases=37 iterations=1000 elapsed_ms=2.771`;
|
||||
autonomous headless suite `63/64`, with only the proprietary
|
||||
`verify_adt_m2_placements.gd` probe unavailable because `data/extracted` is
|
||||
absent; cold/editor parse completed without script diagnostics after restoring
|
||||
ignored generated/native worktree artifacts; checkpoint dry-run retained
|
||||
`7/7`; documentation passed with `50` module specifications; coordination
|
||||
passed with `77` historical expired-claim warnings.
|
||||
- Fidelity: exact Mesh/MultiMesh identity, indexed/fallback presentation,
|
||||
render settings, attachment and scheduler/queue boundaries are unchanged.
|
||||
No private-asset or original-client visual parity claim is added.
|
||||
- Remaining risks: synchronous main-thread Node creation remains; no private WMO
|
||||
visual comparison, long traversal, leak/GPU or p95/p99 evidence.
|
||||
- Documentation: new full module specification with API/I/O and data-flow,
|
||||
sequence/dependency diagrams; renderer registry/source map and `RENDER.md`
|
||||
updated.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:f4d5e29 -->
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001:705354d -->
|
||||
|
||||
- Merge: `705354d` (`merge: WMO render group materializer`)
|
||||
- Post-merge: materializer PASS `cases=37 iterations=1000 elapsed_ms=3.236`;
|
||||
all nine adjacent WMO services, shutdown, materials, facade, internal-access
|
||||
`30`, manifest `7/7`, documentation `50` and coordination passed.
|
||||
@@ -0,0 +1,78 @@
|
||||
# M03-RND-WMO-RUNTIME-MESH-FINALIZER-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-RUNTIME-MESH-FINALIZER-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex/m03-wmo-runtime-mesh-finalizer`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-runtime-mesh-finalizer`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move cached WMO runtime Mesh material refresh versioning, surface iteration and
|
||||
material-definition reconstruction from the loader into one service.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change WMO traversal, placement, build-job scheduling or Node materialization.
|
||||
- Change WMOBuilder shader/material behavior or cache formats.
|
||||
- Change texture path ordering, metadata names or visible output.
|
||||
- Generalize M2 and WMO finalization behind a shared abstraction.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_runtime_mesh_finalizer.gd`,
|
||||
`src/tools/verify_wmo_runtime_mesh_finalizer.gd`,
|
||||
`docs/modules/wmo-runtime-mesh-finalizer.md`, this claim
|
||||
- Shared: loader, WMO build/cache and world renderer specs/verifiers,
|
||||
`RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Null meshes remain null; accepted meshes retain exact Resource identity.
|
||||
- Refresh metadata version `10` remains the admission boundary.
|
||||
- Only stale `ArrayMesh` surfaces with cached WMO texture metadata are rebuilt.
|
||||
- Texture indices remain compact and ordered texture0, texture1, texture2.
|
||||
- WMO flags, shader, blend mode and cached colors are forwarded unchanged.
|
||||
- Scene traversal, builder material semantics and shutdown order are unchanged.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO render and scene Resource finalizers
|
||||
- Blocks: remaining loader-owned WMO runtime materialization cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic identity/version/type/surface/metadata/color/definition/source/timing
|
||||
contracts; adjacent WMO/renderer gates; full suite and checkpoint dry-run.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
|
||||
diagrams; adjacent renderer docs and M03 Evidence.
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: implementation, verification and documentation
|
||||
- Next: next unclaimed M03 renderer extraction package
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `ae40e3f` (`render: extract WMO runtime Mesh finalizer`)
|
||||
- Results: finalizer PASS `cases=27 iterations=1000 elapsed_ms=0.402` in the
|
||||
full run; autonomous headless suite `63/63`; documentation `49`; coordination
|
||||
passed with `34` historical warnings; checkpoint dry-run `7/7`.
|
||||
- Remaining risks: WMOBuilder surface mutation remains synchronous main-thread
|
||||
work; no private WMO corpus, visual comparison, leak/GPU or p95/p99 evidence.
|
||||
- Documentation updated: new full module specification with API/I/O and
|
||||
data-flow/state/sequence/dependency diagrams; renderer registry/source map,
|
||||
`RENDER.md` and M03 Evidence.
|
||||
- Merge: `d65ebee` (`merge: WMO runtime Mesh finalizer`)
|
||||
- Post-merge: finalizer PASS `cases=27 iterations=1000 elapsed_ms=0.202`;
|
||||
WMO scene/render cache/finalizer/queue/planner/registry/resolver, shutdown,
|
||||
materials, facade, internal-access `30`, manifest and checkpoint dry-run
|
||||
`7/7`, documentation `49` and coordination gates passed.
|
||||
@@ -0,0 +1,93 @@
|
||||
# M03-RND-WMO-RUNTIME-SCENE-PREPARER-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-RUNTIME-SCENE-PREPARER-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-runtime-scene-preparer`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-runtime-scene-preparer`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move cached/live WMO subtree preparation—recursive Mesh finalization, direct
|
||||
Occluders-child policy and recursive shadow enabling—from the loader into one
|
||||
stateless main-thread service.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change scene/prototype loading, validation, placement or attachment.
|
||||
- Change WMO material refresh rules, cache versions or builder behavior.
|
||||
- Change occlusion/shadow defaults or add visibility/portal behavior.
|
||||
- Change queue, scheduler, ownership or shutdown lifecycle.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_runtime_scene_preparer.gd`,
|
||||
`src/tools/verify_wmo_runtime_scene_preparer.gd`,
|
||||
`docs/modules/wmo-runtime-scene-preparer.md`, this claim
|
||||
- Shared: loader, renderer module registry/specification, `RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Cached preparation finalizes every MeshInstance3D Mesh and non-null
|
||||
MultiMeshInstance3D Mesh in depth-first traversal order.
|
||||
- Live preparation does not cross the runtime Mesh finalizer boundary.
|
||||
- Disabled occlusion removes only the direct child named `Occluders` and queues
|
||||
it for deletion; enabled occlusion retains it.
|
||||
- Enabled shadows recursively set every GeometryInstance3D to ON; disabled
|
||||
shadows preserve existing per-node values.
|
||||
- The service borrows the subtree and retains no Node or Resource.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO runtime Mesh finalizer
|
||||
- Blocks: remaining loader-owned WMO scene/prototype orchestration cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic cached/live traversal, exact resource/order, occluder, shadow,
|
||||
invalid-input, ownership, source and bounded-timing contracts; adjacent WMO,
|
||||
renderer, documentation and coordination gates; 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: `ffed91c` (`render: extract WMO runtime scene preparer`)
|
||||
- Results: preparer PASS `cases=31 iterations=1000 elapsed_ms=3.669`;
|
||||
updated Mesh-finalizer boundary PASS `cases=28 iterations=1000
|
||||
elapsed_ms=0.204`; headless suite `64/65` with no unexpected failures and
|
||||
only the proprietary ADT placement probe unavailable without `data/extracted`;
|
||||
editor parse had zero script diagnostics; checkpoint dry-run retained `7/7`;
|
||||
documentation passed with `51` module specifications; coordination passed
|
||||
with `77` historical expired-claim warnings.
|
||||
- Fidelity: cached/live preparation distinction, exact Mesh traversal order,
|
||||
direct `Occluders` lookup/removal and enabled/preserved shadow semantics are
|
||||
unchanged. No private-asset or original-client parity claim is added.
|
||||
- Remaining risks: recursive Resource/SceneTree mutation remains synchronous;
|
||||
no asset-backed WMO portal/room, visual, leak/GPU or p95/p99 evidence.
|
||||
- Documentation: new full module specification with API/I/O and data-flow,
|
||||
sequence/dependency diagrams; Mesh-finalizer ownership spec, renderer registry,
|
||||
world-renderer source map and `RENDER.md` updated.
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M03-RND-WMO-RUNTIME-SCENE-PREPARER-001:ffed91c -->
|
||||
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M03-RND-WMO-RUNTIME-SCENE-PREPARER-001:57d0a9f -->
|
||||
|
||||
- Merge: `57d0a9f` (`merge: WMO runtime scene preparer`)
|
||||
- Post-merge: preparer PASS `cases=31 iterations=1000 elapsed_ms=3.486`;
|
||||
Mesh finalizer `cases=28`/`0.199ms`; all nine adjacent WMO services, shutdown,
|
||||
materials, facade, internal-access `30`, manifest `7/7`, documentation `51`
|
||||
and coordination passed.
|
||||
@@ -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.
|
||||
@@ -0,0 +1,80 @@
|
||||
# M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001:sindo-main-codex:2026-07-20 -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex`
|
||||
- Target: M03 Renderer Facade and Safe Extraction
|
||||
- Branch: `work/sindo-main-codex/m03-wmo-scene-resource-finalizer`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m03-wmo-scene-resource-finalizer`
|
||||
|
||||
## Outcome
|
||||
|
||||
Move cached WMO PackedScene terminal ResourceLoader polling, probe validation,
|
||||
probe lifetime and scene/missing publication from the loader into one service.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Change `.tscn` path selection, file-size admission or request start behavior.
|
||||
- Own live fallback, placement/build jobs, attached Nodes or scheduler permits.
|
||||
- Change WMOBuilder cache metadata/version rules or visible output.
|
||||
- Merge PackedScene and lightweight render Resource finalization generically.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `src/render/wmo/wmo_scene_resource_finalizer.gd`,
|
||||
`src/tools/verify_wmo_scene_resource_finalizer.gd`,
|
||||
`docs/modules/wmo-scene-resource-finalizer.md`, this claim
|
||||
- Shared: loader, WMO scene cache state spec/verifier, world renderer registry/spec,
|
||||
`RENDER.md`, M03 Evidence
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- Pending requests are polled in detached Dictionary insertion order.
|
||||
- Only LOADED/FAILED statuses complete requests.
|
||||
- Failed, null, non-PackedScene, non-Node3D and stale scenes publish missing.
|
||||
- Accepted PackedScenes retain exact Resource identity.
|
||||
- Validation instantiates once and releases accepted/rejected Node3D probes once.
|
||||
- Admission, oversize logging, live fallback and shutdown order are unchanged.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: accepted WMO scene Resource cache state
|
||||
- Blocks: remaining loader-owned WMO live fallback/materialization cleanup
|
||||
|
||||
## Verification
|
||||
|
||||
- Synthetic status/order/type/probe/validation/lifetime/adoption/source/timing
|
||||
contracts; adjacent WMO/renderer gates; full suite and checkpoint dry-run.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Inline API docs; module API/I/O/ownership; data-flow/state/sequence/dependency
|
||||
diagrams; adjacent renderer docs and M03 Evidence.
|
||||
|
||||
## Status
|
||||
|
||||
- State: accepted
|
||||
- Done: implementation, verification and documentation
|
||||
- Next: next unclaimed M03 renderer extraction package
|
||||
- Blocked by:
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `7779e9e` (`render: extract WMO scene resource finalizer`)
|
||||
- Results: finalizer PASS `cases=26 iterations=1000 elapsed_ms=4.186`;
|
||||
adjacent WMO scene/render cache/finalizer/queue/planner/registry/resolver and
|
||||
shutdown checks passed; autonomous headless suite `61/61`; documentation `48`;
|
||||
coordination passed with `34` historical warnings; checkpoint dry-run `7/7`.
|
||||
- Remaining risks: PackedScene probe validation remains synchronous main-thread
|
||||
work; no serialized private WMO stale/oversize, long leak, visual,
|
||||
descriptor-pressure or p95/p99 evidence was added.
|
||||
- Documentation updated: new full module specification with API/I/O and
|
||||
data-flow/state/sequence/dependency diagrams; adjacent WMO cache/world-renderer
|
||||
specs, module registry, `RENDER.md` and M03 Evidence.
|
||||
- Merge: `6a0f9bd` (`merge: WMO scene resource finalizer`)
|
||||
- Post-merge: finalizer PASS `cases=26 iterations=1000 elapsed_ms=3.970`;
|
||||
WMO scene/render cache/finalizer/queue/planner/registry/resolver/shutdown,
|
||||
facade, internal-access `30`, manifest and checkpoint dry-run `7/7`,
|
||||
documentation `48` and coordination gates passed.
|
||||
@@ -0,0 +1,95 @@
|
||||
# M04-RND-FIDELITY-ROADMAP-001
|
||||
|
||||
<!-- OPENWC_CLAIM:M04-RND-FIDELITY-ROADMAP-001:sindo-main-codex-renderer-roadmap:2026-08-04 -->
|
||||
<!-- OPENWC_INTEGRATION:ACCEPTED:M04-RND-FIDELITY-ROADMAP-001:afbc4fa -->
|
||||
|
||||
## Owner
|
||||
|
||||
- Agent ID: `sindo-main-codex-renderer-roadmap`
|
||||
- Target: M04 Renderer Fidelity and Graphics Foundation
|
||||
- Branch: `work/sindo-main-codex-renderer-roadmap/m04-renderer-fidelity-plan`
|
||||
- Worktree: `C:\Users\sindo\open-wc-worktrees\m04-renderer-fidelity-plan`
|
||||
|
||||
## Outcome
|
||||
|
||||
Insert an evidence-driven renderer-fidelity milestone immediately after M03 and
|
||||
shift the existing M04–M13 executable targets to M05–M14 without losing their
|
||||
content, dependencies or status semantics.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Implement renderer behavior or claim visual parity in this package.
|
||||
- Capture or commit proprietary original-client screenshots.
|
||||
- Begin the shifted Editor, content, network or gameplay milestones.
|
||||
|
||||
## Paths
|
||||
|
||||
- Exclusive: `targets/04-renderer-fidelity.md`, this claim
|
||||
- Shared/hotspot: all numbered target filenames and cross-target dependencies,
|
||||
`targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, `targets/roadmap/`,
|
||||
`docs/ROADMAP.md`, renderer/testing/feature documentation where milestone IDs
|
||||
or fidelity gates are normative
|
||||
|
||||
## Contracts and data
|
||||
|
||||
- M03 remains `DONE`; new M04 becomes the only `ACTIVE` target.
|
||||
- Existing executable targets shift monotonically: old M04–M13 become M05–M14.
|
||||
- The new renderer gate uses original build-12340 captures as the authoritative
|
||||
oracle and treats Noggit as a secondary composition/reference tool.
|
||||
- Proprietary images remain outside Git; manifests, metadata, hashes, metrics and
|
||||
human approval records are repository evidence.
|
||||
- `Blizzlike335` and opt-in enhanced/racing graphics remain explicit profiles.
|
||||
|
||||
## Dependencies
|
||||
|
||||
- Requires: completed M03 renderer facade and closeout contracts.
|
||||
- Blocks: renderer-fidelity implementation and all shifted milestones.
|
||||
|
||||
## Verification
|
||||
|
||||
- Coordination and documentation gates; target marker/index consistency; link
|
||||
and milestone-reference audit; `git diff --check`.
|
||||
|
||||
## Fidelity evidence
|
||||
|
||||
- Planning evidence only: exact local client build `3.3.5.12340`, seven existing
|
||||
paired checkpoints and the user-proposed two-viewpoint-per-location corpus.
|
||||
No new parity claim is made.
|
||||
|
||||
## Documentation deliverables
|
||||
|
||||
- Executable M04 target with reference, static, temporal, subsystem, performance
|
||||
and acceptance gates; shifted target dependencies; development/subsystem
|
||||
roadmap and testing/feature-map alignment.
|
||||
|
||||
## Status
|
||||
|
||||
- State: integrated
|
||||
- Done: inserted M04 renderer fidelity, shifted old M04–M13 to M05–M14,
|
||||
updated dependencies, normative roadmaps, profile boundaries and test policy
|
||||
- Next: begin the M04 reference manifest/capture contract work package when the
|
||||
user supplies or approves the original-client CSV/capture corpus
|
||||
- Blocked by:
|
||||
|
||||
<!-- OPENWC_HANDOFF:READY:M04-RND-FIDELITY-ROADMAP-001:f77f50c -->
|
||||
|
||||
## Handoff
|
||||
|
||||
- Commit: `f77f50c` on
|
||||
`work/sindo-main-codex-renderer-roadmap/m04-renderer-fidelity-plan`.
|
||||
- Outcome: M03 remains `DONE`; new M04 is the only `ACTIVE` target; Editor,
|
||||
content, server, world editor, network, gameplay, playable client, quest,
|
||||
completeness and dungeon targets moved intact to M05–M14.
|
||||
- Contracts: original build-12340 captures are authoritative; Noggit is a
|
||||
secondary composition reference; proprietary pixels remain outside Git;
|
||||
Blizzlike/Enhanced/Racing graphics profiles are explicit boundaries.
|
||||
- Verification: coordination passed `targets=15 active=1`; documentation passed
|
||||
`module_specs=53 required_files=7`; target sequence passed `M00–M14`; old
|
||||
executable-target links `0`; `git diff --check` passed.
|
||||
- Fidelity: planning and corpus provenance only; no renderer implementation or
|
||||
new parity claim is included.
|
||||
- Documentation: executable M04, target index/dependency graph, renderer
|
||||
subsystem plan, high-level roadmap, architecture, testing/tooling/coding/
|
||||
documentation policies and affected future milestone references updated.
|
||||
- Cache/migration: no runtime/cache/data format changes. The exact CSV schema is
|
||||
intentionally deferred to the first M04 contract work package.
|
||||
@@ -9,6 +9,12 @@ OpenWC состоит из двух продуктов на общей плат
|
||||
|
||||
Оба продукта используют общие канонические модели, импорт данных и renderer preview, но не разделяют UI, lifecycle и права на изменение данных.
|
||||
|
||||
Планируемый racing fork является третьим потребителем graphics foundation. Он
|
||||
может использовать world streaming, materials, lighting, shadows, liquids,
|
||||
characters, animation и effects contracts, но не зависит от WoW network,
|
||||
gameplay state или proprietary asset repository. Его улучшения выбираются
|
||||
отдельным `Racing` graphics profile и не меняют `Blizzlike335`.
|
||||
|
||||
## Архитектурные принципы
|
||||
|
||||
1. Сервер авторитетен для боя, ресурсов, инвентаря, квестового прогресса и общего мира.
|
||||
@@ -57,6 +63,11 @@ TrinityCore/AzerothCore ◄── Network Adapter ◄── Runtime Client
|
||||
|
||||
Renderer получает `StreamingFocus`, `WorldVisualSnapshot` и presentation-команды. Он НЕ ДОЛЖЕН читать packets, SQL или gameplay input.
|
||||
|
||||
`GraphicsProfile` выбирается на composition boundary и задаёт material,
|
||||
lighting, shadow, liquid, effects, distance и post-processing capabilities.
|
||||
Внутренние shader/services не смешивают Blizzlike/Enhanced/Racing policy через
|
||||
неявные глобальные switches.
|
||||
|
||||
### UI
|
||||
|
||||
Владеет login/realm/character screens, HUD и FrameXML/Lua-compatible presentation. UI читает immutable view models и отправляет intents. Lua API получает capability-based facade; прямой доступ к network, filesystem и editor API запрещён.
|
||||
@@ -157,6 +168,7 @@ addons/
|
||||
- `ServerSchemaAdapter` — inspect, import, diff, generate, validate.
|
||||
- `ContentTypeDescriptor` — schema, inspector, validator, compiler.
|
||||
- `WorldRenderer` — streaming focus и entity presentation.
|
||||
- `GraphicsProfile` — explicit Blizzlike/Enhanced/Racing visual capabilities.
|
||||
- `GameplaySystem` — commands/events без scene dependency.
|
||||
- `EditorTool` — selection, command creation и gizmo, без прямой записи.
|
||||
- `TestFixtureProvider` — обезличенные packets, DB snapshots и content fixtures.
|
||||
|
||||
@@ -197,7 +197,7 @@ func request_streaming_tile_load(request: StreamingTileLoadRequest) -> void:
|
||||
- Предпочитать именованные predicates длинным boolean expressions.
|
||||
- Branch по capability/profile должен быть локальным и типизированным.
|
||||
- Не распространять `if core == "azerothcore"` по проекту; использовать adapter/capability.
|
||||
- Не смешивать Blizzlike и Enhanced branches в каждом shader/service: выбирать profile/strategy на boundary.
|
||||
- Не смешивать Blizzlike, Enhanced и Racing branches в каждом shader/service: выбирать profile/strategy на boundary.
|
||||
- Pattern/table-driven mapping предпочтительнее сотен одинаковых `if`, если таблица остаётся читаемой и валидируемой.
|
||||
|
||||
## Comments
|
||||
|
||||
@@ -181,7 +181,7 @@ stateDiagram-v2
|
||||
- Uniform/global parameter: coordinate/color space, range, units и producer.
|
||||
- Material profile: supported WoW shader/blend modes и approximations.
|
||||
- Expensive branch/texture dependency имеет cost/fallback note.
|
||||
- Blizzlike и Enhanced behavior документируются отдельно.
|
||||
- Blizzlike, Enhanced и Racing behavior документируются отдельно.
|
||||
|
||||
### Network codecs
|
||||
|
||||
|
||||
@@ -61,7 +61,7 @@ Gameplay state, network session, renderer world, editor session, caches конк
|
||||
- Visibility ranges/HLOD, occlusion и automatic mesh LOD использовать совместно по профилю.
|
||||
- Unique materials/textures минимизировать; descriptor/resource counts являются budget metric.
|
||||
- RenderingServer RIDs имеют явного владельца и освобождаются в deterministic shutdown test.
|
||||
- Shader/material profiles разделяют Blizzlike и Enhanced; runtime не компилирует тяжёлые варианты при пересечении ADT boundary.
|
||||
- Shader/material profiles разделяют Blizzlike, Enhanced и Racing; runtime не компилирует тяжёлые варианты при пересечении ADT boundary.
|
||||
|
||||
## EditorPlugin lifecycle
|
||||
|
||||
|
||||
+15
-1
@@ -2,6 +2,11 @@
|
||||
|
||||
Reference-код используется для исследования форматов, поведения и архитектурных вариантов. Он не определяет API OpenWC и не копируется без проверки лицензии, корректности и соответствия Godot.
|
||||
|
||||
Актуальные branches и pinned commits локальных Git-референсов зафиксированы в
|
||||
[`reference/README.md`](../reference/README.md#git-reference-revisions). Gitlinks
|
||||
точно фиксируют ревизию, а `.gitmodules` задаёт canonical remote и ветку
|
||||
для контролируемого обновления.
|
||||
|
||||
## Основные источники
|
||||
|
||||
### OpenWC renderer
|
||||
@@ -17,12 +22,13 @@ Reference-код используется для исследования фор
|
||||
|
||||
### WoWee
|
||||
|
||||
- Исследование обновлено до `master` commit `607ea3b8369851014721416293f8e95dfbe64eec` (2026-09-05), относительно прежнего reviewed pin `8456c236b57140e98667d6d8188f5cd1cc226daf`.
|
||||
- `reference/WoWee/docs/architecture.md` — разделение renderer/network/game/UI/pipeline.
|
||||
- `reference/WoWee/tools/editor/FORMAT_SPEC.md` — open formats, coordinates, collision, packaging и SQL export.
|
||||
- `reference/WoWee/TESTING.md` — единая точка запуска тестов, fixtures, sanitizers и CI discipline.
|
||||
- `reference/WoWee/EXPANSION_GUIDE.md` — protocol/data profile separation.
|
||||
|
||||
Используем: детерминированные authoring formats, headless parity, adapter profiles и validation-first pipeline. Не принимаем автоматически конкретные форматы или заявленную полноту реализации.
|
||||
Особенно полезны: WotLK M2 track/footstep fixtures, liquid mask/surface-grid tests, shader-interface checks, retained widget tree, единый XML→Lua `CreateFrame` path, поэлементный takeover default UI и большая система headless/static FrameXML/Lua audits. Не переносим монолитный Lua/game binding, unknown-API fallback, Vulkan-specific ownership или native-padding `.w*` formats. Particle/render rules, placement rotations и UI semantics требуют независимых build-12340 fixtures; часть real-asset tests WoWee пропускает отсутствие assets как success. Лицензия содержит дополнительный запрет commercial-game use, поэтому WoWee остаётся research-only reference без копирования или вендоринга кода. Полная evaluation card: [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md#wowee--карточка-референса).
|
||||
|
||||
### Noggit Red
|
||||
|
||||
@@ -51,6 +57,14 @@ Reference-код используется для исследования фор
|
||||
|
||||
Не переносим напрямую старый browser stack, WebSocket proxy, React/Three.js abstractions или pipeline server. Заявленное поведение проверяем по TrinityCore/AzerothCore и оригинальному клиенту; proof-of-concept не является спецификацией полноты.
|
||||
|
||||
### Benilla
|
||||
|
||||
- [`reference/benilla`](../reference/benilla) ([upstream](https://github.com/samwhosung/benilla)) — pinned research submodule с независимым клиентом WoW 1.12.1 build 5875 на Rust/Bevy; исследование OpenWC зафиксировано на commit `bc1a2428dd7e00ca8abbfb8f0bf53750dae7b123`.
|
||||
- Renderer references: M2 pose/palette animation, global sequences, material/pass ordering, общий dynamic-effect stream, particles, model particles, ribbons, WMO portal visibility, lighting, sky/weather и streaming.
|
||||
- UI references: отдельный engine-free TOC/FrameXML/Lua core, template/layout/widget model, deterministic event/`OnUpdate` ordering, legacy+modern handler arguments, sandbox, SavedVariables и большой набор compatibility tests.
|
||||
|
||||
Используем decomposition, algorithms, diagnostics и test ideas. Не переносим Bevy/ECS architecture и не считаем Vanilla behavior доказательством WotLK: M2/DBC/layout/API/security differences повторно проверяются по WoW 3.3.5a build 12340. Benilla пока не является oracle для third-party addons или secure/taint semantics; его Lua 5.1 runtime содержит compatibility work для Lua 5.0 target. Полная evaluation card находится в [`TOOLING_CATALOG.md`](TOOLING_CATALOG.md#benilla--карточка-референса).
|
||||
|
||||
### recast-rs
|
||||
|
||||
- [`wowemulation-dev/recast-rs`](https://github.com/wowemulation-dev/recast-rs) — Rust-порт Recast/Detour: navmesh generation, tiled pathfinding, spatial queries, crowd simulation и dynamic obstacles.
|
||||
|
||||
+27
-14
@@ -18,7 +18,20 @@
|
||||
|
||||
Готово, когда render sandbox сохраняет текущее качество, а domain тестируется без scene tree.
|
||||
|
||||
## M1 — Content Project и Editor shell
|
||||
## M1 — Renderer fidelity и graphics foundation
|
||||
|
||||
- Original-client build-12340 corpus: две базовые позиции выбранной локации,
|
||||
settings/time/weather/camera metadata, static и temporal evidence.
|
||||
- Точные color, terrain, M2/WMO material, sky/fog/weather и shadow rules.
|
||||
- MH2O/MCLQ/MLIQ liquids, GPU animation, characters, particles и ribbons.
|
||||
- Раздельные `Blizzlike335` и opt-in `Enhanced/Racing` graphics profiles.
|
||||
- Paired semantic-region comparison, human approval и traversal/GPU budgets.
|
||||
|
||||
Готово, когда утверждённая capture matrix не содержит неклассифицированных
|
||||
визуально различимых gaps, а общие graphics contracts пригодны клиенту, Editor
|
||||
preview и racing fork.
|
||||
|
||||
## M2 — Content Project и Editor shell
|
||||
|
||||
- `addons/openwc_editor` с workspace и docks.
|
||||
- Content Project schema, stable IDs, save/load/migration.
|
||||
@@ -28,7 +41,7 @@
|
||||
|
||||
Готово, когда небольшой synthetic project можно создать, изменить, undo, перезапустить Editor и получить идентичное состояние.
|
||||
|
||||
## M2 — Server inspector и adapters
|
||||
## M3 — Server inspector и adapters
|
||||
|
||||
- TrinityCore/AzerothCore connection profiles.
|
||||
- Schema detection и capabilities.
|
||||
@@ -38,7 +51,7 @@
|
||||
|
||||
Готово, когда одна сущность round-trip проходит оба поддерживаемых adapter profile без молчаливой потери данных.
|
||||
|
||||
## M3 — World Editor MVP
|
||||
## M4 — World Editor MVP
|
||||
|
||||
- Map viewport и coordinate overlays.
|
||||
- Server spawn visualization.
|
||||
@@ -48,7 +61,7 @@
|
||||
|
||||
Готово, когда NPC размещается в Editor и появляется на тестовом core в ожидаемой позиции.
|
||||
|
||||
## M4 — Quest vertical slice
|
||||
## M5 — Quest vertical slice
|
||||
|
||||
- Quest form и chain graph.
|
||||
- Kill/collect/explore objectives, giver/ender, rewards и localization.
|
||||
@@ -57,7 +70,7 @@
|
||||
|
||||
Готово, когда созданный в Editor квест полностью проходится клиентом.
|
||||
|
||||
## M5 — Playable network client
|
||||
## M6 — Playable network client
|
||||
|
||||
- Auth, realm, character selection и world session.
|
||||
- Entity/update fields и world spawn.
|
||||
@@ -66,7 +79,7 @@
|
||||
|
||||
Готово, когда клиент стабильно входит в мир, перемещается и видит синхронизированные entities.
|
||||
|
||||
## M6 — Core gameplay
|
||||
## M7 — Core gameplay
|
||||
|
||||
- Combat/spells/auras/death.
|
||||
- Inventory/equipment/loot/vendors.
|
||||
@@ -76,7 +89,7 @@
|
||||
|
||||
Готово, когда базовый leveling loop проходит без внешнего клиента.
|
||||
|
||||
## M7 — Dungeon authoring
|
||||
## M8 — Dungeon authoring
|
||||
|
||||
- DungeonPackage, encounters, triggers, doors и spawn groups.
|
||||
- SmartAI/script skeleton generation.
|
||||
@@ -85,7 +98,7 @@
|
||||
|
||||
Готово, когда custom dungeon собирается, разворачивается и проходится группой на test core.
|
||||
|
||||
## M8 — Compatibility и completeness
|
||||
## M9 — Compatibility и completeness
|
||||
|
||||
- Feature matrix WoW 3.3.5a.
|
||||
- Addon compatibility tiers.
|
||||
@@ -98,17 +111,17 @@
|
||||
При равной ценности порядок такой:
|
||||
|
||||
1. безопасность данных и воспроизводимость;
|
||||
2. корректность протокола и authoritative state;
|
||||
3. пользовательский vertical slice;
|
||||
4. diagnostics и testability;
|
||||
5. frame pacing;
|
||||
6. визуальная точность и polish.
|
||||
2. original-client renderer fidelity и frame pacing текущего M04;
|
||||
3. корректность протокола и authoritative state;
|
||||
4. пользовательский vertical slice;
|
||||
5. diagnostics и testability;
|
||||
6. opt-in визуальные улучшения после Blizzlike evidence.
|
||||
|
||||
## Не делать раньше времени
|
||||
|
||||
- прямую запись в production DB;
|
||||
- универсальный visual scripting для любой C++ механики;
|
||||
- массовую реализацию Lua API без работающего UI slice;
|
||||
- большой rewrite существующего renderer;
|
||||
- полный custom renderer до bounded shader/backend spike и profiler evidence;
|
||||
- multi-expansion abstraction до устойчивого профиля 3.3.5a;
|
||||
- proprietary asset packaging в репозитории.
|
||||
|
||||
@@ -44,8 +44,8 @@ ID записывается в claim, ветке, PR/MR и handoff. Нельзя
|
||||
```text
|
||||
M01-FND-COORDS-001
|
||||
M03-RND-SCHEDULER-001
|
||||
M08-NET-SRP-001
|
||||
M12-UIA-LUA-SPIKE-001
|
||||
M09-NET-SRP-001
|
||||
M13-UIA-LUA-SPIKE-001
|
||||
```
|
||||
|
||||
Program codes определены в [`../targets/DEVELOPMENT_ROADMAP.md`](../targets/DEVELOPMENT_ROADMAP.md).
|
||||
@@ -214,7 +214,7 @@ Merge order:
|
||||
|
||||
```text
|
||||
fnd(M01): add canonical coordinate mapper
|
||||
net(M08): decode auth challenge safely
|
||||
net(M09): decode auth challenge safely
|
||||
rnd(M03): extract streaming target planner
|
||||
test(M00): add paired checkpoint manifest
|
||||
```
|
||||
|
||||
@@ -54,6 +54,42 @@
|
||||
- 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).
|
||||
|
||||
M04 renderer fidelity использует оригинальный клиент build 12340 как
|
||||
authoritative visual oracle. Noggit допускается как дополнительный reference
|
||||
композиции/placements и Editor UX, но не подтверждает lighting, shadows, liquids,
|
||||
materials, animation или effects.
|
||||
|
||||
Reference corpus импортируется через versioned CSV manifest. Для каждой
|
||||
выбранной локации базово снимаются `wide` и `ground` viewpoints; отдельные
|
||||
specialized captures добавляются для уникальных interior, liquid, shadow,
|
||||
character и effect policies. Capture metadata фиксирует WoW/server coordinates,
|
||||
доступные camera fields, time, weather, graphics profile, viewport, artifact
|
||||
name и provenance. Отсутствующее значение хранится как `Unknown`, а не
|
||||
восстанавливается предположением.
|
||||
|
||||
Proprietary screenshots/video остаются вне Git. Repository evidence включает
|
||||
schema, SHA-256, static/temporal metrics, semantic region classification и human
|
||||
approval. Static parity требует согласованных geometry/framing и material/light/
|
||||
shadow/liquid regions; temporal parity отдельно проверяет phase, duration,
|
||||
trajectory, UV motion и emitter lifetime.
|
||||
|
||||
### Navigation compatibility
|
||||
|
||||
- Golden synthetic meshes проверяют slope, climb, radius erosion, holes, tiled seams и off-mesh connections.
|
||||
|
||||
+40
-2
@@ -40,13 +40,15 @@ Decision/ADR:
|
||||
| OpenWC native loaders | ADOPTED | MPQ/BLP/ADT/WDT/M2/WMO | Текущий import/render pipeline | Неполная fidelity форматов |
|
||||
| StormLib | ADOPTED | MPQ | Чтение архивов через native extension | Version/license/update audit |
|
||||
| WowUnreal | REFERENCE | Полный клиент | Coverage, acceptance criteria, networking/UI research | Unreal-specific design |
|
||||
| WoWee | REFERENCE | Клиент/editor/formats | Architecture, editor workflows, tests, open formats | Заявления требуют независимой проверки |
|
||||
| Noggit Red | REFERENCE | World editor | Terrain/placement UX, UID workflows | Не Godot architecture |
|
||||
| WoWee | REFERENCE | WotLK client/render/UI/editor | M2/liquid fixtures, retained FrameXML UI, Lua diagnostics, progressive UI takeover и authoring validation | Modified MIT запрещает commercial-game use; fidelity claims требуют build-12340 oracle |
|
||||
| Original WoW 3.3.5a build 12340 client | ADOPTED | Visual/behavior oracle | Private static/temporal capture corpus, settings and paired comparison | Proprietary artifacts remain outside Git; camera/settings provenance must be explicit |
|
||||
| Noggit Red | REFERENCE | World editor | Terrain/placement UX, UID workflows и secondary render-composition reference | Не Godot architecture; не authoritative lighting/material/shadow/liquid oracle |
|
||||
| open-realm | REFERENCE | Formats/runtime | Независимая проверка parsers/render behavior | Другая архитектура и coverage |
|
||||
| whoa | REFERENCE | Client behavior | 3.3.5a runtime semantics и fixtures | Лицензия и переносимость отдельных решений |
|
||||
| wow.export | REFERENCE | Asset conversion | M2/WMO/material/export edge cases | Web-specific pipeline |
|
||||
| Blender WoW Studio | REFERENCE | Authoring/conversion | WMO/M2/ADT authoring knowledge | Blender-specific UI/data model |
|
||||
| [Wowser](https://github.com/wowserhq/wowser) | REFERENCE | 3.3.5a web client | Auth/realm/character/world protocol, binary parsing, asset pipeline и render research | Старый JS/WebGL proof-of-concept, неполный клиент |
|
||||
| [Benilla](https://github.com/samwhosung/benilla) | REFERENCE | WoW 1.12.1 client/render/UI | M2 GPU animation, materials, particles/ribbons, WMO portals, FrameXML/Lua architecture и compatibility-test ideas | Vanilla build 5875 и Bevy-specific implementation не доказывают WotLK build-12340 fidelity |
|
||||
| [warcraft-rs](https://github.com/wowemulation-dev/warcraft-rs) | CANDIDATE | WoW formats/CLI | Independent MPQ/DBC/BLP/ADT/WDT/WDL/M2/WMO validation и conversion oracle | Rust/tool duplication; claims require fixtures |
|
||||
| [WoWDBDefs](https://github.com/wowdev/WoWDBDefs) | REFERENCE | Client DB schemas | Versioned DBC definitions и typed-code generation input | Definitions still require build-specific validation |
|
||||
| [wow_dbc](https://github.com/gtker/wow_dbc) | REFERENCE | DBC | 1.12/2.4.3/3.3.5 read/write и SQLite conversion ideas | Older release, Rust integration unnecessary by default |
|
||||
@@ -57,6 +59,23 @@ Decision/ADR:
|
||||
| [Keira3](https://github.com/azerothcore/Keira3) | REFERENCE | AzerothCore DB editor | Field semantics, SQL generation и DB editor UX | AGPL; schema-specific web architecture |
|
||||
| [WowBench](https://sourceforge.net/projects/wowbench/) | REFERENCE | WoW UI/API | Offline XML/Lua API emulation и addon test ideas | Старый и неполный implementation |
|
||||
|
||||
## WoWee — карточка референса
|
||||
|
||||
- **Name / URL:** [WoWee](https://github.com/WoWee-Dev/WoWee), локально `reference/WoWee`.
|
||||
- **Status:** `REFERENCE`; код не подключается как dependency и не вендорится.
|
||||
- **Reviewed update:** `master` от `626243e937fb93965fa583a6507ed5a1aa7dda4b` до `8456c236b57140e98667d6d8188f5cd1cc226daf` (2026-08-12): 2557 commits. Raw diff содержит 1622 files, 164940 additions и 1595923 deletions, но объём deletions в основном создают удалённые generated SQL/data/build artifacts; полезный signal сосредоточен в renderer, UI/FrameXML, pipeline и tests.
|
||||
- **Checkout refresh:** текущий upstream checkout — `607ea3b8369851014721416293f8e95dfbe64eec` (2026-09-05). Это обновление pins не является новой fidelity evaluation; релевантные изменения требуют отдельного bounded review перед использованием.
|
||||
- **Renderer findings:** новый общий M2 track sampler отделяет global-sequence time от animation sequence и добавляет finite-value guards, но Hermite/Bezier пока линейно аппроксимируются. Централизованный M2+skin/external-`.anim` loader и selective animation loading полезны как pattern для границы parser/resolver. Новые water-mask и water-surface-grid tests фиксируют LSB-first 8x8 chunk masks, solid fallback, rotated WMO liquid projection, inclusive far edge и degenerate axes. Vertex-layout tests сверяют CPU declarations с shader inputs. GPU lifetime/deferred-release решения полезны концептуально, но Vulkan implementation не переносится в Godot.
|
||||
- **Animation/effects evidence:** WotLK asset tests покрывают color/alpha tracks, независимый wrap global sequences и `$FSD` footstep events. Отсутствующие private assets в части тестов дают `SUCCEED`, поэтому зелёный run не доказывает, что fixture реально исполнялся. Particle/ribbon delta в основном добавляет sampling, batching, descriptor reuse и diagnostics; flame/smoke/ribbon поведение содержит эвристики и не заменяет build-12340 capture oracle. Для архитектуры shared effect stream Benilla остаётся более сильным secondary reference.
|
||||
- **Placement warning:** WoWee прямо оставляет rotation order наклонённых doodad placements нерешённым; имеющийся test различает только upright yaw. OpenWC не меняет calibrated transforms без tilted `MDDF`/`MODF` fixture из build 12340.
|
||||
- **UI/FrameXML findings:** retained widget tree отделён от renderer, сохраняет WoW bottom-left/y-up coordinates до единственного draw-boundary flip и покрывает anchors, draw order, hit testing, visibility, scroll, controls и ownership headless tests. XML компилируется в Lua и проходит тот же `CreateFrame`/template path, что Lua-created widgets. Progressive takeover передаёт отдельные default-UI элементы FrameXML с явными bridges для portrait/model/minimap/world-map content. Это сильная модель поэтапной миграции M05, но не готовая архитектура OpenWC.
|
||||
- **Lua/tooling findings:** runtime ограничивает стандартные библиотеки, ставит instruction-hook timeout, копирует listener list перед dispatch, ограничивает event recursion, нормализует event argument types и диагностирует stack/source/line. Большой headless corpus и статические audits проверяют XML emission, templates, `$parent`, handler arity, event order/arity, missing APIs, nil arithmetic, globals и keybinding takeover. Однако `lua_engine.cpp` монолитен, unknown-API fallback маскирует отсутствующие контракты, а настоящих taint/secure execution semantics нет; OpenWC сохраняет отдельный `LuaRuntime`, view models/intents и fail-closed compatibility tiers.
|
||||
- **Pipeline/editor findings:** общий bounded binary-I/O layer, finite vertex sanitization, DXT block tests, streaming manifest parse и единые validation/save reports полезны как test/validation patterns. Собственные `.w*` formats зависят от native padding/endianness и не подходят как canonical portable OpenWC artifacts без отдельной спецификации.
|
||||
- **License/data risk:** repository LICENSE — MIT с дополнительным запретом использовать software как основу или компонент commercial video games без письменного разрешения; original music имеет отдельный all-rights-reserved notice. Поэтому допустимы research, decomposition и независимо реализованные fixtures; копирование/вендоринг кода требует предварительного legal review.
|
||||
- **Fidelity limits:** проект WotLK-aware и использует build 12340, но остаётся active WIP. В исследованном diff нет систематического paired original-client visual corpus; часть tests повторяет internal implementation, а README перечисляет runtime regressions. WoWee — источник гипотез и test cases, не oracle. Oracle OpenWC остаётся оригинальный клиент 3.3.5a build 12340.
|
||||
- **Bounded adoption:** для M04 приоритетны обязательные (не silent-skip) build-12340 fixtures для M2 global-sequence/color-alpha sampling, MH2O/MCLQ/MLIQ masks и rotated WMO water, CPU-mesh/shader interface verification и tilted placement order. Для M05 — engine-free widget/layout core, единый XML/Lua creation path, progressive takeover и headless audit taxonomy. Любое поведение принимается только после paired capture/fixture с provenance и hash.
|
||||
- **Decision / ADR:** `REFERENCE`, не dependency. ADR нужен, если WoWee-inspired решение меняет публичный renderer/UI/Lua contract, artifact schema, engine boundary или вводит third-party code.
|
||||
|
||||
## rilua — карточка кандидата
|
||||
|
||||
- **Problem solved:** Lua 5.1.1 VM, bytecode, embedding и официальный compatibility corpus для addon runtime.
|
||||
@@ -77,6 +96,25 @@ Decision/ADR:
|
||||
- **Known gaps:** proof-of-concept, неполный gameplay/UI, browser WebSocket proxy вместо native TCP, устаревший JS ecosystem (React 0.14/Three.js 0.77 era), архитектура не переносится напрямую в Godot.
|
||||
- **Decision policy:** использовать для cross-check и test ideas; не добавлять Node/Web dependencies и не копировать browser-specific abstractions в OpenWC.
|
||||
|
||||
## Benilla — карточка референса
|
||||
|
||||
- **Name / URL:** [samwhosung/benilla](https://github.com/samwhosung/benilla), локально `reference/benilla`.
|
||||
- **Status:** `REFERENCE`; исходники доступны как pinned research submodule, но не подключены к build/runtime как dependency и не вендорятся.
|
||||
- **Pinned research revision:** `bc1a2428dd7e00ca8abbfb8f0bf53750dae7b123` (проверен 2026-09-05). Репозиторий публикуется автором как squashed snapshots из private tree, поэтому при повторном исследовании revision фиксируется заново.
|
||||
- **Problem studied:** полный независимый клиент WoW 1.12.1 build 5875 на Rust/Bevy, включая streamed world renderer, M2 animation/materials, WMO portal visibility, liquids/sky/weather, particles/ribbons/spell visuals и собственный TOC/FrameXML/Lua UI runtime.
|
||||
- **Planned boundary:** использовать только как независимый источник алгоритмов, decomposition, diagnostics и test cases. OpenWC сохраняет свои `WorldRenderFacade`, Godot main-thread/finalization boundaries, `LuaRuntime`, immutable UI view models/intents и build-12340 contracts; прямой Bevy/ECS или Rust-type bridge не вводится.
|
||||
- **Useful renderer source map:** `crates/benilla-world/src/model_render*`, `rig_anim*`, `rig_palette*`, `particles*`, `ribbons.rs`, `wmo_portal*`, `lighting*`, `weather*`, `terrain_stream*` и `shaders/wow_effect.wgsl`; format-side M2 animation/particle/ribbon tracks находятся в `crates/benilla-formats/src/models/anim.rs`, `particles.rs` и `ribbons.rs`.
|
||||
- **Useful UI/Lua source map:** engine-free crate `crates/benilla-ui`: `toc.rs`, `framexml.rs`, `loader/*`, `layout.rs`, `widget/*`, `order.rs`, `script/event.rs`, `script/tick.rs`, `script/saved.rs`, sandbox/stdlib bindings и compatibility tests. Особенно полезны сохранение document/load order, bottom-up `OnLoad`, nested-safe restore legacy globals `this/event/argN`, deterministic event/`OnUpdate` order и разделение plain host state от engine adapter.
|
||||
- **License:** `MIT OR Apache-2.0`; конкретное заимствование кода требует сохранения license/attribution и отдельной проверки совместимости с лицензиями OpenWC и third-party dependencies.
|
||||
- **Platforms/toolchain:** stable Rust, Bevy `0.18.1`, `mlua 0.11` с vendored Lua 5.1 и локальным `lua-src` patch pipeline. Это research stack, не предлагаемый OpenWC toolchain.
|
||||
- **Fidelity evidence:** исходники содержат подробные build-5875 byte-law annotations, corpus tests, parser fixtures и temporal/render probes. Сам приватный reverse-engineering corpus не входит в repository, а опубликованный код ориентирован на Vanilla 1.12.1, поэтому ни один его результат сам по себе не является evidence для WoW 3.3.5a build 12340.
|
||||
- **Known gaps:** README прямо ограничивает Lua built-in UI и пока не заявляет third-party addon support; taint/secure execution отсутствуют; runtime использует Lua 5.1 с точечными Lua-5.0 compatibility patches для Vanilla; renderer и scheduling зависят от Bevy render phases/ECS. Layouts, M2 records, shader flags, spell visuals, UI API и security semantics должны повторно проверяться для WotLK.
|
||||
- **Security/data risks:** Lua sandbox удаляет filesystem/OS/package/debug/native reach и принимает только text chunks, но без taint/protected-action модели этого недостаточно для 3.3.5a addons. Proprietary client assets и private RE artifacts не копируются; тесты OpenWC используют легально полученные локальные data и разрешённые metadata/fixtures.
|
||||
- **Performance evidence:** Benilla документирует collapsed rig pose/palette arrays вместо десятков тысяч bone entities и один shared effect vertex/index stream вместо per-emitter dynamic meshes; source comments приводят локальные measurements (включая около 145 mesh changes/frame до shared stream). Эти числа не воспроизведены на Godot и служат только гипотезой для bounded spike.
|
||||
- **Spike and acceptance criteria:** (1) renderer — на build-12340 M2 fixtures сравнить bone palette/global sequences, material ordering, particle plane/sphere/spline emitters, head/tail/model particles и ribbons по phase/duration/lifetime; затем проверить Godot-friendly shared effect buffer/pool без нарушения M03 budgets; (2) UI — прогнать WotLK TOC/FrameXML/Lua corpus на document order, inheritance, anchors, handler calling convention, event order, `OnUpdate`, SavedVariables, restricted libraries и error text; secure actions/taint имеют отдельный обязательный corpus. Любое принятое решение требует original-client paired evidence, p50/p95/p99 и regression fixtures.
|
||||
- **Pinned version/update policy:** исследование и ссылки привязаны к указанному commit. Обновление проводится вручную по diff релевантных directories; moving `main` не становится новым oracle автоматически.
|
||||
- **Decision / ADR:** `REFERENCE`, не dependency. ADR требуется только если Benilla-inspired решение меняет публичный renderer/effects/UI/Lua contract, cache/schema, engine boundary или добавляет dependency.
|
||||
|
||||
## recast-rs — карточка кандидата
|
||||
|
||||
- **Problem solved:** Recast navmesh generation, Detour queries, tiled navigation и dynamic obstacles.
|
||||
|
||||
@@ -48,9 +48,15 @@
|
||||
| WMO render Resource cache state | Implemented extraction | [`wmo-render-resource-cache-state.md`](wmo-render-resource-cache-state.md) |
|
||||
| WMO render Resource finalizer | Implemented extraction | [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md) |
|
||||
| WMO scene Resource cache state | Implemented extraction | [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md) |
|
||||
| WMO scene Resource finalizer | Implemented extraction | [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md) |
|
||||
| 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) |
|
||||
|
||||
@@ -241,7 +241,7 @@ runtime data migration.
|
||||
| Current starter outfit | Partial | Existing resolver reused unchanged | Needs extracted DBC fixture and client comparison |
|
||||
| Current skin/geoset composition | Partial | Existing components reused | Full equipment/customization fidelity incomplete |
|
||||
| Build-12340 animation semantics | Planned | No original-client fixture | Capture animation IDs/transitions/timing |
|
||||
| Runtime equipment/network updates | Planned | No snapshot contract | M08/M09/M12 work |
|
||||
| Runtime equipment/network updates | Planned | No snapshot contract | M09/M10/M13 work |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
|
||||
@@ -237,7 +237,7 @@ separate versioned movement snapshot contract.
|
||||
|
||||
- Terrain/collision policy can consume displacement without changing input or velocity ownership.
|
||||
- A future server-aware predictor may replace this controller behind the scene composition boundary.
|
||||
- A typed movement snapshot may expose deterministic replay state when M08/M09 require it.
|
||||
- A typed movement snapshot may expose deterministic replay state when M09/M10 require it.
|
||||
- A future application profile can map to this narrow capability value without
|
||||
coupling the movement controller to the application shell.
|
||||
|
||||
@@ -252,8 +252,8 @@ separate versioned movement snapshot contract.
|
||||
| Typed sprint/free-flight exclusion | Implemented | Pure and real-scene profile regressions | Application shell must select profile explicitly later |
|
||||
| Terrain height query | Implemented | Typed `TerrainQuery` and injected player regression | Ground-snap policy remains scene-owned |
|
||||
| Terrain collision movement policy | Planned | Height-only query does not model collision | Add slopes/holes/collision later |
|
||||
| Jump/fall/swim | Planned | M02/M09 roadmap | Requires terrain/liquid and server contracts |
|
||||
| Prediction/reconciliation | Planned | M08/M09 roadmap | Requires movement snapshot/network contract |
|
||||
| Jump/fall/swim | Planned | M02/M10 roadmap | Requires terrain/liquid and server contracts |
|
||||
| Prediction/reconciliation | Planned | M09/M10 roadmap | Requires movement snapshot/network contract |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
|
||||
@@ -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 |
|
||||
@@ -227,7 +227,7 @@ Existing ADT/native/cache format versions remain unchanged.
|
||||
| Renderer loaded-mesh diagnostic backend | Implemented | M03 facade typed sample and detached snapshot contract | Not composed into gameplay; triangle Mesh ray is diagnostic only |
|
||||
| Authoritative renderer/physics backend | Planned | Boundary permits replacement | Define holes/slopes/collision/readiness semantics before gameplay composition |
|
||||
| Holes/slopes/collision | Planned | Outside height-only contract | Later movement/physics package |
|
||||
| Liquid/swim query | Planned | Outside contract | M09/M12 world gameplay |
|
||||
| Liquid/swim query | Planned | Outside contract | M10/M13 world gameplay |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
|
||||
@@ -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,214 @@
|
||||
# WMO Render Group Materializer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target | M03 Renderer Facade and Safe Extraction |
|
||||
| Work package | `M03-RND-WMO-RENDER-GROUP-MATERIALIZER-001` |
|
||||
| Owner | Render |
|
||||
| Last verified | 2026-08-01 |
|
||||
|
||||
## Purpose
|
||||
|
||||
`WmoRenderGroupMaterializer` creates and attaches one lightweight cached WMO
|
||||
render group on the renderer main thread. It owns the duplicated
|
||||
`MeshInstance3D`/`MultiMeshInstance3D` presentation rules that previously lived
|
||||
inside `StreamingWorldLoader`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- select a build step, advance/cancel a job or consume a render permit;
|
||||
- load, cache, validate or finalize WMO Resources;
|
||||
- resolve placement transforms or own the placement root;
|
||||
- choose Editor persistence policy or serialize generated nodes;
|
||||
- change WMO cache formats, materials, visibility or shadow policy.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
The loader obtains a queue-owned WMO root and render Resource, asks
|
||||
`WmoRenderBuildStepPlanner` for one operation, and finalizes the selected Mesh.
|
||||
The materializer then performs only indexed node presentation and attachment.
|
||||
The loader retains scheduler, queue and Editor composition responsibilities.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Queue[WmoRenderBuildQueue] --> Loader[StreamingWorldLoader]
|
||||
Planner[WmoRenderBuildStepPlanner] --> Loader
|
||||
Loader --> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
Loader --> Materializer[WmoRenderGroupMaterializer]
|
||||
Materializer --> MeshNode[MeshInstance3D]
|
||||
Materializer --> MultiMeshNode[MultiMeshInstance3D]
|
||||
MeshNode --> Root[Queue-owned WMO Node3D root]
|
||||
MultiMeshNode --> Root
|
||||
Loader --> EditorOwner[Optional Editor owner assignment]
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Role | Thread/lifetime | Failure behavior |
|
||||
|---|---|---|---|
|
||||
| `materialize_mesh_group(...)` | Create, configure and attach one indexed Mesh group | Renderer main thread; stateless after return | Null/invalid parent, null Mesh or negative index returns null |
|
||||
| `materialize_multimesh_group(...)` | Create, configure and attach one indexed MultiMesh doodad group | Renderer main thread; stateless after return | Null/invalid parent, null MultiMesh or negative index returns null |
|
||||
|
||||
Both methods preserve exact Resource identity. `group_index` selects the optional
|
||||
name and transform; missing names use the historical indexed fallback and a
|
||||
missing transform leaves `Transform3D.IDENTITY`. Positive visibility range
|
||||
applies its caller-supplied margin. Shadow mode is always applied explicitly.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Queue-owned WMO `Node3D` root | `WmoRenderBuildQueue` via loader | Materializer | Borrowed; not retained | One main-thread call |
|
||||
| Input | Selected `Mesh` or `MultiMesh` | WMO render Resource via loader | Materializer | Borrowed exact Resource | Parent-node lifetime after attach |
|
||||
| Input | Names, transforms and selected index | WMO render Resource/planner via loader | Materializer | Borrowed value collections | One call |
|
||||
| Input | Visibility end/margin and shadow flag | Loader quality profile | Materializer | Scalar values | One call |
|
||||
| Output | Attached `MeshInstance3D` or `MultiMeshInstance3D` | Materializer | Loader/SceneTree | Parent root owns node and Resource reference | Until placement release/world teardown |
|
||||
|
||||
Side effects:
|
||||
|
||||
- allocates exactly one Godot geometry node for valid input;
|
||||
- applies name, optional transform, shadow and optional visibility settings;
|
||||
- attaches the node exactly once to the supplied WMO root.
|
||||
|
||||
It performs no filesystem, ResourceLoader, worker, RenderingServer RID, cache,
|
||||
queue, permit, logging or direct Editor-owner mutation.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[Root + Resource + indexed metadata + render settings] --> Validate{Valid root/resource/index?}
|
||||
Validate -->|no| Null[Return null; no attachment]
|
||||
Validate -->|yes| Kind{Mesh or MultiMesh method}
|
||||
Kind --> Mesh[Create MeshInstance3D]
|
||||
Kind --> Multi[Create MultiMeshInstance3D]
|
||||
Mesh --> Configure[Name + optional transform + render settings]
|
||||
Multi --> Configure
|
||||
Configure --> Attach[Attach once to WMO root]
|
||||
Attach --> Return[Return borrowed attached node]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Queue as WmoRenderBuildQueue
|
||||
participant Finalizer as WmoRuntimeMeshFinalizer
|
||||
participant Materializer as WmoRenderGroupMaterializer
|
||||
participant Root as WMO Node3D root
|
||||
Loader->>Queue: read front job and cursors
|
||||
Loader->>Finalizer: finalize selected Mesh
|
||||
Loader->>Materializer: materialize selected group
|
||||
Materializer->>Root: add_child(geometry instance)
|
||||
Materializer-->>Loader: attached node or null
|
||||
Loader->>Loader: optional Editor ownership
|
||||
Loader->>Queue: adopt planned cursors
|
||||
Loader->>Loader: consume one group permit
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Materializer[WmoRenderGroupMaterializer]
|
||||
Materializer --> Engine[Node3D / GeometryInstance3D / Mesh / MultiMesh]
|
||||
Materializer -. no dependency .-> IO[ResourceLoader / FileAccess]
|
||||
Materializer -. no dependency .-> Queue[WmoRenderBuildQueue]
|
||||
Materializer -. no dependency .-> Scheduler[RenderBudgetScheduler]
|
||||
Materializer -. no dependency .-> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Both public methods are main-thread only because they mutate the SceneTree.
|
||||
- The caller owns the parent root; after attachment the root owns the new node.
|
||||
- The node retains the exact input Mesh or MultiMesh Resource reference.
|
||||
- The service retains no Node, Resource, RID, collection or per-group state.
|
||||
- Loader-owned optional recursive Editor ownership runs after successful return.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Recovery |
|
||||
|---|---|---|---|
|
||||
| Null/freed parent | Guard | Return null; allocate/attach nothing | Caller validates current queue job |
|
||||
| Null Resource | Guard | Return null | Loader advances the historically selected cursor |
|
||||
| Negative index | Guard | Return null | Planner supplies non-negative selected indices |
|
||||
| Missing name | Bounds check | Use `Group_N` or `DoodadGroup_N` | Rebuild cache metadata if desired |
|
||||
| Missing transform | Bounds check | Retain identity transform | Rebuild cache metadata if desired |
|
||||
| Placement cancellation | Loader/queue | Parent and children released by existing lifecycle | Re-request placement later |
|
||||
| Shutdown | Loader lifecycle | Service has no retained state to drain | New loader composes a new service |
|
||||
|
||||
## Configuration, capabilities and profiles
|
||||
|
||||
The service introduces no configuration or capability. It accepts the existing
|
||||
`wmo_visibility_range`, `CHUNK_SIZE` margin and `wmo_cast_shadows` values chosen
|
||||
by the loader quality profile. Blizzlike and Enhanced selection remains outside.
|
||||
|
||||
## Persistence, cache and migrations
|
||||
|
||||
No persisted data or cache format changes. The service neither reads nor writes
|
||||
WMO cache files and requires no migration or rebake.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The service emits no logs or metrics. Existing `wmo_groups` queue depth, build
|
||||
permits and loader lifecycle diagnostics remain authoritative.
|
||||
|
||||
## Verification and fidelity evidence
|
||||
|
||||
- `verify_wmo_render_group_materializer.gd` covers exact Mesh/MultiMesh identity,
|
||||
indexed and fallback names, optional transforms, shadows, positive/disabled
|
||||
visibility, attachment, invalid input, source boundaries and 1,000 groups.
|
||||
- Adjacent WMO queue/planner/finalizer and checkpoint regressions protect the
|
||||
unchanged orchestration and visible output.
|
||||
- This is an exact code-motion extraction of existing Godot presentation rules;
|
||||
it adds no original-client 3.3.5a visual parity claim.
|
||||
|
||||
## Performance budgets
|
||||
|
||||
The synthetic contract requires 1,000 simple Mesh group materializations in
|
||||
under one second. Production work remains limited to one group per scheduler
|
||||
permit. Asset-backed CPU/GPU p95/p99 and long traversal remain required evidence.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed WMO traversal can measure group attachment and lifetime without
|
||||
changing this API.
|
||||
- Additional render settings belong here only when they apply equally to both
|
||||
lightweight group-node kinds and have fidelity evidence.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Mesh group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed visual/GPU p95/p99 pending |
|
||||
| MultiMesh doodad group materialization | Implemented extraction | Identity/name/transform/render/attachment contract | Asset-backed traversal/leak evidence pending |
|
||||
| Build planning/queue progress | Loader-owned | Existing planner/queue regressions | Further orchestration extraction pending |
|
||||
| Editor persistence ownership | Loader-owned | Source-boundary contract | Editor scene-save integration evidence pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- SceneTree node creation remains synchronous main-thread work by design.
|
||||
- The synthetic fixture does not measure private WMO assets, GPU upload, portal
|
||||
visibility, original-client visuals, long traversal or leak behavior.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_render_group_materializer.gd` | Indexed geometry-node creation, settings and attachment |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, finalization, queue, permits, Editor ownership and lifecycle |
|
||||
| `src/tools/verify_wmo_render_group_materializer.gd` | Synthetic contract, ownership boundary and timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-render-build-step-planner.md`](wmo-render-build-step-planner.md)
|
||||
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
|
||||
- [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.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)
|
||||
@@ -0,0 +1,224 @@
|
||||
# WMO Runtime Mesh Finalizer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-WMO-RUNTIME-MESH-FINALIZER-001` |
|
||||
| Owners | Cached WMO runtime Mesh refresh version, surface iteration and material reconstruction |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-runtime-mesh-finalizer`, 2026-07-18 |
|
||||
| Profiles/capabilities | Profile-independent cached WMO material refresh |
|
||||
|
||||
## Purpose
|
||||
|
||||
Finalize stale cached WMO runtime Mesh materials outside
|
||||
`StreamingWorldLoader`: preserve exact Mesh identity, stamp the historical
|
||||
refresh version and reconstruct eligible surface materials through WMOBuilder.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Traverse scene Nodes or own Mesh/MultiMesh attachment and lifetime.
|
||||
- Select WMO cache paths, poll ResourceLoader or schedule render build jobs.
|
||||
- Change WMOBuilder shaders, textures, blend rules or cache serialization.
|
||||
- Share an abstraction with the behaviorally different M2 finalizer.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader render build step] -->|Mesh plus extracted directory| Finalizer[WmoRuntimeMeshFinalizer]
|
||||
Preparer[WmoRuntimeScenePreparer cached traversal] -->|Mesh plus extracted directory| Finalizer
|
||||
Finalizer -->|material definition plus compact texture paths| Builder[WMOBuilder material boundary]
|
||||
Builder -->|rebuilt Material| Finalizer
|
||||
Finalizer -->|same Mesh identity| Loader
|
||||
Finalizer -->|same Mesh identity| Preparer
|
||||
```
|
||||
|
||||
The runtime scene preparer owns cached-scene traversal; the loader owns render
|
||||
build-step selection and composition. The finalizer owns only the in-place
|
||||
Resource operation and depends on an injected WMO material builder.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `finalize_mesh(mesh, extracted_directory)` | Command/query | Stamp and refresh a stale Mesh in place | Renderer main thread; stateless between calls | Null returns null; unsupported Mesh returns exact identity |
|
||||
| `rebuild_cached_material(material, extracted_directory)` | Query/boundary | Reconstruct one metadata-bearing cached WMO Material | Renderer main thread; borrowed input | Null/unmarked/missing builder returns null |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | `Mesh` and refresh metadata | Cached WMO scene/render group | Finalizer | Borrowed Resource | Call duration; identity retained |
|
||||
| Input | Extracted content directory | Loader configuration | WMOBuilder | Borrowed String value | Call duration |
|
||||
| Input | Texture paths, WMO flags/shader/blend and cached colors | Surface Material metadata/parameters | Finalizer | Borrowed values | One surface rebuild |
|
||||
| Output | Compact texture-path array and material definition | Finalizer | WMOBuilder | Detached values | One builder call |
|
||||
| Output | Rebuilt Material | WMOBuilder | ArrayMesh surface | Surface adopts exact Resource | Mesh lifetime |
|
||||
| Output | Exact input Mesh | Finalizer | Runtime scene preparer or loader build step | Caller retains ownership | Existing cache/scene lifetime |
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Mesh[Borrowed Mesh] --> Null{Null?}
|
||||
Null -->|yes| ReturnNull[Return null]
|
||||
Null -->|no| Current{Refresh version at least 10?}
|
||||
Current -->|yes| ReturnSame[Return exact Mesh]
|
||||
Current -->|no| Stamp[Stamp version 10]
|
||||
Stamp --> Array{ArrayMesh?}
|
||||
Array -->|no| ReturnSame
|
||||
Array -->|yes| Surface[Iterate surfaces]
|
||||
Surface --> Eligible{Material has texture0 metadata?}
|
||||
Eligible -->|no| Keep[Keep exact Material]
|
||||
Eligible -->|yes| Definition[Compact paths and copy metadata/colors]
|
||||
Definition --> Builder[WMOBuilder build material]
|
||||
Builder --> Result{Non-null result?}
|
||||
Result -->|yes| Adopt[Replace surface Material]
|
||||
Result -->|no| Keep
|
||||
Keep --> Surface
|
||||
Adopt --> Surface
|
||||
Surface -->|done| ReturnSame
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Stale: missing/version below 10
|
||||
[*] --> Current: version at least 10
|
||||
Stale --> Current: stamp before optional surface refresh
|
||||
Current --> Current: subsequent calls are identity-only
|
||||
```
|
||||
|
||||
The version stamp is intentionally applied before type/material eligibility, as
|
||||
in the extracted loader behavior. The finalizer retains no Mesh or Material.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Preparer as WmoRuntimeScenePreparer
|
||||
participant Finalizer as WmoRuntimeMeshFinalizer
|
||||
participant Mesh as ArrayMesh
|
||||
participant Builder as WMOBuilder
|
||||
alt lightweight render build step
|
||||
Loader->>Finalizer: finalize_mesh(mesh, extracted_directory)
|
||||
else cached scene traversal
|
||||
Preparer->>Finalizer: finalize_mesh(mesh, extracted_directory)
|
||||
end
|
||||
Finalizer->>Mesh: read/stamp refresh metadata
|
||||
loop each stale ArrayMesh surface
|
||||
Finalizer->>Mesh: surface_get_material(index)
|
||||
alt cached WMO metadata exists
|
||||
Finalizer->>Builder: _build_material(definition, compact paths, directory)
|
||||
Builder-->>Finalizer: rebuilt Material or null
|
||||
Finalizer->>Mesh: surface_set_material when non-null
|
||||
end
|
||||
end
|
||||
Finalizer-->>Loader: exact Mesh identity for build path
|
||||
Finalizer-->>Preparer: exact Mesh identity for cached traversal path
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
Preparer[WmoRuntimeScenePreparer] --> Finalizer
|
||||
Finalizer --> Engine[Mesh / ArrayMesh / Material]
|
||||
Finalizer --> Builder[Injected WMO material builder]
|
||||
Finalizer -. no dependency .-> Nodes[Node traversal/lifetime]
|
||||
Finalizer -. no dependency .-> RL[ResourceLoader]
|
||||
Finalizer -. no dependency .-> Queue[Build queue/scheduler]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- The renderer main thread serializes Mesh metadata and surface mutation.
|
||||
- The caller owns Mesh identity, scene traversal, attachment and destruction.
|
||||
- ArrayMesh surfaces adopt only non-null builder results; otherwise the exact
|
||||
cached Material remains attached.
|
||||
- The service retains no Resource, Node, RID, file, queue or per-path state.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Null Mesh | Guard | Return null | Synthetic contract | Correct caller input |
|
||||
| Current Mesh | Version metadata | Return exact identity, no builder call | Identity/version contract | Version bump makes it stale |
|
||||
| Unsupported Mesh subtype | Type check after stamp | Return exact identity | PrimitiveMesh contract | No surface work required |
|
||||
| Null/unmarked Material | Metadata guard | Preserve exact surface value | Surface contract | Rebuild cache with metadata |
|
||||
| Missing builder/null result | Boundary guard/result | Preserve exact Material | Synthetic contract | Correct composition/retry after version bump |
|
||||
| Shutdown | Loader lifecycle | No retained work to cancel | Shutdown suite | New loader composes a new service |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
Refresh version `10` and metadata key `wow_wmo_material_refresh_version` are the
|
||||
existing runtime compatibility boundary. No project setting, profile, permit or
|
||||
feature flag is introduced.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
The module writes only the existing runtime Mesh metadata stamp and serializes
|
||||
nothing. It does not change WMO scene/render cache formats or trigger rebakes.
|
||||
A future material-rule change must deliberately bump the refresh version.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The finalizer emits no logs and allocates no metrics. Existing `wmobuild` queue
|
||||
metrics and loader diagnostics remain the operational correlation surface.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_wmo_runtime_mesh_finalizer.gd`: null/current/non-ArrayMesh identity,
|
||||
version stamp, null/unmarked surface retention, compact path indices,
|
||||
flags/shader/blend/default and shader colors, exact builder adoption,
|
||||
missing-builder behavior, source ownership and 1,000 current calls under one second.
|
||||
- Adjacent WMO queue/cache/finalizer, shutdown, material and baseline regressions
|
||||
protect orchestration and visible output.
|
||||
- Fidelity evidence is exact behavior-preserving extraction; no private asset or
|
||||
original-client visual-parity claim is added.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed WMO fixtures may compare reconstructed surface parameters without
|
||||
changing the service contract.
|
||||
- A deliberate material refresh change may bump the owned version with old/new
|
||||
fixtures and paired visual evidence.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Runtime refresh admission | Implemented extraction | Identity/version/type contracts | Serialized cache fixture pending |
|
||||
| Cached material definition reconstruction | Implemented extraction | Path/metadata/color contracts | Asset-backed visual comparison pending |
|
||||
| Scene traversal | Runtime scene preparer-owned | Cached/live traversal regressions | Asset-backed traversal pending |
|
||||
| Node materialization | Loader/group-materializer-owned | Existing WMO regressions | Further safe extraction pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Surface mutation and WMOBuilder material construction remain synchronous
|
||||
main-thread work.
|
||||
- No proprietary WMO corpus, long leak run, GPU timing, traversal p95/p99 or
|
||||
paired build-12340 visual capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Refresh admission, surface iteration and material definition reconstruction |
|
||||
| `addons/mpq_extractor/loaders/wmo_builder.gd` | WMO shader/material construction semantics |
|
||||
| `src/render/wmo/wmo_runtime_scene_preparer.gd` | Cached scene traversal and finalizer delegation |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, build jobs, placement and lifetime |
|
||||
| `src/tools/verify_wmo_runtime_mesh_finalizer.gd` | Identity/version/material/source/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-render-build-queue.md`](wmo-render-build-queue.md)
|
||||
- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.md)
|
||||
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.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)
|
||||
@@ -0,0 +1,211 @@
|
||||
# WMO Runtime Scene Preparer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented |
|
||||
| Target | M03 Renderer Facade and Safe Extraction |
|
||||
| Work package | `M03-RND-WMO-RUNTIME-SCENE-PREPARER-001` |
|
||||
| Owner | Render |
|
||||
| Last verified | 2026-08-01 |
|
||||
|
||||
## Purpose
|
||||
|
||||
`WmoRuntimeScenePreparer` applies the existing post-instantiation rules to a
|
||||
borrowed WMO subtree on the renderer main thread. Cached scenes receive recursive
|
||||
Mesh finalization before render policy; live-built duplicates receive render
|
||||
policy only.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- load, validate, instantiate, duplicate, place or attach a WMO scene;
|
||||
- change `WmoRuntimeMeshFinalizer` material/version rules;
|
||||
- own the supplied subtree or decide Editor persistence;
|
||||
- change cache formats, queue progress, permits or shutdown;
|
||||
- implement portal/room visibility or recursive occluder discovery.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
The loader distinguishes validated cached PackedScene instances from duplicated
|
||||
live-built prototypes. That distinction remains explicit through two public
|
||||
methods so the live path does not gain cached-Mesh refresh behavior.
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Cached[Validated cached WMO instance] --> Loader[StreamingWorldLoader]
|
||||
Live[Duplicated live-built WMO instance] --> Loader
|
||||
Loader -->|prepare_cached_instance| Preparer[WmoRuntimeScenePreparer]
|
||||
Loader -->|prepare_live_instance| Preparer
|
||||
Preparer -->|cached only| MeshFinalizer[WmoRuntimeMeshFinalizer]
|
||||
Preparer --> Policy[Direct Occluders policy + recursive shadow enabling]
|
||||
Policy --> Borrowed[Borrowed WMO Node3D subtree]
|
||||
Loader --> Placement[Placement/attachment/registry lifecycle]
|
||||
```
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Role | Thread/lifetime | Failure behavior |
|
||||
|---|---|---|---|
|
||||
| `prepare_cached_instance(instance, extracted_directory, enable_occlusion_culling, cast_shadows)` | Finalize cached subtree Meshes, then apply render policy | Renderer main thread; stateless after return | Null/freed root returns false |
|
||||
| `prepare_live_instance(instance, enable_occlusion_culling, cast_shadows)` | Apply render policy without Mesh finalization | Renderer main thread; stateless after return | Null/freed root returns false |
|
||||
|
||||
Both methods borrow the root and return a success flag. Disabled occlusion removes
|
||||
only the direct child named `Occluders`, matching the previous loader lookup.
|
||||
Enabled shadows set all descendant `GeometryInstance3D` nodes to ON. Disabled
|
||||
shadows preserve every existing value rather than forcing OFF.
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Cached or live-built WMO `Node3D` | Loader scene/prototype adapter | Preparer | Borrowed exact subtree | One main-thread call |
|
||||
| Input | Extracted directory | Loader configuration | Runtime Mesh finalizer via preparer | Borrowed String | Cached preparation call |
|
||||
| Input | Occlusion/shadow policy | Loader quality configuration | Preparer | Scalar values | One call |
|
||||
| Internal | Mesh/MultiMesh Mesh reference | Borrowed subtree traversal | `WmoRuntimeMeshFinalizer` | Exact Resource reference; not retained by preparer | One cached call |
|
||||
| Output | Success flag | Preparer | Loader | Value | Immediate |
|
||||
| Side effect | Mesh refresh, optional child removal and shadow mutation | Preparer | Borrowed subtree | SceneTree remains loader/placement-owned | Until subtree release |
|
||||
|
||||
No filesystem, ResourceLoader, worker, RID, cache, queue, placement, attachment,
|
||||
Editor-owner or diagnostic side effect is introduced.
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Input[Borrowed root + path kind + policies] --> Valid{Root valid?}
|
||||
Valid -->|no| False[Return false]
|
||||
Valid -->|yes cached| Walk[Parent-before-children traversal]
|
||||
Valid -->|yes live| Policy
|
||||
Walk --> Mesh{MeshInstance or non-null MultiMesh?}
|
||||
Mesh -->|yes| Finalize[WmoRuntimeMeshFinalizer.finalize_mesh]
|
||||
Mesh -->|no| Next[Continue]
|
||||
Finalize --> Next
|
||||
Next --> Policy[Apply direct Occluders and shadow policies]
|
||||
Policy --> True[Return true]
|
||||
```
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Preparer as WmoRuntimeScenePreparer
|
||||
participant Finalizer as WmoRuntimeMeshFinalizer
|
||||
participant Root as Borrowed WMO subtree
|
||||
alt cached scene
|
||||
Loader->>Preparer: prepare_cached_instance(root, directory, policies)
|
||||
loop parent-before-children Mesh traversal
|
||||
Preparer->>Finalizer: finalize_mesh(exact Mesh, directory)
|
||||
end
|
||||
else live-built duplicate
|
||||
Loader->>Preparer: prepare_live_instance(root, policies)
|
||||
end
|
||||
Preparer->>Root: optional direct Occluders removal
|
||||
Preparer->>Root: optional recursive shadow ON
|
||||
Preparer-->>Loader: true/false
|
||||
Loader->>Loader: place, attach, register and own lifetime
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Preparer[WmoRuntimeScenePreparer]
|
||||
Preparer --> Finalizer[WmoRuntimeMeshFinalizer]
|
||||
Preparer --> Engine[Node3D / GeometryInstance3D / Mesh / MultiMesh]
|
||||
Preparer -. no dependency .-> IO[ResourceLoader / FileAccess]
|
||||
Preparer -. no dependency .-> Queue[WMO queues / RenderBudgetScheduler]
|
||||
Preparer -. no dependency .-> Placement[WmoPlacementResolver / Registry]
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Calls are main-thread only because Mesh Resources and SceneTree nodes mutate.
|
||||
- The loader/placement registry retains ownership of the root and descendants.
|
||||
- Mesh finalization receives exact borrowed Resource identities.
|
||||
- A removed direct `Occluders` child is detached and `queue_free()`d as before.
|
||||
- The service retains no Node, Resource, RID, path, collection or state after return.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Recovery |
|
||||
|---|---|---|---|
|
||||
| Null/freed root | Guard | Return false, no traversal | Caller drops/retries stale placement |
|
||||
| Missing finalizer composition | Null dependency | Skip Mesh refresh but apply render policy | Fix renderer composition before cached use |
|
||||
| Null Mesh | Exact finalizer call for MeshInstance; finalizer handles null | Continue traversal | Cache rebuild/finalizer diagnostics |
|
||||
| Null MultiMesh or its Mesh | Guard/finalizer contract | Skip null MultiMesh; finalizer handles null Mesh | Continue safely |
|
||||
| Occlusion disabled | Direct-child lookup | Detach and queue-free `Occluders` | Reinstantiate to restore subtree |
|
||||
| Shadows disabled | Policy branch | Preserve current node settings | Re-run with enabled policy if required |
|
||||
| Placement cancellation/shutdown | Loader lifecycle | Service has no retained work to cancel | Existing subtree release/drain order |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The service introduces no setting. It receives existing
|
||||
`enable_occlusion_culling` and `wmo_cast_shadows` values. The direct-child name
|
||||
`Occluders` and shadow-ON behavior are compatibility rules, not new capabilities.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No serialized format or cache version changes. Cached Mesh refresh metadata
|
||||
continues to belong to `WmoRuntimeMeshFinalizer`; no rebake or migration is needed.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
The preparer emits no log or metric. Loader placement/build metrics and the Mesh
|
||||
finalizer contracts remain the diagnostic surfaces.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_wmo_runtime_scene_preparer.gd` covers cached parent-before-children
|
||||
Mesh/MultiMesh identity, directory forwarding, null MultiMesh, direct versus
|
||||
nested `Occluders`, enabled/preserved shadows, live finalizer suppression,
|
||||
missing dependency, invalid roots, source boundaries and 1,000 traversals.
|
||||
- Adjacent WMO finalizer/cache/queue/shutdown and checkpoint checks protect the
|
||||
unchanged lifecycle and presentation behavior.
|
||||
- Fidelity evidence is exact behavior-preserving extraction; no asset-backed or
|
||||
original-client 3.3.5a visual-parity claim is added.
|
||||
|
||||
The synthetic budget is 1,000 two-Mesh cached preparations under one second.
|
||||
Asset-backed CPU/GPU p95/p99, traversal and leak measurements remain pending.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Asset-backed traversal can validate material identity and subtree lifetime
|
||||
without expanding this service contract.
|
||||
- Portal/room visibility requires its own documented WMO service and evidence;
|
||||
it must not be hidden inside generic subtree preparation.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Cached WMO subtree Mesh finalization | Implemented extraction | Exact traversal/resource/directory contract | Asset-backed visual/GPU p95/p99 pending |
|
||||
| Live/cached render policy | Implemented extraction | Direct occluder and recursive shadow contract | Portal/room and asset-backed traversal pending |
|
||||
| Placement/attachment/lifetime | Loader-owned | Existing registry/shutdown regressions | Further orchestration extraction pending |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- Recursive Mesh finalization and shadow mutation remain synchronous main-thread work.
|
||||
- The missing-finalizer branch degrades safely for isolated tests but production
|
||||
composition must always inject `WmoRuntimeMeshFinalizer`.
|
||||
- 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_runtime_scene_preparer.gd` | Cached/live subtree traversal and render policy |
|
||||
| `src/render/wmo/wmo_runtime_mesh_finalizer.gd` | Borrowed Mesh refresh/version/material rules |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Composition, instantiation, placement, attachment and lifetime |
|
||||
| `src/tools/verify_wmo_runtime_scene_preparer.gd` | Synthetic traversal/policy/boundary/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-runtime-mesh-finalizer.md`](wmo-runtime-mesh-finalizer.md)
|
||||
- [`wmo-scene-resource-finalizer.md`](wmo-scene-resource-finalizer.md)
|
||||
- [`wmo-render-group-materializer.md`](wmo-render-group-materializer.md)
|
||||
- [`world-renderer.md`](world-renderer.md)
|
||||
- [`../../RENDER.md`](../../RENDER.md)
|
||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||
@@ -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)
|
||||
@@ -14,8 +14,9 @@
|
||||
|
||||
Own mutually exclusive cached, missing and pending states for normalized WMO
|
||||
scene paths. The state accepts only caller-validated `PackedScene` references and
|
||||
records `.tscn` paths for threaded requests whose I/O, size limit and cache
|
||||
metadata validation remain in `StreamingWorldLoader`.
|
||||
records `.tscn` paths for threaded requests. Size admission remains in
|
||||
`StreamingWorldLoader`; terminal I/O/probe validation belongs to
|
||||
`WmoSceneResourceFinalizer`.
|
||||
|
||||
## Non-goals
|
||||
|
||||
@@ -33,8 +34,8 @@ flowchart LR
|
||||
Loader --> State[WmoSceneResourceCacheState]
|
||||
Loader --> Size[File existence and size limit]
|
||||
Size --> ResourceLoader[Godot ResourceLoader]
|
||||
ResourceLoader --> Loader
|
||||
Loader --> Probe[Instantiate, metadata/version check, free]
|
||||
ResourceLoader --> Finalizer[WmoSceneResourceFinalizer]
|
||||
Finalizer --> Probe[Instantiate, metadata/version check, free]
|
||||
Probe -->|accepted PackedScene or missing| State
|
||||
Loader --> Live[Live-prototype fallback]
|
||||
```
|
||||
@@ -82,7 +83,7 @@ flowchart TD
|
||||
Check -->|allowed| Request[Start threaded request]
|
||||
Request -->|error| Mark
|
||||
Request -->|accepted| Remember[remember_request]
|
||||
Remember --> Poll[Loader polls snapshot]
|
||||
Remember --> Poll[Scene Resource finalizer polls snapshot]
|
||||
Poll -->|failure| CompleteMissing[complete as missing]
|
||||
Poll -->|loaded| Validate[Instantiate and validate cache metadata]
|
||||
Validate -->|valid| CompleteScene[complete with scene]
|
||||
@@ -114,24 +115,27 @@ stateDiagram-v2
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant State as WmoSceneResourceCacheState
|
||||
participant Finalizer as WmoSceneResourceFinalizer
|
||||
participant RL as ResourceLoader
|
||||
Loader->>State: cached/missing/pending queries
|
||||
Loader->>Loader: exists and wmo_max_runtime_scene_mb check
|
||||
Loader->>RL: load_threaded_request(.tscn)
|
||||
Loader->>State: remember_request(normalized, path)
|
||||
loop renderer tick
|
||||
Loader->>State: request_paths_snapshot()
|
||||
Loader->>RL: load_threaded_get_status(path)
|
||||
Loader->>Finalizer: poll_terminal_requests(State)
|
||||
Finalizer->>State: request_paths_snapshot()
|
||||
Finalizer->>RL: load_threaded_get_status(path)
|
||||
end
|
||||
Loader->>RL: load_threaded_get(path)
|
||||
Loader->>Loader: instantiate, validate WMO metadata, free probe
|
||||
Loader->>State: complete with scene or as missing
|
||||
Finalizer->>RL: load_threaded_get(path)
|
||||
Finalizer->>Finalizer: instantiate, validate WMO metadata, free probe
|
||||
Finalizer->>State: complete with scene or as missing
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- State owns three Dictionaries and strong references to accepted PackedScenes.
|
||||
- Loader owns paths, file measurement, requests, validation and all Node lifetime.
|
||||
- Loader owns paths, file measurement, request admission and placed Nodes;
|
||||
finalizer owns terminal I/O and call-local validation-probe lifetime.
|
||||
- All mutation is serialized on the renderer main thread; no mutex is required.
|
||||
- Detached request snapshots allow terminal removal while polling.
|
||||
- Scene instantiation borrows the PackedScene and does not transfer cache ownership.
|
||||
@@ -143,7 +147,7 @@ sequenceDiagram
|
||||
| Missing `.tscn` | Loader ResourceLoader existence check | Direct missing state | Transient reset permits later retry |
|
||||
| Oversize `.tscn` | Loader byte limit | Missing plus unchanged debug log | Raise limit/rebuild, then reset |
|
||||
| Request-start/load failure | Loader error/status | Direct or terminal missing | Reset and retry later |
|
||||
| Wrong type/stale metadata | Loader PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
|
||||
| Wrong type/stale metadata | Finalizer PackedScene/probe validation | Terminal missing; probe freed | Rebuild cache and reset |
|
||||
| Shutdown while pending | Loader drains snapshot | Clear transient, then full cache release | New loader starts absent |
|
||||
|
||||
## Configuration and capabilities
|
||||
@@ -184,8 +188,9 @@ in the loader; the state emits no logs.
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Cached WMO PackedScene state | Implemented extraction | Lifecycle/source/timing and shutdown verifiers | Asset-backed traversal/leak evidence pending |
|
||||
| Size and cache metadata validation | Preserved in loader | Source boundary and WMO regressions | Oversize/stale asset fixtures could follow |
|
||||
| ResourceLoader I/O and live fallback | Partial/loader-owned | Existing runtime behavior | Separate extraction if justified |
|
||||
| Size admission | Preserved in loader | Source boundary and WMO regressions | Oversize asset fixture could follow |
|
||||
| Terminal I/O and cache validation | Implemented extraction | Finalizer type/lifetime/source/timing verifier | Serialized stale fixture pending |
|
||||
| Live fallback | Loader-owned | Existing runtime behavior | Separate extraction if justified |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
@@ -199,7 +204,8 @@ in the loader; the state emits no logs.
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request state and resets |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, requests, validation, fallback and Node lifetime |
|
||||
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal I/O, probe validation/lifetime and publication |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Paths, file limit, request admission, fallback and placed Nodes |
|
||||
| `src/tools/verify_wmo_scene_resource_cache_state.gd` | State, boundary and timing regression |
|
||||
| `src/tools/verify_render_runtime_cache_shutdown.gd` | Loader final cache ownership regression |
|
||||
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
# WMO Scene Resource Finalizer
|
||||
|
||||
## Metadata
|
||||
|
||||
| Field | Value |
|
||||
|---|---|
|
||||
| Status | Implemented extraction |
|
||||
| Target/work package | M03 / `M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001` |
|
||||
| Owners | Cached WMO PackedScene terminal polling, validation-probe lifetime and scene/missing publication |
|
||||
| Last verified | Worktree `work/sindo-main-codex/m03-wmo-scene-resource-finalizer`, 2026-07-18 |
|
||||
| Profiles/capabilities | Profile-independent cached WMO `.tscn` finalization |
|
||||
|
||||
## Purpose
|
||||
|
||||
Finalize pending cached WMO `.tscn` requests outside `StreamingWorldLoader`:
|
||||
poll terminal status, retrieve a PackedScene, instantiate one validation probe,
|
||||
apply existing WMOBuilder cache metadata rules, release the probe and publish
|
||||
either the exact scene or the historical missing outcome.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Select paths, measure file size or start ResourceLoader requests.
|
||||
- Instantiate placed WMO scenes or execute live WMOBuilder fallback.
|
||||
- Own cache lifetime, placements, build queues, permits or attached Nodes.
|
||||
- Change cache metadata/version rules, fallback order or visible behavior.
|
||||
|
||||
## Context and boundaries
|
||||
|
||||
```mermaid
|
||||
flowchart LR
|
||||
Loader[StreamingWorldLoader] -->|compose and tick| Finalizer[WmoSceneResourceFinalizer]
|
||||
Cache[WmoSceneResourceCacheState] -->|pending snapshot| Finalizer
|
||||
Finalizer --> ResourceLoader
|
||||
Finalizer --> Probe[Temporary Node3D probe]
|
||||
Probe --> Validator[WMOBuilder cache validator]
|
||||
Finalizer -->|PackedScene or missing| Cache
|
||||
Cache --> Fallback[Placed cached scene or live fallback]
|
||||
```
|
||||
|
||||
The service may depend on ResourceLoader, PackedScene/Node3D lifetime and the
|
||||
injected cache validator. FileAccess, size policy, placement, attached SceneTree,
|
||||
scheduler and application layers remain outside it.
|
||||
|
||||
## Public API
|
||||
|
||||
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||
|---|---|---|---|---|
|
||||
| `poll_terminal_requests(cache_state)` | Command/query | Poll every pending scene path once and publish terminal outcomes | Renderer main thread; stateless between calls | Null state returns zero; non-terminal retained |
|
||||
| `is_scene_cache_current(scene)` | Command/query | Instantiate, validate and release one probe | Renderer main thread; call-local Node | Null/no validator/wrong root/stale false |
|
||||
| `load_threaded_get_status(path)` | Boundary query | Read threaded status | Renderer main thread; injectable tests | ResourceLoader semantics |
|
||||
| `load_threaded_get(path)` | Boundary query | Retrieve terminal Resource | Renderer main thread; injectable tests | Null/wrong type rejected |
|
||||
|
||||
## Inputs and outputs
|
||||
|
||||
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||
|---|---|---|---|---|---|
|
||||
| Input | Detached normalized-path to `.tscn` path Dictionary | Scene cache state | Finalizer polling | Caller-owned copy | One pass |
|
||||
| Input | Opaque load status/terminal Resource | ResourceLoader | Finalizer | Value/borrowed Resource | One path |
|
||||
| Input | Cached WMO PackedScene | ResourceLoader | Probe validator | Borrowed reference | One completion |
|
||||
| Input | Metadata validator | Loader composition | Finalizer | Borrowed Object | Finalizer lifetime |
|
||||
| Output | Exact validated PackedScene | Finalizer | Scene cache state | Cache adopts strong reference | Until full clear |
|
||||
| Output | Missing transition | Finalizer | Scene cache state | Path-only state | Until transient clear |
|
||||
| Side effect | Temporary validation Node | PackedScene/finalizer | Validator | Finalizer-owned | Freed before return |
|
||||
|
||||
## Data flow
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
Snapshot[Detached pending snapshot] --> Poll[Poll next status in insertion order]
|
||||
Poll --> Terminal{Loaded or failed?}
|
||||
Terminal -->|no| Retain[Retain pending]
|
||||
Terminal -->|failed| Missing[Complete missing]
|
||||
Terminal -->|loaded| Get[Get Resource]
|
||||
Get --> Type{PackedScene?}
|
||||
Type -->|no| Missing
|
||||
Type -->|yes| Instantiate[Instantiate one probe]
|
||||
Instantiate --> Root{Node3D root?}
|
||||
Root -->|no| FreeRejected[Free rejected Node] --> Missing
|
||||
Root -->|yes| Validate[WMOBuilder metadata validation]
|
||||
Validate --> Free[Free Node3D probe]
|
||||
Free --> Current{Current?}
|
||||
Current -->|yes| Adopt[Adopt exact PackedScene]
|
||||
Current -->|no| Missing
|
||||
```
|
||||
|
||||
## Lifecycle/state
|
||||
|
||||
```mermaid
|
||||
stateDiagram-v2
|
||||
[*] --> Polling
|
||||
Polling --> Pending: non-terminal
|
||||
Polling --> Probing: loaded PackedScene
|
||||
Polling --> Missing: failed/null/wrong type
|
||||
Probing --> Missing: wrong root or stale metadata
|
||||
Probing --> Cached: current metadata
|
||||
Probing --> Released: probe freed before outcome
|
||||
Released --> Missing
|
||||
Released --> Cached
|
||||
Pending --> Polling: later tick
|
||||
```
|
||||
|
||||
The finalizer retains no request or Node. Cache state owns Pending/Cached/Missing
|
||||
lifetime; every instantiated probe is synchronously released before return.
|
||||
|
||||
## Main sequence
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant Loader as StreamingWorldLoader
|
||||
participant Finalizer as WmoSceneResourceFinalizer
|
||||
participant Cache as WmoSceneResourceCacheState
|
||||
participant RL as ResourceLoader
|
||||
participant Validator as WMOBuilder validator
|
||||
Loader->>Finalizer: poll_terminal_requests(Cache)
|
||||
Finalizer->>Cache: request_paths_snapshot()
|
||||
loop insertion-ordered paths
|
||||
Finalizer->>RL: load_threaded_get_status(path)
|
||||
alt failed
|
||||
Finalizer->>Cache: complete_request_as_missing(path)
|
||||
else loaded
|
||||
Finalizer->>RL: load_threaded_get(path)
|
||||
Finalizer->>Finalizer: instantiate Node3D probe
|
||||
Finalizer->>Validator: is_scene_cache_current(probe)
|
||||
Finalizer->>Finalizer: free probe
|
||||
Finalizer->>Cache: complete with exact scene or missing
|
||||
else non-terminal
|
||||
Finalizer->>Finalizer: retain request
|
||||
end
|
||||
end
|
||||
```
|
||||
|
||||
## Dependency diagram
|
||||
|
||||
```mermaid
|
||||
flowchart TB
|
||||
Loader[StreamingWorldLoader] --> Finalizer[WmoSceneResourceFinalizer]
|
||||
Loader --> Cache[WmoSceneResourceCacheState]
|
||||
Finalizer --> Cache
|
||||
Finalizer --> ResourceLoader
|
||||
Finalizer --> PackedScene
|
||||
Finalizer --> Node3D
|
||||
Finalizer --> Validator[WMOBuilder cache validator]
|
||||
Finalizer -. no dependency .-> FileAccess
|
||||
Finalizer -. no dependency .-> Placement
|
||||
Finalizer -. no dependency .-> Scheduler
|
||||
```
|
||||
|
||||
## Ownership, threading and resources
|
||||
|
||||
- Renderer main thread serializes polling, instantiation and cache publication.
|
||||
- Finalizer owns every call-local validation probe and frees it before return.
|
||||
- Non-Node3D rejected roots are also freed, closing the prior leak edge.
|
||||
- Cache state adopts accepted exact PackedScene references until full clear.
|
||||
- Loader owns admission/size policy, live fallback, placed Nodes and shutdown order.
|
||||
|
||||
## Errors, cancellation and recovery
|
||||
|
||||
| Failure/state | Detection | Behavior | Diagnostic | Recovery |
|
||||
|---|---|---|---|---|
|
||||
| Null cache state | Guard | Return zero | Contract verifier | Correct composition |
|
||||
| Non-terminal | Status | Retain pending | Pending count | Poll later |
|
||||
| Failed/null/wrong Resource | Status/type | Complete missing | Synthetic fixture | Reset/rebuild cache |
|
||||
| Non-Node3D root | Probe type | Free probe; complete missing | Type/lifetime fixture | Rebuild cache |
|
||||
| Stale metadata | Injected validator | Free probe; complete missing | Validation fixture | Rebuild/reset |
|
||||
| Shutdown | Loader lifecycle | Drain then clear cache | Shutdown verifier | New loader starts absent |
|
||||
|
||||
## Configuration and capabilities
|
||||
|
||||
The loader injects the existing WMOBuilder cache validator. `wmo_cache_dir`,
|
||||
`wmo_max_runtime_scene_mb`, debug logging, request timing and WMO budgets remain
|
||||
loader-owned. No setting, profile or cache version is added.
|
||||
|
||||
## Persistence, cache and migration
|
||||
|
||||
No data is serialized. Existing WMOBuilder metadata/version acceptance is called
|
||||
unchanged before adoption. The rejected-root lifetime fix needs no migration or
|
||||
rebake and does not alter accepted cache output.
|
||||
|
||||
## Diagnostics and observability
|
||||
|
||||
- Poll returns terminal completion count for tests/future metrics.
|
||||
- Existing pending count and renderer `wmobuild` metrics remain unchanged.
|
||||
- No logs are emitted; oversize debug logging remains at loader admission.
|
||||
- Normalized WMO relative path remains the correlation key.
|
||||
|
||||
## Verification
|
||||
|
||||
- `verify_wmo_scene_resource_finalizer.gd`: null/non-terminal, insertion order,
|
||||
failed/load boundary, null/wrong/non-Node3D/stale rejection, current exact
|
||||
identity, accepted/rejected probe release, source ownership and timing.
|
||||
- Scene cache, render finalizer/cache, WMO queue/planner/registry/resolver,
|
||||
shutdown, facade, internal-access and baseline regressions cover neighbors.
|
||||
- Fidelity evidence is exact orchestration extraction plus rejected-root leak
|
||||
repair; no asset-backed visual or original-client parity claim is made.
|
||||
|
||||
## Extension points
|
||||
|
||||
- Legal current/stale serialized `.tscn` fixtures can extend evidence unchanged.
|
||||
- Placed-scene preparation stays separate because it mutates an attached WMO
|
||||
instance rather than a call-local validation probe.
|
||||
|
||||
## Capability status
|
||||
|
||||
| Capability | Status | Evidence | Gap/next step |
|
||||
|---|---|---|---|
|
||||
| Cached WMO scene terminal polling | Implemented extraction | Status/order/source/timing verifier | Asset-backed traversal pending |
|
||||
| Probe validation/lifetime | Implemented extraction | Current/stale/type/free fixtures | Serialized fixture pending |
|
||||
| Scene/missing publication | Implemented extraction | Exact identity/negative fixtures | Asset-backed lifetime/leak run pending |
|
||||
| File-size admission/live fallback | Loader-owned | Existing scene-cache regressions | Separate extraction if justified |
|
||||
|
||||
## Known gaps and risks
|
||||
|
||||
- PackedScene instantiation/metadata validation remains synchronous main-thread work.
|
||||
- No proprietary WMO corpus, serialized stale/oversize fixture, long leak run,
|
||||
traversal p95/p99 or paired original-client capture is included.
|
||||
|
||||
## Source map
|
||||
|
||||
| Path | Responsibility |
|
||||
|---|---|
|
||||
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Terminal polling, probe validation/lifetime and publication |
|
||||
| `src/render/wmo/wmo_scene_resource_cache_state.gd` | Scene/missing/request ownership and resets |
|
||||
| `src/scenes/streaming/streaming_world_loader.gd` | Admission/size policy, live fallback and placed Node ownership |
|
||||
| `addons/mpq_extractor/loaders/wmo_builder.gd` | Existing cache metadata/version validation |
|
||||
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | Terminal/probe/lifetime/source/timing regression |
|
||||
|
||||
## Related decisions and references
|
||||
|
||||
- [`wmo-scene-resource-cache-state.md`](wmo-scene-resource-cache-state.md)
|
||||
- [`wmo-render-resource-finalizer.md`](wmo-render-resource-finalizer.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-m2-animation-playback`, 2026-07-18 |
|
||||
| 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
|
||||
@@ -70,6 +70,12 @@ flowchart LR
|
||||
WmoBuildStep --> Loader
|
||||
Loader --> WmoBuildQueue[WmoRenderBuildQueue]
|
||||
WmoBuildQueue --> Loader
|
||||
Loader --> WmoGroupMaterializer[WmoRenderGroupMaterializer]
|
||||
WmoGroupMaterializer --> Scene
|
||||
Loader --> WmoScenePreparer[WmoRuntimeScenePreparer]
|
||||
WmoScenePreparer --> Scene
|
||||
Loader --> WmoInstanceFactory[WmoSceneInstanceFactory]
|
||||
WmoInstanceFactory --> WmoScenePreparer
|
||||
Native --> Parsed[Parsed tile/model data]
|
||||
Parsed --> Loader
|
||||
Loader --> Scene[SceneTree nodes]
|
||||
@@ -164,9 +170,13 @@ from externally reading/writing loader-private queue, task, cache and tile-state
|
||||
| `WmoPlacementRegistry.add_reference/release_reference/contains/active_count/diagnostic_snapshot/clear` | Internal WMO service | Owns placement-key to tile/global reference sets | Renderer main thread; map session | Empty/unknown/non-owner input is rejected without mutation |
|
||||
| `WmoRenderBuildStepPlanner.plan_step` | Internal pure WMO service | Selects one mesh-first lightweight render-group operation and next cursors | Main/any thread; stateless | Raw integer comparisons are preserved without clamping |
|
||||
| `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 |
|
||||
| `WmoSceneResourceFinalizer` | Internal WMO terminal-I/O service | Polls cached scene requests, validates/frees probes and publishes scene/missing outcomes | Renderer main thread; stateless across calls | Failed/wrong/stale scenes complete missing; probes always released |
|
||||
| `AdtWaterLoadPipelineState` | Internal liquid async-state service | Owns ADT water FIFO/dedupe, active task IDs and mutex result mailbox | Main-thread state; worker result publication | Invalid/duplicate requests rejected; clear does not interrupt workers |
|
||||
| `AdtWaterSceneFinalizer.attach_water_scene` | Internal liquid main-thread service | Builds and attaches one existing-format ADT Water subtree | Main thread; stateless, returned Node tile-owned | Empty/invalid/dry input returns null without attachment |
|
||||
|
||||
@@ -205,6 +215,9 @@ loader configuration remains transitional composition data, not a caller API.
|
||||
| Internal WMO ownership | Resolved placement key and tile/global reference key | Loader / `WmoPlacementRegistry` | Loader create/retain/final-free decisions | Registry-owned String sets; detached diagnostics | Map session or final release |
|
||||
| Internal WMO build step | Mesh/MultiMesh counts and job cursors | Loader / `WmoRenderBuildStepPlanner` | Loader materialization/cursor adapter | Fresh scalar Dictionary | One group operation |
|
||||
| Internal WMO 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 |
|
||||
@@ -412,13 +425,21 @@ sequenceDiagram
|
||||
reference sets. `WmoRenderBuildStepPlanner` owns only a call-local operation
|
||||
and cursor plan. `WmoRenderBuildQueue` owns typed pending jobs, FIFO keys and
|
||||
strong root/resource references without freeing engine objects.
|
||||
`WmoRenderGroupMaterializer` owns indexed MeshInstance3D/MultiMeshInstance3D
|
||||
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`
|
||||
similarly owns validated PackedScenes, negative entries and pending `.tscn`
|
||||
paths. The loader retains request admission, PackedScene/FileAccess I/O, size
|
||||
and scene-cache validation, live fallback, materialization, permits, validity
|
||||
reactions and every Node lifecycle action.
|
||||
paths; `WmoSceneResourceFinalizer` owns terminal ResourceLoader I/O and
|
||||
validation-probe lifetime. The loader retains request admission, FileAccess
|
||||
size checks, live fallback, Mesh finalization, permits, Editor ownership,
|
||||
validity reactions and every placed-Node lifecycle action.
|
||||
- `AdtWaterLoadPipelineState` owns pending request order/deduplication, opaque
|
||||
active task IDs and the worker-safe parsed-result mailbox. The loader retains
|
||||
WorkerThreadPool start/wait, ADTLoader parsing, concurrency/finalize permits,
|
||||
@@ -556,6 +577,15 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
transition, completion/raw integer behavior, source ownership and bounded timing.
|
||||
- WMO render build queue contract: typed references/cursors, FIFO, duplicate
|
||||
replacement, stale-front cleanup, cancel/clear engine lifetime and bounded timing.
|
||||
- WMO render group materializer contract: exact Resource identity, indexed and
|
||||
fallback names/transforms, render settings, attachment, source ownership and
|
||||
bounded main-thread timing.
|
||||
- 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.
|
||||
@@ -615,10 +645,15 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| WMO placement resolver | Implemented extraction | Scene-free path/identity/transform/source/timing contract | Asset-backed comparison pending |
|
||||
| WMO placement registry | Implemented extraction | Scene-free ownership/lifecycle/source/timing contract | Build/resource state and asset-backed cross-tile corpus pending |
|
||||
| WMO render build step planner | Implemented extraction | Scene-free order/cursor/source/timing contract | Asset-backed traversal p95/p99 pending |
|
||||
| WMO render build queue | Implemented extraction | Typed lifecycle/order/ownership/source/timing contract | Materialization and asset-backed traversal/leak evidence pending |
|
||||
| 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 | ResourceLoader/live-fallback extraction and asset-backed traversal/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 |
|
||||
| WMO scene Resource finalizer | Implemented extraction | Status/order/type/probe/lifetime/adoption/source/timing contract | Serialized stale/oversize and asset-backed evidence pending |
|
||||
| WMO runtime Mesh finalizer | Implemented extraction | Identity/version/material-definition/source/timing contract | Asset-backed visual/leak/GPU/p95/p99 evidence pending |
|
||||
| WMO rendering | Partial | Cached group rendering | Portals/rooms/material parity |
|
||||
| ADT water load pipeline state | Implemented extraction | Scene-free FIFO/task/thread/source/timing contract | Parse/finalization and asset-backed traversal/leak evidence pending |
|
||||
| Liquids | Partial | MH2O/MLIQ paths | LiquidType/depth/shore fidelity |
|
||||
@@ -694,6 +729,11 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/render/m2/m2_native_animation_resource_observer.gd` | Native candidate/read/build/cache observation |
|
||||
| `src/render/m2/m2_prototype_cache_state.gd` | Detached prototype ownership and negative lookup outcomes |
|
||||
| `src/render/wmo/wmo_render_resource_finalizer.gd` | Lightweight WMO terminal polling, validation and publication |
|
||||
| `src/render/wmo/wmo_scene_resource_finalizer.gd` | Cached WMO terminal polling, probe validation/lifetime and publication |
|
||||
| `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 |
|
||||
@@ -729,6 +769,11 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
||||
| `src/tools/verify_m2_native_animation_resource_observer.gd` | Native candidate/cache/raw/build/adoption/boundary/timing regression |
|
||||
| `src/tools/verify_m2_prototype_cache_state.gd` | M2 prototype identity/negative/lifecycle/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_render_resource_finalizer.gd` | WMO render status/order/validation/adoption/boundary/timing regression |
|
||||
| `src/tools/verify_wmo_scene_resource_finalizer.gd` | WMO scene status/order/probe/lifetime/adoption/boundary/timing regression |
|
||||
| `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 |
|
||||
@@ -740,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 |
|
||||
|
||||
@@ -6,6 +6,29 @@ Godot project scanning is disabled for this directory by `.gdignore`. Reference
|
||||
assets remain available to Git, text search and external tooling, but Godot must
|
||||
not generate `.import` sidecars inside nested reference repositories.
|
||||
|
||||
## Git reference revisions
|
||||
|
||||
Remote branches were fetched with pruning and the checked-out default branches
|
||||
were fast-forwarded on 2026-09-05. The parent repository pins the exact commits
|
||||
through gitlinks; `.gitmodules` records the branch used for an intentional future
|
||||
`git submodule update --remote`. Inspect local state before advancing the parent
|
||||
gitlink pins; checked-out branches in this refresh used `git pull --ff-only`.
|
||||
|
||||
| Reference | Canonical branch | Pinned commit |
|
||||
| --- | --- | --- |
|
||||
| `open-realm` | `main` | `950a1e8c1343a6e2139cc448f779cb2957a8d3f8` |
|
||||
| `whoa` | `master` | `ea1345636045635ac601b7d525eb0c38c6c0f6dd` |
|
||||
| `WoWee` | `master` | `607ea3b8369851014721416293f8e95dfbe64eec` |
|
||||
| `WowUnreal` | `main` | `c2a4b9827b2e7672e799085c5bf631d37c4ca5d3` |
|
||||
| `wow.export` | `main` | `c2fd7bde36a712be78a5da896c995b84fbfa2545` |
|
||||
| `benilla` | `main` | `bc1a2428dd7e00ca8abbfb8f0bf53750dae7b123` |
|
||||
| `blender-wow-studio/.../pywowlib` | `master` | `55276dc5c2195da7fe136638a2a59716622f8c65` |
|
||||
|
||||
`blender-wow-studio-3.4-1.1.0_Experimental` and `noggit-red` are tracked
|
||||
snapshots, not top-level gitlinks. They do not have a local branch and require a
|
||||
separate, provenance-checked snapshot refresh instead of a blind pull. The
|
||||
embedded `pywowlib` gitlink is maintained independently as listed above.
|
||||
|
||||
- `blender-wow-studio-3.4-1.1.0_Experimental/` - Blender add-on sources for M2/WMO import/export reference.
|
||||
- `wow.export/` - wow.export source tree and Blender OBJ importer reference.
|
||||
- `noggit-red/` - Noggit RED source tree from `https://gitlab.com/dirtbikercj/noggit-red`, used as a reference for WoW 3.3.5 map editing, ADT/WMO/M2 placement behavior, UID handling and editor workflows.
|
||||
|
||||
+1
-1
Submodule reference/WoWee updated: 626243e937...607ea3b836
Submodule
+1
Submodule reference/benilla added at bc1a2428dd
+1
-1
Submodule reference/open-realm updated: c7ed0545c8...950a1e8c13
+1
-1
Submodule reference/whoa updated: 74bc963a1c...ea13456360
+1
-1
Submodule reference/wow.export updated: 7f8d4f8784...c2fd7bde36
@@ -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,101 @@
|
||||
class_name WmoRenderGroupMaterializer
|
||||
extends RefCounted
|
||||
|
||||
## Creates and attaches one lightweight cached WMO render group. Resource
|
||||
## finalization, build-step selection and queue progress remain caller-owned.
|
||||
## All methods mutate SceneTree nodes and must run on the renderer main thread.
|
||||
|
||||
|
||||
## Creates one MeshInstance3D for [param group_index], applies the indexed name
|
||||
## and optional transform contracts, then attaches it to [param wmo_parent_root].
|
||||
## The parent owns the returned node; the exact Mesh identity is retained.
|
||||
## Null/invalid parents, null meshes and negative indices return null.
|
||||
func materialize_mesh_group(
|
||||
wmo_parent_root: Node3D,
|
||||
mesh: Mesh,
|
||||
group_names: PackedStringArray,
|
||||
group_transforms: Array,
|
||||
group_index: int,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> MeshInstance3D:
|
||||
if (
|
||||
wmo_parent_root == null
|
||||
or not is_instance_valid(wmo_parent_root)
|
||||
or mesh == null
|
||||
or group_index < 0
|
||||
):
|
||||
return null
|
||||
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = (
|
||||
group_names[group_index]
|
||||
if group_index < group_names.size()
|
||||
else "Group_%d" % group_index
|
||||
)
|
||||
mesh_instance.mesh = mesh
|
||||
if group_index < group_transforms.size():
|
||||
mesh_instance.transform = group_transforms[group_index]
|
||||
_apply_render_settings(
|
||||
mesh_instance,
|
||||
visibility_range_end,
|
||||
visibility_range_end_margin,
|
||||
cast_shadows
|
||||
)
|
||||
wmo_parent_root.add_child(mesh_instance)
|
||||
return mesh_instance
|
||||
|
||||
|
||||
## Creates one MultiMeshInstance3D for [param group_index], applies the indexed
|
||||
## name and optional transform contracts, then attaches it to the supplied root.
|
||||
## The parent owns the returned node; the exact MultiMesh identity is retained.
|
||||
## Null/invalid parents, null MultiMeshes and negative indices return null.
|
||||
func materialize_multimesh_group(
|
||||
wmo_parent_root: Node3D,
|
||||
multimesh: MultiMesh,
|
||||
group_names: PackedStringArray,
|
||||
group_transforms: Array,
|
||||
group_index: int,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> MultiMeshInstance3D:
|
||||
if (
|
||||
wmo_parent_root == null
|
||||
or not is_instance_valid(wmo_parent_root)
|
||||
or multimesh == null
|
||||
or group_index < 0
|
||||
):
|
||||
return null
|
||||
|
||||
var multimesh_instance := MultiMeshInstance3D.new()
|
||||
multimesh_instance.name = (
|
||||
group_names[group_index]
|
||||
if group_index < group_names.size()
|
||||
else "DoodadGroup_%d" % group_index
|
||||
)
|
||||
multimesh_instance.multimesh = multimesh
|
||||
if group_index < group_transforms.size():
|
||||
multimesh_instance.transform = group_transforms[group_index]
|
||||
_apply_render_settings(
|
||||
multimesh_instance,
|
||||
visibility_range_end,
|
||||
visibility_range_end_margin,
|
||||
cast_shadows
|
||||
)
|
||||
wmo_parent_root.add_child(multimesh_instance)
|
||||
return multimesh_instance
|
||||
|
||||
|
||||
func _apply_render_settings(
|
||||
geometry_instance: GeometryInstance3D,
|
||||
visibility_range_end: float,
|
||||
visibility_range_end_margin: float,
|
||||
cast_shadows: bool) -> void:
|
||||
geometry_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if visibility_range_end > 0.0:
|
||||
geometry_instance.visibility_range_end = visibility_range_end
|
||||
geometry_instance.visibility_range_end_margin = visibility_range_end_margin
|
||||
@@ -0,0 +1 @@
|
||||
uid://cu4tw6868rbkm
|
||||
@@ -0,0 +1,105 @@
|
||||
class_name WmoRuntimeMeshFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Refreshes cached WMO runtime Mesh materials through the configured builder.
|
||||
## Scene traversal, placement and Node lifetime remain caller-owned.
|
||||
|
||||
const MATERIAL_REFRESH_VERSION := 10
|
||||
const MATERIAL_REFRESH_VERSION_METADATA := "wow_wmo_material_refresh_version"
|
||||
|
||||
var _wmo_material_builder: Object
|
||||
|
||||
|
||||
func _init(wmo_material_builder: Object) -> void:
|
||||
_wmo_material_builder = wmo_material_builder
|
||||
|
||||
|
||||
## Finalizes a stale Mesh in place and returns the exact input Resource identity.
|
||||
## Null remains null; already-current meshes do not cross the builder boundary.
|
||||
func finalize_mesh(mesh: Mesh, extracted_directory: String) -> Mesh:
|
||||
if mesh == null:
|
||||
return null
|
||||
if int(mesh.get_meta(MATERIAL_REFRESH_VERSION_METADATA, 0)) >= MATERIAL_REFRESH_VERSION:
|
||||
return mesh
|
||||
mesh.set_meta(MATERIAL_REFRESH_VERSION_METADATA, MATERIAL_REFRESH_VERSION)
|
||||
if not (mesh is ArrayMesh):
|
||||
return mesh
|
||||
|
||||
var array_mesh := mesh as ArrayMesh
|
||||
for surface_index in range(array_mesh.get_surface_count()):
|
||||
var rebuilt_material := rebuild_cached_material(
|
||||
array_mesh.surface_get_material(surface_index),
|
||||
extracted_directory
|
||||
)
|
||||
if rebuilt_material != null:
|
||||
array_mesh.surface_set_material(surface_index, rebuilt_material)
|
||||
return mesh
|
||||
|
||||
|
||||
## Rebuilds a material carrying the WMO cache metadata contract. Materials
|
||||
## without texture0 metadata and failed builder results remain unchanged by the caller.
|
||||
func rebuild_cached_material(
|
||||
material: Material,
|
||||
extracted_directory: String
|
||||
) -> Material:
|
||||
if (
|
||||
material == null
|
||||
or not material.has_meta("texture0_path")
|
||||
or _wmo_material_builder == null
|
||||
):
|
||||
return null
|
||||
|
||||
var texture_paths := PackedStringArray()
|
||||
var texture0_index := _append_texture_path(
|
||||
texture_paths,
|
||||
String(material.get_meta("texture0_path", ""))
|
||||
)
|
||||
var texture1_index := _append_texture_path(
|
||||
texture_paths,
|
||||
String(material.get_meta("texture1_path", ""))
|
||||
)
|
||||
var texture2_index := _append_texture_path(
|
||||
texture_paths,
|
||||
String(material.get_meta("texture2_path", ""))
|
||||
)
|
||||
|
||||
var diffuse_color := Color.WHITE
|
||||
var emissive_color := Color.BLACK
|
||||
var secondary_color := Color.WHITE
|
||||
if material is ShaderMaterial:
|
||||
var shader_material := material as ShaderMaterial
|
||||
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
|
||||
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
|
||||
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
|
||||
if diffuse_value is Color:
|
||||
diffuse_color = diffuse_value
|
||||
if emissive_value is Color:
|
||||
emissive_color = emissive_value
|
||||
if secondary_value is Color:
|
||||
secondary_color = secondary_value
|
||||
|
||||
var material_definition := {
|
||||
"texture0": texture0_index,
|
||||
"texture1": texture1_index,
|
||||
"texture2": texture2_index,
|
||||
"flags": int(material.get_meta("wow_flags", 0)),
|
||||
"shader": int(material.get_meta("wow_shader", 0)),
|
||||
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
|
||||
"diffuse_color": diffuse_color,
|
||||
"emissive_color": emissive_color,
|
||||
"color2": secondary_color,
|
||||
}
|
||||
return _wmo_material_builder.call(
|
||||
"_build_material",
|
||||
material_definition,
|
||||
texture_paths,
|
||||
extracted_directory
|
||||
) as Material
|
||||
|
||||
|
||||
func _append_texture_path(texture_paths: PackedStringArray, texture_path: String) -> int:
|
||||
if texture_path.is_empty():
|
||||
return -1
|
||||
var texture_index := texture_paths.size()
|
||||
texture_paths.append(texture_path)
|
||||
return texture_index
|
||||
@@ -0,0 +1 @@
|
||||
uid://bsxqkjuk77cgf
|
||||
@@ -0,0 +1,80 @@
|
||||
class_name WmoRuntimeScenePreparer
|
||||
extends RefCounted
|
||||
|
||||
## Applies the existing cached/live WMO subtree preparation rules on the
|
||||
## renderer main thread. Placement, attachment and subtree lifetime stay caller-owned.
|
||||
|
||||
var _runtime_mesh_finalizer: Object
|
||||
|
||||
|
||||
func _init(runtime_mesh_finalizer: Object) -> void:
|
||||
_runtime_mesh_finalizer = runtime_mesh_finalizer
|
||||
|
||||
|
||||
## Prepares an instantiated cached WMO scene by finalizing every borrowed Mesh,
|
||||
## then applying the historical direct Occluders-child and shadow policies.
|
||||
## Returns false for a null/freed root without mutating or retaining anything.
|
||||
func prepare_cached_instance(
|
||||
instance: Node3D,
|
||||
extracted_directory: String,
|
||||
enable_occlusion_culling: bool,
|
||||
cast_shadows: bool) -> bool:
|
||||
if instance == null or not is_instance_valid(instance):
|
||||
return false
|
||||
_finalize_meshes_in_subtree(instance, extracted_directory)
|
||||
_apply_runtime_render_policy(instance, enable_occlusion_culling, cast_shadows)
|
||||
return true
|
||||
|
||||
|
||||
## Prepares a duplicated live-built WMO without re-finalizing its Meshes.
|
||||
## Returns false for a null/freed root without mutating or retaining anything.
|
||||
func prepare_live_instance(
|
||||
instance: Node3D,
|
||||
enable_occlusion_culling: bool,
|
||||
cast_shadows: bool) -> bool:
|
||||
if instance == null or not is_instance_valid(instance):
|
||||
return false
|
||||
_apply_runtime_render_policy(instance, enable_occlusion_culling, cast_shadows)
|
||||
return true
|
||||
|
||||
|
||||
func _finalize_meshes_in_subtree(node: Node, extracted_directory: String) -> void:
|
||||
if _runtime_mesh_finalizer != null:
|
||||
if node is MeshInstance3D:
|
||||
_runtime_mesh_finalizer.call(
|
||||
"finalize_mesh",
|
||||
(node as MeshInstance3D).mesh,
|
||||
extracted_directory
|
||||
)
|
||||
elif node is MultiMeshInstance3D:
|
||||
var multimesh := (node as MultiMeshInstance3D).multimesh
|
||||
if multimesh != null:
|
||||
_runtime_mesh_finalizer.call(
|
||||
"finalize_mesh",
|
||||
multimesh.mesh,
|
||||
extracted_directory
|
||||
)
|
||||
for child in node.get_children():
|
||||
_finalize_meshes_in_subtree(child, extracted_directory)
|
||||
|
||||
|
||||
func _apply_runtime_render_policy(
|
||||
instance: Node3D,
|
||||
enable_occlusion_culling: bool,
|
||||
cast_shadows: bool) -> void:
|
||||
if not enable_occlusion_culling:
|
||||
var occluders := instance.get_node_or_null("Occluders")
|
||||
if occluders != null:
|
||||
instance.remove_child(occluders)
|
||||
occluders.queue_free()
|
||||
if cast_shadows:
|
||||
_enable_shadow_casting_recursive(instance)
|
||||
|
||||
|
||||
func _enable_shadow_casting_recursive(node: Node) -> void:
|
||||
if node is GeometryInstance3D:
|
||||
(node as GeometryInstance3D).cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
)
|
||||
for child in node.get_children():
|
||||
_enable_shadow_casting_recursive(child)
|
||||
@@ -0,0 +1 @@
|
||||
uid://d1t0vlkco8kw8
|
||||
@@ -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
|
||||
@@ -0,0 +1,97 @@
|
||||
class_name WmoSceneResourceFinalizer
|
||||
extends RefCounted
|
||||
|
||||
## Polls cached WMO PackedScene requests and publishes validated outcomes.
|
||||
## File-size admission, live fallback and attached Node lifetime remain caller-owned.
|
||||
|
||||
var _scene_cache_validator: Object
|
||||
var _resource_loader_adapter: Object
|
||||
|
||||
|
||||
func _init(
|
||||
scene_cache_validator: Object,
|
||||
resource_loader_adapter: Object = null
|
||||
) -> void:
|
||||
_scene_cache_validator = scene_cache_validator
|
||||
_resource_loader_adapter = resource_loader_adapter
|
||||
|
||||
|
||||
## Polls a detached pending snapshot in insertion order. Terminal requests are
|
||||
## published as the exact validated PackedScene or the historical missing state.
|
||||
func poll_terminal_requests(scene_resource_cache_state: RefCounted) -> int:
|
||||
if scene_resource_cache_state == null:
|
||||
return 0
|
||||
var completed_request_count := 0
|
||||
var request_paths: Dictionary = scene_resource_cache_state.call(
|
||||
"request_paths_snapshot"
|
||||
)
|
||||
for normalized_relative_path_variant in request_paths.keys():
|
||||
var normalized_relative_path := String(normalized_relative_path_variant)
|
||||
var resource_path := String(request_paths[normalized_relative_path_variant])
|
||||
var load_status := load_threaded_get_status(resource_path)
|
||||
if (
|
||||
load_status != ResourceLoader.THREAD_LOAD_LOADED
|
||||
and load_status != ResourceLoader.THREAD_LOAD_FAILED
|
||||
):
|
||||
continue
|
||||
if load_status != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
scene_resource_cache_state.call(
|
||||
"complete_request_as_missing",
|
||||
normalized_relative_path
|
||||
)
|
||||
completed_request_count += 1
|
||||
continue
|
||||
var loaded_resource := load_threaded_get(resource_path)
|
||||
var loaded_scene := loaded_resource as PackedScene
|
||||
if is_scene_cache_current(loaded_scene):
|
||||
scene_resource_cache_state.call(
|
||||
"complete_request_with_scene",
|
||||
normalized_relative_path,
|
||||
loaded_scene
|
||||
)
|
||||
else:
|
||||
scene_resource_cache_state.call(
|
||||
"complete_request_as_missing",
|
||||
normalized_relative_path
|
||||
)
|
||||
completed_request_count += 1
|
||||
return completed_request_count
|
||||
|
||||
|
||||
## Instantiates one Node3D probe, delegates the existing WMOBuilder metadata
|
||||
## check and synchronously releases the probe before returning.
|
||||
func is_scene_cache_current(scene: PackedScene) -> bool:
|
||||
if scene == null or _scene_cache_validator == null:
|
||||
return false
|
||||
var instantiated_node := scene.instantiate()
|
||||
if not (instantiated_node is Node3D):
|
||||
if instantiated_node != null:
|
||||
instantiated_node.free()
|
||||
return false
|
||||
var instance := instantiated_node as Node3D
|
||||
var is_current := bool(_scene_cache_validator.call(
|
||||
"is_scene_cache_current",
|
||||
instance
|
||||
))
|
||||
instance.free()
|
||||
return is_current
|
||||
|
||||
|
||||
## Production ResourceLoader status boundary; injectable for synthetic tests.
|
||||
func load_threaded_get_status(resource_path: String) -> int:
|
||||
if _resource_loader_adapter != null:
|
||||
return int(_resource_loader_adapter.call(
|
||||
"load_threaded_get_status",
|
||||
resource_path
|
||||
))
|
||||
return ResourceLoader.load_threaded_get_status(resource_path)
|
||||
|
||||
|
||||
## Production ResourceLoader result boundary; injectable for synthetic tests.
|
||||
func load_threaded_get(resource_path: String) -> Resource:
|
||||
if _resource_loader_adapter != null:
|
||||
return _resource_loader_adapter.call(
|
||||
"load_threaded_get",
|
||||
resource_path
|
||||
) as Resource
|
||||
return ResourceLoader.load_threaded_get(resource_path)
|
||||
@@ -0,0 +1 @@
|
||||
uid://cp2r3tadn8l6q
|
||||
@@ -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 := []
|
||||
|
||||
@@ -30,6 +30,21 @@ const WMO_RENDER_RESOURCE_FINALIZER_SCRIPT := preload(
|
||||
const WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
)
|
||||
const WMO_SCENE_RESOURCE_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_resource_finalizer.gd"
|
||||
)
|
||||
const WMO_RUNTIME_MESH_FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
const WMO_RENDER_GROUP_MATERIALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
)
|
||||
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")
|
||||
@@ -120,7 +135,6 @@ const RENDER_BUDGET_SCHEDULER_SCRIPT := preload("res://src/render/streaming/rend
|
||||
const REQUIRED_BAKED_TILE_FORMAT_VERSION := 5
|
||||
const REQUIRED_SPLAT_TILE_FORMAT_VERSION := 1
|
||||
const REQUIRED_CONTROL_SPLAT_TILE_FORMAT_VERSION := 3
|
||||
const WMO_MATERIAL_REFRESH_VERSION := 10
|
||||
const RENDER_GROUND_QUERY_RAY_HEIGHT_UNITS := 5000.0
|
||||
|
||||
const TILE_SIZE := COORDINATE_MAPPER_SCRIPT.ADT_TILE_SIZE_YARDS
|
||||
@@ -370,8 +384,22 @@ var _wmo_render_resource_finalizer := WMO_RENDER_RESOURCE_FINALIZER_SCRIPT.new(
|
||||
)
|
||||
var _wmo_render_build_queue := WMO_RENDER_BUILD_QUEUE_SCRIPT.new()
|
||||
var _wmo_scene_resource_cache_state := WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()
|
||||
var _wmo_scene_resource_finalizer := WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new(
|
||||
WMO_BUILDER_SCRIPT
|
||||
)
|
||||
var _wmo_runtime_mesh_finalizer := WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new(
|
||||
WMO_BUILDER_SCRIPT
|
||||
)
|
||||
var _wmo_render_group_materializer := WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()
|
||||
var _wmo_runtime_scene_preparer := WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new(
|
||||
_wmo_runtime_mesh_finalizer
|
||||
)
|
||||
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()
|
||||
@@ -3671,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)
|
||||
@@ -3711,24 +3745,9 @@ func _process_wmo_build_jobs() -> void:
|
||||
|
||||
|
||||
func _drain_wmo_scene_loads() -> void:
|
||||
var request_paths := _wmo_scene_resource_cache_state.request_paths_snapshot()
|
||||
for normalized_rel_variant in request_paths.keys():
|
||||
var normalized_rel := String(normalized_rel_variant)
|
||||
var path := String(request_paths[normalized_rel])
|
||||
var status := ResourceLoader.load_threaded_get_status(path)
|
||||
if status != ResourceLoader.THREAD_LOAD_LOADED and status != ResourceLoader.THREAD_LOAD_FAILED:
|
||||
continue
|
||||
if status != ResourceLoader.THREAD_LOAD_LOADED:
|
||||
_wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel)
|
||||
continue
|
||||
var resource: Resource = ResourceLoader.load_threaded_get(path)
|
||||
if resource is PackedScene and _is_wmo_scene_cache_current(resource as PackedScene):
|
||||
_wmo_scene_resource_cache_state.complete_request_with_scene(
|
||||
normalized_rel,
|
||||
resource as PackedScene
|
||||
)
|
||||
else:
|
||||
_wmo_scene_resource_cache_state.complete_request_as_missing(normalized_rel)
|
||||
_wmo_scene_resource_finalizer.poll_terminal_requests(
|
||||
_wmo_scene_resource_cache_state
|
||||
)
|
||||
|
||||
|
||||
func _drain_wmo_render_loads() -> void:
|
||||
@@ -3849,26 +3868,19 @@ func _process_wmo_render_build_jobs() -> void:
|
||||
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MESH:
|
||||
var mesh := meshes[selected_index] as Mesh
|
||||
if mesh != null:
|
||||
_refresh_cached_wmo_mesh_materials(mesh)
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = (
|
||||
mesh_names[selected_index]
|
||||
if selected_index < mesh_names.size()
|
||||
else "Group_%d" % selected_index
|
||||
_wmo_runtime_mesh_finalizer.finalize_mesh(mesh, extracted_dir)
|
||||
var mesh_instance := _wmo_render_group_materializer.materialize_mesh_group(
|
||||
root as Node3D,
|
||||
mesh,
|
||||
mesh_names,
|
||||
mesh_transforms,
|
||||
selected_index,
|
||||
wmo_visibility_range,
|
||||
CHUNK_SIZE,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
mesh_instance.mesh = mesh
|
||||
if selected_index < mesh_transforms.size():
|
||||
mesh_instance.transform = mesh_transforms[selected_index]
|
||||
mesh_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if wmo_cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if wmo_visibility_range > 0.0:
|
||||
mesh_instance.visibility_range_end = wmo_visibility_range
|
||||
mesh_instance.visibility_range_end_margin = CHUNK_SIZE
|
||||
(root as Node3D).add_child(mesh_instance)
|
||||
_set_editor_owner_recursive(mesh_instance)
|
||||
if mesh_instance != null:
|
||||
_set_editor_owner_recursive(mesh_instance)
|
||||
_wmo_render_build_queue.adopt_cursors(
|
||||
unique_key,
|
||||
int(build_step["next_mesh_index"]),
|
||||
@@ -3880,26 +3892,21 @@ func _process_wmo_render_build_jobs() -> void:
|
||||
if build_step["operation"] == WMO_RENDER_BUILD_STEP_PLANNER_SCRIPT.OPERATION_MULTIMESH:
|
||||
var multimesh := multimeshes[selected_index] as MultiMesh
|
||||
if multimesh != null:
|
||||
_refresh_cached_wmo_mesh_materials(multimesh.mesh)
|
||||
var multimesh_instance := MultiMeshInstance3D.new()
|
||||
multimesh_instance.name = (
|
||||
multimesh_names[selected_index]
|
||||
if selected_index < multimesh_names.size()
|
||||
else "DoodadGroup_%d" % selected_index
|
||||
_wmo_runtime_mesh_finalizer.finalize_mesh(multimesh.mesh, extracted_dir)
|
||||
var multimesh_instance := (
|
||||
_wmo_render_group_materializer.materialize_multimesh_group(
|
||||
root as Node3D,
|
||||
multimesh,
|
||||
multimesh_names,
|
||||
multimesh_transforms,
|
||||
selected_index,
|
||||
wmo_visibility_range,
|
||||
CHUNK_SIZE,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
)
|
||||
multimesh_instance.multimesh = multimesh
|
||||
if selected_index < multimesh_transforms.size():
|
||||
multimesh_instance.transform = multimesh_transforms[selected_index]
|
||||
multimesh_instance.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if wmo_cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
if wmo_visibility_range > 0.0:
|
||||
multimesh_instance.visibility_range_end = wmo_visibility_range
|
||||
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
|
||||
(root as Node3D).add_child(multimesh_instance)
|
||||
_set_editor_owner_recursive(multimesh_instance)
|
||||
if multimesh_instance != null:
|
||||
_set_editor_owner_recursive(multimesh_instance)
|
||||
_wmo_render_build_queue.adopt_cursors(
|
||||
unique_key,
|
||||
int(build_step["next_mesh_index"]),
|
||||
@@ -3926,123 +3933,13 @@ func _get_resource_file_size(path: String) -> int:
|
||||
return int(length)
|
||||
|
||||
|
||||
func _is_wmo_scene_cache_current(scene: PackedScene) -> bool:
|
||||
if scene == null:
|
||||
return false
|
||||
var instance := scene.instantiate() as Node3D
|
||||
if instance == null:
|
||||
return false
|
||||
var current := _is_wmo_node_cache_current(instance)
|
||||
instance.free()
|
||||
return current
|
||||
|
||||
|
||||
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:
|
||||
_refresh_cached_wmo_materials_recursive(instance)
|
||||
if not enable_occlusion_culling:
|
||||
var occluders := instance.get_node_or_null("Occluders")
|
||||
if occluders != null:
|
||||
instance.remove_child(occluders)
|
||||
occluders.queue_free()
|
||||
if wmo_cast_shadows:
|
||||
_apply_shadow_cast_recursive(instance, true)
|
||||
|
||||
|
||||
func _refresh_cached_wmo_materials_recursive(node: Node) -> void:
|
||||
if node is MeshInstance3D:
|
||||
_refresh_cached_wmo_mesh_materials((node as MeshInstance3D).mesh)
|
||||
elif node is MultiMeshInstance3D:
|
||||
var multimesh := (node as MultiMeshInstance3D).multimesh
|
||||
if multimesh != null:
|
||||
_refresh_cached_wmo_mesh_materials(multimesh.mesh)
|
||||
for child in node.get_children():
|
||||
_refresh_cached_wmo_materials_recursive(child)
|
||||
|
||||
|
||||
func _refresh_cached_wmo_mesh_materials(mesh: Mesh) -> void:
|
||||
if mesh == null:
|
||||
return
|
||||
if int(mesh.get_meta("wow_wmo_material_refresh_version", 0)) >= WMO_MATERIAL_REFRESH_VERSION:
|
||||
return
|
||||
mesh.set_meta("wow_wmo_material_refresh_version", WMO_MATERIAL_REFRESH_VERSION)
|
||||
if not (mesh is ArrayMesh):
|
||||
return
|
||||
|
||||
var array_mesh := mesh as ArrayMesh
|
||||
for surface_idx in array_mesh.get_surface_count():
|
||||
var rebuilt := _rebuild_cached_wmo_material(array_mesh.surface_get_material(surface_idx))
|
||||
if rebuilt != null:
|
||||
array_mesh.surface_set_material(surface_idx, rebuilt)
|
||||
|
||||
|
||||
func _rebuild_cached_wmo_material(material: Material) -> Material:
|
||||
if material == null or not material.has_meta("texture0_path"):
|
||||
return null
|
||||
|
||||
var tex0_path := String(material.get_meta("texture0_path", ""))
|
||||
var tex1_path := String(material.get_meta("texture1_path", ""))
|
||||
var tex2_path := String(material.get_meta("texture2_path", ""))
|
||||
var textures := PackedStringArray()
|
||||
var tex0_index := -1
|
||||
var tex1_index := -1
|
||||
var tex2_index := -1
|
||||
if not tex0_path.is_empty():
|
||||
tex0_index = textures.size()
|
||||
textures.append(tex0_path)
|
||||
if not tex1_path.is_empty():
|
||||
tex1_index = textures.size()
|
||||
textures.append(tex1_path)
|
||||
if not tex2_path.is_empty():
|
||||
tex2_index = textures.size()
|
||||
textures.append(tex2_path)
|
||||
|
||||
var diffuse := Color.WHITE
|
||||
var emissive := Color.BLACK
|
||||
var secondary := Color.WHITE
|
||||
if material is ShaderMaterial:
|
||||
var shader_material := material as ShaderMaterial
|
||||
var diffuse_value: Variant = shader_material.get_shader_parameter("diffuse_color")
|
||||
var emissive_value: Variant = shader_material.get_shader_parameter("emissive_color")
|
||||
var secondary_value: Variant = shader_material.get_shader_parameter("secondary_color")
|
||||
if diffuse_value is Color:
|
||||
diffuse = diffuse_value
|
||||
if emissive_value is Color:
|
||||
emissive = emissive_value
|
||||
if secondary_value is Color:
|
||||
secondary = secondary_value
|
||||
|
||||
var mat_def := {
|
||||
"texture0": tex0_index,
|
||||
"texture1": tex1_index,
|
||||
"texture2": tex2_index,
|
||||
"flags": int(material.get_meta("wow_flags", 0)),
|
||||
"shader": int(material.get_meta("wow_shader", 0)),
|
||||
"blend_mode": int(material.get_meta("wow_blend_mode", 0)),
|
||||
"diffuse_color": diffuse,
|
||||
"emissive_color": emissive,
|
||||
"color2": secondary,
|
||||
}
|
||||
return WMO_BUILDER_SCRIPT._build_material(mat_def, textures, extracted_dir)
|
||||
_wmo_runtime_scene_preparer.prepare_cached_instance(
|
||||
instance,
|
||||
extracted_dir,
|
||||
enable_occlusion_culling,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
|
||||
|
||||
func _cancel_wmo_build_job(tile_key: String) -> void:
|
||||
@@ -4552,18 +4449,6 @@ func _apply_visibility_range_recursive(node: Node, range_end: float) -> void:
|
||||
_apply_visibility_range_recursive(child, range_end)
|
||||
|
||||
|
||||
func _apply_shadow_cast_recursive(node: Node, cast_shadows: bool) -> void:
|
||||
if node is GeometryInstance3D:
|
||||
var geometry := node as GeometryInstance3D
|
||||
geometry.cast_shadow = (
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON
|
||||
if cast_shadows
|
||||
else GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
)
|
||||
for child in node.get_children():
|
||||
_apply_shadow_cast_recursive(child, cast_shadows)
|
||||
|
||||
|
||||
func _strip_occluders_recursive(node: Node) -> void:
|
||||
for child in node.get_children():
|
||||
if child is OccluderInstance3D:
|
||||
@@ -4740,18 +4625,20 @@ 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)
|
||||
if not enable_occlusion_culling:
|
||||
var occluders := instance.get_node_or_null("Occluders")
|
||||
if occluders != null:
|
||||
instance.remove_child(occluders)
|
||||
occluders.queue_free()
|
||||
if wmo_cast_shadows:
|
||||
_apply_shadow_cast_recursive(instance, true)
|
||||
# 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,
|
||||
wmo_cast_shadows
|
||||
)
|
||||
return instance
|
||||
|
||||
|
||||
@@ -4846,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,330 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free WMO render-group Resource identity, indexed presentation,
|
||||
## attachment, ownership-boundary and bounded-timing regression.
|
||||
|
||||
const MATERIALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
)
|
||||
const MATERIALIZER_PATH := "res://src/render/wmo/wmo_render_group_materializer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_mesh_group_contract(failures)
|
||||
_verify_multimesh_group_contract(failures)
|
||||
_verify_fallback_and_zero_visibility_contract(failures)
|
||||
_verify_invalid_inputs(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_RENDER_GROUP_MATERIALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_RENDER_GROUP_MATERIALIZER PASS cases=37 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_mesh_group_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var mesh := ArrayMesh.new()
|
||||
var group_transform := Transform3D(Basis.IDENTITY, Vector3(1.0, 2.0, 3.0))
|
||||
var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
mesh,
|
||||
PackedStringArray(["Exterior", "Interior"]),
|
||||
[Transform3D.IDENTITY, group_transform],
|
||||
1,
|
||||
1800.0,
|
||||
533.33333,
|
||||
false
|
||||
)
|
||||
_expect_true(mesh_instance != null, "valid Mesh group returned", failures)
|
||||
_expect_same(mesh_instance.get_parent(), parent_root, "Mesh group attached once", failures)
|
||||
_expect_equal(parent_root.get_child_count(), 1, "one Mesh parent child", failures)
|
||||
_expect_string_equal(mesh_instance.name, "Interior", "indexed Mesh name", failures)
|
||||
_expect_same(mesh_instance.mesh, mesh, "exact Mesh identity", failures)
|
||||
_expect_true(mesh_instance.transform == group_transform, "indexed Mesh transform", failures)
|
||||
_expect_equal(
|
||||
mesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
||||
"Mesh shadow disabled",
|
||||
failures
|
||||
)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end, 1800.0, "Mesh visibility end", failures)
|
||||
_expect_float_equal(
|
||||
mesh_instance.visibility_range_end_margin,
|
||||
533.33333,
|
||||
"Mesh visibility margin",
|
||||
failures
|
||||
)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_multimesh_group_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.mesh = ArrayMesh.new()
|
||||
multimesh.instance_count = 1
|
||||
var group_transform := Transform3D(Basis.IDENTITY, Vector3(4.0, 5.0, 6.0))
|
||||
var multimesh_instance: MultiMeshInstance3D = (
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root,
|
||||
multimesh,
|
||||
PackedStringArray(["Doodads"]),
|
||||
[group_transform],
|
||||
0,
|
||||
2600.0,
|
||||
400.0,
|
||||
true
|
||||
)
|
||||
)
|
||||
_expect_same(
|
||||
multimesh_instance.get_parent(),
|
||||
parent_root,
|
||||
"MultiMesh group attached once",
|
||||
failures
|
||||
)
|
||||
_expect_string_equal(multimesh_instance.name, "Doodads", "indexed MultiMesh name", failures)
|
||||
_expect_same(multimesh_instance.multimesh, multimesh, "exact MultiMesh identity", failures)
|
||||
_expect_true(
|
||||
multimesh_instance.transform == group_transform,
|
||||
"indexed MultiMesh transform",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
multimesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||
"MultiMesh shadow enabled",
|
||||
failures
|
||||
)
|
||||
_expect_float_equal(
|
||||
multimesh_instance.visibility_range_end,
|
||||
2600.0,
|
||||
"MultiMesh visibility end",
|
||||
failures
|
||||
)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_fallback_and_zero_visibility_contract(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
var mesh_instance: MeshInstance3D = materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
ArrayMesh.new(),
|
||||
PackedStringArray(),
|
||||
[],
|
||||
7,
|
||||
0.0,
|
||||
99.0,
|
||||
true
|
||||
)
|
||||
_expect_string_equal(mesh_instance.name, "Group_7", "Mesh fallback name", failures)
|
||||
_expect_true(mesh_instance.transform == Transform3D.IDENTITY, "missing Mesh transform stays identity", failures)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end, 0.0, "zero Mesh visibility unchanged", failures)
|
||||
_expect_float_equal(mesh_instance.visibility_range_end_margin, 0.0, "disabled Mesh margin unchanged", failures)
|
||||
var multimesh_instance: MultiMeshInstance3D = (
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root,
|
||||
MultiMesh.new(),
|
||||
PackedStringArray(),
|
||||
[],
|
||||
8,
|
||||
0.0,
|
||||
99.0,
|
||||
false
|
||||
)
|
||||
)
|
||||
_expect_string_equal(
|
||||
multimesh_instance.name,
|
||||
"DoodadGroup_8",
|
||||
"MultiMesh fallback name",
|
||||
failures
|
||||
)
|
||||
_expect_equal(parent_root.get_child_count(), 2, "fallback groups attached", failures)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_invalid_inputs(failures: Array[String]) -> void:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
var mesh := ArrayMesh.new()
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
null, mesh, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null Mesh parent rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null Mesh rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root, mesh, PackedStringArray(), [], -1, 0.0, 0.0, true
|
||||
) == null,
|
||||
"negative Mesh index rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_multimesh_group(
|
||||
null, MultiMesh.new(), PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null MultiMesh parent rejected",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
materializer.materialize_multimesh_group(
|
||||
parent_root, null, PackedStringArray(), [], 0, 0.0, 0.0, true
|
||||
) == null,
|
||||
"null MultiMesh rejected",
|
||||
failures
|
||||
)
|
||||
_expect_equal(parent_root.get_child_count(), 0, "invalid inputs do not attach", failures)
|
||||
parent_root.free()
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var materializer_source := FileAccess.get_file_as_string(MATERIALIZER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
var loader_adapter_source := _source_between(
|
||||
loader_source,
|
||||
"func _process_wmo_render_build_jobs()",
|
||||
"func _cancel_wmo_render_build_job(",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_RENDER_GROUP_MATERIALIZER_SCRIPT.new()"),
|
||||
"loader composes WMO group materializer",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_adapter_source.count("_wmo_render_group_materializer.materialize_"),
|
||||
2,
|
||||
"loader delegates both group kinds",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_adapter_source.contains("MeshInstance3D.new()"),
|
||||
"loader releases MeshInstance construction",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_adapter_source.contains("MultiMeshInstance3D.new()"),
|
||||
"loader releases MultiMeshInstance construction",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_adapter_source.count("_set_editor_owner_recursive("),
|
||||
2,
|
||||
"loader retains Editor ownership",
|
||||
failures
|
||||
)
|
||||
for required_rule in [
|
||||
"wmo_parent_root.add_child(mesh_instance)",
|
||||
"wmo_parent_root.add_child(multimesh_instance)",
|
||||
"else \"Group_%d\" % group_index",
|
||||
"else \"DoodadGroup_%d\" % group_index",
|
||||
"geometry_instance.visibility_range_end_margin = visibility_range_end_margin",
|
||||
]:
|
||||
_expect_true(
|
||||
materializer_source.contains(required_rule),
|
||||
"materializer owns %s" % required_rule,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"RenderingServer.",
|
||||
".owner =",
|
||||
"_wmo_render_build_queue",
|
||||
"_render_budget_scheduler",
|
||||
"WmoRuntimeMeshFinalizer",
|
||||
]:
|
||||
_expect_false(
|
||||
materializer_source.contains(forbidden_dependency),
|
||||
"materializer omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var materializer := MATERIALIZER_SCRIPT.new()
|
||||
var parent_root := Node3D.new()
|
||||
get_root().add_child(parent_root)
|
||||
var mesh := ArrayMesh.new()
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for group_index in 1000:
|
||||
materializer.materialize_mesh_group(
|
||||
parent_root,
|
||||
mesh,
|
||||
PackedStringArray(),
|
||||
[],
|
||||
group_index,
|
||||
0.0,
|
||||
0.0,
|
||||
false
|
||||
)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_equal(parent_root.get_child_count(), 1000, "timing groups complete", failures)
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "1000 groups under one second", failures)
|
||||
parent_root.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _source_between(
|
||||
source: String,
|
||||
start_marker: String,
|
||||
end_marker: String,
|
||||
failures: Array[String]) -> String:
|
||||
var start_offset := source.find(start_marker)
|
||||
var end_offset := source.find(end_marker, start_offset + start_marker.length())
|
||||
if start_offset < 0 or end_offset <= start_offset:
|
||||
failures.append("cannot isolate source between %s and %s" % [start_marker, end_marker])
|
||||
return ""
|
||||
return source.substr(start_offset, end_offset - start_offset)
|
||||
|
||||
|
||||
func _expect_true(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
if not condition:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(condition: bool, label: String, failures: Array[String]) -> void:
|
||||
_expect_true(not condition, label, failures)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%d actual=%d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_float_equal(actual: float, expected: float, label: String, failures: Array[String]) -> void:
|
||||
if not is_equal_approx(actual, expected):
|
||||
failures.append("%s expected=%.6f actual=%.6f" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_equal(actual: String, expected: String, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s expected=%s actual=%s" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b1kt1agus8efe
|
||||
@@ -0,0 +1,299 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free identity, refresh-version, material-definition, ownership and
|
||||
## timing regression for cached WMO runtime Mesh finalization.
|
||||
|
||||
const FINALIZER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
|
||||
)
|
||||
const FINALIZER_PATH := "res://src/render/wmo/wmo_runtime_mesh_finalizer.gd"
|
||||
const SCENE_PREPARER_PATH := "res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeWmoMaterialBuilder extends RefCounted:
|
||||
var definitions: Array[Dictionary] = []
|
||||
var texture_path_sets: Array[PackedStringArray] = []
|
||||
var extracted_directories: Array[String] = []
|
||||
var rebuilt_material: Material = StandardMaterial3D.new()
|
||||
|
||||
func _build_material(
|
||||
material_definition: Dictionary,
|
||||
texture_paths: PackedStringArray,
|
||||
extracted_directory: String
|
||||
) -> Material:
|
||||
definitions.append(material_definition.duplicate(true))
|
||||
texture_path_sets.append(texture_paths.duplicate())
|
||||
extracted_directories.append(extracted_directory)
|
||||
return rebuilt_material
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_null_current_and_non_array_contract(failures)
|
||||
_verify_unmarked_surface_contract(failures)
|
||||
_verify_standard_material_definition(failures)
|
||||
_verify_shader_material_colors(failures)
|
||||
_verify_missing_builder_contract(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_RUNTIME_MESH_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_RUNTIME_MESH_FINALIZER PASS cases=28 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_null_current_and_non_array_contract(failures: Array[String]) -> void:
|
||||
var builder := FakeWmoMaterialBuilder.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
|
||||
_expect_true(
|
||||
finalizer.call("finalize_mesh", null, "res://data/extracted") == null,
|
||||
"null Mesh remains null",
|
||||
failures
|
||||
)
|
||||
var current_mesh := ArrayMesh.new()
|
||||
current_mesh.set_meta("wow_wmo_material_refresh_version", 10)
|
||||
_expect_true(
|
||||
finalizer.call("finalize_mesh", current_mesh, "res://data/extracted") == current_mesh,
|
||||
"current Mesh exact identity retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(builder.definitions.size(), 0, "current Mesh skips builder", failures)
|
||||
var primitive_mesh := BoxMesh.new()
|
||||
_expect_true(
|
||||
finalizer.call("finalize_mesh", primitive_mesh, "res://data/extracted") == primitive_mesh,
|
||||
"non-ArrayMesh exact identity retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
int(primitive_mesh.get_meta("wow_wmo_material_refresh_version", 0)),
|
||||
10,
|
||||
"non-ArrayMesh stamped current",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_unmarked_surface_contract(failures: Array[String]) -> void:
|
||||
var builder := FakeWmoMaterialBuilder.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
|
||||
var mesh := _mesh_with_surfaces(2)
|
||||
var ordinary_material := StandardMaterial3D.new()
|
||||
mesh.surface_set_material(1, ordinary_material)
|
||||
var finalized_mesh: Mesh = finalizer.call(
|
||||
"finalize_mesh",
|
||||
mesh,
|
||||
"res://data/extracted"
|
||||
) as Mesh
|
||||
_expect_true(finalized_mesh == mesh, "stale ArrayMesh exact identity retained", failures)
|
||||
_expect_true(mesh.surface_get_material(0) == null, "null surface remains null", failures)
|
||||
_expect_true(
|
||||
mesh.surface_get_material(1) == ordinary_material,
|
||||
"unmarked material exact identity retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(builder.definitions.size(), 0, "unmarked surfaces skip builder", failures)
|
||||
_expect_equal(
|
||||
int(mesh.get_meta("wow_wmo_material_refresh_version", 0)),
|
||||
10,
|
||||
"stale ArrayMesh stamped current",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_standard_material_definition(failures: Array[String]) -> void:
|
||||
var builder := FakeWmoMaterialBuilder.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
|
||||
var cached_material := StandardMaterial3D.new()
|
||||
cached_material.set_meta("texture0_path", "World\\A.blp")
|
||||
cached_material.set_meta("texture1_path", "")
|
||||
cached_material.set_meta("texture2_path", "World\\C.blp")
|
||||
cached_material.set_meta("wow_flags", 17)
|
||||
cached_material.set_meta("wow_shader", 4)
|
||||
cached_material.set_meta("wow_blend_mode", 6)
|
||||
var mesh := _mesh_with_surfaces(1)
|
||||
mesh.surface_set_material(0, cached_material)
|
||||
finalizer.call("finalize_mesh", mesh, "res://fixture/extracted")
|
||||
_expect_equal(builder.definitions.size(), 1, "eligible surface rebuilt once", failures)
|
||||
_expect_true(
|
||||
mesh.surface_get_material(0) == builder.rebuilt_material,
|
||||
"exact builder Material adopted",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
builder.texture_path_sets[0] == PackedStringArray(["World\\A.blp", "World\\C.blp"]),
|
||||
"texture paths compacted in slot order",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
builder.extracted_directories == ["res://fixture/extracted"],
|
||||
"extracted directory forwarded exactly",
|
||||
failures
|
||||
)
|
||||
var definition := builder.definitions[0]
|
||||
_expect_equal(int(definition["texture0"]), 0, "texture0 compact index", failures)
|
||||
_expect_equal(int(definition["texture1"]), -1, "empty texture1 index", failures)
|
||||
_expect_equal(int(definition["texture2"]), 1, "texture2 compact index", failures)
|
||||
_expect_equal(int(definition["flags"]), 17, "WMO flags forwarded", failures)
|
||||
_expect_equal(int(definition["shader"]), 4, "WMO shader forwarded", failures)
|
||||
_expect_equal(int(definition["blend_mode"]), 6, "WMO blend mode forwarded", failures)
|
||||
_expect_true(definition["diffuse_color"] == Color.WHITE, "default diffuse color", failures)
|
||||
_expect_true(definition["emissive_color"] == Color.BLACK, "default emissive color", failures)
|
||||
_expect_true(definition["color2"] == Color.WHITE, "default secondary color", failures)
|
||||
|
||||
|
||||
func _verify_shader_material_colors(failures: Array[String]) -> void:
|
||||
var builder := FakeWmoMaterialBuilder.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
|
||||
var shader := Shader.new()
|
||||
shader.code = (
|
||||
"shader_type spatial; uniform vec4 diffuse_color; "
|
||||
+ "uniform vec4 emissive_color; uniform vec4 secondary_color;"
|
||||
)
|
||||
var cached_material := ShaderMaterial.new()
|
||||
cached_material.shader = shader
|
||||
cached_material.set_shader_parameter("diffuse_color", Color(0.1, 0.2, 0.3, 0.4))
|
||||
cached_material.set_shader_parameter("emissive_color", Color(0.5, 0.6, 0.7, 0.8))
|
||||
cached_material.set_shader_parameter("secondary_color", Color(0.9, 0.8, 0.7, 0.6))
|
||||
cached_material.set_meta("texture0_path", "World\\Only.blp")
|
||||
var mesh := _mesh_with_surfaces(1)
|
||||
mesh.surface_set_material(0, cached_material)
|
||||
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
|
||||
var definition := builder.definitions[0]
|
||||
_expect_true(
|
||||
definition["diffuse_color"] == Color(0.1, 0.2, 0.3, 0.4),
|
||||
"shader diffuse color forwarded",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
definition["emissive_color"] == Color(0.5, 0.6, 0.7, 0.8),
|
||||
"shader emissive color forwarded",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
definition["color2"] == Color(0.9, 0.8, 0.7, 0.6),
|
||||
"shader secondary color forwarded",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_missing_builder_contract(failures: Array[String]) -> void:
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(null)
|
||||
var cached_material := StandardMaterial3D.new()
|
||||
cached_material.set_meta("texture0_path", "World\\A.blp")
|
||||
var mesh := _mesh_with_surfaces(1)
|
||||
mesh.surface_set_material(0, cached_material)
|
||||
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
|
||||
_expect_true(
|
||||
mesh.surface_get_material(0) == cached_material,
|
||||
"missing builder retains cached Material identity",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var finalizer_source := _read_text(FINALIZER_PATH, failures)
|
||||
var scene_preparer_source := _read_text(SCENE_PREPARER_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_RUNTIME_MESH_FINALIZER_SCRIPT.new("),
|
||||
"loader composes runtime Mesh finalizer",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_runtime_mesh_finalizer.finalize_mesh("),
|
||||
"loader delegates runtime Mesh finalization",
|
||||
failures
|
||||
)
|
||||
for released_token in [
|
||||
"func _refresh_cached_wmo_mesh_materials(",
|
||||
"func _rebuild_cached_wmo_material(",
|
||||
"const WMO_MATERIAL_REFRESH_VERSION",
|
||||
]:
|
||||
_expect_false(
|
||||
loader_source.contains(released_token),
|
||||
"loader releases %s" % released_token,
|
||||
failures
|
||||
)
|
||||
for owned_token in [
|
||||
"const MATERIAL_REFRESH_VERSION := 10",
|
||||
"wow_wmo_material_refresh_version",
|
||||
"array_mesh.surface_get_material(surface_index)",
|
||||
"_wmo_material_builder.call(",
|
||||
]:
|
||||
_expect_true(
|
||||
finalizer_source.contains(owned_token),
|
||||
"finalizer owns %s" % owned_token,
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _refresh_cached_wmo_materials_recursive("),
|
||||
"loader releases scene traversal",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
scene_preparer_source.contains("func _finalize_meshes_in_subtree("),
|
||||
"runtime scene preparer owns scene traversal",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var builder := FakeWmoMaterialBuilder.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(builder)
|
||||
var mesh := ArrayMesh.new()
|
||||
mesh.set_meta("wow_wmo_material_refresh_version", 10)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(1000):
|
||||
finalizer.call("finalize_mesh", mesh, "res://data/extracted")
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
if elapsed_milliseconds >= 1000.0:
|
||||
failures.append(
|
||||
"1000 current Mesh finalizations took %.3fms (budget < 1000ms)"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _mesh_with_surfaces(surface_count: int) -> ArrayMesh:
|
||||
var mesh := ArrayMesh.new()
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = PackedVector3Array([
|
||||
Vector3.ZERO,
|
||||
Vector3.RIGHT,
|
||||
Vector3.UP,
|
||||
])
|
||||
for surface_index in range(surface_count):
|
||||
mesh.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return mesh
|
||||
|
||||
|
||||
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 source := file.get_as_text()
|
||||
file.close()
|
||||
return source
|
||||
|
||||
|
||||
func _expect_true(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://bhj5k6lmi7na8
|
||||
@@ -0,0 +1,308 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free cached/live WMO subtree traversal, render-policy, ownership and
|
||||
## bounded-timing regression for runtime scene preparation.
|
||||
|
||||
const PREPARER_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
||||
)
|
||||
const PREPARER_PATH := "res://src/render/wmo/wmo_runtime_scene_preparer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeRuntimeMeshFinalizer extends RefCounted:
|
||||
var finalized_meshes: Array[Mesh] = []
|
||||
var extracted_directories: Array[String] = []
|
||||
|
||||
func finalize_mesh(mesh: Mesh, extracted_directory: String) -> Mesh:
|
||||
finalized_meshes.append(mesh)
|
||||
extracted_directories.append(extracted_directory)
|
||||
return mesh
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_cached_instance_contract(failures)
|
||||
_verify_enabled_occlusion_and_disabled_shadow_contract(failures)
|
||||
_verify_live_instance_contract(failures)
|
||||
_verify_missing_finalizer_and_invalid_input(failures)
|
||||
_verify_source_boundaries(failures)
|
||||
var elapsed_milliseconds := _verify_bounded_timing(failures)
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("WMO_RUNTIME_SCENE_PREPARER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_RUNTIME_SCENE_PREPARER PASS cases=31 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_cached_instance_contract(failures: Array[String]) -> void:
|
||||
var finalizer := FakeRuntimeMeshFinalizer.new()
|
||||
var preparer := PREPARER_SCRIPT.new(finalizer)
|
||||
var fixture := _create_scene_fixture()
|
||||
var instance: Node3D = fixture["root"]
|
||||
var mesh_instance: MeshInstance3D = fixture["mesh_instance"]
|
||||
var multimesh_instance: MultiMeshInstance3D = fixture["multimesh_instance"]
|
||||
var direct_occluders: Node3D = fixture["direct_occluders"]
|
||||
var nested_occluders: Node3D = fixture["nested_occluders"]
|
||||
get_root().add_child(instance)
|
||||
var prepared: bool = preparer.prepare_cached_instance(
|
||||
instance,
|
||||
"res://fixture/extracted",
|
||||
false,
|
||||
true
|
||||
)
|
||||
_expect_true(prepared, "cached instance accepted", failures)
|
||||
_expect_equal(finalizer.finalized_meshes.size(), 2, "two non-null Meshes finalized", failures)
|
||||
_expect_same(finalizer.finalized_meshes[0], mesh_instance.mesh, "Mesh traversal first", failures)
|
||||
_expect_same(
|
||||
finalizer.finalized_meshes[1],
|
||||
multimesh_instance.multimesh.mesh,
|
||||
"MultiMesh Mesh traversal second",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
finalizer.extracted_directories == [
|
||||
"res://fixture/extracted",
|
||||
"res://fixture/extracted",
|
||||
],
|
||||
"extracted directory forwarded for every Mesh",
|
||||
failures
|
||||
)
|
||||
_expect_true(instance.get_node_or_null("Occluders") == null, "direct Occluders removed", failures)
|
||||
_expect_true(direct_occluders.is_queued_for_deletion(), "direct Occluders queued for deletion", failures)
|
||||
_expect_same(
|
||||
nested_occluders.get_parent(),
|
||||
fixture["nested_root"],
|
||||
"nested same-name node retained",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
mesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||
"Mesh shadows enabled",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
multimesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||
"MultiMesh shadows enabled",
|
||||
failures
|
||||
)
|
||||
instance.free()
|
||||
|
||||
|
||||
func _verify_enabled_occlusion_and_disabled_shadow_contract(
|
||||
failures: Array[String]) -> void:
|
||||
var finalizer := FakeRuntimeMeshFinalizer.new()
|
||||
var preparer := PREPARER_SCRIPT.new(finalizer)
|
||||
var fixture := _create_scene_fixture()
|
||||
var instance: Node3D = fixture["root"]
|
||||
var mesh_instance: MeshInstance3D = fixture["mesh_instance"]
|
||||
var multimesh_instance: MultiMeshInstance3D = fixture["multimesh_instance"]
|
||||
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED
|
||||
multimesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
_expect_true(
|
||||
preparer.prepare_cached_instance(instance, "res://data/extracted", true, false),
|
||||
"enabled-occlusion cached instance accepted",
|
||||
failures
|
||||
)
|
||||
_expect_same(
|
||||
fixture["direct_occluders"].get_parent(),
|
||||
instance,
|
||||
"enabled occlusion retains direct child",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
mesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_DOUBLE_SIDED,
|
||||
"disabled shadow policy preserves Mesh value",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
multimesh_instance.cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_OFF,
|
||||
"disabled shadow policy preserves MultiMesh value",
|
||||
failures
|
||||
)
|
||||
instance.free()
|
||||
|
||||
|
||||
func _verify_live_instance_contract(failures: Array[String]) -> void:
|
||||
var finalizer := FakeRuntimeMeshFinalizer.new()
|
||||
var preparer := PREPARER_SCRIPT.new(finalizer)
|
||||
var fixture := _create_scene_fixture()
|
||||
var instance: Node3D = fixture["root"]
|
||||
_expect_true(
|
||||
preparer.prepare_live_instance(instance, false, true),
|
||||
"live instance accepted",
|
||||
failures
|
||||
)
|
||||
_expect_equal(finalizer.finalized_meshes.size(), 0, "live path skips Mesh finalizer", failures)
|
||||
_expect_true(instance.get_node_or_null("Occluders") == null, "live direct Occluders removed", failures)
|
||||
_expect_equal(
|
||||
fixture["mesh_instance"].cast_shadow,
|
||||
GeometryInstance3D.SHADOW_CASTING_SETTING_ON,
|
||||
"live Mesh shadows enabled",
|
||||
failures
|
||||
)
|
||||
instance.free()
|
||||
|
||||
|
||||
func _verify_missing_finalizer_and_invalid_input(failures: Array[String]) -> void:
|
||||
var preparer := PREPARER_SCRIPT.new(null)
|
||||
var fixture := _create_scene_fixture()
|
||||
var instance: Node3D = fixture["root"]
|
||||
_expect_true(
|
||||
preparer.prepare_cached_instance(instance, "res://data/extracted", false, true),
|
||||
"missing finalizer retains render-policy preparation",
|
||||
failures
|
||||
)
|
||||
_expect_true(instance.get_node_or_null("Occluders") == null, "missing finalizer still applies occluder policy", failures)
|
||||
_expect_false(
|
||||
preparer.prepare_cached_instance(null, "res://data/extracted", false, true),
|
||||
"null cached root rejected",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
preparer.prepare_live_instance(null, false, true),
|
||||
"null live root rejected",
|
||||
failures
|
||||
)
|
||||
instance.free()
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var preparer_source := FileAccess.get_file_as_string(PREPARER_PATH)
|
||||
var loader_source := FileAccess.get_file_as_string(LOADER_PATH)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_RUNTIME_SCENE_PREPARER_SCRIPT.new("),
|
||||
"loader composes runtime scene preparer",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_wmo_runtime_scene_preparer.prepare_cached_instance("),
|
||||
1,
|
||||
"loader delegates cached preparation once",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
loader_source.count("_wmo_runtime_scene_preparer.prepare_live_instance("),
|
||||
1,
|
||||
"loader delegates live preparation once",
|
||||
failures
|
||||
)
|
||||
for released_token in [
|
||||
"func _refresh_cached_wmo_materials_recursive(",
|
||||
"func _apply_shadow_cast_recursive(",
|
||||
]:
|
||||
_expect_false(
|
||||
loader_source.contains(released_token),
|
||||
"loader releases %s" % released_token,
|
||||
failures
|
||||
)
|
||||
for owned_token in [
|
||||
"func _finalize_meshes_in_subtree(",
|
||||
"instance.get_node_or_null(\"Occluders\")",
|
||||
"instance.remove_child(occluders)",
|
||||
"func _enable_shadow_casting_recursive(",
|
||||
"GeometryInstance3D.SHADOW_CASTING_SETTING_ON",
|
||||
]:
|
||||
_expect_true(
|
||||
preparer_source.contains(owned_token),
|
||||
"preparer owns %s" % owned_token,
|
||||
failures
|
||||
)
|
||||
for forbidden_dependency in [
|
||||
"ResourceLoader.",
|
||||
"FileAccess.",
|
||||
"WorkerThreadPool.",
|
||||
"RenderingServer.",
|
||||
".owner =",
|
||||
"add_child(",
|
||||
"duplicate(",
|
||||
"_wmo_render_build_queue",
|
||||
"_render_budget_scheduler",
|
||||
]:
|
||||
_expect_false(
|
||||
preparer_source.contains(forbidden_dependency),
|
||||
"preparer omits %s ownership" % forbidden_dependency,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var finalizer := FakeRuntimeMeshFinalizer.new()
|
||||
var preparer := PREPARER_SCRIPT.new(finalizer)
|
||||
var fixture := _create_scene_fixture(false)
|
||||
var instance: Node3D = fixture["root"]
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in 1000:
|
||||
preparer.prepare_cached_instance(instance, "res://data/extracted", true, false)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_equal(finalizer.finalized_meshes.size(), 2000, "timing traversal complete", failures)
|
||||
_expect_true(elapsed_milliseconds < 1000.0, "1000 preparations under one second", failures)
|
||||
instance.free()
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _create_scene_fixture(include_direct_occluders: bool = true) -> Dictionary:
|
||||
var root := Node3D.new()
|
||||
root.name = "FixtureWmo"
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "Exterior"
|
||||
mesh_instance.mesh = ArrayMesh.new()
|
||||
mesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
root.add_child(mesh_instance)
|
||||
var nested_root := Node3D.new()
|
||||
nested_root.name = "Nested"
|
||||
root.add_child(nested_root)
|
||||
var multimesh := MultiMesh.new()
|
||||
multimesh.transform_format = MultiMesh.TRANSFORM_3D
|
||||
multimesh.mesh = ArrayMesh.new()
|
||||
var multimesh_instance := MultiMeshInstance3D.new()
|
||||
multimesh_instance.name = "Doodads"
|
||||
multimesh_instance.multimesh = multimesh
|
||||
multimesh_instance.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||
nested_root.add_child(multimesh_instance)
|
||||
var null_multimesh_instance := MultiMeshInstance3D.new()
|
||||
nested_root.add_child(null_multimesh_instance)
|
||||
var nested_occluders := Node3D.new()
|
||||
nested_occluders.name = "Occluders"
|
||||
nested_root.add_child(nested_occluders)
|
||||
var direct_occluders: Node3D = null
|
||||
if include_direct_occluders:
|
||||
direct_occluders = Node3D.new()
|
||||
direct_occluders.name = "Occluders"
|
||||
root.add_child(direct_occluders)
|
||||
return {
|
||||
"root": root,
|
||||
"mesh_instance": mesh_instance,
|
||||
"nested_root": nested_root,
|
||||
"multimesh_instance": multimesh_instance,
|
||||
"nested_occluders": nested_occluders,
|
||||
"direct_occluders": direct_occluders,
|
||||
}
|
||||
|
||||
|
||||
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_same(actual: Variant, expected: Variant, label: String, failures: Array[String]) -> void:
|
||||
if not is_same(actual, expected):
|
||||
failures.append(label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://bigpi11md6vxd
|
||||
@@ -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
|
||||
@@ -6,6 +6,7 @@ const CACHE_STATE_SCRIPT := preload(
|
||||
"res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
)
|
||||
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
@@ -233,6 +234,7 @@ func _verify_detached_sorted_diagnostics(failures: Array[String]) -> void:
|
||||
func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var cache_state_source := _read_text(CACHE_STATE_PATH, failures)
|
||||
var finalizer_source := _read_text(FINALIZER_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_SCENE_RESOURCE_CACHE_STATE_SCRIPT.new()"),
|
||||
"loader composes scene cache state",
|
||||
@@ -266,15 +268,22 @@ func _verify_ownership_boundaries(failures: Array[String]) -> void:
|
||||
for retained_loader_rule in [
|
||||
"wmo_max_runtime_scene_mb",
|
||||
"_get_resource_file_size(cache_path)",
|
||||
"_is_wmo_scene_cache_current(resource as PackedScene)",
|
||||
"scene.instantiate() as Node3D",
|
||||
"WMO_BUILDER_SCRIPT.is_scene_cache_current(node)",
|
||||
]:
|
||||
_expect_true(
|
||||
loader_source.contains(retained_loader_rule),
|
||||
"loader retains %s" % retained_loader_rule,
|
||||
failures
|
||||
)
|
||||
for finalizer_rule in [
|
||||
"scene.instantiate()",
|
||||
"is_scene_cache_current",
|
||||
"instance.free()",
|
||||
]:
|
||||
_expect_true(
|
||||
finalizer_source.contains(finalizer_rule),
|
||||
"finalizer owns %s" % finalizer_rule,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
|
||||
@@ -0,0 +1,346 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free terminal-I/O, probe validation/lifetime, adoption, boundary and
|
||||
## timing regression for the cached WMO PackedScene finalizer.
|
||||
|
||||
const FINALIZER_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_finalizer.gd")
|
||||
const CACHE_STATE_SCRIPT := preload("res://src/render/wmo/wmo_scene_resource_cache_state.gd")
|
||||
const FINALIZER_PATH := "res://src/render/wmo/wmo_scene_resource_finalizer.gd"
|
||||
const CACHE_STATE_PATH := "res://src/render/wmo/wmo_scene_resource_cache_state.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
|
||||
|
||||
class FakeSceneCacheValidator extends RefCounted:
|
||||
var validated_nodes: Array[Node] = []
|
||||
|
||||
func is_scene_cache_current(node: Node) -> bool:
|
||||
validated_nodes.append(node)
|
||||
return bool(node.get_meta("current_wmo_cache", false))
|
||||
|
||||
|
||||
class FakeResourceLoaderAdapter extends RefCounted:
|
||||
var status_by_path: Dictionary = {}
|
||||
var resource_by_path: Dictionary = {}
|
||||
var status_paths: Array[String] = []
|
||||
var loaded_paths: Array[String] = []
|
||||
|
||||
func load_threaded_get_status(resource_path: String) -> int:
|
||||
status_paths.append(resource_path)
|
||||
return int(status_by_path.get(resource_path, ResourceLoader.THREAD_LOAD_IN_PROGRESS))
|
||||
|
||||
func load_threaded_get(resource_path: String) -> Resource:
|
||||
loaded_paths.append(resource_path)
|
||||
return resource_by_path.get(resource_path) as Resource
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_null_and_non_terminal_poll(failures)
|
||||
_verify_failed_and_loaded_poll_order(failures)
|
||||
_verify_loaded_resource_rejections(failures)
|
||||
_verify_current_scene_adoption_and_probe_lifetime(failures)
|
||||
_verify_validation_contract(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_RESOURCE_FINALIZER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
print(
|
||||
"WMO_SCENE_RESOURCE_FINALIZER PASS cases=26 iterations=1000 elapsed_ms=%.3f"
|
||||
% elapsed_milliseconds
|
||||
)
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_null_and_non_terminal_poll(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", null)),
|
||||
0,
|
||||
"null cache state rejected",
|
||||
failures
|
||||
)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
cache_state.call("remember_request", "world/pending.wmo", "res://cache/pending.tscn")
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
0,
|
||||
"non-terminal request retained",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(cache_state.call("has_request", "world/pending.wmo")),
|
||||
"pending state remains",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.status_paths,
|
||||
["res://cache/pending.tscn"],
|
||||
"pending path polled once",
|
||||
failures
|
||||
)
|
||||
_expect_equal(adapter.loaded_paths.size(), 0, "pending scene not loaded", failures)
|
||||
|
||||
|
||||
func _verify_failed_and_loaded_poll_order(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var requests := {
|
||||
"world/pending.wmo": "res://cache/pending.tscn",
|
||||
"world/failed.wmo": "res://cache/failed.tscn",
|
||||
"world/loaded.wmo": "res://cache/loaded.tscn",
|
||||
}
|
||||
for normalized_relative_path in requests:
|
||||
cache_state.call(
|
||||
"remember_request",
|
||||
normalized_relative_path,
|
||||
requests[normalized_relative_path]
|
||||
)
|
||||
adapter.status_by_path = {
|
||||
"res://cache/failed.tscn": ResourceLoader.THREAD_LOAD_FAILED,
|
||||
"res://cache/loaded.tscn": ResourceLoader.THREAD_LOAD_LOADED,
|
||||
}
|
||||
adapter.resource_by_path["res://cache/loaded.tscn"] = current_scene
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
2,
|
||||
"failed and loaded scenes complete",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.status_paths,
|
||||
[
|
||||
"res://cache/pending.tscn",
|
||||
"res://cache/failed.tscn",
|
||||
"res://cache/loaded.tscn",
|
||||
],
|
||||
"detached insertion order preserved",
|
||||
failures
|
||||
)
|
||||
_expect_string_array(
|
||||
adapter.loaded_paths,
|
||||
["res://cache/loaded.tscn"],
|
||||
"only loaded terminal scene retrieved",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
bool(cache_state.call("is_missing", "world/failed.wmo")),
|
||||
"failed request publishes missing",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/loaded.wmo") == current_scene,
|
||||
"loaded scene identity adopted",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_loaded_resource_rejections(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var rejected_resources := {
|
||||
"null": null,
|
||||
"wrong_type": Resource.new(),
|
||||
"stale": _packed_scene(false, true, failures),
|
||||
"non_node_3d": _packed_scene(true, false, failures),
|
||||
}
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
for rejection_name in rejected_resources:
|
||||
var normalized_relative_path := "world/%s.wmo" % rejection_name
|
||||
var cache_path := "res://cache/%s.tscn" % rejection_name
|
||||
cache_state.call("remember_request", normalized_relative_path, cache_path)
|
||||
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_LOADED
|
||||
adapter.resource_by_path[cache_path] = rejected_resources[rejection_name]
|
||||
_expect_equal(
|
||||
int(finalizer.call("poll_terminal_requests", cache_state)),
|
||||
4,
|
||||
"all invalid loaded Resources complete",
|
||||
failures
|
||||
)
|
||||
for rejection_name in rejected_resources:
|
||||
_expect_true(
|
||||
bool(cache_state.call("is_missing", "world/%s.wmo" % rejection_name)),
|
||||
"%s loaded Resource publishes missing" % rejection_name,
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
validator.validated_nodes.size(),
|
||||
1,
|
||||
"only Node3D stale scene reaches validator",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
is_instance_valid(validator.validated_nodes[0]),
|
||||
"stale validation probe released",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_current_scene_adoption_and_probe_lifetime(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator, adapter)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
cache_state.call("remember_request", "world/current.wmo", "res://cache/current.tscn")
|
||||
adapter.status_by_path["res://cache/current.tscn"] = ResourceLoader.THREAD_LOAD_LOADED
|
||||
adapter.resource_by_path["res://cache/current.tscn"] = current_scene
|
||||
finalizer.call("poll_terminal_requests", cache_state)
|
||||
_expect_true(
|
||||
cache_state.call("scene_for", "world/current.wmo") == current_scene,
|
||||
"current PackedScene exact identity adopted",
|
||||
failures
|
||||
)
|
||||
_expect_equal(
|
||||
validator.validated_nodes.size(),
|
||||
1,
|
||||
"current scene instantiated exactly once",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
is_instance_valid(validator.validated_nodes[0]),
|
||||
"accepted validation probe released",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_validation_contract(failures: Array[String]) -> void:
|
||||
var validator := FakeSceneCacheValidator.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(validator)
|
||||
var current_scene := _packed_scene(true, true, failures)
|
||||
_expect_true(
|
||||
bool(finalizer.call("is_scene_cache_current", current_scene)),
|
||||
"current Node3D scene accepted",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
bool(finalizer.call("is_scene_cache_current", null)),
|
||||
"null scene rejected",
|
||||
failures
|
||||
)
|
||||
var missing_validator_finalizer: RefCounted = FINALIZER_SCRIPT.new(null)
|
||||
_expect_false(
|
||||
bool(missing_validator_finalizer.call("is_scene_cache_current", current_scene)),
|
||||
"missing validator rejects scene without instantiation",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_source_boundaries(failures: Array[String]) -> void:
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
var finalizer_source := _read_text(FINALIZER_PATH, failures)
|
||||
var cache_source := _read_text(CACHE_STATE_PATH, failures)
|
||||
_expect_true(
|
||||
loader_source.contains("WMO_SCENE_RESOURCE_FINALIZER_SCRIPT.new("),
|
||||
"loader composes scene finalizer",
|
||||
failures
|
||||
)
|
||||
_expect_true(
|
||||
loader_source.contains("_wmo_scene_resource_finalizer.poll_terminal_requests("),
|
||||
"loader delegates scene terminal polling",
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
loader_source.contains("func _is_wmo_scene_cache_current("),
|
||||
"loader releases PackedScene probe validation",
|
||||
failures
|
||||
)
|
||||
for required_token in [
|
||||
"ResourceLoader.load_threaded_get_status(resource_path)",
|
||||
"ResourceLoader.load_threaded_get(resource_path)",
|
||||
"scene.instantiate()",
|
||||
"is_scene_cache_current",
|
||||
"instance.free()",
|
||||
"complete_request_with_scene",
|
||||
"complete_request_as_missing",
|
||||
]:
|
||||
_expect_true(
|
||||
finalizer_source.contains(required_token),
|
||||
"finalizer owns %s" % required_token,
|
||||
failures
|
||||
)
|
||||
_expect_false(
|
||||
cache_source.contains("ResourceLoader."),
|
||||
"cache state remains free of ResourceLoader",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_bounded_timing(failures: Array[String]) -> float:
|
||||
var adapter := FakeResourceLoaderAdapter.new()
|
||||
var finalizer: RefCounted = FINALIZER_SCRIPT.new(
|
||||
FakeSceneCacheValidator.new(),
|
||||
adapter
|
||||
)
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for iteration in range(1000):
|
||||
var cache_state: RefCounted = CACHE_STATE_SCRIPT.new()
|
||||
var normalized_relative_path := "world/object_%d.wmo" % iteration
|
||||
var cache_path := "res://cache/object_%d.tscn" % iteration
|
||||
cache_state.call("remember_request", normalized_relative_path, cache_path)
|
||||
adapter.status_by_path[cache_path] = ResourceLoader.THREAD_LOAD_FAILED
|
||||
finalizer.call("poll_terminal_requests", cache_state)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
if elapsed_milliseconds >= 1000.0:
|
||||
failures.append(
|
||||
"1000 terminal polls took %.3fms (budget < 1000ms)" % elapsed_milliseconds
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _packed_scene(
|
||||
is_current: bool,
|
||||
use_node_3d_root: bool,
|
||||
failures: Array[String]
|
||||
) -> PackedScene:
|
||||
var root: Node = Node3D.new() if use_node_3d_root else Node.new()
|
||||
root.set_meta("current_wmo_cache", is_current)
|
||||
var scene := PackedScene.new()
|
||||
var pack_error := scene.pack(root)
|
||||
root.free()
|
||||
if pack_error != OK:
|
||||
failures.append("cannot pack synthetic scene: %d" % pack_error)
|
||||
return scene
|
||||
|
||||
|
||||
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 source := file.get_as_text()
|
||||
file.close()
|
||||
return source
|
||||
|
||||
|
||||
func _expect_true(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_false(value: bool, label: String, failures: Array[String]) -> void:
|
||||
if value:
|
||||
failures.append(label)
|
||||
|
||||
|
||||
func _expect_equal(actual: int, expected: int, label: String, failures: Array[String]) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %d, got %d" % [label, expected, actual])
|
||||
|
||||
|
||||
func _expect_string_array(
|
||||
actual: Array,
|
||||
expected: Array,
|
||||
label: String,
|
||||
failures: Array[String]
|
||||
) -> void:
|
||||
if actual != expected:
|
||||
failures.append("%s: expected %s, got %s" % [label, expected, actual])
|
||||
@@ -0,0 +1 @@
|
||||
uid://cs6gj0x3i3fg6
|
||||
@@ -69,6 +69,6 @@
|
||||
- Remaining risks: direct build-12340 timing evidence for movement/camera,
|
||||
jump/fall/swim/fly, autorun, left-button camera/select behavior, active camera
|
||||
collision, terrain slopes/holes/liquids, persisted keybindings, server authority,
|
||||
prediction and reconciliation remain for M09/M10/M12. Runtime renderer scenes
|
||||
prediction and reconciliation remain for M10/M11/M13. Runtime renderer scenes
|
||||
continue to default to explicit `RenderSandbox` until an application shell
|
||||
selects `Blizzlike335`. M02 makes no complete `1:1` compatibility claim.
|
||||
|
||||
+127
-13
@@ -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
|
||||
|
||||
@@ -26,8 +27,8 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
|
||||
## Evidence
|
||||
|
||||
- Date: 2026-07-18
|
||||
- Revision/worktree: master merges `7ece2ab`, `7e35de7`, `80cb084`, `d6e5b53`, `c69abd6`, `606770c`, `630a0c1`, `f36fabb`, `24aef13`, `2342430`, `396be5e`, `f88bf97`, `3f84717`, `d2cb52f`, `f9e5a4f`, `c06aed5`, `a878e7c`, `60603e1`, `b4955d6`, `ada0fd9`, `f470775`, `874fd0f`, `38f831e`, `83b3618`, `3094561`, `fa85985`, `1fafdde`, `7cd07dd`, `1de50f9`, `3dccd3e`, `e7a7c67`, `c0fc191`, `c251985`, `5e82daa`, `1dc013e`, `99a90dd`, `4354834`, `a043c79`, `f79e064`, `d37c799`, `06f6394`, `34b7000`, `1acddab`;
|
||||
- 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`, `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`,
|
||||
@@ -43,6 +44,11 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
`M03-RND-WMO-RENDER-RESOURCE-CACHE-001`,
|
||||
`M03-RND-WMO-RENDER-RESOURCE-FINALIZER-001`,
|
||||
`M03-RND-WMO-SCENE-RESOURCE-CACHE-001`,
|
||||
`M03-RND-WMO-SCENE-RESOURCE-FINALIZER-001`,
|
||||
`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`,
|
||||
@@ -142,6 +148,28 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
Post-merge scene state (`40.113ms`), render state (`43.434ms`), shutdown,
|
||||
WMO queue/resolver, facade, internal-access `40`, `7/7` manifest,
|
||||
documentation and coordination gates remained green.
|
||||
WMO scene Resource finalizer passed `cases=26 iterations=1000
|
||||
elapsed_ms=4.186` with insertion-order status polling, non-terminal retention,
|
||||
failed/null/wrong-type/non-Node3D/stale rejection, current exact PackedScene
|
||||
adoption, accepted/rejected probe release and source boundaries. All 61
|
||||
autonomous headless regressions passed; internal-access remained `30`,
|
||||
documentation covered `48` module specifications, coordination retained `34`
|
||||
historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans.
|
||||
Merge `6a0f9bd` passed post-merge finalizer (`3.970ms`), WMO scene/render
|
||||
cache/finalizer/queue/planner/registry/resolver/shutdown, facade,
|
||||
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation
|
||||
`48` and coordination.
|
||||
WMO runtime Mesh finalizer passed `cases=27 iterations=1000` with null/current/
|
||||
non-ArrayMesh exact identity, refresh-version stamping, null/unmarked surface
|
||||
retention, compact texture indices, exact flags/shader/blend/default and
|
||||
ShaderMaterial colors, builder-result adoption and source boundaries. All 63
|
||||
autonomous headless verifiers passed; internal-access remained `30`,
|
||||
documentation covered `49` module specifications, coordination retained `34`
|
||||
historical expired-claim warnings and checkpoint dry-run kept all `7/7` plans.
|
||||
Merge `d65ebee` passed post-merge finalizer (`0.202ms`), WMO scene/render
|
||||
cache/finalizer/queue/planner/registry/resolver, shutdown, materials, facade,
|
||||
internal-access `30`, manifest and checkpoint dry-run `7/7`, documentation
|
||||
`49` and coordination.
|
||||
ADT water load pipeline state passed `cases=12 iterations=100
|
||||
elapsed_ms=86.170`; its initial direct no-`.godot` run passed (`94.576ms`),
|
||||
WMO caches/queue/planner/registry/resolver/material/shutdown and 18 adjacent
|
||||
@@ -339,6 +367,43 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
Merge `34b7000` passed post-merge finalizer (`0.826ms`), Mesh pipeline/cache/
|
||||
extractor/runtime-finalizer/raw-repository/prototype/shutdown, facade,
|
||||
internal-access `30`, manifest `7/7`, documentation `46` and coordination.
|
||||
WMO render group materializer passed `cases=37 iterations=1000
|
||||
elapsed_ms=2.771` with exact Mesh/MultiMesh identity, indexed and fallback
|
||||
names/transforms, explicit shadow mode, positive/disabled visibility settings,
|
||||
single attachment, invalid-input and source-ownership contracts. The autonomous
|
||||
suite passed `63/64`; only the proprietary ADT placement probe was unavailable
|
||||
without `data/extracted`. Editor parse completed without script diagnostics,
|
||||
internal-access remained `30`, documentation covered `50` module
|
||||
specifications, coordination passed with `77` historical expired-claim
|
||||
warnings and checkpoint dry-run retained `7/7` plans. Merge `705354d` is the
|
||||
accepted integration revision; post-merge materializer (`3.236ms`), all nine
|
||||
adjacent WMO services, shutdown, materials, facade, internal-access `30`,
|
||||
manifest `7/7`, documentation `50` and coordination remained green.
|
||||
WMO runtime scene preparer passed `cases=31 iterations=1000
|
||||
elapsed_ms=3.669`; the updated runtime Mesh finalizer boundary passed
|
||||
`cases=28 iterations=1000 elapsed_ms=0.204`. Cached preparation preserved
|
||||
exact parent-before-children Mesh/MultiMesh finalization, while live-built
|
||||
duplicates skipped that boundary; direct `Occluders` removal and enabled/
|
||||
preserved shadow policies stayed unchanged. The suite passed `64/65` with no
|
||||
unexpected failures and only the proprietary ADT placement probe unavailable.
|
||||
Editor parse had zero script diagnostics, documentation covered `51` module
|
||||
specifications and checkpoint dry-run retained `7/7`. Merge `57d0a9f`
|
||||
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
|
||||
@@ -407,9 +472,15 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
loader-owned request admission and shutdown drain order are unchanged.
|
||||
Cached WMO scene paths retain direct negative caching for missing/oversize/
|
||||
request-start failures, terminal missing state for load/validation failures and
|
||||
the prior transient-reset versus final-shutdown lifetime. File-size limits,
|
||||
`PackedScene` probe instantiation, WMOBuilder metadata validation and probe-node
|
||||
destruction remain loader-owned before state accepts a scene.
|
||||
the prior transient-reset versus final-shutdown lifetime. File-size limits and
|
||||
request admission remain loader-owned; terminal I/O, `PackedScene` probe,
|
||||
WMOBuilder metadata validation and probe release belong to the scene finalizer.
|
||||
Rejected non-Node3D probes are now freed without changing fallback or output.
|
||||
Cached WMO runtime material refresh remains version `10`; exact Mesh identity,
|
||||
stale stamping, surface order, compact texture path indices, cached metadata/
|
||||
colors and WMOBuilder output adoption are unchanged. Those rules now belong to
|
||||
the runtime Mesh finalizer while cached-scene/build-job Node traversal, permits,
|
||||
attachment and lifetime remain loader-owned.
|
||||
ADT water requests retain FIFO order and tile-key deduplication; tile release
|
||||
removes pending requests without cancelling active work, task completion and
|
||||
result publication order are unchanged, and the mutex remains limited to the
|
||||
@@ -487,8 +558,13 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
typed WMO render build job/queue and loader enqueue/drain/cancel/clear adapters,
|
||||
lightweight WMO render Resource/missing/request state and loader lookup/request/
|
||||
reset adapters, plus terminal Resource polling/validation/publication finalizer,
|
||||
cached WMO PackedScene/missing/request state and loader file-size/I/O/probe/
|
||||
validation/reset adapters,
|
||||
cached WMO PackedScene/missing/request state and loader file-size/admission/
|
||||
reset adapters, plus terminal PackedScene polling/probe/lifetime/publication
|
||||
finalizer,
|
||||
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,
|
||||
@@ -548,11 +624,49 @@ Runtime и Editor используют facade; planner/scheduler тестиру
|
||||
observers produce typed per-operation state, including native observation;
|
||||
animation and static Mesh terminal ResourceLoader drains are extracted, while
|
||||
action execution, material-prototype lookup and root cleanup remain loader-owned;
|
||||
cached-scene WMO ResourceLoader/FileAccess I/O, live fallback and group
|
||||
materialization remain in the loader; lightweight render terminal I/O is separated;
|
||||
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; 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.
|
||||
|
||||
@@ -0,0 +1,113 @@
|
||||
# M04 — Renderer Fidelity and Graphics Foundation
|
||||
|
||||
<!-- OPENWC_TARGET:ACTIVE -->
|
||||
|
||||
## Outcome
|
||||
|
||||
Довести графическую основу OpenWC до evidence-backed визуальной совместимости с
|
||||
оригинальным клиентом WoW 3.3.5a build 12340 и создать общий graphics contract
|
||||
для дальнейшего клиента, Editor preview и отдельного racing-профиля.
|
||||
|
||||
Noggit используется как дополнительный ориентир композиции и authoring preview,
|
||||
но не заменяет оригинальный клиент как fidelity oracle.
|
||||
|
||||
Benilla используется как pinned secondary reference для M2 animation/material
|
||||
architecture, particles/ribbons, WMO portal visibility и temporal test ideas. Его
|
||||
Vanilla 1.12.1/Bevy behavior не заменяет build-12340 captures и не подтверждает
|
||||
WotLK parity без отдельного fixture comparison.
|
||||
|
||||
## Reference corpus
|
||||
|
||||
- [ ] Принять versioned CSV manifest и валидатор оригинальных captures.
|
||||
- [ ] Импортировать минимум две позиции каждой выбранной локации: `wide` для
|
||||
неба/тумана/дальности/композиции и `ground` для terrain/material/M2/WMO.
|
||||
- [ ] Сохранять map/zone/subzone, WoW/server coordinates, orientation, доступные
|
||||
camera parameters, time, weather, graphics profile, viewport и имя артефакта.
|
||||
- [ ] Не угадывать отсутствующие camera значения: хранить `Unknown`, landmark и
|
||||
ограничение сравнения.
|
||||
- [ ] Хранить proprietary screenshots/video вне Git; в репозитории сохранять
|
||||
manifest, SHA-256, provenance, metrics, classifications и human approval.
|
||||
- [ ] Добавить выбранную матрицу dawn/day/dusk/night, weather, shadow и liquid
|
||||
quality без полного комбинаторного умножения всех локаций.
|
||||
- [ ] Добавить temporal clips/кадры для water, animation, billboards, particles,
|
||||
ribbons, weather и spell effect lifecycle.
|
||||
|
||||
## Graphics foundation
|
||||
|
||||
- [ ] Ввести явный graphics profile contract: `Blizzlike335` и opt-in
|
||||
`Enhanced/Racing`; улучшения не меняют Blizzlike-путь и не маскируют gaps.
|
||||
- [ ] Зафиксировать color pipeline: source color space, sampling, gamma,
|
||||
tonemapping, fog, alpha coverage и deterministic capture output.
|
||||
- [ ] Заменять approximate constants только после paired original-client
|
||||
evidence; каждое осознанное расхождение регистрировать по подсистеме.
|
||||
- [ ] Сохранить `WorldRenderFacade`, streaming budgets, cache versioning и
|
||||
main-thread GPU finalization boundaries M03.
|
||||
- [ ] Не начинать полный custom `RenderingDevice` renderer без bounded spike,
|
||||
profiler evidence и ADR; сначала использовать точные Godot shader passes.
|
||||
|
||||
## Required implementation order
|
||||
|
||||
- [ ] Environment: точная DBC interpolation, sky/skybox, ambient/directional
|
||||
light, zone transitions, fog, day/night и weather presentation.
|
||||
- [ ] Terrain: MCNK topology/normals/holes, MCAL variants/layer animation,
|
||||
filtering/mips, MCSH/baked shadow data, seams и WDL distant terrain.
|
||||
- [ ] M2/WMO materials: build-12340 blend/depth/cull flags, alpha test,
|
||||
multi-texture combiners, vertex colors, emissive/unlit, UV animation,
|
||||
billboards, WMO indoor/outdoor, portals, local lights и fog semantics.
|
||||
- [ ] Shadows: воспроизвести оригинальную policy для выбранных graphics settings;
|
||||
terrain/baked, character/object и dynamic shadows проверять раздельно.
|
||||
- [ ] Liquids: MH2O/MCLQ/MLIQ geometry, masks/heights/depth, original animated
|
||||
textures и water/ocean/magma/slime/underwater behavior. Enhanced reflections,
|
||||
normals и shore blending остаются opt-in.
|
||||
- [ ] Animation/characters: GPU skinning path, sequences/blending/global
|
||||
sequences, attachments, geosets, customization, equipment, mounts и vehicles;
|
||||
CPU deformation оставить только измеренным fallback/diagnostic path.
|
||||
- [ ] Effects: M2 particles/ribbons, texture animation, weather emitters,
|
||||
SpellVisual/SpellVisualKit cast/projectile/impact/aura presentation, pooling,
|
||||
deterministic lifetime и visibility budgets.
|
||||
- [ ] Racing readiness: renderer/world/character/effects APIs не зависят от WoW
|
||||
gameplay/network; racing profile меняет только явные graphics capabilities.
|
||||
|
||||
## Verification
|
||||
|
||||
- Original-client/OpenWC paired captures используют одинаковые viewport, camera
|
||||
contract, position, time, weather и graphics profile либо явно классифицируют
|
||||
неизвестное поле.
|
||||
- Static comparison включает full-frame и semantic regions; geometry/framing,
|
||||
material, lighting, shadow, liquid, animation/effect gaps не смешиваются.
|
||||
- Temporal comparison проверяет phase, duration, trajectory, UV motion и emitter
|
||||
lifetime, а не только один случайный кадр.
|
||||
- GPU test matrix покрывает Godot Forward+/D3D12/Vulkan, cold/warm load, rapid
|
||||
traversal, teleport, weather/time transitions и profile switching.
|
||||
- Обязательны p50/p95/p99, max hitch, draw calls, visible objects, CPU/GPU memory,
|
||||
shader/cache stability и Node/Resource/RID leak checks.
|
||||
|
||||
## Exit criteria
|
||||
|
||||
- Reference corpus покрывает обязательные static и temporal классы и проходит
|
||||
schema/provenance/hash validation.
|
||||
- Все пункты renderer coverage map имеют evidence-backed статус; `Verified`
|
||||
допускается только с original-client fixture и human approval.
|
||||
- Для утверждённой build-12340 capture matrix не остаётся неклассифицированных
|
||||
визуально различимых gaps; принятые engine/GPU отклонения имеют измеренный
|
||||
tolerance, rationale и artifact evidence.
|
||||
- `Blizzlike335` сохраняет оригинальную policy, `Enhanced/Racing` выключаем и не
|
||||
изменяет default behavior.
|
||||
- Long traversal не создаёт recurring hitches, cache churn или renderer leaks;
|
||||
M03 performance budgets остаются зелёными либо изменены только отдельным
|
||||
evidence-backed решением.
|
||||
- Character, animation, liquid, shadow и effects contracts пригодны для runtime,
|
||||
Editor preview и будущего racing fork без зависимости от network/gameplay.
|
||||
|
||||
## Evidence
|
||||
|
||||
- Date:
|
||||
- Revision/worktree:
|
||||
- Original-client build/settings/corpus:
|
||||
- Commands:
|
||||
- Static/temporal comparison results:
|
||||
- Performance and lifetime results:
|
||||
- Human approval:
|
||||
- API/module docs and diagrams:
|
||||
- Changed files:
|
||||
- Remaining risks:
|
||||
@@ -1,4 +1,4 @@
|
||||
# M04 — Godot Editor Shell
|
||||
# M05 — Godot Editor Shell
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M05 — Content Project
|
||||
# M06 — Content Project
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M06 — TrinityCore/AzerothCore Server Adapters
|
||||
# M07 — TrinityCore/AzerothCore Server Adapters
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M07 — World Editor Vertical Slice
|
||||
# M08 — World Editor Vertical Slice
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M08 — WoW 3.3.5a Network Foundation
|
||||
# M09 — WoW 3.3.5a Network Foundation
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M09 — Gameplay Domain
|
||||
# M10 — Gameplay Domain
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M10 — First Playable Client Slice
|
||||
# M11 — First Playable Client Slice
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M11 — Quest End-to-End
|
||||
# M12 — Quest End-to-End
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M12 — WoW 3.3.5a Client Completeness
|
||||
# M13 — WoW 3.3.5a Client Completeness
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# M13 — Dungeon and Encounter Authoring
|
||||
# M14 — Dungeon and Encounter Authoring
|
||||
|
||||
<!-- OPENWC_TARGET:OPEN -->
|
||||
|
||||
@@ -8,12 +8,15 @@ Godot-specific implementation rules обязательны из [`../docs/GODOT_
|
||||
|
||||
## Итоговый продукт
|
||||
|
||||
OpenWC должен предоставить четыре согласованных режима:
|
||||
OpenWC должен предоставить пять согласованных режимов/потребителей:
|
||||
|
||||
1. `Blizzlike335` — клиент WoW 3.3.5a build 12340 с максимально точным наблюдаемым поведением.
|
||||
2. `Enhanced` — opt-in графические, UX и accessibility улучшения, не меняющие wire protocol и server authority без отдельного profile.
|
||||
3. `AuthoringStudio` — инструменты мира, контента, БД и playtest внутри Godot Editor.
|
||||
4. `HeadlessTools` — import, validation, bake, packet replay, content build и CI без GUI.
|
||||
5. `Racing` — downstream fork/profile, использующий общие world, character,
|
||||
animation, effects и graphics contracts без зависимости от WoW gameplay и
|
||||
network lifecycle.
|
||||
|
||||
Один кодовый путь должен обслуживать runtime и editor preview через публичные contracts. Generated preview nodes, DB records и caches не являются источником истины.
|
||||
|
||||
@@ -21,7 +24,7 @@ OpenWC должен предоставить четыре согласованн
|
||||
|
||||
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
|
||||
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
|
||||
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; M02 декомпозировал player input, movement, terrain query, camera и presentation без observable regression. Текущая работа M03 закрывает renderer стабильным facade и извлекает planner/scheduler без rewrite.
|
||||
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. M01 завершил coordinate, identity и streaming-focus seams; M02 декомпозировал player input, movement, terrain query, camera и presentation без observable regression. M03 закрыл renderer стабильным facade, planner/scheduler и извлечёнными services. Текущий M04 доводит графическую основу до evidence-backed build-12340 fidelity до начала Editor/network milestones.
|
||||
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
|
||||
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
|
||||
|
||||
@@ -45,14 +48,15 @@ M00 renderer baseline
|
||||
└─ M01 coordinates/contracts
|
||||
└─ M02 input/movement/camera/presentation split
|
||||
└─ M03 renderer facade
|
||||
├─ M04 editor shell → M05 content → M06 server adapters → M07 world editor
|
||||
└─ M08 network → M09 gameplay domain → M10 playable client
|
||||
M07 + M10 → M11 quest end-to-end
|
||||
└─ M12 completeness waves
|
||||
└─ M13 dungeon authoring
|
||||
└─ M04 renderer fidelity/graphics foundation
|
||||
├─ M05 editor shell → M06 content → M07 server adapters → M08 world editor
|
||||
└─ M09 network → M10 gameplay domain → M11 playable client
|
||||
M08 + M11 → M12 quest end-to-end
|
||||
└─ M13 completeness waves
|
||||
└─ M14 dungeon authoring
|
||||
```
|
||||
|
||||
M12 не является одним большим этапом. Он выполняется последовательными completeness waves:
|
||||
M13 не является одним большим этапом. Он выполняется последовательными completeness waves:
|
||||
|
||||
1. `C1 Core loop` — combat, spells, auras, death, loot, inventory, quests.
|
||||
2. `C2 Character` — creation/customization, equipment, animation, pets, mounts.
|
||||
@@ -73,16 +77,17 @@ M12 не является одним большим этапом. Он выпо
|
||||
| M01 | FND typed coordinates/IDs/contracts | Server/ADT/Godot round-trip и StreamingFocus |
|
||||
| M02 | FND composition, GMP movement, RND presenters | Player split без visual/control regression |
|
||||
| M03 | RND facade/planner/scheduler, FND jobs | Renderer доступен через API и сохраняет budgets |
|
||||
| M04 | EDT plugin shell, QAR editor tests | Reload-safe workspace, commands, undo/recovery |
|
||||
| M05 | FND content schemas/cache, EDT project | Deterministic Content Project build/migration |
|
||||
| M06 | SRV environments/snapshots/adapters | Two-core read/diff/dry-run round-trip |
|
||||
| M07 | EDT world editing/navigation/deploy | NPC placement на dev-core без ручного SQL |
|
||||
| M08 | NET transport/auth/protocol/entities | Headless auth → world и deterministic replay |
|
||||
| M09 | GMP world state/movement/targeting | Packet replay → deterministic gameplay snapshot |
|
||||
| M10 | FND app lifecycle, RND/UIA presentation | Игрок входит, видит entities и перемещается |
|
||||
| M11 | GMP quests, EDT quest tools, SRV deploy, UIA quest UI | Author → deploy → complete quest своим клиентом |
|
||||
| M12 C1–C8 | NET/GMP/RND/UIA completeness + QAR | Feature matrix `Compatible/Verified` по waves |
|
||||
| M13 | EDT dungeon/encounter, SRV modules, GMP instances | Custom dungeon group playtest end-to-end |
|
||||
| M04 | RND/QAR reference corpus, materials, environment, shadows, liquids, characters/effects | Original-client-paired graphics foundation и отделённый Enhanced/Racing profile |
|
||||
| M05 | EDT plugin shell, QAR editor tests | Reload-safe workspace, commands, undo/recovery |
|
||||
| M06 | FND content schemas/cache, EDT project | Deterministic Content Project build/migration |
|
||||
| M07 | SRV environments/snapshots/adapters | Two-core read/diff/dry-run round-trip |
|
||||
| M08 | EDT world editing/navigation/deploy | NPC placement на dev-core без ручного SQL |
|
||||
| M09 | NET transport/auth/protocol/entities | Headless auth → world и deterministic replay |
|
||||
| M10 | GMP world state/movement/targeting | Packet replay → deterministic gameplay snapshot |
|
||||
| M11 | FND app lifecycle, RND/UIA presentation | Игрок входит, видит entities и перемещается |
|
||||
| M12 | GMP quests, EDT quest tools, SRV deploy, UIA quest UI | Author → deploy → complete quest своим клиентом |
|
||||
| M13 C1–C8 | NET/GMP/RND/UIA completeness + QAR | Feature matrix `Compatible/Verified` по waves |
|
||||
| M14 | EDT dungeon/encounter, SRV modules, GMP instances | Custom dungeon group playtest end-to-end |
|
||||
|
||||
## Recommended implementation rhythm
|
||||
|
||||
@@ -101,6 +106,8 @@ M12 не является одним большим этапом. Он выпо
|
||||
## Cross-cutting contracts
|
||||
|
||||
- `BuildProfile` — `Blizzlike335`, `Enhanced`, authoring и test capabilities.
|
||||
- `GraphicsProfile` — explicit Blizzlike/Enhanced/Racing material, lighting,
|
||||
shadow, liquid, effects, distance и post-processing capabilities.
|
||||
- `CoordinateMapper` — единственное преобразование WoW/Godot/ADT/server coordinates.
|
||||
- `AssetRepository` — original assets, overlays, caches и provenance.
|
||||
- `ProtocolProfile` — build, opcodes, update fields, codecs и server capabilities.
|
||||
|
||||
+23
-53
@@ -4,23 +4,23 @@
|
||||
|
||||
Этот каталог задаёт обязательную последовательность реализации. Архитектура описана в `docs/`, фактическое состояние renderer — в `RENDER.md`, а здесь находятся исполняемые milestones.
|
||||
|
||||
Полная инженерная декомпозиция всех подсистем находится в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md). Перед работой над milestone агент читает соответствующий subsystem plan из `targets/roadmap/`, но статус меняет только в исполняемом target-файле M00–M13.
|
||||
Полная инженерная декомпозиция всех подсистем находится в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md). Перед работой над milestone агент читает соответствующий subsystem plan из `targets/roadmap/`, но статус меняет только в исполняемом target-файле M00–M14.
|
||||
|
||||
Параллельная командная работа выполняется по [`../docs/TEAM_WORKFLOW.md`](../docs/TEAM_WORKFLOW.md): отдельные worktrees, claims, exclusive paths, contract-first и интегратор milestone.
|
||||
|
||||
## Current target
|
||||
## Текущая цель
|
||||
|
||||
`M03` — [03-renderer-facade.md](03-renderer-facade.md)
|
||||
|
||||
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
||||
`M04` — [04-renderer-fidelity.md](04-renderer-fidelity.md)
|
||||
*Начало фазы выполнения:* `YYYY-MM-DD`
|
||||
Одновременно `АКТИВНАЯ` может быть только одна цель. Следующая цель становится `АКТИВНОЙ` после появления валидной метки `OPENWC_TARGET_DONE` у предыдущей.
|
||||
|
||||
Статусные метки применяются только к исполняемым файлам `targets/[0-9][0-9]-*.md`. `DEVELOPMENT_ROADMAP.md` и `targets/roadmap/` являются нормативной декомпозицией, но не создают параллельную систему статусов.
|
||||
|
||||
## Definition of fidelity 1:1
|
||||
## Определение фидели 1:1
|
||||
|
||||
`1:1` означает совпадение наблюдаемого поведения с оригинальным клиентом WoW 3.3.5a build 12340 для выбранного сценария:
|
||||
|
||||
- wire protocol и update fields;
|
||||
- жилл протокол и обновляемые поля;
|
||||
- игровые состояния и переходы;
|
||||
- координаты, timing и server authority;
|
||||
- UI/Lua/FrameXML semantics;
|
||||
@@ -36,53 +36,23 @@
|
||||
| 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 |
|
||||
| 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 |
|
||||
| M08 | [Network foundation](08-network-foundation.md) | M03 | OPEN |
|
||||
| M09 | [Gameplay domain](09-gameplay-domain.md) | M08 | OPEN |
|
||||
| M10 | [Playable client slice](10-playable-client.md) | M09, M03 | OPEN |
|
||||
| M11 | [Quest end-to-end](11-quest-end-to-end.md) | M07, M10 | OPEN |
|
||||
| M12 | [WoW 3.3.5a completeness](12-client-completeness.md) | M11 | OPEN |
|
||||
| M13 | [Dungeon authoring](13-dungeon-authoring.md) | M12 foundation | OPEN |
|
||||
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | DONE |
|
||||
| M04 | [Renderer fidelity and graphics foundation](04-renderer-fidelity.md) | M03 | ACTIVE |
|
||||
| M05 | [Godot Editor shell](05-editor-shell.md) | M04 | OPEN |
|
||||
| M06 | [Content Project](06-content-project.md) | M05 | OPEN |
|
||||
| M07 | [Server database adapters](07-server-adapters.md) | M06 | OPEN |
|
||||
| M08 | [World Editor vertical slice](08-world-editor-slice.md) | M07 | OPEN |
|
||||
| M09 | [Network foundation](09-network-foundation.md) | M04 | OPEN |
|
||||
| M10 | [Gameplay domain](10-gameplay-domain.md) | M09 | OPEN |
|
||||
| M11 | [Playable client slice](11-playable-client.md) | M10, M04 | OPEN |
|
||||
| M12 | [Quest end-to-end](12-quest-end-to-end.md) | M08, M11 | OPEN |
|
||||
| M13 | [WoW 3.3.5a completeness](13-client-completeness.md) | M12 | OPEN |
|
||||
| M14 | [Dungeon authoring](14-dungeon-authoring.md) | M13 foundation | OPEN |
|
||||
|
||||
M12 выполняется восемью completeness waves, определёнными в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md), а не одной массовой реализацией.
|
||||
M13 выполняется восемью completeness waves, определёнными в [`DEVELOPMENT_ROADMAP.md`](DEVELOPMENT_ROADMAP.md), а не одной массовой реализацией.
|
||||
|
||||
M07 и M08 могут выполняться параллельными командами только после M06/M03 соответственно и только если пользователь явно разрешил параллельную работу. Их результаты сходятся в M11.
|
||||
M08 и M09 могут выполняться параллельными командами только после M07/M04 соответственно и только если пользователь явно разрешил параллельную работу. Их результаты сходятся в M12.
|
||||
|
||||
`recast-rs` запланирован как optional navigation backend в M05, исследовательский CLI spike и world validation в M07, затем полноценная dungeon navigation validation в M13. Он не блокирует M00–M04 и не считается автоматически совместимым с server `mmaps`.
|
||||
`recast-rs` запланирован как optional navigation backend в M06, исследовательский CLI spike и world validation в M08, затем полноценная dungeon navigation validation в M14. Он не блокирует M00–M05 и не считается автоматически совместимым с server `mmaps`.
|
||||
|
||||
`rilua` зарегистрирован как Lua 5.1.1 runtime/test-oracle candidate для M12. До compatibility spike он не добавляется как dependency; UI строится через заменяемый `LuaRuntime`, поскольку WoW API, FrameXML, secure actions и taint не входят в готовый VM.
|
||||
|
||||
Wowser зарегистрирован как независимый MIT reference для M08 и M12: auth → world protocol lifecycle, binary/data handling, asset pipeline и общий client coverage. Его старый browser stack не является кандидатом на интеграцию; полезные решения принимаются только после проверки против build 12340 и server cores.
|
||||
|
||||
## Общий quality gate
|
||||
|
||||
Каждая цель должна сохранить:
|
||||
|
||||
- headless загрузку Godot project;
|
||||
- затронутые unit/contract/integration tests;
|
||||
- renderer checkpoints при изменении рендера;
|
||||
- отсутствие новых нарушений dependency rules;
|
||||
- обновлённые документы и diagnostics;
|
||||
- выполнен documentation gate: inline public API, module spec, inputs/outputs и обязательные diagrams;
|
||||
- выполнен coding gate: явные имена, KISS, отсутствие premature abstraction и необоснованной optimization;
|
||||
- проходят `tools/verify_coordination.ps1` и `tools/verify_documentation.ps1`;
|
||||
- воспроизводимость без proprietary данных в Git.
|
||||
|
||||
## Evidence format
|
||||
|
||||
В конце target-файла агент заполняет:
|
||||
|
||||
```text
|
||||
## Evidence
|
||||
- Date:
|
||||
- Revision/worktree:
|
||||
- Commands:
|
||||
- Results:
|
||||
- Fidelity comparison:
|
||||
- Changed files:
|
||||
- Remaining risks:
|
||||
```
|
||||
`rilua` зарегистрирован как Lua 5.1.1 runtime/test-oracle
|
||||
@@ -10,7 +10,7 @@
|
||||
- Отделить user settings, developer settings, server connection profiles и content project settings.
|
||||
- Secrets хранить вне Git/Resource; поддержать environment/OS credential provider.
|
||||
- Настройки иметь schema version, defaults, validation, migration и reset-by-section.
|
||||
- `Blizzlike335` сделать неизменяемым базовым профилем; enhanced settings наследуют его явно.
|
||||
- `Blizzlike335` сделать неизменяемым базовым профилем; Enhanced/Racing settings наследуют его явно и не меняют default behavior.
|
||||
|
||||
## FND-2. Application lifecycle
|
||||
|
||||
|
||||
@@ -4,6 +4,59 @@
|
||||
|
||||
Воспроизвести визуальные правила WoW 3.3.5a и дать opt-in улучшения, сохранив frame pacing на больших мирах Godot.
|
||||
|
||||
## M04 execution policy
|
||||
|
||||
M04 выполняет renderer fidelity до Editor, network и gameplay milestones. M03
|
||||
facade, ownership, streaming budgets и cache rules сохраняются: fidelity работа
|
||||
расширяет services и shaders, но не возвращает прямой cross-layer доступ к
|
||||
streamer internals.
|
||||
|
||||
Authoritative oracle — оригинальный клиент build 12340. Noggit полезен для
|
||||
обзорной композиции, placements и будущего Editor viewport, но не подтверждает
|
||||
точные lighting, shadow, liquid, material или effect semantics.
|
||||
|
||||
Pinned Benilla source полезен как secondary implementation reference для
|
||||
pose/palette animation, global sequences, material/pass ordering, shared dynamic
|
||||
effect buffers, M2 particles/ribbons и WMO portal traversal. Поскольку Benilla
|
||||
ориентирован на Vanilla build 5875 и Bevy, каждое заимствованное правило сначала
|
||||
проверяется на build-12340 fixture и адаптируется к существующим Godot/M03
|
||||
ownership, scheduling и finalization boundaries.
|
||||
|
||||
WoWee revision `607ea3b8369851014721416293f8e95dfbe64eec` добавляет полезные WotLK-oriented
|
||||
test patterns для M2 global sequences/color-alpha tracks, `$FSD` event timing,
|
||||
liquid masks, rotated WMO water projection и CPU/shader vertex contracts. Они
|
||||
становятся M04 fixture candidates, но не oracle: Hermite/Bezier sampling и effects
|
||||
частично аппроксимированы, tilted placement order не решён, а часть asset tests
|
||||
silent-skips missing data. OpenWC fixtures обязаны фиксировать build 12340,
|
||||
provenance/hash и фактическое исполнение. Из-за дополнительного license restriction
|
||||
WoWee используется только как research reference без копирования/вендоринга кода.
|
||||
|
||||
Reference corpus принимает versioned CSV и приватные captures. Для каждой
|
||||
выбранной локации базовый набор содержит две позиции: `wide` для sky/fog/WDL и
|
||||
общей композиции, `ground` для terrain/material/M2/WMO. Специализированные
|
||||
water, interior, shadow, character и effect viewpoints добавляются только когда
|
||||
две базовые позиции не покрывают уникальную визуальную policy.
|
||||
|
||||
Repository хранит schema, coordinates/camera/settings metadata, SHA-256,
|
||||
comparison metrics, gap classification и human approval. Proprietary image/video
|
||||
bytes остаются вне Git. Неизвестные camera/time/weather/settings поля отмечаются
|
||||
как `Unknown`; их запрещено угадывать или автоматически считать parity evidence.
|
||||
|
||||
Обязательный порядок реализации:
|
||||
|
||||
1. reference manifest, validator, paired capture и semantic region diff;
|
||||
2. deterministic color/gamma/sampling/alpha/fog pipeline;
|
||||
3. environment/sky/weather и terrain/WDL/MCSH;
|
||||
4. M2/WMO material passes и indoor/outdoor lighting;
|
||||
5. graphics-setting-aware shadows и liquids;
|
||||
6. GPU animation, characters, attachments, equipment и mounts;
|
||||
7. particles, ribbons, weather и spell visual lifecycle;
|
||||
8. Enhanced/Racing profile separation, traversal/performance/lifetime closeout.
|
||||
|
||||
Полный custom `RenderingDevice` backend допускается только после bounded spike,
|
||||
доказавшего, что точный critical pass нельзя реализовать Godot spatial/sky/
|
||||
particle/compositor механизмами в установленном performance budget.
|
||||
|
||||
## RND-1. Renderer architecture
|
||||
|
||||
- `WorldRenderFacade` принимает focus, environment snapshot и entity presentation commands.
|
||||
@@ -99,4 +152,9 @@
|
||||
|
||||
## Gate
|
||||
|
||||
Видимый мир не деградирует при декомпозиции; under-foot/near-field quality готова до появления в кадре; long traversal не создаёт recurring hitches/leaks; Enhanced всегда отключаем.
|
||||
Для утверждённой original-client capture matrix отсутствуют неклассифицированные
|
||||
визуально различимые gaps; каждый `Verified` class имеет fixture, metrics и human
|
||||
approval. Under-foot/near-field quality готова до появления в кадре; temporal
|
||||
effects совпадают по phase/duration/trajectory; long traversal не создаёт
|
||||
recurring hitches/leaks; Enhanced/Racing всегда отключаем и не меняет
|
||||
`Blizzlike335` default behavior.
|
||||
|
||||
@@ -90,7 +90,19 @@
|
||||
|
||||
- Original 3.3.5a FrameXML/Lua extracted from legally owned data is primary behavior corpus.
|
||||
- `rilua`/PUC-Rio are VM candidates/oracles, not complete WoW UI.
|
||||
- WowBench/Wowser/WowUnreal/WoWee provide API/event/loader test ideas.
|
||||
- WoWee revision `607ea3b8369851014721416293f8e95dfbe64eec` provides a large
|
||||
headless FrameXML/Lua corpus, retained widget-tree/layout tests, a unified
|
||||
XML-to-Lua `CreateFrame` path, progressive default-UI takeover and static API/
|
||||
event/handler audits. OpenWC reuses the test taxonomy and staged-handover model,
|
||||
not its monolithic Lua/game binding, permissive unknown-API fallback or absent
|
||||
secure/taint semantics; its restricted license also keeps it reference-only.
|
||||
- WowBench/Wowser/WowUnreal provide additional API/event/loader test ideas.
|
||||
- Pinned [Benilla](https://github.com/samwhosung/benilla) provides a strong
|
||||
engine-free TOC/FrameXML/Lua decomposition and compatibility-test reference:
|
||||
document/load order, templates, anchors, widget identity, handler globals,
|
||||
deterministic event/`OnUpdate` order, sandbox and SavedVariables. Its Vanilla
|
||||
Lua 5.0 compatibility layer and missing secure/taint coverage are not WotLK
|
||||
semantics and require a build-12340 corpus.
|
||||
|
||||
## Gate
|
||||
|
||||
|
||||
@@ -66,7 +66,7 @@
|
||||
|
||||
## EDT-8. Graphics extension tools
|
||||
|
||||
- Material/shader profile editor с Blizzlike/Enhanced comparison.
|
||||
- Material/shader profile editor с Blizzlike/Enhanced/Racing comparison.
|
||||
- Texture replacement/upscale overlays и mip/alpha/color-space validation.
|
||||
- Water/sky/weather/light preview with fixed time and reference capture.
|
||||
- Character/equipment/animation/attachment preview.
|
||||
|
||||
@@ -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