Compare commits

...

8 Commits

Author SHA1 Message Date
sindoring 09b3e87a8d fix(M00): release GUI capture render resources
Work-Package: M00-QAR-GUI-RID-SHUTDOWN-001
Agent: sindo-main-codex
Tests: render baseline dry-run; runtime cache shutdown; comparator self-test; coordination and documentation gates
Fidelity: shutdown-only ownership fix; no visual or Blizzlike335 behavior change
2026-07-13 11:08:30 +04:00
sindoring d1df794cd6 Merge pull request 'test(M00): complete animation and sky evidence' (#14) from work/sindo-main-codex/m00-animation-sky-evidence into master
Reviewed-on: #14
2026-07-13 09:02:32 +03:00
sindoring 0327e4862d test(M00): complete animation and sky evidence
Work-Package: M00-QAR-ANIMATION-SKY-EVIDENCE-001
Agent: sindo-main-codex
Tests: 14-pair visual audit; manifest/self-test; coordination/documentation gates
Fidelity: sky evidence paired approximately; Gryphon animation observed but placement-unpaired
2026-07-13 10:01:19 +04:00
sindoring 239cb2432e Merge pull request 'test(M00): record final paired evidence' (#13) from work/sindo-main-codex/m00-final-paired-evidence into master
Reviewed-on: #13
2026-07-12 02:28:00 +03:00
sindoring b5ddc86c92 test(M00): record final paired evidence 2026-07-12 03:21:39 +04:00
sindoring 3d90c6d79a Merge pull request 'test(M00): add landmark-region visual diff' (#12) from work/sindo-main-codex/m00-landmark-region-diff into master
Reviewed-on: #12
2026-07-12 02:07:30 +03:00
sindoring a10206b797 test(M00): add landmark-region visual diff 2026-07-12 03:03:59 +04:00
sindoring 3172f41ee5 Merge pull request 'test(M00): refine Goldshire camera framing' (#11) from work/sindo-main-codex/m00-camera-framing-refinement into master
Reviewed-on: #11
2026-07-12 01:51:08 +03:00
14 changed files with 628 additions and 18 deletions
+8
View File
@@ -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`.
- 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
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.
+4 -1
View File
@@ -788,7 +788,10 @@ static func _build_tile_water_root(data: Dictionary, origin_offset: Vector3) ->
mi.cast_shadow = GeometryInstance3D.SHADOW_CASTING_SETTING_OFF
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(
@@ -0,0 +1,78 @@
# M00-QAR-ANIMATION-SKY-EVIDENCE-001 — Animation and sky evidence
<!-- OPENWC_CLAIM:M00-QAR-ANIMATION-SKY-EVIDENCE-001:sindo-main-codex:2026-07-15 -->
## 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,78 @@
# M00-QAR-FINAL-PAIRED-EVIDENCE-001 — Final paired evidence audit
<!-- OPENWC_CLAIM:M00-QAR-FINAL-PAIRED-EVIDENCE-001:sindo-main-codex:2026-07-14 -->
## 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`
@@ -0,0 +1,78 @@
# M00-QAR-GUI-RID-SHUTDOWN-001 — GUI capture RID shutdown
<!-- OPENWC_CLAIM:M00-QAR-GUI-RID-SHUTDOWN-001:sindo-main-codex:2026-07-15 -->
## 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,78 @@
# M00-QAR-LANDMARK-REGION-DIFF-001 — Landmark-region visual diff
<!-- OPENWC_CLAIM:M00-QAR-LANDMARK-REGION-DIFF-001:sindo-main-codex:2026-07-14 -->
## 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`
+6
View File
@@ -54,4 +54,10 @@ A WMO-ready joint grid fixed yaw at 10 degrees and evaluated pitch `[-25, -20, -
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.
+76
View File
@@ -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.
+11 -4
View File
@@ -143,7 +143,8 @@ sequenceDiagram
Stream->>Render: evict outside retention range
Stream->>Worker: shutdown: wait for WorkerThreadPool tasks
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
@@ -153,6 +154,8 @@ sequenceDiagram
- Parsed/grouped results передаются обратно через guarded result queues.
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
- 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 после публикации.
## 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 |
| 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 |
| 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
@@ -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.
- 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.
- Manual diagnostics: [`../RENDER_BASELINE.md`](../RENDER_BASELINE.md) and [`../../RENDER.md`](../../RENDER.md).
@@ -233,7 +236,9 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- 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.
- 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.
- 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.
- 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.
- Public API is mostly exported configuration rather than stable contracts.
@@ -251,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 |
| `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/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 |
| `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 |
@@ -262,5 +268,6 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- [`../../RENDER.md`](../../RENDER.md)
- [`../RENDER_BASELINE.md`](../RENDER_BASELINE.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)
- [`../GODOT_BEST_PRACTICES.md`](../GODOT_BEST_PRACTICES.md)
+52 -7
View File
@@ -374,6 +374,39 @@ func _exit_tree() -> void:
_shutting_down = true
_wait_for_tile_tasks()
_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:
@@ -2781,15 +2814,27 @@ func _clear_streamed_world() -> void:
_free_render_instance(chunk_rid)
var tile_root: Node = state.get("root", null)
if tile_root:
tile_root.queue_free()
_queue_free_streamed_node(tile_root)
_tile_states.clear()
_clear_tile_mesh_cache()
_clear_quality_terrain_mesh_cache()
_clear_wmo_registry()
_ensure_world_wmo_root()
if enable_wmo:
_place_global_wmo()
if not _shutting_down:
_ensure_world_wmo_root()
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:
@@ -3889,10 +3934,10 @@ func _clear_wmo_registry() -> void:
var entry: Dictionary = entry_variant
var node: Node = entry.get("node", null)
if node and is_instance_valid(node):
node.queue_free()
_queue_free_streamed_node(node)
_wmo_registry.clear()
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
@@ -4458,7 +4503,7 @@ func _cancel_m2_build_job(key: String) -> void:
var job: Dictionary = _m2_build_jobs[key]
var root: Node = job.get("root", null)
if root != null and is_instance_valid(root):
root.queue_free()
_queue_free_streamed_node(root)
_m2_build_jobs.erase(key)
+63 -5
View File
@@ -3,6 +3,7 @@ extends SceneTree
## Compares original-client JPG/PNG references with OpenWC checkpoint PNGs.
## Usage: godot --headless --path . --script res://src/tools/compare_render_checkpoints.gd --
## --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]
## 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
index += 1
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}
if index + 1 >= raw_arguments.size():
return {"error": "missing value for %s" % argument}
var key := argument.trim_prefix("--").replace("-", "_")
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():
return {"error": "%s requires a number" % argument}
parsed[key] = float(value)
@@ -72,6 +78,24 @@ func _parse_arguments(raw_arguments: PackedStringArray) -> Dictionary:
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:
reference_directory = ProjectSettings.globalize_path(reference_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,
"mean_threshold": options.mean_threshold,
"changed_ratio_threshold": options.changed_ratio_threshold,
"comparison_region": _region_array(options.get("region", Rect2i())),
"compared_count": results.size() - missing_count,
"failed_count": failed_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()],
"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 changed_pixel_count := 0
var pixel_count := reference_image.get_width() * reference_image.get_height()
for y in reference_image.get_height():
for x in reference_image.get_width():
var pixel_count := comparison_region.size.x * comparison_region.size.y
for y in range(comparison_region.position.y, comparison_region.end.y):
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))
error_sum += perceptual_error
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",
"width": reference_image.get_width(),
"height": reference_image.get_height(),
"region": _region_array(comparison_region),
"mean_perceptual_error": mean_perceptual_error,
"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:
var reference_linear := reference_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:
push_error("RENDER_CHECKPOINT_DIFF SELF_TEST: expected identical pass and changed failure")
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:
return 1
var filtered_options := options.duplicate()
@@ -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)
+11 -1
View File
@@ -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
try {
Invoke-GodotStep "project-load" @("--headless", "--path", ".", "--quit")
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 "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 "coordinate-calibration" @("--headless", "--path", ".", "--script", "res://src/tools/verify_render_coordinate_calibration.gd")
@@ -80,7 +90,7 @@ try {
"--viewport-height", $VisualComparisonHeight.ToString()
)
Invoke-GodotStep "visual-capture" $visualCaptureArgs
Invoke-GodotStep "visual-comparison" @(
Invoke-GodotComparisonStep "visual-comparison" @(
"--headless", "--path", ".",
"--script", "res://src/tools/compare_render_checkpoints.gd",
"--",
@@ -12,6 +12,7 @@ param(
[double[]]$CameraFovValues = @(),
[int]$ViewportWidth = 1280,
[int]$ViewportHeight = 900,
[int[]]$ComparisonRegion = @(),
[double]$WaitSeconds = 8.0,
[double]$MeasureSeconds = 0.1,
[switch]$PlanOnly
@@ -33,6 +34,9 @@ if (-not (Test-Path -LiteralPath $GodotPath)) {
if (-not (Test-Path -LiteralPath $ReferenceDirectory)) {
throw "Reference directory not found: $ReferenceDirectory"
}
if ($ComparisonRegion.Count -notin @(0, 4)) {
throw "ComparisonRegion requires x,y,width,height"
}
$invariantCulture = [Globalization.CultureInfo]::InvariantCulture
$isJointFovSweep = $CameraFovValues.Count -gt 0
@@ -89,6 +93,9 @@ try {
"--reference", $ReferenceDirectory, "--candidate", $candidateOutput,
"--only", $Checkpoint, "--output", $comparisonReport
)
if ($ComparisonRegion.Count -eq 4) {
$comparisonArguments += @("--region", ($ComparisonRegion -join ","))
}
$comparisonProcess = Start-Process -FilePath $GodotPath -ArgumentList $comparisonArguments -Wait -PassThru -NoNewWindow
if ($comparisonProcess.ExitCode -notin @(0, 1)) {
throw "Comparison failed for yaw=$yawText pitch=$pitchText"
@@ -131,6 +138,7 @@ try {
viewport = @($ViewportWidth, $ViewportHeight)
wait_seconds = $WaitSeconds
measure_seconds = $MeasureSeconds
comparison_region = @($ComparisonRegion)
candidates = $rankedCandidates
}
New-Item -ItemType Directory -Force -Path $absoluteOutput | Out-Null