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>
This commit is contained in:
MechaCat02
2026-06-05 07:19:08 +02:00
parent acd1656753
commit ef93a4fa14
620 changed files with 108303 additions and 1 deletions

View File

@@ -0,0 +1,66 @@
# Phase C+13 broad-impact assessment
## Resolved
| event | before | after |
|---|---|---|
| Main idx 103862 `NtCreateFile(game:\\dat\\files.tbl)` | ours=`0x00000000` (synth-success) / canary=`0xc0000034` | ours=`0xc0000034` matches canary bit-for-bit |
## Advanced
| metric | before | after | delta |
|---|---|---|---|
| Main cold-vs-cold matched prefix (canary tid=6 → ours tid=1) | 103,862 | 104,574 | **+712 events** |
| Stable digest `imports` | 40,447 | 40,390 | -57 |
| Stable digest `instructions` | 50,000,002 | 50,000,007 | +5 |
`imports -57` is the population of previously-masked missing-disc-file
probes: each one used to emit a synth-success kernel.call/return pair
plus an `import.call`, ours now emits just a NOT_FOUND kernel.return.
Net 57 import-call slots freed = ~28 distinct missing-file paths that
the boot validator now sees as NOT_FOUND.
## Persisted (sister chains, all unchanged)
| canary_tid | ours_tid | matched | notes |
|---|---|---|---|
| 4 | 11 | 9 | no divergence in compared events |
| 7 | 2 | 29 | no divergence in compared events |
| 12 | 7 | 2 | pre-existing KeWaitForSingleObject TIMEOUT/SUCCESS |
| 14 | 9 | 39 | pre-existing XAudio init divergence |
| 15 | 10 | 15 | no divergence in compared events |
No sister-chain regression. The fix is scoped to `open_vfs_file`'s
disc-path arm; sister tids don't touch this code path during their
captured windows.
## NEW (surfaced by the fix)
| location | observation |
|---|---|
| Main idx 104574 | Ordinal-of-call mismatch: canary does **two** sequential `RtlEnterCriticalSection` (104571, 104574); ours does one `Enter` then `Leave`. Likely an error-handling path in Sylpheed's boot validator that takes a nested lock on disc-file NOT_FOUND. |
This is the expected pattern: scoping the synth-empty surfaces the
code branch that handles missing disc files honestly. The new
divergence is at the next layer of the boot validator's error-path
logic and becomes the Phase C+14 target.
## Risk: did anything regress in non-cold scenarios?
- **Kernel unit tests**: 177 → 181 pass; no failures. The new
`nt_create_file_non_disc_prefix_missing_still_synthesizes` test
proves the legacy synth-empty path is preserved for non-disc
prefixes (i.e. writable system partitions).
- **Phase B image hash**: unchanged. The fix doesn't touch image
loading.
- **Lockstep goldens**: `--stable-digest` 3× determinism PASS; no
jitter introduced. The C+12 baseline digest was already a
stable-mode reference, so the new digest just becomes the C+13
baseline.
## Summary
One divergence Resolved (idx 103862). +712 events Advanced on the
main chain. Zero sister regressions. One NEW divergence at idx
104574 — a higher-quality target (error-handling path divergence)
than a synth-empty masked divergence.

View File

