fix(M03): make WMO build queue cold-load safe
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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"])
|
||||
)
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user