chore: hand the workspace over — the gated launchers, and the state of play

Resuming happens on the other machine, so anything that lived only in this
workspace either comes into the repo or gets named as something to carry.

**The launchers come in.** `run-canary-safe.sh` (Wine) and
`run-canary-native-safe.sh` (native) are the ONLY sanctioned way to start Canary
from the editor — they force software Vulkan and refuse to launch while a
hardware Vulkan device is visible, because Canary on the AMD GPU takes VS Code
with it. They sat in the workspace root, outside git, hardcoding one machine's
absolute paths, which is exactly why they could not be checked in. They now
derive the workspace from their own location and honour `$SYLPHEED_ISO` and
`$CANARY_BIN`, so the layout is a default rather than a requirement. Same for
`run-canary-native.sh` (interactive, hardware Vulkan — not from the IDE),
`diagnose-freeze.sh`, `live-guest-state.sh`, `heaptrack-wrap.sh` and the
`asound-null.conf` the native launcher needs beside it.

Both safe launchers were run from `tools/` before this commit: the native one
reached content in 20 s (`VERDICT: HEALTHY`, title + 25,398 XMA lines, no
faults), the Wine one ran 25 s and logged 4 `ADV.wmv` hits.

**`docs/agents/HANDOFF-2026-09-18.md`** records what changed since 2026-09-16,
what is on the server, what cannot travel through git, and the traps this machine
paid for — chiefly that the guest-PC branch probe exists only on the fork's
snapshot branch, so a rebuild from `sylpheed-re` silently loses it.

**`HANDOFF-2026-09-06.md`** gets a correction note rather than an edit: a plain
clone works again now that the 545 MB branch is gone, and its baselines are two
baselines old.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
sim
2026-09-18 07:17:36 +02:00
parent 4efd9f5936
commit e45a56ad83
9 changed files with 580 additions and 0 deletions

16
tools/asound-null.conf Normal file
View File

@@ -0,0 +1,16 @@
# ALSA config for HEADLESS Xenia-Canary boot-check runs (run-canary-native-safe.sh).
#
# Why: the ALSA apu driver opens PCM "default". Passing --apu=nop instead (to get
# silence) leaves the guest's XAudio render client NULL, and Project Sylpheed then
# dereferences null+0x3C at guest PC 0x824D7C40 -> a FALSE boot crash that does NOT
# happen in interactive runs with real audio. So we must keep the ALSA apu ALIVE
# (guest gets a valid client) but route its output to the null device: fully
# silent, snd_pcm_open("default") still succeeds, no false crash.
#
# Point ALSA at this file via env: ALSA_CONFIG_PATH=<path>/asound-null.conf
pcm.!default {
type null
}
ctl.!default {
type null
}

58
tools/diagnose-freeze.sh Executable file
View File

