extends Node3D
## Frame-time benchmark along a FIXED camera path, on a guaranteed-visible window
## (hyper-games #108 perf). Same shell as _depth_shot: root named "Game", the shared
## Sky3DExtended borrowed from game.tscn, the map instanced under it.
##
## The camera IS the terrain's viewer: DCTerrain streams LOD and builds collision
## around viewport->get_camera_3d() (dc_terrain.cpp:655, :1379), so flying a camera
## exercises the same terrain work a player does. Player physics / IK / animation
## are NOT in this frame.
##
## vsync is OFF so a frame's time is its work, not the 60 Hz wait
## (Performance.TIME_PROCESS includes that wait).
##
## BENCH_MAP=map_dc_erosion
## BENCH_PATH='x,y,z;x,y,z;...' waypoints, flown in order at BENCH_SPEED m/s
## BENCH_SPEED=40 BENCH_SETTLE=25 (s held at the first waypoint before timing)
## BENCH_PITCH=-10 (deg; the camera faces the horizontal direction of travel)
## BENCH_OUT=/path/prefix -> prefix.csv (one row per timed frame)
## BENCH_SIZE=1920x1080 (window size measured at; native on tabertron)
## BENCH_SHADER_PARAMS='{"density_normal_blend":0.0}' (terrain material A/B)
## BENCH_SUBVIEWPORT=full|area (#124 B0) render the 3D through a SubViewport the way
## Godot's editor does (SubViewportContainer, stretch, input off, the root's World3D
## inherited): `full` = the whole window, `area` = Blender's centre area (75% x 94%).
## The bench camera STAYS in the root (DCTerrain streams around the root's camera,
## and 23 game lookups read it); a follower camera in the SubViewport copies it each
## frame; the root renders no 3D (disable_3d). The root's scaling / AA settings are
## copied over (MSAA / SSAA / TAA / debanding are root-only at startup).
## BENCH_SHOT=/path.png the rendered 3D image at timing start (clouds, DLSS: by eye)
## "$GODOT" --path . --resolution 1920x1080 res://scenes/test/bench_path.tscn
const ProbeWindow := preload("res://scenes/test/probe_window.gd")
var _cam: Camera3D
var _terrain: Node = null
var _path: Array[Vector3] = []
var _speed := 40.0
var _settle := 25.0
var _pitch := -10.0
var _out := "/tmp/bench_path"
var _ws := ""
## Window size the frame is measured at. The target is native 1920x1080 (all three
## of tabertron's displays); per-pixel costs scale with it.
var _size := Vector2i(1920, 1080)
var _fullscreen := false
var _t := 0.0
var _seg := 0
var _seg_t := 0.0
var _yaw := 0.0
var _timing := false
var _last_us := 0
var _rows: Array[PackedFloat64Array] = []
var _hidden := 0
func _v3(s: String) -> Vector3:
var p := s.split(",")
return Vector3(float(p[0]), float(p[1]), float(p[2]))
func _env(k: String, d: String) -> String:
var v := OS.get_environment(k)
return v if v != "" else d
func _ready() -> void:
# ';' or '|' between waypoints ('|' survives tools whose env syntax is "A=1; B=2;").
for w in _env("BENCH_PATH", "0,450,10000;0,450,0").replace("|", ";").split(";"):
_path.append(_v3(w))
_speed = float(_env("BENCH_SPEED", "40"))
_settle = float(_env("BENCH_SETTLE", "25"))
_pitch = float(_env("BENCH_PITCH", "-10"))
_out = _env("BENCH_OUT", _out)
var sz := _env("BENCH_SIZE", "1920x1080").split("x")
_size = Vector2i(int(sz[0]), int(sz[1]))
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
var game: Node = (load("res://scenes/game/game.tscn") as PackedScene).instantiate()
var sky: Node = game.get_node("Sky3DExtended")
game.remove_child(sky)
game.free()
add_child(sky)
var tod: Node = sky.get_node_or_null("TimeOfDay")
if tod != null:
tod.current_time = 11.0
if tod.has_method("_update_celestial_coords"):
tod._update_celestial_coords()
_cam = Camera3D.new()
_cam.far = 60000.0
_cam.near = 0.5
add_child(_cam)
_cam.current = true
_yaw = _travel_yaw(0)
_place(_path[0])
var map: Node = (load("res://scenes/maps/%s.tscn" % _env("BENCH_MAP", "map_mountain")) as PackedScene).instantiate()
add_child(map)
var found := map.find_children("*", "DCTerrain", true, false)
_terrain = found[0] if not found.is_empty() else null
# BENCH_FIND_COAST=x,z: walk +x in 1 km steps (200 km max) with the map's own
# _surface_y and print where the seabed first rises above WATER_Y -- a view with a
# coastline in it, so a fetch A/B is not trivially "all open water".
if _env("BENCH_FIND_COAST", "") != "" and map.has_method("_surface_y"):
var s := _env("BENCH_FIND_COAST", "0,0").split(",")
var x0 := float(s[0])
var z0 := float(s[1])
var wy: float = float(map.get("WATER_Y"))
for dir: Vector2 in [Vector2(1, 0), Vector2(-1, 0), Vector2(0, 1), Vector2(0, -1)]:
var hit := false
for k in 200:
var x := x0 + 1000.0 * k * dir.x
var z := z0 + 1000.0 * k * dir.y
var h: float = map.call("_surface_y", _terrain, x, z)
if h > wy:
print("[bench-path] coast: surface %.0f m > water %.0f at x=%.0f z=%.0f (%d km toward %s)" % [h, wy, x, z, k, dir])
hit = true
break
if not hit:
print("[bench-path] coast: none within 200 km toward %s" % dir)
# A/B for the submerged far-plane clamp (map_dc_erosion._on_submerged_changed):
# cut every listener so the baseline renders exactly as before it existed.
if _env("BENCH_NO_SUBMERGED_CULL", "") == "1":
# Not find_children(type): its type test is is_class(), blind to class_name.
for o in map.find_children("*", "", true, false):
if o is Ocean:
for c in o.get_signal_connection_list(&"submerged_changed"):
o.disconnect(&"submerged_changed", c["callable"])
print("[bench-path] submerged cull DISCONNECTED (baseline)")
RenderingServer.viewport_set_measure_render_time(get_viewport().get_viewport_rid(), true)
if _env("BENCH_SUBVIEWPORT", "") != "":
_sub_setup(_env("BENCH_SUBVIEWPORT", "full"))
# 3D render scale (the game's render_scale row). A cost that does NOT fall with it
# is geometry, not pixels.
if _env("BENCH_SCALE", "") != "":
get_viewport().scaling_3d_scale = float(_env("BENCH_SCALE", "1.0"))
# LOD distance factor, set AFTER the terrain's _ready: DCTerrain::_ready overwrites
# lod_factor with 2.0 (dc_terrain.cpp:346), so it is applied one frame later.
if _env("BENCH_LOD_FACTOR", "") != "" and _terrain != null:
_terrain.set_deferred("lod_factor", float(_env("BENCH_LOD_FACTOR", "2.0")))
print("[bench-path] map %s, %d waypoints, %.0f m/s, settle %.0f s, terrain %s"
% [_env("BENCH_MAP", "map_mountain"), _path.size(), _speed, _settle, _terrain != null])
var _sub: SubViewport = null
var _sub_cam: Camera3D = null
## #124 B0: the 3D view as Godot's editor builds it (node_3d_editor_viewport.cpp:6822-6844).
func _sub_setup(mode: String) -> void:
var layer := CanvasLayer.new()
add_child(layer)
var c := SubViewportContainer.new()
c.stretch = true
layer.add_child(c)
c.set_anchors_and_offsets_preset(Control.PRESET_FULL_RECT)
if mode == "area":
c.anchor_right = 0.75
c.anchor_bottom = 0.94
c.offset_right = 0.0
c.offset_bottom = 0.0
_sub = SubViewport.new()
_sub.gui_disable_input = true
c.add_child(_sub)
_sub_cam = Camera3D.new()
_sub_cam.far = _cam.far
_sub_cam.near = _cam.near
_sub.add_child(_sub_cam)
_sub_cam.current = true
_sub_cam.global_transform = _cam.global_transform # placed before the settle, as _cam is
_sub_cam.fov = _cam.fov
var root := get_viewport()
for p: String in ["scaling_3d_mode", "scaling_3d_scale", "fsr_sharpness", "msaa_3d", "screen_space_aa",
"use_taa", "use_debanding", "mesh_lod_threshold", "use_hdr_2d"]:
_sub.set(p, root.get(p))
root.disable_3d = true
RenderingServer.viewport_set_measure_render_time(_sub.get_viewport_rid(), true)
print("[bench-path] SUBVIEWPORT %s: scaling_3d_mode %d scale %.2f msaa %d taa %s; root disable_3d; camera %s (root) -> follower"
% [mode, _sub.scaling_3d_mode, _sub.scaling_3d_scale, _sub.msaa_3d, _sub.use_taa, root.get_camera_3d()])
## BENCH_SUBVIEWPORT=toggle (#124 B0): the GPU here throttles thermally (87-88 C, clocks
## falling run to run), so separate runs cannot compare a ~10 ms difference. One run
## alternates root rendering and SubViewport rendering every TOGGLE_S; each window's
## neighbours share its temperature, clock and view. The first TOGGLE_SKIP_S after a
## switch is not counted (the frame in flight belongs to the other state).
const TOGGLE_S := 5.0
const TOGGLE_SKIP_S := 0.5
var _tog_sub := false
var _tog_t := 0.0
## Plain Arrays: a PackedFloat64Array is a VALUE type, so appending to one read out of a
## Dictionary appends to a copy (the first toggle run recorded nothing).
var _tog: Dictionary = {"root": [], "sub": []}
func _toggle_apply() -> void:
get_viewport().disable_3d = _tog_sub
_sub.render_target_update_mode = SubViewport.UPDATE_ALWAYS if _tog_sub else SubViewport.UPDATE_DISABLED
(_sub.get_parent() as Control).visible = _tog_sub
func _toggle_step(gpu: float) -> void:
if _tog_t == 0.0:
_toggle_apply()
_tog_t += get_process_delta_time()
if _tog_t >= TOGGLE_SKIP_S:
(_tog["sub" if _tog_sub else "root"] as Array).append(gpu)
if _tog_t >= TOGGLE_S:
_tog_sub = not _tog_sub
_tog_t = 0.0001
_toggle_apply()
func _toggle_report() -> void:
for k: String in ["root", "sub"]:
var v := PackedFloat64Array(_tog[k] as Array)
var sum := 0.0
for x in v:
sum += x
print("[bench-path] TOGGLE %-4s gpu ms n %5d mean %6.2f p50 %6.2f p95 %6.2f"
% [k, v.size(), sum / maxf(v.size(), 1), _pct(v, 0.5), _pct(v, 0.95)])
## Horizontal heading of segment i; a vertical segment keeps the previous heading.
func _travel_yaw(i: int) -> float:
if i + 1 >= _path.size():
return _yaw
var d := _path[i + 1] - _path[i]
if Vector2(d.x, d.z).length() < 0.1:
return _yaw
return atan2(-d.x, -d.z)
func _place(p: Vector3) -> void:
_cam.global_position = p
_cam.rotation = Vector3(deg_to_rad(_pitch), _yaw, 0.0)
if _sub_cam != null:
_sub_cam.global_transform = _cam.global_transform
_sub_cam.fov = _cam.fov
func _process(dt: float) -> void:
_t += dt
if _ws == "":
_ws = ProbeWindow.free_workspace()
if not ProbeWindow.drawing(get_tree().root):
ProbeWindow.move_to(_ws, _size)
elif not _fullscreen and _ws != "":
# Floating at 1920x1080 still loses the i3 border and bar (measured 1914x1024).
# Fullscreen on that output gives the native 1920x1080 the target is set at.
_fullscreen = true
OS.execute("i3-msg", ['[id="%d"] fullscreen enable'
% DisplayServer.window_get_native_handle(DisplayServer.WINDOW_HANDLE)], [])
if not _timing:
if _t < _settle:
return
_timing = true
# BISECTING A FIXED COST (the 6 ms depth pre-pass that ignored pixels, draws and
# triangles): take the terrain out of the frame, or keep its geometry and give it
# a trivial material.
# NOT terrain.visible = false: DCTerrain draws through its own RenderingServer
# instances and never handles NOTIFICATION_VISIBILITY_CHANGED, so that hides
# nothing. Culling every layer at the camera draws no geometry at all; a cost
# that survives it is per-pass overhead, not anything drawn.
# BENCH_FILTER="lod_min,lod_max,seams,internal" -> DCTerrain.debug_filter_visible
# (show only leaves in a LOD range; seams on/off; hide parents still holding an
# instance unless internal=1). One-shot: use on a SETTLED tree (BENCH_LOD_FACTOR=1).
if _env("BENCH_FILTER", "") != "" and _terrain != null:
var f := _env("BENCH_FILTER", "").split(",")
var r: Dictionary = _terrain.call("debug_filter_visible", int(f[0]), int(f[1]),
f[2] == "1", f[3] == "1")
print("[bench-path] filter lod %s..%s seams=%s internal=%s -> %s" % [f[0], f[1], f[2], f[3], str(r)])
# BENCH_HIDE="relative/node/path,..." under the map: hide ordinary scene nodes.
if _env("BENCH_HIDE", "") != "":
var mapn: Node = _terrain.get_parent() if _terrain != null else null
for p in _env("BENCH_HIDE", "").split(","):
var hn: Node = mapn.get_node_or_null(p) if mapn != null else null
if hn is Node3D:
(hn as Node3D).visible = false
print("[bench-path] hide %s -> %s" % [p, "hidden" if hn is Node3D else "NOT FOUND"])
# #124 B0 bisect: the world's compositor (Nubis clouds, cloud_bank.gd) off.
if _env("BENCH_NO_COMPOSITOR", "") == "1":
for we in get_tree().root.find_children("*", "WorldEnvironment", true, false):
(we as WorldEnvironment).compositor = null
print("[bench-path] every WorldEnvironment.compositor -> null")
if _env("BENCH_CULL_ALL", "") == "1":
_cam.cull_mask = 0
print("[bench-path] camera cull_mask 0 (no geometry)")
if _env("BENCH_FLAT_TERRAIN_SHADER", "") == "1" and _terrain != null:
var flat := Shader.new()
flat.code = "shader_type spatial;\nvoid fragment() { ALBEDO = vec3(0.5); }\n"
var fm := ShaderMaterial.new()
fm.shader = flat
_terrain.set("custom_material", fm)
print("[bench-path] terrain material -> FLAT shader")
# A/B knobs on the OCEAN's material (sheet only; the horizon plate is flat).
var op := OS.get_environment("BENCH_OCEAN_PARAMS")
if op != "" and _terrain != null:
for o in _terrain.get_parent().find_children("*", "", true, false):
if o is Ocean and (o as GeometryInstance3D).material_override is ShaderMaterial:
var od: Dictionary = JSON.parse_string(op)
for k in od:
((o as GeometryInstance3D).material_override as ShaderMaterial).set_shader_parameter(String(k), od[k])
print("[bench-path] ocean shader params %s" % op)
# A/B knobs on the terrain material, applied once the map has set its own.
var sp := OS.get_environment("BENCH_SHADER_PARAMS")
if sp != "" and _terrain != null:
var mat: Variant = _terrain.get("custom_material")
if mat is ShaderMaterial:
var d: Dictionary = JSON.parse_string(sp)
for k in d:
(mat as ShaderMaterial).set_shader_parameter(String(k), d[k])
print("[bench-path] terrain shader params %s" % sp)
else:
print("[bench-path] ⚠ BENCH_SHADER_PARAMS set but the terrain has no ShaderMaterial")
_last_us = Time.get_ticks_usec()
print("[bench-path] timing starts at t=%.1f s, viewport %s, 3D %s" % [_t, get_viewport().get_visible_rect().size,
_sub.size if _sub != null else get_viewport().get_visible_rect().size])
if _env("BENCH_SHOT", "") != "":
var vp: Viewport = _sub if _sub != null else get_viewport()
vp.get_texture().get_image().save_png(_env("BENCH_SHOT", ""))
print("[bench-path] shot -> %s" % _env("BENCH_SHOT", ""))
return
var now := Time.get_ticks_usec()
var frame_ms := (now - _last_us) / 1000.0
_last_us = now
if not ProbeWindow.drawing(get_tree().root):
_hidden += 1
var gpu := RenderingServer.viewport_get_measured_render_time_gpu(get_viewport().get_viewport_rid())
if _sub != null: # the 3D is the SubViewport's; the root draws 2D only
gpu += RenderingServer.viewport_get_measured_render_time_gpu(_sub.get_viewport_rid())
if _env("BENCH_SUBVIEWPORT", "") == "toggle":
_toggle_step(gpu)
var s: Dictionary = _terrain.get_frame_stats() if _terrain != null else {}
_rows.append(PackedFloat64Array([
_t, _cam.global_position.y, frame_ms, gpu,
float(s.get("total_ms", 0.0)), float(s.get("traverse_ms", 0.0)),
float(s.get("upload_ms", 0.0)), float(s.get("collision_ms", 0.0)),
float(s.get("dispatched", 0)), float(s.get("uploaded", 0))]))
# Advance along the path by distance, not by frame: a slow frame moves further.
var step := _speed * dt
while step > 0.0 and _seg + 1 < _path.size():
var a := _path[_seg]
var b := _path[_seg + 1]
var left := a.distance_to(b) - _seg_t
if step < left:
_seg_t += step
step = 0.0
else:
step -= left
_seg += 1
_seg_t = 0.0
_yaw = _travel_yaw(_seg)
if _seg + 1 >= _path.size():
_finish()
return
_place(_path[_seg].lerp(_path[_seg + 1], _seg_t / _path[_seg].distance_to(_path[_seg + 1])))
## Every terrain chunk mesh (DCTerrain.debug_chunk_meshes: leaves with a mesh, NOT the
## seam meshes): triangles and vertices by LOD and by distance band, the largest
## chunks, and the vertex format -- to see whether a few chunks or the layout carry
## the fixed depth pre-pass cost.
func _chunk_report() -> void:
var cam_pos := _cam.global_position
var by_lod := {}
var by_band := {}
var rows: Array = []
var fmt := -1
for c: Dictionary in _terrain.call("debug_chunk_meshes"):
var m: Mesh = c["mesh"]
if m == null or m.get_surface_count() == 0:
continue
var tris := 0
var verts := 0
for s in m.get_surface_count():
var arr := m.surface_get_arrays(s)
var v: PackedVector3Array = arr[Mesh.ARRAY_VERTEX]
verts += v.size()
var ix: Variant = arr[Mesh.ARRAY_INDEX]
tris += ((ix as PackedInt32Array).size() if ix != null and (ix as PackedInt32Array).size() > 0 else v.size()) / 3
if fmt < 0 and m is ArrayMesh:
fmt = (m as ArrayMesh).surface_get_format(0)
var origin: Vector3 = c["origin"]
var world := (_terrain as Node3D).to_global(origin)
var d := world.distance_to(cam_pos)
var lod := int(c["lod"])
var band := "0-100m" if d < 100.0 else ("100-300m" if d < 300.0 else ("300-1200m" if d < 1200.0 else ">1200m"))
for pair in [[by_lod, lod], [by_band, band]]:
var acc: Dictionary = pair[0]
if not acc.has(pair[1]):
acc[pair[1]] = [0, 0, 0]
acc[pair[1]][0] += 1
acc[pair[1]][1] += tris
acc[pair[1]][2] += verts
rows.append([tris, verts, lod, d])
var keys := by_lod.keys()
keys.sort()
for k in keys:
print("[chunks] lod %2d: %4d chunks %9d tris %9d verts" % [k, by_lod[k][0], by_lod[k][1], by_lod[k][2]])
for k in ["0-100m", "100-300m", "300-1200m", ">1200m"]:
if by_band.has(k):
print("[chunks] %-9s: %4d chunks %9d tris %9d verts" % [k, by_band[k][0], by_band[k][1], by_band[k][2]])
rows.sort_custom(func(a, b): return a[0] > b[0])
for r in rows.slice(0, 8):
print("[chunks] top: %8d tris %8d verts lod %d at %.0f m" % [r[0], r[1], r[2], r[3]])
print("[chunks] vertex format flags 0x%x (CUSTOM0-3 present: %s)" % [fmt,
str([fmt & Mesh.ARRAY_FORMAT_CUSTOM0 != 0, fmt & Mesh.ARRAY_FORMAT_CUSTOM1 != 0,
fmt & Mesh.ARRAY_FORMAT_CUSTOM2 != 0, fmt & Mesh.ARRAY_FORMAT_CUSTOM3 != 0])])
func _pct(v: PackedFloat64Array, p: float) -> float:
if v.is_empty():
return 0.0
var s := v.duplicate()
s.sort()
return s[clampi(int(round(p * (s.size() - 1))), 0, s.size() - 1)]
func _col(c: int) -> PackedFloat64Array:
var r := PackedFloat64Array()
for row in _rows:
r.append(row[c])
return r
func _finish() -> void:
if _env("BENCH_SUBVIEWPORT", "") == "toggle":
_toggle_report()
var f := FileAccess.open(_out + ".csv", FileAccess.WRITE)
f.store_line("t,cam_y,frame_ms,gpu_ms,terrain_total_ms,traverse_ms,upload_ms,collision_ms,dispatched,uploaded")
for row in _rows:
var cells := PackedStringArray()
for x in row:
cells.append("%.3f" % x)
f.store_line(",".join(cells))
f.close()
var fr := _col(2)
var slow := 0
for x in fr:
if x > 33.4:
slow += 1
var total := 0.0
for x in fr:
total += x
print("[bench-path] %d frames over %.1f s, mean %.1f fps%s" % [fr.size(), total / 1000.0,
1000.0 * fr.size() / maxf(total, 1.0),
" ⚠ NOT DRAWING for %d frames — invalid" % _hidden if _hidden > 0 else ""])
for c in [[2, "frame"], [3, "gpu"], [4, "terrain total"], [5, "traverse"], [6, "upload"], [7, "collision"]]:
var v := _col(c[0])
print("[bench-path] %-14s ms p50 %7.2f p95 %7.2f p99 %7.2f max %8.2f"
% [c[1], _pct(v, 0.5), _pct(v, 0.95), _pct(v, 0.99), _pct(v, 1.0)])
print("[bench-path] frames > 33.4 ms: %d (%.0f%%) -> %s.csv" % [slow, 100.0 * slow / maxf(fr.size(), 1), _out])
# What the last frame DREW, per Godot: separates "too many triangles" from
# "expensive draws" without guessing (depth pre-pass stayed 6 ms at a quarter of the
# pixels AND a quarter of the chunks, 2026-10-07).
var vp_rid := get_viewport().get_viewport_rid()
for t in [[RenderingServer.VIEWPORT_RENDER_INFO_TYPE_VISIBLE, "visible"],
[RenderingServer.VIEWPORT_RENDER_INFO_TYPE_SHADOW, "shadow"]]:
print("[bench-path] render info %-7s: %d objects, %d primitives, %d draw calls" % [t[1],
RenderingServer.viewport_get_render_info(vp_rid, t[0], RenderingServer.VIEWPORT_RENDER_INFO_OBJECTS_IN_FRAME),
RenderingServer.viewport_get_render_info(vp_rid, t[0], RenderingServer.VIEWPORT_RENDER_INFO_PRIMITIVES_IN_FRAME),
RenderingServer.viewport_get_render_info(vp_rid, t[0], RenderingServer.VIEWPORT_RENDER_INFO_DRAW_CALLS_IN_FRAME)])
if _env("BENCH_CHUNK_REPORT", "") == "1" and _terrain != null:
_chunk_report()
# The biggest scene MESHES (DCTerrain chunks are RenderingServer instances, not
# nodes, so anything large here is NOT terrain).
var meshes: Array = []
for n in get_tree().root.find_children("*", "MeshInstance3D", true, false):
var mi := n as MeshInstance3D
if mi.mesh == null:
continue
var v := 0
for s in mi.mesh.get_surface_count():
var arr := mi.mesh.surface_get_arrays(s)
v += (arr[Mesh.ARRAY_VERTEX] as PackedVector3Array).size() if arr.size() > 0 and arr[Mesh.ARRAY_VERTEX] != null else 0
meshes.append([v, mi.is_visible_in_tree(), String(mi.get_path())])
meshes.sort_custom(func(a, b): return a[0] > b[0])
for m in meshes.slice(0, 6):
print("[bench-path] mesh %8d verts visible=%s %s" % [m[0], str(m[1]), m[2]])
# The last frame, for A/B-ing what a change does to the IMAGE. The chunk count and
# settled flag say whether two shots saw the same geometry.
get_viewport().get_texture().get_image().save_png(_out + ".png")
if _terrain != null:
# Not get("lod_factor"): the getter returns the inspector value (3.0) while
# _ready forces the EFFECTIVE factor to 2.0 (dc_terrain.cpp:346).
print("[bench-path] terrain chunks %d, settled %s, lod_factor %s, scale %.2f -> %s.png" % [
int(_terrain.call("get_chunk_count")), str(_terrain.call("is_lod_settled")),
_env("BENCH_LOD_FACTOR", "2.0 (engine default)"), get_viewport().scaling_3d_scale, _out])
get_tree().quit(0)