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