extends Node
## CSG STRAY GATE — after SUBTRACT-only carves, is any terrain surface left INSIDE
## the removed volume?
##
## . scripts/engine.sh
## "$GODOT" --path . --rendering-driver vulkan --resolution 1280x720 res://scenes/test/csg_stray_gate.tscn
##
## The reported symptom (2026-09-29, screenshots 17-16-18 .. 17-17-32): small floating
## pieces and thin sheets left behind in carved space. With only subtracts, the true
## surface lies OUTSIDE every carved shape, so a triangle whose centroid is inside a
## shape by more than TOL is geometry that should have been removed. Counted for both
## the rendered mesh (debug_visual_faces) and the collision (debug_collision_faces).
##
## Measured four times, each must be 0:
## near camera 25 m from the carves, terrain settled
## revisit camera flown 1.5 km away (LOD merges) and back (LOD splits), settled --
## the path a player takes, and the one that serves CACHED pre-meshes
## rebuilt rebuild(): every chunk re-meshed, uploads in any order
## lod1 terrain_min_lod 1: a coarse/fine chunk boundary through the trench
## Plus the SEAM CHECK (_seam_real) at the revisit state.
##
## The strays were a COARSE chunk's overlap strip drawn inside a FINER neighbour
## (hyper-games #88: quad_emit has no ownership trim). DCTerrain.overlap_clip_mode
## cuts that strip at the boundary plane; this gate is its proof.
##
## ⛔ NOT HEADLESS (the mountain's field is GPU-only). Exit 0 = no strays, 1 = strays.
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 # the radius used in the reported session
const TOL := 1.0 # 2 LOD0 voxels (0.5 m): meshing error, not a stray
const SETTLE_FRAMES := 60
const SETTLE_CAP_S := 90.0
var _map: Node
var _cam: Camera3D
var _edits: Array = [] # {kind: "sphere"|"box", c: Vector3}
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(label: String) -> float:
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_collision_queue_size")) == 0 and int(tr.call("get_worker_queue_depth")) == 0
# overlap re-trims run under a per-frame budget: settled means none pending
if ok and tr.has_method("get_overlap_stats"):
ok = int((tr.call("get_overlap_stats") as Dictionary).get("retrim_queue", 0)) == 0
calm = calm + 1 if ok else 0
print("[STRAY] settle %s: %.1f s (%s)" % [label, _since(t0), "settled" if calm >= SETTLE_FRAMES else "CAP HIT"])
return _since(t0)
func _inside(e: Dictionary, p: Vector3) -> bool:
var d: Vector3 = p - e.c
if e.kind == "sphere":
return d.length() < R - TOL
return maxf(absf(d.x), maxf(absf(d.y), absf(d.z))) < R - TOL
## Triangles whose centroid lies inside any carved shape. Returns [count, samples].
func _strays(faces: PackedVector3Array) -> Array:
var n := 0
var samples := []
for i in range(0, faces.size() - 2, 3):
var c := (faces[i] + faces[i + 1] + faces[i + 2]) / 3.0
for e: Dictionary in _edits:
if _inside(e, c):
n += 1
if samples.size() < 4:
samples.append(c.snappedf(0.01))
break
return [n, samples]
func _measure(label: String) -> int:
var tr := _terrain()
var total := 0
# One query around the whole cluster of each site; faces are world-space.
var sites := {}
for e: Dictionary in _edits:
sites[e.site] = true
for s: Vector3 in sites.keys():
var vis: Array = _strays(tr.call("debug_visual_faces", s, 20.0))
var col: Array = _strays(tr.call("debug_collision_faces", s, 20.0))
var ci: Dictionary = tr.call("debug_chunk_info", s)
print("[STRAY] %-7s site %s lod %s visual strays %d %s collision strays %d %s"
% [label, str(s.snappedf(0.1)), str(ci.get("lod", "?")), vis[0], str(vis[1]), col[0], str(col[1])])
total += int(vis[0]) + int(col[0])
return total
## SEAM CHECK on the REAL overlap clip (hyper-games #88, A2), same LOD tree:
## DCTerrain.overlap_clip_mode re-trims every stored crossing set when changed, so
## 0 = unclipped (the reference), 1 = clip where finer (ships), 2 = clip all (RULER)
## are compared on identical leaves. Rays cross every coarse/fine chunk boundary
## within SEAM_RADIUS (2 m past each rectangle; leaves collected 40 m further). A
## HOLE = a ray whose mode-0 hit is transversal (|n.dir| >= 0.5) and that misses in
## the mode under test even 1 m longer at both ends. Pass: mode 1 has 0 strays and
## 0 holes, and mode 2 HAS holes (the check can fail).
const SEAM_LAYER := 1 << 19
const SEAM_RADIUS := 80.0
var seam_fail := 0
func _drawn_faces(tr: Node, site: Vector3, reach: float) -> PackedVector3Array:
var faces := PackedVector3Array()
var n_mesh := 0
var n_surf := 0
var n_vert := 0
var all_meshes: Array = tr.call("debug_chunk_meshes")
for dm: Dictionary in all_meshes:
var g0: Vector3 = dm["origin"]
var lv: float = dm["voxel"]
var lo: Vector3 = g0 + Vector3(2, 2, 2) * lv
if site.clamp(lo, lo + Vector3(28, 28, 28) * lv).distance_to(site) > reach:
continue
var am: ArrayMesh = dm["mesh"]
n_mesh += 1
for si in am.get_surface_count():
var arr: Array = am.surface_get_arrays(si)
n_surf += 1
if arr.size() <= Mesh.ARRAY_INDEX:
continue
var vs: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
var ix: PackedInt32Array = arr[Mesh.ARRAY_INDEX]
n_vert += vs.size()
for t in ix.size():
faces.append(vs[ix[t]] + g0)
print("[SEAM] drawn: %d chunk meshes listed, %d within reach, %d surfaces, %d verts -> %d tris" % [all_meshes.size(), n_mesh, n_surf, n_vert, faces.size() / 3])
return faces
func _seam_real() -> void:
var tr := _terrain()
if not tr.has_method("get_overlap_stats"):
print("[SEAM] no overlap clip on this build")
seam_fail += 1
return
var site := Vector3.ZERO
var spheres: Array = []
for e: Dictionary in _edits:
if e.kind == "sphere":
site = e.site
spheres.append(e.c)
var leaves := []
for dm: Dictionary in tr.call("debug_chunk_meshes"):
var lv: float = dm["voxel"]
var lo: Vector3 = (dm["origin"] as Vector3) + Vector3(2, 2, 2) * lv
var hi: Vector3 = lo + Vector3(28, 28, 28) * lv
if site.clamp(lo, hi).distance_to(site) <= SEAM_RADIUS + 40.0:
leaves.append({"lo": lo, "hi": hi, "lv": lv, "lod": int(dm["lod"])})
var bounds := []
for A: Dictionary in leaves:
for B: Dictionary in leaves:
if int(A.lod) <= int(B.lod):
continue
for ax in 3:
for side in [0, 1]:
var plane: float = (A.hi as Vector3)[ax] if side == 1 else (A.lo as Vector3)[ax]
var other: float = (B.lo as Vector3)[ax] if side == 1 else (B.hi as Vector3)[ax]
if absf(plane - other) > 0.01:
continue
var r0 := (A.lo as Vector3).max(B.lo)
var r1 := (A.hi as Vector3).min(B.hi)
var ok := true
for b in 3:
if b != ax and r1[b] - r0[b] < 0.5:
ok = false
if ok and ((r0 + r1) * 0.5).distance_to(site) <= SEAM_RADIUS:
bounds.append({"axis": ax, "at": plane, "r0": r0, "r1": r1, "lv": float(B.lv)})
var rays := []
for bd: Dictionary in bounds:
var ax: int = bd.axis
var step: float = bd.lv
for ray_ax in 3:
if ray_ax == ax:
continue
var u := 3 - ax - ray_ax
for off in [-0.5, 0.5]:
var x0: float = (bd.r0 as Vector3)[u] + step * 0.5
while x0 < (bd.r1 as Vector3)[u]:
var a := Vector3.ZERO
a[ax] = float(bd.at) + off * step
a[u] = x0
a[ray_ax] = (bd.r1 as Vector3)[ray_ax] + 2.0
var b := a
b[ray_ax] = (bd.r0 as Vector3)[ray_ax] - 2.0
rays.append([a, b])
x0 += step
print("[SEAM] %d leaves, %d coarse/fine boundaries, %d rays; stats %s" % [leaves.size(), bounds.size(), rays.size(), str(tr.call("get_overlap_stats"))])
var ref_hits := PackedByteArray()
var ref_trans := {}
var holes_by_mode := {}
for mode in [0, 1, 2]:
var t0 := Time.get_ticks_usec()
tr.set("overlap_clip_mode", mode)
var retrim_ms := (Time.get_ticks_usec() - t0) / 1000.0
await get_tree().process_frame
var faces := _drawn_faces(tr, site, SEAM_RADIUS + 40.0)
var strays := 0
for i in range(0, faces.size() - 2, 3):
var cen := (faces[i] + faces[i + 1] + faces[i + 2]) / 3.0
for c: Vector3 in spheres:
if cen.distance_to(c) < R - TOL:
strays += 1
break
var body := StaticBody3D.new()
body.collision_layer = SEAM_LAYER
body.collision_mask = 0
var cs := CollisionShape3D.new()
var shape := ConcavePolygonShape3D.new()
shape.backface_collision = true
shape.set_faces(faces)
cs.shape = shape
body.add_child(cs)
add_child(body)
await get_tree().physics_frame
await get_tree().physics_frame
var space := body.get_world_3d().direct_space_state
var hits := PackedByteArray()
hits.resize(rays.size())
var holes := 0
var hole_at := []
for k in rays.size():
var a0: Vector3 = rays[k][0]
var b0: Vector3 = rays[k][1]
var qp := PhysicsRayQueryParameters3D.create(a0, b0, SEAM_LAYER)
qp.hit_back_faces = true
var hit: Dictionary = space.intersect_ray(qp)
hits[k] = 0 if hit.is_empty() else 1
if mode == 0 and not hit.is_empty():
ref_trans[k] = absf((hit["normal"] as Vector3).dot((b0 - a0).normalized())) >= 0.5
if mode != 0 and ref_hits[k] == 1 and hits[k] == 0 and bool(ref_trans.get(k, false)):
var dir := (b0 - a0).normalized()
var qx := PhysicsRayQueryParameters3D.create(a0 - dir, b0 + dir, SEAM_LAYER)
qx.hit_back_faces = true
if space.intersect_ray(qx).is_empty():
holes += 1
if hole_at.size() < 4:
hole_at.append(str(a0.snappedf(0.1)))
if mode == 0:
ref_hits = hits
body.queue_free()
await get_tree().physics_frame
var nh := 0
for h in hits:
nh += h
holes_by_mode[mode] = holes
print("[SEAM] mode %d: tris %d strays %d ray hits %d/%d HOLES vs mode 0: %d %s (mode switch re-trim %.1f ms)"
% [mode, faces.size() / 3, strays, nh, rays.size(), holes, str(hole_at), retrim_ms])
if mode == 1 and (strays > 0 or holes > 0):
seam_fail += 1
if int(holes_by_mode.get(2, 0)) == 0:
print("[SEAM] RULER FAILED: mode 2 (clip everything) opened no holes -- the check cannot see one")
seam_fail += 1
tr.set("overlap_clip_mode", 1)
print("[SEAM] %s; stats %s" % ["PASS" if seam_fail == 0 else "FAIL", str(tr.call("get_overlap_stats"))])
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()
add_child(_map)
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("load")
# Two sites near the spawn, the reported area: a trench of overlapping hard spheres
# and one of overlapping boxes, each sunk 1 m into the surface.
var tr := _terrain()
var Probe = load(PROBE)
var plan := [["sphere", Vector3(spawn.x + 20.0, 0, spawn.z)], ["box", Vector3(spawn.x + 20.0, 0, spawn.z + 30.0)]]
for site_def in plan:
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("[STRAY] 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("aim " + site_def[0])
for k in 8:
var c := site + Vector3(1.5 * k, -1.0, 0.0)
if site_def[0] == "sphere":
mgr.call("apply_sphere", c, R, -3.0, 1) # op 1 = hard sphere, as RMB type 2
else:
mgr.call("apply_edit", c, Vector3(R, R, R), -3.0)
_edits.append({"kind": site_def[0], "c": c, "site": site})
await get_tree().create_timer(0.2).timeout # the reported cadence: ~5 carves/s
print("[STRAY] %d edits, terrain edit count %d" % [_edits.size(), int(tr.call("get_edit_count"))])
var last: Vector3 = _edits[-1].site
_cam.global_position = last + Vector3(0, 25, 25)
_cam.look_at(last)
await _settle("near")
var near := _measure("near")
_cam.global_position = last + Vector3(1500, 600, 0)
_cam.look_at(last)
await _settle("far")
_cam.global_position = last + Vector3(0, 25, 25)
_cam.look_at(last)
await _settle("revisit")
var revisit := _measure("revisit")
await _seam_real()
# REBUILD: every chunk meshed fresh, in whatever order uploads land. A coarse
# chunk that uploads BEFORE its finer neighbour is drawn must still end up
# clipped -- visual by re-trim, collision by the clip-only shape swap. This is
# where 37 collision strays survived when collision was trimmed at upload only.
tr.call("rebuild")
await _settle("rebuilt")
var rebuilt := _measure("rebuilt")
# UNIFORM LOD 1 (terrain_min_lod is a floor: LOD 2 leaves remain further out):
# the coarse/fine boundary now runs through the sphere trench at its floor.
tr.call("set_terrain_min_lod", 1)
tr.call("rebuild")
await _settle("uniform lod1")
var uniform1 := _measure("lod1")
tr.call("set_terrain_min_lod", 0)
var total := near + revisit + rebuilt + uniform1
print("[STRAY] near %d, revisit %d, rebuilt %d, uniform lod1 %d stray triangles; seam %s"
% [near, revisit, rebuilt, uniform1, "PASS" if seam_fail == 0 else "FAIL"])
print("RESULT: %s" % ("PASS" if total == 0 and seam_fail == 0 else "FAIL"))
get_tree().quit(0 if total == 0 and seam_fail == 0 else 1)