From ad9ba7af4e9677b07c5e20bcd0c9ca2c221fc14b Mon Sep 17 00:00:00 2001 From: sindoring Date: Sun, 12 Jul 2026 01:28:26 +0400 Subject: [PATCH] fix(renderer): drain threaded loads on shutdown --- .../claims/M00-QAR-CAPTURE-SHUTDOWN-001.md | 78 +++++++++++++++++++ docs/modules/world-renderer.md | 9 ++- .../streaming/streaming_world_loader.gd | 16 ++-- src/tools/capture_render_checkpoints.gd | 5 ++ 4 files changed, 97 insertions(+), 11 deletions(-) create mode 100644 coordination/claims/M00-QAR-CAPTURE-SHUTDOWN-001.md diff --git a/coordination/claims/M00-QAR-CAPTURE-SHUTDOWN-001.md b/coordination/claims/M00-QAR-CAPTURE-SHUTDOWN-001.md new file mode 100644 index 0000000..28921b0 --- /dev/null +++ b/coordination/claims/M00-QAR-CAPTURE-SHUTDOWN-001.md @@ -0,0 +1,78 @@ +# M00-QAR-CAPTURE-SHUTDOWN-001 — Capture shutdown drain + + + +## 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 diff --git a/docs/modules/world-renderer.md b/docs/modules/world-renderer.md index f80e3fc..a2aaeef 100644 --- a/docs/modules/world-renderer.md +++ b/docs/modules/world-renderer.md @@ -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. diff --git a/src/scenes/streaming/streaming_world_loader.gd b/src/scenes/streaming/streaming_world_loader.gd index 618d9da..22d2c7c 100644 --- a/src/scenes/streaming/streaming_world_loader.gd +++ b/src/scenes/streaming/streaming_world_loader.gd @@ -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() diff --git a/src/tools/capture_render_checkpoints.gd b/src/tools/capture_render_checkpoints.gd index 2539ac0..b7b9bae 100644 --- a/src/tools/capture_render_checkpoints.gd +++ b/src/tools/capture_render_checkpoints.gd @@ -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) -- 2.52.0