fix(renderer): drain threaded loads on shutdown #7

Merged
sindoring merged 1 commits from work/sindo-main-codex/m00-capture-shutdown into master 2026-07-12 00:39:22 +03:00
4 changed files with 97 additions and 11 deletions
Showing only changes of commit ad9ba7af4e - Show all commits
@@ -0,0 +1,78 @@
# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain
<!-- OPENWC_CLAIM:M00-QAR-CAPTURE-SHUTDOWN-001:sindo-main-codex:2026-07-14 -->
## Ownership
- Target: M00
- Program: QAR
- Owner/Agent ID: sindo-main-codex
- Branch: `work/sindo-main-codex/m00-capture-shutdown`
- Lease expires UTC: 2026-07-14
- Integrator: milestone integrator
## Outcome
Eliminate the reproducible capture-tool ObjectDB leak by completing owned threaded resource requests and draining deferred scene deletion before process exit.
## Non-goals
- Refactoring renderer resource ownership.
- Hiding genuine RID or worker-task leaks.
- Changing capture images, metrics, or streaming behavior.
## Paths
- Exclusive: capture tool shutdown sequence
- Shared/hotspots: renderer baseline documentation
- Generated/ignored: verbose Godot logs
## Contracts and data
- Public API/events: none
- Schema/format version: unchanged
- Migration/compatibility: none
- Consumers: M00 baseline runner
## Dependencies
- Requires: reproducible verbose leak diagnostic
- Blocks: clean M00 capture shutdown evidence
- External state: none
## Verification
- Commands: identical verbose dry-run before/after, M00 dry-run, repository gates
- Fixtures: ADT-boundary filtered capture
- Fidelity evidence: no visual behavior change
- Performance budget: blocking resource completion is restricted to shutdown; two SceneTree drain frames only
## Documentation deliverables
- Inline public API docs: shutdown rationale comment
- Module specification: recovery/known-risk update
- Data-flow diagram: unchanged
- Sequence/state/dependency diagrams: shutdown sequence documented in baseline
- Source map/status updates: baseline evidence
## Simplicity and naming
- Important names introduced: none
- Simplest considered solution: await two SceneTree frames after `queue_free` and finish registered in-flight ResourceLoader requests during loader shutdown
- Rejected complexity/abstractions: resource registry or manual child traversal
- Unavoidable complexity and justification: deferred deletion requires frame drain
- Measured optimization evidence: not applicable
## Status
- State: ready-for-review
- Done: one leaked RefCounted reproduced; isolation attributed it to `StreamingWorldLoader`; active threaded tile request was confirmed as the retained object; all owned threaded resource registries now finish in-progress requests before clearing; two identical verbose ADT-boundary runs and the full M00 dry-run completed without ObjectDB leak diagnostics
- Next: integrator review and merge
- Blocked by:
## Handoff
- Commit: this work-package commit
- Results: verbose filtered capture returned one report and zero `Leaked instance`/`ObjectDB instances leaked` lines on two consecutive runs; `run_render_baseline.ps1 -DryRun` passed all gates and seven checkpoints
- Remaining risks:
- Documentation updated: `docs/modules/world-renderer.md` shutdown sequence, ownership contract, recovery table and known-risk status
+6 -3
View File
@@ -141,6 +141,9 @@ sequenceDiagram
Stream->>Budget: enqueue finalize operations
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
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
```
## Ownership, threading and resources
@@ -149,7 +152,7 @@ sequenceDiagram
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
- Parsed/grouped results передаются обратно через guarded result queues.
- Mesh/material/node/RID finalization выполняется main thread и ограничивается exported budgets.
- Shutdown/map switch обязан отменить/дождаться jobs и освободить RIDs; M00 всё ещё фиксирует leaked-resource risk.
- Shutdown/map switch обязан дождаться WorkerThreadPool jobs и зарегистрированных threaded ResourceLoader requests до очистки очередей, nodes, caches и RIDs.
- Cache resources считаются immutable после публикации.
## Errors, cancellation and recovery
@@ -162,7 +165,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 | Shutdown report | Ownership cleanup before DONE |
| Shutdown leak | Godot leak/RID diagnostics | Resource retained | Verbose shutdown report | Drain owned worker and resource requests before clearing registries |
## Configuration and capabilities
@@ -226,7 +229,7 @@ Exact exported settings and cache versions remain documented in [`../../RENDER.m
- Terrain-height probe исключил under-terrain состояние для всех пяти точек; waterfall exact-XZ miss классифицирован как TriangleMesh seam/edge и подтверждён nearby sample в 2 units.
- Camera-occluder probe не нашёл camera containment в пяти точках; paired mismatch локализован прежде всего в manual look direction/target/FOV calibration, с явным ограничением по RID-only geometry.
- Empirical FOV sweep выявил, что checkpoint camera должна явно вызывать `make_current()`; после исправления projection ranking остаётся inconclusive из-за неизвестного manual yaw/pitch/framing reference.
- D3D12 descriptor and shutdown RID/resource issues remain.
- 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.
- M2/WMO/material/particle/ribbon/portal parity incomplete.
- Public API is mostly exported configuration rather than stable contracts.
@@ -2073,7 +2073,7 @@ func _wait_for_tile_tasks() -> void:
var path: String = String(pending.get("path", ""))
if not path.is_empty():
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_tile_loading_tasks.clear()
@@ -2094,7 +2094,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_mesh_load_requests.clear()
_m2_mesh_finalize_queue.clear()
@@ -2105,7 +2105,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_m2_animation_load_requests.clear()
_m2_animation_finalize_queue.clear()
@@ -2115,7 +2115,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_scene_load_requests.clear()
@@ -2124,7 +2124,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_wmo_render_load_requests.clear()
_wmo_render_missing_cache.clear()
@@ -2137,7 +2137,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_upgrade_tasks.clear()
@@ -2148,7 +2148,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_control_splat_cache_tasks.clear()
@@ -2162,7 +2162,7 @@ func _wait_for_tile_tasks() -> void:
if path.is_empty():
continue
var status := ResourceLoader.load_threaded_get_status(path)
if status == ResourceLoader.THREAD_LOAD_LOADED:
if status == ResourceLoader.THREAD_LOAD_IN_PROGRESS or status == ResourceLoader.THREAD_LOAD_LOADED:
ResourceLoader.load_threaded_get(path)
_terrain_splat_cache_tasks.clear()
_terrain_splat_result_mutex.lock()
+5
View File
@@ -2,6 +2,7 @@ extends SceneTree
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
const M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
const SHUTDOWN_DRAIN_FRAMES := 2
func _initialize() -> void:
@@ -167,6 +168,10 @@ func _capture_async() -> void:
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
world.queue_free()
# SceneTree deletion is deferred. Allow the world and its queued children
# to run their shutdown paths before engine teardown.
for unused_frame in SHUTDOWN_DRAIN_FRAMES:
await process_frame
if captured <= 0:
push_error("No checkpoints selected. --only filter was: %s" % only)
quit(1)