test: add the fake gamescope producer and the end-to-end scripts

fake_gamescope mirrors gamescope 3.16.28's src/pipewire.cpp closely enough to
exercise the requested_size negotiation, MemFd buffers and CORRUPTED frames.
e2e-fake.sh runs it against the client and the mock in the current PipeWire
session; nested-gamescope.sh does the same with a real nested gamescope and
compares the load with the old gstreamer pipeline.
This commit is contained in:
2026-09-12 10:10:53 +03:00
parent 1aa7d802eb
commit e2b1c2983d
3 changed files with 1102 additions and 0 deletions
+214
View File
@@ -0,0 +1,214 @@
#!/usr/bin/env bash
# End-to-end check in the current PipeWire session (spec 9.3):
# examples/fake_gamescope + gamescope-hyperhdr + examples/mock_hyperhdr.
#
# The fake node gets a unique name, so a real gamescope node is never touched.
# Requires: pw-link, pw-dump, jq; gst-launch-1.0 with pipewiresink for scenario 4.
set -uo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root"
cargo build --release --bin gamescope-hyperhdr --example fake_gamescope --example mock_hyperhdr || exit 1
bin="$root/target/release"
logs="$(mktemp -d /tmp/gshdr-e2e.XXXXXX)"
name="gamescope-e2e-$$"
client_node="gamescope-hyperhdr"
port=$((20000 + RANDOM % 20000))
failures=0
mock_pid="" fake_pid="" client_pid="" gst_pid=""
pass() { printf ' \033[32mPASS\033[0m %s\n' "$*"; }
fail() { printf ' \033[31mFAIL\033[0m %s\n' "$*"; failures=$((failures + 1)); }
skip() { printf ' \033[33mSKIP\033[0m %s\n' "$*"; }
check() {
local desc=$1
shift
if "$@"; then pass "$desc"; else fail "$desc"; fi
}
# wait_pid PID TIMEOUT_S: returns the exit status, or kills the process and returns 124.
wait_pid() {
local pid=$1 end=$((SECONDS + $2))
while [[ -e /proc/$pid && "$(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null)" != Z ]]; do
if ((SECONDS >= end)); then
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
return 124
fi
sleep 0.05
done
wait "$pid" 2>/dev/null
}
cleanup() {
for pid in $client_pid $fake_pid $gst_pid $mock_pid; do
kill "$pid" 2>/dev/null
wait_pid "$pid" 3
done
echo "logs: $logs"
}
trap cleanup EXIT
# wait_count FILE REGEX MIN_COUNT TIMEOUT_S [FROM_LINE]
wait_count() {
local file=$1 re=$2 min=$3 from=${5:-1} end=$((SECONDS + $4))
while :; do
local n
n=$(tail -n "+$from" "$file" 2>/dev/null | grep -Ec -- "$re")
((n >= min)) && return 0
((SECONDS >= end)) && return 1
sleep 0.2
done
}
wait_log() { wait_count "$1" "$2" 1 "${3:-10}" "${4:-1}"; }
next_line() { echo $(($(wc -l <"$1") + 1)); }
absent_since() { ! tail -n "+$3" "$1" | grep -Eq -- "$2"; }
start_mock() {
"$bin/examples/mock_hyperhdr" --listen "127.0.0.1:$port" --timeout-secs 5 >>"$logs/mock.log" 2>&1 &
mock_pid=$!
}
start_fake() {
echo "--- start $*" >>"$logs/fake.log"
"$bin/examples/fake_gamescope" --name "$name" "$@" >>"$logs/fake.log" 2>&1 &
fake_pid=$!
}
stop_fake() {
kill "$fake_pid" 2>/dev/null
wait_pid "$fake_pid" 3
fake_pid=""
}
start_client() {
local log=$1
shift
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" --target "$name" "$@" >"$log" 2>&1 &
client_pid=$!
}
stop_client() {
kill -TERM "$client_pid" 2>/dev/null
wait_pid "$client_pid" 3
client_pid=""
}
# "<output port> -> <input port>" for every link.
links() {
pw-link -l 2>/dev/null | awk '/^[^ \t]/ { port = $0 } /\|->/ { sub(/^[ \t]*\|->[ \t]*/, ""); print port " -> " $0 }'
}
linked_to_fake() { links | grep -q "^$name:.* -> $client_node:"; }
foreign_links() { links | grep " -> $client_node:" | grep -v "^$name:"; }
node_exists() {
pw-dump 2>/dev/null | jq -e --arg n "$1" \
'any(.[]; .type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n)' >/dev/null
}
# Negotiated Format of the client's ports, e.g. "BGRx 320x180".
client_port_format() {
pw-dump 2>/dev/null | jq -r --arg n "$client_node" '
[.[] | select(.type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n) | .id] as $ids
| .[] | select(.type == "PipeWire:Interface:Port" and (.info.props["node.id"] as $id | $ids | index($id)))
| .info.params.Format[]? | "\(.format) \(.size.width)x\(.size.height)"'
}
echo "== setup (fake node '$name', mock on port $port, logs in $logs)"
start_mock
wait_log "$logs/mock.log" "listening on" 5 || { fail "mock did not start"; exit 1; }
start_client "$logs/client.log"
wait_log "$logs/client.log" "waiting for a '$name'" 5 || { fail "client did not start"; exit 1; }
start_fake --width 3840 --height 2160 --fps 60
echo "== 1. requested_size renegotiation"
check "1st Format event is the output size 3840x2160" wait_log "$logs/client.log" 'format: BGRx 3840x2160.*event=1'
check "2nd Format event is 320x180" wait_log "$logs/client.log" 'format: BGRx 320x180.*event=2'
check "frames arrive as 320x180" wait_log "$logs/client.log" 'receiving frames: 320x180 .*sending 320x180'
check "registered with priority 150" wait_log "$logs/client.log" 'registered with priority 150'
check "mock receives 320x180 RGB" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true'
echo "== 2. graph"
sleep 1
links >"$logs/links.txt"
client_port_format >"$logs/format.txt"
check "client is linked to the fake node" linked_to_fake
check "nothing else is linked to the client" test -z "$(foreign_links)"
check "pw-dump: negotiated $(tr '\n' ' ' <"$logs/format.txt")" grep -q "BGRx 320x180" "$logs/format.txt"
echo "== 3. source disappears and returns"
mock_mark=$(next_line "$logs/mock.log")
client_mark=$(next_line "$logs/client.log")
stop_fake
check "client sends Clear when the node disappears" wait_log "$logs/client.log" 'source gone: sent Clear' 5 "$client_mark"
check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 5 "$mock_mark"
check "mock sees the client close the connection" wait_log "$logs/mock.log" 'Disconnected\(ClosedByClient\)' 5 "$mock_mark"
sleep 1
mock_mark=$(next_line "$logs/mock.log")
start_fake --width 3840 --height 2160 --fps 60
check "client re-registers without a restart" wait_log "$logs/mock.log" 'Register \{ origin: "gamescope", priority: 150 \}' 10 "$mock_mark"
check "frames flow again" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark"
echo "== 4. foreign Video/Source is ignored"
client_mark=$(next_line "$logs/client.log")
stop_fake
wait_log "$logs/client.log" 'destroying stream' 5 "$client_mark"
client_mark=$(next_line "$logs/client.log")
if command -v gst-launch-1.0 >/dev/null; then
foreign="e2e-foreign-$$"
gst-launch-1.0 -q videotestsrc is-live=true \
! video/x-raw,format=BGRx,width=320,height=180,framerate=30/1 \
! pipewiresink mode=provide "stream-properties=props,media.class=Video/Source,node.name=$foreign" \
>"$logs/gst.log" 2>&1 &
gst_pid=$!
sleep 3
if node_exists "$foreign"; then
check "client does not create a stream" absent_since "$logs/client.log" 'stream created' "$client_mark"
check "client node does not exist" test "$(node_exists "$client_node" && echo yes)" != yes
check "nothing is linked to the client" test -z "$(links | grep " -> $client_node:")"
else
skip "pipewiresink did not create node '$foreign'"
fi
kill "$gst_pid" 2>/dev/null
wait_pid "$gst_pid" 3
gst_pid=""
else
skip "gst-launch-1.0 not installed"
fi
echo "== 5. SIGTERM"
mock_mark=$(next_line "$logs/mock.log")
start_fake --width 3840 --height 2160 --fps 60
wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark"
started=$(date +%s%N)
kill -TERM "$client_pid"
wait_pid "$client_pid" 3
status=$?
elapsed_ms=$((($(date +%s%N) - started) / 1000000))
client_pid=""
check "exit code 0 (got $status)" test "$status" -eq 0
check "exits within 1 s (${elapsed_ms} ms)" test "$elapsed_ms" -lt 1000
check "sends Clear on shutdown" grep -q 'shutting down: sent Clear' "$logs/client.log"
check "mock gets Clear{150}" wait_log "$logs/mock.log" 'Clear \{ priority: 150 \}' 2 "$mock_mark"
echo "== 6. --supersample 2"
mock_mark=$(next_line "$logs/mock.log")
start_client "$logs/client-supersample.log" --supersample 2
check "requests 640x360" wait_log "$logs/client-supersample.log" 'format: BGRx 640x360.*requested_size 640x360'
check "downscales 640x360 to 320x180" wait_log "$logs/client-supersample.log" 'receiving frames: 640x360 .*sending 320x180'
check "mock receives 320x180" wait_log "$logs/mock.log" 'image 320x180 len=172800 size_ok=true' 10 "$mock_mark"
stop_client
echo "== 7. static picture: keepalive holds the connection"
stop_fake
start_fake --width 3840 --height 2160 --fps 60 --static
mock_mark=$(next_line "$logs/mock.log")
start_client "$logs/client-static.log"
wait_log "$logs/mock.log" 'image 320x180' 10 "$mock_mark"
sleep 8
check "no idle timeout on the server (5 s)" absent_since "$logs/mock.log" 'Disconnected' "$mock_mark"
check "keepalive images keep arriving" wait_count "$logs/mock.log" 'images in' 3 1 "$mock_mark"
stop_client
echo
if ((failures == 0)); then
echo "all scenarios passed"
else
echo "$failures check(s) failed"
fi
exit $((failures > 0))
+148
View File
@@ -0,0 +1,148 @@
#!/usr/bin/env bash
# Spec 9.4.1 / 9.4.3 with a real gamescope: a nested headless gamescope running vkcube,
# gamescope-hyperhdr and the mock HyperHDR, plus a load comparison with the old gst pipeline
# (pipewiresrc → videorate → videoscale → videoconvert, into fakesink instead of v4l2loopback).
#
# Environment: W, H (output size, default 3840x2160), REFRESH (60), SECS (measurement, 10),
# CLIENT_ARGS (extra client flags, e.g. "--supersample 2").
set -uo pipefail
root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
cd "$root"
cargo build --release --bin gamescope-hyperhdr --example mock_hyperhdr || exit 1
W=${W:-3840} H=${H:-2160} REFRESH=${REFRESH:-60} SECS=${SECS:-10}
read -r -a client_args <<<"${CLIENT_ARGS:-}"
bin="$root/target/release"
logs="$(mktemp -d /tmp/gshdr-nested.XXXXXX)"
port=$((20000 + RANDOM % 20000))
mock_pid="" gs_pid="" client_pid="" gst_pid=""
node_exists() {
pw-dump 2>/dev/null | jq -e --arg n "$1" \
'any(.[]; .type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n)' >/dev/null
}
if node_exists gamescope; then
echo "a 'gamescope' node already exists (gamescope-tv running?); stop it first" >&2
exit 1
fi
wait_pid() {
local pid=$1 end=$((SECONDS + $2))
while [[ -e /proc/$pid && "$(awk '{ print $3 }' "/proc/$pid/stat" 2>/dev/null)" != Z ]]; do
if ((SECONDS >= end)); then
kill -9 "$pid" 2>/dev/null
wait "$pid" 2>/dev/null
return 124
fi
sleep 0.02
done
wait "$pid" 2>/dev/null
}
cleanup() {
for pid in $client_pid $gst_pid $gs_pid $mock_pid; do
kill "$pid" 2>/dev/null
wait_pid "$pid" 5
done
echo "logs: $logs"
}
trap cleanup EXIT
wait_log() {
local end=$((SECONDS + $3))
until grep -Eq -- "$2" "$1" 2>/dev/null; do
((SECONDS >= end)) && return 1
sleep 0.1
done
}
links() {
pw-link -l 2>/dev/null | awk '/^[^ \t]/ { port = $0 } /\|->/ { sub(/^[ \t]*\|->[ \t]*/, ""); print port " -> " $0 }'
}
port_format() {
pw-dump 2>/dev/null | jq -r --arg n "$1" '
[.[] | select(.type == "PipeWire:Interface:Node" and .info.props["node.name"] == $n) | .id] as $ids
| .[] | select(.type == "PipeWire:Interface:Port" and (.info.props["node.id"] as $id | $ids | index($id)))
| .info.params.Format[]? | "\(.format) \(.size.width)x\(.size.height)"'
}
cpu_ticks() { awk '{ print $14 + $15 }' "/proc/$1/stat"; }
rss_kib() { awk '/^VmRSS:/ { print $2 }' "/proc/$1/status"; }
# measure NAME=PID...: CPU (% of one core) and RSS over SECS seconds
measure() {
local hz item t0 t1
hz=$(getconf CLK_TCK)
local -A before
for item in "$@"; do before[$item]=$(cpu_ticks "${item#*=}"); done
t0=$(date +%s%N)
sleep "$SECS"
t1=$(date +%s%N)
for item in "$@"; do
local pid=${item#*=}
awk -v ticks="$(($(cpu_ticks "$pid") - before[$item]))" -v hz="$hz" -v ns="$((t1 - t0))" \
-v name="${item%%=*}" -v rss="$(rss_kib "$pid")" \
'BEGIN { printf " %-20s CPU %6.2f %% RSS %6.1f MB\n", name, ticks / hz / (ns / 1e9) * 100, rss / 1024 }'
done
}
"$bin/examples/mock_hyperhdr" --listen "127.0.0.1:$port" >"$logs/mock.log" 2>&1 &
mock_pid=$!
echo "== nested gamescope ${W}x${H}@${REFRESH} (headless, vkcube)"
gamescope --backend headless -W "$W" -H "$H" -r "$REFRESH" -- vkcube >"$logs/gamescope.log" 2>&1 &
gs_pid=$!
for _ in $(seq 50); do node_exists gamescope && break; sleep 0.2; done
node_exists gamescope || { echo "gamescope node did not appear, see $logs/gamescope.log"; exit 1; }
sleep 2
echo "== gamescope-hyperhdr ${client_args[*]}"
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" "${client_args[@]}" >"$logs/client.log" 2>&1 &
client_pid=$!
wait_log "$logs/client.log" 'receiving frames' 15 || echo " no frames within 15 s"
wait_log "$logs/client.log" 'registered with priority' 5
grep -E 'format:|receiving frames|registered|warn|WARN' "$logs/client.log" | sed 's/^[^ ]* */ /'
echo " links: $(links | grep ' -> gamescope-hyperhdr:' | tr '\n' ' ')"
echo " format: $(port_format gamescope-hyperhdr | tr '\n' ' ')"
echo " load over $SECS s:"
measure gamescope="$gs_pid" gamescope-hyperhdr="$client_pid"
echo " mock: $(grep 'images in' "$logs/mock.log" | tail -1 | sed 's/^.*INFO //')"
started=$(date +%s%N)
kill -TERM "$client_pid"
wait_pid "$client_pid" 3
status=$?
client_pid=""
echo " SIGTERM: exit $status after $((($(date +%s%N) - started) / 1000000)) ms, $(grep -c 'Clear { priority' "$logs/mock.log") Clear(s) at the mock"
if command -v gst-launch-1.0 >/dev/null; then
echo "== old pipeline: pipewiresrc → videorate 30 → videoscale 320x180 → videoconvert YUY2 → fakesink"
sleep 2
gst-launch-1.0 -q pipewiresrc target-object=gamescope do-timestamp=true keepalive-time=200 \
! videorate ! video/x-raw,framerate=30/1 \
! videoscale ! video/x-raw,width=320,height=180 \
! videoconvert ! video/x-raw,format=YUY2 \
! fakesink sync=false >"$logs/gst.log" 2>&1 &
gst_pid=$!
sleep 4
echo " format: $(links | grep -oE '[^ ]+ -> gst-launch[^ ]*' | head -1) $(port_format gst-launch-1.0 | tr '\n' ' ')"
echo " load over $SECS s:"
measure gamescope="$gs_pid" gst-launch-1.0="$gst_pid"
kill "$gst_pid"
wait_pid "$gst_pid" 3
gst_pid=""
fi
echo "== gamescope stops → Clear"
"$bin/gamescope-hyperhdr" --host 127.0.0.1 --port "$port" "${client_args[@]}" >"$logs/client2.log" 2>&1 &
client_pid=$!
wait_log "$logs/client2.log" 'registered with priority' 15
clears_before=$(grep -c 'Clear { priority' "$logs/mock.log")
started=$(date +%s%N)
kill -TERM "$gs_pid"
until (($(grep -c 'Clear { priority' "$logs/mock.log") > clears_before)); do
(($(date +%s%N) - started > 10000000000)) && break
sleep 0.01
done
echo " Clear at the mock $((($(date +%s%N) - started) / 1000000)) ms after SIGTERM to gamescope"
wait_pid "$gs_pid" 10
gs_pid=""
sleep 1
grep -E 'source node removed|destroying stream|source gone|ERROR' "$logs/client2.log" | sed 's/^[^ ]* */ /'