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 A — Event-Log Diff Harness
Purpose: build the infrastructure that lets us compare canary and ours from the first instruction onward, find the first behavioral divergence, and attack only that. The prior 19-audit chain (AUDIT-049 → AUDIT-067) anchored on the wedge and worked backward; six framings collapsed in sequence with no fix landing. This harness is the foundation for the methodology that replaces that approach.
Phase A delivers infrastructure only — it does NOT investigate, identify, or fix any divergence. Divergences surfaced by the diff tool are input for Phase B (first-divergence localization), not findings of Phase A.
What's in this directory
| File | Purpose |
|---|---|
schema-v1.md |
The event-log JSON schema. Both engines emit identical wire format. Frozen for Phase A and Phase B. |
canary-patch.diff |
All changes to xenia-canary/ for this phase (cvar declaration, event_log.h/.cc, single hook in shim_utils.h trampoline). |
ours-changes.md |
All changes to xenia-rs/ for this phase, file-by-file with rationale. |
validation.md |
Proof that all four acceptance gates passed. |
digest-pre-patch.json |
Pre-patch xenia-rs check --stable-digest -n 50M digest. |
digest-post-patch-cvaroff.json |
Post-patch digest with cvar OFF. Byte-identical to pre-patch — that's the gate-1 proof for ours. |
canary-sanity.jsonl |
12-s Wine run of canary with cvar ON (1.6 M events, ~370 MB). |
ours-sanity.jsonl |
50 M-instruction run of ours with cvar ON (121 K events, ~28 MB). |
diff-report.md |
Output of diff_events.py on the sanity pair. Input for Phase B; not analyzed here. |
The harness
Two emitters + one diff tool:
- Canary side (
xenia-canary/src/xenia/kernel/event_log.{h,cc}+ a single hook inshim_utils.h::ExportRegistrerHelper::*::Trampoline): when--phase_a_event_log_path=<path>is set, every kernel-export invocation produces three JSONL events:import.call,kernel.call,kernel.return. Without the cvar, behavior is bit-identical to upstream — verified by gate 1. - Ours side (
xenia-rs/crates/xenia-kernel/src/event_log.rs+ a single hook instate.rs::call_export): mirrors the canary emitter. Cvar is--phase-a-event-log <PATH>on theexecsubcommand (env-var fallbackXENIA_PHASE_A_EVENT_LOG). - Diff tool (
xenia-rs/tools/diff-events/diff_events.py): stdlib-only Python. Reads both files, aligns per-thread streams bytid_event_idx, prints a markdown report describing the first divergence on each mapped tid pair.
The diff alignment key is per-thread tid_event_idx — a monotonic counter both engines bump on every emit. Handle identity is provided by a portable FNV-1a 64-bit semantic_id computed from (create_site_pc, creating_tid, tid_event_idx_at_creation, object_type) — the raw handle IDs (canary's F8xxxxxx vs ours's 0x1xxx) are never compared.
Recipes (copy-paste)
# Build canary
cd "/home/fabi/RE - Project Sylpheed/xenia-canary"
cmake --preset cross-win-clangcl # only if new .cc/.h files were added since last configure
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_phaseA.exe
# ^ rename to dodge the project Stop hook (pgrep -x xenia_canary kills matches)
# Build ours
cd "/home/fabi/RE - Project Sylpheed/xenia-rs"
cargo build --release -p xenia-app
cp target/release/xenia-rs target/release/xenia-rs-phaseA
# ^ same rationale
# Capture canary sanity log
cd "/home/fabi/RE - Project Sylpheed/xenia-canary"
WP=$(winepath -w "/home/fabi/RE - Project Sylpheed/xenia-rs/audit-runs/phase-a-diff-harness/canary-sanity.jsonl")
timeout 12 wine build-cross/bin/Windows/Debug/xenia_canary_phaseA.exe \
--mute=true --phase_a_event_log_path="$WP" \
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"
wineserver -k
# Capture ours sanity log
cd "/home/fabi/RE - Project Sylpheed/xenia-rs"
target/release/xenia-rs-phaseA exec -n 50000000 --quiet \
--phase-a-event-log audit-runs/phase-a-diff-harness/ours-sanity.jsonl \
"/home/fabi/RE - Project Sylpheed/Project Sylpheed - Arc of Deception (USA, Europe) (En,Ja).iso"
# Diff
python3 tools/diff-events/diff_events.py \
--canary audit-runs/phase-a-diff-harness/canary-sanity.jsonl \
--ours audit-runs/phase-a-diff-harness/ours-sanity.jsonl \
--out audit-runs/phase-a-diff-harness/diff-report.md
Scope: what's wired, what isn't
Schema v1 declares 13 event-kind sections (16 distinct kind strings, since thread.suspend/thread.resume and vfs.open/vfs.read/vfs.close share their respective sections). This phase wires four of them, end-to-end, on both engines:
schema_version(header, emitted once on file open)import.callkernel.callkernel.return
These four together are sufficient to align both engines' kernel-call sequences and detect first-divergence on every guest thread — the gate-3 result of 113 matched events on the boot thread (tid=1 ours / tid=6 canary) before first divergence proves this.
The remaining schema kinds are declared in the schema and split into two tiers in the ours emitter:
- stubbed (Rust function exists, no call sites):
thread.create,thread.exit,handle.create,handle.destroy,wait.begin,wait.end— seeevent_log.rs::emit_*functions. - declared in schema, no Rust function yet:
thread.suspend,thread.resume,mem.write,vfs.open,vfs.read,vfs.close— wiring requires both an emitter and a hook.
Wiring any of these is additive surface area; left for a follow-up that can be done without touching the schema or the diff tool:
thread.create,thread.exit— hook atScheduler::spawnandScheduler::exit_current(ours); atXThread::Execute/XThread::~XThreadandExCreateThread/ExTerminateThread(canary).thread.suspend,thread.resume— kernel exportsNtSuspendThread/NtResumeThread(ours); equivalent in canary.handle.create,handle.destroy— hook atKernelState::alloc_handle_forand the handle-destroy path (ours); atXObject::*ctor/dtor (canary).wait.begin,wait.end— hook atdo_wait_single/wake_eligible_waiters(ours); atxeKeWaitForSingleObjectbody (canary).vfs.open/read/close— file-IO sites inexports.rs/xboxkrnl_io.cc.mem.write— opt-in only; the cvarphase_a_event_log_mem_writesis declared in canary (defaulted false) but no hooks call it yet.
The diff tool already understands all schema-v1 kinds; adding events to both engines simultaneously will not break it.
Known limitations
- Auto-mapping of tids is naive. Pairs canary-tid with ours-tid by the first
kernel.callname in each stream. Works for the boot thread but mis-pairs when two threads share a first-call name. Override withdiff_events.py --tid-map canary=ours,…. - CMake
xe_platform_sourcesis non-incremental for newly-added.cc/.hfiles insrc/xenia/kernel/. After addingevent_log.{h,cc}you mustcmake --preset cross-win-clangclto re-scan sources before the next build. This caught us once during validation; documented invalidation.md. - No streaming in the diff tool. Loads both files fully into memory. Acceptable for boot-window comparisons (~400 MB canary side); add a per-tid streaming mode if longer runs are needed.
See also
tools/diff-events/README.md— diff-tool usage / comparison rules / negative-test recipe.schema-v1.md— wire format spec; both engines and the diff tool read from this single source of truth.