Files
xenia-rs/audit-runs/phase-b-state-equivalence
MechaCat02 ef93a4fa14 handoff: VSync/event-wedge fixes + iterate 2.A–2.BC research notes
Source changes (dormant parity infra, retained from iterate 2.AI/2.AO):
- xenia-kernel/exports.rs: nt_create_event manual_reset polarity +
  related event wiring
- xenia-gpu/mmio_region.rs: D1MODE_VBLANK_VLINE_STATUS hardcode parity

Also lands the audit-runs/ analysis notes (.md/.txt/.json digests) for the
iterate 2.x VSync/0x10e8/0x1004 wedge investigation. Raw trace dumps
(.jsonl/.gz/.csv/.stdout) and agent worktrees (.claude/) are gitignored as
regenerable local artifacts — see memory + HANDOFF for the running findings.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-05 07:19:08 +02:00
..

Phase B — Initial-state equivalence snapshot & diff

Purpose. Build the infrastructure that catalogs every observable state difference between xenia-canary and xenia-rs at the moment immediately before the first guest PPC instruction of the XEX entry_point executes. Without a baseline equivalence proof, Phase C's "first runtime divergence" claim from the Phase A event-log harness is meaningful only relative to whatever baseline the snapshot point actually captures.

Phase B is purely catalog. It does not investigate divergences, does not hypothesize causes, and does not propose fixes. Those belong to Phase C+. The 19-audit anchor-on-wedge failure (AUDIT-049→067) is the reason this discipline is enforced.

What's in this directory

File Purpose
canary-patch.diff All changes to xenia-canary/ for this phase — cvar declarations, phase_b_snapshot.{h,cc}, single hook in xthread.cc::Execute.
ours-changes.md All changes to xenia-rs/ for this phase, file-by-file with rationale.
validation.md Proof that all five acceptance gates passed (cvar-OFF determinism, well-formedness, hash-determinism, invariants, negative test).
digest-post-phaseB-cvaroff.json Post-Phase-B xenia-rs check --stable-digest -n 50M digest with cvar OFF. Byte-identical to Phase A's baseline → Phase B gate 1 PASS for ours.
snap-001/canary/ Full canary snapshot dir (5 JSON files + manifest).
snap-001/ours/ Full ours snapshot dir (5 JSON files + manifest).
report.md Output of diff_state.py on the sanity pair — the Phase B divergence catalog. STOP-class result on image_loaded_sha256.
report.json Machine-readable sibling of report.md.

The harness

Two emitters + one diff tool:

  • Canary side (src/xenia/kernel/phase_b_snapshot.{h,cc} + a single hook in xthread.cc::Execute): when --phase_b_snapshot_dir=<dir> is set, immediately before the JIT executes the first guest instruction at entry_point(), write five JSON files + manifest under <dir>/canary/. Without the cvar, behavior is bit-identical to upstream.
  • Ours side (crates/xenia-kernel/src/phase_b_snapshot.rs + a single hook in crates/xenia-app/src/main.rs::worker_prologue): mirrors the canary emitter. CLI flag is --phase-b-snapshot-dir <DIR> on the exec subcommand (env-var fallback XENIA_PHASE_B_SNAPSHOT_DIR).
  • Diff tool (tools/diff-state/diff_state.py): stdlib-only Python. Reads both snapshot dirs, walks each file applying the field-skip / set-vs-sequence rules, classifies divergences (σ-structural / δ-content / γ-kernel-content / κ-cache / ε-host-allocator / τ-host-timing), exits 2 on STOP-class.

Snapshot point — equivalence claim

The hooks fire at equivalent moments in both engines:

  • Canary: at xthread.cc:583, one line before processor()->Execute(thread_state_, address, args.data(), args.size()). Guard: address == GetExecutableModule()->entry_point().
  • Ours: in worker_prologue at main.rs:2228, one block after let pc = kernel.scheduler.ctx(hw_id).pc;. Guard: pc == kernel.entry_pc && current_tid == INITIAL_GUEST_TID.

Validation gate 4 reads both cpu_state.pc files and confirms they equal config.xex_entry_point in their respective engine: PASS.

Recipes (copy-paste)

Pre-clean caches (NOT optional per spec)

rm -rf ~/.local/share/Xenia/cache/      # canary persistent cache
# ours cache root is tmpfs-default per AUDIT-038. Verify XENIA_CACHE_PERSIST is unset.

Build

