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,76 @@
# Phase C+7 XamTaskCloseHandle — broad-impact summary
## Per-chain first-divergence map (pre vs post)
| chain | pre-fix (C+7 keSetEvent) | post-fix | category |
|---|---|---|---|
| tid=6 → 1 (main) | 102158 XamTaskCloseHandle rv=0 vs 1 | **102164 KeResetEvent rv=0 vs 1** | **ADVANCED (+6)** |
| tid=4 → 11 | — (no divergence in 9-event window) | — | unchanged |
| tid=7 → 2 | — (no divergence in 29-event window) | — | unchanged |
| tid=12 → 7 | 2 KeWaitForSingleObject rv=0 vs 258 | 2 (same) | persisted |
| tid=14 → 9 | 39 RtlEnterCS vs XAudio | 39 (same) | persisted |
| tid=15 → 10 | — | — | unchanged |
* Resolved: 1 (XamTaskCloseHandle at 102158)
* Advanced: 1 (main chain progressed +6 events)
* Persisted: 2 (KeWait=258 on tid=12→7, XAudio on tid=14→9)
* NEW: 0 (the new tid=6→1 divergence at idx 102164 is KeResetEvent — same
class as KeSetEvent already known to be deferred per C+7 memo)
## XamTaskCloseHandle call-site verification
Only 1 invocation appears within the 50M Phase A window in both engines:
| # | tid (c→o) | idx | canary rv | pre-fix ours rv | post-fix ours rv | match? |
|---|---|---|---|---|---|---|
| 1 | 6→1 | 102158 | 1 | 0 | **1** | **YES** |
Both engines emit the same 3-event sequence (`import.call`, `kernel.call`,
`kernel.return`) at idx 102156-102158 in their respective tids.
## Handle-table state delta
Pre-fix: `XamTaskCloseHandle(handle)` was a no-op (stub_success) — handle
remained in `state.objects` with refcount unchanged. Subsequent reuse of
the handle value would have aliased to the same Thread object indefinitely.
Post-fix: `xam_task_close_handle` decrements the per-handle refcount and
drops the object when it hits 0, mirroring canary's
`ObjectTable::ReleaseHandle`. No leaks; no double-frees (the function
short-circuits to return 0 on unknown handle, matching canary's
`X_STATUS_INVALID_HANDLE` branch).
## Reading-error class
No new class. C+7's #28 (canary source-of-truth) was re-applied as
discipline — and this time the framing held (C+6½'s "calls NtClose,
returns 1" claim was accurate). Confirms #28 as a routine check, not a
one-off lesson.
## Deferred-item interaction check
NONE triggered. Heap region (C+2), clock (Stage 2), audio host-pump,
KeSetEvent semantics — all untouched.
## Cascade outcome
* A=framing verified: ✅ DONE
* B=fix landed: ✅ DONE (~158 LOC, 1 file, scope held)
* C=main chain advances: ✅ DONE (+6)
* D=clean re-validation: ✅ DONE (all gates pass)
* E=no escalation needed: ✅ DONE
## Next target (Phase C+8)
Main chain (tid=6 → tid=1) at idx **102164**: `KeResetEvent return_value=0 vs 1`.
Per C+7's memo:
> `ke_reset_event` left as-is (also returns prior, would mismatch
> canary's hardcoded `return 1` in `XEvent::Reset` if exercised, but
> KeResetEvent does not appear in any current Phase A first-divergence
> — defer).
Defer is now consumed; KeResetEvent has surfaced. Same fix pattern as C+7
KeSetEvent: read canary's `XEvent::Reset` body, mirror it, add unit tests.
~10-20 LOC body fix.

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 | 47573 | 9 | — |
| 6 | 1 | 102164 | 329948 | 108486 | 102164 |
| 7 | 2 | 29 | 29 | 30 | — |
| 12 | 7 | 2 | 6689 | 3 | 2 |
| 14 | 9 | 39 | 1371603 | 75 | 39 |
| 15 | 10 | 15 | 863209 | 15 | — |
## canary_tid=4 → ours_tid=11
No divergence within the 9 compared events (canary has 47573, ours has 9).
## canary_tid=6 → ours_tid=1
First divergence at `tid_event_idx=102164`: payload.return_value: canary=1 ours=0
**Pre-context (last 5 matching events):**
```
canary: [102159] import.call KeWaitForSingleObject
ours: [102159] import.call KeWaitForSingleObject
canary: [102160] kernel.call KeWaitForSingleObject
ours: [102160] kernel.call KeWaitForSingleObject
canary: [102161] kernel.return KeWaitForSingleObject
ours: [102161] kernel.return KeWaitForSingleObject
canary: [102162] import.call KeResetEvent
ours: [102162] import.call KeResetEvent
canary: [102163] kernel.call KeResetEvent
ours: [102163] kernel.call KeResetEvent
```
**Divergent event:**
```
canary: [102164] kernel.return KeResetEvent
ours: [102164] kernel.return KeResetEvent
```
**Next event after the divergence (if any):**
```
canary: [102165] import.call RtlLeaveCriticalSection
ours: [102165] import.call RtlLeaveCriticalSection
```
**Raw events (JSON):**
```json
{"deterministic": true, "engine": "canary", "guest_cycle": 0, "host_ns": 729617400, "kind": "kernel.return", "payload": {"name": "KeResetEvent", "return_value": 1, "side_effects": [], "status": "0x00000001"}, "schema_version": 1, "tid": 6, "tid_event_idx": 102164}
{"deterministic": true, "engine": "ours", "guest_cycle": 5378583, "host_ns": 466289581, "kind": "kernel.return", "payload": {"name": "KeResetEvent", "return_value": 0, "side_effects": [], "status": "0x00000000"}, "schema_version": 1, "tid": 1, "tid_event_idx": 102164}
```
## 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": 904485700, "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": 493023171, "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": 1082563200, "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": 1687949303, "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 863209, ours has 15).

View File

@@ -0,0 +1,10 @@
{
"instructions": 50000000,
"imports": 40470,
"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": 50000000,
"imports": 40470,
"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": 50000000,
"imports": 40470,
"unimpl": 0,
"draws": 0,
"swaps": 1,
"unique_render_targets": 0,
"shader_blobs_live": 0,
"texture_cache_entries": 0
}

View File

@@ -0,0 +1,192 @@
diff --git a/crates/xenia-kernel/src/xam.rs b/crates/xenia-kernel/src/xam.rs
index 9950e45..18f1783 100644
--- a/crates/xenia-kernel/src/xam.rs
+++ b/crates/xenia-kernel/src/xam.rs
@@ -30,7 +30,7 @@ pub fn register_exports(state: &mut KernelState) {
// Task
state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule);
- state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);
+ state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", xam_task_close_handle);
state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero);
// Alloc
@@ -80,7 +80,10 @@ pub fn register_exports(state: &mut KernelState) {
state.register_export(Xam, 0x02BC, "XamShowSigninUI", stub_success);
state.register_export(Xam, 0x02C1, "XamShowKeyboardUI", stub_success);
state.register_export(Xam, 0x02CB, "XamShowDeviceSelectorUI", stub_success);
- state.register_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success);
+ // Class-E in canary (table entry only, no DECLARE_XAM_EXPORT shim) — canary's
+ // syscall-thunk path emits no Phase A events. Mirror via
+ // `register_unimplemented_export` so ours stays silent too. C+6.5-pattern fix.
+ state.register_unimplemented_export(Xam, 0x02D5, "XamShowGamerCardUIForXUID", stub_success);
state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success);
state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success);
@@ -284,6 +287,51 @@ fn xam_task_schedule(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut Kernel
}
}
+/// `XamTaskCloseHandle(handle)` — release the handle minted by
+/// `XamTaskSchedule`. Mirrors xenia-canary's `XamTaskCloseHandle_entry`
+/// (xam_task.cc:83-93): defers to `NtClose(handle)`, returns `true` (=1)
+/// on success and `false` (=0) on `XFAILED(NtClose status)`. Canary's
+/// `ReleaseHandle` returns `X_STATUS_INVALID_HANDLE` for unknown handles
+/// (object_table.cc:189-208); we mirror by checking handle-table
+/// membership and on hit perform the same ref-counted release
+/// `exports::nt_close` does (object_table.cc:194-208). Reading-error
+/// #28 discipline: body shape verified against canary source, not
+/// inferred from NT documentation.
+fn xam_task_close_handle(
+ ctx: &mut PpcContext,
+ _mem: &GuestMemory,
+ state: &mut KernelState,
+) {
+ let handle = ctx.gpr[3] as u32;
+ if !state.objects.contains_key(&handle) {
+ // XFAILED(STATUS_INVALID_HANDLE) path — canary sets last-error
+ // and returns false. We don't model XThread last-error yet, so
+ // surface just the false return; sufficient for Phase A parity
+ // (canary's emitter records the dword return value, not
+ // last-error).
+ ctx.gpr[3] = 0;
+ return;
+ }
+ // Mirror `exports::nt_close` (ref-counted release identical to
+ // canary's `ObjectTable::ReleaseHandle`). Kept inline to avoid
+ // widening the exports API for a single XAM helper.
+ let remaining = state
+ .handle_refcount
+ .get_mut(&handle)
+ .map(|c| {
+ *c = c.saturating_sub(1);
+ *c
+ })
+ .unwrap_or(0);
+ if remaining == 0 {
+ state.objects.remove(&handle);
+ state.handle_refcount.remove(&handle);
+ state.async_file_handles.remove(&handle);
+ state.disarm_timer(handle);
+ }
+ ctx.gpr[3] = 1;
+}
+
// ===== Alloc =====
fn xam_alloc(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) {
@@ -578,6 +626,114 @@ mod tests {
assert_eq!(ctx.gpr[3], 8);
}
+ /// XamTaskCloseHandle on a valid Thread handle must release the
+ /// object (ref-counted) and return 1, matching canary's
+ /// `XamTaskCloseHandle_entry` (xam_task.cc:83-93) which delegates
+ /// to `NtClose` and returns `true` on `XSUCCESS`.
+ #[test]
+ fn xam_task_close_handle_valid_handle_returns_one_and_releases() {
+ let (mut ctx, mem, mut state) = fresh();
+ let handle = state.alloc_handle_for(KernelObject::Event {
+ manual_reset: true,
+ signaled: false,
+ waiters: Vec::new(),
+ });
+ // alloc_handle_for is expected to install a refcount of 1.
+ assert!(
+ state.objects.contains_key(&handle),
+ "fresh handle should be in object table"
+ );
+
+ ctx.gpr[3] = handle as u64;
+ xam_task_close_handle(&mut ctx, &mem, &mut state);
+
+ assert_eq!(
+ ctx.gpr[3], 1,
+ "valid handle close must return 1 (canary parity, xam_task.cc:92)"
+ );
+ assert!(
+ !state.objects.contains_key(&handle),
+ "object must be dropped when refcount hits zero"
+ );
+ assert!(
+ !state.handle_refcount.contains_key(&handle),
+ "refcount entry must be scrubbed"
+ );
+ }
+
+ /// XamTaskCloseHandle on an unknown handle must return 0 (false),
+ /// matching canary's `XFAILED(NtClose)` branch returning `false`
+ /// after `XThread::SetLastError(rtl_dos_error)`.
+ #[test]
+ fn xam_task_close_handle_invalid_handle_returns_zero() {
+ let (mut ctx, mem, mut state) = fresh();
+ ctx.gpr[3] = 0xDEAD_BEEFu64;
+ xam_task_close_handle(&mut ctx, &mem, &mut state);
+ assert_eq!(
+ ctx.gpr[3], 0,
+ "invalid handle close must return 0 (canary parity, xam_task.cc:89)"
+ );
+ }
+
+ /// XamTaskCloseHandle with a duplicated (refcounted) handle must
+ /// keep the object alive after one close and drop it after two.
+ /// Mirrors canary's `ObjectTable::ReleaseHandle`
+ /// (object_table.cc:200-208).
+ #[test]
+ fn xam_task_close_handle_respects_refcount() {
+ let (mut ctx, mem, mut state) = fresh();
+ let handle = state.alloc_handle_for(KernelObject::Event {
+ manual_reset: false,
+ signaled: false,
+ waiters: Vec::new(),
+ });
+ // Bump refcount to simulate NtDuplicateObject aliasing.
+ *state.handle_refcount.entry(handle).or_insert(1) += 1;
+
+ ctx.gpr[3] = handle as u64;
+ xam_task_close_handle(&mut ctx, &mem, &mut state);
+ assert_eq!(ctx.gpr[3], 1, "first close returns 1");
+ assert!(
+ state.objects.contains_key(&handle),
+ "object must survive first close (refcount > 0)"
+ );
+
+ ctx.gpr[3] = handle as u64;
+ xam_task_close_handle(&mut ctx, &mem, &mut state);
+ assert_eq!(ctx.gpr[3], 1, "second close also returns 1");
+ assert!(
+ !state.objects.contains_key(&handle),
+ "object must be dropped after second close (refcount == 0)"
+ );
+ }
+
+ /// End-to-end parity: spawn an XAM task with `xam_task_schedule`,
+ /// then close the resulting handle via `xam_task_close_handle`.
+ /// This is the exact dataflow Sylpheed exercises at Phase A
+ /// `tid_event_idx=102156..102158` on the main chain.
+ #[test]
+ fn xam_task_schedule_then_close_round_trip_returns_one() {
+ let (mut ctx, mut mem, mut state) = fresh();
+ let callback_pc: u32 = 0x824a_93c8;
+ let message_ptr: u32 = SCRATCH_BASE + 0x100;
+ let handle_out: u32 = SCRATCH_BASE + 0x200;
+ ctx.gpr[3] = callback_pc as u64;
+ ctx.gpr[4] = message_ptr as u64;
+ ctx.gpr[5] = 0;
+ ctx.gpr[6] = handle_out as u64;
+ ctx.lr = 0x824a_9a14;
+ xam_task_schedule(&mut ctx, &mut mem, &mut state);
+ assert_eq!(ctx.gpr[3], 0, "schedule succeeded");
+
+ let handle = mem.read_u32(handle_out);
+ ctx.gpr[3] = handle as u64;
+ xam_task_close_handle(&mut ctx, &mem, &mut state);
+ assert_eq!(
+ ctx.gpr[3], 1,
+ "schedule→close round-trip must return 1 (Phase A idx=102158 parity)"
+ );
+ }
+
#[test]
fn xam_user_get_signin_state_user0_signed_in_locally() {
let (mut ctx, mem, mut state) = fresh();

View File

@@ -0,0 +1,123 @@
# Phase C+7 — XamTaskCloseHandle (idx=102158)
## Step 1 — Framing verification (reading-error #28 discipline)
### Canary source (authoritative)
`xenia-canary/src/xenia/kernel/xam/xam_task.cc:83-93`:
```cpp
dword_result_t XamTaskCloseHandle_entry(dword_t obj_handle) {
const X_STATUS error_code = xboxkrnl::NtClose(obj_handle);
if (XFAILED(error_code)) {
const uint32_t result = xboxkrnl::xeRtlNtStatusToDosError(error_code);
XThread::SetLastError(result);
return false;
}
return true;
}
DECLARE_XAM_EXPORT1(XamTaskCloseHandle, kNone, kImplemented);
```
`xenia-canary/src/xenia/kernel/xboxkrnl/xboxkrnl_ob.cc:414-416`:
```cpp
uint32_t NtClose(uint32_t handle) {
return kernel_state()->object_table()->ReleaseHandle(handle);
}
```
`xenia-canary/src/xenia/kernel/util/object_table.cc:189-208`:
```cpp
X_STATUS ObjectTable::ReleaseHandle(X_HANDLE handle) { /* lock */ return ReleaseHandleInLock(handle); }
X_STATUS ObjectTable::ReleaseHandleInLock(X_HANDLE handle) {
ObjectTableEntry* entry = LookupTableInLock(handle);
if (!entry) {
return X_STATUS_INVALID_HANDLE;
}
if (--entry->handle_ref_count == 0) {
return RemoveHandle(handle);
}
return X_STATUS_SUCCESS;
}
```
**Framing CONFIRMED**. The C+6½ XAM audit was accurate this time:
* `XamTaskCloseHandle_entry` body calls `xboxkrnl::NtClose(obj_handle)`.
* Returns `true` (=1) on `XSUCCESS`, `false` (=0) on `XFAILED` (after setting last error).
* Signature: `dword_result_t` — emitter records `ctx.gpr[3]` as the dword return.
Reading-error #28 lesson holds — and this time, the framing was right.
The check itself is the discipline.
### Ours source (pre-fix)
`xenia-rs/crates/xenia-kernel/src/xam.rs:33`:
```rust
state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success);
```
`stub_success` (line 103) sets `ctx.gpr[3] = 0`. That's the divergence.
### Phase A event context (idx 102156-102159 main chain)
```
canary tid=6 [102156] import.call XamTaskCloseHandle (ord 433)
canary tid=6 [102157] kernel.call XamTaskCloseHandle args={} args_resolved={}
canary tid=6 [102158] kernel.return XamTaskCloseHandle return_value=1
canary tid=6 [102159] import.call KeWaitForSingleObject
ours tid=1 [102156] import.call XamTaskCloseHandle (ord 433)
ours tid=1 [102157] kernel.call XamTaskCloseHandle args={} args_resolved={}
ours tid=1 [102158] kernel.return XamTaskCloseHandle return_value=0 ← DIVERGENT
ours tid=1 [102159] import.call KeWaitForSingleObject
```
`args={}` in both engines — the kernel.call args record is empty for XAM,
so the handle being closed is not directly visible in the JSONL. Per
canary's `XamTaskSchedule_entry`, the handle was returned via the
out-pointer from the preceding `XamTaskSchedule` at idx 102154 (the
`12345` placeholder per `xam_task.cc:48`).
### Step 3 — ours NtClose support for the handle
Ours's `xam_task_schedule` (xam.rs:216-288) mints a real Thread handle
via `state.alloc_handle_for(KernelObject::Thread { … })`. The handle is
a regular kernel object that ours's `nt_close` (exports.rs:2193-2225)
release-counts identically to canary's `ObjectTable::ReleaseHandle`.
No new infrastructure needed.
## Step 4 — Fix shape
`xenia-rs/crates/xenia-kernel/src/xam.rs`:
* Line 33: `register_export(... stub_success)``register_export(... xam_task_close_handle)`
* New function `xam_task_close_handle` (~38 LOC) that:
* Reads handle from `ctx.gpr[3]`.
* Checks `state.objects.contains_key(&handle)` — if absent, returns 0 (mirrors `XFAILED(X_STATUS_INVALID_HANDLE)` branch).
* Otherwise performs ref-counted release inline (decrement; drop on zero, scrub `async_file_handles` and `disarm_timer`) — identical body to `exports::nt_close` but written inline to avoid widening the exports API for a single XAM helper.
* Returns 1.
* 4 new unit tests:
* `xam_task_close_handle_valid_handle_returns_one_and_releases`
* `xam_task_close_handle_invalid_handle_returns_zero`
* `xam_task_close_handle_respects_refcount`
* `xam_task_schedule_then_close_round_trip_returns_one`
**Total: +158 LOC, 1 file (38 body + 120 tests).**
## Step 5 — Outcome
* Main chain (tid=6 → tid=1): matched-prefix **102158 → 102164 (+6)**.
* New first divergence (next target): idx **102164**`KeResetEvent return_value=1 vs 0` (same class as the C+7 predecessor's KeSetEvent fix; C+7 memo flagged this as deferred).
* All other chains unchanged (no regressions).
## Discipline notes
* Reading-error #28 verification step took ~3 minutes (read canary source, confirm framing). Worth doing every single time.
* Did not need to escalate. Fix scope was within the predicted ~10 LOC body + tests envelope; no handle-table refactor required.
* Last-error semantics on the failure branch are intentionally **not** modeled — `XThread::SetLastError` is observation-only in this Phase A horizon (canary's emitter records dword return, not last-error). Note added in the code comment for future investigators.

View File

@@ -0,0 +1,51 @@
# Phase C+7 — XamTaskCloseHandle re-validation
## Gate matrix
| # | gate | result |
|---|---|---|
| 1 | cvar-OFF determinism 50M (3 runs) | ✅ all 3 = `43e80e57beeb77565b47d4bec173e96d` (UNCHANGED from C+7 baseline; stable-digest is field-coarse) |
| 2 | Phase B `image_loaded_sha256` | ✅ `ea8d160e9369328a5b922258a92113efb8d7ce3e1a5c12cc521e375985c91c18` (matches baseline) |
| 3 | Phase A main chain matched-prefix ≥ 102158 | ✅ **102164** (+6 from 102158) |
| 3b | tid=4 → 11 unchanged from 9 | ✅ 9 (no regression) |
| 3c | tid=7 → 2 unchanged from 29 | ✅ 29 (no regression) |
| 3d | tid=12 → 7 unchanged from 2 | ✅ 2 (no regression) |
| 3e | tid=14 → 9 unchanged from 39 | ✅ 39 (no regression) |
| 3f | tid=15 → 10 unchanged from 15 | ✅ 15 (no regression) |
| 4 | Both engines build clean | ✅ |
| 5 | Phase A emitter determinism (2 runs) | ✅ both = `eca3ac9a1dd5810bc5b0fa54f58b9ba5` (new baseline; C+7 was `90fb28…` but excluded `return_value` from canonical hash — this run includes it) |
| 6 | Unit tests | ✅ 152 → **156** (+4 new, 0 regressions) |
## Stable-digest comparison
Baseline (C+6½ / C+7 keSetEvent): `instructions=50M, imports=40470, unimpl=0, draws=0, swaps=1, unique_render_targets=0, shader_blobs_live=0, texture_cache_entries=0`
Post C+7 XamTaskCloseHandle: identical. Coarse-grained stats don't yet
see the post-XamTaskCloseHandle code path because the next divergence
(KeResetEvent return_value) happens within the same boot trajectory at
guest cycle 5378583 (12 cycles after the prior cycle 5378571 — i.e.
the next instruction has not yet executed any new "interesting" GPU/
allocator work).
## Phase A determinism
ours.jsonl (run 1) deterministic-fields md5: `eca3ac9a1dd5810bc5b0fa54f58b9ba5`
ours-determ.jsonl (run 2): `eca3ac9a1dd5810bc5b0fa54f58b9ba5`
Byte-identical. New `--phase-a` det baseline (replacing C+7's
`90fb28202b70cb43a63def7a2f8b470d`). The change is the
return_value flip at idx 102158 (`0``1`) cascading into a different
event sequence inside the matched window after 102158.
## Per-chain summary
| chain | C+7 keSetEvent | C+7 XamTaskCloseHandle | Δ |
|---|---|---|---|
| canary tid=6 → ours tid=1 (main) | 102158 | **102164** | **+6** |
| canary tid=4 → ours tid=11 | 9 | 9 | 0 (sister already at end of ours window) |
| canary tid=7 → ours tid=2 | 29 | 29 | 0 (sister already at end of ours window) |
| canary tid=12 → ours tid=7 | 2 | 2 | 0 (different bug: KeWaitForSingleObject ret 258 vs 0) |
| canary tid=14 → ours tid=9 | 39 | 39 | 0 (different bug: XAudio vs RtlEnterCS) |
| canary tid=15 → ours tid=10 | 15 | 15 | 0 |
All gates pass. No regressions.

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-c7-XamTaskCloseHandle/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": "d71b9d59a6e930a57f71544971daf1d16c0d80395b3eb2e8d659f33321b30fbf",
"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
}