Compare commits
21 Commits
fix/corpus
...
fix/decode
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
1fdbb5f197 | ||
| 8108958a77 | |||
| b9d009b7a2 | |||
| b0906f3ed6 | |||
| 1fec20a2a3 | |||
| 46ee0d7f78 | |||
| c0a5840ce8 | |||
| 4156cb6bd1 | |||
| e16556dc29 | |||
| d96b225e47 | |||
| 4c1e15b818 | |||
| 9a8746d380 | |||
| 1cbbfd90cb | |||
| e82d7bff6f | |||
| 00585952a9 | |||
| cd3c1a2f73 | |||
| 2f39c8826e | |||
| 4a8f1221b2 | |||
| 06601c2a48 | |||
|
|
e45a56ad83 | ||
|
|
c88391b68c |
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@@ -31,6 +31,24 @@ env:
|
|||||||
# So: one job, on the machine that exists, building for the machine that exists.
|
# So: one job, on the machine that exists, building for the machine that exists.
|
||||||
# If a second architecture is ever wanted here it needs a second RUNNER, not a
|
# If a second architecture is ever wanted here it needs a second RUNNER, not a
|
||||||
# second matrix row.
|
# second matrix row.
|
||||||
|
#
|
||||||
|
# ── The toolchain is PINNED, in three places, deliberately ───────────────────
|
||||||
|
#
|
||||||
|
# All three jobs used `dtolnay/rust-toolchain@stable`, which resolves to whatever
|
||||||
|
# stable is on the day the job runs. A lint gate that floats is not a gate: the
|
||||||
|
# same tree goes green or red depending on the date, and this repo has already
|
||||||
|
# produced a disagreement between two people reading the same commit (#15).
|
||||||
|
# `collapsible_else_if` is the example — `warn` on 1.92.0, `allow`-by-default
|
||||||
|
# pedantic on 1.98.1, so a clean local run and a red CI run were both correct.
|
||||||
|
#
|
||||||
|
# `1.98.1` is the version run 206 resolved, and `docker/ci/Dockerfile` pins the
|
||||||
|
# same one, so `docker/ci/run cargo clippy …` on a desktop is a true stand-in for
|
||||||
|
# this workflow rather than an approximation of it.
|
||||||
|
#
|
||||||
|
# To bump: change all three `dtolnay/rust-toolchain@` refs here AND the `FROM
|
||||||
|
# rust:<version>-bookworm` in `docker/ci/Dockerfile` in one commit, so the two
|
||||||
|
# can never drift apart silently. A bump is a change to the gate and belongs in
|
||||||
|
# its own PR, where the new lints it turns on are the diff.
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# ── Native build, on the one runner there is ────────────────────────────────
|
# ── Native build, on the one runner there is ────────────────────────────────
|
||||||
@@ -42,13 +60,15 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust toolchain
|
- name: Install Rust toolchain
|
||||||
# `stable` installs a MINIMAL profile: rustc, cargo, rust-std and no
|
# Pinned — see the toolchain note at the top of this file.
|
||||||
|
#
|
||||||
|
# This action installs a MINIMAL profile: rustc, cargo, rust-std and no
|
||||||
# more. Components have to be named. Without this line the Clippy step
|
# more. Components have to be named. Without this line the Clippy step
|
||||||
# below dies on "'cargo-clippy' is not installed for the toolchain
|
# below dies on "'cargo-clippy' is not installed for the toolchain
|
||||||
# 'stable-aarch64-unknown-linux-gnu'" — which is not a lint result, it
|
# 'stable-aarch64-unknown-linux-gnu'" — which is not a lint result, it
|
||||||
# is the step never having run. The `fmt` job below always got this
|
# is the step never having run. The `fmt` job below always got this
|
||||||
# right; this one never did.
|
# right; this one never did.
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@1.98.1
|
||||||
with:
|
with:
|
||||||
components: clippy
|
components: clippy
|
||||||
|
|
||||||
@@ -78,7 +98,7 @@ jobs:
|
|||||||
run: cargo test --workspace
|
run: cargo test --workspace
|
||||||
|
|
||||||
# The tally above cannot tell you what it verified. `cargo test` reports
|
# The tally above cannot tell you what it verified. `cargo test` reports
|
||||||
# the same 207/0/14 whether the disc corpus was exercised or entirely
|
# the same count whether the disc corpus was exercised or entirely
|
||||||
# absent -- a gated suite that skips still counts as passed, and the
|
# absent -- a gated suite that skips still counts as passed, and the
|
||||||
# `ignored` column is a static count of `#[ignore]` attributes that cannot
|
# `ignored` column is a static count of `#[ignore]` attributes that cannot
|
||||||
# move at runtime. Issue #16. This prints what the run ACTUALLY had, from
|
# move at runtime. Issue #16. This prints what the run ACTUALLY had, from
|
||||||
@@ -114,7 +134,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
- name: Install Rust toolchain + WASM target
|
- name: Install Rust toolchain + WASM target
|
||||||
uses: dtolnay/rust-toolchain@stable
|
uses: dtolnay/rust-toolchain@1.98.1
|
||||||
with:
|
with:
|
||||||
targets: wasm32-unknown-unknown
|
targets: wasm32-unknown-unknown
|
||||||
|
|
||||||
@@ -161,7 +181,7 @@ jobs:
|
|||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: dtolnay/rust-toolchain@stable
|
- uses: dtolnay/rust-toolchain@1.98.1
|
||||||
with:
|
with:
|
||||||
components: rustfmt
|
components: rustfmt
|
||||||
- run: cargo fmt --all -- --check
|
- run: cargo fmt --all -- --check
|
||||||
|
|||||||
43
Cargo.lock
generated
43
Cargo.lock
generated
@@ -221,7 +221,7 @@ version = "1.1.5"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
checksum = "40c48f72fd53cd289104fc64099abca73db4166ad86ea0b4341abe65af83dadc"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -232,7 +232,7 @@ checksum = "291e6a250ff86cd4a820112fb8898808a366d8f9f58ce16d1f538353ad55747d"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"anstyle",
|
"anstyle",
|
||||||
"once_cell_polyfill",
|
"once_cell_polyfill",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -2507,7 +2507,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -3824,7 +3824,7 @@ version = "0.50.3"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -4818,7 +4818,7 @@ dependencies = [
|
|||||||
"errno",
|
"errno",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.12.1",
|
"linux-raw-sys 0.12.1",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5116,7 +5116,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
|||||||
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
checksum = "3a766e1110788c36f4fa1c2b71b387a7815aa65f88ce0229841826633d93723e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5197,7 +5197,7 @@ dependencies = [
|
|||||||
"colored",
|
"colored",
|
||||||
"image",
|
"image",
|
||||||
"indicatif",
|
"indicatif",
|
||||||
"sylpheed-formats 0.1.0",
|
"sylpheed-formats",
|
||||||
"texpresso",
|
"texpresso",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
@@ -5213,7 +5213,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"sylpheed-formats 0.1.0 (git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-09-01)",
|
"sylpheed-formats",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5234,25 +5234,6 @@ dependencies = [
|
|||||||
"xdvdfs",
|
"xdvdfs",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
|
||||||
name = "sylpheed-formats"
|
|
||||||
version = "0.1.0"
|
|
||||||
source = "git+https://git.mc02.dev/fabi/Sylpheed.git?tag=formats-pin-2026-09-01#1cd5b8b1cb1f02eefc0865e1a1fe280e44831c9d"
|
|
||||||
dependencies = [
|
|
||||||
"anyhow",
|
|
||||||
"binrw",
|
|
||||||
"flate2",
|
|
||||||
"futures",
|
|
||||||
"rayon",
|
|
||||||
"serde",
|
|
||||||
"serde_json",
|
|
||||||
"thiserror 2.0.18",
|
|
||||||
"tokio",
|
|
||||||
"tracing",
|
|
||||||
"ttf-parser 0.24.1",
|
|
||||||
"xdvdfs",
|
|
||||||
]
|
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sylpheed-ppc"
|
name = "sylpheed-ppc"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5276,7 +5257,7 @@ dependencies = [
|
|||||||
"image",
|
"image",
|
||||||
"rfd",
|
"rfd",
|
||||||
"rodio",
|
"rodio",
|
||||||
"sylpheed-formats 0.1.0",
|
"sylpheed-formats",
|
||||||
"thiserror 2.0.18",
|
"thiserror 2.0.18",
|
||||||
"tracing",
|
"tracing",
|
||||||
"tracing-subscriber",
|
"tracing-subscriber",
|
||||||
@@ -5417,7 +5398,7 @@ dependencies = [
|
|||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.4",
|
"rustix 1.1.4",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.52.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -5776,7 +5757,7 @@ checksum = "f2f6fb2847f6742cd76af783a2a2c49e9375d0a111c7bef6f71cd9e738c72d6e"
|
|||||||
dependencies = [
|
dependencies = [
|
||||||
"memoffset",
|
"memoffset",
|
||||||
"tempfile",
|
"tempfile",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.60.2",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
@@ -6294,7 +6275,7 @@ version = "0.1.11"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|||||||
@@ -55,33 +55,19 @@ license.workspace = true
|
|||||||
# a squash-merge can orphan, and no way for the exporter to be built against a
|
# a squash-merge can orphan, and no way for the exporter to be built against a
|
||||||
# decoder it was never tested with. A decoder change and the exporter change it
|
# decoder it was never tested with. A decoder change and the exporter change it
|
||||||
# requires now land in the same commit or not at all.
|
# requires now land in the same commit or not at all.
|
||||||
# PINNED BY TAG, which is what MISSION section 2 prescribes and what the tagging
|
# ✅ UNPINNED 2026-09-20. The pin above was deliberate and carried its own exit
|
||||||
# rule exists for: "the RE agent tags when it lands something you need and tells
|
# condition -- "revert to the path dependency the day the tag is an ancestor of
|
||||||
# you over the message channel -- that is how you stay current without floating."
|
# `main`" -- because while it held, `sylpheed-cli` built from the WORKSPACE crate
|
||||||
# That is exactly what happened here.
|
# and the exporter from the tag, so `tools/port/verify-screen` compared two eras
|
||||||
|
# instead of detecting drift. `formats-pin-2026-09-01` is now an ancestor of
|
||||||
|
# `main` (measured), so the two read one decoder again.
|
||||||
#
|
#
|
||||||
# The tag carries the CORRECTED keyframe association: a placement group is an
|
# 🔴 AND THE PIN HAD A COST NOBODY PRICED IN: it made this repository depend on
|
||||||
# 8-byte header then `frames` x {u32 time; 36-byte pose}, so pose 0's time is the
|
# its OWN history by tag. The issue-#49 history rewrite replaced every commit,
|
||||||
# group's lead-in word and EVERY POSE IS TIMED, including the last. The working
|
# the locked rev vanished, and the build broke for every clean checkout while
|
||||||
# tree's copy still has the retired `SYLPHEED_KF_TIME_SHIFT` knob -- a superseded
|
# still working on the machine that did the rewrite, because its `~/.cargo/git`
|
||||||
# partial fix that got the association right but left pose 0 untimed, which is
|
# still held the old object (PR #60). A path dependency cannot fail that way.
|
||||||
# why testing it moved the untimed frame from last to first instead of removing
|
sylpheed-formats = { path = "../sylpheed-formats" }
|
||||||
# it. The old reading is behind `SYLPHEED_KF_TIME_LEGACY=1` here.
|
|
||||||
#
|
|
||||||
# 🔴 THE COST, STATED: `sylpheed-cli` builds from the WORKSPACE crate, so until
|
|
||||||
# this lands on `main` the exporter and the reference renderer read DIFFERENT
|
|
||||||
# decoders and `tools/port/verify-screen` is comparing two eras rather than
|
|
||||||
# detecting drift. `tools/port/verify-capture` is unaffected -- it compares the
|
|
||||||
# port against oracle CAPTURES and never touches the CLI -- and it is the check
|
|
||||||
# that matters. Revert to the path dependency the day the tag is an ancestor of
|
|
||||||
# `main`.
|
|
||||||
# Bumped c -> d 2026-08-29. What I wanted from the new state: `d` carries parser
|
|
||||||
# and `audio.rs` changes on top of `c`. ⚠️ Its headline change -- Reborn's
|
|
||||||
# renderer drawing `rotation_deg`, and `compose` drawing a leaf that carries
|
|
||||||
# geometry -- does NOT reach this port from here: `sylpheed-cli` builds from the
|
|
||||||
# WORKSPACE crate, so the reference renderer stays unrotated until the tag lands
|
|
||||||
# on `main`. This bump is for the parser, not for the renderer.
|
|
||||||
sylpheed-formats = { git = "https://git.mc02.dev/fabi/Sylpheed.git", tag = "formats-pin-2026-09-01" }
|
|
||||||
|
|
||||||
serde = { version = "1", features = ["derive"] }
|
serde = { version = "1", features = ["derive"] }
|
||||||
serde_json = "1"
|
serde_json = "1"
|
||||||
|
|||||||
@@ -6,14 +6,34 @@
|
|||||||
//! silence. One rule, two outcomes.
|
//! silence. One rule, two outcomes.
|
||||||
|
|
||||||
use std::path::Path;
|
use std::path::Path;
|
||||||
|
use std::sync::OnceLock;
|
||||||
|
|
||||||
use sylpheed_formats::{slb, PakArchive};
|
use sylpheed_formats::{slb, PakArchive};
|
||||||
|
|
||||||
mod common;
|
mod common;
|
||||||
use common::skip_without_disc;
|
use common::skip_without_disc;
|
||||||
|
|
||||||
|
/// One archive for the whole binary.
|
||||||
|
///
|
||||||
|
/// `PakArchive` holds the entire concatenated payload in memory, and
|
||||||
|
/// `sound.pak` is **1.01 GB** (`sound.p00`-`.p04`). Opening it per call — which
|
||||||
|
/// the helpers below did, inside loops — put one copy per test thread in flight,
|
||||||
|
/// so at the default thread count the suite needed ~6 GB and was SIGKILLed by
|
||||||
|
/// the CI container's 7 GB cap (`--memory-swap` equals `--memory`, so there is
|
||||||
|
/// no swap to absorb it). A killed suite prints no `test result:` line at all,
|
||||||
|
/// so it vanishes from the tally rather than failing visibly.
|
||||||
|
///
|
||||||
|
/// The archive is immutable once open and every accessor takes `&self`, so one
|
||||||
|
/// shared instance is equivalent to N private ones — at 1/N the memory.
|
||||||
|
fn sound(root: &Path) -> &'static PakArchive {
|
||||||
|
static SOUND: OnceLock<PakArchive> = OnceLock::new();
|
||||||
|
// Every caller passes the same `disc_root()`, so first-writer-wins is the
|
||||||
|
// same archive whichever test initialises it.
|
||||||
|
SOUND.get_or_init(|| PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak"))
|
||||||
|
}
|
||||||
|
|
||||||
fn bank(root: &Path, n: u32) -> Vec<u8> {
|
fn bank(root: &Path, n: u32) -> Vec<u8> {
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(root);
|
||||||
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
|
let path = format!("eng\\etc\\VOICE_D_{n}.slb");
|
||||||
let entry = snd.find_by_name(&path).expect("bank present");
|
let entry = snd.find_by_name(&path).expect("bank present");
|
||||||
snd.read(entry).expect("read")
|
snd.read(entry).expect("read")
|
||||||
@@ -71,7 +91,7 @@ fn all_zero_leading_region_is_skipped() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn bank_named(root: &Path, path: &str) -> Vec<u8> {
|
fn bank_named(root: &Path, path: &str) -> Vec<u8> {
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(root);
|
||||||
let entry = snd
|
let entry = snd
|
||||||
.find_by_name(path)
|
.find_by_name(path)
|
||||||
.unwrap_or_else(|| panic!("{path} present"));
|
.unwrap_or_else(|| panic!("{path} present"));
|
||||||
@@ -154,7 +174,7 @@ fn derived_offset_recovers_voice_banks_without_regressing_etc() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scan_data_offset_agrees_with_the_riff_derived_answer() {
|
fn scan_data_offset_agrees_with_the_riff_derived_answer() {
|
||||||
skip_without_disc!(root);
|
skip_without_disc!(root);
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(&root);
|
||||||
let mut checked = 0usize;
|
let mut checked = 0usize;
|
||||||
let mut agreed = 0usize;
|
let mut agreed = 0usize;
|
||||||
for lang in ["eng", "jpn"] {
|
for lang in ["eng", "jpn"] {
|
||||||
@@ -198,7 +218,7 @@ fn scan_data_offset_agrees_with_the_riff_derived_answer() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn scan_only_returns_known_offsets() {
|
fn scan_only_returns_known_offsets() {
|
||||||
skip_without_disc!(root);
|
skip_without_disc!(root);
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(&root);
|
||||||
let mut seen = 0usize;
|
let mut seen = 0usize;
|
||||||
for n in 1u32..200 {
|
for n in 1u32..200 {
|
||||||
for path in [
|
for path in [
|
||||||
@@ -232,7 +252,7 @@ fn scan_only_returns_known_offsets() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_waves_declared_size_is_confirmed_by_the_next_seek() {
|
fn a_waves_declared_size_is_confirmed_by_the_next_seek() {
|
||||||
skip_without_disc!(root);
|
skip_without_disc!(root);
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(&root);
|
||||||
let mut checked = 0usize;
|
let mut checked = 0usize;
|
||||||
for n in 1u32..400 {
|
for n in 1u32..400 {
|
||||||
for path in [
|
for path in [
|
||||||
@@ -289,7 +309,7 @@ fn a_waves_declared_size_is_confirmed_by_the_next_seek() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn a_bank_that_states_its_own_header_has_no_leading_segment() {
|
fn a_bank_that_states_its_own_header_has_no_leading_segment() {
|
||||||
skip_without_disc!(root);
|
skip_without_disc!(root);
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(&root);
|
||||||
let mut with_header = 0usize;
|
let mut with_header = 0usize;
|
||||||
let mut mid_bank = 0usize;
|
let mut mid_bank = 0usize;
|
||||||
// Peek at the 56-byte header through the archive's flat data rather than
|
// Peek at the 56-byte header through the archive's flat data rather than
|
||||||
@@ -327,7 +347,7 @@ fn a_bank_that_states_its_own_header_has_no_leading_segment() {
|
|||||||
#[test]
|
#[test]
|
||||||
fn the_menu_music_bank_is_exactly_two_sub_waves() {
|
fn the_menu_music_bank_is_exactly_two_sub_waves() {
|
||||||
skip_without_disc!(root);
|
skip_without_disc!(root);
|
||||||
let snd = PakArchive::open(root.join("dat/sound.pak")).expect("sound.pak");
|
let snd = sound(&root);
|
||||||
for (name, sizes) in [
|
for (name, sizes) in [
|
||||||
("BGM_103.slb", [3_876_864usize, 3_930_112]),
|
("BGM_103.slb", [3_876_864usize, 3_930_112]),
|
||||||
("BGM_001.slb", [4_466_688, 4_673_536]),
|
("BGM_001.slb", [4_466_688, 4_673_536]),
|
||||||
|
|||||||
@@ -21,18 +21,51 @@ IMAGE="${SYLPH_CI_IMAGE:-sylph-ci:local}"
|
|||||||
CPUS="${SYLPH_CI_CPUS:-6}"
|
CPUS="${SYLPH_CI_CPUS:-6}"
|
||||||
MEM_GB="${SYLPH_CI_MEM_GB:-7}"
|
MEM_GB="${SYLPH_CI_MEM_GB:-7}"
|
||||||
|
|
||||||
|
CARGO_VOL="${SYLPH_CI_CARGO_VOL:-sylph-ci-cargo}"
|
||||||
|
TARGET_VOL="${SYLPH_CI_TARGET_VOL:-sylph-ci-target}"
|
||||||
|
|
||||||
|
# 🔴 Docker on the dev boxes is ROOTFUL, so without `--user` every byte the build
|
||||||
|
# writes into the bind-mounted repo is owned by root — and the user then needs
|
||||||
|
# `sudo` to delete their own artifacts. The regen command in the workspace
|
||||||
|
# `CLAUDE.md` writes `sylpheed.db` straight into /work, so it lands root-owned,
|
||||||
|
# and a stray root-owned file is exactly what survived the last cleanup and had
|
||||||
|
# to be sudo'd away.
|
||||||
|
#
|
||||||
|
# The catch is that the daemon creates a named volume root-owned, so a `--user`
|
||||||
|
# container cannot write /cargo or /target at all. Take ownership once — and
|
||||||
|
# only when it is actually wrong, because a recursive chown across a ~36 GB
|
||||||
|
# target volume is not something to repeat on every invocation.
|
||||||
|
RUN_UID="$(id -u)"
|
||||||
|
RUN_GID="$(id -g)"
|
||||||
|
|
||||||
|
docker volume create "$CARGO_VOL" >/dev/null
|
||||||
|
docker volume create "$TARGET_VOL" >/dev/null
|
||||||
|
|
||||||
|
# Both volumes, not just one: they are chowned together but can drift apart if an
|
||||||
|
# older root-owned run created only one of them.
|
||||||
|
vol_owner="$(docker run --rm -v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" "$IMAGE" \
|
||||||
|
stat -c %u /cargo /target 2>/dev/null | sort -u | tr '\n' ' ' || echo unknown)"
|
||||||
|
if [ "$vol_owner" != "$RUN_UID " ]; then
|
||||||
|
echo "docker/ci/run: chowning the cargo/target volumes to $RUN_UID:$RUN_GID (one-off)" >&2
|
||||||
|
docker run --rm \
|
||||||
|
-v "$CARGO_VOL:/cargo" -v "$TARGET_VOL:/target" \
|
||||||
|
"$IMAGE" chown -R "$RUN_UID:$RUN_GID" /cargo /target
|
||||||
|
fi
|
||||||
|
|
||||||
args=(
|
args=(
|
||||||
--rm
|
--rm
|
||||||
--cpus "$CPUS"
|
--cpus "$CPUS"
|
||||||
--memory "${MEM_GB}g"
|
--memory "${MEM_GB}g"
|
||||||
--memory-swap "${MEM_GB}g"
|
--memory-swap "${MEM_GB}g"
|
||||||
--pids-limit 2048
|
--pids-limit 2048
|
||||||
|
# Run as the invoking user so build output in /work is owned by them, not root.
|
||||||
|
--user "$RUN_UID:$RUN_GID"
|
||||||
-v "$REPO:/work"
|
-v "$REPO:/work"
|
||||||
# Named volumes, not bind mounts: the host tree keeps a 32 GB `target/` from
|
# Named volumes, not bind mounts: the host tree keeps a 32 GB `target/` from
|
||||||
# earlier host-side builds, and mixing the two produces rebuilds that look
|
# earlier host-side builds, and mixing the two produces rebuilds that look
|
||||||
# like cache misses and are actually two toolchains fighting over one directory.
|
# like cache misses and are actually two toolchains fighting over one directory.
|
||||||
-v sylph-ci-cargo:/cargo -e CARGO_HOME=/cargo
|
-v "$CARGO_VOL:/cargo" -e CARGO_HOME=/cargo
|
||||||
-v sylph-ci-target:/target -e CARGO_TARGET_DIR=/target
|
-v "$TARGET_VOL:/target" -e CARGO_TARGET_DIR=/target
|
||||||
-w /work
|
-w /work
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -10,11 +10,22 @@
|
|||||||
# AVAILABLE MEMORY as well as core count — a full-parallel build of this tree
|
# AVAILABLE MEMORY as well as core count — a full-parallel build of this tree
|
||||||
# has OOM-killed the host outright.
|
# has OOM-killed the host outright.
|
||||||
#
|
#
|
||||||
# build-canary [Release|Debug] [extra cmake --build args]
|
# `Checked` is the default, and should stay it: it is what the host builds and
|
||||||
|
# what `run-canary` picks first, so the two agree by construction. Building
|
||||||
|
# `Release` here instead leaves a second binary that `run-canary` will not use
|
||||||
|
# -- hours of CPU for something nothing runs. `Checked` is optimised, with
|
||||||
|
# assertions left in.
|
||||||
|
#
|
||||||
|
# build-canary [Checked|Release|Debug] [extra cmake --build args]
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
CONFIG="${1:-Release}"; shift || true
|
CONFIG="${1:-Checked}"; shift || true
|
||||||
SRC="${PROJECT_DIR:-/work}/xenia-canary"
|
# $XENIA_SRC FIRST. In this container the Canary source is bind-mounted at
|
||||||
|
# `/canary`, and `$PROJECT_DIR/xenia-canary` does not exist -- so the old default
|
||||||
|
# made this script unusable here, and the documented workaround was to symlink
|
||||||
|
# `/canary` into the repository and remember to delete it again (CONTAINER-NOTES
|
||||||
|
# §"The toolchain is real"). The launcher already exports the right path; read it.
|
||||||
|
SRC="${XENIA_SRC:-${PROJECT_DIR:-/work}/xenia-canary}"
|
||||||
BUILD="${XENIA_BUILD_DIR:-/sylph-home/re/canary-build}"
|
BUILD="${XENIA_BUILD_DIR:-/sylph-home/re/canary-build}"
|
||||||
JOBS="${SYLPH_JOBS:-2}"
|
JOBS="${SYLPH_JOBS:-2}"
|
||||||
|
|
||||||
|
|||||||
@@ -48,14 +48,25 @@ fi
|
|||||||
PROJECT_DIR="${PROJECT_DIR:-/work}"
|
PROJECT_DIR="${PROJECT_DIR:-/work}"
|
||||||
|
|
||||||
# ── Binary ───────────────────────────────────────────────────────────────────
|
# ── Binary ───────────────────────────────────────────────────────────────────
|
||||||
|
# `Checked` FIRST. All three configurations can be built, but `Checked` is the
|
||||||
|
# one this project actually builds, so it is the one carrying our
|
||||||
|
# instrumentation; the `Release/` and `Debug/` binaries beside it are months-old
|
||||||
|
# leftovers that still run and still boot the game, which is exactly what makes
|
||||||
|
# them dangerous -- a probe run against one reports zero hits and reads as a
|
||||||
|
# finding about the game. Measured on this box 2026-09-21: `Checked` has
|
||||||
|
# `audit_61_branch_probe_pcs`, `Release` (Aug 28) and `Debug` (Jul 19) do not.
|
||||||
pick_bin() {
|
pick_bin() {
|
||||||
[ -n "${XENIA_BIN:-}" ] && { echo "$XENIA_BIN"; return; }
|
[ -n "${XENIA_BIN:-}" ] && { echo "$XENIA_BIN"; return; }
|
||||||
for c in \
|
# Configuration is the OUTER loop, location the inner one: a `Checked` build
|
||||||
"${XENIA_BUILD_DIR:-/sylph-home/re/canary-build}/bin/Linux/Release/xenia_canary" \
|
# anywhere beats a `Release` build anywhere. The other order picks a stale
|
||||||
"${XENIA_BUILD_DIR:-/sylph-home/re/canary-build}/bin/Linux/Debug/xenia_canary" \
|
# container `Release` over a fresh instrumented repo `Checked`, which is the
|
||||||
"$PROJECT_DIR/xenia-canary/build/bin/Linux/Release/xenia_canary" \
|
# exact mistake this is here to stop.
|
||||||
"$PROJECT_DIR/xenia-canary/build/bin/Linux/Debug/xenia_canary"; do
|
for cfg in Checked Release Debug; do
|
||||||
[ -x "$c" ] && { echo "$c"; return; }
|
for c in \
|
||||||
|
"${XENIA_BUILD_DIR:-/sylph-home/re/canary-build}/bin/Linux/$cfg/xenia_canary" \
|
||||||
|
"$PROJECT_DIR/xenia-canary/build/bin/Linux/$cfg/xenia_canary"; do
|
||||||
|
[ -x "$c" ] && { echo "$c"; return; }
|
||||||
|
done
|
||||||
done
|
done
|
||||||
}
|
}
|
||||||
BIN="$(pick_bin)"
|
BIN="$(pick_bin)"
|
||||||
@@ -64,6 +75,13 @@ if [ -z "${BIN:-}" ]; then
|
|||||||
exit 1
|
exit 1
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# Say what is in the binary before the run, not after reading an empty log.
|
||||||
|
# A missing probe is a build that predates it, never a quiet game.
|
||||||
|
for sym in audit_61_branch_probe_pcs RE-DRAW; do
|
||||||
|
grep -aqm1 -- "$sym" "$BIN" \
|
||||||
|
|| echo "run-canary: ⚠ $BIN has NO '$sym' -- it predates that instrumentation. Rebuild with: build-canary" >&2
|
||||||
|
done
|
||||||
|
|
||||||
# ── ISO ──────────────────────────────────────────────────────────────────────
|
# ── ISO ──────────────────────────────────────────────────────────────────────
|
||||||
ISO="${SYLPH_ISO:-}"
|
ISO="${SYLPH_ISO:-}"
|
||||||
if [ -z "$ISO" ]; then
|
if [ -z "$ISO" ]; then
|
||||||
|
|||||||
@@ -122,11 +122,28 @@ docker_args() {
|
|||||||
# (file offset = VA - 0x82000000), which removes the need to boot the
|
# (file offset = VA - 0x82000000), which removes the need to boot the
|
||||||
# emulator and scrape /dev/shm to get at it. An earlier belief that this
|
# emulator and scrape /dev/shm to get at it. An earlier belief that this
|
||||||
# file was STALE was tested and refuted -- it is current.
|
# file was STALE was tested and refuted -- it is current.
|
||||||
-v "${SYLPH_XENIA_RS:-$PROJECT/xenia-rs}:/xenia-rs:ro"
|
|
||||||
-v "${SYLPH_PE:-$PROJECT/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).pe}:/image/sylpheed.pe:ro"
|
-v "${SYLPH_PE:-$PROJECT/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).pe}:/image/sylpheed.pe:ro"
|
||||||
-e "SYLPHEED_DB=/xenia-rs/sylpheed.db"
|
# 🔴 THE DISASSEMBLY DATABASE. This used to be read from the xenia-rs clone
|
||||||
|
# (`SYLPHEED_DB=/xenia-rs/sylpheed.db`). That repo is retired, its directory
|
||||||
|
# is gone, and the mount that served it was removed -- so the variable named
|
||||||
|
# a path that did not exist and `zq.py` had nothing to open. The database now
|
||||||
|
# lives beside the repo as a build artefact; mounted at the container's repo
|
||||||
|
# root, which is where `zq.py` looks when `$SYLPHEED_DB` is unset.
|
||||||
|
#
|
||||||
|
# Read-only on purpose: the host owns it, DuckDB takes an exclusive lock to
|
||||||
|
# write, and two agents plus the human sharing one file would corrupt it.
|
||||||
|
# Regenerating means writing elsewhere and pointing `$SYLPHEED_DB` at it.
|
||||||
-e "SYLPHEED_PE=/image/sylpheed.pe"
|
-e "SYLPHEED_PE=/image/sylpheed.pe"
|
||||||
-e "SYLPHEED_IMAGE_BASE=0x82000000"
|
-e "SYLPHEED_IMAGE_BASE=0x82000000"
|
||||||
|
# 🔴 EVIDENCE IS SHARED, NOT COMMITTED (issue #49). Each agent works in its
|
||||||
|
# OWN clone, so a gitignored capture written in one container is invisible to
|
||||||
|
# the other and to the human -- git used to be the transport and no longer is.
|
||||||
|
# One host directory, bind-mounted into both agents, is: all three see the
|
||||||
|
# same files live, every `docs/re/captures/...` citation resolves everywhere,
|
||||||
|
# and nothing can reach git history. Read-write on purpose -- showing each
|
||||||
|
# other a screenshot is the point.
|
||||||
|
-v "${SYLPH_CAPTURES:-$PROJECT/Sylpheed/docs/re/captures}:/work/docs/re/captures"
|
||||||
|
|
||||||
# The shared exchange: transient files with provenance, outside git history.
|
# The shared exchange: transient files with provenance, outside git history.
|
||||||
-v "sylpheed-exchange:/exchange"
|
-v "sylpheed-exchange:/exchange"
|
||||||
-e "PROJECT_DIR=/work"
|
-e "PROJECT_DIR=/work"
|
||||||
@@ -268,6 +285,20 @@ docker_args() {
|
|||||||
echo "==> NOTE: no ISO at $_iso -- Canary has nothing to boot." >&2
|
echo "==> NOTE: no ISO at $_iso -- Canary has nothing to boot." >&2
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
# The disassembly database. `zq.py` looks at the repo root when $SYLPHEED_DB
|
||||||
|
# is unset, so mounting it there needs no environment variable at all -- which
|
||||||
|
# is what the dead `/xenia-rs/sylpheed.db` variable taught: a path in an env
|
||||||
|
# var and a path in a mount drift apart silently.
|
||||||
|
_db="${SYLPH_DB:-$PROJECT/Sylpheed/sylpheed.db}"
|
||||||
|
if [ -f "$_db" ]; then
|
||||||
|
_out+=(-v "$_db:/work/sylpheed.db:ro")
|
||||||
|
else
|
||||||
|
echo "==> NOTE: no sylpheed.db at $_db -- /sylph-dis and zq.py cannot run." >&2
|
||||||
|
echo " build one: cargo run --release -p sylpheed-xexdb --bin sylph-xexdb -- \\" >&2
|
||||||
|
echo " dis \"\$SYLPH_ISO\" --db sylpheed.db --analyze sql --quiet" >&2
|
||||||
|
echo " then re-apply the RE names with tools/apply_re_symbols.sql" >&2
|
||||||
|
fi
|
||||||
|
|
||||||
# ── GPU ──
|
# ── GPU ──
|
||||||
# Three distinct cases, and conflating them is how you end up believing you
|
# Three distinct cases, and conflating them is how you end up believing you
|
||||||
# have hardware Vulkan while actually running llvmpipe:
|
# have hardware Vulkan while actually running llvmpipe:
|
||||||
|
|||||||
@@ -67,6 +67,14 @@ docker_args() {
|
|||||||
-v "sylpheed-port-claude:/sylph-home/port/.claude"
|
-v "sylpheed-port-claude:/sylph-home/port/.claude"
|
||||||
-v "${SYLPH_CLAUDE_HOME:-$HOME/.claude}:/sylph-home/port/.claude.seed:ro"
|
-v "${SYLPH_CLAUDE_HOME:-$HOME/.claude}:/sylph-home/port/.claude.seed:ro"
|
||||||
-v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro"
|
-v "${SYLPH_CLAUDE_JSON:-$HOME/.claude.json}:/sylph-home/port/.claude.host.json:ro"
|
||||||
|
# 🔴 EVIDENCE IS SHARED, NOT COMMITTED (issue #49). Each agent works in its
|
||||||
|
# OWN clone, so a gitignored capture written in one container is invisible to
|
||||||
|
# the other and to the human -- git used to be the transport and no longer is.
|
||||||
|
# One host directory, bind-mounted into both agents, is: all three see the
|
||||||
|
# same files live, every `docs/re/captures/...` citation resolves everywhere,
|
||||||
|
# and nothing can reach git history. Read-write on purpose -- showing each
|
||||||
|
# other a screenshot is the point.
|
||||||
|
-v "${SYLPH_CAPTURES:-$WORKSPACE/Sylpheed/docs/re/captures}:/work/docs/re/captures"
|
||||||
-v "sylpheed-exchange:/exchange"
|
-v "sylpheed-exchange:/exchange"
|
||||||
-e "PROJECT_DIR=/work"
|
-e "PROJECT_DIR=/work"
|
||||||
# Same guardrail as the decoder, added the same day and for its reason: the
|
# Same guardrail as the decoder, added the same day and for its reason: the
|
||||||
|
|||||||
@@ -1,3 +1,27 @@
|
|||||||
|
# ✅ CLOSED 2026-09-20 — every phase done, and the two human decisions taken
|
||||||
|
|
||||||
|
Nothing on this page is outstanding. It is kept as the record of how the two-repo
|
||||||
|
layout was reached; **where it describes the repository's present state it is
|
||||||
|
now out of date on purpose**, and the notes below say how.
|
||||||
|
|
||||||
|
| the page says | what is true now |
|
||||||
|
|---|---|
|
||||||
|
| "**Still open, for the human**: the history fork" | **Taken: full purge.** `git filter-repo` stripped 182 assets from every commit; force-pushed 2026-09-19. `main` `2a93be99` → `e16556dc`, `.git` **217 MB → 114 MB**, **0** assets anywhere in history, verified from an independent fresh clone. Backups + runbook: `~/sylph-repo-backup-2026-09-19/` |
|
||||||
|
| "`docs/re/captures/` is 118 MB of game screenshots inside a public repository" | **Decided (#49): no game assets in the repos.** Captures live on disk, gitignored, shared between the host and both agents by bind mount; derived measurements (csv/tsv/txt/log/json/npy) stay tracked. PR #59 |
|
||||||
|
| "all six repos are `private=False`" | Still true, and now deliberate — there is nothing game-derived in them |
|
||||||
|
| "`ai-agent-*.md`, `scratch/`, `_voice_span.rs` — keep or drop" | `scratch/` and `_voice_span.rs` dropped in the 2026-09-18 cleanup; the `ai-agent-*.md` files are the human's and stay |
|
||||||
|
| the four archived repos | unchanged: `Sylpheed-Godot`, `xenia-rs`, `xex2tractor` archived read-only, `Syplheed-Reborn` deleted. ⚠️ Their local clones are **gone from this machine** too, and `sylph-decoder` no longer mounts `xenia-rs` |
|
||||||
|
|
||||||
|
🔴 **The one thing this page got right and nobody acted on.** It named
|
||||||
|
`crates/sylpheed-export`'s dependency on **this repository by tag** as the hard
|
||||||
|
blocker for a rewrite. It was not re-checked before the purge, the locked rev
|
||||||
|
vanished, and the build broke for every clean checkout while still working on the
|
||||||
|
machine that did the rewrite — its `~/.cargo/git` still held the old object.
|
||||||
|
Fixed in PR #60 (re-lock) and PR #61 (path dependency, which removes the class).
|
||||||
|
**Before any future history rewrite, grep for a dependency on the repo's own URL.**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
# Consolidating six repositories into two
|
# Consolidating six repositories into two
|
||||||
|
|
||||||
**Set by the human, 2026-09-13.** The end state is **two** repositories:
|
**Set by the human, 2026-09-13.** The end state is **two** repositories:
|
||||||
|
|||||||
@@ -12,13 +12,20 @@ already went wrong once.
|
|||||||
* **The toolchain is real.** `tools/re-capture/rebuild_canary.sh` exists because
|
* **The toolchain is real.** `tools/re-capture/rebuild_canary.sh` exists because
|
||||||
the old box had no cmake/ninja/clang and only runtime sonames, so it hand-
|
the old box had no cmake/ninja/clang and only runtime sonames, so it hand-
|
||||||
relinked object files. **Do not use it here.** Use `build-canary`.
|
relinked object files. **Do not use it here.** Use `build-canary`.
|
||||||
🔴 **But `build-canary` does not work in this container as it stands
|
✅ **FIXED 2026-09-21 — `build-canary` now reads `$XENIA_SRC` first**, which the
|
||||||
|
launcher already sets to `/canary`, so there is no symlink to make and none to
|
||||||
|
forget. The two paragraphs below are kept because they are how the defect was
|
||||||
|
found and refuted, but **neither describes the script any more.**
|
||||||
|
|
||||||
|
🔴 ~~**`build-canary` does not work in this container as it stands
|
||||||
(2026-08-29).** It builds `${PROJECT_DIR:-/work}/xenia-canary`, which **does
|
(2026-08-29).** It builds `${PROJECT_DIR:-/work}/xenia-canary`, which **does
|
||||||
not exist here** — the Canary source is at **`/canary`** (`$XENIA_SRC`). The
|
not exist here** — the Canary source is at **`/canary`** (`$XENIA_SRC`). The
|
||||||
warm 235 MB tree at `/sylph-home/re/canary-build` is configured with
|
warm 235 MB tree at `/sylph-home/re/canary-build` is configured with
|
||||||
`CMAKE_HOME_DIRECTORY=/work/xenia-canary`, also missing, and its
|
`CMAKE_HOME_DIRECTORY=/work/xenia-canary`, also missing, and its
|
||||||
`build-Release.ninja` carries **no per-file rules** — it wants to re-run CMake
|
`build-Release.ninja` carries **no per-file rules** — it wants to re-run CMake
|
||||||
first, which would fail on the absent source root.
|
first, which would fail on the absent source root.~~ The warm tree is gone too:
|
||||||
|
every `sylph-*` volume was removed in the 2026-09-18 cleanup, so the next build
|
||||||
|
configures from scratch against `/canary` and caches the right source root.
|
||||||
|
|
||||||
✅ **The conclusion drawn from all that is REFUTED (2026-08-31).** The note went
|
✅ **The conclusion drawn from all that is REFUTED (2026-08-31).** The note went
|
||||||
on to say *"any Canary change is a full reconfigure against `/canary` plus a full
|
on to say *"any Canary change is a full reconfigure against `/canary` plus a full
|
||||||
@@ -38,9 +45,10 @@ already went wrong once.
|
|||||||
warm 235 MB tree is otherwise intact and the ninja re-run resolves its rules
|
warm 235 MB tree is otherwise intact and the ninja re-run resolves its rules
|
||||||
from the symlink.
|
from the symlink.
|
||||||
|
|
||||||
⚠️ **Remove the symlink when you are done.** `/work` is the repository, and
|
⚠️ ~~**Remove the symlink when you are done.**~~ No longer needed — but if you
|
||||||
`xenia-canary` is not in `.gitignore`, so it shows up as untracked and can be
|
ever make one by hand, note that `/work` is the repository and `xenia-canary`
|
||||||
swept into a `git add -A`.
|
is not in `.gitignore`, so it shows up as untracked and can be swept into a
|
||||||
|
`git add -A`.
|
||||||
* **numpy and Pillow are installed.** `entities2.py`, `flight_probe.py` and the
|
* **numpy and Pillow are installed.** `entities2.py`, `flight_probe.py` and the
|
||||||
image oracles work. Their absence used to look like a logic bug.
|
image oracles work. Their absence used to look like a logic bug.
|
||||||
|
|
||||||
|
|||||||
@@ -88,6 +88,42 @@ sylph-agent-canary-build the warm 235 MB Canary build tr
|
|||||||
sylpheed-exchange agent -> agent files, read-only in
|
sylpheed-exchange agent -> agent files, read-only in
|
||||||
```
|
```
|
||||||
|
|
||||||
|
**Plus one bind mount, which is not a volume on purpose** — the host's
|
||||||
|
`Sylpheed/docs/re/captures/`, mounted read-write into both agents at
|
||||||
|
`/work/docs/re/captures`:
|
||||||
|
|
||||||
|
```
|
||||||
|
-v "${SYLPH_CAPTURES:-<workspace>/Sylpheed/docs/re/captures}:/work/docs/re/captures"
|
||||||
|
```
|
||||||
|
|
||||||
|
🔴 **Why it has to exist.** Each agent works in its OWN clone, so before issue
|
||||||
|
#49 the transport for evidence was *git*: commit the screenshot, the other agent
|
||||||
|
pulls it. #49 removed that — captures are gitignored now — and without a
|
||||||
|
replacement a capture written in one container is invisible to the other agent,
|
||||||
|
to the human, and to `check-capture-citations`, which would call every citation
|
||||||
|
dangling. One host directory shared by all three restores it: the same file is
|
||||||
|
live everywhere, every `docs/re/captures/...` citation resolves, and nothing can
|
||||||
|
reach git history. Read-write on both, because showing each other a screenshot
|
||||||
|
is the point.
|
||||||
|
|
||||||
|
Verified rather than assumed: container A wrote a `.png` there, a **separate**
|
||||||
|
container B read it back, the host saw it, and `git status` reported **0**
|
||||||
|
changes.
|
||||||
|
|
||||||
|
⚠️ **Evidence that must cross MACHINES still cannot go this way** — a bare clone
|
||||||
|
on the other desktop has no captures at all. Attach it to the issue or PR; that
|
||||||
|
is the only channel that travels.
|
||||||
|
|
||||||
|
⚠️ **The agent volumes were all deleted in the 2026-09-18 cleanup** and Docker
|
||||||
|
recreates them empty on next launch. Nothing was lost — both agent clones were
|
||||||
|
verified clean with nothing unpushed, and the exchange held only spent artefacts.
|
||||||
|
One side effect is welcome: the empty `*-claude` volumes mean the next launch
|
||||||
|
starts a **fresh** session, which is the documented workaround for the
|
||||||
|
"resumes the old brief" defect below.
|
||||||
|
|
||||||
|
⚠️ **`sylph-decoder` used to mount `xenia-rs` read-only. That repo is retired and
|
||||||
|
the directory is gone**, so the mount pointed at nothing; removed.
|
||||||
|
|
||||||
**Credentials** — three files on the host, `chmod 600`, mounted read-only:
|
**Credentials** — three files on the host, `chmod 600`, mounted read-only:
|
||||||
|
|
||||||
```
|
```
|
||||||
|
|||||||
@@ -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`.
|
||||||
|
|||||||
108
docs/agents/HANDOFF-2026-09-18.md
Normal file
108
docs/agents/HANDOFF-2026-09-18.md
Normal 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.
|
||||||
@@ -129,10 +129,18 @@ exchange volume carries the working artefacts.
|
|||||||
| kind | where | why |
|
| kind | where | why |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| code, decoded knowledge | **git** | history, review, permanence |
|
| code, decoded knowledge | **git** | history, review, permanence |
|
||||||
| evidence cited by a finding | **git** | it is the proof |
|
| evidence cited by a finding — a screenshot, a frame, a savegame | **`docs/re/captures/`, present but NEVER committed** | it is the proof, but it is game content. The directory is gitignored and shared between the host and both agents, so all three see the same file; `tools/re/check-capture-citations` fails if a citation has no file, and fails again if an asset is tracked |
|
||||||
|
| a measurement you derived — csv, tsv, txt, log, json | **git** | our own numbers, not game content, and most findings rest on them |
|
||||||
|
| evidence that must cross machines | **attached to the issue or PR** | captures are local-only now, so a bare clone has none of them. Attaching is the only channel that reaches the other desktop or a reviewer |
|
||||||
| **evidence a human must look at** — the screenshot or film behind a `state/needs-human` item | **attached to that issue** | it travels *with* the item, a person sees it in a browser, and it cannot be orphaned from the claim it supports |
|
| **evidence a human must look at** — the screenshot or film behind a `state/needs-human` item | **attached to that issue** | it travels *with* the item, a person sees it in a browser, and it cannot be orphaned from the claim it supports |
|
||||||
| exploratory captures, work in progress, "look at this" | **`share`** → `/exchange` | no history; would bloat the repo forever |
|
| exploratory captures, work in progress, "look at this" | **`share`** → `/exchange` | no history; would bloat the repo forever |
|
||||||
|
|
||||||
|
⚠️ **This table's first row used to say "git", and that is how 76 MB of
|
||||||
|
screenshots accumulated** — directly above the rule forbidding exactly that. The
|
||||||
|
two contradicted each other for months and the table won, because it was the one
|
||||||
|
that told you what to *do*. Issue #49 settled it and the history was rewritten;
|
||||||
|
if you find yourself reaching for `git add -f` on a capture, the answer is no.
|
||||||
|
|
||||||
🔴 **Never commit game content.** Not sprites, not audio, not transcoded video,
|
🔴 **Never commit game content.** Not sprites, not audio, not transcoded video,
|
||||||
not a capture of the running game — under *any* directory name. On 2026-09-04
|
not a capture of the running game — under *any* directory name. On 2026-09-04
|
||||||
this rule was live, and freshly tightened, while **545 MB of extracted disc
|
this rule was live, and freshly tightened, while **545 MB of extracted disc
|
||||||
|
|||||||
@@ -56,11 +56,28 @@ rather than left in a document you might not reach.
|
|||||||
| path | what | env |
|
| path | what | env |
|
||||||
|---|---|---|
|
|---|---|---|
|
||||||
| `/image/sylpheed.pe` | the decompressed executable image | `SYLPHEED_PE` |
|
| `/image/sylpheed.pe` | the decompressed executable image | `SYLPHEED_PE` |
|
||||||
| `/xenia-rs/sylpheed.db` | a disassembly database, 586 MB | `SYLPHEED_DB` |
|
| `sylpheed.db` at the repo root | the disassembly database, 337 MB | — (`zq.py` finds it; `$SYLPHEED_DB` only to override) |
|
||||||
|
| `/canary/build/bin/Linux/Checked/xenia_canary` | the built oracle, inside the `/canary` mount | — |
|
||||||
| `/disc` | the extracted disc | `SYLPHEED_DISC` |
|
| `/disc` | the extracted disc | `SYLPHEED_DISC` |
|
||||||
| `/iso/game.iso` | the retail ISO Canary boots | `SYLPH_ISO` |
|
| `/iso/game.iso` | the retail ISO Canary boots | `SYLPH_ISO` |
|
||||||
| `/canary` | the Canary source, read-write | `XENIA_SRC` |
|
| `/canary` | the Canary source, read-write | `XENIA_SRC` |
|
||||||
|
|
||||||
|
⚠️ **The database is read-only and is NOT in git** — it is a build artefact, and
|
||||||
|
`docs/re/captures/` is the same (issue #49). If either is missing the launcher
|
||||||
|
says so and prints the command that builds it. Query the database with
|
||||||
|
`python3 tools/zq.py …`, never by reading a disassembly dump.
|
||||||
|
|
||||||
|
⚠️ **Take the oracle from `Checked/`, not `Release/`.** `Checked` is what this
|
||||||
|
tree actually builds (optimised, with assertions); the `Release/` binary beside
|
||||||
|
it is a stale August build with no `audit_61` probe in it. `strings` on the two
|
||||||
|
is how that was found, and is how to check any binary before quoting a run from
|
||||||
|
it.
|
||||||
|
|
||||||
|
The `Checked` build carries all three instrumentations — `RE-INPUT`, `RE-DRAW`
|
||||||
|
and the `audit_61` guest-PC branch probe (fork PR #2 brought the probe onto
|
||||||
|
`sylpheed-re`; before it, the probe lived only on `phase-a-tracing`). Verified
|
||||||
|
by `strings` on the binary, not assumed.
|
||||||
|
|
||||||
**The `.pe` is a flat VA dump**: file offset = `VA - 0x82000000`. Reading
|
**The `.pe` is a flat VA dump**: file offset = `VA - 0x82000000`. Reading
|
||||||
`0x820A1630` is `seek(0xA1630)`. No XEX decrypt, no LZX, **no booted emulator** —
|
`0x820A1630` is `seek(0xA1630)`. No XEX decrypt, no LZX, **no booted emulator** —
|
||||||
dumping guest memory works but makes the whole static corpus depend on a running
|
dumping guest memory works but makes the whole static corpus depend on a running
|
||||||
|
|||||||
@@ -5,7 +5,8 @@ Confidence: ✅ `CONFIRMED` · 🟡 `PROBABLE` · ❔ `HYPOTHESIS`. See [README]
|
|||||||
Also durable, and worth reading before proposing anything:
|
Also durable, and worth reading before proposing anything:
|
||||||
[`REFUTED.md`](REFUTED.md) — what has already been tested and died ·
|
[`REFUTED.md`](REFUTED.md) — what has already been tested and died ·
|
||||||
[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for ·
|
[`METHOD.md`](METHOD.md) — the traps this corpus has already paid for ·
|
||||||
[`BACKLOG.md`](BACKLOG.md) — what is still open.
|
[`BACKLOG.md`](BACKLOG.md) — what is still open ·
|
||||||
|
[`disc-contents.md`](disc-contents.md) — what files the disc actually holds, and where.
|
||||||
|
|
||||||
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
|
Formats we've already reversed are, for now, **documented by their parser + disc round-trip
|
||||||
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
|
tests** (the executable spec) rather than a prose file — the "Spec" column points there.
|
||||||
@@ -72,6 +73,7 @@ files, which is how the same ground got covered twice.
|
|||||||
| [`autopilot-memory-driven.md`](autopilot-memory-driven.md) | Memory-driven autopilot — build log and current state | 🟢 IT FLIES, KILLS AND SURVIVES — but it loses the mission anyway. |
|
| [`autopilot-memory-driven.md`](autopilot-memory-driven.md) | Memory-driven autopilot — build log and current state | 🟢 IT FLIES, KILLS AND SURVIVES — but it loses the mission anyway. |
|
||||||
| [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) | Getting past the title screen in the container — three traps and one blocker | ✅ CONFIRMED for the three traps (each reproduced, and two of them |
|
| [`canary-scripted-input-traps.md`](canary-scripted-input-traps.md) | Getting past the title screen in the container — three traps and one blocker | ✅ CONFIRMED for the three traps (each reproduced, and two of them |
|
||||||
| [`challenge-mission-gate.md`](challenge-mission-gate.md) | Challenge / EX missions — the stage set, the GamePart graph, and the kind field | ✅ for the static structure (stage set, GamePart ids, the config-section |
|
| [`challenge-mission-gate.md`](challenge-mission-gate.md) | Challenge / EX missions — the stage set, the GamePart graph, and the kind field | ✅ for the static structure (stage set, GamePart ids, the config-section |
|
||||||
|
| [`disc-contents.md`](disc-contents.md) | What is actually on the disc, and where | ✅ CONFIRMED — layout, counts and `media_id` re-measured against the retail extract 2026-09-18; `resource3d/` is in `hidden/`, not `dat/` |
|
||||||
| [`dynamic-re-state-restore.md`](dynamic-re-state-restore.md) | The container's dynamic-RE state is not durable — how to rebuild it | ✅ CONFIRMED by rebuilding it (2026-08-23). Everything the dynamic |
|
| [`dynamic-re-state-restore.md`](dynamic-re-state-restore.md) | The container's dynamic-RE state is not durable — how to rebuild it | ✅ CONFIRMED by rebuilding it (2026-08-23). Everything the dynamic |
|
||||||
| [`flight-controls-runtime.md`](flight-controls-runtime.md) | In-flight control mapping — measured, not assumed | ✅ for the weapon bindings (ammo counters move), 🟡 for the rest (HUD |
|
| [`flight-controls-runtime.md`](flight-controls-runtime.md) | In-flight control mapping — measured, not assumed | ✅ for the weapon bindings (ammo counters move), 🟡 for the rest (HUD |
|
||||||
| [`flight-speed-law.md`](flight-speed-law.md) | The throttle is a TARGET-SPEED selector — measured against the definition (2026-08-13) | ✅ for the shape of the law, 🟡 for the unit scale. |
|
| [`flight-speed-law.md`](flight-speed-law.md) | The throttle is a TARGET-SPEED selector — measured against the definition (2026-08-13) | ✅ for the shape of the law, 🟡 for the unit scale. |
|
||||||
|
|||||||
@@ -18,6 +18,22 @@ A wrong-but-confident note is worse than no note: someone builds on it and the b
|
|||||||
for weeks. Every entry therefore carries an explicit **confidence** and its **evidence**.
|
for weeks. Every entry therefore carries an explicit **confidence** and its **evidence**.
|
||||||
This mirrors the project method — *measure the oracle, never infer; refute before believing.*
|
This mirrors the project method — *measure the oracle, never infer; refute before believing.*
|
||||||
|
|
||||||
|
### Captures are local-only (issue #49)
|
||||||
|
|
||||||
|
The repository carries **code, tooling and docs**. Screenshots and savegame
|
||||||
|
blobs are game-derived, so since 2026-09-19 they live in `docs/re/captures/`
|
||||||
|
on disk and are **gitignored** — the pages' relative links still resolve on a
|
||||||
|
machine that has them, and nothing ships.
|
||||||
|
|
||||||
|
Derived measurements (`csv`, `tsv`, `txt`, `log`, `json`, `jsonl`, `npy`) are
|
||||||
|
our own numbers rather than game content, and stay tracked — they are what most
|
||||||
|
claims here actually rest on.
|
||||||
|
|
||||||
|
`tools/re/check-capture-citations` enforces both halves: a cited capture must be
|
||||||
|
**present**, and a game asset must **not be tracked**. ⚠️ A fresh clone has no
|
||||||
|
captures, so its citations will not resolve until the captures are copied in;
|
||||||
|
that is expected, and the checker is a local gate rather than a CI one.
|
||||||
|
|
||||||
### Clean-room firewall
|
### Clean-room firewall
|
||||||
|
|
||||||
- ✅ Allowed: behaviour descriptions, field offsets/types, formulas, state machines,
|
- ✅ Allowed: behaviour descriptions, field offsets/types, formulas, state machines,
|
||||||
|
|||||||
13
docs/re/captures/.gitignore
vendored
Normal file
13
docs/re/captures/.gitignore
vendored
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
# Game-derived assets stay on disk and ship nowhere (issue #49).
|
||||||
|
#
|
||||||
|
# The pages' relative links still resolve on a machine that has the captures,
|
||||||
|
# so the evidence stays followable where it exists — it is simply not committed.
|
||||||
|
# `tools/re/check-capture-citations` enforces both halves: a cited capture must
|
||||||
|
# be PRESENT here, and an asset must NOT be tracked.
|
||||||
|
#
|
||||||
|
# Derived measurements (csv, tsv, txt, log, json, jsonl, npy) are our own
|
||||||
|
# numbers rather than game content, and remain tracked.
|
||||||
|
*.png
|
||||||
|
*.jpg
|
||||||
|
*.jpeg
|
||||||
|
*.bin
|
||||||
104
docs/re/disc-contents.md
Normal file
104
docs/re/disc-contents.md
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
# What is actually on the disc, and where
|
||||||
|
|
||||||
|
**Status:** ✅ `CONFIRMED` for the layout and the counts — every figure below was
|
||||||
|
re-measured against the retail extract on 2026-09-18, not copied from the source
|
||||||
|
document. ✅ `CONFIRMED` for `media_id`, read from the XEX header.
|
||||||
|
|
||||||
|
This page exists because the corpus never had one: it documents formats in depth
|
||||||
|
(see [INDEX](INDEX.md)) but nowhere said **what files the disc holds and where they sit**.
|
||||||
|
It is adopted from a pre-RE-era `GAME_CONTENTS.md` that lived homeless in the workspace
|
||||||
|
root through the repository consolidation. **Only the measured parts were carried over** —
|
||||||
|
see [What was dropped](#what-was-dropped-and-why) at the end, which matters more than the
|
||||||
|
rest of the page.
|
||||||
|
|
||||||
|
Extracted from the XISO image with [extract-xiso](https://github.com/XboxDev/extract-xiso).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Identity
|
||||||
|
|
||||||
|
| Field | Value | Source |
|
||||||
|
|---|---|---|
|
||||||
|
| `media_id` | `0x2D2E2EEB` | XEX `execution_info`, via `.xex.json` |
|
||||||
|
| `title_id` | `0x53512D14` (`"SQ"` + `0x2D14`) | XEX `execution_info` |
|
||||||
|
| `disc_number` / `disc_count` | 1 / 1 | XEX `execution_info` |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Top-level layout
|
||||||
|
|
||||||
|
```
|
||||||
|
<extract root>/
|
||||||
|
├── default.xex ← the game executable (XEX2, PowerPC BE)
|
||||||
|
├── config.ini ← language table (Shift-JIS comments)
|
||||||
|
├── $SystemUpdate/ ← su20076000_00000000 (dashboard update, not game data)
|
||||||
|
├── dat/ ← 71 entries
|
||||||
|
└── hidden/ ← 5 entries
|
||||||
|
```
|
||||||
|
|
||||||
|
⚠️ **`resource3d/` is in `hidden/`, not in `dat/`.** So are `DefTables` and `MiscBin`.
|
||||||
|
The source document placed all three under `dat/`; that is wrong, and it is the reason
|
||||||
|
this page re-measured rather than transcribed. `SYLPHEED_RES3D` points at
|
||||||
|
`hidden/resource3d` for exactly this reason.
|
||||||
|
|
||||||
|
### `dat/` — 71 entries
|
||||||
|
|
||||||
|
| Group | Count | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `*.pak` + `*.p00` pairs | 33 + 33 | the IPFB archives — format ✅ decoded, see [INDEX](INDEX.md) |
|
||||||
|
| `sound.pak` + `sound.p00`–`.p04` | 6 | one archive whose payload is split across five chunks |
|
||||||
|
| `movie/` | 109 entries | |
|
||||||
|
|
||||||
|
`dat/movie/` holds **97 `.wmv`** files plus **six language packs** as `.pak`/`.p00`
|
||||||
|
pairs (`deu eng esp fra ita jpn`). ⚠️ Those packs carry **subtitles and fonts, not voice** —
|
||||||
|
all voice and SFX live in `sound.pak`. That trap is recorded separately; do not go looking
|
||||||
|
for dialogue audio in the movie directory.
|
||||||
|
|
||||||
|
### `hidden/` — 5 entries
|
||||||
|
|
||||||
|
| Entry | Size | Note |
|
||||||
|
|---|---|---|
|
||||||
|
| `resource3d/` | 166 files | `.xpr` texture/model containers (`Base.xpr`, `BG_*.xpr`, stage and ship sets) |
|
||||||
|
| `DefTables.pak` / `.p00` | 17 596 B / 3 058 037 B | balance and definition tables |
|
||||||
|
| `MiscBin.pak` / `.p00` | 496 B / 22 553 238 B | |
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## `config.ini`
|
||||||
|
|
||||||
|
Plain-text INI, **Shift-JIS** comments (they render as mojibake in a UTF-8 reader — the
|
||||||
|
file is not corrupt). `[SYSTEM]` is present but empty; `[LANGUAGE]` maps the Xbox 360
|
||||||
|
locale constants onto the disc's three-letter directory names, with `eng` as the default:
|
||||||
|
|
||||||
|
```ini
|
||||||
|
[LANGUAGE]
|
||||||
|
= eng ; default
|
||||||
|
#0x01 = eng ; XC_LANGUAGE_ENGLISH
|
||||||
|
#0x02 = jpn ; XC_LANGUAGE_JAPANESE
|
||||||
|
#0x03 = deu ; XC_LANGUAGE_GERMAN
|
||||||
|
```
|
||||||
|
|
||||||
|
Those keys are why the six-language pack naming above is what it is.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## What was dropped, and why
|
||||||
|
|
||||||
|
The source document was written **before** the formats were reversed, and roughly half of
|
||||||
|
it was speculation phrased as status. Carrying that forward would have put claims into the
|
||||||
|
corpus that the corpus itself has already refuted — the precise failure mode
|
||||||
|
[README](README.md) warns about ("a wrong-but-confident note is worse than no note").
|
||||||
|
Dropped:
|
||||||
|
|
||||||
|
- **A "Known File Formats" status table** marking `dat/*.pak` as *"⏳ Unknown — magic bytes
|
||||||
|
TBD"*, and `.XWB`/`.XSB` as *"⏳ TODO"*. The `.pak` container is ✅ decoded disc-wide.
|
||||||
|
[INDEX](INDEX.md) is the authority on format status; a second table would only drift.
|
||||||
|
- **A "PAK Archive Structure (TBD)" section** guessing each archive's contents from its
|
||||||
|
name ("`GP_BUNK.pak` — likely barracks/crew quarters UI"). Those are guesses, and the
|
||||||
|
real contents are known.
|
||||||
|
- **An "RE Entry Points" section** recommending loading `default.xex` into Ghidra to find
|
||||||
|
the loaders. Static analysis now goes through `sylpheed.db` (see the workspace
|
||||||
|
`CLAUDE.md` and `/sylph-dis`).
|
||||||
|
|
||||||
|
Nothing measured was dropped. The layout, the counts, the identity fields and the language
|
||||||
|
table are all re-verified above.
|
||||||
@@ -19,12 +19,13 @@ The instrumented branch stops at the Stage 02 briefing under a storm of
|
|||||||
behind `upstream/canary_experimental` (`a5a18f5c7`); our branch carries 50 of its
|
behind `upstream/canary_experimental` (`a5a18f5c7`); our branch carries 50 of its
|
||||||
own.
|
own.
|
||||||
|
|
||||||
* **`version.h` is never generated.** The build fails on
|
* ~~**`version.h` is never generated.**~~ ✅ **FIXED — `CMakeLists.txt` now
|
||||||
`trace_writer.cc:17: fatal error: 'version.h' file not found`. Upstream's
|
generates it at configure time**, calling `xenia-build.py`'s
|
||||||
`xenia-build.py` writes it from git HEAD; the container's `build-canary`
|
`generate_version_h()` and falling back to a stub if that fails, so a
|
||||||
wrapper does not invoke it, and our tree only builds because a stale copy from
|
CMake-direct build no longer depends on a stale copy in the build directory.
|
||||||
an old `sylpheed-re` build sits in the build directory. Regenerated by hand in
|
It used to fail on `trace_writer.cc:17: fatal error: 'version.h' file not
|
||||||
exactly the format that script emits.
|
found`, and the fix had to exist before the 2026-09-18 cleanup deleted the
|
||||||
|
build volume that was carrying that stale copy.
|
||||||
* **`build-canary` reports success on a failed build.** The harness recorded
|
* **`build-canary` reports success on a failed build.** The harness recorded
|
||||||
"completed (exit code 0)" while ninja had stopped with `1 error generated`.
|
"completed (exit code 0)" while ninja had stopped with `1 error generated`.
|
||||||
Only the missing binary gave it away.
|
Only the missing binary gave it away.
|
||||||
|
|||||||
16
tools/asound-null.conf
Normal file
16
tools/asound-null.conf
Normal 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
58
tools/diagnose-freeze.sh
Executable 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
14
tools/heaptrack-wrap.sh
Executable 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
72
tools/live-guest-state.sh
Executable 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>."
|
||||||
3
tools/re-capture/.gitignore
vendored
Normal file
3
tools/re-capture/.gitignore
vendored
Normal file
@@ -0,0 +1,3 @@
|
|||||||
|
|
||||||
|
# Game-derived digit templates for ob_read.py — pixels from the game (issue #49).
|
||||||
|
ob_digits.png
|
||||||
@@ -10,10 +10,17 @@
|
|||||||
largest thing in the repository, and the one place a file can be added, never
|
largest thing in the repository, and the one place a file can be added, never
|
||||||
cited, and never noticed.
|
cited, and never noticed.
|
||||||
|
|
||||||
Two failures, which are opposites and must not be conflated:
|
⚠️ 2026-09-19, issue #49: **game assets are no longer committed.** Screenshots
|
||||||
|
and savegame blobs live in the working tree and are gitignored, so the pages'
|
||||||
|
relative links still resolve on a machine that has them while nothing ships.
|
||||||
|
That inverts half of this check — "is it committed?" became "is it PRESENT?",
|
||||||
|
and a NEW failure appeared: an asset that IS tracked. Both are below.
|
||||||
|
|
||||||
* a page cites a capture that **is not committed** — a reader following it
|
Three failures, which are opposites and must not be conflated:
|
||||||
|
|
||||||
|
* a page cites a capture that **is not present** — a reader following it
|
||||||
gets nothing. That is an error, exactly as in `check-citations`.
|
gets nothing. That is an error, exactly as in `check-citations`.
|
||||||
|
* a game asset that **is tracked by git** — issue #49 says it must not be.
|
||||||
* a capture that **no page cites** — not an error. It may be evidence a page
|
* a capture that **no page cites** — not an error. It may be evidence a page
|
||||||
should have cited, and deleting on that basis would silently ratify the
|
should have cited, and deleting on that basis would silently ratify the
|
||||||
omission. Reported, counted, never failed on.
|
omission. Reported, counted, never failed on.
|
||||||
@@ -65,7 +72,16 @@ def committed() -> tuple[set[str], set[str]]:
|
|||||||
proposed. `tools/port/check-citations` uses the working tree for exactly
|
proposed. `tools/port/check-citations` uses the working tree for exactly
|
||||||
this reason; so does this.
|
this reason; so does this.
|
||||||
"""
|
"""
|
||||||
files = {l for l in git("ls-files", ROOT).splitlines() if l}
|
# 🔴 NOT `git ls-files`: since #49 the assets are deliberately untracked, so
|
||||||
|
# the index no longer knows they exist. Presence is a question about the
|
||||||
|
# working tree, and asking git would report every screenshot as missing and
|
||||||
|
# fail on all 203 citations.
|
||||||
|
files = set()
|
||||||
|
for dirpath, _dirnames, filenames in os.walk(ROOT):
|
||||||
|
for fn in filenames:
|
||||||
|
if fn == ".gitignore":
|
||||||
|
continue
|
||||||
|
files.add(os.path.join(dirpath, fn).replace(os.sep, "/"))
|
||||||
dirs = set()
|
dirs = set()
|
||||||
for f in files:
|
for f in files:
|
||||||
parts = f.split("/")
|
parts = f.split("/")
|
||||||
@@ -74,6 +90,24 @@ def committed() -> tuple[set[str], set[str]]:
|
|||||||
return files, dirs
|
return files, dirs
|
||||||
|
|
||||||
|
|
||||||
|
ASSET_SUFFIXES = (".png", ".jpg", ".jpeg", ".bin")
|
||||||
|
|
||||||
|
|
||||||
|
def tracked_assets() -> list[str]:
|
||||||
|
"""Game assets that are committed — forbidden since issue #49.
|
||||||
|
|
||||||
|
The repo carries code, tooling and docs. A screenshot that sneaks back in
|
||||||
|
is invisible in review (a binary shows as "Bin 0 -> 1234567 bytes") and is
|
||||||
|
permanent once merged, because removing it later needs a history rewrite.
|
||||||
|
So this is the half of the check that has to be loud.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
for l in git("ls-files").splitlines():
|
||||||
|
if l.lower().endswith(ASSET_SUFFIXES):
|
||||||
|
out.append(l)
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
def cited() -> set[str]:
|
def cited() -> set[str]:
|
||||||
raw = git("grep", "-rhoE", CITE_ERE, "--", *SEARCH, SELF).splitlines()
|
raw = git("grep", "-rhoE", CITE_ERE, "--", *SEARCH, SELF).splitlines()
|
||||||
out = set()
|
out = set()
|
||||||
@@ -141,7 +175,7 @@ def selftest() -> int:
|
|||||||
real_dir = next(iter(dirs), None)
|
real_dir = next(iter(dirs), None)
|
||||||
real_file = next(iter(files), None)
|
real_file = next(iter(files), None)
|
||||||
if not real_dir or not real_file:
|
if not real_dir or not real_file:
|
||||||
print("selftest: 🔴 no captures committed — nothing to test against")
|
print("selftest: 🔴 no captures present — nothing to test against")
|
||||||
return 2
|
return 2
|
||||||
cases = [
|
cases = [
|
||||||
("planted dangling caught", "docs/re/captures/no-such-file-anywhere.png", False),
|
("planted dangling caught", "docs/re/captures/no-such-file-anywhere.png", False),
|
||||||
@@ -168,7 +202,15 @@ def selftest() -> int:
|
|||||||
print(" %-34s %s"
|
print(" %-34s %s"
|
||||||
% ("own fixtures not counted", "🔴 FAILED" if fixture_counted else "ok"))
|
% ("own fixtures not counted", "🔴 FAILED" if fixture_counted else "ok"))
|
||||||
|
|
||||||
ok = gathering_ok and not fixture_counted
|
# 🔴 THE #49 RULE NEEDS ITS OWN TICK. Presence now comes from the working
|
||||||
|
# tree, so a checkout with the captures present looks identical whether or
|
||||||
|
# not they are tracked — only this assertion can tell the difference.
|
||||||
|
stowaways = tracked_assets()
|
||||||
|
print(" %-34s %s%s"
|
||||||
|
% ("no game asset tracked", "ok" if not stowaways else "🔴 FAILED",
|
||||||
|
"" if not stowaways else " (%d tracked)" % len(stowaways)))
|
||||||
|
|
||||||
|
ok = gathering_ok and not fixture_counted and not stowaways
|
||||||
for name, path, want in cases:
|
for name, path, want in cases:
|
||||||
got = resolves(path.rstrip(".,;:)`"), files, dirs)
|
got = resolves(path.rstrip(".,;:)`"), files, dirs)
|
||||||
mark = "ok" if got == want else "🔴 FAILED"
|
mark = "ok" if got == want else "🔴 FAILED"
|
||||||
@@ -188,19 +230,48 @@ def main() -> int:
|
|||||||
print(f)
|
print(f)
|
||||||
return 0
|
return 0
|
||||||
|
|
||||||
print("captures committed : %d" % len(files))
|
stowaways = tracked_assets()
|
||||||
|
print("captures present on disk : %d" % len(files))
|
||||||
print(" cited by a page or a tool : %d" % (len(files) - len(orphans)))
|
print(" cited by a page or a tool : %d" % (len(files) - len(orphans)))
|
||||||
print(" cited by nothing : %d (reported, not failed —" % len(orphans))
|
print(" cited by nothing : %d (reported, not failed —" % len(orphans))
|
||||||
print(" an orphan may be evidence a page owes)")
|
print(" an orphan may be evidence a page owes)")
|
||||||
|
# 🔴 A BARE CLONE HAS NO CAPTURES AT ALL, and that is not a defect.
|
||||||
|
# Since #49 the assets are never committed, so a fresh clone, a worktree or
|
||||||
|
# a CI checkout legitimately has none — and the naive check calls all 134
|
||||||
|
# citations dangling and exits 1. A gate that is red on every clean checkout
|
||||||
|
# is one people learn to ignore, which is how the last wrong-by-default
|
||||||
|
# check in this file cost a session. Distinguish "none here" from "this one
|
||||||
|
# is missing": with some assets present, a gap is real and still fails.
|
||||||
|
assets_here = sum(1 for f in files if f.lower().endswith(ASSET_SUFFIXES))
|
||||||
|
if assets_here == 0 and dangling:
|
||||||
|
print(" ⓘ no captures on this checkout: %d citations unresolved" % len(dangling))
|
||||||
|
print(" Expected — captures are local-only (#49) and a fresh clone has none.")
|
||||||
|
print(" Copy them in, or read the evidence on the issue it is attached to.")
|
||||||
|
print(" 🔴 game assets TRACKED : %d" % len(stowaways))
|
||||||
|
return 1 if stowaways else 0
|
||||||
|
|
||||||
|
rc = 0
|
||||||
if dangling:
|
if dangling:
|
||||||
print(" 🔴 cited but NOT committed: %d" % len(dangling))
|
print(" 🔴 cited but NOT present : %d" % len(dangling))
|
||||||
for d in dangling:
|
for d in dangling:
|
||||||
print(" %s" % d)
|
print(" %s" % d)
|
||||||
print("\n🔴 a reader following those gets nothing. Commit the capture, fix the")
|
print("\n🔴 a reader following those gets nothing. Restore the capture, fix the")
|
||||||
print(" path, or drop the citation.")
|
print(" path, or drop the citation.")
|
||||||
return 1
|
rc = 1
|
||||||
print(" 🔴 cited but NOT committed: 0")
|
else:
|
||||||
return 0
|
print(" 🔴 cited but NOT present : 0")
|
||||||
|
|
||||||
|
if stowaways:
|
||||||
|
print(" 🔴 game assets TRACKED : %d (issue #49 — code, tooling, docs only)" % len(stowaways))
|
||||||
|
for a in stowaways[:10]:
|
||||||
|
print(" %s" % a)
|
||||||
|
if len(stowaways) > 10:
|
||||||
|
print(" … and %d more" % (len(stowaways) - 10))
|
||||||
|
print("\n🔴 run `git rm --cached` on those; they stay on disk and stay ignored.")
|
||||||
|
rc = 1
|
||||||
|
else:
|
||||||
|
print(" 🔴 game assets TRACKED : 0")
|
||||||
|
return rc
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|||||||
98
tools/run-canary-native-safe.sh
Executable file
98
tools/run-canary-native-safe.sh
Executable 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
130
tools/run-canary-native.sh
Executable 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
72
tools/run-canary-safe.sh
Executable 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)"
|
||||||
Reference in New Issue
Block a user