extends SceneTree
## GATE — the undersea field on the GPU equals the graph sampled on the CPU (§4 P2).
##
## gate_undersea_geiss_graph samples the graph on the CPU (evaluate_socket). What the
## game meshes is the EMITTED GLSL, run by DCTerrainGeneratorProcNodesBounded inside
## dc-terrain's density_fill. This compares the two at the same world points:
## dc-terrain DCTerrain.debug_gpu_density() -> evaluate_density -> proc_density
## CPU UnderseaGeissGraph with a constant Vector, evaluate_socket, built with
## the SCENE's knobs
## The GPU runs the bridged scene's graph (blender/undersea_geiss.tscn), and since
## 2026-10-07 so does the CPU side: THE SCENE IS CANON. The builder was recalibrated on
## 2026-10-06 and the scene was not re-seeded; the user chose to keep the scene's look
## (hyper-games #108). So the CPU reference is a duplicate(true) of the scene's own graph
## with each Position node rewired to a constant VectorNode (Position reads geometry,
## which a point sample does not have), evaluated at the socket feeding Group Output.
## Whether the scene still matches the builder is printed, not gated. (ProcNodeGraph.
## evaluate_density_at is not a substitute: it returns 0 for a Group Output Float.)
## Three blocks straddling the ground plane (y ~ 0, ~±40 m of relief since the 2026-10-06
## rebuild; the old blocks at -24 / -140 / -320 m were all solid). It also settles the
## SIGN: the CPU graph is positive below the surface, and dc-terrain treats positive as
## solid, so agreement means the GPU field is solid where it should be.
##
## Negative control: the CPU sampled half a voxel sideways must FAIL, or the comparison is
## too loose to mean anything.
##
## ⚠ NEEDS A RENDERING DEVICE — run WINDOWED (DISPLAY=:0, no --headless). A hidden
## window still has one (dc-terrain brings up its own compute thread).
##
## Run: CI=true HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . --script res://scenes/test/gate_undersea_gpu_parity.gd
const VOXEL := 1.0
const CELLS := 8
const ORIGINS := [Vector3(13.0, -4.0, 7.0), Vector3(-41.0, -6.0, 66.0), Vector3(250.0, -2.0, -180.0)]
const MAX_ABS := 0.05 # m — float precision, not structure
const MAX_STEP_FLIPS := 0.01 # fraction of points allowed to straddle a floor boundary
var ok := true
var checks := 0
var _terrain: Node
var _frames := 0
var _g: ProcNodeGraph
## The CPU sample graph: a copy of the scene's, Position nodes -> these VectorNodes.
var _s: ProcNodeGraph
var _pos_nodes: Array[int] = []
var _out_node := -1
var _out_socket := -1
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 _initialize() -> void:
_g = UnderseaGeissGraph.load_graph()
_p(_g != null, "the scene holds the undersea graph")
var knobs := UnderseaGeissGraph.knobs_for(_g)
var a: String = _g.compile_to_glsl_function("proc_density", false, PackedStringArray()) if _g != null else ""
var b: String = UnderseaGeissGraph.make(null, knobs).compile_to_glsl_function("proc_density", false, PackedStringArray())
print(" info: the scene's structure %s the builder's" % ("MATCHES" if a == b else "differs from (scene is canon)"))
_build_sample_graph()
_p(not _pos_nodes.is_empty() and _out_node >= 0,
"the CPU sample graph is the scene's own (%d Position node(s) rewired, output node %d socket %d)"
% [_pos_nodes.size(), _out_node, _out_socket])
var gen: Object = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
gen.set("world_scale", 1.0)
gen.set("graph", _g)
_p(bool(gen.call("has_compiled_shader")), "the bounded generator compiled the graph",
String(gen.call("get_last_compile_error")))
_terrain = ClassDB.instantiate("DCTerrain")
_terrain.set("use_gpu", true)
_terrain.set("voxel_size", VOXEL)
_terrain.set("generator", gen)
root.add_child(_terrain)
## Copy the scene's graph; rewire every Position output to a fresh VectorNode; find
## the (node, socket) feeding the Group Output's first input.
func _build_sample_graph() -> void:
_s = _g.duplicate(true) as ProcNodeGraph
var conns: PackedInt32Array = _s.get_connections_flat()
var go := -1
for i in _s.get_node_count():
var n: ProcNode = _s.get_node(i)
if n == null:
continue
if String(n.get_node_type()) == "GroupOutput":
go = i
elif n is PositionNode:
var v := _s.add_node(VectorNode.new())
_pos_nodes.append(v)
for c in range(0, conns.size(), 4):
if conns[c] == i:
_s.disconnect_sockets(i, conns[c + 1], conns[c + 2], conns[c + 3])
_s.connect_sockets(v, 0, conns[c + 2], conns[c + 3])
for c in range(0, conns.size(), 4):
if conns[c + 2] == go and conns[c + 3] == 0:
_out_node = conns[c]
_out_socket = conns[c + 1]
func _cpu(p: Vector3) -> float:
for v in _pos_nodes:
_s.get_node(v).set("value", p)
return float(_s.evaluate_socket(_out_node, _out_socket))
## {max, flips, mean} of |gpu - cpu(p + shift)| over one block.
func _compare(gpu: PackedFloat32Array, org: Vector3, shift: Vector3) -> Dictionary:
var s := CELLS + 1
var worst := 0.0
var flips := 0
var sum := 0.0
var n := 0
for z in s:
for y in s:
for x in s:
var d := absf(gpu[z * s * s + y * s + x] - _cpu(org + Vector3(x, y, z) * VOXEL + shift))
if d > 1.0:
flips += 1 # a floor step decided differently — counted, not maxed
else:
worst = maxf(worst, d)
sum += minf(d, 1.0)
n += 1
return {"max": worst, "flips": float(flips) / float(n), "mean": sum / float(n)}
func _process(_dt: float) -> bool:
_frames += 1
if _frames < 90:
return false
var all_solid := 0
var all_air := 0
for org: Vector3 in ORIGINS:
var gpu: PackedFloat32Array = _terrain.call("debug_gpu_density", org, VOXEL, CELLS)
if gpu.size() != (CELLS + 1) * (CELLS + 1) * (CELLS + 1):
_p(false, "debug_gpu_density returned %d values at %s (GPU declined)" % [gpu.size(), str(org)])
continue
var r := _compare(gpu, org, Vector3.ZERO)
var s1 := CELLS + 1
for probe: Vector3i in [Vector3i(0, 0, 0), Vector3i(4, 4, 4), Vector3i(8, 2, 5)]:
var gi := probe.z * s1 * s1 + probe.y * s1 + probe.x
print(" sample %s: gpu %.4f cpu %.4f" % [str(org + Vector3(probe) * VOXEL), gpu[gi], _cpu(org + Vector3(probe) * VOXEL)])
_p(float(r["max"]) <= MAX_ABS and float(r["flips"]) <= MAX_STEP_FLIPS,
"GPU == CPU at %s" % str(org), "max %.4f m, mean %.4f, step flips %.2f%%"
% [float(r["max"]), float(r["mean"]), 100.0 * float(r["flips"])])
for v in gpu:
if v > 0.0: all_solid += 1
else: all_air += 1
_p(all_solid > 0 and all_air > 0, "the blocks hold both solid and air (%d / %d)" % [all_solid, all_air])
# ---- negative control ----
var ctl_org: Vector3 = ORIGINS[1]
var ctl: PackedFloat32Array = _terrain.call("debug_gpu_density", ctl_org, VOXEL, CELLS)
if ctl.size() == (CELLS + 1) * (CELLS + 1) * (CELLS + 1):
var c := _compare(ctl, ctl_org, Vector3(VOXEL * 0.5, 0, 0)) # half a voxel SIDEWAYS: a subtle offset
_p(float(c["max"]) > MAX_ABS or float(c["flips"]) > MAX_STEP_FLIPS,
"negative control: half a voxel sideways FAILS the comparison",
"max %.4f mean %.4f flips %.1f%%" % [float(c["max"]), float(c["mean"]), 100.0 * float(c["flips"])])
print("\n%d checks — %s" % [checks, "PASS" if ok else "FAIL"])
quit(0 if ok else 1)
return true