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 `--`): [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: -- [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