OverviewFilesCommitsIssuesDocsReleasesPeers

hyper-tps / scenes / test / bridged_density_render_cost.gd

89 lines · gd ref:
extends SceneTree
## How long does the spliced material cost to actually RENDER -- not just parse?
##
##   DISPLAY=:0 godot --path . --rendering-driver vulkan \
##       --script res://scenes/test/bridged_density_render_cost.gd -- --taps 6
##
## WHY THIS EXISTS. bridged_density_splice_gate proves the text COMPILES, and
## measured 3.7 s for that on a real device. Neither number covers what the game
## actually does: putting the material on a visible surface, where Godot
## compiles a pipeline per render pass (opaque, shadow, depth) and each one is a
## separate specialization of a 196 KB shader. That cost lands as a FREEZE the
## first time the mountain is drawn, which is what "hanging at runtime" looks
## like from the outside. Reading shader.cpp cannot tell you how long it takes.
##
## --taps controls how many evaluate_density call sites the detail normal uses,
## because compile cost is driven by CODE SIZE at the call sites, and that is
## the one knob that reduces it without changing the field.

const BDM := preload("res://scenes/maps/bridged_density_material.gd")
const MOUNTAIN_SCENE := "res://blender/mountain_voronoi2.tscn"
const WORLD_SCALE := 100.0

var _taps := 6
var _frames := 0
var _t_first := 0
var _mat: ShaderMaterial = null


func _initialize() -> void:
	var args := OS.get_cmdline_user_args()
	for i in range(args.size() - 1):
		if args[i] == "--taps": _taps = int(args[i + 1])

	var ps: PackedScene = load(MOUNTAIN_SCENE)
	var inst: Node = ps.instantiate()
	var src: Material = inst.get_node(^"Mountain").get("material_override")
	var graph: Resource = inst.get_node(^"Mountain/ProcGeoNodes").get("graph")
	inst.free()

	var gen = ClassDB.instantiate("DCTerrainGeneratorProcNodesBounded")
	gen.set_world_scale(WORLD_SCALE)
	gen.set_heightfield(true)
	gen.set_graph(graph)

	var t0 := Time.get_ticks_msec()
	var built := BDM.build(src as ShaderMaterial, gen, _taps)
	print("[COST] taps=%d  build=%d ms  bytes=%d  %s"
			% [_taps, Time.get_ticks_msec() - t0, String(built["code"]).length(),
			   ", ".join(built["log"])])
	if built["material"] == null:
		quit(1)
		return
	_mat = built["material"]

	# A visible surface, a light that casts shadows, and a camera pointing at
	# it: that is the minimum that forces the opaque AND shadow pipelines.
	var world := Node3D.new()
	root.add_child(world)
	var mi := MeshInstance3D.new()
	var sphere := SphereMesh.new()
	sphere.radius = 50.0
	sphere.height = 100.0
	mi.mesh = sphere
	mi.material_override = _mat
	world.add_child(mi)
	var lt := DirectionalLight3D.new()
	lt.shadow_enabled = true
	lt.rotation_degrees = Vector3(-45, 30, 0)
	world.add_child(lt)
	var cam := Camera3D.new()
	cam.position = Vector3(0, 40, 160)
	world.add_child(cam)
	cam.make_current()
	_t_first = Time.get_ticks_msec()
	print("[COST] scene up, waiting on first drawn frames")


func _process(_dt: float) -> bool:
	_frames += 1
	if _frames == 1:
		print("[COST] taps=%d  FIRST FRAME after %d ms  <-- this is the freeze"
				% [_taps, Time.get_ticks_msec() - _t_first])
	if _frames >= 30:
		print("[COST] taps=%d  30 frames in %d ms total"
				% [_taps, Time.get_ticks_msec() - _t_first])
		quit(0)
		return true
	return false