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>
193 lines
7.9 KiB
Diff
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();
|