OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_glsl_publish_race.gd

82 lines · gd ref:
extends SceneTree
## GATE — a generator recompile must never hand the GPU thread a half-built shader.
##
## DCTerrainGeneratorProcNodes._recompile_glsl() used to clear() its source string IN
## PLACE and rebuild it over milliseconds, while the worker / GPU threads held that
## string's c_str(). A batch read in that window compiled an EMPTY body: the density
## wrapper's main then calls an evaluate_density that does not exist —
## "405: 'evaluate_density' : no matching overloaded function found" — and the batch
## fell back (seen in gate_undersea_erosion's log, 2026-10-13). After the clear, the
## reassignment freed the old buffer under the GPU thread: a use-after-free.
##
##   R1  while chunks stream, two recompiles per frame for N frames: the GPU thread
##       saw NO empty body (DCTerrain.get_gpu_shader_stats().empty_bodies)
##   R2  and no density compile failed (compile_failures)
##   R0  control: the GPU really worked during the storm (gpu_submits > 0), or R1/R2
##       would pass on a terrain that never asked
##
## ⚠ WINDOWED (needs a RenderingDevice). Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . --script res://scenes/test/gate_glsl_publish_race.gd

const MAP := "res://scenes/maps/map_dc_erosion.tscn"
const T_STREAM := 240      # frames to let the first chunks stream before the storm
const N_STORM := 150       # frames of two recompiles each
const T_SETTLE := 90

var ok := true
var checks := 0
var _frame := 0
var _map: Node
var _gen: Object
var _terrain: Node
var _s0: Dictionary = {}
var _submits := 0
var _amp := 0.0
var _wl := 0.0


func _p(c: bool, m: String, got: Variant = "") -> void:
	checks += 1
	print(("ok   " if c else "FAIL: ") + m + ("" if str(got) == "" else "  (" + str(got) + ")"))
	if not c:
		ok = false


func _process(_d: float) -> bool:
	_frame += 1
	if _frame == 1:
		_map = (load(MAP) as PackedScene).instantiate()
		root.add_child(_map)
	elif _frame == T_STREAM:
		_gen = _map.get("_gen")
		_terrain = _map.get_node_or_null("DCTerrain")
		if _gen == null or _terrain == null or not _terrain.has_method("get_gpu_shader_stats"):
			_p(false, "the erosion map has a generator, a terrain and get_gpu_shader_stats")
			return _finish()
		_amp = float(_gen.call("get_submarine_amp"))
		_wl = float(_gen.call("get_submarine_wavelength"))
		_s0 = _terrain.call("get_gpu_shader_stats")
	elif _frame > T_STREAM and _frame <= T_STREAM + N_STORM:
		# TWO recompiles in one frame, as gate_undersea_erosion's stage does: the second
		# lands while the first one's rebuild has workers in flight.
		var k := _frame - T_STREAM
		_gen.call("set_submarine_amp", _amp * (1.0 + 0.01 * float(k % 2 + 1)))
		_gen.call("set_submarine_wavelength", _wl * (1.0 + 0.01 * float(k % 3 + 1)))
		_submits += int((_terrain.call("get_frame_stats") as Dictionary).get("gpu_submits", 0))
	elif _frame == T_STREAM + N_STORM + T_SETTLE:
		var s1: Dictionary = _terrain.call("get_gpu_shader_stats")
		var empty := int(s1["empty_bodies"]) - int(_s0["empty_bodies"])
		var fails := int(s1["compile_failures"]) - int(_s0["compile_failures"])
		_p(_submits > 0, "R0 the GPU worked during the storm (gpu_submits)", _submits)
		_p(empty == 0, "R1 no empty shader body reached the GPU thread", empty)
		_p(fails == 0, "R2 no density shader compile failed", fails)
		_gen.call("set_submarine_amp", _amp)
		_gen.call("set_submarine_wavelength", _wl)
		return _finish()
	return false


func _finish() -> bool:
	print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
	quit(0 if ok else 1)
	return true