extends SceneTree
## GATE — the terrain refines from the PLAYER'S point of view, not one world-space
## octree quadrant at a time (user 2026-10-06: "only half the mountain coalescing to its
## highest lod ... as if half the mountain is waiting for the other half").
##
## map_mountain from its first spawn point, camera looking at the massif. Every 0.5 s
## the leaf LOD is sampled on a 3D grid over the mountain (debug_chunk_info); once the
## tree settles each sample's FINAL LOD is known, and every earlier sample's EXCESS
## (lod - final) says how much coarser than wanted it still was, per quadrant (sign of
## x, sign of z). The spawn stands ON the z = 0 plane, so z- and z+ at the same x are
## MIRROR IMAGES of each other from the player's point of view and must fall TOGETHER:
## the gap between mirrored quadrants must stay under MAX_SPREAD levels. (The x+ side,
## the camera's, legitimately needs more levels than the far x- side — comparing ALL
## quadrants would fail a correct, view-ordered refinement.)
##
## Measured before the fix (2026-10-06): quadrants finished one after another at
## t = 2, 4, 6, 8 s; mirrored gap 6.24 levels, and the camera's OWN quadrant was last.
## Pre-mesh dispatch was ordered by distance to the waiting node's CENTRE, so a huge
## coarse cell (centre far away even with the camera inside it) lost to every finer,
## nearer request in the half that started first.
##
## ⚠ WINDOWED. Unset CI or the window is hidden and presents at ~1 Hz:
## env -u CI HYPERTPS_DAEMON_TCP=127.0.0.1:1 DISPLAY=:0 "$HYPER_TPS_GODOT" --path . \
## --script res://scenes/test/gate_lod_refine_order.gd
const MAP := "res://scenes/maps/map_mountain.tscn"
const HALF := 3000.0
const STEP := 300.0
const YS := [100.0, 400.0, 800.0, 1200.0, 1800.0]
const MAX_SPREAD := 1.5
const MAX_SECS := 60.0
const MIN_FPS := 15
const ProbeWindow := preload("res://scenes/test/probe_window.gd")
var _ws := ""
var _t: Node
var _pts: PackedVector3Array = []
var _hist: Array = []
var _last := 0
var _start := 0
var _fps_min := 1 << 30
var _fps_min_t := 0.0
var _last_frames := 0
func _initialize() -> void:
_ws = ProbeWindow.free_workspace()
var m: Node = (load(MAP) as PackedScene).instantiate()
root.add_child(m)
var spawn := Vector3.ZERO
for n in m.get_children():
if n.is_in_group("spawn_points"):
spawn = (n as Node3D).position
break
var cam := Camera3D.new()
cam.far = 20000.0
root.add_child(cam)
cam.look_at_from_position(spawn + Vector3(0, 1.7, 0), Vector3(0, spawn.y + 300.0, 0))
cam.current = true
var x := -HALF
while x <= HALF + 0.1:
var z := -HALF
while z <= HALF + 0.1:
for y: float in YS:
_pts.append(Vector3(x, y, z))
z += STEP
x += STEP
_start = Time.get_ticks_msec()
func _process(_d: float) -> bool:
if not ProbeWindow.drawing(root):
ProbeWindow.move_to(_ws, Vector2i(1600, 900))
if _t == null:
var f := root.find_children("*", "DCTerrain", true, false)
if f.is_empty():
return false
_t = f[0]
var now := Time.get_ticks_msec()
if now - _last < 500:
return false
# MAIN-LOOP rate, not drawn fps: LOD streaming runs on the main loop. A window
# that briefly cannot draw (moved / minimized) reads 1 drawn fps while the loop
# runs at full speed; a window hidden behind a fullscreen one throttles the LOOP
# itself to ~1 Hz (NVIDIA present), and that is what makes a run prove nothing.
var frames := Engine.get_process_frames()
# The first sample has no previous one: _last is 0 and the map's _ready may have
# blocked for seconds before the first frame (map_mountain: ~8 s) — not a stall
# DURING streaming, so it is not counted.
var fps := int((frames - _last_frames) * 1000.0 / maxi(now - _last, 1)) if _last > 0 else 1 << 30
_last_frames = frames
_last = now
if now - _start > 3000:
if fps < _fps_min:
_fps_min = fps
_fps_min_t = (now - _start) / 1000.0
var row := PackedInt32Array()
row.resize(_pts.size())
for i in _pts.size():
row[i] = int((_t.debug_chunk_info(_pts[i]) as Dictionary).get("lod", 99))
_hist.append(row)
var el := (now - _start) / 1000.0
if el < 5.0 or (not _t.is_lod_settled() and el < MAX_SECS):
return false
_report(el)
return true
func _report(el: float) -> void:
var fin: PackedInt32Array = _hist[_hist.size() - 1]
var worst := 0.0
var worst_t := 0.0
var worst_line := ""
for s in _hist.size():
var row: PackedInt32Array = _hist[s]
var sum := [0, 0, 0, 0]
var cnt := [0, 0, 0, 0]
for i in _pts.size():
var q := (2 if _pts[i].x >= 0 else 0) + (1 if _pts[i].z >= 0 else 0)
sum[q] += row[i] - fin[i]
cnt[q] += 1
var ex: Array = []
for q in 4:
ex.append(float(sum[q]) / maxi(cnt[q], 1))
var spread: float = maxf(absf(ex[0] - ex[1]), absf(ex[2] - ex[3])) # x-: z-/z+, x+: z-/z+
if spread > worst:
worst = spread
worst_t = s * 0.5
worst_line = "x-z- %.2f x-z+ %.2f x+z- %.2f x+z+ %.2f" % ex
var settled: bool = _t.is_lod_settled()
print(" settled=%s after %.1f s, %d chunks, min fps %d at t=%.1f s" % [settled, el, _t.get_chunk_count(), _fps_min, _fps_min_t])
print(" worst mirrored-quadrant gap %.2f levels at t=%.1f s (mean excess: %s)" % [worst, worst_t, worst_line])
var ok := settled and worst <= MAX_SPREAD and _fps_min >= MIN_FPS
if not settled:
print("FAIL never settled in %.0f s — final LODs unknown, the spread proves nothing" % MAX_SECS)
if _fps_min < MIN_FPS:
print("FAIL min fps %d < %d — window hidden? proves nothing" % [_fps_min, MIN_FPS])
if worst > MAX_SPREAD:
print("FAIL one half refined %.2f levels ahead of its mirror image (max %.1f)" % [worst, MAX_SPREAD])
print("PASS" if ok else "FAIL")
quit(0 if ok else 1)