extends SceneTree
## THE FIELD, FROM BOTH SIDES, AS NUMBERS.
##
## "$HYPER_TPS_GODOT" --headless --path . --script res://scenes/test/density_field_gate.gd -- \
## --map res://scenes/maps/map_dc_diffusion.tscn --out /tmp/field.txt
##
## WHY THIS EXISTS. The proc-nodes decomposition (hyper-games #53 section 9.1)
## moves the mountain, the detail and the erosion knobs out of a hand-assembled
## wrapper and into graph nodes. Its acceptance is "both maps' emitted GLSL
## unchanged in BEHAVIOUR" -- and a diff of 60 KB of shader text cannot say that,
## because the whole point of the change is that the text is different. So the
## check has to be the field itself.
##
## TWO INDEPENDENT PATHS, because they are two independent implementations and
## the failure that matters most is them disagreeing:
## CPU DCTerrain.sample_density -> ProcNodesBridge::evaluate (collision)
## GPU DCTerrain.debug_gpu_density -> the compiled shader (what you see)
## A player falls through ground that is visibly there exactly when these part.
##
## SELF-TEST FIRST, and it can fail. #50 shipped an instrument that returned an
## identical number for every input and #53 shipped one whose threshold filtered
## out the entire problem. So before any row is written this asserts that the
## CPU field VARIES across the grid, that the GPU field VARIES, and that the two
## are not accidentally the same array. A run whose header says SELFTEST FAIL is
## not evidence of anything.
var MAP := "res://scenes/maps/map_dc_diffusion.tscn"
var OUT := "/tmp/density_field.txt"
## Probe centre. The default is inside map_dc_diffusion's baked block
## (world x 38370..69150, z 23010..53790); outside it the sampler CLAMPS and
## every row would report the same clamped edge, which reads as a pass.
var OX := 52800.0
var OZ := 37440.0
var STEP := 240.0
var N := 8
## Surface search band. Wide enough for both maps' floor/ceiling.
var Y_LO := -11000.0
var Y_HI := 10000.0
var _map: Node = null
## The GPU thread is created LAZILY, inside the meshing loop, the first time a
## chunk actually needs a GPU fill (dc_terrain.cpp:1600). One frame after
## add_child there is no thread yet and debug_gpu_density returns an empty
## array -- which reads exactly like "the GPU declined". So wait for it, and say
## how long it took rather than quietly reporting half a gate.
var _frames := 0
const GPU_WAIT_FRAMES := 600
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
for i in range(args.size() - 1):
match args[i]:
"--map": MAP = args[i + 1]
"--out": OUT = args[i + 1]
"--ox": OX = float(args[i + 1])
"--oz": OZ = float(args[i + 1])
"--step": STEP = float(args[i + 1])
"--n": N = int(args[i + 1])
var packed := load(MAP) as PackedScene
if packed == null:
print("FAIL — %s did not load" % MAP)
quit(2)
return
_map = packed.instantiate()
root.add_child(_map)
## Bisect for the density zero crossing. Density is positive below the surface
## and negative above (the SDF convention every wrapper in this stack ends on),
## so this is a plain sign search and does not care which path produced it.
func _surface(terrain: Node, x: float, z: float) -> float:
var lo := Y_LO
var hi := Y_HI
if terrain.call("sample_density", Vector3(x, lo, z)) <= 0.0:
return NAN # no ground anywhere in the band
if terrain.call("sample_density", Vector3(x, hi, z)) > 0.0:
return NAN # solid to the ceiling
for _i in range(48):
var mid := (lo + hi) * 0.5
if terrain.call("sample_density", Vector3(x, mid, z)) > 0.0:
lo = mid
else:
hi = mid
return (lo + hi) * 0.5
func _process(_dt: float) -> bool:
var terrain: Node = _map.get_node_or_null("DCTerrain")
if terrain == null:
print("FAIL — no DCTerrain in %s" % MAP); quit(1); return true
if terrain.get("generator") == null:
print("FAIL — DCTerrain has no generator; it is drawing built-in noise")
quit(1); return true
# ---- wait for the GPU thread ----------------------------------------
_frames += 1
var probe: PackedFloat32Array = PackedFloat32Array()
if terrain.has_method("debug_gpu_density"):
probe = terrain.call("debug_gpu_density", Vector3(OX, 0.0, OZ), 1.0, 8)
if probe.is_empty() and _frames < GPU_WAIT_FRAMES:
return false
# ---- CPU field -------------------------------------------------------
var cpu: Array[float] = []
for jz in range(N):
for ix in range(N):
cpu.append(_surface(terrain, OX + ix * STEP, OZ + jz * STEP))
# ---- GPU field -------------------------------------------------------
# One block at the probe centre, at a 1 m voxel so voxel_scale is LOD 0 and
# no Nyquist term is engaged -- the same field the finest chunks mesh.
var gpu: PackedFloat32Array = probe
# ---- SELF-TEST, before anything is written ---------------------------
var fails: Array[String] = []
var finite := 0
var cmin := INF
var cmax := -INF
for v in cpu:
if is_finite(v):
finite += 1
cmin = minf(cmin, v)
cmax = maxf(cmax, v)
if finite < cpu.size() / 2:
fails.append("CPU: %d/%d columns found no surface in [%.0f, %.0f]" % [
finite, cpu.size(), Y_LO, Y_HI])
if finite > 1 and absf(cmax - cmin) < 0.001:
fails.append("CPU: every column reports the same height (%.6f) — a broken ruler" % cmin)
if gpu.is_empty():
fails.append("GPU: debug_gpu_density returned nothing after %d frames — the GPU declined, so half this gate is blind" % _frames)
else:
var gmin := INF
var gmax := -INF
for v in gpu:
gmin = minf(gmin, float(v))
gmax = maxf(gmax, float(v))
if absf(gmax - gmin) < 1e-9:
fails.append("GPU: every cell reports %.9f — a broken ruler" % gmin)
var f := FileAccess.open(OUT, FileAccess.WRITE)
if fails.is_empty():
f.store_line("# SELFTEST PASS (cpu varies %.3f..%.3f m, gpu %d cells)" % [cmin, cmax, gpu.size()])
else:
f.store_line("# SELFTEST FAIL — this file is NOT evidence")
for s in fails:
f.store_line("# " + s)
f.store_line("# map=%s ox=%.1f oz=%.1f step=%.1f n=%d gpu_ready_after=%d frames" % [MAP, OX, OZ, STEP, N, _frames])
f.store_line("# CPU surface Y by bisection of DCTerrain.sample_density")
for jz in range(N):
for ix in range(N):
var v := cpu[jz * N + ix]
f.store_line("cpu %8.1f %8.1f %s" % [
OX + ix * STEP, OZ + jz * STEP,
("nan" if not is_finite(v) else "%.6f" % v)])
f.store_line("# GPU density, debug_gpu_density(origin=(%.1f,0,%.1f), voxel=1, cells=8)" % [OX, OZ])
for i in range(gpu.size()):
f.store_line("gpu %4d %.9f" % [i, gpu[i]])
f.flush()
f.close()
if fails.is_empty():
print("wrote %s — SELFTEST PASS (cpu %.3f..%.3f m over %d columns, %d gpu cells)" % [
OUT, cmin, cmax, finite, gpu.size()])
quit(0)
else:
print("wrote %s — SELFTEST FAIL:" % OUT)
for s in fails:
print(" " + s)
quit(4)
return true