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