fix(M03): make WMO build queue cold-load safe

This commit is contained in:
2026-07-17 00:59:11 +04:00
parent ab40a76b62
commit e576ae2cad
4 changed files with 77 additions and 18 deletions
+8 -4
View File
@@ -69,7 +69,11 @@ editor UI are forbidden dependencies.
| `has_pending()` / `pending_count()` | Query | Observe FIFO entries including duplicate/stale keys | Renderer main thread | None |
| `active_job_count()` | Query | Count current keyed jobs | Renderer main thread | None |
| `front_key()` / `pop_front()` | Queue operation | Inspect/remove one front FIFO key | Renderer main thread | Empty queue returns empty String |
| `job_for(placement_key)` | Query | Borrow current typed job or null for stale key | Renderer main thread | No ownership transfer |
| `has_job(placement_key)` | Query | Distinguish a live job from a stale FIFO key | Renderer main thread | Unknown key returns false |
| `job_for(placement_key)` | Diagnostic/test query | Borrow current job through its `RefCounted` base or return null | Renderer main thread | Runtime loader uses typed accessors instead |
| `root_for` / `render_resource_for` | Typed query | Borrow current engine references without exposing job implementation type | Renderer main thread | Stale key returns null |
| `mesh_index_for` / `multimesh_index_for` | Typed query | Return current cursors | Renderer main thread | Stale key returns zero |
| `adopt_cursors(placement_key, next_mesh_index, next_multimesh_index)` | Command/query | Atomically update current job cursors | Renderer main thread | Stale key returns false |
| `cancel(placement_key)` | Command/query | Remove keyed job and first matching FIFO key | Renderer main thread | Unknown key returns false; stale duplicates remain |
| `clear()` | Command | Release all job/key references idempotently | Reset/shutdown | Does not free engine objects |
| `diagnostic_snapshot()` | Query | Return detached FIFO order and key-sorted scalar jobs | Caller-owned result | None |
@@ -134,15 +138,15 @@ sequenceDiagram
participant Planner as WmoRenderBuildStepPlanner
Loader->>Queue: enqueue(key, root, resource)
loop while permit and pending key
Loader->>Queue: front_key + job_for
Loader->>Queue: front_key + has_job
alt stale key
Loader->>Queue: pop_front
else live job
Queue-->>Loader: borrowed Job
Loader->>Queue: typed root/resource/cursor accessors
Loader->>Planner: plan_step(counts, job cursors)
Planner-->>Loader: operation and next cursors
Loader->>Loader: materialize one group on main thread
Loader->>Job: adopt_cursors(next mesh, next MultiMesh)
Loader->>Queue: adopt_cursors(key, next mesh, next MultiMesh)
Loader->>Loader: consume one permit
end
end
+56 -5
View File
@@ -35,9 +35,60 @@ func front_key() -> String:
return _queued_placement_keys.front()
## Returns the current typed job for a placement key, or null for a stale key.
func job_for(placement_key: String) -> WmoRenderBuildJob:
return _jobs_by_placement_key.get(placement_key, null) as WmoRenderBuildJob
## Returns whether a FIFO placement key still has a current job record.
func has_job(placement_key: String) -> bool:
return _jobs_by_placement_key.has(placement_key)
## Returns the current job record as its engine base type, or null for a stale
## key. Runtime consumers should prefer the typed accessors below.
func job_for(placement_key: String) -> RefCounted:
return _jobs_by_placement_key.get(placement_key, null) as RefCounted
## Returns the borrowed root for a current job, or null for a stale key.
func root_for(placement_key: String) -> Node3D:
var job := job_for(placement_key)
if job == null:
return null
return job.call("root") as Node3D
## Returns the borrowed render resource for a current job, or null when stale.
func render_resource_for(placement_key: String) -> Resource:
var job := job_for(placement_key)
if job == null:
return null
return job.call("render_resource") as Resource
## Returns the current mesh cursor, or zero for a stale key.
func mesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("mesh_index"))
## Returns the current MultiMesh cursor, or zero for a stale key.
func multimesh_index_for(placement_key: String) -> int:
var job := job_for(placement_key)
if job == null:
return 0
return int(job.call("multimesh_index"))
## Adopts both cursors for a current job and returns whether it existed.
func adopt_cursors(
placement_key: String,
next_mesh_index: int,
next_multimesh_index: int
) -> bool:
var job := job_for(placement_key)
if job == null:
return false
job.call("adopt_cursors", next_mesh_index, next_multimesh_index)
return true
## Pops and returns the front FIFO key, or an empty String when already empty.
@@ -79,9 +130,9 @@ func diagnostic_snapshot() -> Dictionary:
var jobs: Array[Dictionary] = []
for placement_key_variant in placement_keys:
var placement_key := String(placement_key_variant)
var job := job_for(placement_key)
var job: RefCounted = job_for(placement_key)
if job != null:
jobs.append(job.diagnostic_snapshot())
jobs.append(job.call("diagnostic_snapshot") as Dictionary)
return {
"queued_placement_keys": _queued_placement_keys.duplicate(),
"jobs": jobs,
@@ -3768,17 +3768,16 @@ func _process_wmo_render_build_jobs() -> void:
while _render_budget_scheduler.has_remaining_permit(
RENDER_BUDGET_SCHEDULER_SCRIPT.WMO_RENDER_GROUP_BUILD) and _wmo_render_build_queue.has_pending():
var unique_key := _wmo_render_build_queue.front_key()
var job := _wmo_render_build_queue.job_for(unique_key)
if job == null:
if not _wmo_render_build_queue.has_job(unique_key):
_wmo_render_build_queue.pop_front()
continue
var root: Node = job.root()
var root: Node = _wmo_render_build_queue.root_for(unique_key)
if root == null or not is_instance_valid(root):
_cancel_wmo_render_build_job(unique_key)
continue
var render_resource: Resource = job.render_resource()
var render_resource: Resource = _wmo_render_build_queue.render_resource_for(unique_key)
if render_resource == null:
_cancel_wmo_render_build_job(unique_key)
continue
@@ -3786,11 +3785,11 @@ func _process_wmo_render_build_jobs() -> void:
var meshes: Array = render_resource.get("meshes")
var mesh_transforms: Array = render_resource.get("mesh_transforms")
var mesh_names: PackedStringArray = render_resource.get("mesh_names")
var mesh_index := job.mesh_index()
var mesh_index := _wmo_render_build_queue.mesh_index_for(unique_key)
var multimeshes: Array = render_resource.get("multimeshes")
var multimesh_transforms: Array = render_resource.get("multimesh_transforms")
var multimesh_names: PackedStringArray = render_resource.get("multimesh_names")
var multimesh_index := job.multimesh_index()
var multimesh_index := _wmo_render_build_queue.multimesh_index_for(unique_key)
var build_step: Dictionary = _wmo_render_build_step_planner.plan_step(
meshes.size(),
mesh_index,
@@ -3821,7 +3820,8 @@ func _process_wmo_render_build_jobs() -> void:
mesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(mesh_instance)
_set_editor_owner_recursive(mesh_instance)
job.adopt_cursors(
_wmo_render_build_queue.adopt_cursors(
unique_key,
int(build_step["next_mesh_index"]),
int(build_step["next_multimesh_index"])
)
@@ -3851,7 +3851,8 @@ func _process_wmo_render_build_jobs() -> void:
multimesh_instance.visibility_range_end_margin = CHUNK_SIZE
(root as Node3D).add_child(multimesh_instance)
_set_editor_owner_recursive(multimesh_instance)
job.adopt_cursors(
_wmo_render_build_queue.adopt_cursors(
unique_key,
int(build_step["next_mesh_index"]),
int(build_step["next_multimesh_index"])
)
+4 -1
View File
@@ -133,7 +133,10 @@ func _verify_loader_and_engine_lifetime_boundaries(failures: Array[String]) -> v
failures
)
_expect_true(loader_source.contains("_wmo_render_build_queue.enqueue("), "loader delegates enqueue", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.job_for("), "loader reads typed job", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.has_job("), "loader checks stale keys", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.root_for("), "loader borrows root", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.render_resource_for("), "loader borrows resource", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.adopt_cursors("), "loader delegates cursors", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.cancel("), "loader delegates cancel", failures)
_expect_true(loader_source.contains("_wmo_render_build_queue.clear()"), "loader delegates clear", failures)
_expect_false(loader_source.contains("var _wmo_render_build_jobs"), "legacy job dictionary removed", failures)