@@ -0,0 +1,104 @@
# Cold-vs-cold result — Phase C+13 (2026-05-14)
| canary_tid | ours_tid | matched | first_divergence | event-kind at divergence |
|---|---|---|---|---|
| 6 | 1 | **104574** | 104574 | `import.call`: canary=RtlEnterCriticalSection (ord 293) / ours=RtlLeaveCriticalSection (ord 304) — NEW divergence, surfaced by C+13 fix |
| 4 | 11 | 9 | — | no divergence in 9 events — unchanged from C+12 |
| 7 | 2 | 29 | — | no divergence in 29 events — unchanged from C+12 |
| 12 | 7 | 2 | 2 | pre-existing KeWaitForSingleObject TIMEOUT/SUCCESS — unchanged |
| 14 | 9 | 39 | 39 | pre-existing XAudio init — unchanged |
| 15 | 10 | 15 | — | no divergence in 15 events — unchanged |
Main matched-prefix advance: **+712 events** (103,862 → 104,574).
## New cold digest baseline
`e1dfcb1559f987b35012a7f2dc6d93f5` (was `ad4f74ee324fdedb0bfdd4cc4c6468e9`).
Stable-counter delta vs C+12 (digest-cold-stable-1.json):
```
instructions: 50000002 → 50000007 (+5)
imports: 40447 → 40390 (-57)
unimpl: 0 → 0
draws: 0 → 0
swaps: 1 → 1
unique_render_targets: 0 → 0
shader_blobs_live: 0 → 0
texture_cache_entries: 0 → 0
```
The `imports` drop is the expected signature of the fix:
where ours previously synth-empty'd missing disc paths
(emitting an extra kernel.call/return pair), it now returns
NOT_FOUND inline. The net `imports` delta (-57) suggests roughly
~57 missing-disc-file probes per cold boot were previously
masked.
## Phase B `image_loaded_sha256`
`ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18`
(unchanged from C+12 / C+11.1 / C+11 — the engine fix is observation-
only on the loaded image bytes).
## Verification of the divergence resolution at idx 103862
Ours (post-fix) emission at idx 103862:
```json
{
"kind": "kernel.return",
"tid": 1,
"tid_event_idx": 103862,
"payload": {
"name": "NtCreateFile",
"return_value": 18446744072635809844,
"status": "0xc0000034",
"side_effects": []
}
}
```
Canary (oracle) emission at idx 103862:
```json
{
"kind": "kernel.return",
"tid": 6,
"tid_event_idx": 103862,
"payload": {
"name": "NtCreateFile",
"return_value": 18446744072635809844,
"status": "0xc0000034",
"side_effects": []
}
}
```
Bit-identical match — `STATUS_OBJECT_NAME_NOT_FOUND` (0xC0000034)
on both sides for the missing `game:\dat\files.tbl` open.
## New first divergence (next target)
`tid_event_idx=104574`: ordinal-of-call mismatch in a tight
`RtlEnterCriticalSection` / `RtlLeaveCriticalSection` sequence.
Pre-context (last 5 matching events both engines):
```
[104569] kernel.call RtlLeaveCriticalSection
[104570] kernel.return RtlLeaveCriticalSection
[104571] import.call RtlEnterCriticalSection
[104572] kernel.call RtlEnterCriticalSection
[104573] kernel.return RtlEnterCriticalSection
```
Divergent event:
```
canary: [104574] import.call RtlEnterCriticalSection
ours: [104574] import.call RtlLeaveCriticalSection
```
Reading: canary does **two** sequential `RtlEnterCriticalSection`
calls (nested-lock pattern around the NOT_FOUND error-handling
path); ours does **one** Enter followed by a Leave (single-lock
pattern). This is downstream code branching differently — likely
an error-handling path in the boot validator that takes a
double-lock when a disc lookup misses. Investigate the function
on whose path Sylpheed is at `pc` 104574 in ours.

View File

