# 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`](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`](ours-changes.md) | All changes to `xenia-rs/` for this phase, file-by-file with rationale. |
| [`validation.md`](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`](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}`](../../../../xenia-canary/src/xenia/kernel/phase_b_snapshot.h) + a single hook in [`xthread.cc::Execute`](../../../../xenia-canary/src/xenia/kernel/xthread.cc#L583)): when `--phase_b_snapshot_dir=
` is set, immediately before the JIT executes the first guest instruction at `entry_point()`, write five JSON files + manifest under `/canary/`. Without the cvar, behavior is bit-identical to upstream.
- **Ours side** ([`crates/xenia-kernel/src/phase_b_snapshot.rs`](../../../crates/xenia-kernel/src/phase_b_snapshot.rs) + a single hook in [`crates/xenia-app/src/main.rs::worker_prologue`](../../../crates/xenia-app/src/main.rs)): mirrors the canary emitter. CLI flag is `--phase-b-snapshot-dir ` on the `exec` subcommand (env-var fallback `XENIA_PHASE_B_SNAPSHOT_DIR`).
- **Diff tool** ([`tools/diff-state/diff_state.py`](../../../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`](../../../../xenia-canary/src/xenia/kernel/xthread.cc#L583), 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`](../../../crates/xenia-app/src/main.rs), 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)
```bash
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
```bash
# 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`](../../../../.claude/projects/-home-fabi-RE---Project-Sylpheed/memory/feedback_stop_hook_kills_xenia_rs.md).
Every canary invocation uses `--mute=true` per
[`feedback_canary_mute_default.md`](../../../../.claude/projects/-home-fabi-RE---Project-Sylpheed/memory/feedback_canary_mute_default.md).
### Snapshot canary
```bash
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
```bash
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
```bash
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
- [Phase A README](../phase-a-diff-harness/README.md) — the upstream event-log harness Phase B builds on.
- [`tools/diff-state/README.md`](../../../tools/diff-state/README.md) — diff-tool usage / classification rules / negative-test recipe.