@@ -0,0 +1,58 @@
#!/usr/bin/env bash
# Freeze autopsy for a LIVE (hung) Xenia-Canary process.
#
# When the game or the emulator freezes, DO NOT kill it. Run this instead: it
# attaches gdb to the running process and dumps a backtrace of every thread,
# which names exactly who is stuck and on what (the guest audio callback, a
# kernel lock, the GPU present, an XMA context lock, ...).
#
# The Release binary is NOT stripped, so we get real function names.
#
# Usage: ./diagnose-freeze.sh [output_file]
# Safe: read-only. gdb detaches afterwards and the process keeps running, so
# you can dump twice and diff -- if two dumps 10s apart are identical, it is a
# true deadlock, not slow progress.
set -u
OUT="${1:-/tmp/canary_freeze_$(date +%H%M%S).txt}"
PID=$(pgrep -x xenia_canary | head -1)
if [ -z "$PID" ]; then
echo "No running xenia_canary process found (is it still up? don't kill it!)."
exit 1
fi
command -v gdb >/dev/null || { echo "ABORT: gdb not installed."; exit 4; }
echo "Attaching to xenia_canary (pid $PID) -- read-only, it keeps running."
{
echo "=== xenia_canary freeze autopsy pid=$PID $(date) ==="
echo
echo "--- /proc/$PID/status ---"
grep -E "^(State|Threads)" "/proc/$PID/status" 2>/dev/null
echo
echo "--- per-thread kernel wait channel (who is blocked, cheap) ---"
for t in /proc/"$PID"/task/*; do
tid=$(basename "$t")
printf " tid %-7s state=%-2s wchan=%-24s %s\n" \
"$tid" \
"$(awk '{print $3}' "$t/stat" 2>/dev/null)" \
"$(cat "$t/wchan" 2>/dev/null || echo '-')" \
"$(cat "$t/comm" 2>/dev/null)"
done
echo
echo "--- all thread backtraces (gdb) ---"
} > "$OUT"
gdb -p "$PID" -batch \
-ex "set pagination off" \
-ex "set confirm off" \
-ex "thread apply all bt" \
-ex "detach" 2>&1 | tee -a "$OUT" > /dev/null
echo "Wrote: $OUT"
echo
echo "--- threads that look blocked ---"
grep -E "^Thread |pthread_cond_wait|futex|__lll_lock|Wait|Acquire" "$OUT" | head -40
echo
echo "Full dump: $OUT (run again in ~10s and diff to confirm a true deadlock)"

14
tools/heaptrack-wrap.sh Executable file
View File

@@ -0,0 +1,14 @@
#!/usr/bin/env bash
# Wrapper so run-canary-native.sh runs xenia under heaptrack (host-malloc leak
# profiler). Usage:
# sudo apt install heaptrack # one-time
# CANARY_BIN="$PWD/tools/heaptrack-wrap.sh" tools/run-canary-native.sh
# Then: boot -> into a menu/READY ROOM (or a short mission) so RSS climbs a few
# hundred MB, then QUIT xenia normally (window close / menu quit) so heaptrack
# flushes its dump. Output: /tmp/xenia-heaptrack.*.zst (or .gz). Tell Claude and
# it will run heaptrack_print/analyze to name the leaking call stacks.
set -u
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$(cd "$HERE/../.." && pwd)"
REAL_BIN="${CANARY_REAL_BIN:-$WORKSPACE/xenia-canary-native/build/bin/Linux/Release/xenia_canary}"
exec heaptrack -o /tmp/xenia-heaptrack "$REAL_BIN" "$@"

72
tools/live-guest-state.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# Read the GUEST state of a live (hung) xenia_canary — who is the guest spinning
# on, and what is it waiting for.
#
# In JIT code the x64 backend keeps:
# rsi = PPCContext* rdi = guest membase
# (x64_emitter.cc: GetContextReg()=rsi, GetMembaseReg()=rdi)
# PPCContext offsets (computed against this build's header):
# r[0..31] @ +40 (8B each) ctr @ +296 lr @ +304
# thread_state @ +2704 virtual_membase @ +2712
#
# Needs ptrace: sudo sysctl -w kernel.yama.ptrace_scope=0 (restore with =1)
# Read-only: gdb detaches, the process keeps running.
#
# Usage: ./live-guest-state.sh ["Thread Name"] (default: Main XThread)
set -u
WANT="${1:-Main XThread}"
PID=$(pgrep -x xenia_canary | head -1)
[ -n "$PID" ] || { echo "no xenia_canary running"; exit 1; }
if [ "$(cat /proc/sys/kernel/yama/ptrace_scope 2>/dev/null)" != "0" ]; then
echo "ABORT: ptrace is locked (yama ptrace_scope != 0). Run once:"
echo " sudo sysctl -w kernel.yama.ptrace_scope=0"
exit 3
fi
RAW=$(mktemp /tmp/guest_state_XXXX.txt)
gdb -p "$PID" -batch \
-ex "set pagination off" -ex "set confirm off" \
-ex "thread find $WANT" \
-ex "thread apply all -ascending printf \"@@TH %d %s\\n\", \$_thread, \$_gthread" \
2>/dev/null | grep -E "Thread .* has name|@@TH" > "$RAW"
# gdb "thread find" prints e.g.: Thread 34 has target name 'Main XThread (F...'
GTH=$(grep -m1 "has .*name" "$RAW" | sed -E 's/.*Thread ([0-9]+) has.*/\1/')
[ -n "$GTH" ] || { echo "could not locate a thread named '$WANT'"; cat "$RAW"; exit 4; }
echo "gdb thread #$GTH == '$WANT' (pid $PID)"
gdb -p "$PID" -batch \
-ex "set pagination off" -ex "set confirm off" \
-ex "thread $GTH" \
-ex "echo \n=== host frame ===\n" \
-ex "printf \"host rip = %#lx\\n\", \$rip" \
-ex "bt 8" \
-ex "echo \n=== guest registers (PPCContext @ rsi) ===\n" \
-ex "set \$ctx = (unsigned long)\$rsi" \
-ex "printf \"ctx = %#lx\\n\", \$ctx" \
-ex "printf \"lr = %#lx\\n\", *(unsigned long*)(\$ctx+304)" \
-ex "printf \"ctr = %#lx\\n\", *(unsigned long*)(\$ctx+296)" \
-ex "printf \"r1(sp)= %#lx\\n\", *(unsigned long*)(\$ctx+40+8*1)" \
-ex "printf \"r3 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*3)" \
-ex "printf \"r4 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*4)" \
-ex "printf \"r5 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*5)" \
-ex "printf \"r6 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*6)" \
-ex "printf \"r7 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*7)" \
-ex "printf \"r8 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*8)" \
-ex "printf \"r9 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*9)" \
-ex "printf \"r10 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*10)" \
-ex "printf \"r11 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*11)" \
-ex "printf \"r12 = %#lx\\n\", *(unsigned long*)(\$ctx+40+8*12)" \
-ex "printf \"membase = %#lx\\n\", *(unsigned long*)(\$ctx+2712)" \
-ex "echo \n=== guest stack bytes @ r1 (BE; look for 82xxxxxx = code) ===\n" \
-ex "set \$mb = *(unsigned long*)(\$ctx+2712)" \
-ex "set \$sp = *(unsigned long*)(\$ctx+40+8*1)" \
-ex "x/128xb \$mb + \$sp" \
-ex "detach" 2>&1 | grep -vE "^\[|Reading symbols|no debugging symbols|Detaching"
rm -f "$RAW"
echo
echo "Guest code addresses look like 0x82xxxxxx — feed lr / stack hits to zq.py fn <pc>."

98
tools/run-canary-native-safe.sh Executable file
View File

@@ -0,0 +1,98 @@
#!/usr/bin/env bash
# SAFE launcher for the NATIVE Linux Xenia-Canary build on THIS box.
#
# Same safety contract as run-canary-safe.sh (the Wine/Windows variant): the AMD
# GPU + GPU-accelerated VS Code means a hardware-Vulkan render CRASHES VS Code.
# This wrapper forces software Vulkan (lavapipe/llvmpipe) IN THE SAME process and
# HARD-REFUSES to launch if any hardware Vulkan device is still visible.
#
# Difference vs run-canary-safe.sh: runs the native ELF directly (NO wine).
#
# Usage: tools/run-canary-native-safe.sh [seconds]
# env: CANARY_BIN override binary (default = worktree Release build)
# CANARY_EXTRA_ARGS space-separated extra cvars (values w/o spaces)
# Output: /tmp/canary_native.stdout (+ xenia.log in the binary dir). Prints rc + ADV.wmv hits.
set -u
# --- force software Vulkan, belt-and-suspenders across loader/driver variants ---
export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json
export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
export MESA_VK_DEVICE_SELECT=llvmpipe
export LIBGL_ALWAYS_SOFTWARE=1
# --- keep background runs SILENT *without* killing the audio system ---
# DO NOT use --apu=nop: with no audio system the guest's XAudio render client is
# NULL and Project Sylpheed derefs null+0x3C at PC 0x824D7C40 -> a FALSE boot
# crash that never happens in interactive runs. Instead keep the ALSA apu ALIVE
# and route PCM "default" to the null device (see asound-null.conf): silent, but
# snd_pcm_open succeeds so the guest is happy. --mute does NOT silence ALSA.
export SDL_AUDIODRIVER=dummy
export ALSA_CONFIG_PATH="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/asound-null.conf"
[ -f "$ALSA_CONFIG_PATH" ] || { echo "ABORT: asound-null.conf missing next to script"; exit 4; }
# Paths derive from where this script sits — see the note in run-canary-safe.sh.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$(cd "$HERE/../.." && pwd)"
BIN_DEFAULT="$WORKSPACE/xenia-canary-native/build/bin/Linux/Release/xenia_canary"
BIN_EXE="${CANARY_BIN:-$BIN_DEFAULT}"
ISO="${SYLPHEED_ISO:-$WORKSPACE/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso}"
[ -f "$ISO" ] || { echo "ABORT: no ISO at '$ISO' — set \$SYLPHEED_ISO"; exit 4; }
SECS="${1:-95}"
[ -x "$BIN_EXE" ] || { echo "ABORT: native binary not found/executable: $BIN_EXE"; exit 4; }
# --- HARD PRE-FLIGHT GATE: only proceed if Vulkan exposes software devices ONLY ---
devs="$(vulkaninfo --summary 2>/dev/null | grep -i deviceName || true)"
if echo "$devs" | grep -qiE 'radv|amd|radeon|nvidia|geforce|intel\b'; then
echo "ABORT(pre-flight): a HARDWARE Vulkan device is still visible -> refusing (would crash VS Code):"
echo "$devs"
exit 3
fi
if ! echo "$devs" | grep -qiE 'llvmpipe'; then
echo "ABORT(pre-flight): llvmpipe not visible; lavapipe ICD missing? devs=[$devs]"
exit 3
fi
echo "pre-flight OK: software-only Vulkan -> $devs"
# --- clean slate ---
pkill -x xenia_canary 2>/dev/null; pkill -x Xvfb 2>/dev/null; sleep 1
Xvfb :99 -screen 0 1280x720x24 -nolisten tcp >/tmp/xvfb.log 2>&1 &
XVFB=$!
sleep 2
export DISPLAY=:99
cd "$(dirname "$BIN_EXE")" || { echo "ABORT: bin dir missing"; kill "$XVFB" 2>/dev/null; exit 4; }
rm -f xenia.log
# Real ALSA apu (routed to null sink via ALSA_CONFIG_PATH) so the guest gets a
# valid render client; --mute belt-and-suspenders. NEVER add --apu=nop here.
args=(--log_level=3 --mute=true)
if [ -n "${CANARY_EXTRA_ARGS:-}" ]; then
read -ra EXTRA <<< "$CANARY_EXTRA_ARGS"
args+=("${EXTRA[@]}")
fi
echo "launch (native, sw-vulkan, SILENT null-ALSA, ${SECS}s): $(basename "$BIN_EXE") ${args[*]}"
# -k 5: xenia catches SIGTERM and can hang on shutdown; force-KILL 5s later.
timeout -k 5 "$SECS" "$BIN_EXE" "$ISO" "${args[@]}" >/tmp/canary_native.stdout 2>&1
rc=$?
pkill -x xenia_canary 2>/dev/null; kill "$XVFB" 2>/dev/null; pkill -x Xvfb 2>/dev/null
# --- trustworthy boot-health verdict (NOT log-line-count, NOT rc) ---
# A healthy boot reaches actual content: title loaded + XMA audio decoding + NO
# host-exception fault loop. rc=137 is EXPECTED (timeout -k kill), not a failure.
faults=$(grep -ac 'Access Violation' /tmp/canary_native.stdout 2>/dev/null)
gthrow=$(grep -ac 'GUEST-THROW' xenia.log 2>/dev/null)
title=$(grep -ac 'Title name: PROJECT SYLPHEED' xenia.log 2>/dev/null)
xma=$(grep -acE 'XmaContext|Processing context' xenia.log 2>/dev/null)
echo "rc=$rc logsize=$(wc -c < xenia.log 2>/dev/null)"
echo "boot-health: title=$title xma=$xma faults=$faults guest_throw=$gthrow"
if [ "$title" -ge 1 ] && [ "$xma" -ge 100 ] && [ "$faults" -eq 0 ] && [ "$gthrow" -eq 0 ]; then
echo "VERDICT: HEALTHY (reached content, no crash)"
elif [ "$gthrow" -ge 1 ]; then
echo "VERDICT: GUEST-THROW CRASH (the real bug) -- see GUEST-THROW lines in xenia.log"
elif [ "$faults" -ge 1 ]; then
echo "VERDICT: FAULT (host access-violation loop) -- crash PC:"
grep -aoE 'PC: 0x[0-9A-Fa-f]+' /tmp/canary_native.stdout | sort | uniq -c | head -3
else
echo "VERDICT: INCOMPLETE (did not reach content in ${SECS}s -- raise timeout?)"
fi

130
tools/run-canary-native.sh Executable file
View File

@@ -0,0 +1,130 @@
#!/usr/bin/env bash
# INTERACTIVE launcher for the NATIVE Linux Xenia-Canary build.
#
# Opens a REAL window on your desktop (display :0) and — unlike
# run-canary-native-safe.sh — uses HARDWARE Vulkan (the AMD GPU) and leaves
# audio ON. This is for hands-on play/testing, not headless tracer runs.
#
# ⚠ WARNING: this renders on the same AMD GPU that drives your VS Code / desktop.
# The historical "crashes VS Code" issue was vkd3d-proton (D3D12->Vulkan under
# Wine); the native build talks to Vulkan directly, so it MAY be fine — but if
# the desktop glitches or VS Code dies, fall back to run-canary-native-safe.sh
# (software Vulkan) or set CANARY_SOFTWARE=1 below.
#
# Usage: tools/run-canary-native.sh [seconds] (no arg => runs until you close it)
# env: CANARY_BIN override binary (default = worktree Release build)
# CANARY_SOFTWARE=1 force software Vulkan (lavapipe) even here
# CANARY_MUTE=1 start muted / no audio device (--apu=nop)
# CANARY_GPU=<substr> pick a specific Vulkan device by name (e.g. radv, 6800)
# CANARY_EXTRA_ARGS space-separated extra cvars
# DISPLAY target X display (default :0 = your screen)
# Output: /tmp/canary_native_interactive.stdout (+ xenia.log in the binary dir).
set -u
# Paths derive from where this script sits — see the note in run-canary-safe.sh.
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$(cd "$HERE/../.." && pwd)"
BIN_DEFAULT="$WORKSPACE/xenia-canary-native/build/bin/Linux/Release/xenia_canary"
BIN_EXE="${CANARY_BIN:-$BIN_DEFAULT}"
ISO="${SYLPHEED_ISO:-$WORKSPACE/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso}"
[ -f "$ISO" ] || { echo "ABORT: no ISO at '$ISO' — set \$SYLPHEED_ISO"; exit 4; }
SECS="${1:-0}" # 0 => no timeout (interactive)
export DISPLAY="${DISPLAY:-:0}"
[ -x "$BIN_EXE" ] || { echo "ABORT: native binary not found/executable: $BIN_EXE"; exit 4; }
# --- Vulkan device selection ---
if [ "${CANARY_SOFTWARE:-0}" = "1" ]; then
export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json
export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
export MESA_VK_DEVICE_SELECT=llvmpipe
echo "GPU: forced software Vulkan (lavapipe)."
else
# Hardware Vulkan. Optionally pin a device by substring (RADV AMD is default [0]).
[ -n "${CANARY_GPU:-}" ] && export MESA_VK_DEVICE_SELECT="$CANARY_GPU"
echo "GPU: HARDWARE Vulkan${CANARY_GPU:+ (pinned: $CANARY_GPU)}."
echo " Visible devices:"; vulkaninfo --summary 2>/dev/null | grep -i deviceName | sed 's/^/ /'
fi
# --- logging: keep it QUIET so kernel/APU debug spam doesn't starve the audio
# worker thread. At --log_level=3 (Debug) the d>/A> firehose (piped through
# tee) stalls the ALSA pipeline and mission audio dies permanently — the
# exact "mission-audio silence" that commit f10484834 fixes in code; the
# log flood defeats the keepalive. log_mask=13 = suppress Kernel|Cpu|Gpu.
# (guest_audio_flags defaults to 0 = Digital Stereo, the Linux-safe path.)
LOGLEVEL="${CANARY_LOGLEVEL:-1}"
LOGMASK="${CANARY_LOGMASK:-13}"
# --- audio ---
args=(--log_level="$LOGLEVEL" --log_mask="$LOGMASK")
if [ "${CANARY_MUTE:-0}" = "1" ]; then
export SDL_AUDIODRIVER=dummy
args+=(--mute=true --apu=nop)
echo "AUDIO: muted (apu=nop)."
else
echo "AUDIO: on (ALSA)."
fi
# --- audio RE: capture true XMA per-stream params (channels/rate/head bytes) to
# xenia.log as you play. Deduped (one line per unique sound); logs at Warning
# so it shows at the audio-safe log level without spam. Off unless requested.
if [ "${CANARY_XMA_PROBE:-0}" = "1" ]; then
args+=(--xma_param_probe=true)
echo "XMA-PARAM probe: ON (params captured to xenia.log)."
fi
# --- audio watchdog: when audio dies mid-mission and never returns, report
# WHICH pipeline stage stopped (guest callback blocked / no XMA decode /
# no frames submitted / host driver not writing). Near-silent while healthy.
if [ "${CANARY_AUDIO_WD:-0}" = "1" ]; then
args+=(--audio_watchdog=true)
echo "AUDIO watchdog: ON (logs 'AUDIO-WD ...' when audio dies)."
fi
# --- hang watchdog: if the guest stops presenting frames for N seconds, the
# emulator dumps every guest thread's registers + guest call stack to
# xenia.log by itself. No debugger, no ptrace, no need to keep the window
# open -- the freeze autopsy is already in the log.
if [ -n "${CANARY_HANG_WD:-}" ]; then
args+=(--hang_watchdog_secs="$CANARY_HANG_WD")
echo "HANG watchdog: ON (${CANARY_HANG_WD}s without a frame => guest dump to xenia.log)."
fi
if [ -n "${CANARY_EXTRA_ARGS:-}" ]; then
read -ra EXTRA <<< "$CANARY_EXTRA_ARGS"
args+=("${EXTRA[@]}")
fi
# --- CPU stress: the mid-mission audio death only shows up under host CPU load
# (the guest's audio callback misses its 5.33ms deadlines and the game tears
# its own audio client down). CANARY_STRESS=<n> spins n busy loops for the
# duration of the run so the bug reproduces on demand instead of by luck.
# They are killed when the run ends. Silent (no audio involvement).
STRESS_PIDS=()
stress_cleanup() {
if [ ${#STRESS_PIDS[@]} -gt 0 ]; then
kill "${STRESS_PIDS[@]}" 2>/dev/null
echo "stress: stopped ${#STRESS_PIDS[@]} load generators."
fi
}
trap stress_cleanup EXIT INT TERM
if [ -n "${CANARY_STRESS:-}" ] && [ "${CANARY_STRESS}" -gt 0 ] 2>/dev/null; then
for _i in $(seq 1 "$CANARY_STRESS"); do
# Pure userspace spin; no I/O, no audio, no privileges.
( while :; do :; done ) &
STRESS_PIDS+=($!)
done
echo "STRESS: ON -- ${CANARY_STRESS} busy loops competing for CPU (of $(nproc) cores)."
fi
pkill -x xenia_canary 2>/dev/null
cd "$(dirname "$BIN_EXE")" || { echo "ABORT: bin dir missing"; exit 4; }
rm -f xenia.log
echo "launch (interactive, DISPLAY=$DISPLAY): $(basename "$BIN_EXE") ${args[*]}"
if [ "$SECS" -gt 0 ] 2>/dev/null; then
timeout -k 5 "$SECS" "$BIN_EXE" "$ISO" "${args[@]}" 2>&1 | tee /tmp/canary_native_interactive.stdout
else
"$BIN_EXE" "$ISO" "${args[@]}" 2>&1 | tee /tmp/canary_native_interactive.stdout
fi
rc=${PIPESTATUS[0]}
echo "rc=$rc ADV.wmv=$(grep -ac 'ADV.wmv' xenia.log 2>/dev/null)"

72
tools/run-canary-safe.sh Executable file
View File

@@ -0,0 +1,72 @@
#!/usr/bin/env bash
# SAFE Xenia-Canary launcher for THIS box (shared AMD GPU + GPU-accelerated VS Code).
#
# Canary's vkd3d-proton renders D3D12->Vulkan; if it picks the AMD GPU it CRASHES
# VS Code. This wrapper forces software Vulkan (llvmpipe/lavapipe) IN THE SAME
# process as wine (env vars do NOT survive across separate shells!), and REFUSES
# to launch if any hardware Vulkan device is still visible (hard pre-flight gate).
#
# Usage: tools/run-canary-safe.sh [audit61_pcs_csv] [seconds]
# e.g. tools/run-canary-safe.sh 0x82507458,0x8250747c 95
# Output: /tmp/canary_video.stdout (+ xenia.log in the binary dir). Prints rc + ADV.wmv hit count.
set -u
# --- force software Vulkan, belt-and-suspenders across loader/driver variants ---
export VK_ICD_FILENAMES=/usr/share/vulkan/icd.d/lvp_icd.json
export VK_DRIVER_FILES=/usr/share/vulkan/icd.d/lvp_icd.json
export MESA_VK_DEVICE_SELECT=llvmpipe
export DXVK_FILTER_DEVICE_NAME=llvmpipe
export VKD3D_FILTER_DEVICE_NAME=llvmpipe
export LIBGL_ALWAYS_SOFTWARE=1
# Paths come from where this script sits — tools/ inside the repo, whose parent
# directory is the workspace holding the fork checkouts and the game data. Both
# are overridable, so a machine that lays things out differently sets the env
# var instead of editing the script. (This file used to hardcode one machine's
# absolute paths, which is why it could not be checked in.)
HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
WORKSPACE="$(cd "$HERE/../.." && pwd)"
BIN="${CANARY_WINE_BIN_DIR:-$WORKSPACE/xenia-canary/build-cross/bin/Windows/Debug}"
ISO="${SYLPHEED_ISO:-$WORKSPACE/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso}"
[ -f "$ISO" ] || { echo "ABORT: no ISO at '$ISO' — set \$SYLPHEED_ISO"; exit 4; }
PROBES="${1:-}"
SECS="${2:-95}"
# --- HARD PRE-FLIGHT GATE: only proceed if Vulkan exposes software devices ONLY ---
devs="$(vulkaninfo --summary 2>/dev/null | grep -i deviceName || true)"
if echo "$devs" | grep -qiE 'radv|amd|radeon|nvidia|geforce|intel\b'; then
echo "ABORT(pre-flight): a HARDWARE Vulkan device is still visible -> refusing (would crash VS Code):"
echo "$devs"
exit 3
fi
if ! echo "$devs" | grep -qiE 'llvmpipe'; then
echo "ABORT(pre-flight): llvmpipe not visible; lavapipe ICD missing? devs=[$devs]"
exit 3
fi
echo "pre-flight OK: software-only Vulkan -> $devs"
# --- clean slate ---
pkill -x xenia_canary_i2d.exe 2>/dev/null; wineserver -k 2>/dev/null; pkill -x Xvfb 2>/dev/null; sleep 1
Xvfb :99 -screen 0 1280x720x24 -nolisten tcp >/tmp/xvfb.log 2>&1 &
XVFB=$!
sleep 2
export DISPLAY=:99
cd "$BIN" || { echo "ABORT: bin dir missing"; kill "$XVFB" 2>/dev/null; exit 4; }
rm -f xenia.log
# Binary is overridable (default keeps the historical _i2d snapshot); extra
# cvars pass through via CANARY_EXTRA_ARGS (space-separated, values w/o spaces).
BIN_EXE="${CANARY_BIN:-xenia_canary_i2d.exe}"
args=(--log_level=3 --mute=true)
[ -n "$PROBES" ] && args+=("--audit_61_branch_probe_pcs=$PROBES")
if [ -n "${CANARY_EXTRA_ARGS:-}" ]; then
read -ra EXTRA <<< "$CANARY_EXTRA_ARGS"
args+=("${EXTRA[@]}")
fi
echo "launch (sw-vulkan, muted, ${SECS}s): wine $BIN_EXE ${args[*]}"
timeout "$SECS" wine "./$BIN_EXE" "$ISO" "${args[@]}" >/tmp/canary_video.stdout 2>&1
rc=$?
pkill -x xenia_canary_i2d.exe 2>/dev/null; wineserver -k 2>/dev/null; kill "$XVFB" 2>/dev/null; pkill -x Xvfb 2>/dev/null
echo "rc=$rc ADV.wmv=$(grep -ac 'ADV.wmv' xenia.log 2>/dev/null) logsize=$(wc -c < xenia.log 2>/dev/null)"