106 lines
4 KiB
GDScript3
106 lines
4 KiB
GDScript3
|
|
extends SceneTree
|
||
|
|
|
||
|
|
# Not a test - a stopwatch for a fullscreen background shader, in the same
|
||
|
|
# spirit as _profile_startup.gd but aimed at the FRAGMENT side instead of
|
||
|
|
# scene setup. Forgejo #68 ("the moon game is a bit slow").
|
||
|
|
#
|
||
|
|
# Renders nothing but one fullscreen ColorRect carrying the given shader, so
|
||
|
|
# the frame time is essentially pure per-pixel shader cost with no gameplay,
|
||
|
|
# UI or draw calls mixed in. Run it via tests/profile_shader.sh, which
|
||
|
|
# supplies Xvfb + Mesa softpipe exactly like screenshot.sh does.
|
||
|
|
#
|
||
|
|
# Softpipe is a pure-C rasterizer, not the phone's GPU, so the ABSOLUTE
|
||
|
|
# numbers mean nothing. What it measures well is RELATIVE per-fragment ALU
|
||
|
|
# cost between two versions of the same shader: softpipe executes the
|
||
|
|
# fragment program per pixel on the CPU, so halving the arithmetic shows up
|
||
|
|
# as roughly half the frame time.
|
||
|
|
#
|
||
|
|
# TWO THINGS WILL MISLEAD YOU IF YOU READ THE RAW NUMBER.
|
||
|
|
#
|
||
|
|
# 1. There is a FLOOR. A shader that does nothing but assign a constant
|
||
|
|
# colour still costs ~657 ms/frame at 720x1280 here, which is softpipe's
|
||
|
|
# own per-pixel overhead. Subtract it before comparing, or a shader that
|
||
|
|
# got 20x cheaper will look like it only got 2x cheaper.
|
||
|
|
#
|
||
|
|
# 2. It cannot price a TEXTURE FETCH against arithmetic. Softpipe does
|
||
|
|
# filtering and mip selection in CPU code, so a fetch measures as
|
||
|
|
# expensive here; real GPUs - mobile ones especially - run it on
|
||
|
|
# dedicated hardware where it is close to free. Trading arithmetic for a
|
||
|
|
# lookup is exactly the kind of change this harness will report backwards.
|
||
|
|
# Use it to compare arithmetic against arithmetic, and nothing else.
|
||
|
|
#
|
||
|
|
# Args (after `--`): <shader_res_path> [timed_frames] [warmup_frames]
|
||
|
|
|
||
|
|
var shader_path: String
|
||
|
|
var timed_frames: int = 30
|
||
|
|
var warmup_frames: int = 10
|
||
|
|
var frames: int = 0
|
||
|
|
var rect: ColorRect
|
||
|
|
var start_usec: int = 0
|
||
|
|
|
||
|
|
|
||
|
|
func _initialize() -> void:
|
||
|
|
var args: PackedStringArray = OS.get_cmdline_user_args()
|
||
|
|
if args.is_empty():
|
||
|
|
printerr("usage: -- <shader_res_path> [timed_frames] [warmup_frames]")
|
||
|
|
quit(1)
|
||
|
|
return
|
||
|
|
shader_path = args[0]
|
||
|
|
if args.size() >= 2:
|
||
|
|
timed_frames = int(args[1])
|
||
|
|
if args.size() >= 3:
|
||
|
|
warmup_frames = int(args[2])
|
||
|
|
|
||
|
|
var shader: Shader = load(shader_path)
|
||
|
|
if shader == null:
|
||
|
|
printerr("could not load shader: %s" % shader_path)
|
||
|
|
quit(1)
|
||
|
|
return
|
||
|
|
|
||
|
|
var material := ShaderMaterial.new()
|
||
|
|
material.shader = shader
|
||
|
|
|
||
|
|
var layer := CanvasLayer.new()
|
||
|
|
layer.layer = -10
|
||
|
|
root.add_child(layer)
|
||
|
|
|
||
|
|
rect = ColorRect.new()
|
||
|
|
rect.material = material
|
||
|
|
rect.anchor_right = 1.0
|
||
|
|
rect.anchor_bottom = 1.0
|
||
|
|
rect.size = root.get_visible_rect().size
|
||
|
|
layer.add_child(rect)
|
||
|
|
|
||
|
|
|
||
|
|
func _process(_delta: float) -> bool:
|
||
|
|
# Move the camera every frame. A static camera would let a driver skip
|
||
|
|
# redundant uniform uploads and would only ever exercise one crater
|
||
|
|
# layout; drifting keeps the measurement honest and representative of
|
||
|
|
# actually flying around.
|
||
|
|
var t: float = float(frames) * 0.05
|
||
|
|
var p: Vector2 = Vector2(cos(t), sin(t)) * 40.0
|
||
|
|
var cell := Vector2(floor(p.x), floor(p.y))
|
||
|
|
rect.material.set_shader_parameter("camera_cell", cell)
|
||
|
|
rect.material.set_shader_parameter("camera_frac", p - cell)
|
||
|
|
rect.material.set_shader_parameter("camera_zoom", 1.0)
|
||
|
|
rect.material.set_shader_parameter("viewport_size", root.get_visible_rect().size)
|
||
|
|
|
||
|
|
frames += 1
|
||
|
|
# Shaders compile on first use and the first frames carry that cost, so
|
||
|
|
# start the clock only once the pipeline is warm.
|
||
|
|
if frames == warmup_frames:
|
||
|
|
start_usec = Time.get_ticks_usec()
|
||
|
|
return false
|
||
|
|
if frames < warmup_frames + timed_frames:
|
||
|
|
return false
|
||
|
|
|
||
|
|
var span_usec: int = Time.get_ticks_usec() - start_usec
|
||
|
|
var per_frame_ms: float = float(span_usec) / 1000.0 / float(timed_frames)
|
||
|
|
var px: Vector2 = root.get_visible_rect().size
|
||
|
|
print("SHADER_PROFILE: %s" % shader_path)
|
||
|
|
print(" viewport : %dx%d (%d px)" % [px.x, px.y, int(px.x * px.y)])
|
||
|
|
print(" frames timed : %d (after %d warmup)" % [timed_frames, warmup_frames])
|
||
|
|
print(" per frame : %.1f ms" % per_frame_ms)
|
||
|
|
print(" per megapixel : %.1f ms" % (per_frame_ms / (px.x * px.y / 1000000.0)))
|
||
|
|
quit(0)
|
||
|
|
return true
|