diff --git a/docs/agents/HANDOFF-2026-09-06.md b/docs/agents/HANDOFF-2026-09-06.md index 4a98ea61..b94bbdf0 100644 --- a/docs/agents/HANDOFF-2026-09-06.md +++ b/docs/agents/HANDOFF-2026-09-06.md @@ -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 survived challenge is stronger than an unchallenged one, not because it changed 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`. diff --git a/docs/agents/HANDOFF-2026-09-18.md b/docs/agents/HANDOFF-2026-09-18.md new file mode 100644 index 00000000..3d98f2ca --- /dev/null +++ b/docs/agents/HANDOFF-2026-09-18.md @@ -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. diff --git a/tools/asound-null.conf b/tools/asound-null.conf new file mode 100644 index 00000000..5435422e --- /dev/null +++ b/tools/asound-null.conf @@ -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=/asound-null.conf +pcm.!default { + type null +} +ctl.!default { + type null +} diff --git a/tools/diagnose-freeze.sh b/tools/diagnose-freeze.sh new file mode 100755 index 00000000..7daffd17 --- /dev/null +++ b/tools/diagnose-freeze.sh @@ -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)" diff --git a/tools/heaptrack-wrap.sh b/tools/heaptrack-wrap.sh new file mode 100755 index 00000000..0013ac3f --- /dev/null +++ b/tools/heaptrack-wrap.sh @@ -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" "$@" diff --git a/tools/live-guest-state.sh b/tools/live-guest-state.sh new file mode 100755 index 00000000..07929fdd --- /dev/null +++ b/tools/live-guest-state.sh @@ -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 ." diff --git a/tools/run-canary-native-safe.sh b/tools/run-canary-native-safe.sh new file mode 100755 index 00000000..ab9b7cdb --- /dev/null +++ b/tools/run-canary-native-safe.sh @@ -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 \ No newline at end of file diff --git a/tools/run-canary-native.sh b/tools/run-canary-native.sh new file mode 100755 index 00000000..19db4705 --- /dev/null +++ b/tools/run-canary-native.sh @@ -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= 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= 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)" \ No newline at end of file diff --git a/tools/run-canary-safe.sh b/tools/run-canary-safe.sh new file mode 100755 index 00000000..108ee28e --- /dev/null +++ b/tools/run-canary-safe.sh @@ -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)"