rnd(M03): extract render budget scheduler
This commit is contained in:
@@ -0,0 +1,175 @@
|
||||
extends SceneTree
|
||||
|
||||
## Asset-free M03 regression for bounded renderer operation permits and cancellation.
|
||||
|
||||
const SCHEDULER_SCRIPT := preload("res://src/render/streaming/render_budget_scheduler.gd")
|
||||
const SCHEDULER_PATH := "res://src/render/streaming/render_budget_scheduler.gd"
|
||||
const LOADER_PATH := "res://src/scenes/streaming/streaming_world_loader.gd"
|
||||
const PERFORMANCE_ITERATIONS := 20000
|
||||
const MAXIMUM_PERMIT_CHECK_MILLISECONDS := 150.0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
var failures: Array[String] = []
|
||||
_verify_bounded_lane_and_diagnostics(failures)
|
||||
_verify_shared_lane_priority(failures)
|
||||
_verify_independent_lanes_and_frame_reset(failures)
|
||||
_verify_invalid_limits_and_unknown_lane(failures)
|
||||
_verify_cancellation(failures)
|
||||
_verify_scene_free_extraction(failures)
|
||||
var elapsed_milliseconds := _measure_permit_checks(failures)
|
||||
|
||||
if not failures.is_empty():
|
||||
for failure in failures:
|
||||
push_error("RENDER_BUDGET_SCHEDULER: %s" % failure)
|
||||
quit(1)
|
||||
return
|
||||
|
||||
print("RENDER_BUDGET_SCHEDULER PASS cases=6 iterations=%d elapsed_ms=%.3f" % [
|
||||
PERFORMANCE_ITERATIONS,
|
||||
elapsed_milliseconds,
|
||||
])
|
||||
quit(0)
|
||||
|
||||
|
||||
func _verify_bounded_lane_and_diagnostics(failures: Array[String]) -> void:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.TILE_FINALIZE: 2})
|
||||
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "first permit", failures)
|
||||
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "second permit", failures)
|
||||
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_FINALIZE), "exhausted permit", failures)
|
||||
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_FINALIZE), 0, "remaining permits", failures)
|
||||
var consumed_snapshot: Dictionary = scheduler.consumed_permits_snapshot()
|
||||
_expect_equal(int(consumed_snapshot[SCHEDULER_SCRIPT.TILE_FINALIZE]), 2, "consumed permits", failures)
|
||||
consumed_snapshot[SCHEDULER_SCRIPT.TILE_FINALIZE] = 99
|
||||
_expect_equal(
|
||||
int(scheduler.consumed_permits_snapshot()[SCHEDULER_SCRIPT.TILE_FINALIZE]),
|
||||
2,
|
||||
"diagnostic snapshot is detached",
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _verify_shared_lane_priority(failures: Array[String]) -> void:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.CHUNK_GEOMETRY: 3})
|
||||
var removals := 0
|
||||
while removals < 2 and scheduler.try_consume_permit(SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
|
||||
removals += 1
|
||||
var creates := 0
|
||||
while scheduler.try_consume_permit(SCHEDULER_SCRIPT.CHUNK_GEOMETRY):
|
||||
creates += 1
|
||||
_expect_equal(removals, 2, "shared lane removals consume first", failures)
|
||||
_expect_equal(creates, 1, "shared lane leaves one create", failures)
|
||||
|
||||
|
||||
func _verify_independent_lanes_and_frame_reset(failures: Array[String]) -> void:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({
|
||||
SCHEDULER_SCRIPT.TILE_LOD_CREATE: 1,
|
||||
SCHEDULER_SCRIPT.TILE_LOD_REMOVE: 2,
|
||||
})
|
||||
_expect_true(scheduler.try_consume_permit(SCHEDULER_SCRIPT.TILE_LOD_CREATE), "create lane permit", failures)
|
||||
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_REMOVE), 2, "remove lane independent", failures)
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.TILE_LOD_CREATE: 3})
|
||||
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_CREATE), 3, "new frame resets lane", failures)
|
||||
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.TILE_LOD_REMOVE), 0, "new frame removes absent lane", failures)
|
||||
_expect_equal(int(scheduler.consumed_permits_snapshot()[SCHEDULER_SCRIPT.TILE_LOD_CREATE]), 0, "new frame resets diagnostics", failures)
|
||||
|
||||
|
||||
func _verify_invalid_limits_and_unknown_lane(failures: Array[String]) -> void:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({
|
||||
SCHEDULER_SCRIPT.WATER_FINALIZE: 0,
|
||||
SCHEDULER_SCRIPT.M2_BUILD: -4,
|
||||
})
|
||||
_expect_false(scheduler.has_remaining_permit(SCHEDULER_SCRIPT.WATER_FINALIZE), "zero limit", failures)
|
||||
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.M2_BUILD), "negative limit clamps", failures)
|
||||
_expect_false(scheduler.try_consume_permit(&"unknown_lane"), "unknown lane", failures)
|
||||
|
||||
|
||||
func _verify_cancellation(failures: Array[String]) -> void:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.WMO_BUILD: 2})
|
||||
scheduler.cancel()
|
||||
_expect_true(scheduler.is_cancelled(), "cancelled state", failures)
|
||||
_expect_false(scheduler.try_consume_permit(SCHEDULER_SCRIPT.WMO_BUILD), "cancel rejects permit", failures)
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.WMO_BUILD: 5})
|
||||
_expect_equal(scheduler.remaining_permits(SCHEDULER_SCRIPT.WMO_BUILD), 0, "begin frame cannot revive cancelled lifecycle", failures)
|
||||
|
||||
|
||||
func _verify_scene_free_extraction(failures: Array[String]) -> void:
|
||||
var scheduler_source := _read_text(SCHEDULER_PATH, failures)
|
||||
_expect_true(scheduler_source.contains("extends RefCounted"), "scheduler is RefCounted", failures)
|
||||
for forbidden_text in ["extends Node", "WorkerThreadPool", "ResourceLoader", "RenderingServer", "Mutex"]:
|
||||
_expect_false(scheduler_source.contains(forbidden_text), "scheduler omits %s" % forbidden_text, failures)
|
||||
|
||||
var loader_source := _read_text(LOADER_PATH, failures)
|
||||
_expect_true(loader_source.contains("begin_frame(_render_operation_limits_for_frame())"), "loader begins one budget frame", failures)
|
||||
_expect_true(loader_source.contains("_render_budget_scheduler.cancel()"), "loader cancels scheduler on teardown", failures)
|
||||
_expect_false(loader_source.contains("var ops_left"), "loader has no local operation counters", failures)
|
||||
_expect_false(loader_source.contains("var finalize_budget"), "loader has no local finalize counter", failures)
|
||||
for exported_limit_name in [
|
||||
"tile_finalize_ops_per_tick",
|
||||
"terrain_upgrade_finalize_ops_per_tick",
|
||||
"terrain_control_splat_cache_finalize_ops_per_tick",
|
||||
"terrain_splat_cache_finalize_ops_per_tick",
|
||||
"terrain_splat_builds_per_tick",
|
||||
"water_finalize_ops_per_tick",
|
||||
"chunk_ops_per_tick",
|
||||
"tiles_per_tick",
|
||||
"tile_lod_ops_per_tick",
|
||||
"tile_lod_remove_ops_per_tick",
|
||||
"m2_animation_finalize_ops_per_tick",
|
||||
"m2_mesh_finalize_ops_per_tick",
|
||||
"m2_build_groups_per_tick",
|
||||
"wmo_build_instances_per_tick",
|
||||
"wmo_render_group_ops_per_tick",
|
||||
"detail_asset_ops_per_tick",
|
||||
]:
|
||||
_expect_true(
|
||||
loader_source.contains(": maxi(") and loader_source.contains(exported_limit_name),
|
||||
"frame snapshot includes %s" % exported_limit_name,
|
||||
failures
|
||||
)
|
||||
|
||||
|
||||
func _measure_permit_checks(failures: Array[String]) -> float:
|
||||
var scheduler = SCHEDULER_SCRIPT.new()
|
||||
scheduler.begin_frame({SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC: PERFORMANCE_ITERATIONS})
|
||||
var started_microseconds := Time.get_ticks_usec()
|
||||
for _iteration in PERFORMANCE_ITERATIONS:
|
||||
scheduler.try_consume_permit(SCHEDULER_SCRIPT.DETAIL_ASSET_SYNC)
|
||||
var elapsed_milliseconds := float(Time.get_ticks_usec() - started_microseconds) / 1000.0
|
||||
_expect_true(
|
||||
elapsed_milliseconds <= MAXIMUM_PERMIT_CHECK_MILLISECONDS,
|
||||
"permit checks stay within %.1f ms (actual %.3f)" % [
|
||||
MAXIMUM_PERMIT_CHECK_MILLISECONDS,
|
||||
elapsed_milliseconds,
|
||||
],
|
||||
failures
|
||||
)
|
||||
return elapsed_milliseconds
|
||||
|
||||
|
||||
func _read_text(path: String, failures: Array[String]) -> String:
|
||||
var file := FileAccess.open(path, FileAccess.READ)
|
||||
if file == null:
|
||||
failures.append("cannot open %s" % path)
|
||||
return ""
|
||||
return file.get_as_text()
|
||||
|
||||
|
||||
func _expect_equal(actual_value: int, expected_value: int, label: String, failures: Array[String]) -> void:
|
||||
if actual_value != expected_value:
|
||||
failures.append("%s expected %d, got %d" % [label, expected_value, actual_value])
|
||||
|
||||
|
||||
func _expect_true(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if not actual_value:
|
||||
failures.append("%s expected true" % label)
|
||||
|
||||
|
||||
func _expect_false(actual_value: bool, label: String, failures: Array[String]) -> void:
|
||||
if actual_value:
|
||||
failures.append("%s expected false" % label)
|
||||
@@ -0,0 +1 @@
|
||||
uid://b27nncxfog7cv
|
||||
Reference in New Issue
Block a user