Compare commits
17 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 1c0dfc761d | |||
| f45695c26a | |||
| c2be8ae95e | |||
| 8636427234 | |||
| 5627180fd5 | |||
| ba81098797 | |||
| f19c0c45a7 | |||
| 09b3e87a8d | |||
| d1df794cd6 | |||
| 0327e4862d | |||
| 239cb2432e | |||
| b5ddc86c92 | |||
| 3d90c6d79a | |||
| a10206b797 | |||
| 3172f41ee5 | |||
| 5a957d2ebb | |||
| 5092995d0c |
@@ -822,6 +822,14 @@ $exe = Join-Path $env:TEMP 'godot-4.6.1-openwc\Godot_v4.6.1-stable_win64.exe'
|
|||||||
- Added `src/tools/capture_render_checkpoints.gd` to load the Eastern Kingdoms streaming scene, move a dedicated camera to fixed regression viewpoints and save PNGs under `user://render_checkpoints`.
|
- Added `src/tools/capture_render_checkpoints.gd` to load the Eastern Kingdoms streaming scene, move a dedicated camera to fixed regression viewpoints and save PNGs under `user://render_checkpoints`.
|
||||||
- Current checkpoints cover the Elwynn waterfall from front/side and Goldshire Inn/Blacksmith windows. Headless runs are dry-run only because the headless display server cannot produce viewport PNGs.
|
- Current checkpoints cover the Elwynn waterfall from front/side and Goldshire Inn/Blacksmith windows. Headless runs are dry-run only because the headless display server cannot produce viewport PNGs.
|
||||||
|
|
||||||
|
## 2026-07-13 Renderer Shutdown Ownership
|
||||||
|
|
||||||
|
- `StreamingWorldLoader` drains its asynchronous work before teardown, leaves parented streamed nodes to SceneTree ownership, then explicitly frees detached M2/WMO prototype nodes and clears resource caches.
|
||||||
|
- `ADTBuilder` explicitly frees a temporary `Water` root when an ADT liquid payload produces no child geometry. Dry tiles must not leave empty nodes in ObjectDB.
|
||||||
|
- `src/tools/verify_render_runtime_cache_shutdown.gd` regression-checks detached prototype and resource-cache release plus empty-liquid-root ownership. The unified render-baseline runner executes it before capture.
|
||||||
|
- A focused 8-second GUI checkpoint and a seven-checkpoint single-pass GUI run finish without Node, renderer resource or RID leak diagnostics. A timing-dependent anonymous zero-reference `RefCounted` ObjectDB warning can remain at engine teardown; it is tracked separately and is not suppressed.
|
||||||
|
- This is shutdown-only ownership behavior and does not alter checkpoint rendering, cache formats or the `Blizzlike335` compatibility profile.
|
||||||
|
|
||||||
## Practical Rule For Future Work
|
## 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.
|
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.
|
||||||
|
|||||||
@@ -788,7 +788,10 @@ static func _build_tile_water_root(data: Dictionary, origin_offset: Vector3) ->
|
|||||||
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
|
||||||
root.add_child(mi)
|
root.add_child(mi)
|
||||||
|
|
||||||
return root if root.get_child_count() > 0 else null
|
if root.get_child_count() > 0:
|
||||||
|
return root
|
||||||
|
root.free()
|
||||||
|
return null
|
||||||
|
|
||||||
|
|
||||||
static func _append_liquid_geometry(
|
static func _append_liquid_geometry(
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-ANIMATION-SKY-EVIDENCE-001 — Animation and sky evidence
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-ANIMATION-SKY-EVIDENCE-001:sindo-main-codex:2026-07-15 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-ANIMATION-SKY-EVIDENCE-001:d1df794 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-animation-sky-evidence`
|
||||||
|
- Lease expires UTC: 2026-07-15
|
||||||
|
- Integrator: milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Add private build 12340 evidence for the remaining sky-transition and native animated-M2 coverage, rerun all paired comparisons, and hand the resulting audit to the milestone integrator.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Committing proprietary screenshots or extracted assets.
|
||||||
|
- Claiming renderer parity from mismatched world placement or animation phase.
|
||||||
|
- Marking M00 DONE; only the milestone integrator may change target status or Evidence.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: this claim and animation/sky evidence documentation
|
||||||
|
- Shared/hotspots: `docs/M00_FINAL_PAIRED_EVIDENCE.md`, `docs/modules/world-renderer.md`
|
||||||
|
- Generated/ignored: normalized private references under `sources/OpenWCReferenceCheckpoints`; captures and reports under `user://`
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: unchanged
|
||||||
|
- Schema/format version: unchanged
|
||||||
|
- Migration/compatibility: existing five-reference comparison remains valid
|
||||||
|
- Consumers: M00 milestone integrator
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: user-captured build 12340 screenshots at 19:00 and two real `GryphonRoost01` animation phases
|
||||||
|
- Blocks: final integrator decision on M00 completion
|
||||||
|
- External state: private screenshots remain outside Git
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: private-reference preparation, full baseline runner, comparator self-test, coordination and documentation gates
|
||||||
|
- Fixtures: seven private build 12340 checkpoint references plus a second animation-phase provenance frame
|
||||||
|
- Fidelity evidence: sky-transition screenshot and observed wing pose change over three seconds
|
||||||
|
- Performance budget: existing M00 capture budgets
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: unchanged
|
||||||
|
- Module specification: paired coverage and remaining limitations
|
||||||
|
- Data-flow diagram: unchanged
|
||||||
|
- Sequence/state/dependency diagrams: unchanged
|
||||||
|
- Source map/status updates: final paired evidence audit
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: none
|
||||||
|
- Simplest considered solution: normalize private captures outside Git and reuse the existing comparator
|
||||||
|
- Rejected complexity/abstractions: adding a video comparator or committing an asset corpus
|
||||||
|
- Unavoidable complexity and justification: animation phase and synthetic placement require human evidence classification alongside pixel metrics
|
||||||
|
- Measured optimization evidence: not applicable
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: exact-size private references registered; real ADT placement verified; canonical and visual captures completed; fourteen comparisons audited
|
||||||
|
- Next: milestone integrator reviews synthetic/real placement classification and GUI RID risk
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: this work-package commit
|
||||||
|
- Results: `compared=14`, `failed=14`, `missing=0`; mean error `0.066893..0.176955`; original-client Gryphon wing phase changed over three seconds
|
||||||
|
- Remaining risks: synthetic probe placement differs from the original-client world placement; GUI RID/resource diagnostics remain separate
|
||||||
|
- Documentation updated: `docs/M00_FINAL_PAIRED_EVIDENCE.md`, `docs/modules/world-renderer.md`
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-CAMERA-FRAMING-REFINEMENT-001 — Goldshire pitch/FOV refinement
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-FRAMING-REFINEMENT-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-FRAMING-REFINEMENT-001:3172f41 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-camera-framing-refinement`
|
||||||
|
- Lease expires UTC: 2026-07-14
|
||||||
|
- Integrator: milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Jointly refine Goldshire checkpoint pitch and FOV against a WMO-ready build 12340 reference.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Changing runtime player camera defaults.
|
||||||
|
- Changing manifest values without an interior optimum and human approval.
|
||||||
|
- Optimizing yaw beyond the ready-scene coarse result.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: joint FOV option in camera-pose sweep and framing evidence
|
||||||
|
- Shared/hotspots: camera-pose operational documentation and renderer module findings
|
||||||
|
- Generated/ignored: candidate PNGs and ranking JSON under `user://`
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: additive `-CameraFovValues`; scalar `-CameraFov` remains compatible
|
||||||
|
- Schema/format version: ranking schema remains version 1 with existing per-candidate FOV field
|
||||||
|
- Migration/compatibility: scalar sweeps retain existing candidate paths
|
||||||
|
- Consumers: M00 fidelity workflow
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: WMO-ready 8-second capture contract
|
||||||
|
- Blocks: Goldshire manifest framing decision
|
||||||
|
- External state: private build 12340 reference remains outside Git
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: joint-grid plan regression, real ready-scene sweep, comparator self-test, repository gates
|
||||||
|
- Fixtures: private Goldshire Inn reference
|
||||||
|
- Fidelity evidence: joint pitch/FOV ranking and human inspection
|
||||||
|
- Performance budget: offline diagnostic
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: PowerShell parameter
|
||||||
|
- Module specification: framing finding
|
||||||
|
- Data-flow diagram: FOV joins pose grid
|
||||||
|
- Sequence/state/dependency diagrams: unchanged
|
||||||
|
- Source map/status updates: operational guide
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `CameraFovValues`, `effectiveCameraFovValues`
|
||||||
|
- Simplest considered solution: one additional loop around the existing grid
|
||||||
|
- Rejected complexity/abstractions: generic optimizer
|
||||||
|
- Unavoidable complexity and justification: projection and pitch jointly affect framing
|
||||||
|
- Measured optimization evidence: bounded 1x5x3 grid
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: additive joint FOV grid, compatibility-preserving paths, plan regression, ready-scene 15-candidate ranking and human inspection
|
||||||
|
- Next: add approved landmark/region scoring before any manifest camera calibration
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: this work-package commit
|
||||||
|
- Results: numeric best FOV 62/yaw 10/pitch -25 at mean 0.078843 and ratio 0.667721 was visually rejected as grass-dominated; FOV 38/yaw 10/pitch -10 better matched building scale at mean 0.084220
|
||||||
|
- Remaining risks: full-frame metric does not preserve landmark framing; no manifest values were changed
|
||||||
|
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-CAMERA-OCCLUDERS-001 — Camera occluder diagnostic
|
# M00-QAR-CAMERA-OCCLUDERS-001 — Camera occluder diagnostic
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-OCCLUDERS-001:sindo-main-codex:2026-07-13 -->
|
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-OCCLUDERS-001:sindo-main-codex:2026-07-13 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-OCCLUDERS-001:9d34a47 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-CAMERA-POSE-EVIDENCE-001 — Goldshire camera pose evidence
|
# M00-QAR-CAMERA-POSE-EVIDENCE-001 — Goldshire camera pose evidence
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
|
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-POSE-EVIDENCE-001:eca76c5 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-CAMERA-POSE-SWEEP-001 — Empirical camera pose sweep
|
# M00-QAR-CAMERA-POSE-SWEEP-001 — Empirical camera pose sweep
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
<!-- OPENWC_CLAIM:M00-QAR-CAMERA-POSE-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAMERA-POSE-SWEEP-001:657a1d8 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain
|
# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-CAPTURE-SHUTDOWN-001:sindo-main-codex:2026-07-14 -->
|
<!-- OPENWC_CLAIM:M00-QAR-CAPTURE-SHUTDOWN-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-CAPTURE-SHUTDOWN-001:8d4641a -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-COORD-CALIBRATION-001 — Renderer coordinate calibration
|
# M00-QAR-COORD-CALIBRATION-001 — Renderer coordinate calibration
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-COORD-CALIBRATION-001:sindo-main-codex:2026-07-13 -->
|
<!-- OPENWC_CLAIM:M00-QAR-COORD-CALIBRATION-001:sindo-main-codex:2026-07-13 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-COORD-CALIBRATION-001:dfc1031 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-FINAL-PAIRED-EVIDENCE-001 — Final paired evidence audit
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-FINAL-PAIRED-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-FINAL-PAIRED-EVIDENCE-001:239cb24 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-final-paired-evidence`
|
||||||
|
- Lease expires UTC: 2026-07-14
|
||||||
|
- Integrator: milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Produce one final WMO-ready paired report for all available build 12340 references and reconcile the remaining M00 acceptance gaps.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Achieving renderer parity inside M00.
|
||||||
|
- Calibrating unknown manual camera parameters.
|
||||||
|
- Marking the target DONE; only the milestone integrator may do so.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: paired-run exit handling and final evidence record
|
||||||
|
- Shared/hotspots: renderer baseline/module findings
|
||||||
|
- Generated/ignored: final captures and reports under `user://`
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: visual comparison exit 0/1 both count as completed report generation
|
||||||
|
- Schema/format version: unchanged
|
||||||
|
- Migration/compatibility: comparator infrastructure errors remain fatal
|
||||||
|
- Consumers: M00 integrator
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: merged region diagnostic and five private reference images
|
||||||
|
- Blocks: integrator decision on M00 completion
|
||||||
|
- External state: private images remain outside Git
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: complete paired baseline run, report audit, repository gates
|
||||||
|
- Fixtures: five build 12340 references
|
||||||
|
- Fidelity evidence: cold/warm comparison metrics and human gap classification
|
||||||
|
- Performance budget: existing M00 capture budgets
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: PowerShell comparison step semantics
|
||||||
|
- Module specification: final evidence status
|
||||||
|
- Data-flow diagram: unchanged
|
||||||
|
- Sequence/state/dependency diagrams: unchanged
|
||||||
|
- Source map/status updates: baseline acceptance summary
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `Invoke-GodotComparisonStep`
|
||||||
|
- Simplest considered solution: allow only comparator exit codes 0 and 1
|
||||||
|
- Rejected complexity/abstractions: suppressing all process failures
|
||||||
|
- Unavoidable complexity and justification: threshold exceedance is evidence, not infrastructure failure
|
||||||
|
- Measured optimization evidence: not applicable
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: paired-run exit contract fixed; full baseline and visual capture completed; ten pairs audited; final acceptance document written
|
||||||
|
- Next: integrator decides animated/sky reference policy and GUI RID-risk disposition
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: this work-package commit
|
||||||
|
- Results: compared=10, failed=10, missing=0; mean error 0.066892..0.176952; cold/warm gaps stable; full workflow returned success with threshold evidence preserved
|
||||||
|
- Remaining risks: no build 12340 animated-M2 or sky-transition reference; GUI renderer RID/resources still reported at exit
|
||||||
|
- Documentation updated: `docs/M00_FINAL_PAIRED_EVIDENCE.md`, `docs/modules/world-renderer.md`
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-FOV-SWEEP-001 — Empirical camera FOV sweep
|
# M00-QAR-FOV-SWEEP-001 — Empirical camera FOV sweep
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-FOV-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
<!-- OPENWC_CLAIM:M00-QAR-FOV-SWEEP-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-FOV-SWEEP-001:f548507 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-GUI-RID-SHUTDOWN-001 — GUI capture RID shutdown
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-GUI-RID-SHUTDOWN-001:sindo-main-codex:2026-07-15 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-GUI-RID-SHUTDOWN-001:f19c0c4 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-gui-rid-shutdown`
|
||||||
|
- Lease expires UTC: 2026-07-15
|
||||||
|
- Integrator: milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Identify ownership of GUI render-baseline shutdown leaks and apply the smallest regression-covered fix that lets queued world resources release before engine teardown.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Renderer architecture decomposition.
|
||||||
|
- Hiding or filtering Godot leak diagnostics.
|
||||||
|
- Changing M00 status, Evidence or `OPENWC_TARGET_DONE`.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: capture shutdown sequencing and its regression diagnostic
|
||||||
|
- Shared/hotspots: `src/tools/capture_render_checkpoints.gd`, renderer module shutdown documentation
|
||||||
|
- Generated/ignored: GUI capture output and verbose logs under `user://`
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: unchanged
|
||||||
|
- Schema/format version: unchanged
|
||||||
|
- Migration/compatibility: capture images and report schema remain unchanged
|
||||||
|
- Consumers: M00 baseline runner and milestone integrator
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: merged seven-reference evidence audit
|
||||||
|
- Blocks: integrator disposition of the final GUI renderer leak risk
|
||||||
|
- External state: proprietary references are not required for the focused shutdown regression
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: focused GUI single-checkpoint capture with verbose shutdown, headless project load, manifest/self-tests, repository gates
|
||||||
|
- Fixtures: existing non-proprietary render-baseline manifest and local extracted corpus
|
||||||
|
- Fidelity evidence: no visual behavior change; focused PNG hash/report remains valid
|
||||||
|
- Performance budget: shutdown-only change
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: shutdown method semantics if introduced
|
||||||
|
- Module specification: shutdown ownership and observed leak status
|
||||||
|
- Data-flow diagram: unchanged
|
||||||
|
- Sequence/state/dependency diagrams: update shutdown sequence only if behavior changes
|
||||||
|
- Source map/status updates: capture tool if changed
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `_release_runtime_caches_for_shutdown`, `_free_detached_node_cache`, `_queue_free_streamed_node`
|
||||||
|
- Simplest considered solution: explicitly release capture-owned scene references and drain deferred deletion
|
||||||
|
- Rejected complexity/abstractions: global RID registry or suppressed engine diagnostics
|
||||||
|
- Unavoidable complexity and justification: async streaming jobs must finish before resource ownership can drain
|
||||||
|
- Measured optimization evidence: not applicable
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: reproduced the focused leak; assigned detached prototype/resource caches and empty liquid roots to explicit owners; added a headless regression; verified zero Node/resource/RID leak lines in focused and seven-checkpoint GUI captures
|
||||||
|
- Next: integrator review and disposition of the residual engine-level zero-reference `RefCounted` diagnostic
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: this work-package commit
|
||||||
|
- Results: before the fix the focused capture retained 6 mesh, 6 material, 4 shader, 6 texture, 6 instance and RenderingDevice RIDs (30 resources); after the fix focused and seven-checkpoint captures retained none
|
||||||
|
- Remaining risks: D3D12 descriptor fallback remains unrelated; Godot can report a timing-dependent anonymous `RefCounted` whose reference count is already zero, without retained Node/resource/RID ownership
|
||||||
|
- Documentation updated: `RENDER.md`, `docs/modules/world-renderer.md`, `docs/M00_FINAL_PAIRED_EVIDENCE.md`; existing renderer shutdown sequence updated, no new diagram required
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-INTEGRATOR-CLOSEOUT-001 — M00 milestone closeout
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-INTEGRATOR-CLOSEOUT-001:sindo-main-codex:2026-07-15 -->
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M00-QAR-INTEGRATOR-CLOSEOUT-001:ba81098 -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-integrator-closeout`
|
||||||
|
- Lease expires UTC: 2026-07-15
|
||||||
|
- Integrator: sindo-main-codex, assigned by the user on 2026-07-13
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Audit merged M00 work packages, refresh final Evidence, close the milestone and activate M01.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Change renderer behavior or compatibility claims.
|
||||||
|
- Start M01 implementation.
|
||||||
|
- Commit proprietary references or generated capture output.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: M00/M01 target status and final M00 Evidence
|
||||||
|
- Shared/hotspots: `targets/README.md`, `targets/DEVELOPMENT_ROADMAP.md`, completed M00 claims
|
||||||
|
- Generated/ignored: `user://` post-merge reports
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: unchanged
|
||||||
|
- Schema/format version: unchanged
|
||||||
|
- Migration/compatibility: none
|
||||||
|
- Consumers: milestone integrator and M01 work packages
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: merged M00 PRs #1 through #15 and final paired evidence
|
||||||
|
- Blocks: M01 activation
|
||||||
|
- External state: private reference images remain outside Git; their hashes and metrics are already recorded
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: post-merge render baseline dry-run, coordination gate, documentation gate
|
||||||
|
- Fixtures: seven-checkpoint non-proprietary manifest and recorded private-reference hashes/metrics
|
||||||
|
- Fidelity evidence: closeout preserves the explicit non-parity classification in `docs/M00_FINAL_PAIRED_EVIDENCE.md`
|
||||||
|
- Performance budget: no runtime change
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: unchanged
|
||||||
|
- Module specification: unchanged
|
||||||
|
- Data-flow diagram: unchanged
|
||||||
|
- Sequence/state/dependency diagrams: unchanged
|
||||||
|
- Source map/status updates: target index, roadmap current state and M00 Evidence
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: none
|
||||||
|
- Simplest considered solution: status and Evidence-only integrator patch
|
||||||
|
- Rejected complexity/abstractions: new closeout tooling for a one-time milestone transition
|
||||||
|
- Unavoidable complexity and justification: none
|
||||||
|
- Measured optimization evidence: not applicable
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: verified all M00 PRs and merge commits, refreshed final Evidence, closed M00, activated M01 and passed transition gates
|
||||||
|
- Next: review and merge the closeout PR before starting an M01 implementation claim
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `ba81098`
|
||||||
|
- Results: exactly one ACTIVE target (M01); M00 DONE marker accepted; post-merge baseline, comparator self-test, coordination and documentation gates passed
|
||||||
|
- Remaining risks: documented renderer fidelity gaps and D3D12 fallback continue beyond M00
|
||||||
|
- Documentation updated: target status/index, development-roadmap current state and final M00 Evidence
|
||||||
@@ -0,0 +1,79 @@
|
|||||||
|
# M00-QAR-LANDMARK-REGION-DIFF-001 — Landmark-region visual diff
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M00-QAR-LANDMARK-REGION-DIFF-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-LANDMARK-REGION-DIFF-001:3d90c6d -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M00
|
||||||
|
- Program: QAR
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m00-landmark-region-diff`
|
||||||
|
- Lease expires UTC: 2026-07-14
|
||||||
|
- Integrator: milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Rank camera candidates inside an approved common screen region so unrelated grass/sky coverage cannot dominate landmark framing.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Automatic feature detection or image warping.
|
||||||
|
- Different crops for reference and candidate.
|
||||||
|
- Changing manifest camera values without human approval.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: comparator common-region option and sweep pass-through
|
||||||
|
- Shared/hotspots: camera-pose documentation and renderer fidelity findings
|
||||||
|
- Generated/ignored: local ROI sweep images/reports
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: additive comparator `--region x,y,width,height` and sweep `-ComparisonRegion`
|
||||||
|
- Schema/format version: report/ranking schema remains version 1 with additive region metadata
|
||||||
|
- Migration/compatibility: omitted region preserves full-frame behavior
|
||||||
|
- Consumers: M00 paired camera calibration
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: ready-scene joint pitch/FOV sweep
|
||||||
|
- Blocks: landmark-weighted Goldshire framing decision
|
||||||
|
- External state: private build 12340 reference
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: comparator synthetic ROI regression, invalid-bound regression, sweep plan, bounded real ROI sweep, repository gates
|
||||||
|
- Fixtures: synthetic 2x2 images and private Goldshire reference
|
||||||
|
- Fidelity evidence: facade-region ranking plus human inspection
|
||||||
|
- Performance budget: region comparison visits only selected pixels
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: comparator usage and PowerShell parameter
|
||||||
|
- Module specification: ROI finding
|
||||||
|
- Data-flow diagram: approved region into comparator
|
||||||
|
- Sequence/state/dependency diagrams: unchanged
|
||||||
|
- Source map/status updates: camera-pose guide
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `ComparisonRegion`, `comparison_region`
|
||||||
|
- Simplest considered solution: one common integer rectangle
|
||||||
|
- Rejected complexity/abstractions: masks, feature detectors, separate movable crops
|
||||||
|
- Unavoidable complexity and justification: full-frame color error is biased by large terrain regions
|
||||||
|
- Measured optimization evidence: bounded pixel loop
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: common ROI parser/validation/reporting, synthetic inclusion/exclusion regression, sweep pass-through, 15-candidate Goldshire rescoring and human inspection
|
||||||
|
- Next: define explicit approved landmark coordinates or mask; do not calibrate manifest from the common rectangle alone
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: this work-package commit
|
||||||
|
- Results: ROI [300,100,1400,650] ranked FOV 38/yaw 10/pitch -15 first at mean 0.081777 and ratio 0.697185; visually the facade remains oversized and cropped
|
||||||
|
- Remaining risks: rectangle includes non-semantic road/terrain pixels and cannot prove landmark alignment
|
||||||
|
- Documentation updated: `docs/CAMERA_POSE_SWEEP.md`, `docs/modules/world-renderer.md`
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-TERRAIN-HEIGHT-001 — Terrain height diagnostic
|
# M00-QAR-TERRAIN-HEIGHT-001 — Terrain height diagnostic
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-TERRAIN-HEIGHT-001:sindo-main-codex:2026-07-13 -->
|
<!-- OPENWC_CLAIM:M00-QAR-TERRAIN-HEIGHT-001:sindo-main-codex:2026-07-13 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-TERRAIN-HEIGHT-001:f8538ba -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-TILE-OWNERSHIP-001 — Waterfall terrain tile ownership
|
# M00-QAR-TILE-OWNERSHIP-001 — Waterfall terrain tile ownership
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-TILE-OWNERSHIP-001:sindo-main-codex:2026-07-13 -->
|
<!-- OPENWC_CLAIM:M00-QAR-TILE-OWNERSHIP-001:sindo-main-codex:2026-07-13 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-TILE-OWNERSHIP-001:2306200 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-VISUAL-DIFF-001 — Renderer checkpoint visual diff
|
# M00-QAR-VISUAL-DIFF-001 — Renderer checkpoint visual diff
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-VISUAL-DIFF-001:sindo-main-codex:2026-07-13 -->
|
<!-- OPENWC_CLAIM:M00-QAR-VISUAL-DIFF-001:sindo-main-codex:2026-07-13 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-VISUAL-DIFF-001:19df5b7 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00-QAR-WMO-READINESS-001 — Goldshire WMO readiness
|
# M00-QAR-WMO-READINESS-001 — Goldshire WMO readiness
|
||||||
|
|
||||||
<!-- OPENWC_CLAIM:M00-QAR-WMO-READINESS-001:sindo-main-codex:2026-07-14 -->
|
<!-- OPENWC_CLAIM:M00-QAR-WMO-READINESS-001:sindo-main-codex:2026-07-14 -->
|
||||||
|
<!-- OPENWC_INTEGRATION:ACCEPTED:M00-QAR-WMO-READINESS-001:5092995 -->
|
||||||
|
|
||||||
## Ownership
|
## Ownership
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,111 @@
|
|||||||
|
# M01-FND-COORDS-001 — Canonical coordinate contracts
|
||||||
|
|
||||||
|
<!-- OPENWC_CLAIM:M01-FND-COORDS-001:sindo-main-codex:2026-07-15 -->
|
||||||
|
<!-- OPENWC_HANDOFF:READY:M01-FND-COORDS-001:f45695c -->
|
||||||
|
|
||||||
|
## Ownership
|
||||||
|
|
||||||
|
- Target: M01
|
||||||
|
- Program: FND
|
||||||
|
- Owner/Agent ID: sindo-main-codex
|
||||||
|
- Branch: `work/sindo-main-codex/m01-coordinate-mapper`
|
||||||
|
- Lease expires UTC: 2026-07-15
|
||||||
|
- Integrator: M01 milestone integrator
|
||||||
|
|
||||||
|
## Outcome
|
||||||
|
|
||||||
|
Introduce scene-free value objects for canonical WoW, server, ADT placement,
|
||||||
|
ADT tile/chunk/local and Godot world coordinate spaces, plus the single pure
|
||||||
|
`CoordinateMapper` contract for position and world-yaw conversions.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Migrating `StreamingWorldLoader`, native parsers, sky, player or renderer code.
|
||||||
|
- Defining model-specific MDDF/MODF Euler rotation corrections.
|
||||||
|
- Introducing `StreamingFocus`, typed IDs or a test framework dependency.
|
||||||
|
- Claiming visual parity from arithmetic round trips.
|
||||||
|
|
||||||
|
## Paths
|
||||||
|
|
||||||
|
- Exclusive: `src/domain/coordinates/**`, `src/tools/verify_coordinate_mapper.gd`,
|
||||||
|
`docs/modules/coordinate-mapping.md`, `docs/adr/0001-canonical-world-coordinates.md`
|
||||||
|
- Shared/hotspots: `docs/modules/README.md`, this claim
|
||||||
|
- Generated/ignored: Godot import metadata and local test output
|
||||||
|
|
||||||
|
## Contracts and data
|
||||||
|
|
||||||
|
- Public API/events: immutable coordinate value objects and pure mapper methods
|
||||||
|
- Schema/format version: no persisted format; contract version 1 in module docs
|
||||||
|
- Migration/compatibility: additive contract; existing renderer conversions remain
|
||||||
|
unchanged until a later consumer migration package
|
||||||
|
- Consumers: future renderer, network, gameplay and server-data adapters
|
||||||
|
- Rollout order: contract and fixtures first, then native/parser and renderer consumers
|
||||||
|
|
||||||
|
## Dependencies
|
||||||
|
|
||||||
|
- Requires: merged M00 build 12340 coordinate calibration evidence
|
||||||
|
- Blocks: M01 renderer coordinate migration and `StreamingFocus`
|
||||||
|
- External state: no proprietary inputs; reference formulas are source-only evidence
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Commands: headless coordinate contract probe, headless project load,
|
||||||
|
coordination and documentation gates
|
||||||
|
- Fixtures: five build 12340 camera points, ADT map center/edge/boundary points,
|
||||||
|
server/ADT/Godot round trips and yaw normalization cases
|
||||||
|
- Fidelity evidence: M00 observed build 12340 positions plus independent
|
||||||
|
OpenRealm/WoWee and TrinityCore grid formulas
|
||||||
|
- Performance budget: constant-time scalar arithmetic with no allocation beyond result values
|
||||||
|
|
||||||
|
## Documentation deliverables
|
||||||
|
|
||||||
|
- Inline public API docs: every public value object and mapper method
|
||||||
|
- Module specification: purpose, public API, inputs/outputs, ownership, errors and source map
|
||||||
|
- Data-flow diagram: server/ADT/canonical/Godot mappings
|
||||||
|
- Dependency diagram: canonical contract consumers and forbidden scene dependency
|
||||||
|
- Sequence/state diagrams: not applicable; mapper is synchronous and stateless
|
||||||
|
- ADR: coordinate axes, units, tile indexing and compatibility rollout
|
||||||
|
|
||||||
|
## Simplicity and naming
|
||||||
|
|
||||||
|
- Important names introduced: `CanonicalWowWorldPosition`, `ServerWorldPosition`,
|
||||||
|
`AdtPlacementPosition`, `AdtTileCoordinate`, `AdtChunkCoordinate`,
|
||||||
|
`AdtTileLocalPosition`, `GodotWorldPosition`, `WorldYaw`, `CoordinateMapper`
|
||||||
|
- Simplest considered solution: scalar immutable value objects and one pure mapper
|
||||||
|
- Rejected complexity/abstractions: generic vector-space framework, matrices,
|
||||||
|
dependency injection and a third-party test framework
|
||||||
|
- Unavoidable complexity and justification: distinct source spaces prevent silent axis swaps
|
||||||
|
- Measured optimization evidence: not applicable
|
||||||
|
|
||||||
|
## Status
|
||||||
|
|
||||||
|
- State: ready-for-review
|
||||||
|
- Done: repository/reference audit, ADR, immutable value objects, pure mapper,
|
||||||
|
build 12340/boundary/yaw fixtures, module specification and repository gates
|
||||||
|
- Next: integrator review and merge before renderer/native consumer migration
|
||||||
|
- Blocked by:
|
||||||
|
|
||||||
|
## Handoff
|
||||||
|
|
||||||
|
- Commit: `f45695c`
|
||||||
|
- Branch: `work/sindo-main-codex/m01-coordinate-mapper`
|
||||||
|
- Outcome: additive contract for canonical/server/ADT/Godot positions, ADT
|
||||||
|
tile/chunk/local ownership and world-facing yaw; no existing consumer changed
|
||||||
|
- Public contracts: contract version 1 documented in
|
||||||
|
`docs/modules/coordinate-mapping.md`; no persisted schema or cache change
|
||||||
|
- Verification: coordinate mapper PASS (5 golden points, 8 tile boundaries,
|
||||||
|
5 yaw cases); M00 coordinate calibration PASS (5 points, maximum error
|
||||||
|
`0.000015`); renderer manifest, coordination, documentation, dependency and
|
||||||
|
whitespace checks passed; headless project load completed with expected
|
||||||
|
missing proprietary-data diagnostics in the isolated worktree
|
||||||
|
- Fidelity: observed build 12340 positions match within `0.002` yard; formulas
|
||||||
|
cross-checked against TrinityCore, OpenRealm and WoWee; visual parity is not claimed
|
||||||
|
- Migration/rebuild: none for this additive package; consumers migrate only
|
||||||
|
after merge and must preserve renderer checkpoints
|
||||||
|
- Remaining risks: native loaders and renderer still use manual conversions;
|
||||||
|
MDDF/MODF Euler mapping is intentionally unresolved; server-spawn integration
|
||||||
|
and finite-value adapter validation remain later M01 packages
|
||||||
|
- Recommended merge order: this contract first, coordinate fixtures/server-spawn
|
||||||
|
audit second, renderer/native consumers third, manual-conversion search gate last
|
||||||
|
- Documentation: ADR 0001; coordinate-mapping module spec with data-flow and
|
||||||
|
dependency diagrams; inline API documentation on all public symbols
|
||||||
@@ -14,11 +14,12 @@ This offline M00 diagnostic recovers reproducible checkpoint framing when an ori
|
|||||||
-Checkpoint elwynn_adt_boundary `
|
-Checkpoint elwynn_adt_boundary `
|
||||||
-YawOffsets -15,-10,-5,0,5,10,15 `
|
-YawOffsets -15,-10,-5,0,5,10,15 `
|
||||||
-PitchOffsets -10,-5,0,5,10 `
|
-PitchOffsets -10,-5,0,5,10 `
|
||||||
|
-CameraFovValues 38,50,62 `
|
||||||
-ViewportWidth 1280 -ViewportHeight 960 `
|
-ViewportWidth 1280 -ViewportHeight 960 `
|
||||||
-WaitSeconds 8
|
-WaitSeconds 8
|
||||||
```
|
```
|
||||||
|
|
||||||
Each candidate receives its own output directory and comparison report. The viewport must exactly match the reference image dimensions; the runner fails on `size_mismatch` instead of inventing a score. The default 8-second wait matches the M00 manifest and is a readiness precondition: lowering it requires evidence that the checkpoint's WMO/M2 content has already appeared. `ranking.json` sorts candidates by mean perceptual error and then changed-pixel ratio. Use `-PlanOnly` to validate the Cartesian grid and output paths without rendering. Run a coarse grid first, then a finer grid around the best candidate.
|
Each candidate receives its own output directory and comparison report. `-CameraFovValues` adds FOV as another bounded grid dimension; omit it to retain the scalar `-CameraFov` workflow and legacy candidate paths. The viewport must exactly match the reference image dimensions; the runner fails on `size_mismatch` instead of inventing a score. The default 8-second wait matches the M00 manifest and is a readiness precondition: lowering it requires evidence that the checkpoint's WMO/M2 content has already appeared. `ranking.json` sorts candidates by mean perceptual error and then changed-pixel ratio. Use `-PlanOnly` to validate the Cartesian grid and output paths without rendering. Run a coarse grid first, then a finer grid around the best candidate.
|
||||||
|
|
||||||
```mermaid
|
```mermaid
|
||||||
flowchart LR
|
flowchart LR
|
||||||
@@ -47,4 +48,16 @@ These values are invalid camera-pose evidence because the sweep used a 2-second
|
|||||||
|
|
||||||
The ready-scene 3x3 rerun at FOV 62 degrees ranked `(yaw=10, pitch=-10)` first with mean error `0.087952` and changed-pixel ratio `0.665527`, compared with `0.101402`/`0.702489` at zero offsets. Human inspection confirms that the inn is present and the direction is plausible. Pitch remains on the tested boundary and the building scale differs from the reference, so these offsets are coarse evidence only; a joint pitch/FOV refinement is required before changing manifest camera values.
|
The ready-scene 3x3 rerun at FOV 62 degrees ranked `(yaw=10, pitch=-10)` first with mean error `0.087952` and changed-pixel ratio `0.665527`, compared with `0.101402`/`0.702489` at zero offsets. Human inspection confirms that the inn is present and the direction is plausible. Pitch remains on the tested boundary and the building scale differs from the reference, so these offsets are coarse evidence only; a joint pitch/FOV refinement is required before changing manifest camera values.
|
||||||
|
|
||||||
|
## Goldshire joint pitch/FOV refinement — 2026-07-12
|
||||||
|
|
||||||
|
A WMO-ready joint grid fixed yaw at 10 degrees and evaluated pitch `[-25, -20, -15, -10, -5]` across FOV `[38, 50, 62]`. Full-frame error ranked FOV 62/pitch -25 first at `0.078843` with changed-pixel ratio `0.667721`. Human inspection rejects it as a manifest calibration: the view is dominated by grass and crops the inn. FOV 38/pitch -10 is closer to the reference building scale but ranks only `0.084220`, and horizontal landmark alignment remains different.
|
||||||
|
|
||||||
|
The grid therefore has no approved joint optimum. Full-frame color error continues to trade landmark framing against large terrain regions. The manifest remains unchanged. The next registration method must score an approved inn/road/tree landmark region or explicit landmark coordinates rather than the entire image.
|
||||||
|
|
||||||
|
## Common landmark region — 2026-07-12
|
||||||
|
|
||||||
|
The comparator accepts an optional common screen rectangle through `--region x,y,width,height`; the sweep runner exposes it as `-ComparisonRegion x,y,width,height`. Coordinates are integer pixels in the shared reference/candidate image space. Both images must have identical dimensions and the rectangle must be fully inside them. Omitting the option preserves full-frame behavior.
|
||||||
|
|
||||||
|
The existing 15 WMO-ready Goldshire candidates were rescored inside `[300,100,1400,650]`, covering the reference facade/chimney and nearby trees. ROI error ranked FOV 38/yaw 10/pitch -15 first at `0.081777` with changed-pixel ratio `0.697185`. This is an interior pitch candidate and improves on the full-frame grass preference, but human inspection still rejects manifest calibration: the facade is too large, its upper part is cropped, and the rectangle retains substantial road/terrain pixels. A common rectangle is useful diagnostic weighting, not semantic landmark registration. Explicit approved landmark coordinates or a mask remain necessary.
|
||||||
|
|
||||||
During this run the sweep orchestration was hardened in two ways: expected comparator exit code `1` is now collected through an explicit child process, and viewport dimensions are explicit. A reference/candidate `size_mismatch` now fails before ranking instead of producing an empty metric.
|
During this run the sweep orchestration was hardened in two ways: expected comparator exit code `1` is now collected through an explicit child process, and viewport dimensions are explicit. A reference/candidate `size_mismatch` now fails before ranking instead of producing an empty metric.
|
||||||
|
|||||||
@@ -0,0 +1,76 @@
|
|||||||
|
# M00 Final Paired Evidence — 2026-07-13
|
||||||
|
|
||||||
|
## Scope and result
|
||||||
|
|
||||||
|
The final local M00 audit used Godot 4.6.1, the existing cache state, the manifest-standard 8-second readiness wait, and `2560x1440` visual candidates. Seven private WoW 3.3.5a build 12340 references produced fourteen cold/warm comparisons with no missing pair. Private images and generated reports remain outside Git.
|
||||||
|
|
||||||
|
Every pair exceeded the strict regression tolerance. Comparator exit code `1` means a valid report with measured visual gaps; only codes outside `0/1` are infrastructure failures. This evidence establishes coverage and current differences, not renderer parity.
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\tools\run_render_baseline.ps1 `
|
||||||
|
-Output user://render_baseline_m00_complete_2026-07-13 `
|
||||||
|
-CacheState existing `
|
||||||
|
-WaitSeconds 8 `
|
||||||
|
-MeasureSeconds 0.5 `
|
||||||
|
-ReferenceCheckpointDirectory .\sources\OpenWCReferenceCheckpoints `
|
||||||
|
-VisualComparisonReport user://render_baseline_m00_complete_2026-07-13/visual_comparison.json `
|
||||||
|
-VisualComparisonWidth 2560 `
|
||||||
|
-VisualComparisonHeight 1440
|
||||||
|
```
|
||||||
|
|
||||||
|
The initial shell timeout detached the canonical capture process after it started. The process completed all fourteen canonical captures and wrote `report.json`; the documented visual-capture and comparator stages were then invoked separately with the same arguments. The final output directory contains both complete reports.
|
||||||
|
|
||||||
|
## Paired metrics
|
||||||
|
|
||||||
|
| Checkpoint | Pass | Mean perceptual error | Changed-pixel ratio | Classification |
|
||||||
|
|---|---|---:|---:|---|
|
||||||
|
| `elwynn_adt_boundary` | cold | 0.087258 | 0.634580 | Composition plus terrain/vegetation/material gaps |
|
||||||
|
| `elwynn_adt_boundary` | warm | 0.087255 | 0.634598 | Stable against revisit |
|
||||||
|
| `elwynn_sky_dusk` | cold | 0.094778 | 0.575956 | Approximate paired camera; sky color, lighting and scene-composition gaps |
|
||||||
|
| `elwynn_sky_dusk` | warm | 0.094757 | 0.575898 | Stable against revisit |
|
||||||
|
| `elwynn_terrain_overview` | cold | 0.174137 | 0.851791 | Largest composition/framing gap |
|
||||||
|
| `elwynn_terrain_overview` | warm | 0.176955 | 0.861271 | Stable gap, not a cold-load artifact |
|
||||||
|
| `elwynn_waterfall_liquid` | cold | 0.088629 | 0.706522 | Framing, terrain, liquid and lighting gaps |
|
||||||
|
| `elwynn_waterfall_liquid` | warm | 0.087398 | 0.693690 | Stable against revisit |
|
||||||
|
| `goldshire_dense_m2` | cold | 0.066893 | 0.575257 | Smallest available paired gap; vegetation/material differences remain |
|
||||||
|
| `goldshire_dense_m2` | warm | 0.066927 | 0.575592 | Stable against revisit |
|
||||||
|
| `goldshire_inn_large_wmo` | cold | 0.101224 | 0.701835 | WMO is ready; manual framing and presentation gaps remain |
|
||||||
|
| `goldshire_inn_large_wmo` | warm | 0.101206 | 0.701855 | Stable against revisit |
|
||||||
|
| `gryphon_roost_native_animation` | cold | 0.119467 | 0.882073 | Same build-12340 asset, different real/synthetic placement and uncontrolled animation phase |
|
||||||
|
| `gryphon_roost_native_animation` | warm | 0.119471 | 0.882076 | Full-frame metric is diagnostic only; animation observation is human evidence |
|
||||||
|
|
||||||
|
Cold and warm values are close for all seven references. The measured gaps are therefore not explained by first-visit cache readiness. Goldshire timing evidence separately demonstrated that the 8-second wait is necessary for WMO publication.
|
||||||
|
|
||||||
|
## Animation and sky provenance
|
||||||
|
|
||||||
|
The private source corpus preserves exact screenshot bytes; no resize, crop or generated content was used:
|
||||||
|
|
||||||
|
| Evidence | SHA-256 | Interpretation |
|
||||||
|
|---|---|---|
|
||||||
|
| `elwynn_sky_dusk.jpg` | `1fb06836cacae714c76eaf9b8bf7c292fd5e2f2df33eef821bfe4c4a6bd97072` | Build 12340, Elwynn camera position, server time approximately 19:00, UI hidden |
|
||||||
|
| `gryphon_roost_native_animation.jpg` | `d3488c741cb8dfe7212198e48332842631a951862a4388b67045338f37357447` | Build 12340 real-world `GryphonRoost01` placement, first visible wing phase |
|
||||||
|
| diagnostic animation phase B | `1025572c7ab9fa0d2e500ff026be46eb9c1e6e5447116dbdc3675dd5f44574ab` | Same camera three seconds later; visibly raised wing phase |
|
||||||
|
| diagnostic placement context | `b12c0b713a7d75123ed30f5a316b33537331442cd56615aea69214c7cc4e7a42` | Two real Lakeshire placements and surrounding build-12340 scene |
|
||||||
|
|
||||||
|
An ADT scan found no `GryphonRoost01` placement in Goldshire. The M00 checkpoint deliberately injects the model there as a synthetic probe. The original-client observation therefore uses two real Lakeshire placements. The three-second pose change establishes `Observed` native animation behavior, but the full-frame comparison cannot establish spatial or animation-phase parity.
|
||||||
|
|
||||||
|
## Fidelity interpretation
|
||||||
|
|
||||||
|
M00 now has private build 12340 evidence for every required coverage category: terrain, ADT boundary, dense M2, large WMO, liquid, animated M2 and sky transition. The sky reference is approximately paired by map position/time and manually approved composition. Animated-M2 evidence is asset-paired but placement-unpaired because the OpenWC probe is synthetic.
|
||||||
|
|
||||||
|
The reports demonstrate current differences and do not claim `1:1`. Camera calibration experiments also show that unknown manual yaw/pitch/FOV cannot be recovered reliably from full-frame color error alone. Region scoring reduces terrain bias but still requires semantic landmark approval.
|
||||||
|
|
||||||
|
## Operational risks
|
||||||
|
|
||||||
|
A follow-up shutdown ownership fix removed the GUI renderer leaks found by the final visual capture. Before the fix, the focused animated-M2 checkpoint retained 6 mesh, 6 material, 4 shader, 6 texture, 6 instance and associated RenderingDevice RIDs, for 30 resources in use. After the fix, both the focused 8-second capture and a seven-checkpoint single-pass capture completed without Node, resource or RID leak diagnostics.
|
||||||
|
|
||||||
|
Godot can still emit a timing-dependent ObjectDB warning for one or more anonymous `RefCounted` objects whose reported reference count is already zero. The warning has no retained Node, renderer resource or RID ownership in the verified captures. It remains recorded as an engine-teardown diagnostic rather than being hidden, but it is distinct from the resolved GUI renderer leak.
|
||||||
|
|
||||||
|
## Integrator decision checklist
|
||||||
|
|
||||||
|
- Seven required reference checkpoints: cold/warm report complete, `compared=14`, `missing=0`.
|
||||||
|
- Animated M2: build-12340 native motion observed in two phases; synthetic/real placement mismatch explicitly classified.
|
||||||
|
- Sky transition: build-12340 19:00 evidence present with approximate paired camera and human-approved composition.
|
||||||
|
- Renderer visual differences: registered as gaps, not parity claims.
|
||||||
|
- GUI renderer RID/resource diagnostics: resolved by explicit shutdown ownership and covered by a headless regression verifier; the residual zero-reference `RefCounted` engine-teardown warning remains tracked separately.
|
||||||
|
- Target status, target Evidence and `OPENWC_TARGET_DONE` marker: integrator-only actions.
|
||||||
@@ -0,0 +1,151 @@
|
|||||||
|
# ADR 0001: Canonical world coordinate contract
|
||||||
|
|
||||||
|
- Status: Accepted for M01 contract implementation
|
||||||
|
- Date: 2026-07-13
|
||||||
|
- Work package: `M01-FND-COORDS-001`
|
||||||
|
- Decision owners: M01 foundation/integration owners
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
OpenWC currently performs coordinate conversion in native ADT/WDT loaders,
|
||||||
|
`StreamingWorldLoader`, sky, player and renderer diagnostic scripts. The same
|
||||||
|
numbers are frequently carried in `Vector3` or dictionaries, so the coordinate
|
||||||
|
space is implicit. Existing names also mix ADT filename indices, renderer X/Z
|
||||||
|
indices and MDDF/MODF placement fields.
|
||||||
|
|
||||||
|
M00 recorded five positions observed in WoW 3.3.5a build 12340. They verify the
|
||||||
|
current client-position to Godot-position arithmetic within `0.002` yard, but
|
||||||
|
they do not define a cross-layer contract and do not prove camera composition
|
||||||
|
or visual parity.
|
||||||
|
|
||||||
|
Independent references agree on a 64 by 64 ADT grid and an approximately
|
||||||
|
`533.3333` yard tile size. They do not all use the word “canonical” for the same
|
||||||
|
axis order. OpenWC therefore needs names tied to observable formats instead of
|
||||||
|
adopting an internal convention from one reference project.
|
||||||
|
|
||||||
|
## Decision
|
||||||
|
|
||||||
|
### Canonical position
|
||||||
|
|
||||||
|
`CanonicalWowWorldPosition(X, Y, Z)` preserves the numeric X/Y/Z order used by
|
||||||
|
the WoW client, movement protocol and TrinityCore/AzerothCore world positions.
|
||||||
|
X and Y are horizontal; Z is height. Units are WoW yards.
|
||||||
|
|
||||||
|
`ServerWorldPosition` is a distinct type but converts component-for-component
|
||||||
|
to canonical. The type boundary prevents a server record from being passed
|
||||||
|
directly to renderer code and leaves room for adapter validation without a
|
||||||
|
silent axis swap.
|
||||||
|
|
||||||
|
### World extent and ADT indices
|
||||||
|
|
||||||
|
The contract uses:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ADT_TILE_SIZE_YARDS = 1600 / 3
|
||||||
|
ADT_TILES_PER_MAP_AXIS = 64
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS = 32 * ADT_TILE_SIZE_YARDS
|
||||||
|
|
||||||
|
tileX = floor(32 - canonical.X / ADT_TILE_SIZE_YARDS)
|
||||||
|
tileY = floor(32 - canonical.Y / ADT_TILE_SIZE_YARDS)
|
||||||
|
```
|
||||||
|
|
||||||
|
`AdtTileCoordinate(tile_x, tile_y)` always means the indices in
|
||||||
|
`Map_tileX_tileY.adt`. Results are intentionally not clamped: the exact
|
||||||
|
south-east extent produces index 64 and remains diagnosable as out of bounds.
|
||||||
|
Normal valid indices are 0 through 63.
|
||||||
|
|
||||||
|
An `AdtTileLocalPosition` is measured east and south from the tile's
|
||||||
|
north-west corner. An `AdtChunkCoordinate` uses `chunk_x` south and `chunk_y`
|
||||||
|
east, with valid indices 0 through 15. These direction names avoid pretending
|
||||||
|
that ADT file axes are Godot X/Z axes.
|
||||||
|
|
||||||
|
### ADT placement fields
|
||||||
|
|
||||||
|
MDDF/MODF position fields are represented in on-disk X/height/Z order:
|
||||||
|
|
||||||
|
```text
|
||||||
|
adt.X = halfExtent - canonical.X
|
||||||
|
adt.height = canonical.Z
|
||||||
|
adt.Z = halfExtent - canonical.Y
|
||||||
|
```
|
||||||
|
|
||||||
|
Model-local MDDF/MODF Euler corrections are not world orientation. They remain
|
||||||
|
outside this contract until a placement-orientation package has format- and
|
||||||
|
model-specific fixtures.
|
||||||
|
|
||||||
|
### Godot renderer basis
|
||||||
|
|
||||||
|
The compatibility renderer keeps one Godot unit equal to one WoW yard and uses
|
||||||
|
Y-up coordinates from the north-west map extent:
|
||||||
|
|
||||||
|
```text
|
||||||
|
godot.X = halfExtent - canonical.Y # east
|
||||||
|
godot.Y = canonical.Z # up
|
||||||
|
godot.Z = halfExtent - canonical.X # south
|
||||||
|
```
|
||||||
|
|
||||||
|
`GodotWorldPosition` contains scalars rather than `Vector3`. A scene/render
|
||||||
|
adapter may construct `Vector3` only at its boundary.
|
||||||
|
|
||||||
|
### World yaw
|
||||||
|
|
||||||
|
Server/canonical yaw zero faces increasing WoW X; positive yaw turns toward
|
||||||
|
increasing WoW Y. In the mapped Godot basis, Godot forward `-Z` and positive
|
||||||
|
Y-axis yaw produce the same numeric world angle. Mapper conversions therefore
|
||||||
|
normalize the value to `[-PI, PI)` without adding an offset.
|
||||||
|
|
||||||
|
This identity applies to world-facing yaw only. It does not apply to M2/WMO
|
||||||
|
asset axes or placement Euler triples.
|
||||||
|
|
||||||
|
## Alternatives considered
|
||||||
|
|
||||||
|
### Make renderer coordinates canonical
|
||||||
|
|
||||||
|
Rejected because protocol, database and ADT adapters would silently inherit a
|
||||||
|
presentation-specific offset and Y-up basis.
|
||||||
|
|
||||||
|
### Adopt WoWee's internal canonical axis names unchanged
|
||||||
|
|
||||||
|
Rejected because that reference swaps server X/Y into its chosen north/west
|
||||||
|
ordering. OpenWC preserves observable client/server field names and makes the
|
||||||
|
render/ADT transformations explicit instead.
|
||||||
|
|
||||||
|
### Use `Vector3` plus comments
|
||||||
|
|
||||||
|
Rejected because comments cannot prevent server, ADT and Godot values from
|
||||||
|
being passed across the wrong boundary.
|
||||||
|
|
||||||
|
### Generic vector-space or matrix framework
|
||||||
|
|
||||||
|
Rejected as unnecessary complexity for fixed affine transforms and integer
|
||||||
|
grid ownership rules.
|
||||||
|
|
||||||
|
## Consequences
|
||||||
|
|
||||||
|
- Network and server-data adapters can expose typed positions without knowing
|
||||||
|
Godot.
|
||||||
|
- Renderer consumers must explicitly map and then create Godot vectors at their
|
||||||
|
own boundary.
|
||||||
|
- ADT filename indices can no longer be confused with renderer X/Z tile names.
|
||||||
|
- Existing native/parser and renderer conversions remain unchanged until
|
||||||
|
separate consumer migration packages merge after this contract.
|
||||||
|
- Manual axis conversions become review/test violations once migration is
|
||||||
|
complete; this ADR alone does not delete compatibility paths.
|
||||||
|
- Persisted schemas are unchanged. Future serialized positions must include an
|
||||||
|
explicit coordinate-space/version discriminator.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Five M00 build-12340 client camera positions map to the recorded Godot
|
||||||
|
checkpoints within `0.002` yard.
|
||||||
|
- Server, ADT placement, Godot and tile-local conversions round-trip.
|
||||||
|
- Exact center, tile, chunk and world-extent boundaries have explicit tests.
|
||||||
|
- World-yaw normalization and round-trip cases cover cardinal angles and wrap.
|
||||||
|
|
||||||
|
## References
|
||||||
|
|
||||||
|
- [TrinityCore GridDefines](https://github.com/TrinityCore/TrinityCore/blob/master/src/server/game/Grids/GridDefines.h)
|
||||||
|
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_adt.c`
|
||||||
|
- `reference/open-realm/games/world-of-warcraft/renderer/wow/r_wowmap_objects.c`
|
||||||
|
- `reference/WoWee/include/core/coordinates.hpp`
|
||||||
|
- [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||||
@@ -7,6 +7,7 @@
|
|||||||
| Module | Current status | Primary specification |
|
| Module | Current status | Primary specification |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
|
| Foundation/data | Planned/Partial | [`../../targets/roadmap/01-foundation-and-data.md`](../../targets/roadmap/01-foundation-and-data.md) |
|
||||||
|
| Coordinate mapping | Implemented | [`coordinate-mapping.md`](coordinate-mapping.md) |
|
||||||
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
| Renderer | Partial | [`world-renderer.md`](world-renderer.md), [`../../RENDER.md`](../../RENDER.md) |
|
||||||
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
| Network/session | Planned | [`../../targets/roadmap/03-network-and-session.md`](../../targets/roadmap/03-network-and-session.md) |
|
||||||
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
| Gameplay | Prototype/Planned | [`../../targets/roadmap/04-gameplay-systems.md`](../../targets/roadmap/04-gameplay-systems.md) |
|
||||||
|
|||||||
@@ -0,0 +1,218 @@
|
|||||||
|
# Coordinate Mapping
|
||||||
|
|
||||||
|
## Metadata
|
||||||
|
|
||||||
|
| Field | Value |
|
||||||
|
|---|---|
|
||||||
|
| Status | Implemented |
|
||||||
|
| Target/work package | `M01-FND-COORDS-001` |
|
||||||
|
| Owners | Foundation/domain |
|
||||||
|
| Last verified | Worktree `work/sindo-main-codex/m01-coordinate-mapper`, 2026-07-13 |
|
||||||
|
| Profiles/capabilities | All profiles; contract version 1 |
|
||||||
|
|
||||||
|
## Purpose
|
||||||
|
|
||||||
|
Provide scene-free value objects and one pure `CoordinateMapper` for positions,
|
||||||
|
ADT grid ownership and world-facing yaw. The module makes every source space
|
||||||
|
explicit before renderer, network, gameplay and authoring consumers migrate.
|
||||||
|
|
||||||
|
## Non-goals
|
||||||
|
|
||||||
|
- Reading ADT, packet, SQL or scene data.
|
||||||
|
- Constructing `Vector3`, `Transform3D`, Node or Resource values.
|
||||||
|
- Correcting M2/WMO local axes or MDDF/MODF Euler rotations.
|
||||||
|
- Selecting maps, validating terrain height or owning streaming focus.
|
||||||
|
- Persisting coordinate values in this work package.
|
||||||
|
|
||||||
|
## Context and boundaries
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Server[Server / movement X,Y,Z] --> ServerPosition[ServerWorldPosition]
|
||||||
|
ADT[MDDF/MODF X,height,Z] --> AdtPosition[AdtPlacementPosition]
|
||||||
|
ServerPosition --> Mapper[CoordinateMapper]
|
||||||
|
AdtPosition --> Mapper
|
||||||
|
Mapper --> Canonical[CanonicalWowWorldPosition]
|
||||||
|
Canonical --> Tile[AdtTileCoordinate + AdtTileLocalPosition]
|
||||||
|
Canonical --> GodotPosition[GodotWorldPosition]
|
||||||
|
GodotPosition --> Adapter[Future scene/render adapter]
|
||||||
|
Adapter --> Vector[Vector3 at boundary]
|
||||||
|
Scene[Node / Camera3D / scene lookup] -. forbidden .-> Mapper
|
||||||
|
```
|
||||||
|
|
||||||
|
Allowed dependencies:
|
||||||
|
|
||||||
|
- GDScript scalar math and `RefCounted` lifetime;
|
||||||
|
- other value objects in `src/domain/coordinates`;
|
||||||
|
- callers in network, gameplay, render and server-data adapters.
|
||||||
|
|
||||||
|
Forbidden dependencies:
|
||||||
|
|
||||||
|
- `Node`, `Resource`, SceneTree, Camera3D and global scene lookup;
|
||||||
|
- packet buffers, SQL rows, asset parsers and renderer state;
|
||||||
|
- `Vector3` as a public mapper input or output;
|
||||||
|
- model-specific placement corrections.
|
||||||
|
|
||||||
|
## Public API
|
||||||
|
|
||||||
|
| Symbol | Kind | Purpose | Thread/lifetime | Errors |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| `CanonicalWowWorldPosition` | Immutable value | Client/server X/Y/Z in yards | Any thread; caller-owned reference | No bounds validation |
|
||||||
|
| `ServerWorldPosition` | Immutable value | Server/wire X/Y/Z in yards | Any thread; caller-owned reference | No adapter validation |
|
||||||
|
| `AdtPlacementPosition` | Immutable value | MDDF/MODF X/height/Z fields | Any thread; caller-owned reference | No file validation |
|
||||||
|
| `GodotWorldPosition` | Immutable value | Godot east/up/south scalars | Any thread; caller-owned reference | Does not create `Vector3` |
|
||||||
|
| `AdtTileCoordinate` | Immutable value | `Map_tileX_tileY.adt` indices | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
|
||||||
|
| `AdtTileLocalPosition` | Immutable value | East/height/south offsets from tile north-west corner | Any thread; caller-owned reference | Values are intentionally unclamped |
|
||||||
|
| `AdtChunkCoordinate` | Immutable value | MCNK chunk indices inside one tile | Any thread; caller-owned reference | `is_within_map_grid()` reports invalid indices |
|
||||||
|
| `CanonicalWowWorldYaw` | Immutable value | Canonical world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||||
|
| `ServerWorldYaw` | Immutable value | Movement orientation in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||||
|
| `GodotWorldYaw` | Immutable value | Godot Y-axis world yaw in radians | Any thread; caller-owned reference | Constructor does not normalize |
|
||||||
|
| `CoordinateMapper.server_to_canonical` / inverse | Pure static methods | Preserve server X/Y/Z as canonical | Synchronous; new result value | Null input is a caller contract violation |
|
||||||
|
| `CoordinateMapper.canonical_to_godot` / inverse | Pure static methods | Apply Y-up renderer basis and map offset | Synchronous; new result value | Non-finite scalars propagate |
|
||||||
|
| `CoordinateMapper.canonical_to_adt_placement` / inverse | Pure static methods | Convert ADT placement field basis | Synchronous; new result value | Non-finite scalars propagate |
|
||||||
|
| `CoordinateMapper.canonical_to_adt_tile` | Pure static method | Compute unclamped filename indices | Synchronous; new result value | Out-of-map result remains invalid |
|
||||||
|
| `CoordinateMapper.canonical_to_adt_tile_local` / inverse | Pure static methods | Split/reconstruct tile-local position | Synchronous; new result value | Caller validates tile ownership |
|
||||||
|
| `CoordinateMapper.adt_tile_local_to_chunk` | Pure static method | Compute unclamped MCNK indices | Synchronous; new result value | Boundary/out-of-range remains diagnosable |
|
||||||
|
| World-yaw mapper methods | Pure static methods | Normalize and cross the world-basis boundary | Synchronous; new result value | Non-finite input is unsupported |
|
||||||
|
|
||||||
|
## Inputs and outputs
|
||||||
|
|
||||||
|
| Direction | Contract/data | Producer | Consumer | Ownership | Thread/lifetime |
|
||||||
|
|---|---|---|---|---|---|
|
||||||
|
| Input | `ServerWorldPosition`, `ServerWorldYaw` | Future protocol/server adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||||
|
| Input | `AdtPlacementPosition` | Future ADT adapter | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||||
|
| Input | `CanonicalWowWorldPosition`, `CanonicalWowWorldYaw` | Gameplay/content/domain | `CoordinateMapper` | Immutable caller value | Any thread, call duration |
|
||||||
|
| Output | `CanonicalWowWorldPosition` | `CoordinateMapper` | Gameplay/content/adapters | New immutable value | Caller-owned reference |
|
||||||
|
| Output | `AdtTileCoordinate`, `AdtTileLocalPosition`, `AdtChunkCoordinate` | `CoordinateMapper` | Streaming/content adapters | New immutable values | Caller-owned references |
|
||||||
|
| Output | `GodotWorldPosition`, `GodotWorldYaw` | `CoordinateMapper` | Future render/scene adapter | New immutable values | Caller-owned references |
|
||||||
|
|
||||||
|
Side effects:
|
||||||
|
|
||||||
|
- none;
|
||||||
|
- no scene mutation, file/cache write, network send, database access or logging.
|
||||||
|
|
||||||
|
## Data flow
|
||||||
|
|
||||||
|
```mermaid
|
||||||
|
flowchart LR
|
||||||
|
Server[ServerWorldPosition X,Y,Z] -->|identity components| Canonical[CanonicalWowWorldPosition]
|
||||||
|
ADT[AdtPlacementPosition X,height,Z] -->|half extent - horizontal| Canonical
|
||||||
|
Canonical -->|floor 32 - axis / tile size| Tile[AdtTileCoordinate]
|
||||||
|
Canonical -->|north-west edge offsets| Local[AdtTileLocalPosition]
|
||||||
|
Local -->|floor offset / chunk size| Chunk[AdtChunkCoordinate]
|
||||||
|
Canonical -->|half extent - Y; Z; half extent - X| Godot[GodotWorldPosition]
|
||||||
|
ServerYaw[ServerWorldYaw] -->|normalize only| CanonicalYaw[CanonicalWowWorldYaw]
|
||||||
|
CanonicalYaw -->|same numeric world angle| GodotYaw[GodotWorldYaw]
|
||||||
|
```
|
||||||
|
|
||||||
|
## Lifecycle/state
|
||||||
|
|
||||||
|
The module is stateless. Each call reads immutable scalar inputs and returns a
|
||||||
|
new value object. There is no initialization, shutdown, cancellation or retry
|
||||||
|
state machine.
|
||||||
|
|
||||||
|
## Main sequence
|
||||||
|
|
||||||
|
No cross-layer asynchronous sequence occurs inside this module. Producers parse
|
||||||
|
or validate their external format first, call the mapper synchronously, and
|
||||||
|
then pass only typed results across the next boundary.
|
||||||
|
|
||||||
|
## Ownership, threading and resources
|
||||||
|
|
||||||
|
- Value objects own only scalar fields and, for chunks, one immutable tile reference.
|
||||||
|
- Callers own returned `RefCounted` references.
|
||||||
|
- Methods do not mutate inputs or shared state and are safe for worker use.
|
||||||
|
- No Node, RID, file, socket, database or cache resource is acquired.
|
||||||
|
|
||||||
|
## Errors, cancellation and recovery
|
||||||
|
|
||||||
|
| Failure | Detection | Behavior | Diagnostic | Recovery |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Tile/chunk outside standard grid | `is_within_map_grid()` | Unclamped indices are preserved | Caller reports source and map context | Reject or apply explicit map policy |
|
||||||
|
| Tile-local value outside half-open tile | Consumer boundary validation | Value is preserved | Caller reports source coordinate | Recompute ownership or reject malformed input |
|
||||||
|
| Null value object | Static typing/call failure | Programming contract violation | GDScript error with call site | Fix producer; no fallback value |
|
||||||
|
| NaN/infinite scalar | Adapter validation | Arithmetic propagates invalid value | Adapter must report source field | Reject malformed external input |
|
||||||
|
|
||||||
|
There is no cancellation because every conversion is constant-time scalar math.
|
||||||
|
|
||||||
|
## Configuration and capabilities
|
||||||
|
|
||||||
|
| Setting/capability | Default | Profile | Runtime mutable | Effect |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| Contract version | 1 | All | No | Axis, unit and boundary semantics in ADR 0001 |
|
||||||
|
| Godot units per WoW yard | 1 | `Blizzlike335` and current tools | No | Preserves current renderer scale |
|
||||||
|
| ADT grid | 64 tiles/axis, 16 chunks/tile | WotLK 3.3.5a | No | Defines index and boundary math |
|
||||||
|
|
||||||
|
## Persistence, cache and migration
|
||||||
|
|
||||||
|
This package adds no persisted format, cache or migration. Existing dictionaries,
|
||||||
|
native `Vector3` payloads and renderer conversion helpers remain compatible.
|
||||||
|
Consumer migration is ordered after this contract and must remove old manual
|
||||||
|
conversions only after its producer/consumer tests pass.
|
||||||
|
|
||||||
|
Any future serialized coordinate must store a schema version and explicit space;
|
||||||
|
raw three-number arrays are not an accepted new persistence contract.
|
||||||
|
|
||||||
|
## Diagnostics and observability
|
||||||
|
|
||||||
|
- Logs: none in production mapper.
|
||||||
|
- Metrics: none; conversion cost is constant and outside frame-budget concern.
|
||||||
|
- Debug views: headless contract probe prints one PASS summary.
|
||||||
|
- Correlation IDs: supplied by external adapters if an invalid source is reported.
|
||||||
|
|
||||||
|
## Verification
|
||||||
|
|
||||||
|
- Unit/contract tests: `src/tools/verify_coordinate_mapper.gd`.
|
||||||
|
- Integration/E2E: consumer migration and `StreamingFocus` packages remain pending.
|
||||||
|
- Fidelity evidence: five build-12340 camera positions from the M00 manifest map
|
||||||
|
within `0.002` yard; independent reference formulas are recorded in ADR 0001.
|
||||||
|
- Performance budgets: constant-time arithmetic; no scene or file access.
|
||||||
|
- Manual diagnostics: not required for the pure contract; renderer checkpoints
|
||||||
|
are required when consumers migrate.
|
||||||
|
|
||||||
|
## Extension points
|
||||||
|
|
||||||
|
- Add typed map IDs around positions without changing axis semantics.
|
||||||
|
- Add a format-specific placement-orientation adapter with its own fixtures.
|
||||||
|
- Add serialization only with explicit space/version fields and migrations.
|
||||||
|
- Add consumer adapters that construct engine-native vectors at their boundary.
|
||||||
|
|
||||||
|
## Capability status
|
||||||
|
|
||||||
|
| Capability | Status | Evidence | Gap/next step |
|
||||||
|
|---|---|---|---|
|
||||||
|
| Canonical/server/Godot position mapping | Implemented | Headless round-trip and five M00 golden points | Migrate producers/consumers |
|
||||||
|
| ADT placement mapping | Implemented | Synthetic/direct round-trip fixtures | Compare native parsed placements during migration |
|
||||||
|
| ADT tile/local/chunk ownership | Implemented | Center, extent, tile and chunk boundary fixtures | Migrate streaming names/ownership |
|
||||||
|
| World-facing yaw mapping | Implemented | Cardinal/wrap round-trip fixtures | Placement Euler remains separate |
|
||||||
|
| Scene-free production contract | Implemented | Source boundary and headless test | Add dependency search gate later in M01 |
|
||||||
|
| Visual/server-spawn integration | Planned | M01 target criteria | Add golden server spawn after adapter migration |
|
||||||
|
|
||||||
|
## Known gaps and risks
|
||||||
|
|
||||||
|
- Existing native loaders and renderer scripts still contain manual conversions.
|
||||||
|
- Current `StreamingWorldLoader` tile names may represent Godot X/Z ordering rather
|
||||||
|
than ADT filename X/Y; migration must preserve visible baseline behavior.
|
||||||
|
- MDDF/MODF rotation comments and implementations disagree across existing paths;
|
||||||
|
this package deliberately does not generalize them into world yaw.
|
||||||
|
- Finite-number validation belongs to external format adapters and is not yet implemented.
|
||||||
|
- Arithmetic agreement does not prove terrain, placement, camera direction or visual parity.
|
||||||
|
|
||||||
|
## Source map
|
||||||
|
|
||||||
|
| Path | Responsibility |
|
||||||
|
|---|---|
|
||||||
|
| `src/domain/coordinates/coordinate_mapper.gd` | Constants and all coordinate-axis conversions |
|
||||||
|
| `src/domain/coordinates/*_position.gd` | Immutable typed position values |
|
||||||
|
| `src/domain/coordinates/adt_tile_coordinate.gd` | ADT filename tile indices and bounds |
|
||||||
|
| `src/domain/coordinates/adt_chunk_coordinate.gd` | MCNK indices and bounds |
|
||||||
|
| `src/domain/coordinates/*_yaw.gd` | Immutable typed world-facing yaw values |
|
||||||
|
| `src/tools/verify_coordinate_mapper.gd` | Headless fixtures and boundary regressions |
|
||||||
|
| `docs/adr/0001-canonical-world-coordinates.md` | Normative axes, units, boundaries and rollout decision |
|
||||||
|
|
||||||
|
## Related decisions and references
|
||||||
|
|
||||||
|
- ADR: [`../adr/0001-canonical-world-coordinates.md`](../adr/0001-canonical-world-coordinates.md)
|
||||||
|
- M00 evidence: [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||||
|
- Architecture: [`../ARCHITECTURE.md`](../ARCHITECTURE.md)
|
||||||
|
- Testing rules: [`../TESTING.md`](../TESTING.md)
|
||||||
@@ -143,7 +143,8 @@ sequenceDiagram
|
|||||||
Stream->>Render: evict outside retention range
|
Stream->>Render: evict outside retention range
|
||||||
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
|
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
|
||||||
Stream->>Stream: shutdown: finish registered ResourceLoader requests
|
Stream->>Stream: shutdown: finish registered ResourceLoader requests
|
||||||
Stream->>Render: clear queues, nodes, caches and RIDs
|
Stream->>Render: clear queues and owned tree nodes/RIDs
|
||||||
|
Stream->>Stream: free detached prototypes and release resource caches
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ownership, threading and resources
|
## Ownership, threading and resources
|
||||||
@@ -153,6 +154,8 @@ sequenceDiagram
|
|||||||
- Parsed/grouped results передаются обратно через guarded result queues.
|
- Parsed/grouped results передаются обратно через guarded result queues.
|
||||||
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
|
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
|
||||||
- Shutdown/map switch обязан дождаться WorkerThreadPool jobs и зарегистрированных threaded ResourceLoader requests до очистки очередей, nodes, caches и RIDs.
|
- Shutdown/map switch обязан дождаться WorkerThreadPool jobs и зарегистрированных threaded ResourceLoader requests до очистки очередей, nodes, caches и RIDs.
|
||||||
|
- Parented streamed nodes остаются owned by SceneTree during shutdown; detached M2/WMO prototype caches are explicitly `free()`d after asynchronous work drains.
|
||||||
|
- A temporary liquid root with no generated child geometry is explicitly freed by `ADTBuilder` instead of being returned or abandoned.
|
||||||
- Cache resources считаются immutable после публикации.
|
- Cache resources считаются immutable после публикации.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## Errors, cancellation and recovery
|
||||||
@@ -165,7 +168,7 @@ sequenceDiagram
|
|||||||
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
|
| Main-thread hitch | Named section timing | Frame spike, work remains queued | `HITCH` log | Lower budget/fix finalize path |
|
||||||
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
|
| D3D12 descriptor exhaustion | Rendering backend error | Render failure/fallback backend | Godot error + baseline report | Dedup resources/fix settings |
|
||||||
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
|
| Teleport/map change | Focus/session transition | Old jobs become stale | Target/session generation | Cancel/drop stale results |
|
||||||
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report | Drain owned worker and resource requests before clearing registries |
|
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report + cache shutdown verifier | Drain requests, preserve tree ownership, then free detached prototypes and clear resource caches |
|
||||||
|
|
||||||
## Configuration and capabilities
|
## Configuration and capabilities
|
||||||
|
|
||||||
@@ -197,7 +200,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
|
|
||||||
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
|
- Unit/contract tests: material mapping, unique-ID dedupe, placement probes, baseline manifest, five-point coordinate calibration, synthetic perceptual checkpoint diff, camera-pose grid plan.
|
||||||
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
|
- Integration/E2E: Eastern Kingdoms/Kalimdor streaming scenes and seven cold/warm checkpoints.
|
||||||
- Fidelity evidence: пять локальных build 12340 reference JPG откалибровали terrain/ADT/M2/WMO/liquid viewpoints; automated paired-image metrics exist, но synthetic animation/dusk и полный human approval ещё не закрыты.
|
- Fidelity evidence: семь локальных build 12340 reference JPG покрывают terrain/ADT/M2/WMO/liquid/sky и реальный `GryphonRoost01`; automated cold/warm metrics имеют `compared=14`, `missing=0`. Sky использует приблизительно paired camera, а animated-M2 остаётся asset-paired/placement-unpaired из-за synthetic probe.
|
||||||
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
|
- Performance budgets: M00 report records cold/warm p95 and max hitch; no final acceptance threshold yet.
|
||||||
- Manual diagnostics: [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md) and [`../../RENDER.md`](../../RENDER.md).
|
- Manual diagnostics: [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md) and [`../../RENDER.md`](../../RENDER.md).
|
||||||
|
|
||||||
@@ -232,7 +235,10 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- Camera-pose sweep can now rank bounded yaw/pitch grids without changing manifest defaults; perceptual ranking remains diagnostic and requires human framing approval.
|
- Camera-pose sweep can now rank bounded yaw/pitch grids without changing manifest defaults; perceptual ranking remains diagnostic and requires human framing approval.
|
||||||
- The first Goldshire pose grid was invalidated by WMO readiness: the inn is absent after 2 seconds but visible after the manifest-standard 8-second wait. Pose comparisons must stabilize asynchronous checkpoint content first.
|
- The first Goldshire pose grid was invalidated by WMO readiness: the inn is absent after 2 seconds but visible after the manifest-standard 8-second wait. Pose comparisons must stabilize asynchronous checkpoint content first.
|
||||||
- A ready-scene Goldshire 3x3 grid improved mean error from `0.101402` at zero offsets to `0.087952` at yaw `10`/pitch `-10`; pitch/FOV refinement remains required before manifest calibration.
|
- A ready-scene Goldshire 3x3 grid improved mean error from `0.101402` at zero offsets to `0.087952` at yaw `10`/pitch `-10`; pitch/FOV refinement remains required before manifest calibration.
|
||||||
- D3D12 descriptor issues remain; the capture-path anonymous `RefCounted` shutdown leak is regression-covered by a clean verbose dry-run, while other RID/resource diagnostics still require independent evidence.
|
- Joint Goldshire pitch/FOV refinement had no human-approved optimum: full-frame error preferred grass-heavy FOV 62/pitch -25, while FOV 38/pitch -10 better matched building scale. Landmark/region scoring is required before calibration.
|
||||||
|
- Common-region scoring `[300,100,1400,650]` moved Goldshire's numeric optimum to FOV 38/yaw 10/pitch -15, but human inspection still found an oversized/cropped facade; semantic landmarks or masks remain required.
|
||||||
|
- Final paired audit compared fourteen cold/warm images for seven private build 12340 references with zero missing pairs; mean error ranges `0.066893..0.176955`. Animated-M2 native motion is observed across two original-client phases, but its real Lakeshire placement differs from the synthetic Goldshire probe. Sky-transition evidence uses the checkpoint map position/time with manually approved approximate framing.
|
||||||
|
- D3D12 descriptor issues remain. Explicit shutdown ownership removes the GUI capture's Node/resource/RID leaks in focused and seven-checkpoint runs; a timing-dependent ObjectDB warning for anonymous zero-reference `RefCounted` objects remains as a separate engine-teardown diagnostic.
|
||||||
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
- M2/WMO/material/particle/ribbon/portal parity incomplete.
|
||||||
- Public API is mostly exported configuration rather than stable contracts.
|
- Public API is mostly exported configuration rather than stable contracts.
|
||||||
|
|
||||||
@@ -250,6 +256,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
| `src/tools/build_*cache.gd`, `src/tools/bake_*cache.gd` | Offline cache generation |
|
| `src/tools/build_*cache.gd`, `src/tools/bake_*cache.gd` | Offline cache generation |
|
||||||
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
|
| `tools/run_render_baseline.ps1` | Unified M00 baseline runner |
|
||||||
| `src/tools/compare_render_checkpoints.gd` | Offline JPG/PNG paired-image perceptual metrics and JSON pass/fail report |
|
| `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 |
|
| `src/tools/capture_render_checkpoints.gd` | Deterministic no-roll checkpoint camera, performance and visual capture |
|
||||||
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
|
| `tools/sweep_render_checkpoint_camera_pose.ps1` | Offline yaw/pitch capture grid and paired-error ranking |
|
||||||
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
|
| `src/tools/verify_render_coordinate_calibration.gd` | Build 12340 camera-coordinate golden point round-trip diagnostic |
|
||||||
@@ -261,5 +268,6 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
|
|||||||
- [`../../RENDER.md`](../../RENDER.md)
|
- [`../../RENDER.md`](../../RENDER.md)
|
||||||
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md)
|
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md)
|
||||||
- [`../CAMERA_POSE_SWEEP.md`](../CAMERA_POSE_SWEEP.md)
|
- [`../CAMERA_POSE_SWEEP.md`](../CAMERA_POSE_SWEEP.md)
|
||||||
|
- [`../M00_FINAL_PAIRED_EVIDENCE.md`](../M00_FINAL_PAIRED_EVIDENCE.md)
|
||||||
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
- [`../../targets/roadmap/02-rendering-and-graphics.md`](../../targets/roadmap/02-rendering-and-graphics.md)
|
||||||
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
|
||||||
|
|||||||
@@ -0,0 +1,44 @@
|
|||||||
|
class_name AdtChunkCoordinate
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable MCNK chunk indices inside one ADT tile.
|
||||||
|
## `chunk_x` grows south and `chunk_y` grows east, matching the canonical
|
||||||
|
## tile conversion contract rather than Godot's X/Z member names.
|
||||||
|
|
||||||
|
const MINIMUM_CHUNK_INDEX: int = 0
|
||||||
|
const MAXIMUM_CHUNK_INDEX: int = 15
|
||||||
|
|
||||||
|
var tile_coordinate: AdtTileCoordinate:
|
||||||
|
get:
|
||||||
|
return _tile_coordinate
|
||||||
|
|
||||||
|
var chunk_x: int:
|
||||||
|
get:
|
||||||
|
return _chunk_x
|
||||||
|
|
||||||
|
var chunk_y: int:
|
||||||
|
get:
|
||||||
|
return _chunk_y
|
||||||
|
|
||||||
|
var _tile_coordinate: AdtTileCoordinate
|
||||||
|
var _chunk_x: int
|
||||||
|
var _chunk_y: int
|
||||||
|
|
||||||
|
|
||||||
|
## Creates chunk indices without clamping so invalid parser input is visible.
|
||||||
|
func _init(tile_coordinate_value: AdtTileCoordinate, chunk_x_value: int, chunk_y_value: int) -> void:
|
||||||
|
_tile_coordinate = tile_coordinate_value
|
||||||
|
_chunk_x = chunk_x_value
|
||||||
|
_chunk_y = chunk_y_value
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether the tile and both chunk indices are inside standard bounds.
|
||||||
|
func is_within_map_grid() -> bool:
|
||||||
|
return (
|
||||||
|
_tile_coordinate != null
|
||||||
|
and _tile_coordinate.is_within_map_grid()
|
||||||
|
and _chunk_x >= MINIMUM_CHUNK_INDEX
|
||||||
|
and _chunk_x <= MAXIMUM_CHUNK_INDEX
|
||||||
|
and _chunk_y >= MINIMUM_CHUNK_INDEX
|
||||||
|
and _chunk_y <= MAXIMUM_CHUNK_INDEX
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b1lf5w5p3b6ye
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class_name AdtPlacementPosition
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable MDDF/MODF position in ADT file field order.
|
||||||
|
## `x_offset_yards` and `z_offset_yards` are measured from the north-west map
|
||||||
|
## extent; `height_yards` is the vertical field stored between them.
|
||||||
|
|
||||||
|
var x_offset_yards: float:
|
||||||
|
get:
|
||||||
|
return _x_offset_yards
|
||||||
|
|
||||||
|
var height_yards: float:
|
||||||
|
get:
|
||||||
|
return _height_yards
|
||||||
|
|
||||||
|
var z_offset_yards: float:
|
||||||
|
get:
|
||||||
|
return _z_offset_yards
|
||||||
|
|
||||||
|
var _x_offset_yards: float
|
||||||
|
var _height_yards: float
|
||||||
|
var _z_offset_yards: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates an ADT placement position in on-disk X/height/Z field order.
|
||||||
|
func _init(x_offset_yards_value: float, height_yards_value: float, z_offset_yards_value: float) -> void:
|
||||||
|
_x_offset_yards = x_offset_yards_value
|
||||||
|
_height_yards = height_yards_value
|
||||||
|
_z_offset_yards = z_offset_yards_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cxxlbkjmdtg7m
|
||||||
@@ -0,0 +1,35 @@
|
|||||||
|
class_name AdtTileCoordinate
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable indices from an ADT filename `Map_tileX_tileY.adt`.
|
||||||
|
## Both axes use the inclusive range 0..63 for a normal WotLK world map.
|
||||||
|
|
||||||
|
const MINIMUM_TILE_INDEX: int = 0
|
||||||
|
const MAXIMUM_TILE_INDEX: int = 63
|
||||||
|
|
||||||
|
var tile_x: int:
|
||||||
|
get:
|
||||||
|
return _tile_x
|
||||||
|
|
||||||
|
var tile_y: int:
|
||||||
|
get:
|
||||||
|
return _tile_y
|
||||||
|
|
||||||
|
var _tile_x: int
|
||||||
|
var _tile_y: int
|
||||||
|
|
||||||
|
|
||||||
|
## Creates file indices without clamping so out-of-map input remains diagnosable.
|
||||||
|
func _init(tile_x_value: int, tile_y_value: int) -> void:
|
||||||
|
_tile_x = tile_x_value
|
||||||
|
_tile_y = tile_y_value
|
||||||
|
|
||||||
|
|
||||||
|
## Returns whether both file indices address the standard 64 by 64 ADT grid.
|
||||||
|
func is_within_map_grid() -> bool:
|
||||||
|
return (
|
||||||
|
_tile_x >= MINIMUM_TILE_INDEX
|
||||||
|
and _tile_x <= MAXIMUM_TILE_INDEX
|
||||||
|
and _tile_y >= MINIMUM_TILE_INDEX
|
||||||
|
and _tile_y <= MAXIMUM_TILE_INDEX
|
||||||
|
)
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bg0a286yty1jw
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class_name AdtTileLocalPosition
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable position relative to the north-west corner of one ADT tile.
|
||||||
|
## East and south offsets are normally in `[0, ADT_TILE_SIZE_YARDS)`; height
|
||||||
|
## remains in the canonical vertical space.
|
||||||
|
|
||||||
|
var east_yards: float:
|
||||||
|
get:
|
||||||
|
return _east_yards
|
||||||
|
|
||||||
|
var height_yards: float:
|
||||||
|
get:
|
||||||
|
return _height_yards
|
||||||
|
|
||||||
|
var south_yards: float:
|
||||||
|
get:
|
||||||
|
return _south_yards
|
||||||
|
|
||||||
|
var _east_yards: float
|
||||||
|
var _height_yards: float
|
||||||
|
var _south_yards: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a tile-local position without clamping boundary values.
|
||||||
|
func _init(east_yards_value: float, height_yards_value: float, south_yards_value: float) -> void:
|
||||||
|
_east_yards = east_yards_value
|
||||||
|
_height_yards = height_yards_value
|
||||||
|
_south_yards = south_yards_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cpit2uvrdud6h
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class_name CanonicalWowWorldPosition
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable WoW 3.3.5a world position in client/server axis order and yards.
|
||||||
|
## X and Y are the horizontal coordinates reported by the client and world
|
||||||
|
## server; Z is height. This type deliberately does not expose a [Vector3].
|
||||||
|
|
||||||
|
var x_yards: float:
|
||||||
|
get:
|
||||||
|
return _x_yards
|
||||||
|
|
||||||
|
var y_yards: float:
|
||||||
|
get:
|
||||||
|
return _y_yards
|
||||||
|
|
||||||
|
var z_yards: float:
|
||||||
|
get:
|
||||||
|
return _z_yards
|
||||||
|
|
||||||
|
var _x_yards: float
|
||||||
|
var _y_yards: float
|
||||||
|
var _z_yards: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a canonical position without changing precision or validating map bounds.
|
||||||
|
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
|
||||||
|
_x_yards = x_yards_value
|
||||||
|
_y_yards = y_yards_value
|
||||||
|
_z_yards = z_yards_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://b75i20d4vnkyh
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
class_name CanonicalWowWorldYaw
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable canonical world-facing yaw in radians.
|
||||||
|
## Zero faces increasing WoW X and positive rotation turns toward increasing Y.
|
||||||
|
|
||||||
|
var radians: float:
|
||||||
|
get:
|
||||||
|
return _radians
|
||||||
|
|
||||||
|
var _radians: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a yaw without normalization; [CoordinateMapper] normalizes conversions.
|
||||||
|
func _init(radians_value: float) -> void:
|
||||||
|
_radians = radians_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://ddu5eiobntlrm
|
||||||
@@ -0,0 +1,137 @@
|
|||||||
|
class_name CoordinateMapper
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Pure, stateless conversions between OpenWC coordinate value objects.
|
||||||
|
## This is the only domain contract allowed to change coordinate axes. It does
|
||||||
|
## not depend on Node, Resource, scene state, map assets or renderer globals.
|
||||||
|
|
||||||
|
const ADT_TILES_PER_MAP_AXIS: int = 64
|
||||||
|
const ADT_CHUNKS_PER_TILE_AXIS: int = 16
|
||||||
|
const ADT_TILE_SIZE_YARDS: float = 1600.0 / 3.0
|
||||||
|
const ADT_CHUNK_SIZE_YARDS: float = ADT_TILE_SIZE_YARDS / float(ADT_CHUNKS_PER_TILE_AXIS)
|
||||||
|
const WOW_WORLD_HALF_EXTENT_YARDS: float = ADT_TILE_SIZE_YARDS * float(ADT_TILES_PER_MAP_AXIS) * 0.5
|
||||||
|
|
||||||
|
|
||||||
|
## Converts server/wire X/Y/Z to canonical WoW X/Y/Z without an axis swap.
|
||||||
|
static func server_to_canonical(server_position: ServerWorldPosition) -> CanonicalWowWorldPosition:
|
||||||
|
return CanonicalWowWorldPosition.new(
|
||||||
|
server_position.x_yards,
|
||||||
|
server_position.y_yards,
|
||||||
|
server_position.z_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts canonical WoW X/Y/Z to server/wire X/Y/Z without an axis swap.
|
||||||
|
static func canonical_to_server(canonical_position: CanonicalWowWorldPosition) -> ServerWorldPosition:
|
||||||
|
return ServerWorldPosition.new(
|
||||||
|
canonical_position.x_yards,
|
||||||
|
canonical_position.y_yards,
|
||||||
|
canonical_position.z_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts canonical WoW world axes to the Godot Y-up compatibility basis.
|
||||||
|
static func canonical_to_godot(canonical_position: CanonicalWowWorldPosition) -> GodotWorldPosition:
|
||||||
|
return GodotWorldPosition.new(
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards,
|
||||||
|
canonical_position.z_yards,
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts the Godot Y-up compatibility basis to canonical WoW world axes.
|
||||||
|
static func godot_to_canonical(godot_position: GodotWorldPosition) -> CanonicalWowWorldPosition:
|
||||||
|
return CanonicalWowWorldPosition.new(
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.z_units,
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - godot_position.x_units,
|
||||||
|
godot_position.y_units
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts canonical WoW coordinates to MDDF/MODF X/height/Z file fields.
|
||||||
|
static func canonical_to_adt_placement(canonical_position: CanonicalWowWorldPosition) -> AdtPlacementPosition:
|
||||||
|
return AdtPlacementPosition.new(
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.x_yards,
|
||||||
|
canonical_position.z_yards,
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - canonical_position.y_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts MDDF/MODF X/height/Z file fields to canonical WoW coordinates.
|
||||||
|
static func adt_placement_to_canonical(adt_position: AdtPlacementPosition) -> CanonicalWowWorldPosition:
|
||||||
|
return CanonicalWowWorldPosition.new(
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.x_offset_yards,
|
||||||
|
WOW_WORLD_HALF_EXTENT_YARDS - adt_position.z_offset_yards,
|
||||||
|
adt_position.height_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns unclamped ADT filename indices for a canonical position.
|
||||||
|
## Exact south/east boundaries belong to the next tile by floor semantics.
|
||||||
|
static func canonical_to_adt_tile(canonical_position: CanonicalWowWorldPosition) -> AdtTileCoordinate:
|
||||||
|
return AdtTileCoordinate.new(
|
||||||
|
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.x_yards / ADT_TILE_SIZE_YARDS)),
|
||||||
|
int(floor(float(ADT_TILES_PER_MAP_AXIS) * 0.5 - canonical_position.y_yards / ADT_TILE_SIZE_YARDS))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts a canonical position to east/south offsets inside its ADT tile.
|
||||||
|
static func canonical_to_adt_tile_local(canonical_position: CanonicalWowWorldPosition) -> AdtTileLocalPosition:
|
||||||
|
var tile_coordinate := canonical_to_adt_tile(canonical_position)
|
||||||
|
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
|
||||||
|
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
|
||||||
|
return AdtTileLocalPosition.new(
|
||||||
|
tile_west_edge_y_yards - canonical_position.y_yards,
|
||||||
|
canonical_position.z_yards,
|
||||||
|
tile_north_edge_x_yards - canonical_position.x_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Reconstructs canonical coordinates from an ADT tile and tile-local offsets.
|
||||||
|
static func adt_tile_local_to_canonical(
|
||||||
|
tile_coordinate: AdtTileCoordinate,
|
||||||
|
local_position: AdtTileLocalPosition
|
||||||
|
) -> CanonicalWowWorldPosition:
|
||||||
|
var tile_north_edge_x_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_x)) * ADT_TILE_SIZE_YARDS
|
||||||
|
var tile_west_edge_y_yards := (float(ADT_TILES_PER_MAP_AXIS) * 0.5 - float(tile_coordinate.tile_y)) * ADT_TILE_SIZE_YARDS
|
||||||
|
return CanonicalWowWorldPosition.new(
|
||||||
|
tile_north_edge_x_yards - local_position.south_yards,
|
||||||
|
tile_west_edge_y_yards - local_position.east_yards,
|
||||||
|
local_position.height_yards
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Returns unclamped MCNK indices for a tile-local position.
|
||||||
|
static func adt_tile_local_to_chunk(
|
||||||
|
tile_coordinate: AdtTileCoordinate,
|
||||||
|
local_position: AdtTileLocalPosition
|
||||||
|
) -> AdtChunkCoordinate:
|
||||||
|
return AdtChunkCoordinate.new(
|
||||||
|
tile_coordinate,
|
||||||
|
int(floor(local_position.south_yards / ADT_CHUNK_SIZE_YARDS)),
|
||||||
|
int(floor(local_position.east_yards / ADT_CHUNK_SIZE_YARDS))
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
## Converts server movement yaw to canonical yaw and normalizes it to [-PI, PI).
|
||||||
|
static func server_to_canonical_yaw(server_yaw: ServerWorldYaw) -> CanonicalWowWorldYaw:
|
||||||
|
return CanonicalWowWorldYaw.new(_normalize_yaw(server_yaw.radians))
|
||||||
|
|
||||||
|
|
||||||
|
## Converts canonical yaw to server movement yaw and normalizes it to [-PI, PI).
|
||||||
|
static func canonical_to_server_yaw(canonical_yaw: CanonicalWowWorldYaw) -> ServerWorldYaw:
|
||||||
|
return ServerWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
|
||||||
|
|
||||||
|
|
||||||
|
## Converts canonical world yaw to Godot Y-axis yaw in the mapped world basis.
|
||||||
|
static func canonical_to_godot_yaw(canonical_yaw: CanonicalWowWorldYaw) -> GodotWorldYaw:
|
||||||
|
return GodotWorldYaw.new(_normalize_yaw(canonical_yaw.radians))
|
||||||
|
|
||||||
|
|
||||||
|
## Converts Godot Y-axis world yaw to canonical yaw in the mapped world basis.
|
||||||
|
static func godot_to_canonical_yaw(godot_yaw: GodotWorldYaw) -> CanonicalWowWorldYaw:
|
||||||
|
return CanonicalWowWorldYaw.new(_normalize_yaw(godot_yaw.radians))
|
||||||
|
|
||||||
|
|
||||||
|
static func _normalize_yaw(radians: float) -> float:
|
||||||
|
return fposmod(radians + PI, TAU) - PI
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bg8p8rrkmg84d
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class_name GodotWorldPosition
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable Godot Y-up renderer position.
|
||||||
|
## One Godot unit equals one WoW yard in the compatibility renderer. X grows
|
||||||
|
## east, Y grows up and Z grows south from the north-west map extent.
|
||||||
|
|
||||||
|
var x_units: float:
|
||||||
|
get:
|
||||||
|
return _x_units
|
||||||
|
|
||||||
|
var y_units: float:
|
||||||
|
get:
|
||||||
|
return _y_units
|
||||||
|
|
||||||
|
var z_units: float:
|
||||||
|
get:
|
||||||
|
return _z_units
|
||||||
|
|
||||||
|
var _x_units: float
|
||||||
|
var _y_units: float
|
||||||
|
var _z_units: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a renderer position without exposing it as a cross-layer [Vector3].
|
||||||
|
func _init(x_units_value: float, y_units_value: float, z_units_value: float) -> void:
|
||||||
|
_x_units = x_units_value
|
||||||
|
_y_units = y_units_value
|
||||||
|
_z_units = z_units_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://tp7jo14w25nc
|
||||||
@@ -0,0 +1,17 @@
|
|||||||
|
class_name GodotWorldYaw
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable Godot Y-axis yaw in radians for the canonical world basis.
|
||||||
|
## With Godot forward `-Z`, the numeric angle equals WoW world yaw after the
|
||||||
|
## position basis conversion. Model-local MDDF/MODF corrections are separate.
|
||||||
|
|
||||||
|
var radians: float:
|
||||||
|
get:
|
||||||
|
return _radians
|
||||||
|
|
||||||
|
var _radians: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a Godot world yaw without normalization.
|
||||||
|
func _init(radians_value: float) -> void:
|
||||||
|
_radians = radians_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cuy2skhdv3f6g
|
||||||
@@ -0,0 +1,29 @@
|
|||||||
|
class_name ServerWorldPosition
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable TrinityCore/AzerothCore world position in wire/database X/Y/Z order.
|
||||||
|
## Units are WoW yards. A distinct type prevents server positions from being
|
||||||
|
## passed directly to renderer APIs even though the scalar values are canonical.
|
||||||
|
|
||||||
|
var x_yards: float:
|
||||||
|
get:
|
||||||
|
return _x_yards
|
||||||
|
|
||||||
|
var y_yards: float:
|
||||||
|
get:
|
||||||
|
return _y_yards
|
||||||
|
|
||||||
|
var z_yards: float:
|
||||||
|
get:
|
||||||
|
return _z_yards
|
||||||
|
|
||||||
|
var _x_yards: float
|
||||||
|
var _y_yards: float
|
||||||
|
var _z_yards: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a server position without changing precision or validating map bounds.
|
||||||
|
func _init(x_yards_value: float, y_yards_value: float, z_yards_value: float) -> void:
|
||||||
|
_x_yards = x_yards_value
|
||||||
|
_y_yards = y_yards_value
|
||||||
|
_z_yards = z_yards_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dilhr7ym3xr43
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
class_name ServerWorldYaw
|
||||||
|
extends RefCounted
|
||||||
|
|
||||||
|
## Immutable TrinityCore/AzerothCore movement orientation in radians.
|
||||||
|
## It uses the WoW client convention: zero faces +X and positive turns toward +Y.
|
||||||
|
|
||||||
|
var radians: float:
|
||||||
|
get:
|
||||||
|
return _radians
|
||||||
|
|
||||||
|
var _radians: float
|
||||||
|
|
||||||
|
|
||||||
|
## Creates a server yaw without normalization.
|
||||||
|
func _init(radians_value: float) -> void:
|
||||||
|
_radians = radians_value
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://dr8k82t2qgewd
|
||||||
@@ -374,6 +374,39 @@ func _exit_tree() -> void:
|
|||||||
_shutting_down = true
|
_shutting_down = true
|
||||||
_wait_for_tile_tasks()
|
_wait_for_tile_tasks()
|
||||||
_clear_streamed_world()
|
_clear_streamed_world()
|
||||||
|
_release_runtime_caches_for_shutdown()
|
||||||
|
|
||||||
|
|
||||||
|
## Frees detached prototype nodes and releases render resources owned by this
|
||||||
|
## loader after all asynchronous work has completed. Map refreshes retain these
|
||||||
|
## caches; only scene shutdown destroys them.
|
||||||
|
func _release_runtime_caches_for_shutdown() -> void:
|
||||||
|
_free_detached_node_cache(_m2_scene_cache)
|
||||||
|
_free_detached_node_cache(_m2_animated_scene_cache)
|
||||||
|
_free_detached_node_cache(_wmo_prototype_cache)
|
||||||
|
_m2_mesh_cache.clear()
|
||||||
|
_m2_static_animation_cache.clear()
|
||||||
|
_m2_missing_cache.clear()
|
||||||
|
_wmo_render_cache.clear()
|
||||||
|
_wmo_scene_resource_cache.clear()
|
||||||
|
_wmo_render_missing_cache.clear()
|
||||||
|
_wmo_scene_cache_missing.clear()
|
||||||
|
_wmo_missing_cache.clear()
|
||||||
|
_shared_tex_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
|
func _free_detached_node_cache(node_cache: Dictionary) -> void:
|
||||||
|
for cached_value in node_cache.values():
|
||||||
|
if not (cached_value is Node):
|
||||||
|
continue
|
||||||
|
var cached_node := cached_value as Node
|
||||||
|
if not is_instance_valid(cached_node):
|
||||||
|
continue
|
||||||
|
if cached_node.is_inside_tree():
|
||||||
|
cached_node.queue_free()
|
||||||
|
else:
|
||||||
|
cached_node.free()
|
||||||
|
node_cache.clear()
|
||||||
|
|
||||||
|
|
||||||
func _apply_quality_preset() -> void:
|
func _apply_quality_preset() -> void:
|
||||||
@@ -2781,15 +2814,27 @@ func _clear_streamed_world() -> void:
|
|||||||
_free_render_instance(chunk_rid)
|
_free_render_instance(chunk_rid)
|
||||||
var tile_root: Node = state.get("root", null)
|
var tile_root: Node = state.get("root", null)
|
||||||
if tile_root:
|
if tile_root:
|
||||||
tile_root.queue_free()
|
_queue_free_streamed_node(tile_root)
|
||||||
|
|
||||||
_tile_states.clear()
|
_tile_states.clear()
|
||||||
_clear_tile_mesh_cache()
|
_clear_tile_mesh_cache()
|
||||||
_clear_quality_terrain_mesh_cache()
|
_clear_quality_terrain_mesh_cache()
|
||||||
_clear_wmo_registry()
|
_clear_wmo_registry()
|
||||||
_ensure_world_wmo_root()
|
if not _shutting_down:
|
||||||
if enable_wmo:
|
_ensure_world_wmo_root()
|
||||||
_place_global_wmo()
|
if enable_wmo:
|
||||||
|
_place_global_wmo()
|
||||||
|
|
||||||
|
|
||||||
|
## During world shutdown, parented nodes are recursively owned by the world
|
||||||
|
## root. Queueing them separately from `_exit_tree()` can detach them from that
|
||||||
|
## deletion and leave empty Node objects alive at engine teardown.
|
||||||
|
func _queue_free_streamed_node(node: Node) -> void:
|
||||||
|
if node == null or not is_instance_valid(node):
|
||||||
|
return
|
||||||
|
if _shutting_down and node.is_inside_tree():
|
||||||
|
return
|
||||||
|
node.queue_free()
|
||||||
|
|
||||||
|
|
||||||
func _reset_terrain_root_children() -> void:
|
func _reset_terrain_root_children() -> void:
|
||||||
@@ -3889,10 +3934,10 @@ func _clear_wmo_registry() -> void:
|
|||||||
var entry: Dictionary = entry_variant
|
var entry: Dictionary = entry_variant
|
||||||
var node: Node = entry.get("node", null)
|
var node: Node = entry.get("node", null)
|
||||||
if node and is_instance_valid(node):
|
if node and is_instance_valid(node):
|
||||||
node.queue_free()
|
_queue_free_streamed_node(node)
|
||||||
_wmo_registry.clear()
|
_wmo_registry.clear()
|
||||||
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
if _world_wmo_root and is_instance_valid(_world_wmo_root):
|
||||||
_world_wmo_root.queue_free()
|
_queue_free_streamed_node(_world_wmo_root)
|
||||||
_world_wmo_root = null
|
_world_wmo_root = null
|
||||||
|
|
||||||
|
|
||||||
@@ -4458,7 +4503,7 @@ func _cancel_m2_build_job(key: String) -> void:
|
|||||||
var job: Dictionary = _m2_build_jobs[key]
|
var job: Dictionary = _m2_build_jobs[key]
|
||||||
var root: Node = job.get("root", null)
|
var root: Node = job.get("root", null)
|
||||||
if root != null and is_instance_valid(root):
|
if root != null and is_instance_valid(root):
|
||||||
root.queue_free()
|
_queue_free_streamed_node(root)
|
||||||
_m2_build_jobs.erase(key)
|
_m2_build_jobs.erase(key)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ extends SceneTree
|
|||||||
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
|
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
|
||||||
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
|
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
|
||||||
## --reference <directory> --candidate <directory> [--output <report.json>]
|
## --reference <directory> --candidate <directory> [--output <report.json>]
|
||||||
|
## [--region x,y,width,height]
|
||||||
## [--pixel-threshold 0.04] [--mean-threshold 0.015] [--changed-ratio-threshold 0.01]
|
## [--pixel-threshold 0.04] [--mean-threshold 0.015] [--changed-ratio-threshold 0.01]
|
||||||
## Use --self-test for a synthetic regression check that writes only under user://.
|
## Use --self-test for a synthetic regression check that writes only under user://.
|
||||||
|
|
||||||
@@ -56,13 +57,18 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
|||||||
parsed.self_test = true
|
parsed.self_test = true
|
||||||
index += 1
|
index += 1
|
||||||
continue
|
continue
|
||||||
if argument not in ["--reference", "--candidate", "--output", "--only", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
|
if argument not in ["--reference", "--candidate", "--output", "--only", "--region", "--pixel-threshold", "--mean-threshold", "--changed-ratio-threshold"]:
|
||||||
return {"error": "unknown argument: %s" % argument}
|
return {"error": "unknown argument: %s" % argument}
|
||||||
if index + 1 >= raw_arguments.size():
|
if index + 1 >= raw_arguments.size():
|
||||||
return {"error": "missing value for %s" % argument}
|
return {"error": "missing value for %s" % argument}
|
||||||
var key := argument.trim_prefix("--").replace("-", "_")
|
var key := argument.trim_prefix("--").replace("-", "_")
|
||||||
var value := raw_arguments[index + 1]
|
var value := raw_arguments[index + 1]
|
||||||
if key.ends_with("threshold"):
|
if key == "region":
|
||||||
|
var region_parse_result := _parse_comparison_region(value)
|
||||||
|
if region_parse_result.has("error"):
|
||||||
|
return region_parse_result
|
||||||
|
parsed[key] = region_parse_result.region
|
||||||
|
elif key.ends_with("threshold"):
|
||||||
if not value.is_valid_float():
|
if not value.is_valid_float():
|
||||||
return {"error": "%s requires a number" % argument}
|
return {"error": "%s requires a number" % argument}
|
||||||
parsed[key] = float(value)
|
parsed[key] = float(value)
|
||||||
@@ -72,6 +78,24 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
|
|||||||
return parsed
|
return parsed
|
||||||
|
|
||||||
|
|
||||||
|
func _parse_comparison_region(value: String) -> Dictionary:
|
||||||
|
var components := value.split(",", false)
|
||||||
|
if components.size() != 4:
|
||||||
|
return {"error": "--region requires x,y,width,height"}
|
||||||
|
for component in components:
|
||||||
|
if not component.strip_edges().is_valid_int():
|
||||||
|
return {"error": "--region components must be integers"}
|
||||||
|
var region := Rect2i(
|
||||||
|
int(components[0]),
|
||||||
|
int(components[1]),
|
||||||
|
int(components[2]),
|
||||||
|
int(components[3])
|
||||||
|
)
|
||||||
|
if region.position.x < 0 or region.position.y < 0 or region.size.x <= 0 or region.size.y <= 0:
|
||||||
|
return {"error": "--region requires non-negative x/y and positive width/height"}
|
||||||
|
return {"region": region}
|
||||||
|
|
||||||
|
|
||||||
func _compare_directories(reference_directory: String, candidate_directory: String, options: Dictionary) -> Dictionary:
|
func _compare_directories(reference_directory: String, candidate_directory: String, options: Dictionary) -> Dictionary:
|
||||||
reference_directory = ProjectSettings.globalize_path(reference_directory)
|
reference_directory = ProjectSettings.globalize_path(reference_directory)
|
||||||
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
candidate_directory = ProjectSettings.globalize_path(candidate_directory)
|
||||||
@@ -108,6 +132,7 @@ func _compare_directories(reference_directory: String, candidate_directory: Stri
|
|||||||
"pixel_threshold": options.pixel_threshold,
|
"pixel_threshold": options.pixel_threshold,
|
||||||
"mean_threshold": options.mean_threshold,
|
"mean_threshold": options.mean_threshold,
|
||||||
"changed_ratio_threshold": options.changed_ratio_threshold,
|
"changed_ratio_threshold": options.changed_ratio_threshold,
|
||||||
|
"comparison_region": _region_array(options.get("region", Rect2i())),
|
||||||
"compared_count": results.size() - missing_count,
|
"compared_count": results.size() - missing_count,
|
||||||
"failed_count": failed_count,
|
"failed_count": failed_count,
|
||||||
"missing_count": missing_count,
|
"missing_count": missing_count,
|
||||||
@@ -127,12 +152,22 @@ func _compare_images(reference_path: String, candidate_path: String, options: Di
|
|||||||
"reference_size": [reference_image.get_width(), reference_image.get_height()],
|
"reference_size": [reference_image.get_width(), reference_image.get_height()],
|
||||||
"candidate_size": [candidate_image.get_width(), candidate_image.get_height()],
|
"candidate_size": [candidate_image.get_width(), candidate_image.get_height()],
|
||||||
}
|
}
|
||||||
|
var comparison_region: Rect2i = options.get("region", Rect2i(Vector2i.ZERO, reference_image.get_size()))
|
||||||
|
if comparison_region == Rect2i():
|
||||||
|
comparison_region = Rect2i(Vector2i.ZERO, reference_image.get_size())
|
||||||
|
var image_region := Rect2i(Vector2i.ZERO, reference_image.get_size())
|
||||||
|
if not image_region.encloses(comparison_region):
|
||||||
|
return {
|
||||||
|
"status": "region_out_of_bounds",
|
||||||
|
"region": _region_array(comparison_region),
|
||||||
|
"image_size": [reference_image.get_width(), reference_image.get_height()],
|
||||||
|
}
|
||||||
|
|
||||||
var error_sum := 0.0
|
var error_sum := 0.0
|
||||||
var changed_pixel_count := 0
|
var changed_pixel_count := 0
|
||||||
var pixel_count := reference_image.get_width() * reference_image.get_height()
|
var pixel_count := comparison_region.size.x * comparison_region.size.y
|
||||||
for y in reference_image.get_height():
|
for y in range(comparison_region.position.y, comparison_region.end.y):
|
||||||
for x in reference_image.get_width():
|
for x in range(comparison_region.position.x, comparison_region.end.x):
|
||||||
var perceptual_error := _perceptual_color_error(reference_image.get_pixel(x, y), candidate_image.get_pixel(x, y))
|
var perceptual_error := _perceptual_color_error(reference_image.get_pixel(x, y), candidate_image.get_pixel(x, y))
|
||||||
error_sum += perceptual_error
|
error_sum += perceptual_error
|
||||||
if perceptual_error > float(options.pixel_threshold):
|
if perceptual_error > float(options.pixel_threshold):
|
||||||
@@ -144,11 +179,18 @@ func _compare_images(reference_path: String, candidate_path: String, options: Di
|
|||||||
"status": "passed" if passed else "threshold_exceeded",
|
"status": "passed" if passed else "threshold_exceeded",
|
||||||
"width": reference_image.get_width(),
|
"width": reference_image.get_width(),
|
||||||
"height": reference_image.get_height(),
|
"height": reference_image.get_height(),
|
||||||
|
"region": _region_array(comparison_region),
|
||||||
"mean_perceptual_error": mean_perceptual_error,
|
"mean_perceptual_error": mean_perceptual_error,
|
||||||
"changed_pixel_ratio": changed_pixel_ratio,
|
"changed_pixel_ratio": changed_pixel_ratio,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
func _region_array(region: Rect2i) -> Array[int]:
|
||||||
|
if region == Rect2i():
|
||||||
|
return []
|
||||||
|
return [region.position.x, region.position.y, region.size.x, region.size.y]
|
||||||
|
|
||||||
|
|
||||||
func _perceptual_color_error(reference_color: Color, candidate_color: Color) -> float:
|
func _perceptual_color_error(reference_color: Color, candidate_color: Color) -> float:
|
||||||
var reference_linear := reference_color.srgb_to_linear()
|
var reference_linear := reference_color.srgb_to_linear()
|
||||||
var candidate_linear := candidate_color.srgb_to_linear()
|
var candidate_linear := candidate_color.srgb_to_linear()
|
||||||
@@ -229,6 +271,22 @@ func _run_self_test() -> int:
|
|||||||
if not identical_report.passed or changed_report.passed:
|
if not identical_report.passed or changed_report.passed:
|
||||||
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
|
||||||
return 1
|
return 1
|
||||||
|
var region_options := options.duplicate()
|
||||||
|
region_options["region"] = Rect2i(1, 1, 1, 1)
|
||||||
|
var excluded_change_report := _compare_directories(reference_directory, changed_directory, region_options)
|
||||||
|
if not excluded_change_report.passed or excluded_change_report.comparison_region != [1, 1, 1, 1]:
|
||||||
|
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: region must exclude pixels outside the rectangle")
|
||||||
|
return 1
|
||||||
|
var out_of_bounds_options := options.duplicate()
|
||||||
|
out_of_bounds_options["region"] = Rect2i(1, 1, 2, 2)
|
||||||
|
var out_of_bounds_result := _compare_images(
|
||||||
|
reference_directory.path_join("synthetic.jpg"),
|
||||||
|
changed_directory.path_join("synthetic__cold_process.png"),
|
||||||
|
out_of_bounds_options
|
||||||
|
)
|
||||||
|
if out_of_bounds_result.status != "region_out_of_bounds":
|
||||||
|
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: out-of-bounds region must fail")
|
||||||
|
return 1
|
||||||
if reference_image.save_jpg(reference_directory.path_join("ignored.jpg"), 1.0) != OK:
|
if reference_image.save_jpg(reference_directory.path_join("ignored.jpg"), 1.0) != OK:
|
||||||
return 1
|
return 1
|
||||||
var filtered_options := options.duplicate()
|
var filtered_options := options.duplicate()
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
## Headless contract verification for M01 canonical coordinate value objects and
|
||||||
|
## [CoordinateMapper]. Uses synthetic boundaries plus five positions observed in
|
||||||
|
## the original WoW 3.3.5a build 12340 during the M00 paired renderer session.
|
||||||
|
|
||||||
|
const CoordinateMapperScript = preload("res://src/domain/coordinates/coordinate_mapper.gd")
|
||||||
|
const CanonicalWowWorldPositionScript = preload("res://src/domain/coordinates/canonical_wow_world_position.gd")
|
||||||
|
const ServerWorldPositionScript = preload("res://src/domain/coordinates/server_world_position.gd")
|
||||||
|
const AdtPlacementPositionScript = preload("res://src/domain/coordinates/adt_placement_position.gd")
|
||||||
|
const AdtTileCoordinateScript = preload("res://src/domain/coordinates/adt_tile_coordinate.gd")
|
||||||
|
const AdtTileLocalPositionScript = preload("res://src/domain/coordinates/adt_tile_local_position.gd")
|
||||||
|
const CanonicalWowWorldYawScript = preload("res://src/domain/coordinates/canonical_wow_world_yaw.gd")
|
||||||
|
const ServerWorldYawScript = preload("res://src/domain/coordinates/server_world_yaw.gd")
|
||||||
|
|
||||||
|
const RENDER_BASELINE_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
||||||
|
const GOLDEN_POSITION_TOLERANCE_YARDS := 0.002
|
||||||
|
const ROUND_TRIP_TOLERANCE_YARDS := 0.000001
|
||||||
|
const BOUNDARY_EPSILON_YARDS := 0.001
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
var failures: Array[String] = []
|
||||||
|
_verify_build_12340_golden_positions(failures)
|
||||||
|
_verify_server_and_adt_round_trips(failures)
|
||||||
|
_verify_tile_and_chunk_boundaries(failures)
|
||||||
|
_verify_world_yaw_conversions(failures)
|
||||||
|
|
||||||
|
if not failures.is_empty():
|
||||||
|
for failure in failures:
|
||||||
|
push_error("COORDINATE_MAPPER: %s" % failure)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
|
||||||
|
print("COORDINATE_MAPPER PASS golden_points=5 tile_boundaries=8 yaw_cases=5")
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_build_12340_golden_positions(failures: Array[String]) -> void:
|
||||||
|
var manifest := _load_json_object(RENDER_BASELINE_MANIFEST_PATH, failures)
|
||||||
|
if manifest.is_empty():
|
||||||
|
return
|
||||||
|
|
||||||
|
var golden_point_count := 0
|
||||||
|
for checkpoint_variant in manifest.get("checkpoints", []):
|
||||||
|
if not (checkpoint_variant is Dictionary):
|
||||||
|
continue
|
||||||
|
var checkpoint: Dictionary = checkpoint_variant
|
||||||
|
var reference_wow_camera_variant = checkpoint.get("reference_wow_camera", null)
|
||||||
|
if not (reference_wow_camera_variant is Array) or reference_wow_camera_variant.size() != 3:
|
||||||
|
continue
|
||||||
|
|
||||||
|
var expected_godot_camera_variant = checkpoint.get("camera", null)
|
||||||
|
if not (expected_godot_camera_variant is Array) or expected_godot_camera_variant.size() != 3:
|
||||||
|
failures.append("%s has no three-component renderer camera" % checkpoint.get("name", "checkpoint"))
|
||||||
|
continue
|
||||||
|
|
||||||
|
var canonical_position = CanonicalWowWorldPositionScript.new(
|
||||||
|
float(reference_wow_camera_variant[0]),
|
||||||
|
float(reference_wow_camera_variant[1]),
|
||||||
|
float(reference_wow_camera_variant[2])
|
||||||
|
)
|
||||||
|
var godot_position = CoordinateMapperScript.canonical_to_godot(canonical_position)
|
||||||
|
var checkpoint_name := String(checkpoint.get("name", "checkpoint"))
|
||||||
|
_expect_near(godot_position.x_units, float(expected_godot_camera_variant[0]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot X" % checkpoint_name, failures)
|
||||||
|
_expect_near(godot_position.y_units, float(expected_godot_camera_variant[1]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Y" % checkpoint_name, failures)
|
||||||
|
_expect_near(godot_position.z_units, float(expected_godot_camera_variant[2]), GOLDEN_POSITION_TOLERANCE_YARDS, "%s Godot Z" % checkpoint_name, failures)
|
||||||
|
|
||||||
|
var round_trip_position = CoordinateMapperScript.godot_to_canonical(godot_position)
|
||||||
|
_expect_canonical_position(round_trip_position, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "%s Godot round trip" % checkpoint_name, failures)
|
||||||
|
golden_point_count += 1
|
||||||
|
|
||||||
|
_expect_equal(golden_point_count, 5, "build 12340 golden point count", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_server_and_adt_round_trips(failures: Array[String]) -> void:
|
||||||
|
var server_position = ServerWorldPositionScript.new(-9153.334, 386.666, 180.0)
|
||||||
|
var canonical_position = CoordinateMapperScript.server_to_canonical(server_position)
|
||||||
|
_expect_near(canonical_position.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X remains canonical X", failures)
|
||||||
|
_expect_near(canonical_position.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y remains canonical Y", failures)
|
||||||
|
_expect_near(canonical_position.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z remains canonical Z", failures)
|
||||||
|
|
||||||
|
var round_trip_server = CoordinateMapperScript.canonical_to_server(canonical_position)
|
||||||
|
_expect_near(round_trip_server.x_yards, server_position.x_yards, ROUND_TRIP_TOLERANCE_YARDS, "server X round trip", failures)
|
||||||
|
_expect_near(round_trip_server.y_yards, server_position.y_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Y round trip", failures)
|
||||||
|
_expect_near(round_trip_server.z_yards, server_position.z_yards, ROUND_TRIP_TOLERANCE_YARDS, "server Z round trip", failures)
|
||||||
|
|
||||||
|
var adt_position = CoordinateMapperScript.canonical_to_adt_placement(canonical_position)
|
||||||
|
_expect_near(adt_position.x_offset_yards, 26220.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement X offset", failures)
|
||||||
|
_expect_near(adt_position.height_yards, 180.0, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement height", failures)
|
||||||
|
_expect_near(adt_position.z_offset_yards, 16680.000666666667, GOLDEN_POSITION_TOLERANCE_YARDS, "ADT placement Z offset", failures)
|
||||||
|
var adt_round_trip = CoordinateMapperScript.adt_placement_to_canonical(adt_position)
|
||||||
|
_expect_canonical_position(adt_round_trip, canonical_position, ROUND_TRIP_TOLERANCE_YARDS, "ADT placement round trip", failures)
|
||||||
|
|
||||||
|
var direct_adt_position = AdtPlacementPositionScript.new(26220.000666666667, 180.0, 16680.000666666667)
|
||||||
|
var direct_canonical = CoordinateMapperScript.adt_placement_to_canonical(direct_adt_position)
|
||||||
|
_expect_canonical_position(direct_canonical, canonical_position, GOLDEN_POSITION_TOLERANCE_YARDS, "direct ADT placement conversion", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_tile_and_chunk_boundaries(failures: Array[String]) -> void:
|
||||||
|
var half_extent := CoordinateMapperScript.WOW_WORLD_HALF_EXTENT_YARDS
|
||||||
|
var tile_size := CoordinateMapperScript.ADT_TILE_SIZE_YARDS
|
||||||
|
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(half_extent, half_extent, 0.0)), 0, 0, true, "north-west extent", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(0.0, 0.0, 0.0)), 32, 32, true, "map center", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(BOUNDARY_EPSILON_YARDS, BOUNDARY_EPSILON_YARDS, 0.0)), 31, 31, true, "north-west of center boundary", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-BOUNDARY_EPSILON_YARDS, -BOUNDARY_EPSILON_YARDS, 0.0)), 32, 32, true, "south-east of center boundary", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent + BOUNDARY_EPSILON_YARDS, -half_extent + BOUNDARY_EPSILON_YARDS, 0.0)), 63, 63, true, "inside south-east extent", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-half_extent, -half_extent, 0.0)), 64, 64, false, "exclusive south-east extent", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(tile_size, -tile_size, 0.0)), 31, 33, true, "whole-tile offsets", failures)
|
||||||
|
_expect_tile(CoordinateMapperScript.canonical_to_adt_tile(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0)), 49, 31, true, "Elwynn golden tile", failures)
|
||||||
|
|
||||||
|
var tile_coordinate = AdtTileCoordinateScript.new(49, 31)
|
||||||
|
var local_position = CoordinateMapperScript.canonical_to_adt_tile_local(CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0))
|
||||||
|
var local_round_trip = CoordinateMapperScript.adt_tile_local_to_canonical(tile_coordinate, local_position)
|
||||||
|
_expect_canonical_position(local_round_trip, CanonicalWowWorldPositionScript.new(-9153.334, 386.666, 180.0), ROUND_TRIP_TOLERANCE_YARDS, "ADT tile-local round trip", failures)
|
||||||
|
_expect_true(local_position.east_yards >= 0.0 and local_position.east_yards < tile_size, "tile-local east offset is half-open", failures)
|
||||||
|
_expect_true(local_position.south_yards >= 0.0 and local_position.south_yards < tile_size, "tile-local south offset is half-open", failures)
|
||||||
|
|
||||||
|
var chunk_local_position = AdtTileLocalPositionScript.new(
|
||||||
|
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS * 2.0,
|
||||||
|
180.0,
|
||||||
|
CoordinateMapperScript.ADT_CHUNK_SIZE_YARDS
|
||||||
|
)
|
||||||
|
var chunk_coordinate = CoordinateMapperScript.adt_tile_local_to_chunk(tile_coordinate, chunk_local_position)
|
||||||
|
_expect_equal(chunk_coordinate.chunk_x, 1, "exact south chunk boundary", failures)
|
||||||
|
_expect_equal(chunk_coordinate.chunk_y, 2, "exact east chunk boundary", failures)
|
||||||
|
_expect_true(chunk_coordinate.is_within_map_grid(), "chunk coordinate is inside map grid", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_world_yaw_conversions(failures: Array[String]) -> void:
|
||||||
|
var yaw_cases := [0.0, PI * 0.5, -PI * 0.5, PI, TAU + 0.25]
|
||||||
|
for yaw_radians in yaw_cases:
|
||||||
|
var server_yaw = ServerWorldYawScript.new(float(yaw_radians))
|
||||||
|
var canonical_yaw = CoordinateMapperScript.server_to_canonical_yaw(server_yaw)
|
||||||
|
var godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_yaw)
|
||||||
|
var canonical_round_trip = CoordinateMapperScript.godot_to_canonical_yaw(godot_yaw)
|
||||||
|
var server_round_trip = CoordinateMapperScript.canonical_to_server_yaw(canonical_round_trip)
|
||||||
|
var expected_normalized_yaw := fposmod(float(yaw_radians) + PI, TAU) - PI
|
||||||
|
_expect_near(canonical_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "canonical yaw normalization", failures)
|
||||||
|
_expect_near(godot_yaw.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "Godot yaw basis", failures)
|
||||||
|
_expect_near(server_round_trip.radians, expected_normalized_yaw, ROUND_TRIP_TOLERANCE_YARDS, "server yaw round trip", failures)
|
||||||
|
|
||||||
|
var canonical_pi_yaw = CanonicalWowWorldYawScript.new(PI)
|
||||||
|
var normalized_godot_yaw = CoordinateMapperScript.canonical_to_godot_yaw(canonical_pi_yaw)
|
||||||
|
_expect_near(normalized_godot_yaw.radians, -PI, ROUND_TRIP_TOLERANCE_YARDS, "positive PI normalizes to negative PI", failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_tile(tile_coordinate, expected_x: int, expected_y: int, expected_valid: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_equal(tile_coordinate.tile_x, expected_x, "%s tile X" % label, failures)
|
||||||
|
_expect_equal(tile_coordinate.tile_y, expected_y, "%s tile Y" % label, failures)
|
||||||
|
_expect_equal(tile_coordinate.is_within_map_grid(), expected_valid, "%s validity" % label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_canonical_position(actual_position, expected_position, tolerance: float, label: String, failures: Array[String]) -> void:
|
||||||
|
_expect_near(actual_position.x_yards, expected_position.x_yards, tolerance, "%s X" % label, failures)
|
||||||
|
_expect_near(actual_position.y_yards, expected_position.y_yards, tolerance, "%s Y" % label, failures)
|
||||||
|
_expect_near(actual_position.z_yards, expected_position.z_yards, tolerance, "%s Z" % label, failures)
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_near(actual_value: float, expected_value: float, tolerance: float, label: String, failures: Array[String]) -> void:
|
||||||
|
if absf(actual_value - expected_value) > tolerance:
|
||||||
|
failures.append("%s expected %.9f, got %.9f" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_equal(actual_value, expected_value, label: String, failures: Array[String]) -> void:
|
||||||
|
if actual_value != expected_value:
|
||||||
|
failures.append("%s expected %s, got %s" % [label, expected_value, actual_value])
|
||||||
|
|
||||||
|
|
||||||
|
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||||
|
if not actual_value:
|
||||||
|
failures.append("%s expected true" % label)
|
||||||
|
|
||||||
|
|
||||||
|
func _load_json_object(path: String, failures: Array[String]) -> Dictionary:
|
||||||
|
var file := FileAccess.open(path, FileAccess.READ)
|
||||||
|
if file == null:
|
||||||
|
failures.append("cannot open JSON fixture: %s" % path)
|
||||||
|
return {}
|
||||||
|
var parsed = JSON.parse_string(file.get_as_text())
|
||||||
|
if not (parsed is Dictionary):
|
||||||
|
failures.append("JSON fixture is not an object: %s" % path)
|
||||||
|
return {}
|
||||||
|
return parsed
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://bmxjtf1fsrl2x
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
extends SceneTree
|
||||||
|
|
||||||
|
const STREAMING_WORLD_LOADER := preload("res://src/scenes/streaming/streaming_world_loader.gd")
|
||||||
|
const ADT_BUILDER := preload("res://addons/mpq_extractor/loaders/adt_builder.gd")
|
||||||
|
|
||||||
|
var _failures := 0
|
||||||
|
|
||||||
|
|
||||||
|
func _initialize() -> void:
|
||||||
|
_verify_empty_water_root_ownership()
|
||||||
|
_verify_runtime_cache_ownership()
|
||||||
|
|
||||||
|
if _failures > 0:
|
||||||
|
push_error("Render runtime cache shutdown verification failed: %d issue(s)" % _failures)
|
||||||
|
quit(1)
|
||||||
|
return
|
||||||
|
print("Render runtime cache shutdown verification passed.")
|
||||||
|
quit(0)
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_empty_water_root_ownership() -> void:
|
||||||
|
var node_count_before := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||||
|
var empty_water_root: Node3D = ADT_BUILDER.build_tile_water_scene({"chunks": []}, Vector3.ZERO)
|
||||||
|
var node_count_after := int(Performance.get_monitor(Performance.OBJECT_NODE_COUNT))
|
||||||
|
_expect(empty_water_root == null, "empty ADT liquid payload should not produce a water root")
|
||||||
|
_expect(node_count_after == node_count_before, "empty ADT liquid payload should release its temporary water root")
|
||||||
|
|
||||||
|
|
||||||
|
func _verify_runtime_cache_ownership() -> void:
|
||||||
|
var loader: Node = STREAMING_WORLD_LOADER.new()
|
||||||
|
var static_m2_prototype := Node3D.new()
|
||||||
|
var animated_m2_prototype := Node3D.new()
|
||||||
|
var wmo_prototype := Node3D.new()
|
||||||
|
|
||||||
|
loader.set("_m2_scene_cache", {"static": static_m2_prototype})
|
||||||
|
loader.set("_m2_animated_scene_cache", {"animated": animated_m2_prototype})
|
||||||
|
loader.set("_wmo_prototype_cache", {"wmo": wmo_prototype})
|
||||||
|
loader.set("_m2_mesh_cache", {"mesh": ArrayMesh.new()})
|
||||||
|
loader.set("_m2_static_animation_cache", {"static": true})
|
||||||
|
loader.set("_m2_missing_cache", {"missing": true})
|
||||||
|
loader.set("_wmo_render_cache", {"render": Resource.new()})
|
||||||
|
loader.set("_wmo_scene_resource_cache", {"scene": PackedScene.new()})
|
||||||
|
loader.set("_wmo_render_missing_cache", {"missing": true})
|
||||||
|
loader.set("_wmo_scene_cache_missing", {"missing": true})
|
||||||
|
loader.set("_wmo_missing_cache", {"missing": true})
|
||||||
|
loader.set("_shared_tex_cache", {"texture": ImageTexture.new()})
|
||||||
|
|
||||||
|
loader.call("_release_runtime_caches_for_shutdown")
|
||||||
|
|
||||||
|
_expect(not is_instance_valid(static_m2_prototype), "static M2 prototype should be freed")
|
||||||
|
_expect(not is_instance_valid(animated_m2_prototype), "animated M2 prototype should be freed")
|
||||||
|
_expect(not is_instance_valid(wmo_prototype), "WMO prototype should be freed")
|
||||||
|
for cache_name in [
|
||||||
|
"_m2_scene_cache",
|
||||||
|
"_m2_animated_scene_cache",
|
||||||
|
"_wmo_prototype_cache",
|
||||||
|
"_m2_mesh_cache",
|
||||||
|
"_m2_static_animation_cache",
|
||||||
|
"_m2_missing_cache",
|
||||||
|
"_wmo_render_cache",
|
||||||
|
"_wmo_scene_resource_cache",
|
||||||
|
"_wmo_render_missing_cache",
|
||||||
|
"_wmo_scene_cache_missing",
|
||||||
|
"_wmo_missing_cache",
|
||||||
|
"_shared_tex_cache",
|
||||||
|
]:
|
||||||
|
var cache: Dictionary = loader.get(cache_name)
|
||||||
|
_expect(cache.is_empty(), "%s should be empty after shutdown" % cache_name)
|
||||||
|
|
||||||
|
loader.free()
|
||||||
|
|
||||||
|
|
||||||
|
func _expect(condition: bool, message: String) -> void:
|
||||||
|
if condition:
|
||||||
|
return
|
||||||
|
_failures += 1
|
||||||
|
push_error(message)
|
||||||
@@ -1,6 +1,7 @@
|
|||||||
# M00 — Renderer Baseline
|
# M00 — Renderer Baseline
|
||||||
|
|
||||||
<!-- OPENWC_TARGET:ACTIVE -->
|
<!-- OPENWC_TARGET:DONE -->
|
||||||
|
<!-- OPENWC_TARGET_DONE:M00:work/sindo-main-codex/m00-integrator-closeout:2026-07-13 -->
|
||||||
|
|
||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
@@ -38,10 +39,10 @@ godot --headless --path . --script res://src/tools/capture_render_checkpoints.gd
|
|||||||
|
|
||||||
## Evidence
|
## Evidence
|
||||||
|
|
||||||
- Date: 2026-07-10
|
- Date: 2026-07-13
|
||||||
- Revision/worktree: `93bfe11` + worktree baseline changes.
|
- Revision/worktree: merged M00 implementation through `f19c0c4`; closeout worktree `work/sindo-main-codex/m00-integrator-closeout`.
|
||||||
- Commands: `.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1 -Output user://render_baseline_runner_test`; `.\tools\run_render_baseline.ps1 -Output user://render_baseline_m00_2026-07-10 -CacheState existing`.
|
- Commands: `.\tools\run_render_baseline.ps1 -Output user://render_baseline_m00_complete_2026-07-13 -CacheState existing -WaitSeconds 8 -MeasureSeconds 0.5 -ReferenceCheckpointDirectory .\sources\OpenWCReferenceCheckpoints -VisualComparisonReport user://render_baseline_m00_complete_2026-07-13/visual_comparison.json -VisualComparisonWidth 2560 -VisualComparisonHeight 1440`; `.\tools\run_render_baseline.ps1 -DryRun -WaitSeconds 0.1 -MeasureSeconds 0.1 -Output user://render_baseline_m00_post_merge`; `.\tools\verify_coordination.ps1`; `.\tools\verify_documentation.ps1`.
|
||||||
- Results: headless load, renderer material smoke, M2 unique-id dedupe and manifest contract passed. Full run produced 14 PNGs and 14 result records for 7 cold/warm checkpoints. Actual renderer was Godot 4.6.1, Vulkan Forward+, NVIDIA RTX 5070, 1280x900, High profile, with all seven cache families present. Cold p95 range was 31.585..86.537 ms with max hitch 258.456 ms; warm p95 range was 18.824..38.085 ms with max hitch 163.363 ms. The final streaming snapshots had an empty tile queue. Report: `user://render_baseline_m00_2026-07-10/report.json`.
|
- Results: the final GUI run produced fourteen cold/warm captures for seven checkpoints and all seven required coverage categories, with `compared=14` and `missing=0`. The post-merge dry-run passed project load, material, M2 unique-id dedupe, runtime-cache shutdown, manifest and five-point coordinate calibration checks. Coordination and documentation gates passed. Renderer shutdown follow-up removed retained Node/resource/RID diagnostics in focused and seven-checkpoint GUI runs.
|
||||||
- Fidelity comparison: fixed positions/time/profile and current visual gaps are recorded, but no approved paired screenshots from the original WoW 3.3.5a build 12340 were available. This baseline does not claim parity.
|
- Fidelity comparison: private original-client WoW 3.3.5a build 12340 references cover terrain, ADT boundary, dense M2, large WMO, liquid, animated M2 and sky transition. Every measured pair exceeds the strict visual tolerance and is recorded as a current gap rather than parity. Sky framing is approximately paired; animated M2 is asset-paired but placement-unpaired because the OpenWC Goldshire probe is synthetic. Full metrics and provenance are recorded in `docs/M00_FINAL_PAIRED_EVIDENCE.md`.
|
||||||
- Changed files: `RENDER.md`, `docs/RENDER_BASELINE.md`, `src/scenes/streaming/streaming_world_loader.gd`, `src/tools/capture_render_checkpoints.gd`, `src/tools/render_baseline_manifest.json`, `src/tools/verify_render_baseline_manifest.gd`, `tools/run_render_baseline.ps1`, `targets/00-render-baseline.md`.
|
- Changed files: `RENDER.md`, `docs/RENDER_BASELINE.md`, `docs/M00_FINAL_PAIRED_EVIDENCE.md`, `docs/modules/world-renderer.md`, renderer baseline scripts/verifiers, manifest/capture tooling, streaming shutdown ownership and M00 coordination claims.
|
||||||
- Remaining risks: original-client paired comparison is still required before DONE; D3D12 descriptor heap creation failed and the measured run fell back to Vulkan; shutdown reported leaked renderer RIDs/resources; current captures intentionally preserve visible terrain/placement/material/animation gaps, including the synthetic native animated M2 probe; perceptual image diff/tolerance is documented but not automated yet.
|
- Remaining risks: renderer parity is not claimed; documented terrain/material/placement/animation/lighting/liquid/culling gaps remain future renderer work. D3D12 descriptor heap creation can fall back to Vulkan. Godot can emit a timing-dependent anonymous zero-reference `RefCounted` teardown warning without retained Node/resource/RID ownership.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# M01 — Coordinates and Architecture Seams
|
# M01 — Coordinates and Architecture Seams
|
||||||
|
|
||||||
<!-- OPENWC_TARGET:OPEN -->
|
<!-- OPENWC_TARGET:ACTIVE -->
|
||||||
|
|
||||||
## Outcome
|
## Outcome
|
||||||
|
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ OpenWC должен предоставить четыре согласованн
|
|||||||
|
|
||||||
- MPQ/BLP/ADT/WDT/M2/WMO native parsing и renderer vertical slice уже существуют.
|
- 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.
|
- Runtime streaming, terrain quality, MultiMesh M2, WMO caches, liquids, sky и character experiments находятся в рабочем состоянии, но сосредоточены в крупных orchestration scripts.
|
||||||
- Renderer baseline M00 создан; до `DONE` не хватает paired comparison с оригинальным клиентом и закрытия известных diagnostic gaps.
|
- Renderer baseline M00 завершён: paired comparison с оригинальным клиентом записывает измеренные gaps без заявления parity, а обязательные diagnostic gaps закрыты или явно классифицированы. Текущая работа M01 вводит coordinate и architecture seams.
|
||||||
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
|
- Gameplay domain, network protocol, production UI/Lua, audio orchestration и server adapters в основном предстоит реализовать.
|
||||||
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
|
- Editor plugin пока решает extraction/preview задачи, но не является полноценной authoring platform.
|
||||||
|
|
||||||
|
|||||||
+3
-3
@@ -10,7 +10,7 @@
|
|||||||
|
|
||||||
## Current target
|
## Current target
|
||||||
|
|
||||||
`M00` — [00-render-baseline.md](00-render-baseline.md)
|
`M01` — [01-coordinates-and-seams.md](01-coordinates-and-seams.md)
|
||||||
|
|
||||||
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
Одновременно `ACTIVE` может быть только одна цель. Следующая цель становится `ACTIVE` после появления валидной `OPENWC_TARGET_DONE` метки у предыдущей.
|
||||||
|
|
||||||
@@ -33,8 +33,8 @@
|
|||||||
|
|
||||||
| ID | Цель | Зависит от | Статус |
|
| ID | Цель | Зависит от | Статус |
|
||||||
|---|---|---|---|
|
|---|---|---|---|
|
||||||
| M00 | [Renderer baseline](00-render-baseline.md) | — | ACTIVE |
|
| M00 | [Renderer baseline](00-render-baseline.md) | — | DONE |
|
||||||
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | OPEN |
|
| M01 | [Coordinates and architecture seams](01-coordinates-and-seams.md) | M00 | ACTIVE |
|
||||||
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | OPEN |
|
| M02 | [Player decomposition](02-player-decomposition.md) | M01 | OPEN |
|
||||||
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | OPEN |
|
| M03 | [Renderer facade and extraction](03-renderer-facade.md) | M02 | OPEN |
|
||||||
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
|
| M04 | [Godot Editor shell](04-editor-shell.md) | M03 | OPEN |
|
||||||
|
|||||||
@@ -40,11 +40,21 @@ function Invoke-GodotStep {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function Invoke-GodotComparisonStep {
|
||||||
|
param([string]$Name, [string[]]$Arguments)
|
||||||
|
Write-Host "[$Name] $GodotPath $($Arguments -join ' ')"
|
||||||
|
$process = Start-Process -FilePath $GodotPath -ArgumentList $Arguments -NoNewWindow -Wait -PassThru
|
||||||
|
if ($process.ExitCode -notin @(0, 1)) {
|
||||||
|
throw "$Name failed with exit code $($process.ExitCode)"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Push-Location $repoRoot
|
Push-Location $repoRoot
|
||||||
try {
|
try {
|
||||||
Invoke-GodotStep "project-load" @("--headless", "--path", ".", "--quit")
|
Invoke-GodotStep "project-load" @("--headless", "--path", ".", "--quit")
|
||||||
Invoke-GodotStep "render-materials" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_materials.gd")
|
Invoke-GodotStep "render-materials" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_materials.gd")
|
||||||
Invoke-GodotStep "m2-unique-dedupe" @("--headless", "--path", ".", "--script", "res://src/tools/verify_m2_unique_dedupe.gd")
|
Invoke-GodotStep "m2-unique-dedupe" @("--headless", "--path", ".", "--script", "res://src/tools/verify_m2_unique_dedupe.gd")
|
||||||
|
Invoke-GodotStep "runtime-cache-shutdown" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_runtime_cache_shutdown.gd")
|
||||||
Invoke-GodotStep "baseline-manifest" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_baseline_manifest.gd")
|
Invoke-GodotStep "baseline-manifest" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_baseline_manifest.gd")
|
||||||
Invoke-GodotStep "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
|
Invoke-GodotStep "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
|
||||||
|
|
||||||
@@ -80,7 +90,7 @@ try {
|
|||||||
"--viewport-height", $VisualComparisonHeight.ToString()
|
"--viewport-height", $VisualComparisonHeight.ToString()
|
||||||
)
|
)
|
||||||
Invoke-GodotStep "visual-capture" $visualCaptureArgs
|
Invoke-GodotStep "visual-capture" $visualCaptureArgs
|
||||||
Invoke-GodotStep "visual-comparison" @(
|
Invoke-GodotComparisonStep "visual-comparison" @(
|
||||||
"--headless", "--path", ".",
|
"--headless", "--path", ".",
|
||||||
"--script", "res://src/tools/compare_render_checkpoints.gd",
|
"--script", "res://src/tools/compare_render_checkpoints.gd",
|
||||||
"--",
|
"--",
|
||||||
|
|||||||
@@ -9,8 +9,10 @@ param(
|
|||||||
[double[]]$YawOffsets = @(-10.0, 0.0, 10.0),
|
[double[]]$YawOffsets = @(-10.0, 0.0, 10.0),
|
||||||
[double[]]$PitchOffsets = @(-10.0, 0.0, 10.0),
|
[double[]]$PitchOffsets = @(-10.0, 0.0, 10.0),
|
||||||
[double]$CameraFov = 62.0,
|
[double]$CameraFov = 62.0,
|
||||||
|
[double[]]$CameraFovValues = @(),
|
||||||
[int]$ViewportWidth = 1280,
|
[int]$ViewportWidth = 1280,
|
||||||
[int]$ViewportHeight = 900,
|
[int]$ViewportHeight = 900,
|
||||||
|
[int[]]$ComparisonRegion = @(),
|
||||||
[double]$WaitSeconds = 8.0,
|
[double]$WaitSeconds = 8.0,
|
||||||
[double]$MeasureSeconds = 0.1,
|
[double]$MeasureSeconds = 0.1,
|
||||||
[switch]$PlanOnly
|
[switch]$PlanOnly
|
||||||
@@ -32,9 +34,13 @@ if (-not (Test-Path -LiteralPath $GodotPath)) {
|
|||||||
if (-not (Test-Path -LiteralPath $ReferenceDirectory)) {
|
if (-not (Test-Path -LiteralPath $ReferenceDirectory)) {
|
||||||
throw "Reference directory not found: $ReferenceDirectory"
|
throw "Reference directory not found: $ReferenceDirectory"
|
||||||
}
|
}
|
||||||
|
if ($ComparisonRegion.Count -notin @(0, 4)) {
|
||||||
|
throw "ComparisonRegion requires x,y,width,height"
|
||||||
|
}
|
||||||
|
|
||||||
$invariantCulture = [Globalization.CultureInfo]::InvariantCulture
|
$invariantCulture = [Globalization.CultureInfo]::InvariantCulture
|
||||||
$cameraFovText = $CameraFov.ToString($invariantCulture)
|
$isJointFovSweep = $CameraFovValues.Count -gt 0
|
||||||
|
$effectiveCameraFovValues = if ($isJointFovSweep) { $CameraFovValues } else { @($CameraFov) }
|
||||||
$waitSecondsText = $WaitSeconds.ToString($invariantCulture)
|
$waitSecondsText = $WaitSeconds.ToString($invariantCulture)
|
||||||
$measureSecondsText = $MeasureSeconds.ToString($invariantCulture)
|
$measureSecondsText = $MeasureSeconds.ToString($invariantCulture)
|
||||||
$absoluteOutput = if ($Output.StartsWith("user://")) {
|
$absoluteOutput = if ($Output.StartsWith("user://")) {
|
||||||
@@ -46,11 +52,16 @@ $ranking = [Collections.Generic.List[object]]::new()
|
|||||||
|
|
||||||
Push-Location $repoRoot
|
Push-Location $repoRoot
|
||||||
try {
|
try {
|
||||||
foreach ($yawOffset in $YawOffsets) {
|
foreach ($cameraFovValue in $effectiveCameraFovValues) {
|
||||||
foreach ($pitchOffset in $PitchOffsets) {
|
foreach ($yawOffset in $YawOffsets) {
|
||||||
|
foreach ($pitchOffset in $PitchOffsets) {
|
||||||
|
$cameraFovText = $cameraFovValue.ToString("0.###", $invariantCulture)
|
||||||
$yawText = $yawOffset.ToString("0.###", $invariantCulture)
|
$yawText = $yawOffset.ToString("0.###", $invariantCulture)
|
||||||
$pitchText = $pitchOffset.ToString("0.###", $invariantCulture)
|
$pitchText = $pitchOffset.ToString("0.###", $invariantCulture)
|
||||||
$candidateName = "yaw_$($yawText.Replace('-', 'n').Replace('.', '_'))__pitch_$($pitchText.Replace('-', 'n').Replace('.', '_'))"
|
$candidateName = "yaw_$($yawText.Replace('-', 'n').Replace('.', '_'))__pitch_$($pitchText.Replace('-', 'n').Replace('.', '_'))"
|
||||||
|
if ($isJointFovSweep) {
|
||||||
|
$candidateName = "fov_$($cameraFovText.Replace('.', '_'))__$candidateName"
|
||||||
|
}
|
||||||
$candidateOutput = "$Output/$candidateName"
|
$candidateOutput = "$Output/$candidateName"
|
||||||
$comparisonReport = Join-Path $absoluteOutput "$candidateName.json"
|
$comparisonReport = Join-Path $absoluteOutput "$candidateName.json"
|
||||||
|
|
||||||
@@ -58,7 +69,7 @@ try {
|
|||||||
$ranking.Add([pscustomobject]@{
|
$ranking.Add([pscustomobject]@{
|
||||||
yaw_offset_degrees = $yawOffset
|
yaw_offset_degrees = $yawOffset
|
||||||
pitch_offset_degrees = $pitchOffset
|
pitch_offset_degrees = $pitchOffset
|
||||||
camera_fov_degrees = $CameraFov
|
camera_fov_degrees = $cameraFovValue
|
||||||
candidate_directory = $candidateOutput
|
candidate_directory = $candidateOutput
|
||||||
})
|
})
|
||||||
continue
|
continue
|
||||||
@@ -82,6 +93,9 @@ try {
|
|||||||
"--reference", $ReferenceDirectory, "--candidate", $candidateOutput,
|
"--reference", $ReferenceDirectory, "--candidate", $candidateOutput,
|
||||||
"--only", $Checkpoint, "--output", $comparisonReport
|
"--only", $Checkpoint, "--output", $comparisonReport
|
||||||
)
|
)
|
||||||
|
if ($ComparisonRegion.Count -eq 4) {
|
||||||
|
$comparisonArguments += @("--region", ($ComparisonRegion -join ","))
|
||||||
|
}
|
||||||
$comparisonProcess = Start-Process -FilePath $GodotPath -ArgumentList $comparisonArguments -Wait -PassThru -NoNewWindow
|
$comparisonProcess = Start-Process -FilePath $GodotPath -ArgumentList $comparisonArguments -Wait -PassThru -NoNewWindow
|
||||||
if ($comparisonProcess.ExitCode -notin @(0, 1)) {
|
if ($comparisonProcess.ExitCode -notin @(0, 1)) {
|
||||||
throw "Comparison failed for yaw=$yawText pitch=$pitchText"
|
throw "Comparison failed for yaw=$yawText pitch=$pitchText"
|
||||||
@@ -101,12 +115,13 @@ try {
|
|||||||
$ranking.Add([pscustomobject]@{
|
$ranking.Add([pscustomobject]@{
|
||||||
yaw_offset_degrees = $yawOffset
|
yaw_offset_degrees = $yawOffset
|
||||||
pitch_offset_degrees = $pitchOffset
|
pitch_offset_degrees = $pitchOffset
|
||||||
camera_fov_degrees = $CameraFov
|
camera_fov_degrees = $cameraFovValue
|
||||||
mean_perceptual_error = $meanError
|
mean_perceptual_error = $meanError
|
||||||
changed_pixel_ratio = $changedRatio
|
changed_pixel_ratio = $changedRatio
|
||||||
candidate_directory = $candidateOutput
|
candidate_directory = $candidateOutput
|
||||||
comparison_report = $comparisonReport
|
comparison_report = $comparisonReport
|
||||||
})
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,11 +138,12 @@ try {
|
|||||||
viewport = @($ViewportWidth, $ViewportHeight)
|
viewport = @($ViewportWidth, $ViewportHeight)
|
||||||
wait_seconds = $WaitSeconds
|
wait_seconds = $WaitSeconds
|
||||||
measure_seconds = $MeasureSeconds
|
measure_seconds = $MeasureSeconds
|
||||||
|
comparison_region = @($ComparisonRegion)
|
||||||
candidates = $rankedCandidates
|
candidates = $rankedCandidates
|
||||||
}
|
}
|
||||||
New-Item -ItemType Directory -Force -Path $absoluteOutput | Out-Null
|
New-Item -ItemType Directory -Force -Path $absoluteOutput | Out-Null
|
||||||
$rankingDocument | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath $rankingPath
|
$rankingDocument | ConvertTo-Json -Depth 6 | Set-Content -Encoding UTF8 -LiteralPath $rankingPath
|
||||||
$rankedCandidates | Format-Table yaw_offset_degrees, pitch_offset_degrees, mean_perceptual_error, changed_pixel_ratio
|
$rankedCandidates | Format-Table camera_fov_degrees, yaw_offset_degrees, pitch_offset_degrees, mean_perceptual_error, changed_pixel_ratio
|
||||||
$completionKind = if ($PlanOnly) { "plan" } else { "ranking" }
|
$completionKind = if ($PlanOnly) { "plan" } else { "ranking" }
|
||||||
Write-Output "Camera pose sweep $completionKind completed: $rankingPath"
|
Write-Output "Camera pose sweep $completionKind completed: $rankingPath"
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
Reference in New Issue
Block a user