@@ -0,0 +1,131 @@
# Phase A diff report
**This report is the output of Phase A's diff harness. Divergences
shown here are INPUT for Phase B (first-divergence localization),
not findings of Phase A.** Phase A's job is to make the harness
itself correct, not to analyze what it surfaces.
## Summary
| canary_tid | ours_tid | matched | canary_total | ours_total | first_divergence_at |
|---|---|---|---|---|---|
| 4 | 11 | 9 | 20000 | 9 | — |
| 6 | 1 | 104574 | 250000 | 108408 | 104574 |
| 7 | 2 | 29 | 29 | 30 | — |
| 12 | 7 | 2 | 11783 | 3 | 2 |
| 14 | 9 | 39 | 20000 | 75 | 39 |
| 15 | 10 | 15 | 20000 | 15 | — |
## canary_tid=4 → ours_tid=11
No divergence within the 9 compared events (canary has 20000, ours has 9).
## canary_tid=6 → ours_tid=1
First divergence at `tid_event_idx=104574`: payload.ord: canary=293 ours=304
**Pre-context (last 5 matching events):**
```
canary: [104569] kernel.call RtlLeaveCriticalSection
ours: [104569] kernel.call RtlLeaveCriticalSection
canary: [104570] kernel.return RtlLeaveCriticalSection
ours: [104570] kernel.return RtlLeaveCriticalSection
canary: [104571] import.call RtlEnterCriticalSection
ours: [104571] import.call RtlEnterCriticalSection
canary: [104572] kernel.call RtlEnterCriticalSection
ours: [104572] kernel.call RtlEnterCriticalSection
canary: [104573] kernel.return RtlEnterCriticalSection
ours: [104573] kernel.return RtlEnterCriticalSection
```
**Divergent event:**
```
canary: [104574] import.call RtlEnterCriticalSection
ours: [104574] import.call RtlLeaveCriticalSection
```
**Next event after the divergence (if any):**
```
canary: [104575] kernel.call RtlEnterCriticalSection
ours: [104575] kernel.call RtlLeaveCriticalSection
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 846769100, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "RtlEnterCriticalSection", "ord": 293}, "schema_version": 1, "tid": 6, "tid_event_idx": 104574}
{"deterministic": true, "engine": "ours", "guest_cycle": 5517276, "host_ns": 483675408, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "RtlLeaveCriticalSection", "ord": 304}, "schema_version": 1, "tid": 1, "tid_event_idx": 104574}
```
## canary_tid=7 → ours_tid=2
No divergence within the 29 compared events (canary has 29, ours has 30).
## canary_tid=12 → ours_tid=7
First divergence at `tid_event_idx=2`: payload.return_value: canary=258 ours=0
**Pre-context (last 5 matching events):**
```
canary: [0] import.call KeWaitForSingleObject
ours: [0] import.call KeWaitForSingleObject
canary: [1] kernel.call KeWaitForSingleObject
ours: [1] kernel.call KeWaitForSingleObject
```
**Divergent event:**
```
canary: [2] kernel.return KeWaitForSingleObject
ours: [2] kernel.return KeWaitForSingleObject
```
**Next event after the divergence (if any):**
```
canary: [3] import.call RtlEnterCriticalSection
ours: <end of stream>
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 957667200, "kind": "kernel.return", "payload": {"name": "KeWaitForSingleObject", "return_value": 258, "side_effects": [], "status": "0x00000102"}, "schema_version": 1, "tid": 12, "tid_event_idx": 2}
{"deterministic": true, "engine": "ours", "guest_cycle": 30, "host_ns": 493063903, "kind": "kernel.return", "payload": {"name": "KeWaitForSingleObject", "return_value": 0, "side_effects": [], "status": "0x00000000"}, "schema_version": 1, "tid": 7, "tid_event_idx": 2}
```
## canary_tid=14 → ours_tid=9
First divergence at `tid_event_idx=39`: payload.ord: canary=503 ours=293
**Pre-context (last 5 matching events):**
```
canary: [34] kernel.call KeReleaseSpinLockFromRaisedIrql
ours: [34] kernel.call KeReleaseSpinLockFromRaisedIrql
canary: [35] kernel.return KeReleaseSpinLockFromRaisedIrql
ours: [35] kernel.return KeReleaseSpinLockFromRaisedIrql
canary: [36] import.call KfLowerIrql
ours: [36] import.call KfLowerIrql
canary: [37] kernel.call KfLowerIrql
ours: [37] kernel.call KfLowerIrql
canary: [38] kernel.return KfLowerIrql
ours: [38] kernel.return KfLowerIrql
```
**Divergent event:**
```
canary: [39] import.call XAudioGetVoiceCategoryVolumeChangeMask
ours: [39] import.call RtlEnterCriticalSection
```
**Next event after the divergence (if any):**
```
canary: [40] kernel.call XAudioGetVoiceCategoryVolumeChangeMask
ours: [40] kernel.call RtlEnterCriticalSection
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 1156650800, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "XAudioGetVoiceCategoryVolumeChangeMask", "ord": 503}, "schema_version": 1, "tid": 14, "tid_event_idx": 39}
{"deterministic": true, "engine": "ours", "guest_cycle": 417, "host_ns": 1707576528, "kind": "import.call", "payload": {"module": "xboxkrnl.exe", "name": "RtlEnterCriticalSection", "ord": 293}, "schema_version": 1, "tid": 9, "tid_event_idx": 39}
```
## canary_tid=15 → ours_tid=10
No divergence within the 15 compared events (canary has 20000, ours has 15).

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000007,
"imports": 40390,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000007,
"imports": 40390,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000007,
"imports": 40390,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,110 @@
# Phase C+13 fix.diff — targeted excerpt
# (full tree has many uncommitted prior-phase changes; this file
# documents only the C+13 delta against the pre-C+13 state.)
## crates/xenia-kernel/src/exports.rs
### 1. New helper `is_disc_prefix` (~30 LOC, inserted near `STATUS_OBJECT_NAME_COLLISION`, ~line 1282)
```rust
/// Phase C+13 — does `raw_path` start with a prefix that aliases the
/// (read-only) game disc? Used to scope the synth-empty fallback in
/// `open_vfs_file`: missing disc files report `STATUS_OBJECT_NAME_NOT_FOUND`
/// (matching canary's `NtCreateFile_entry` for game-data lookups), while
/// missing writable-partition paths keep the legacy zero-byte synth.
///
/// Mirrors the disc-mapped subset of `crate::path::DEVICE_PREFIXES`:
/// - `game:\` — canary's symbolic-link alias for the disc
/// (xenia-canary/src/xenia/kernel/kernel_state.cc registrations).
/// - `d:\` / `D:\` — drive-letter alias for the disc.
/// - `\Device\Cdrom0\` — NT device path for the disc.
///
/// Compares case-insensitively to match canary's path resolver.
fn is_disc_prefix(raw_path: &str) -> bool {
let lowered = raw_path.trim_start().to_ascii_lowercase();
const DISC_PREFIXES: &[&str] = &[
"game:\\",
"game:/",
"d:\\",
"d:/",
"\\device\\cdrom0\\",
"\\device\\cdrom0/",
];
DISC_PREFIXES.iter().any(|p| lowered.starts_with(p))
}
```
### 2. `open_vfs_file` — capture raw path (after the `path` initialization, ~line 1314)
```rust
// Phase C+13 — recover the raw (un-stripped) path so we can tell a
// disc-aliased prefix (`game:\`, `d:\`, `\Device\Cdrom0\`) apart from a
// writable-partition prefix (`\Device\Harddisk0\…`, `\??\`, raw "no
// prefix" cases). The synth-empty fallback below covers both today but
// canary's `NtCreateFile_entry` (xboxkrnl_io.cc:83-110) returns the
// VFS lookup status verbatim, which is `STATUS_OBJECT_NAME_NOT_FOUND`
// for any disc path that isn't in the ISO. Scoping the synth to
// non-disc prefixes makes us match canary's behaviour for missing
// game-data files (e.g. `game:\dat\files.tbl` at Phase C+13 idx 103862).
let raw_path = crate::path::object_attributes_raw_name(mem, obj_attrs_ptr)
.unwrap_or_default();
```
### 3. `open_vfs_file` — short-circuit disc paths in the `Err(e)` synth-empty branch (~line 1413)
```rust
Err(e) => {
// Phase C+13 — scope the synth-empty fallback to non-disc
// prefixes only. Canary's `NtCreateFile_entry` returns the VFS
// result verbatim (xboxkrnl_io.cc:83-110); for a missing disc
// file like `game:\dat\files.tbl` that's
// `STATUS_OBJECT_NAME_NOT_FOUND`. Sylpheed handles NOT_FOUND
// cleanly (next event in canary's trace at idx 103862 is
// `RtlNtStatusToDosError(0xc0000034) -> 2`, then the boot
// validator continues), so the synth was masking the
// correct branch.
//
// Synth-empty is still kept for writable system partitions
// (`\Device\Harddisk0\…`, `\Device\Mass*`, `\??\`, raw paths)
// because those aren't backed by the disc — Canary mounts
// them on host directories
// ([xenia_main.cc:612-651](xenia-canary/src/xenia/app/xenia_main.cc));
// ours skips the host mount for those and falls back to the
// legacy stub to avoid regressing audit-006 / audit-018
// disc-validation probes. `cache:/` was already routed to
// `open_cache_file` upstream of this branch (AUDIT-038).
if is_disc_prefix(&raw_path) {
if handle_out != 0 {
mem.write_u32(handle_out, 0);
}
write_io_status_block(
mem,
io_status_block,
STATUS_OBJECT_NAME_NOT_FOUND as u32,
0,
);
tracing::info!(
"Disc path missing: raw={:?} norm={:?} err={} -> NOT_FOUND",
raw_path,
path,
e
);
return STATUS_OBJECT_NAME_NOT_FOUND;
}
// … existing synth-empty branch unchanged …
}
```
### 4. Tests — 4 new tests inserted before `cache_resolve_strips_path_traversal` (~line 7838)
- `is_disc_prefix_recognises_disc_aliases` — unit test for the prefix classifier; covers `game:\`, `D:\`, `\Device\Cdrom0\`, and several non-disc prefixes that MUST return false.
- `nt_create_file_game_prefix_missing_returns_not_found` — primary fix test (idx 103862 in canary's cold trace). Asserts `STATUS_OBJECT_NAME_NOT_FOUND`, null handle, and IOSB.status records NOT_FOUND.
- `nt_create_file_cdrom_prefix_missing_returns_not_found` — `\Device\Cdrom0\` alias variant.
- `nt_create_file_non_disc_prefix_missing_still_synthesizes` — regression guard: a missing `\Device\Harddisk0\Partition1\sys.bin` still gets a zero-byte synth (preserves audit-006 / audit-018 behaviour).
## Summary
Total: ~30 LOC engine code + ~95 LOC tests = ~125 LOC. Kernel tests 177 → 181.
Stable digest shifts: `ad4f74ee324fdedb0bfdd4cc4c6468e9` → `e1dfcb1559f987b35012a7f2dc6d93f5`.
Phase B `image_loaded_sha256` unchanged: `ea8d160e…`.
Main cold-vs-cold matched prefix: **103862 → 104574 (+712)**.

View File

@@ -0,0 +1,110 @@
# Phase C+13 investigation — NtCreateFile `game:\dat\files.tbl`
## Predecessor state (from Phase C+12)
Cold-vs-cold first divergence at `tid_event_idx=103862` on the
canary tid=6 → ours tid=1 main chain:
```
canary: NtCreateFile(path="game:\\dat\\files.tbl") -> 0xc0000034 (STATUS_OBJECT_NAME_NOT_FOUND)
ours: NtCreateFile(path="game:\\dat\\files.tbl") -> 0x00000000 (STATUS_SUCCESS, synth-empty stub)
```
Verified from the C+12 archived JSONLs:
- `xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/ours-cold.jsonl`
events 103860-103862: import.call → kernel.call → kernel.return = 0.
- `xenia-rs/audit-runs/phase-c12-NtQueryFullAttributesFile/canary-cold-tid6-250k.jsonl`
events 103860-103862: same import.call → kernel.call → kernel.return = `0xc0000034`.
Canary's next event (idx 103863): `RtlNtStatusToDosError(0xc0000034) -> 2`
(ERROR_FILE_NOT_FOUND). Sylpheed has proper NOT_FOUND handling and
continues; the synth-empty was masking the correct branch.
## Canary semantics (xboxkrnl_io.cc:39-111)
`NtCreateFile_entry` is a thin wrapper:
1. Translate the `OBJECT_ATTRIBUTES.name` ANSI_STRING to a path.
2. Path validation: `IsValidPath``STATUS_OBJECT_NAME_INVALID` on
reject.
3. Resolve the `root_directory` handle if non-zero.
4. Call `kernel_state()->file_system()->OpenFile(...)`.
5. **Return the `OpenFile` status verbatim.** If the file isn't
present in any mounted device, that's `STATUS_OBJECT_NAME_NOT_FOUND`
(`VirtualFileSystem::ResolvePath` returns nullptr for unresolved
paths, which `OpenFile` translates to NOT_FOUND).
6. On success: alloc handle and write to `handle_out`.
For `game:\dat\files.tbl` specifically: the `game:` symlink points
to the disc device. The disc image ISO doesn't contain `dat/files.tbl`
(disc dump omission — likely a region-specific or build-time-only
file). `OpenFile` returns NOT_FOUND, the function returns NOT_FOUND.
## Ours pre-fix (exports.rs `open_vfs_file`)
`open_vfs_file` had three paths:
1. **Empty path** (root-of-device) → synth-empty handle (special-cased
for `NtCreateFile("game:\")` disc-validation probe).
2. **`cache:` prefix** → host-FS-backed via `open_cache_file` (AUDIT-038).
3. **Otherwise**: call `vfs.read_file(&path)`. On `Ok(bytes)` return
handle. **On `Err(e)`: synth a zero-byte file and return SUCCESS.**
The (3) `Err` branch was the synth-empty fallback. Its docstring
acknowledged two rationales:
> 1. Writable system partitions (`cache:`, `partition0:`, `partition1:`)
> aren't backed by the disc — we synth so opens succeed.
> 2. Disc files that didn't make it into the ISO rip — returning
> NOT_FOUND would make Sylpheed's boot validator call
> `XamShowDirtyDiscErrorUI` → dashboard exit.
But rationale (1) was already covered by the AUDIT-038 `cache:`
short-circuit, and rationale (2) is empirically WRONG: canary returns
NOT_FOUND for `game:\dat\files.tbl` and Sylpheed continues — there
is no `XamShowDirtyDiscErrorUI` event in canary's trace anywhere
near idx 103862. The synth-empty was misleading the boot trajectory
in a non-canary direction.
## Fix shape
Two surgical changes:
1. New helper `is_disc_prefix(raw_path: &str) -> bool` that
recognises `game:\`, `d:\`/`D:\`, and `\Device\Cdrom0\` (the
subset of `path::DEVICE_PREFIXES` that resolves to the read-only
disc).
2. `open_vfs_file` captures the raw path via
`crate::path::object_attributes_raw_name` (alongside the existing
normalised `path`). In the `Err(e)` arm, if `is_disc_prefix(&raw_path)`,
return `STATUS_OBJECT_NAME_NOT_FOUND` (mirroring canary). Otherwise
keep the legacy synth-empty.
The `cache:` short-circuit is upstream of this branch and unchanged;
its existing NOT_FOUND-on-FILE_OPEN-miss behaviour is preserved.
## Tests
- `is_disc_prefix_recognises_disc_aliases` (12 assertions, mix of
disc and non-disc prefixes including case variants).
- `nt_create_file_game_prefix_missing_returns_not_found` — primary.
- `nt_create_file_cdrom_prefix_missing_returns_not_found`
`\Device\Cdrom0\` alias.
- `nt_create_file_non_disc_prefix_missing_still_synthesizes`
regression guard for `\Device\Harddisk0\Partition1\sys.bin`.
## Risk analysis (pre-fix)
- **Sylpheed might crash on NOT_FOUND**: REFUTED. Canary returns
NOT_FOUND, then Sylpheed calls `RtlNtStatusToDosError` and
continues; +712 events of further progress observed in cold-vs-cold
(no fault, no `XamShowDirtyDiscErrorUI`).
- **Reading-error #23 regression**: matched-prefix MUST grow, not
shrink. Cold-vs-cold confirms 103862 → 104574 (+712); no regression.
- **Sister-chain regression**: confirmed unchanged for all 5 sister
chains (see `cold-vs-cold-result.md`).
- **Stable digest shift**: expected; the synth-empty was emitting
one extra kernel.call/return pair (synth-success path); removing
it for disc paths drops `imports` 40447 → 40390 (-57). Phase B
`image_loaded_sha256` is unchanged.

View File

@@ -0,0 +1,25 @@
{
"build_id": "ours-phaseB",
"cvars": {
"phase_b_dump_section_content": false,
"phase_b_snapshot_and_exit": true,
"phase_b_snapshot_dir": "audit-runs/phase-c13-game-dat-files-tbl/phase-b-snap"
},
"deterministic_skip": [
"host_ns_at_snapshot",
"wall_clock_iso8601",
"build_id",
"iso_path",
"cvars.phase_b_snapshot_dir"
],
"engine": "ours",
"host_ns_at_snapshot": 0,
"image_loaded_sha256": "ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18",
"iso_path": "",
"schema_version": 1,
"wall_clock_iso8601": "epoch:0",
"xex_entry_point": "0x824ab748",
"xex_header_sha256": "0000000000000000000000000000000000000000000000000000000000000000",
"xex_image_base": "0x82000000",
"xex_image_size": 9568256
}

View File

@@ -0,0 +1,234 @@
{
"cr": [
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0",
"0x0"
],
"ctr": "0x0000000000000000",
"deterministic_skip": [
"hw_id"
],
"engine": "ours",
"fpr": [
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"fpscr": "0x00000000",
"gpr": [
"0x0000000000000000",
"0x00000000700fff00",
"0x0000000020000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x000000007fff0000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000",
"0x0000000000000000"
],
"hw_id": 0,
"lr": "0x00000000bcbcbcbc",
"msr": "0x0000000000009030",
"pc": "0x824ab748",
"pcr_base": "0x7fff0000",
"schema_version": 1,
"stack_base": "0x00000000",
"stack_limit": "0x00000000",
"thread_id": 1,
"tls_base": "0x00000000",
"vr": [
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000",
"00000000000000000000000000000000"
],
"vrsave": "0xffffffff",
"vscr": "00000000000000000000000000010000",
"xer": {
"ca": 0,
"ov": 0,
"so": 0,
"tbc": 0
}
}

View File

@@ -0,0 +1,62 @@
{
"deterministic_skip": [
"raw_handle_id",
"exports_registered_count"
],
"engine": "ours",
"exports_registered_count": 199,
"exports_registered_sample": [
"xam.xex!NetDll_WSACleanup",
"xam.xex!NetDll_WSAStartup",
"xam.xex!XGetAVPack",
"xam.xex!XGetGameRegion",
"xam.xex!XGetLanguage",
"xam.xex!XGetVideoMode",
"xam.xex!XMsgInProcessCall",
"xam.xex!XMsgStartIORequest",
"xam.xex!XMsgStartIORequestEx",
"xam.xex!XNotifyGetNext",
"xam.xex!XNotifyPositionUI",
"xam.xex!XamAlloc",
"xam.xex!XamContentClose",
"xam.xex!XamContentCreate",
"xam.xex!XamContentCreateEnumerator",
"xam.xex!XamContentDelete",
"xam.xex!XamContentGetCreator",
"xam.xex!XamContentGetDeviceData",
"xam.xex!XamContentGetDeviceName",
"xam.xex!XamContentGetDeviceState",
"xam.xex!XamContentSetThumbnail",
"xam.xex!XamEnableInactivityProcessing",
"xam.xex!XamEnumerate",
"xam.xex!XamFree",
"xam.xex!XamGetExecutionId",
"xam.xex!XamGetSystemVersion",
"xam.xex!XamInputGetCapabilities",
"xam.xex!XamInputGetKeystrokeEx",
"xam.xex!XamInputGetState",
"xam.xex!XamInputSetState",
"xam.xex!XamLoaderLaunchTitle",
"xam.xex!XamLoaderTerminateTitle"
],
"exports_registered_sha256": "bca7668a2a76ce1d1cc4dba8a862a2f16ec6ee3b2aab8a71d8d8bc0ccc89a097",
"handle_name_table": [],
"notification_listeners": [],
"objects": [
{
"details": {
"entry_pc": "0x824ab748",
"exit_code": null,
"hw_id": 0,
"is_entry_thread": true,
"thread_id": 1
},
"handle_semantic_id": "9879c5053fedb1d0",
"name": null,
"raw_handle_id": "0x00001000",
"type": "Thread",
"type_code": 5
}
],
"schema_version": 1
}

View File

@@ -0,0 +1,11 @@
{
"engine": "ours",
"files": {
"config.json": "f9c134628d8f8bf5680246ff0ecdcbd10e3039ee6575ac869a8082ea24dd9318",
"cpu_state.json": "4e6df54ca1939d08854f3a52b49ed2c5ee0823d63cdecad8a7395203dac5443a",
"kernel.json": "2db219d4ca8b0313e53be379b8fcf90ab13b99116e6fac5601f6bdefd1aa6900",
"memory.json": "b96ae4daebfbdd314e574492c1e162f532fa4f89ff5c0d7c6c29743797089cf1",
"vfs.json": "97bb2bda57266d8e0dd1da13309eab5ece43130ef378a0b682917d299e9dc4e1"
},
"schema_version": 1
}

View File

@@ -0,0 +1,84 @@
{
"committed_pages_total": 2594,
"deterministic_skip": [
"host_base_pointer"
],
"engine": "ours",
"guest_address_space_bytes": 4294967296,
"heaps": [
{
"base": "0x00000000",
"name": "v00000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 0
},
"size": "0x40000000"
},
{
"base": "0x40000000",
"name": "v40000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 266
},
"size": "0x40000000"
},
{
"base": "0x80000000",
"name": "v80000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 2336
},
"size": "0x40000000"
},
{
"base": "0x90000000",
"name": "v90000000",
"page_size": 4096,
"page_state_histogram": {
"committed": 0
},
"size": "0x40000000"
}
],
"page_size": 4096,
"regions": [
{
"byte_count": 1048576,
"end": "0x70100000",
"protect": 0,
"section_kind": null,
"sha256": "30e14955ebf1352266dc2ff8067e68104607e750abb9d3b36582b8af909fcb58",
"start": "0x70000000"
},
{
"byte_count": 4096,
"end": "0x7ffe1000",
"protect": 0,
"section_kind": null,
"sha256": "ad7facb2586fc6e966c004d7d1d16b024f5805ff7cb47c7a85dabd8b48892ca7",
"start": "0x7ffe0000"
},
{
"byte_count": 4096,
"end": "0x7fff1000",
"protect": 0,
"section_kind": null,
"sha256": "e35cddaf9c210aed7505ec4cf1c599f58ac2b7ec25b0885db1ee49aba2db519a",
"start": "0x7fff0000"
},
{
"byte_count": 9568256,
"end": "0x82920000",
"protect": 0,
"section_kind": null,
"sha256": "ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18",
"start": "0x82000000"
}
],
"regions_walked": [],
"schema_version": 1,
"section_contents": null
}

View File

@@ -0,0 +1,71 @@
{
"cache_root_listing": [],
"deterministic_skip": [
"host_path_realpath"
],
"engine": "ours",
"mounted_devices_observed_count": 1,
"resolve_path_probes": [
{
"is_directory": true,
"path": "\\Device\\Cdrom0",
"resolved": true,
"size": null
},
{
"is_directory": true,
"path": "\\Device\\Cdrom0\\dat",
"resolved": true,
"size": 4096
},
{
"is_directory": null,
"path": "\\Device\\Cdrom0\\dat\\movie",
"resolved": false,
"size": null
},
{
"is_directory": null,
"path": "\\Device\\Cdrom0\\dat\\movie\\opening.bik",
"resolved": false,
"size": null
},
{
"is_directory": false,
"path": "\\Device\\Cdrom0\\default.xex",
"resolved": true,
"size": 3497984
},
{
"is_directory": null,
"path": "\\Device\\HardDisk0\\Partition1",
"resolved": false,
"size": null
},
{
"is_directory": true,
"path": "cache:\\",
"resolved": true,
"size": null
},
{
"is_directory": null,
"path": "cache:\\nonexistent_probe",
"resolved": false,
"size": null
},
{
"is_directory": true,
"path": "game:\\dat",
"resolved": true,
"size": 4096
},
{
"is_directory": false,
"path": "game:\\default.xex",
"resolved": true,
"size": 3497984
}
],
"schema_version": 1
}

View File

@@ -0,0 +1,50 @@
# Phase C+13 re-validation (cold-vs-cold)
## Cache-wipe protocol (per C+12 reading-error #33)
Both canary storage roots wiped (binary-dir under wine + Linux-build
fallback) before each cold canary run; ours's persistent
`~/.local/share/xenia-rs/cache` wiped between each cold ours run.
Step-by-step:
1. Backup canary's binary-dir cache to `/tmp/canary-bindir-cache-backup.tar.gz`
(4.7 MB, pre-existing oracle preserved).
2. Wipe:
- `find ~/.local/share/xenia-rs/cache -mindepth 1 -delete`
- `find ~/.local/share/Xenia/cache -mindepth 1 -delete` (was already empty)
- `find xenia-canary/build-cross/bin/Windows/Debug/cache -mindepth 1 -delete`
3. Rename ours binary: `cp target/release/xenia-rs target/release/xrs-c13`.
4. Rename canary binary: `cp xenia_canary.exe xc-c13.exe`.
5. 3× cold ours runs with cache wiped before each:
- `find ~/.local/share/xenia-rs/cache -mindepth 1 -delete`
- `./target/release/xrs-c13 check --stable-digest -n 50000000 --out <digest>.json "<ISO>"`
- All three digests bit-identical: `e1dfcb1559f987b35012a7f2dc6d93f5`.
6. Cold ours run with event log:
- `./target/release/xrs-c13 exec --phase-a-event-log /tmp/ours-cold-c13-1.jsonl -n 50000000 "<ISO>"`.
7. Cold canary run (background; killed via `wineserver -k` after
reaching ~6.5 M total events, ~250k tid=6):
- `cd xenia-canary/build-cross/bin/Windows/Debug && /usr/bin/wine ./xc-c13.exe --mute=true --phase_a_event_log_path=/tmp/canary-cold-c13.jsonl "<ISO>"`.
8. Truncate canary jsonl per-tid (tid=6 cap 250k, others 20k cap).
9. Diff: `python3 xenia-rs/tools/diff-events/diff_events.py --canary <truncated> --ours <ours-cold-c13-1.jsonl> --out /tmp/diff-c13.md`.
10. Restore canary's binary-dir cache from backup.
11. Re-run Phase B snapshot to confirm `image_loaded_sha256` unchanged.
## Gate matrix
| gate | result | notes |
|---|---|---|
| Build (`cargo build --release`) | PASS | 1 unrelated `dead_code` warning, no errors |
| Kernel tests (177 → 181) | PASS | 4 new C+13 tests, all pass |
| Full workspace tests | PASS | all crates green (verified via `cargo test --release` summary) |
| Determinism — 3× cold stable-digest | PASS | `e1dfcb1559f987b35012a7f2dc6d93f5` (all 3) |
| Stable digest changed vs C+12 baseline | EXPECTED | `ad4f74ee…``e1dfcb15…` (imports -57, instructions +5) — the fix flips the synth-success branch to NOT_FOUND, shedding a kernel.call/return pair per masked probe |
| Phase B `image_loaded_sha256` unchanged | PASS | `ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18` |
| Cold-vs-cold main matched prefix | PASS | **103,862 → 104,574 (+712)** on main chain |
| Sister-chain regression check | PASS | all 5 sister chains unchanged (matched-prefix unchanged) |
| Canary's binary-dir cache restored | PASS | 23-file oracle re-extracted from backup |
## Failure analysis (Reading-error #23 guard)
The main chain ADVANCED, did not regress. Sister chains all
preserved. The fix is monotone-positive for the diff metric.