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>
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 inxthread.cc::Execute): when--phase_b_snapshot_dir=<dir>is set, immediately before the JIT executes the first guest instruction atentry_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 incrates/xenia-app/src/main.rs::worker_prologue): mirrors the canary emitter. CLI flag is--phase-b-snapshot-dir <DIR>on theexecsubcommand (env-var fallbackXENIA_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 beforeprocessor()->Execute(thread_state_, address, args.data(), args.size()). Guard:address == GetExecutableModule()->entry_point(). - Ours: in
worker_prologueatmain.rs:2228, one block afterlet 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:
- Re-run both engines with
--phase-b-dump-section-contentset. - Open the resulting
memory.json::section_contents[]arrays in both files and binary-diff each region with the same(start, end). - 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 emitsection_contents: nullby 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
QueryRegionInforeports COMMIT for some host-uncommitted pages (physical heap mirrors, low-system-heap reserve), and ours'sis_mappedanalogously 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[]inkernel.jsonis canonical: sorted byhandle_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 theobjects[]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
pcfield. The snapshot emitscpu_state.pc = entry_address(the arg passed toprocessor()->Execute), which is the about-to-execute PC by definition at the hook point. This is documented invalidation.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
- Phase A README — the upstream event-log harness Phase B builds on.
tools/diff-state/README.md— diff-tool usage / classification rules / negative-test recipe.