extends SceneTree
## PROBE (not a gate) — SPOM / fast-POM evaluation. One 4x2 m brick wall (Bricks105) in
## front of a grey backdrop; for each shader mode (spom_probe.gdshader 0..4) and camera
## pose, the viewport's MEASURED GPU time averaged over 90 frames, plus a screenshot.
## DISPLAY=:0 <godot> --path . --rendering-driver vulkan --script res://scenes/test/_probe_spom.gd -- --out DIR [--i3-ws N|auto|off]
##
## ⚠ A HIDDEN WINDOW IS NOT DRAWN AND MEASURES 0.000 ms. Under i3 a new window opens behind
## a fullscreen one (Guake) and Godot reports itself minimized. --i3-ws N moves THIS window,
## floating at a fixed 1600x900, to workspace N. Default `auto` (when i3 is running): the
## visible workspace of the left-most OTHER display with no fullscreen window on it.
## Measuring waits until the window really has a size, and the run fails rather than
## print zeros.
const DIR := "res://texture_packs/ambientcg/Bricks105/"
const MODES := ["normal", "pom", "fpom", "spom", "spom_depth", "pom_off_unif"]
# name, camera position, look-at. The wall is the XY plane at z = 0, facing +Z.
const POSES := [
["head_on", Vector3(0, 0, 1.6), Vector3(0, 0, 0)],
["grazing", Vector3(-2.6, 0.2, 0.55), Vector3(0.6, 0, 0)],
["far", Vector3(0, 0, 11.0), Vector3(0, 0, 0)],
# looking along the wall at its FAR (x = +2) edge: the POM offset moves samples AWAY
# from the eye, so rays can only leave the face through its far edge -> SPOM carves it
["edge", Vector3(0.9, 0.0, 0.25), Vector3(2.0, 0.0, 0.0)],
]
const WARM := 30
const MEASURE := 90
const ROUNDS := 3 # cases INTERLEAVED per round, median reported
const SUPERSAMPLE := 2.0 # 3D scale: 4x the shaded pixels -> shader cost >> noise
var _out := "/tmp/spom"
var _mat: ShaderMaterial
var _shaders: Array = [] # one compiled variant per mode (#define MODE n)
var _round := 0
var _samples: Dictionary = {} # "pose/mode" -> [ms per round]
var _cam: Camera3D
var _vp: RID
var _case := 0
var _f := 0
var _acc := 0.0
var _rows: Array = []
var _i3_ws := ""
var _ready_frames := 0
var _next_i3 := 0
func _img(name: String) -> Image:
var im := Image.load_from_file(ProjectSettings.globalize_path(DIR + name))
im.generate_mipmaps()
return im
## Min-depth pyramid: level k cell = 1 - MAX height of the 2^k x 2^k block it covers.
func _min_depth_pyramid(h: Image) -> ImageTexture:
h.convert(Image.FORMAT_L8)
var w := h.get_width()
var ht := h.get_height()
var level := PackedByteArray()
level.resize(w * ht)
var src := h.get_data()
for i in w * ht:
level[i] = 255 - src[i]
var data := level.duplicate()
while w > 1 or ht > 1:
var nw := maxi(w / 2, 1)
var nh := maxi(ht / 2, 1)
var nxt := PackedByteArray()
nxt.resize(nw * nh)
for y in nh:
for x in nw:
var m := 255
for dy in 2:
for dx in 2:
var sx := mini(x * 2 + dx, w - 1)
var sy := mini(y * 2 + dy, ht - 1)
m = mini(m, level[sy * w + sx])
nxt[y * nw + x] = m
data.append_array(nxt)
level = nxt
w = nw
ht = nh
var im := Image.create_from_data(h.get_width(), h.get_height(), true, Image.FORMAT_R8, data)
return ImageTexture.create_from_image(im)
func _initialize() -> void:
var args := OS.get_cmdline_user_args()
var i := args.find("--out")
if i >= 0 and i + 1 < args.size():
_out = args[i + 1]
DirAccess.make_dir_recursive_absolute(_out)
var w := args.find("--i3-ws")
var ws := args[w + 1] if w >= 0 and w + 1 < args.size() else "auto"
_i3_ws = _i3_free_workspace() if ws == "auto" else ("" if ws == "off" else ws)
print("PROBE i3 workspace: %s" % (_i3_ws if _i3_ws != "" else "(none — not moving the window)"))
DisplayServer.window_set_mode(DisplayServer.WINDOW_MODE_WINDOWED)
DisplayServer.window_set_size(Vector2i(1600, 900))
DisplayServer.window_set_vsync_mode(DisplayServer.VSYNC_DISABLED)
var src := FileAccess.get_file_as_string("res://scenes/test/spom_probe.gdshader")
for m in MODES.size():
var sh := Shader.new()
sh.code = src.replace("shader_type spatial;", "shader_type spatial;\n#define MODE %d" % m)
_shaders.append(sh)
_mat = ShaderMaterial.new()
_mat.shader = _shaders[1]
_mat.set_shader_parameter("albedo_tex", ImageTexture.create_from_image(_img("albedo.jpg")))
_mat.set_shader_parameter("normal_tex", ImageTexture.create_from_image(_img("normal.jpg")))
var h := _img("height.png")
_mat.set_shader_parameter("height_tex", ImageTexture.create_from_image(h))
_mat.set_shader_parameter("mindepth_tex", _min_depth_pyramid(Image.load_from_file(ProjectSettings.globalize_path(DIR + "height.png"))))
var wall := MeshInstance3D.new()
var q := QuadMesh.new()
q.size = Vector2(4, 2)
wall.mesh = q
wall.material_override = _mat
root.add_child(wall)
var back := MeshInstance3D.new()
var bq := QuadMesh.new()
bq.size = Vector2(40, 20)
back.mesh = bq
back.position = Vector3(0, 0, -0.5)
var bm := StandardMaterial3D.new()
bm.albedo_color = Color(0.35, 0.4, 0.45)
back.material_override = bm
root.add_child(back)
var sun := DirectionalLight3D.new()
sun.rotation_degrees = Vector3(-35, -40, 0)
sun.light_energy = 1.4
root.add_child(sun)
var env := WorldEnvironment.new()
env.environment = Environment.new()
env.environment.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
env.environment.ambient_light_color = Color(0.5, 0.5, 0.55)
env.environment.background_mode = Environment.BG_COLOR
env.environment.background_color = Color(0.2, 0.22, 0.25)
root.add_child(env)
_cam = Camera3D.new()
_cam.fov = 70
root.add_child(_cam)
_vp = root.get_viewport_rid()
RenderingServer.viewport_set_measure_render_time(_vp, true)
_set_case()
## The visible workspace of the left-most display whose visible workspace holds NO
## fullscreen window ("" when i3 is not running or every display is covered).
func _i3_free_workspace() -> String:
var o: Array = []
if OS.execute("i3-msg", ["-t", "get_workspaces"], o) != 0 or o.is_empty():
return ""
var wss: Variant = JSON.parse_string(String(o[0]))
var t: Array = []
OS.execute("i3-msg", ["-t", "get_tree"], t)
var tree: Variant = JSON.parse_string(String(t[0])) if not t.is_empty() else null
if not (wss is Array) or not (tree is Dictionary):
return ""
var covered := {} # workspace name -> true when a WINDOW on it is fullscreen
_i3_fullscreen(tree, "", covered)
var best := ""
var best_x := INF
for x: Dictionary in wss:
if bool(x.get("visible")) and not covered.has(String(x.get("name"))):
var rx := float((x.get("rect", {}) as Dictionary).get("x", 0))
if rx < best_x:
best_x = rx
best = str(int(x.get("num")))
return best
func _i3_fullscreen(n: Dictionary, ws: String, covered: Dictionary) -> void:
if String(n.get("type")) == "workspace":
ws = String(n.get("name"))
elif int(n.get("fullscreen_mode", 0)) != 0 and n.get("window") != null and ws != "":
covered[ws] = true # workspace nodes themselves always report mode 1 — skip them
for c: Variant in (n.get("nodes", []) as Array) + (n.get("floating_nodes", []) as Array):
_i3_fullscreen(c, ws, covered)
## The Settings autoload may have put the root viewport on DLSS / a render scale. The
## probe measures the SHADER, so it pins native rendering (runtime only, nothing saved).
func _pin_native() -> void:
if root.scaling_3d_mode != Viewport.SCALING_3D_MODE_BILINEAR or root.scaling_3d_scale != SUPERSAMPLE \
or root.msaa_3d != Viewport.MSAA_DISABLED or root.use_taa:
print("PROBE viewport was scaling_mode=%d scale=%.2f msaa=%d taa=%s -> native" % [
root.scaling_3d_mode, root.scaling_3d_scale, root.msaa_3d, root.use_taa])
root.scaling_3d_mode = Viewport.SCALING_3D_MODE_BILINEAR
root.scaling_3d_scale = SUPERSAMPLE
root.msaa_3d = Viewport.MSAA_DISABLED
root.use_taa = false
root.screen_space_aa = Viewport.SCREEN_SPACE_AA_DISABLED
func _set_case() -> void:
_pin_native()
var p: Array = POSES[_case / MODES.size()]
_mat.shader = _shaders[_case % MODES.size()]
_cam.look_at_from_position(p[1], p[2])
_f = 0
_acc = 0.0
func _process(_d: float) -> bool:
if _ready_frames < 20:
if _i3_ws != "" and _ready_frames == 0 and Time.get_ticks_msec() >= _next_i3:
_next_i3 = Time.get_ticks_msec() + 1000
# Match THIS window by its X id — a title match could grab the user's own game.
var xid := DisplayServer.window_get_native_handle(DisplayServer.WINDOW_HANDLE)
var crit := '[id="%d"]' % xid
var o: Array = []
OS.execute("i3-msg", ['%s move container to workspace number %s; %s floating enable, resize set 1600 900' % [crit, _i3_ws, crit]], o)
var st: Array = []
OS.execute("xprop", ["-id", str(xid), "WM_STATE", "_NET_WM_STATE"], st)
print("PROBE i3 xid=%d %s | %s" % [xid, str(o).strip_edges(), str(st).replace("\\n", " ")])
if root.size.x >= 1000 and DisplayServer.window_get_mode() != DisplayServer.WINDOW_MODE_MINIMIZED:
_ready_frames += 1
elif Time.get_ticks_msec() > 20000: # wall time: hidden frames run unthrottled
print("PROBE FAIL window never drew (size %s mode %d) — hidden behind a fullscreen window? use --i3-ws" % [root.size, DisplayServer.window_get_mode()])
quit(1)
return true
return false
_f += 1
if _f > WARM:
_acc += RenderingServer.viewport_get_measured_render_time_gpu(_vp)
if _f == WARM + MEASURE:
var pose: String = POSES[_case / MODES.size()][0]
var mode: String = MODES[_case % MODES.size()]
var ms := _acc / MEASURE
if ms <= 0.0:
print("PROBE FAIL %s/%s measured 0 ms — the window is not being drawn" % [pose, mode])
quit(1)
return true
var key := "%s/%s" % [pose, mode]
if not _samples.has(key):
_samples[key] = []
_samples[key].append(ms)
if _round == 0:
root.get_texture().get_image().save_png("%s/%s_%s.png" % [_out, pose, mode])
_case += 1
if _case >= POSES.size() * MODES.size():
_case = 0
_round += 1
print("PROBE round %d done" % _round)
if _round >= ROUNDS:
_report()
quit(0)
return true
_set_case()
return false
func _report() -> void:
var px := Vector2(root.size) * SUPERSAMPLE
print("PROBE GPU ms, median of %d interleaved rounds x %d frames, %dx%d shaded px" % [
ROUNDS, MEASURE, int(px.x), int(px.y)])
for p: Array in POSES:
var base := 0.0
for m: String in MODES:
var v: Array = _samples["%s/%s" % [p[0], m]].duplicate()
v.sort()
var med: float = v[v.size() / 2]
if m == "normal":
base = med
print("PROBE %-8s %-11s %.3f ms (+%.3f vs normal) rounds %s" % [
p[0], m, med, med - base, str(v.map(func(x: float) -> String: return "%.3f" % x))])