#!/usr/bin/env bash
# pt-bench.sh — deterministic path-traced frame-time benchmark.
#
# Runs scenes/test/dc_terrain_streaming_pt_noplayer.tscn under the godot-rtx build with
# the PTBench harness enabled: fixed camera path, a settle gate on terrain quiescence +
# frame-time stability, then p50/p95/p99 frame times.
#
# RUN IT FROM A TERMINAL IN YOUR DESKTOP SESSION. The game window must be VISIBLE while
# it runs — under i3 a window that opens as a background tab gets a 0x0 geometry and
# Godot silently clamps the render target to 2x2, which produces ~7ms frame times of
# nothing at all. The harness aborts with exit 3 if that happens, so a bad run cannot
# masquerade as a result, but you still have to give it a real window.
#
# RT/engine work runs the engine binary directly, NOT the shipped `hyper-tps` launcher —
# an engine defect is faster to isolate without the game, autoloads and a deploy
# round-trip. (The canonical-launcher rule still governs verifying the SHIPPED game.)
#
# Usage:
# bash scripts/pt-bench.sh --label=baseline
# bash scripts/pt-bench.sh --label=dlss-rr --dlss=rr_perf
# bash scripts/pt-bench.sh --label=raster --raster
# bash scripts/pt-bench.sh --label=nostream --freeze-lod --static
# GODOT=/path/to/godot bash scripts/pt-bench.sh --label=custom
#
# All arguments are forwarded to the harness — see scenes/test/pt_bench.gd for the list.
# The two most useful comparisons for hyper-games #11:
# --freeze-lod render cost with streaming churn removed
# --raster vs default the raster/PT gap on identical geometry and camera path
#
# Exit codes: 0 = valid run, 2 = UNSETTLED (gate timed out), 3 = no usable window,
# 1 = refused before launching.
set -uo pipefail
REPO="$(cd "$(dirname "$0")/.." && pwd)"
# Default is the terrain scene; SCENE= or --level selects another. The Quake map is
# the one that can actually validate texture LOD (see benchmarks/pt/RESULTS.md).
SCENE="${SCENE:-res://scenes/test/dc_terrain_streaming_pt_noplayer.tscn}"
for a in "$@"; do
if [ "$a" = "--level" ]; then
SCENE="res://scenes/test/dc_level_pt_bench.tscn"
fi
done
# --level is ours, not the harness's; drop it before forwarding.
ARGS=()
for a in "$@"; do [ "$a" = "--level" ] || ARGS+=("$a"); done
set -- "${ARGS[@]}"
GODOT="${GODOT:-$REPO/../nvidia-rtx-godot/bin/godot.linuxbsd.editor.x86_64}"
export DISPLAY="${DISPLAY:-:0}"
# This ERR_PRINT text is compiled in ONLY when neither STREAMLINE_ENABLED nor
# NGX_DLSS_ENABLED is defined (renderer_viewport.cpp:1053), so finding it in the binary
# is a reliable offline "this build has no DLSS" test. Worth having: a whole session went
# into theorising about texture bandwidth while every binary had DLSS compiled out
# (hyper-games #11 §1).
NO_DLSS_MARKER="neither Streamline nor NGX DLSS support"
[ -x "$GODOT" ] || { echo "pt-bench: no engine binary at $GODOT (set GODOT=...)" >&2; exit 1; }
DLSS_REQUESTED="off"
RES_W=1280
RES_H=720
for a in "$@"; do
case "$a" in
--dlss=*) DLSS_REQUESTED="${a#--dlss=}" ;;
--resolution=*) RES_W="${a#--resolution=}"; RES_H="${RES_W#*x}"; RES_W="${RES_W%x*}" ;;
esac
done
if grep -a -q "$NO_DLSS_MARKER" "$GODOT"; then BUILD_DLSS="no"; else BUILD_DLSS="yes"; fi
if [ "$DLSS_REQUESTED" != "off" ] && [ "$BUILD_DLSS" = "no" ]; then
echo "pt-bench: REFUSING — --dlss=$DLSS_REQUESTED requested but $(basename "$GODOT")" >&2
echo " was compiled WITHOUT DLSS (SConstruct:204 defaults use_ngx_dlss=False)." >&2
echo " Rebuild with: scons platform=linuxbsd target=editor use_ngx_dlss=yes" >&2
exit 1
fi
if [ "$DLSS_REQUESTED" != "off" ]; then
BIN_DIR="$(dirname "$GODOT")"
if ! ls "$BIN_DIR"/libnvidia-ngx-dlss*.so* >/dev/null 2>&1 \
&& ! ls "$BIN_DIR"/../thirdparty/ngx/lib/linux_x86_64/libnvidia-ngx-dlss*.so* >/dev/null 2>&1; then
echo "pt-bench: WARNING — no libnvidia-ngx-dlss*.so next to the binary or in" >&2
echo " thirdparty/ngx/lib/linux_x86_64; NGX will fail to load the feature." >&2
fi
fi
mkdir -p "$REPO/benchmarks/pt"
LOG="$REPO/benchmarks/pt/pt-bench-$(date +%Y%m%d-%H%M%S).log"
# Record GPU thermal/power state around the run. RESULTS.md carried an open item for a
# monotonic 6.8% climb across three sequential no-DLSS runs, attributed to "ambient load"
# with no measurement behind it; a map-scene triple later drifted 10.86 -> 12.79 ms (+18%)
# in run order. A run on a 54C idle GPU and a run on an 85C one are not the same
# experiment, and nothing in the report said which had happened. Cheap to record, and it
# turns "unexplained drift" into a column.
gpu_state() {
if command -v nvidia-smi >/dev/null 2>&1; then
nvidia-smi --query-gpu=temperature.gpu,clocks.sm,power.draw --format=csv,noheader 2>/dev/null \
| head -1
else
echo "n/a"
fi
}
GPU_BEFORE="$(gpu_state)"
echo "=== pt-bench ==="
echo " engine : $GODOT"
echo " dlss : compiled-in=$BUILD_DLSS requested=$DLSS_REQUESTED"
echo " scene : $SCENE"
echo " log : $LOG"
echo " args : $*"
echo " gpu : $GPU_BEFORE (temp, sm clock, power — BEFORE the run)"
echo ""
echo " Keep the game window visible and in the foreground for the whole run."
echo ""
# Best-effort under i3: the window opens as a background tab often enough that it is
# worth floating and sizing it. Sized to match --resolution, because a hardcoded size
# here silently overrode the harness's own resize and produced a run at the wrong pixel
# count while the flag said otherwise. The in-harness size guard is the real backstop.
#
# POLL, don't sleep-then-fire-once. A single `sleep 6` could never rescue a run: the
# harness aborts on a 0x0 window at MIN_WARMUP = 2s (pt_bench.gd:339), four seconds
# before the nudge landed. It only ever appeared to work on scenes whose window the WM
# happened to map correctly by itself. A heavy scene — map_dc_terrain, which rebuilds
# terrain and imports a WAD before its first frame — opens as a background tab and died
# every time. Re-assert for 12s so the window is floated the moment it exists, and keep
# asserting, because i3 re-tiles it when the workspace layout changes underneath.
if command -v i3-msg >/dev/null 2>&1; then
( for _ in $(seq 1 24); do
i3-msg "[title=\"^HyperTPS\"] floating enable, resize set $RES_W $RES_H, move position center, focus" \
>/dev/null 2>&1
sleep 0.5
done ) &
NUDGE_PID=$!
trap 'kill $NUDGE_PID 2>/dev/null' EXIT
fi
# Sample the GPU WHILE it renders, not after. A single reading taken once the engine has
# exited catches the card already idling back down (54C/300MHz), which describes the
# cooldown rather than the run — the first version of this reported exactly that and
# would have made every run look thermally identical.
GPU_SAMPLES="$(mktemp)"
if command -v nvidia-smi >/dev/null 2>&1; then
( while :; do
nvidia-smi --query-gpu=temperature.gpu,clocks.sm --format=csv,noheader 2>/dev/null >>"$GPU_SAMPLES"
sleep 2
done ) &
SAMPLER_PID=$!
fi
"$GODOT" --path "$REPO" "$SCENE" -- --pt-bench "$@" 2>&1 | tee "$LOG"
[ -n "${SAMPLER_PID:-}" ] && kill "$SAMPLER_PID" 2>/dev/null
GPU_PEAK="$(sort -t, -k1 -n "$GPU_SAMPLES" 2>/dev/null | tail -1)"
[ -z "$GPU_PEAK" ] && GPU_PEAK="n/a"
rm -f "$GPU_SAMPLES"
echo ""
echo "=== validity checks (a run that fails these is not a measurement) ==="
fail=0
check_absent() { # check_absent <label> <pattern>
if grep -a -q -- "$2" "$LOG"; then
echo " FAIL $1"
grep -a -m3 -- "$2" "$LOG" | sed 's/^/ /'
fail=1
else
echo " ok $1"
fi
}
check_absent "window had usable geometry" "ABORT — render target is"
check_absent "rendered at the requested size" "RESOLUTION MISMATCH"
# "Failed to compile" alone missed a real failure: a debug variant died with
# "Error compiling ClosestHit shader" / "Failed to build base compile context",
# the renderer silently fell back, and the run reported 104ms as if it were a
# measurement. Match the whole family.
check_absent "no RT shader compile failure" "Failed to compile"
check_absent "no shader stage compile error" "Error compiling"
check_absent "no failed compile context" "Failed to build base compile context"
check_absent "no glslang parse failure" "compilation errors. No code generated"
check_absent "no shader parse error" "SHADER ERROR"
check_absent "no invalid-RID storm" "Attempted to free invalid ID"
check_absent "no device loss" "VK_ERROR_DEVICE_LOST"
check_absent "no dangling NGX matrices" "not invertible"
[ "$DLSS_REQUESTED" != "off" ] && check_absent "DLSS available at runtime" "ERROR: DLSS is not available"
# Absence of an error is weak evidence. When DLSS is asked for, require the engine to
# say it actually created the feature — and catch the silent RR->SR downgrade, which is
# only a WARN_PRINT (dlss.cpp:984), so a run can measure Super Resolution while the
# label on the report says Ray Reconstruction.
check_present() { # check_present <label> <pattern>
if grep -a -q -- "$2" "$LOG"; then
echo " ok $1"
# LAST match, not first: the DLSS context is recreated when the window resizes, so
# the first "Context created" line reports a resolution that was not measured.
grep -a -- "$2" "$LOG" | tail -1 | sed 's/^/ /'
else
echo " FAIL $1 (expected \"$2\" in the log)"
fail=1
fi
}
if [ "$DLSS_REQUESTED" != "off" ]; then
check_present "NGX initialised" "\[NGX DLSS\] Initialized successfully"
check_present "render scale in effect" "\[NGX DLSS\] Context created:"
case "$DLSS_REQUESTED" in
rr_*)
check_present "DLSS-RR feature created" "DLSS-RR feature created successfully"
check_absent "no silent RR->SR downgrade" "falling back to DLSS-SR"
;;
sr_*)
check_present "DLSS-SR feature created" "DLSS-SR feature created successfully"
;;
esac
check_absent "no DLSS evaluate failure" "evaluate failed"
fi
if grep -a -q "UNSETTLED" "$LOG"; then
echo " FAIL settle gate reached steady state"
fail=1
else
echo " ok settle gate reached steady state"
fi
echo ""
echo " gpu before : $GPU_BEFORE (idle, before launch)"
echo " gpu peak : $GPU_PEAK (hottest sample DURING the run — compare like with like)"
echo ""
grep -a -E "^\s+(p50|p95|p99|avg)\s+:" "$LOG" | sed 's/^/ /'
grep -a -E "^\s+(window size|viewport rect|render target|min clearance)\s+:" "$LOG" | sed 's/^/ /'
grep -a "\[PT-BENCH\] report:" "$LOG" | sed 's/^/ /'
echo " log: $LOG"
# The engine segfaults on shutdown (hyper-games #11 §7.7) AFTER the report is written,
# so its exit status says nothing about the run. Judge the run by the log.
if grep -a -q "ABORT — render target is" "$LOG"; then
echo ""
echo " >>> NO USABLE WINDOW — nothing was measured. Give the window real geometry"
echo " (i3: float it / focus its tab) and re-run."
exit 3
fi
if [ "$fail" -ne 0 ]; then
echo ""
echo " >>> at least one validity check failed — treat the numbers above as suspect"
exit 2
fi
exit 0