OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / tools / bench_dc_undersea.gd

125 lines · gd ref:
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