OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / hblk_shader_gate.gd

152 lines · gd ref:
extends SceneTree
## Does the generator EMIT the baked-heightfield density shader, and does that
## shader COMPILE?
##
## The failure this exists for has happened here before: a generator that fails
## to emit valid GLSL still loads, still logs its parameters, still renders
## something, and the world comes back empty with a perfectly healthy log
## (dc_terrain_generator_procnodes.cpp's pn_terrain_offset redefinition). So the
## check is not "does it look right" but "does glslc accept the exact string the
## compute pipeline is handed".
##
## THIS GATE DOES NOT TEST THE FIELD'S VALUES. Headless sample_density() takes
## the CPU path (ProcNodesBridge::evaluate), which does not read the heightfield
## at all -- the binding is GPU-only. What the numbers should be is gated
## separately and on the GPU, by terrain-lookdev/gate_heightmap.py against the
## standalone Vulkan harness. This gate covers emission and compilation, which is
## the half that harness cannot see.
##
##   "$HYPER_TPS_GODOT" --headless --script res://scenes/test/hblk_shader_gate.gd
##
## Writes the emitted body to /tmp/hblk_body.glsl for the glslc step.

const HBLK := "/tmp/block.hblk"

const TERRAIN_HEIGHT := 60000.0
const FLOOR_Y := -10900.0
const CEILING_Y := 10100.0


func _build_generator(hblk_path: String) -> DCTerrainGeneratorProcNodes:
	# Same graph map_dc_erosion.gd builds: GradientNoise feeding the erosion
	# node's Height/Slope sockets. Copied deliberately -- a gate that builds a
	# DIFFERENT graph is not testing the shipping one.
	var g := ProcNodeGraph.new()
	var noise := g.add_node(GradientNoiseNode.new())
	g.get_node(noise).set_input_default(1, 3.0)
	g.get_node(noise).set_input_default(2, 3)
	g.get_node(noise).set_input_default(3, 0.1)
	var erosion := g.add_node(ErosionNoiseNode.new())
	g.get_node(erosion).set_input_default(1, 5.0)
	g.get_node(erosion).set_input_default(2, 0.15)
	g.get_node(erosion).set_input_default(3, 0.6)
	g.get_node(erosion).set_input_default(4, 0.7)
	g.get_node(erosion).set_input_default(5, 7)
	g.get_node(erosion).set_input_default(6, 0.5)
	g.connect_sockets(noise, 0, erosion, 7)
	g.connect_sockets(noise, 1, erosion, 8)

	var gen := DCTerrainGeneratorProcNodes.new()
	gen.set_terrain_height(TERRAIN_HEIGHT)
	gen.set_floor_y(FLOOR_Y)
	gen.set_ceiling_y(CEILING_Y)
	if hblk_path != "":
		gen.set_heightfield_path(hblk_path)
	gen.set_graph(g)
	return gen


func _init() -> void:
	var fails := 0

	if not FileAccess.file_exists(HBLK):
		print("MISSING ", HBLK, " -- run: python stitch_block.py --tx 3 --ty 5 --n 4 --out ", HBLK)
		quit(2)
		return

	# ---- CONTROL: no heightfield -> the mountain layer, and no hblk anywhere --
	var plain := _build_generator("")
	var g0: String = plain.get_compiled_glsl()
	var ctl_mtn := g0.contains("pn_terrain_offset = vec3(mtn.x")
	var ctl_no_hb := not g0.contains("heightmap_gn") and not g0.contains("hblk_height_m")
	print("  CONTROL   no heightfield: mountain layer emitted = ", ctl_mtn,
			", heightfield absent = ", ctl_no_hb)
	if not (ctl_mtn and ctl_no_hb):
		print("            FAIL -- the control path is not what it was before this change")
		fails += 1

	# ---- SUBJECT: heightfield -> sampler, shared conversion, substitution -----
	var gen := _build_generator(HBLK)
	var gs: String = gen.get_compiled_glsl()
	var has_sampler := gs.contains("float hblk_height_m(float x, float z)")
	var has_shared := gs.contains("vec3 heightmap_gn(float x, float z)")
	var has_sub := gs.contains("pn_terrain_offset = heightmap_gn(x, z);")
	var no_mtn := not gs.contains("pn_terrain_offset = vec3(mtn.x")
	print("  EMIT      sampler = ", has_sampler, ", shared conversion = ", has_shared,
			", substitution = ", has_sub, ", mountain replaced = ", no_mtn)
	if not (has_sampler and has_shared and has_sub and no_mtn):
		print("            FAIL -- the heightfield path did not emit")
		fails += 1

	# The conversion must be the SHARED text, not a retyped copy. These two lines
	# come from erosion_wrapper_glsl.h and are what the identity gate proved.
	var shared_ok := gs.contains("float H   = y_range * EN_HAMP * 0.5;") \
			and gs.contains("return vec3((h - mid) / H, dx * M_TH / H, dz * M_TH / H);")
	print("  SHARED    conversion is erosion_wrapper_glsl.h's text = ", shared_ok)
	if not shared_ok:
		print("            FAIL -- the units were retyped, which is how #45 lost a session")
		fails += 1

	var f := FileAccess.open("/tmp/hblk_body.glsl", FileAccess.WRITE)
	f.store_string(gs)
	f.close()
	print("  wrote /tmp/hblk_body.glsl (", gs.length(), " bytes) for the glslc step")

	# The shipping map is checked in _process, not here: add_child from _init puts
	# the node in the tree but its _ready does not run until the first frame, so a
	# check made here reads the generator BEFORE the map has built it -- which is
	# exactly the false FAIL this gate produced on its first run.
	_fails = fails
	var packed := load("res://scenes/maps/map_dc_diffusion.tscn") as PackedScene
	if packed == null:
		print("  MAP       FAIL -- map_dc_diffusion.tscn did not load")
		_fails += 1
		_finish()
		return
	_map = packed.instantiate()
	root.add_child(_map)


var _fails := 0
var _map: Node = null


func _process(_dt: float) -> bool:
	# One frame after _init, so the map's _ready has run.
	if _map != null:
		var mg = _map.get("_gen")
		if mg == null:
			print("  MAP       FAIL -- the map has no generator after _ready")
			_fails += 1
		else:
			var mgs: String = mg.get_compiled_glsl()
			var map_ok := mgs.contains("pn_terrain_offset = heightmap_gn(x, z);")
			# The base noise must be OFF, or the model is buried under ~2900 m of fBm.
			var gn_off := mgs.contains("min(0,")
			print("  MAP       shipping map emits the swap = ", map_ok,
					", base noise off = ", gn_off)
			if not (map_ok and gn_off):
				_fails += 1
	_finish()
	return true


func _finish() -> void:
	print("")
	if _fails == 0:
		print("VERDICT: the generator emits the heightfield density body, the unit")
		print("         conversion is the shared text, and the shipping map uses both.")
	else:
		print("VERDICT: ", _fails, " check(s) failed.")
	quit(0 if _fails == 0 else 1)