Compare commits

...

1 Commits

Author SHA1 Message Date
sim
e45a56ad83 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>
2026-09-18 07:17:36 +02:00
9 changed files with 580 additions and 0 deletions

View File

@@ -395,3 +395,15 @@ non-test code does not use tokio, so moving it to `dev-dependencies` is sound an
the `examples/` targets keep compiling. **Claim survives** — recorded because a the `examples/` targets keep compiling. **Claim survives** — recorded because a
survived challenge is stronger than an unchallenged one, not because it changed survived challenge is stronger than an unchallenged one, not because it changed
anything. anything.
---
## Superseded in part — see `HANDOFF-2026-09-18.md`
Two things in this document have since stopped being true:
* **A plain `git clone` works again** (measured 2026-09-17: 56 s, 114 MB, `main` plus tags). The
`--filter=blob:none` advice above was about the branch that carried 545 MB of game-content history,
and that branch was deleted in the consolidation cleanup.
* The baseline numbers quoted here are two baselines old. The current one, with all three corpora
present, is in `CLAUDE.md` and in `HANDOFF-2026-09-18.md`.

View File

@@ -0,0 +1,108 @@
# Hand-off — 2026-09-18, from the second desktop back to the original machine
Written for whoever resumes on `fabi-Hyrican-PC`. It covers what changed between 2026-09-16 and
2026-09-18 on `fabi-MS-7C37`, what is on the server, and what cannot travel through git.
Method and rules are unchanged: `docs/agents/PROTOCOL.md`. The cold-start doc is still
`docs/agents/HANDOFF-2026-09-06.md`, with the corrections at the end of this file.
## Where the work stands
| | |
|---|---|
| `main` | `e732557` |
| baseline, all three corpora present, measured 2026-09-17 in `docker/ci/run` | **45 suites / 377 passed / 0 failed / 14 ignored** |
| open PRs | **#53** (closes #16), **#54** (closes #15) — both `state/needs-human`, neither merged |
| open issues | #3, #4 (F2 audio gains) · #6, #7 (re-propose the port work) · #9, #25 (F6 residue) · #28 (input) · #49, #50 (decisions) · #51 (loop briefs) · #52 (kanji-only strings) |
| fork | issue **#1** — the branch probe exists only on `auto/canary-instrumentation-snapshot-2026-07-28` |
Branches on `fabi/Sylpheed`: `main`, `fix/corpus-mounts-and-paths` (#53), `fix/ci-pin-toolchain` (#54),
`recover/options-menu`, `recover/port-f5-f6`.
## What changed since 2026-09-16
**The consolidation closed.** PRs #46#48 merged; 34 server branches deleted; about 55 GB freed
locally. `Sylpheed` and the `Xenia-Canary` fork are the only live repos — see
`docs/agents/CONSOLIDATION.md`.
**Work from a deleted branch was recovered.** `auto/port-p6-audio` (tip `0148cb8`, 366 commits, never
in a PR) held what issues #6 and #7 ask to re-propose. It is back as two snapshot commits parented on
the branch's fork point `e53d687`:
* `recover/port-f5-f6` — all 84 files the branch changed, i.e. `0148cb8`'s tree minus the 854 exported
game assets it carried. Verified to differ from the original tip by exactly those assets.
* `recover/options-menu` — the nine files of the 2026-09-03 OPTIONS commits. A review slice, not a
buildable tree: the two efforts interleaved commit by commit and do not separate by file.
The 366 original commits were **not** kept — reachable history must not carry game assets. They still
exist unreferenced in the server's object store, so `0148cb8` can be fetched by SHA for as long as the
server keeps it. Both issues carry the details.
**The tracker was brought in line with reality.** #8 closed (its corpus is on `main`), #26 closed as
obsolete (the container tooling is parked for a from-scratch redo and still names the archived
`Syplheed-Reborn`), stale `state/*` labels dropped from closed issues, and four decisions that had been
living in session notes were filed: #49 (screenshots in public repos), #50 (require CI before merge),
#51 (loop briefs, from sylph-pi's note on #38), #52 (kanji-only Shift_JIS strings).
**The corpus control got honest** (#53): one `disc_root()`, no machine-specific fallbacks, and
`docker/ci/run` mounts `$SYLPHEED_RES3D` and `$SYLPHEED_ISO` as well as the disc. Before that an
in-container run silently sat out two corpora while looking like a full one.
**The CI toolchain is pinned** (#54): `dtolnay/rust-toolchain@1.98.1` in all three jobs, matching the
version `docker/ci` already pins, so a local clippy run is a true stand-in for CI.
**The gated launchers are in the repo now**`tools/run-canary-safe.sh` (Wine),
`tools/run-canary-native-safe.sh` (native), `tools/run-canary-native.sh` (interactive, hardware Vulkan),
plus `asound-null.conf`, `diagnose-freeze.sh`, `live-guest-state.sh`, `heaptrack-wrap.sh`. They used to
live in the workspace root, outside git, hardcoding one machine's absolute paths. They now derive the
workspace from their own location and honour `$SYLPHEED_ISO` and `$CANARY_BIN`. Both safe launchers were
run from their new location before this was committed: the native one reached content in 20 s
(`VERDICT: HEALTHY`), the Wine one ran 25 s and logged 4 `ADV.wmv` hits.
## What cannot travel through git
Ask the outgoing machine for these; none of them belong in a repository.
| what | where it was | note |
|---|---|---|
| agent memory | `~/.claude/projects/-home-fabi-RE---Project-Sylpheed/memory/` | 11 files + `MEMORY.md`, ~48 KB. The project's accumulated feedback and reference notes. |
| workspace instructions | `CLAUDE.md`, `README.md` at the workspace root | `CLAUDE.md` is what every session reads first. |
| Claude settings, skills, agent | `.claude/` at the workspace root | `settings.json` (the Stop hooks), `settings.local.json`, `skills/sylph-dis`, `skills/sylph-canary`, `agents/sylph-static.md`. |
| corpus paths | `Sylpheed/.env` | Three variables: `SYLPHEED_DISC`, `SYLPHEED_RES3D`, `SYLPHEED_ISO`. Rewrite for the machine rather than copy. |
| game data | the ISO, the extract, the flat `.pe`, the `.xex.json`, `sylph_extract` → the extract | Never in git. |
| the Gitea token | `~/.sylph-gitea-token`, chmod 600 | Prefer minting a fresh one on the other machine to moving it. |
| Canary builds | `xenia-canary/build-cross/…/Debug` (Wine, 190 MB) and `xenia-canary-native/build` | **The Wine build is the only binary that accepts `--audit_61_branch_probe_pcs`** — see fork #1. Rebuilding from `sylpheed-re` loses the probe. |
| official Canary oracles | `~/xenia_canary_windows/xenia_canary.exe` (`b86414957`), `~/Downloads/xenia_canary_linux/xenia_canary` (`de10b9ef9`) | The builds the game was played on; use them to bisect a regression in the fork. |
Rebuildable, do not copy: `sylpheed.db` (~2.5 min, command in `CLAUDE.md`), `target/`, the
`sylph-ci:local` image and its volumes, `.trunk-bin`.
## Traps this machine paid for
* **The branch probe is not on the fork's default branch.** `run-canary-safe.sh` and the `/sylph-canary`
skill pass `--audit_61_branch_probe_pcs`, defined only on `auto/canary-instrumentation-snapshot-2026-07-28`
(`30d05ee97`). `sylpheed-re` is 232 commits ahead of that branch, so porting it is a cherry-pick with
a build check, not a merge. Fork issue #1.
* **One suite dominates the test wall time.** `twin_pairs_do_not_share_a_buffer` takes ~19 of the ~36
minutes. Budget for it; it is not a hang.
* **Nothing requires CI to pass before a merge** (#50), and CI has no disc, so its green is parser-only.
* **The tally cannot tell you what it verified.** Read `target/sylpheed-corpus-report.txt` (#16).
* `git gc` on this machine will eventually drop the unreferenced `0148cb8`; the server keeps its own
copy independently.
## First moves on the other machine
1. Clone, set `.env`, then `docker build -t sylph-ci:local docker/ci` (~6 min) and re-run the baseline
through `docker/ci/run` with all three corpora. Compare against 45 / 377 / 0 / 14.
2. Review #53 and #54 — both were measured here, neither was merged, because merging is the human's.
3. Answer #49 and #50; both block nothing technically and both shape what comes next.
4. If dynamic RE is next, resolve fork #1 before rebuilding Canary, or carry the Wine build across.
## Corrections to `HANDOFF-2026-09-06.md`
* **§ "clone" — a plain `git clone` works again.** Measured 2026-09-17: 56 s, 114 MB, `main` plus tags.
The `--filter=blob:none` advice was for the branch carrying 545 MB of game-content history, which was
deleted in the consolidation cleanup.
* The `~/.sylph-*` agent credentials and the agent images it describes exist on the original machine
only; the second desktop never had them, and the agent and container tooling is parked for a
from-scratch redo.

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)"