intro-video RE: deep demux-core instrumentation + handoff hygiene

Investigation probes for the ADV.wmv intro-video deadlock (feeder's demux
source-read 0x825211a0 returns no-data 0x80500000 where canary returns 0).

- xenia-kernel/state.rs: extend AUDIT-DEREF (fire_audit_pc_probe_if_match) to
  deep-dump the demux-state object (to +0xfc), its buffer descriptor [sub+4],
  the byte-source sub-objects ([sub+0]/[+0x30]/[+0x94] and [src0+0x2c]), and
  to WALK the windowed-buffer cached-block linked list (AUDIT-BLK: per-block
  64-bit start/size/dataptr) exactly as mapper 0x82522118 does. Read-only,
  env-gated (XENIA_AUDIT_DEREF); lockstep digest unaffected.
- xenia-app/main.rs: XENIA_DUMP_SLOTS observe-only scheduler runqueue dump.
- xenia-app/tests/sylpheed_oracles.rs: resolve ISO via SYLPHEED_ISO, then the
  repo-root sylpheed.iso symlink, then default; fix path typo.
- .gitignore: exclude .claude/ (71k files / 66GB agent worktrees) and the
  local investigation artifacts (audit-runs/, exit-thread-state.json, zq_*.py).

Findings (full chain in handoff notes): all ASF header metadata parses
correctly (packet size 16415, count 4728, 2 streams); cursors correct
(cur=5868, limit=77.6M); the windowed buffer works and slides (512B blocks);
no-data originates deep in the byte-read/parse chain, not from an empty buffer.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-06-26 07:54:15 +02:00
parent 23189b95af
commit 7e9ee1ac33
4 changed files with 106 additions and 7 deletions

8
.gitignore vendored
View File

@@ -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

View File

@@ -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

View File

@@ -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) {

View File

@@ -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);