# Canary — reconfigure required after adding phase_b_snapshot.{h,cc} (CMake
# xe_platform_sources is non-incremental for new sources).
cd xenia-canary
cmake --preset cross-win-clangcl
cmake --build build-cross --preset cross-debug --target xenia-app -j$(nproc)
cp build-cross/bin/Windows/Debug/xenia_canary.exe \
   build-cross/bin/Windows/Debug/xenia_canary_phaseB.exe

# Ours
cd ../xenia-rs
cargo build --release -p xenia-app
cp target/release/xenia-rs target/release/xenia-rs-phaseB

Renamed binaries (xenia_canary_phaseB.exe, xenia-rs-phaseB) dodge the project Stop hook per feedback_stop_hook_kills_xenia_rs.md. Every canary invocation uses --mute=true per feedback_canary_mute_default.md.

Snapshot canary

SNAP="$(pwd)/audit-runs/phase-b-state-equivalence/snap-001"
mkdir -p "$SNAP"
cd ../xenia-canary
WP=$(winepath -w "$SNAP")
wine build-cross/bin/Windows/Debug/xenia_canary_phaseB.exe \
  --mute=true \
  --phase_b_snapshot_dir="$WP" \
  --phase_b_snapshot_and_exit=true \
  "../Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"
wineserver -k

Snapshot ours

cd ../xenia-rs
./target/release/xenia-rs-phaseB exec --quiet \
  --phase-b-snapshot-dir "$SNAP" --phase-b-snapshot-and-exit \
  "../Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"

Diff

python3 tools/diff-state/diff_state.py \
  --canary "$SNAP/canary" \
  --ours   "$SNAP/ours" \
  --out    "$SNAP/../report.md"

Exit code: 0 (no divergence) / 1 (divergences found) / 2 (STOP triggered). The Phase B sanity run produces exit 2 — see report.md for the breakdown.

Phase C handoff

The first divergence Phase C should look at is the image_loaded_sha256 mismatch between the two engines. Canary and ours both reach the same entry_pc = 0x824ab748 but their loaded PE images don't match byte-for-byte. Phase B does not interpret this. Phase C should:

  1. Re-run both engines with --phase-b-dump-section-content set.
  2. Open the resulting memory.json::section_contents[] arrays in both files and binary-diff each region with the same (start, end).
  3. Determine which sections (.text, .rdata, etc.) actually differ, and whether the difference is a relocation, a byte-level XEX decoder discrepancy, or a section-table layout choice.

Until that's resolved, downstream Phase C investigation of the 57 other divergences risks anchoring on symptoms — exactly the failure pattern Phase B was built to avoid.

Scope: what's wired, what isn't

  • Wired end-to-end in both engines: cpu_state, memory, kernel-objects, vfs-probes, config. Five files + manifest with SHA-256.
  • Section-content dump (--phase-b-dump-section-content): cvar declared, plumbed, default OFF. Both engines emit section_contents: null by default. The escape-hatch wiring is in place for Phase C's binary-diff use; the actual bytes aren't dumped in the sanity run.
  • Walk-the-full-committed-page-set on both engines: deliberately NOT done. Canary's QueryRegionInfo reports COMMIT for some host-uncommitted pages (physical heap mirrors, low-system-heap reserve), and ours's is_mapped analogously reports COMMIT for some addresses whose mmap'd page hasn't been touched yet. Reading those addresses faults. The named-region scheme — XEX image + main stack + PCR + TLS — captures the cross-engine-comparable memory without crash risk.

Known limitations

  • Order of objects[] in kernel.json is canonical: sorted by handle_semantic_id. The semantic_id is computed from (type_code, raw_handle) at snapshot time, not from Phase A's (create_site_pc, tid, tid_event_idx_at_creation, type). Reason: objects alive at the snapshot point were minted before Phase B instrumentation could capture the creation tuple. The simpler-formula semantic_id is stable within a single engine but does not correlate one-to-one across engines. The diff tool documents this and treats the objects[] array as a set (sort-and-compare); structural divergences are still surfaced, but γ-content divergences inside matching pairs are not (because the pairing is heuristic, not principled).
  • Canary's PPCContext doesn't expose a pc field. The snapshot emits cpu_state.pc = entry_address (the arg passed to processor()->Execute), which is the about-to-execute PC by definition at the hook point. This is documented in validation.md.
  • Wine prefix vkd3d/dxvk caches are not pre-cleaned by the recipe above. Phase B was run on a warm prefix; cold-prefix runs may produce a single additional vkd3d-related VFS entry. Negligible for this gate; pre-clean explicitly if desired.

See also