Files
xenia-rs/audit-runs/phase-c7-XamTaskCloseHandle/fix.diff
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

193 lines
7.9 KiB
Diff

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();