OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / gate_tile_grid.gd

191 lines · gd ref:
extends SceneTree
## GATE: does the heightfield tile LIST behave the way an incomplete, growing
## world needs it to?
##
## THE THREE REQUIREMENTS ARE ONE REQUIREMENT. The 900 km world is 119x119 =
## 14,161 tiles and 841 exist, so the set is incomplete; you cannot ship an
## incomplete world without a STUB for the gaps; and a stub is only useful if a
## later-generated tile can replace it WITHOUT a reload. Each one implies the
## next.
##
## Runs against REAL per-tile .hblk written from the real cache -- not fakes.
## A fake tile store would prove the test, not the loader.
##
##   "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/gate_tile_grid.gd

const DIR := "user://tile_cache/1234_hblk"
const STUB := -999.0
const PAYLOAD := 256
const MPP := 30.0

var _fails := 0


func ck(name: String, got: Variant, want: Variant) -> void:
	if str(got) == str(want):
		print("  ok    %-50s %s" % [name, str(got)])
	else:
		print("  FAIL  %-50s got %s want %s" % [name, str(got), str(want)])
		_fails += 1


## Centre of tile (tx,ty) in world metres. tx -> Z, ty -> X (stitch_block.py's
## convention; inverting it mirrors the world and still looks plausible).
func centre_of(tx: int, ty: int) -> Vector2:
	return Vector2((ty * PAYLOAD + PAYLOAD / 2) * MPP, (tx * PAYLOAD + PAYLOAD / 2) * MPP)


