extends Node
## GATE: the mesh-time material kernel (#90, Settings > Terrain Material "Vertex")
## writes only FINITE values, for every brush. ashatron's log (2026-10-01) showed
## "[GPU Mesh] material kernel wrote 6/9/15 non-finite values (zeroed)" during box
## carves; dc-terrain zeroes them on readback, so the bake silently loses the
## material at those vertices.
##
## "$G" --path . --rendering-driver vulkan --resolution 1280x720 res://scenes/test/gate_kernel_finite.tscn
##
## Mountain with the kernel forced ON (mountain_terrain_material = 2), then at three
## sites near the spawn: 8 stepped carves + 8 on ONE spot (the "spam" case) with the
## hard sphere, the smooth sphere and the box. Fails if DCTerrain.
## get_kernel_nonfinite_stats().values grew; prints where it grew and on which kind
## of vertex (zero qef normal / non-finite position).
## ⛔ NOT HEADLESS (the mountain's field is GPU-only).
const MAP := "res://scenes/maps/map_mountain.tscn"
const MAP_ID := "map_mountain"
const PROBE := "res://scenes/test/terrain_probe.gd"
const R := 3.0
const SETTLE_FRAMES := 60
const SETTLE_CAP_S := 90.0
const REPLAY := "res://scenes/test/data/ashatron_edits_2026-10-01.txt"
var _map: Node
var _cam: Camera3D
func _ready() -> void:
_run.call_deferred()
func _since(t0: int) -> float:
return (Time.get_ticks_msec() - t0) / 1000.0
func _terrain() -> Node:
return _map.get_node_or_null(^"MountainTerrain")
func _settle() -> void:
var tr := _terrain()
var t0 := Time.get_ticks_msec()
var calm := 0
while calm < SETTLE_FRAMES and _since(t0) < SETTLE_CAP_S:
await get_tree().process_frame
var ok: bool = bool(tr.call("is_lod_settled")) and int(tr.call("get_pending_count")) == 0 \
and int(tr.call("get_worker_queue_depth")) == 0
calm = calm + 1 if ok else 0
func _stats() -> Dictionary:
return _terrain().call("get_kernel_nonfinite_stats")
func _fmt(d: Dictionary) -> String:
return "values %d, verts %d (zero normal %d, bad position %d), chunks %d" % [
d["values"], d["verts"], d["verts_zero_n"], d["verts_bad_p"], d["chunks"]]
func _run() -> void:
if DisplayServer.get_name() == "headless":
print("RESULT: SKIP (needs a display)")
get_tree().quit(2)
return
GameState.enter_map(MAP_ID)
var mgr := TerrainEditManager.new()
mgr.name = "TerrainEditManager"
add_child(mgr)
_cam = Camera3D.new()
_cam.far = 20000.0
add_child(_cam)
_cam.current = true
_map = (load(MAP) as PackedScene).instantiate()
_map.set("mountain_terrain_material", 2) # Vertex: the kernel runs
add_child(_map)
var tr := _terrain()
if tr == null or not tr.has_method("get_kernel_nonfinite_stats"):
print("[GATE-KF] FAIL: no MountainTerrain, or a dc-terrain without get_kernel_nonfinite_stats")
get_tree().quit(1)
return
var spawn := Vector3(0, 250, 0)
var sp := get_tree().get_nodes_in_group("spawn_points")
if not sp.is_empty():
spawn = (sp[0] as Node3D).global_position
_cam.global_position = spawn + Vector3(0, 2, 0)
_cam.look_at(spawn + Vector3(20, 0, 0))
await _settle()
if String(tr.call("get_material_kernel")).is_empty():
print("[GATE-KF] FAIL: the kernel is not set (mesh bake off?)")
get_tree().quit(1)
return
var base := _stats()
print("[GATE-KF] after load: %s" % _fmt(base))
var Probe = load(PROBE)
var fails := 0
var plan := [["hard sphere", Vector3(spawn.x + 20.0, 0, spawn.z)],
["smooth sphere", Vector3(spawn.x + 20.0, 0, spawn.z + 30.0)],
["box", Vector3(spawn.x + 20.0, 0, spawn.z + 60.0)]]
for site_def in plan:
var kind: String = site_def[0]
var col: Vector3 = site_def[1]
var s = _map.call("_find_surface_gpu", Probe, tr, Vector3(col.x, 1500.0, col.z), 64.0)
if s == null:
print("[GATE-KF] no surface at %s" % str(col)); get_tree().quit(3); return
var f = _map.call("_find_surface_gpu", Probe, tr, Vector3(col.x, float(s), col.z), 1.0)
var site := Vector3(col.x, float(f if f != null else s), col.z)
_cam.global_position = site + Vector3(0, 25, 25)
_cam.look_at(site)
await _settle()
var before := _stats()
for k in 16:
var c := site + Vector3(1.5 * mini(k, 7), -1.0, 0.0) # 8 stepped, then 8 on the last spot
match kind:
"hard sphere": mgr.call("apply_sphere", c, R, -3.0, 1)
"smooth sphere": mgr.call("apply_sphere", c, R, -3.0, 0)
_: mgr.call("apply_edit", c, Vector3(R, R, R), -3.0)
await get_tree().create_timer(0.2).timeout
await _settle()
var after := _stats()
var grew := int(after["values"]) - int(before["values"])
print("[GATE-KF] %s %s: +%d non-finite values (+%d verts: zero normal +%d, bad position +%d)" % [
"PASS" if grew == 0 else "FAIL", kind, grew,
int(after["verts"]) - int(before["verts"]),
int(after["verts_zero_n"]) - int(before["verts_zero_n"]),
int(after["verts_bad_p"]) - int(before["verts_bad_p"])])
if grew != 0:
fails += 1
# THE ASHATRON SESSION, edit for edit (data/ashatron_edits_2026-10-01.txt: 600
# carves from its log). The camera follows each carve, as the player did.
var rep := await _replay(mgr)
if rep > 0:
fails += 1
var end := _stats()
print("[GATE-KF] total: %s" % _fmt(end))
if int(base["values"]) != 0:
print("[GATE-KF] FAIL: non-finite values before any carve")
fails += 1
print("RESULT: %s" % ("PASS" if fails == 0 else "FAIL"))
get_tree().quit(0 if fails == 0 else 1)
## Replays REPLAY; returns how many non-finite values it produced. Growth is
## attributed to the kind of the edit applied just before it was seen.
func _replay(mgr: Node) -> int:
var f := FileAccess.open(REPLAY, FileAccess.READ)
var edits: Array = []
while f != null and not f.eof_reached():
var l := f.get_line().strip_edges()
if l.is_empty() or l.begins_with("#"):
continue
var t := l.split(" ")
edits.append([t[0], Vector3(float(t[1]), float(t[2]), float(t[3]))])
var t0 := _stats()
var by_kind := {}
var first := -1
var prev := int(t0["values"])
for i in edits.size():
var kind: String = edits[i][0]
var c: Vector3 = edits[i][1]
_cam.global_position = c + Vector3(0, 12, 12)
_cam.look_at(c)
match kind:
"hard": mgr.call("apply_sphere", c, R, -3.0, 1)
"smooth": mgr.call("apply_sphere", c, R, -3.0, 0)
_: mgr.call("apply_edit", c, Vector3(R, R, R), -3.0)
for k in 4:
await get_tree().process_frame
var v := int(_stats()["values"])
if v > prev:
by_kind[kind] = int(by_kind.get(kind, 0)) + (v - prev)
if first < 0:
first = i
print("[GATE-KF] replay: first non-finite after edit %d (%s at %s)" % [i, kind, str(c)])
prev = v
await _settle()
var t1 := _stats()
var grew := int(t1["values"]) - int(t0["values"])
print("[GATE-KF] %s ashatron replay (%d edits): +%d non-finite values (+%d verts: zero normal +%d, bad position +%d), by kind %s" % [
"PASS" if grew == 0 else "FAIL", edits.size(), grew, int(t1["verts"]) - int(t0["verts"]),
int(t1["verts_zero_n"]) - int(t0["verts_zero_n"]), int(t1["verts_bad_p"]) - int(t0["verts_bad_p"]), str(by_kind)])
return grew