diff --git a/.gitignore b/.gitignore index 530be76..952ac1e 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,17 @@ target/ *.xiso *.db +# Claude Code project data: transcripts, subagent logs, and (huge) agent git +# worktrees. ~71k files / 66 GB — must never be scanned by git or the editor. +.claude/ + # Audit reports / pre-pass findings (local artifacts, not source) audit-out/ audit-*.md +audit-runs/ +exit-thread-state.json +zq_dis.py +zq_fn.py # Run logs from stress harnesses and ad-hoc captures *.stdout diff --git a/crates/xenia-app/src/main.rs b/crates/xenia-app/src/main.rs index 4a58505..08c9c3d 100644 --- a/crates/xenia-app/src/main.rs +++ b/crates/xenia-app/src/main.rs @@ -4276,6 +4276,34 @@ fn dump_thread_diagnostic( } use xenia_kernel::objects::KernelObject; + // STEP-10 diagnostic (observe-only, env-gated `XENIA_DUMP_SLOTS=1`). + // Prints each scheduler slot's full runqueue with the fields needed to + // distinguish "Blocked(Suspended) forever" from "Ready but never picked": + // tid, handle, state, suspend_count, idx, and the slot's running_idx. + // Read-only — no guest state mutation, no scheduler-behavior change — so + // the lockstep digest is unaffected. Not gated on `--quiet` so the + // documented headless invocation still emits it. + if std::env::var("XENIA_DUMP_SLOTS").as_deref() == Ok("1") { + println!("\n=== SLOT DUMP (XENIA_DUMP_SLOTS) ==="); + for (hw_id, slot) in kernel.scheduler.slots.iter().enumerate() { + println!(" hw={} running_idx={:?} depth={}", hw_id, slot.running_idx, slot.runqueue.len()); + for (idx, t) in slot.runqueue.iter().enumerate() { + println!( + " idx={} tid={} handle={:?} state={:?} suspend_count={} pri={} mask={:#04x} pc={:#010x}", + idx, + t.tid, + t.thread_handle.map(|h| format!("{:#06x}", h)), + t.state, + t.suspend_count, + t.priority, + t.affinity_mask, + t.ctx.pc, + ); + } + } + println!("=== END SLOT DUMP ===\n"); + } + // Toolkit-audit fix (2026-06-21): only the ALWAYS-ON thread/waiter table // is suppressed by `--quiet`. The explicitly-armed diagnostics below // (`--trace-handles`, `--trace-handles-focus`, `--dump-addr`) are diff --git a/crates/xenia-app/tests/sylpheed_oracles.rs b/crates/xenia-app/tests/sylpheed_oracles.rs index d2fea29..464dcfc 100644 --- a/crates/xenia-app/tests/sylpheed_oracles.rs +++ b/crates/xenia-app/tests/sylpheed_oracles.rs @@ -24,10 +24,25 @@ use std::process::Command; -const ISO_DEFAULT: &str = "/home/fabi/RE Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"; +const ISO_DEFAULT: &str = "/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"; +/// Resolve the Sylpheed ISO, in priority order: +/// 1. `SYLPHEED_ISO` env var (explicit override), +/// 2. the repo-root `sylpheed.iso` symlink (the documented per-machine +/// setup — see HANDOFF §0b), so a standard checkout runs without env vars, +/// 3. a last-resort absolute default. +/// Returns the first existing path, else the default string so the caller's +/// existence check still produces the "SKIPPING" message. fn iso_path() -> String { - std::env::var("SYLPHEED_ISO").unwrap_or_else(|_| ISO_DEFAULT.to_string()) + if let Ok(p) = std::env::var("SYLPHEED_ISO") { + return p; + } + // Repo root is two levels up from this crate's manifest dir. + let symlink = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../sylpheed.iso"); + if symlink.exists() { + return symlink.to_string_lossy().into_owned(); + } + ISO_DEFAULT.to_string() } fn run_oracle(label: &str, max_instr: u64, golden_rel: &str) { diff --git a/crates/xenia-kernel/src/state.rs b/crates/xenia-kernel/src/state.rs index 50bb967..c15a6ad 100644 --- a/crates/xenia-kernel/src/state.rs +++ b/crates/xenia-kernel/src/state.rs @@ -1307,20 +1307,68 @@ impl KernelState { if let Some((reg, deref_off)) = self.audit_deref { use std::fmt::Write as _; let base = ctx.gpr[reg as usize] as u32; - let dump64 = |label: &str, p: u32| { - let mut s = String::with_capacity(256); + let dumpn = |label: &str, p: u32, len: u32| { + let mut s = String::with_capacity(256 + (len as usize) * 4); let _ = write!(&mut s, "AUDIT-DEREF {} ptr={:#010x}", label, p); let mut o: u32 = 0; - while o < 64 { + while o < len { let _ = write!(&mut s, " +0x{:02x}={:#010x}", o, mem.read_u32(p.wrapping_add(o))); o += 4; } println!("{}", s); }; println!("AUDIT-DEREF-HEAD pc={:#010x} tid={} cycle={} reg=r{} off=0x{:x}", pc, tid, cycle, reg, deref_off); - dump64("item", base); + dumpn("item", base, 64); let sub = mem.read_u32(base.wrapping_add(deref_off)); - dump64("sub", sub); + // STEP 42 — dump the demux-state object deep (to +0xfc): cursors + // [+0x10/0x18/0x20], state [+0x50], source handle [+0x94], count + // [+0x98], gate flags [+0xb8/bc/cc/d4]. Then follow the buffer + // descriptor [sub+4] (bounds at +0x10/+0x14) which defines the + // window of file bytes available to the demux at the cursor. + dumpn("sub", sub, 0x100); + let bufdesc = mem.read_u32(sub.wrapping_add(4)); + dumpn("bufdesc", bufdesc, 0x40); + // STEP 43 — follow the byte-source sub-objects that actually hold + // the windowed file bytes + read position: [sub+0] (byte-source / + // embedded vtable obj 0x40553640), [sub+0x30] (0x40688560), + // [sub+0x94] (source handle 0x41073e40). One holds the data ptr + + // cursor; an empty/short buffer here is the no-data root. + let src0 = mem.read_u32(sub); + dumpn("src0", src0, 0x80); + dumpn("src30", mem.read_u32(sub.wrapping_add(0x30)), 0x80); + dumpn("src94", mem.read_u32(sub.wrapping_add(0x94)), 0x80); + // STEP 44 — the byte-source's buffer/cursor holder [src0+0x2c] + // (=0x40930f60): vtable[4]=0x82524080 reads its file-offset cursor + // [+0x28], cached-range [+0x80]/[+0x88], flags [+0x40], and maps + // offsets to memory blocks via 0x82522118 (the windowed/chunk + // buffer). An empty cached range / null block ptr here = the data + // byte-source never fed → 0x80500000. Follow its block ptr [+0x18]. + let cur = mem.read_u32(src0.wrapping_add(0x2c)); + dumpn("src0_2c", cur, 0x100); + // STEP 45 — walk the cached-block linked list exactly as the + // mapper 0x82522118 does: container=[cur+0x10]; node=[container+8]; + // each node: block=[node+0] (block[+8]=start off64, block[+4]=size), + // next=[node+8]. Print each block's 64-bit start + size so we can + // see whether ANY cached block covers file offset 5868 (the first + // ASF data packet). Empty list / no covering block = the root. + let container = mem.read_u32(cur.wrapping_add(0x10)); + dumpn("blkcont", container, 0x20); + let mut node = mem.read_u32(container.wrapping_add(8)); + let mut k = 0; + while node != 0 && k < 8 { + let block = mem.read_u32(node); + let start_hi = mem.read_u32(block.wrapping_add(8)); + let start_lo = mem.read_u32(block.wrapping_add(0xc)); + let size = mem.read_u32(block.wrapping_add(4)); + let dptr = mem.read_u32(block); + println!( + "AUDIT-BLK k={} node={:#010x} block={:#010x} start={:#010x}_{:#010x} size={:#010x} dataptr={:#010x}", + k, node, block, start_hi, start_lo, size, dptr, + ); + node = mem.read_u32(node.wrapping_add(8)); + k += 1; + } + println!("AUDIT-BLK-END count={}", k); let vt = mem.read_u32(sub); // [sub+0] = vtable // Dump 48 vtable slots so slot 28 (+0x70) and slot 36 (+0x90) show. let mut s = String::with_capacity(512);