Merge pull request 'fix(renderer): drain threaded loads on shutdown' (#7) from work/sindo-main-codex/m00-capture-shutdown into master
Reviewed-on: #7
This commit was merged in pull request #7.
This commit is contained in:
@@ -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
|
||||||
@@ -141,6 +141,9 @@ sequenceDiagram
|
|||||||
Stream->>Budget: enqueue finalize operations
|
Stream->>Budget: enqueue finalize operations
|
||||||
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
|
Budget->>Render: attach bounded terrain/M2/WMO/liquid work
|
||||||
Stream->>Render: evict outside retention range
|
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
|
## Ownership, threading and resources
|
||||||
@@ -149,7 +152,7 @@ sequenceDiagram
|
|||||||
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
- Worker tasks не должны менять SceneTree и shared Resource concurrently.
|
||||||
- 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 обязан отменить/дождаться jobs и освободить RIDs; M00 всё ещё фиксирует leaked-resource risk.
|
- Shutdown/map switch обязан дождаться WorkerThreadPool jobs и зарегистрированных threaded ResourceLoader requests до очистки очередей, nodes, caches и RIDs.
|
||||||
- Cache resources считаются immutable после публикации.
|
- Cache resources считаются immutable после публикации.
|
||||||
|
|
||||||
## Errors, cancellation and recovery
|
## 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 |
|
| 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 | 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
|
## 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.
|
- 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.
|
- 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.
|
- 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.
|
- 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.
|
||||||
|
|
||||||
|
|||||||
@@ -2073,7 +2073,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
var path: String = String(pending.get("path", ""))
|
var path: String = String(pending.get("path", ""))
|
||||||
if not path.is_empty():
|
if not path.is_empty():
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_tile_loading_tasks.clear()
|
_tile_loading_tasks.clear()
|
||||||
|
|
||||||
@@ -2094,7 +2094,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_m2_mesh_load_requests.clear()
|
_m2_mesh_load_requests.clear()
|
||||||
_m2_mesh_finalize_queue.clear()
|
_m2_mesh_finalize_queue.clear()
|
||||||
@@ -2105,7 +2105,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_m2_animation_load_requests.clear()
|
_m2_animation_load_requests.clear()
|
||||||
_m2_animation_finalize_queue.clear()
|
_m2_animation_finalize_queue.clear()
|
||||||
@@ -2115,7 +2115,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_wmo_scene_load_requests.clear()
|
_wmo_scene_load_requests.clear()
|
||||||
|
|
||||||
@@ -2124,7 +2124,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_wmo_render_load_requests.clear()
|
_wmo_render_load_requests.clear()
|
||||||
_wmo_render_missing_cache.clear()
|
_wmo_render_missing_cache.clear()
|
||||||
@@ -2137,7 +2137,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_upgrade_tasks.clear()
|
_terrain_upgrade_tasks.clear()
|
||||||
|
|
||||||
@@ -2148,7 +2148,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_control_splat_cache_tasks.clear()
|
_terrain_control_splat_cache_tasks.clear()
|
||||||
|
|
||||||
@@ -2162,7 +2162,7 @@ func _wait_for_tile_tasks() -> void:
|
|||||||
if path.is_empty():
|
if path.is_empty():
|
||||||
continue
|
continue
|
||||||
var status := ResourceLoader.load_threaded_get_status(path)
|
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)
|
ResourceLoader.load_threaded_get(path)
|
||||||
_terrain_splat_cache_tasks.clear()
|
_terrain_splat_cache_tasks.clear()
|
||||||
_terrain_splat_result_mutex.lock()
|
_terrain_splat_result_mutex.lock()
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ extends SceneTree
|
|||||||
|
|
||||||
const DEFAULT_MANIFEST_PATH := "res://src/tools/render_baseline_manifest.json"
|
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 M2_NATIVE_ANIMATED_BUILDER := preload("res://addons/mpq_extractor/loaders/m2_native_animated_builder.gd")
|
||||||
|
const SHUTDOWN_DRAIN_FRAMES := 2
|
||||||
|
|
||||||
|
|
||||||
func _initialize() -> void:
|
func _initialize() -> void:
|
||||||
@@ -167,6 +168,10 @@ func _capture_async() -> void:
|
|||||||
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
print("RENDER_BASELINE report=%s results=%d" % [report_path, captured])
|
||||||
|
|
||||||
world.queue_free()
|
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:
|
if captured <= 0:
|
||||||
push_error("No checkpoints selected. --only filter was: %s" % only)
|
push_error("No checkpoints selected. --only filter was: %s" % only)
|
||||||
quit(1)
|
quit(1)
|
||||||
|
|||||||
Reference in New Issue
Block a user