extends SceneTree
## dc-terrain meshing benchmark on the undersea map (map_geiss.tscn, the field from
## blender/undersea_geiss.tscn). Was the dc-terrain half of the A/B against the NVIDIA
## raymarcher (removed 2026-10-04); it measures DC alone now. DC pays per chunk to
## build and almost nothing per frame, so it reports:
##
## 1 BUILD wall time to reach steady state, and per chunk
## 2 GEOMETRY chunks, triangles, and the memory they occupy
## 3 FRAME GPU time with the terrain visible vs hidden
## 4 STREAMING left to a follow-up; needs camera motion
##
## The frame measurement SELF-TESTS: if hiding the terrain does not change the
## GPU time, the timer is not measuring the terrain and the number is discarded
## rather than reported. That is exactly how the windowed march bench was found
## to be reporting 0.13 ms for a 138 ms shader.
##
## DISPLAY=:0 "$HYPER_TPS_GODOT" --path . --rendering-driver vulkan \
## --script res://tools/bench_dc_undersea.gd
const MAP := "res://scenes/maps/map_geiss.tscn"
var _map: Node
var _terrain: Node
var _t0 := 0
var _frames := 0
var _settled_at := -1
var _phase := 0
var _samples: Array[float] = []
var _visible_ms := 0.0
func _initialize() -> void:
var ps: PackedScene = load(MAP)
if ps == null:
print("FAIL — cannot load %s" % MAP); quit(1); return
_map = ps.instantiate()
root.add_child(_map)
_terrain = _map.get_node_or_null("DCTerrain")
if _terrain == null:
print("FAIL — no DCTerrain in the map"); quit(1); return
# LOD expands around the active camera. Without one the octree only refines
# at the origin and 37 chunks is not a world, it is a pinprick.
var cam := Camera3D.new()
cam.position = Vector3(0.0, 6.0, 16.0)
cam.rotation_degrees = Vector3(-10.0, 0.0, 0.0)
cam.far = 300000.0
cam.current = true
root.add_child(cam)
_t0 = Time.get_ticks_msec()
print("DC BENCH: %s" % MAP)
## dc-terrain attaches chunk meshes through RenderingServer.instance_create(),
## NOT as scene nodes, so walking the tree finds nothing. Godot's own render
## monitors see them, and they report what is actually DRAWN, which is the right
## quantity to set against a per-pixel marcher anyway.
func _mesh_stats() -> Dictionary:
return {
"prims": int(Performance.get_monitor(Performance.RENDER_TOTAL_PRIMITIVES_IN_FRAME)),
"objects": int(Performance.get_monitor(Performance.RENDER_TOTAL_OBJECTS_IN_FRAME)),
"draws": int(Performance.get_monitor(Performance.RENDER_TOTAL_DRAW_CALLS_IN_FRAME)),
"buf_mb": Performance.get_monitor(Performance.RENDER_BUFFER_MEM_USED) / 1048576.0,
"tex_mb": Performance.get_monitor(Performance.RENDER_TEXTURE_MEM_USED) / 1048576.0,
}
func _process(dt: float) -> bool:
_frames += 1
if _frames == 2:
RenderingServer.viewport_set_measure_render_time(root.get_viewport_rid(), true)
# ---- 1 BUILD: wait for the mesher to stop having work ----
if _settled_at < 0:
if _frames < 30: return false
var pending: int = _terrain.call("get_pending_count")
var active: int = _terrain.call("get_active_chunk_count")
if _frames % 60 == 0:
print(" building… active=%d pending=%d t=%.1fs"
% [active, pending, (Time.get_ticks_msec() - _t0) / 1000.0])
if pending > 0 or active == 0:
if _frames > 3600:
print("FAIL — never settled (pending=%d)" % pending); quit(1); return true
return false
_settled_at = Time.get_ticks_msec()
var build_ms := float(_settled_at - _t0)
var st := _mesh_stats()
print("")
print(" 1 BUILD %.0f ms to steady state, %d chunks -> %.2f ms/chunk"
% [build_ms, active, build_ms / maxf(float(active), 1.0)])
print(" 2 GEOMETRY %d chunks, %s primitives drawn, %d objects, %d draw calls"
% [active, _commas(st["prims"]), st["objects"], st["draws"]])
print(" %.1f MB buffers + %.1f MB textures"
% [st["buf_mb"], st["tex_mb"]])
return false
# ---- 3 FRAME: visible for 90 frames, then hidden for 90 ----
var vp := root.get_viewport_rid()
var gpu: float = maxf(RenderingServer.viewport_get_measured_render_time_gpu(vp), dt * 1000.0)
if _phase == 0:
if _frames > _settled_frames() + 20: _samples.append(gpu)
if _samples.size() >= 90:
_samples.sort()
_visible_ms = _samples[_samples.size() / 2]
_samples.clear()
_terrain.set("visible", false)
_phase = 1
return false
if _frames < _settled_frames() + 220:
if _frames > _settled_frames() + 140: _samples.append(gpu)
return false
_samples.sort()
var hidden_ms: float = _samples[_samples.size() / 2] if not _samples.is_empty() else 0.0
print(" 3 FRAME terrain visible %.2f ms GPU, hidden %.2f ms -> terrain costs %.2f ms"
% [_visible_ms, hidden_ms, _visible_ms - hidden_ms])
if absf(_visible_ms - hidden_ms) < 0.05:
print(" INSTRUMENT SUSPECT — hiding the terrain changed the GPU time by less"
+ " than 0.05 ms. The timer is not measuring the terrain; discard the frame number.")
quit(0); return true
func _settled_frames() -> int:
return 30
static func _commas(n: int) -> String:
var s := str(n); var out := ""
for i in range(s.length()):
if i > 0 and (s.length() - i) % 3 == 0: out += ","
out += s[i]
return out