# 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.