extends Node
## GATE: no multi-chunk dc-terrain GPU submit holds the GPU longer than the
## submit budget (hyper-games #88: on ashatron's GTX 1060 a streaming submit of
## up to 8 chunks held the GPU 50-350 ms, and the game's frame could not present
## until it finished -- the draw-phase hitches).
##
## DC_GPU_SUBMIT_BUDGET_MS=2 "$G" --path . --rendering-driver vulkan --resolution 1280x720 res://scenes/test/gate_gpu_submit_budget.tscn
##
## The budget is forced LOW (2 ms) so a fast GPU has to obey it too: an 8-chunk
## stream submit is ~4.7 ms of GPU on tabertron's 2070, so without sizing this is
## red. Reads DCTerrain.get_frame_stats each frame: gpu_submit_max_multi_gpu_ms is
## the GPU-execution time (timestamps) of the slowest submit that carried more
## than one chunk.
##
## THE LIMIT: budget + one heavy chunk (p99 of single-chunk submits). Chunks at
## one LOD cost 0.4-3 ms each on the 2070 and a chunk's surface is unknown until
## it is meshed, so sizing from the per-chunk estimate can overshoot by one heavy
## chunk (measured: 3.13 and 3.86 ms against a 2 ms budget, single-chunk p99
## 2.97). Chosen 2026-10-01 over a tighter 1.5x-budget limit that runs failed by
## that one chunk. p99, not max: the root chunk (voxel 4096) is a one-off ~22 ms.
## ⛔ NOT HEADLESS (the mountain's field is GPU-only).
const MAP := "res://scenes/maps/map_mountain.tscn"
const MAP_ID := "map_mountain"
const WARMUP_S := 5.0 # the per-chunk estimate starts from nothing
const RUN_S := 25.0
func _ready() -> void:
_run.call_deferred()
func _run() -> void:
if DisplayServer.get_name() == "headless":
print("RESULT: SKIP (needs a display)")
get_tree().quit(2)
return
var budget := float(OS.get_environment("DC_GPU_SUBMIT_BUDGET_MS"))
if budget <= 0.0:
print("RESULT: FAIL (run with DC_GPU_SUBMIT_BUDGET_MS=2)")
get_tree().quit(1)
return
GameState.enter_map(MAP_ID)
var cam := Camera3D.new()
cam.far = 20000.0
add_child(cam)
cam.current = true
var map := (load(MAP) as PackedScene).instantiate()
add_child(map)
var spawn := Vector3(0, 250, 0)
var sp := get_tree().get_nodes_in_group("spawn_points")
if not sp.is_empty():
spawn = (sp[0] as Node3D).global_position
cam.global_position = spawn + Vector3(0, 2, 0)
cam.look_at(spawn + Vector3(20, 0, 0))
var found := get_tree().root.find_children("*", "DCTerrain", true, false)
if found.is_empty():
print("RESULT: FAIL (no DCTerrain)")
get_tree().quit(1)
return
var terrain: Node = found[0]
var t0 := Time.get_ticks_msec()
var submits := 0
var worst_multi := 0.0
var worst_any := 0.0
var worst_line := ""
var singles: Array[float] = []
while (Time.get_ticks_msec() - t0) / 1000.0 < RUN_S:
await get_tree().process_frame
var d: Dictionary = terrain.call("get_frame_stats")
if (Time.get_ticks_msec() - t0) / 1000.0 < WARMUP_S:
continue
submits += int(d.get("gpu_submits", 0))
var m := float(d.get("gpu_submit_max_multi_gpu_ms", 0.0))
if m > worst_multi:
worst_multi = m
worst_line = str(d.get("gpu_submit_worst", ""))
worst_any = maxf(worst_any, float(d.get("gpu_submit_max_gpu_ms", 0.0)))
var s1 := float(d.get("gpu_submit_max_single_gpu_ms", 0.0))
if s1 > 0.0:
singles.append(s1)
singles.sort()
var single_p99 := singles[int(singles.size() * 0.99)] if not singles.is_empty() else 0.0
var limit := budget + single_p99
var ok := submits > 50 and worst_multi > 0.0 and worst_multi <= limit
print("[GATE-GPU-BUDGET] budget %.1f ms, single-chunk p99 %.2f ms (%d frames) -> limit %.2f: %d submits after warm-up, slowest multi-chunk submit %.2f ms GPU, slowest any %.2f ms" % [
budget, single_p99, singles.size(), limit, submits, worst_multi, worst_any])
print("[GATE-GPU-BUDGET] a slow frame's worst job: %s" % worst_line)
print("RESULT: %s" % ("PASS" if ok else "FAIL"))
get_tree().quit(0 if ok else 1)