Files
xenia-rs/audit-runs/phase-b-state-equivalence/README.md
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

140 lines
8.9 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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=<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`](../../../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 <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.