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)