func _init() -> void:
	var d := DirAccess.open(DIR)
	if d == null:
		print("FAIL  no per-tile tiles at %s — run:" % DIR)
		print("      python3 stitch_block.py --per-tile <dir> --limit 12")
		quit(1)
		return
	var files: Array[String] = []
	for f in d.get_files():
		if f.ends_with(".hblk"):
			files.append(f)
	files.sort()
	if files.size() < 2:
		print("FAIL  need >= 2 tiles, found %d" % files.size())
		quit(1)
		return

	var node = ClassDB.instantiate(&"HeightfieldNode")
	node.set_stub_height(STUB)

	print("\n1. AN EMPTY LIST IS ALL STUB — not an error, not a crash")
	ck("no tiles loaded", node.tile_count(), 0)
	ck("sample is the stub", node.sample_height(0.0, 0.0), STUB)

	# Two real tiles, and their coordinates come from the FILENAMES so the test
	# cannot quietly agree with a wrong origin-to-coord inversion.
	var a_parts := files[0].trim_suffix(".hblk").split("_")
	var b_parts := files[1].trim_suffix(".hblk").split("_")
	var a_tx := int(a_parts[1]); var a_ty := int(a_parts[2])
	var b_tx := int(b_parts[1]); var b_ty := int(b_parts[2])
	var a_c := centre_of(a_tx, a_ty)
	var b_c := centre_of(b_tx, b_ty)

	print("\n2. AN INCOMPLETE SET LOADS — the gap reads as stub, the tile reads as terrain")
	ck("add tile A (%d,%d)" % [a_tx, a_ty], node.add_tile(DIR.path_join(files[0])), true)
	ck("tile_count", node.tile_count(), 1)
	ck("has_tile(A)", node.has_tile(a_tx, a_ty), true)
	var ha: float = node.sample_height(a_c.x, a_c.y)
	ck("A samples real terrain", ha != STUB, true)
	ck("  and is a plausible elevation", ha > -6000.0 and ha < 6000.0, true)
	ck("B is NOT loaded", node.has_tile(b_tx, b_ty), false)
	ck("B samples the STUB", node.sample_height(b_c.x, b_c.y), STUB)

	print("\n   CONTROL: an absent tile must NOT be served by clamping to A")
	# Clamping is the failure mode this design exists to avoid: it smears A's
	# edge across the hole, which reads as terrain. If B ever equals a real
	# height while unloaded, the sampler is clamping and the gate is worthless.
	ck("B is not A's edge value", node.sample_height(b_c.x, b_c.y) != ha, true)
	var far := centre_of(a_tx + 999, a_ty + 999)
	ck("far outside every tile is stub", node.sample_height(far.x, far.y), STUB)

	print("\n3. THE LIST UPDATES AT RUNTIME — the stub is replaced, no reload")
	ck("add tile B (%d,%d)" % [b_tx, b_ty], node.add_tile(DIR.path_join(files[1])), true)
	ck("tile_count", node.tile_count(), 2)
	ck("has_tile(B)", node.has_tile(b_tx, b_ty), true)
	var hb: float = node.sample_height(b_c.x, b_c.y)
	ck("B now samples real terrain", hb != STUB, true)
	ck("  A is unchanged by the addition", node.sample_height(a_c.x, a_c.y), ha)

	print("\n   CONTROL: clear_tiles must put the stub back")
	node.clear_tiles()
	ck("cleared", node.tile_count(), 0)
	ck("A is stub again", node.sample_height(a_c.x, a_c.y), STUB)

	print("\n4. THE PACKED GRID REACHES THE ENGINE — and the SHADER TEXT DOES NOT MOVE")
	## The standalone gate (dc-terrain/tests/test_tile_grid_gpu.cpp) proves the
	## GPU addressing on a real device. This proves the same layout survives the
	## trip through the NODE: get_gpu_data() carries the packed buffer, and the
	## emitted prelude — the actual GLSL, not a proxy for it — is byte-identical
	## before and after a tile arrives. If it moved, every fetched tile would
	## recompile the density shader.
	node.clear_tiles()
	ck("cleared for the grid probe", node.has_grid(), false)
	ck("add A", node.add_tile(DIR.path_join(files[0])), true)
	var info_a: Dictionary = node.get_grid_info()
	ck("grid is active", info_a["active"], true)
	ck("table is WORLD-sized, not footprint-sized", info_a["nx"], 119)
	ck("  square", info_a["ny"], 119)
	ck("  table floats", info_a["table"], 119 * 119)
	var prelude_a: String = node.compile_glsl_prelude(0)
	var data_a: PackedFloat32Array = node.get_gpu_data()
	var tn: int = int(info_a["n"])
	ck("buffer = table + 1 tile", data_a.size(), 119 * 119 + tn * tn)
	ck("prelude emits the paged fetch", prelude_a.contains("HBLK_G_TABLE"), true)
	ck("  and NOT the single-tile clamp", prelude_a.contains("clamp(j,0,HBLK_N-1)"), false)

	ck("add B at runtime", node.add_tile(DIR.path_join(files[1])), true)
	var prelude_b: String = node.compile_glsl_prelude(0)
	var data_b: PackedFloat32Array = node.get_gpu_data()
	ck("★ prelude is BYTE-IDENTICAL after the tile arrives", prelude_b, prelude_a)
	ck("  and the buffer GREW by exactly one tile", data_b.size() - data_a.size(), tn * tn)

	print("\n   CONTROL: the prelude must be able to differ at all")
	## Without this, "identical" could just mean the prelude is a constant.
	node.set_world_span(28.0 * 16384.0)
	var prelude_small: String = node.compile_glsl_prelude(0)
	ck("a different world emits a DIFFERENT prelude", prelude_small != prelude_a, true)
	node.set_world_span(28.0 * 32768.0)
	ck("  and restoring it restores the text", node.compile_glsl_prelude(0), prelude_b)

	print("\n5. THE WHOLE GENERATED SET LOADS — 841 tiles, incomplete, in one buffer")
	## The thing the tile list was built for. 841 of 14,161 cells is 5.9% of the
	## world and 8.9% of its own bounding box: a flight path, not a rectangle.
	node.clear_tiles()
	node.set_stub_height(STUB)
	node.set_world_span(28.0 * 32768.0)
	var t0 := Time.get_ticks_msec()
	var loaded := 0
	for f in files:
		if node.add_tile(DIR.path_join(f)):
			loaded += 1
	var ms := Time.get_ticks_msec() - t0
	var info: Dictionary = node.get_grid_info()
	var tn2: int = int(info["n"]) * int(info["n"])
	var expect: int = 119 * 119 + int(info["tiles"]) * tn2
	print("      %d files -> %d tiles resident in %d ms" % [files.size(), int(info["tiles"]), ms])
	ck("every file loaded", loaded, files.size())
	ck("world table unchanged by the volume", info["nx"], 119)
	ck("buffer = table + resident tiles", node.get_gpu_data().size(), expect)
	var mib := float(expect) * 4.0 / 1048576.0
	print("      buffer %.1f MiB  (%d resident, %d refused as out-of-world)"
			% [mib, int(info["tiles"]), files.size() - int(info["tiles"])])
	ck("fits well inside the 8 GB card", mib < 512.0, true)
	## O(n^2) repacking would not have failed — it would have hung. This is the
	## assertion that a bulk load stayed linear.
	ck("load stayed linear (under 10 s for %d tiles)" % files.size(), ms < 10000, true)

	print("\n6. THE GRAPH ACTUALLY CALLS THE SAMPLER")
	## The instrument that would have caught 2026-09-03: everything below the
	## graph was right — 839 tiles resident, 213 MiB uploaded, a correct paged
	## sampler in the prelude — and HeightfieldNode::compile_glsl returned its
	## flat-field fallback because it gated on the SINGLE tile's n_.
	## Checking for "hblk_sample(" cannot see that: the PRELUDE defines it.
	## The node emits exactly one CALL SITE, and that is what this asserts.
	var g2 := ProcNodeGraph.new()
	var hf_idx := g2.add_node(HeightfieldNode.new())
	var hf2 = g2.get_node(hf_idx)
	var empty_glsl: String = g2.compile_to_glsl_function("terrain_density", true)
	print("\n   CONTROL: with NO tiles it must emit the flat fallback")
	ck("no tiles -> no call site", empty_glsl.contains("hf_s = hblk_sample("), false)
	ck("  and it IS the flat fallback", empty_glsl.contains("gn_height = 0.0"), true)
	hf2.set_stub_height(STUB)
	hf2.set_world_span(28.0 * 32768.0)
	ck("add one tile", hf2.add_tile(DIR.path_join(files[0])), true)
	var grid_glsl: String = g2.compile_to_glsl_function("terrain_density", true)
	ck("★ with a GRID the graph CALLS the sampler",
			grid_glsl.contains("hf_s = hblk_sample("), true)
	ck("  and no longer emits the flat fallback",
			grid_glsl.contains("gn_height = 0.0"), false)

	print("\n%s  (%d failure%s)" % ["PASS" if _fails == 0 else "FAIL", _fails,
			"" if _fails == 1 else "s"])
	quit(1 if _fails > 0 else 0)