//! HLE kernel export implementations (xam.xex). use crate::state::{KernelState, ModuleId}; use xenia_cpu::PpcContext; use xenia_memory::{GuestMemory, MemoryAccess}; pub fn register_exports(state: &mut KernelState) { use ModuleId::Xam; // Net state.register_export(Xam, 0x01, "NetDll_WSAStartup", stub_success); state.register_export(Xam, 0x02, "NetDll_WSACleanup", stub_success); // Input state.register_export(Xam, 0x0190, "XamInputGetCapabilities", xam_input_get_capabilities); state.register_export(Xam, 0x0191, "XamInputGetState", xam_input_get_state); state.register_export(Xam, 0x0192, "XamInputSetState", xam_input_set_state); state.register_export(Xam, 0x0198, "XamInputGetKeystrokeEx", xam_input_get_keystroke); // Inactivity state.register_export(Xam, 0x01A0, "XamEnableInactivityProcessing", stub_success); state.register_export(Xam, 0x01A1, "XamResetInactivity", stub_success); // Loader state.register_export(Xam, 0x01A4, "XamLoaderLaunchTitle", xam_loader_launch_title); state.register_export(Xam, 0x01A9, "XamLoaderTerminateTitle", xam_loader_terminate_title); // Task state.register_export(Xam, 0x01AF, "XamTaskSchedule", xam_task_schedule); state.register_export(Xam, 0x01B1, "XamTaskCloseHandle", stub_success); state.register_export(Xam, 0x01B3, "XamTaskShouldExit", stub_return_zero); // Alloc state.register_export(Xam, 0x01EA, "XamAlloc", xam_alloc); state.register_export(Xam, 0x01EC, "XamFree", stub_success); // Msg state.register_export(Xam, 0x01F4, "XMsgInProcessCall", stub_success); state.register_export(Xam, 0x01F7, "XMsgStartIORequest", stub_success); state.register_export(Xam, 0x01FC, "XMsgStartIORequestEx", stub_success); // User state.register_export(Xam, 0x020A, "XamUserGetXUID", xam_user_get_xuid); state.register_export(Xam, 0x020E, "XamUserGetName", xam_user_get_name); state.register_export(Xam, 0x0210, "XamUserGetSigninState", stub_return_zero); state.register_export(Xam, 0x0219, "XamUserReadProfileSettings", xam_user_read_profile_settings); state.register_export(Xam, 0x021A, "XamUserWriteProfileSettings", stub_success); // Enum state.register_export(Xam, 0x0250, "XamEnumerate", stub_error_no_more_files); // Content state.register_export(Xam, 0x0258, "XamContentCreate", stub_success); state.register_export(Xam, 0x025A, "XamContentClose", stub_success); state.register_export(Xam, 0x025B, "XamContentDelete", stub_success); state.register_export(Xam, 0x025C, "XamContentCreateEnumerator", stub_success); state.register_export(Xam, 0x025E, "XamContentGetDeviceData", stub_success); state.register_export(Xam, 0x025F, "XamContentGetDeviceName", stub_success); state.register_export(Xam, 0x0260, "XamContentSetThumbnail", stub_success); state.register_export(Xam, 0x0262, "XamContentGetCreator", stub_success); state.register_export(Xam, 0x0265, "XamContentGetDeviceState", stub_success); // System state.register_export(Xam, 0x0280, "XamGetExecutionId", xam_get_execution_id); state.register_export(Xam, 0x0282, "XamGetSystemVersion", xam_get_system_version); // Notify state.register_export(Xam, 0x028A, "XamNotifyCreateListener", xam_notify_create_listener); state.register_export(Xam, 0x028B, "XNotifyGetNext", xnotify_get_next); state.register_export(Xam, 0x028C, "XNotifyPositionUI", stub_success); // Achievements/Stats state.register_export(Xam, 0x02EE, "XamUserCreateAchievementEnumerator", stub_success); state.register_export(Xam, 0x02F7, "XamUserCreateStatsEnumerator", stub_success); // UI 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); state.register_export(Xam, 0x02D9, "XamShowDirtyDiscErrorUI", stub_success); state.register_export(Xam, 0x02DC, "XamShowMessageBoxUIEx", stub_success); // Session state.register_export(Xam, 0x0316, "XamSessionCreateHandle", xam_session_create_handle); state.register_export(Xam, 0x0317, "XamSessionRefObjByHandle", stub_success); // Locale state.register_export(Xam, 0x03CB, "XGetAVPack", xget_avpack); state.register_export(Xam, 0x03CC, "XGetGameRegion", xget_game_region); state.register_export(Xam, 0x03CD, "XGetLanguage", xget_language); state.register_export(Xam, 0x03D1, "XGetVideoMode", xget_video_mode); } // ===== Generic stubs ===== fn stub_success(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0; } fn stub_return_zero(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0; } fn stub_error_no_more_files(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0x12; // ERROR_NO_MORE_FILES } // ===== Input ===== /// Helper: pack a `GamepadState` into a 12-byte key used to detect input /// changes. Cheap to compare across frames. fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 { let mut bytes = [0u8; 16]; bytes[0..2].copy_from_slice(&state.buttons.to_be_bytes()); bytes[2] = state.left_trigger; bytes[3] = state.right_trigger; bytes[4..6].copy_from_slice(&state.left_stick_x.to_be_bytes()); bytes[6..8].copy_from_slice(&state.left_stick_y.to_be_bytes()); bytes[8..10].copy_from_slice(&state.right_stick_x.to_be_bytes()); bytes[10..12].copy_from_slice(&state.right_stick_y.to_be_bytes()); u128::from_be_bytes(bytes) } fn xam_input_get_capabilities( ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState, ) { // r3 = user_index, r4 = flags, r5 = out X_INPUT_CAPABILITIES* let user = ctx.gpr[3] as u32; let out_ptr = ctx.gpr[5] as u32; let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)); if !connected { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } xenia_hid::write_input_capabilities(mem, out_ptr); ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64; } fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // r3 = user_index, r4 = flags, r5 = out X_INPUT_STATE* let user = ctx.gpr[3] as u32; let out_ptr = ctx.gpr[5] as u32; let Some(ui) = state.ui.as_ref() else { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; }; if !ui.is_connected(user) { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } let gamepad = ui.snapshot_gamepad(); let key = gamepad_key(&gamepad); if key != state.last_input_bytes { state.input_packet_number = state.input_packet_number.wrapping_add(1); state.last_input_bytes = key; } xenia_hid::write_input_state(mem, out_ptr, state.input_packet_number, &gamepad); ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64; } fn xam_input_set_state(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) { // r3 = user_index, r4 = flags, r5 = X_INPUT_VIBRATION* // Rumble is out of scope for Phase 1; we accept the call and return // success so games don't retry in a tight loop, but we never actually // shake anything. let user = ctx.gpr[3] as u32; let connected = state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)); if !connected { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } ctx.gpr[3] = xenia_hid::errors::SUCCESS as u64; } fn xam_input_get_keystroke( ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState, ) { // No keyboard input in Phase 1 — always "queue empty". Games that only // use the gamepad ignore this return code; those that drive text entry // through the keystroke queue simply get a permanently empty queue, which // manifests as no virtual-keyboard input — acceptable for minimal UI. ctx.gpr[3] = xenia_hid::errors::EMPTY as u64; } // ===== Loader ===== fn xam_loader_launch_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { tracing::warn!("XamLoaderLaunchTitle called"); ctx.gpr[3] = 0; } fn xam_loader_terminate_title(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { tracing::warn!("XamLoaderTerminateTitle called"); ctx.gpr[3] = 0; } // ===== Task ===== fn xam_task_schedule(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) { let handle = state.alloc_handle(); tracing::info!("XamTaskSchedule: handle={:#x}", handle); ctx.gpr[3] = 0; } // ===== Alloc ===== fn xam_alloc(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // r3 = flags, r4 = size, r5 = out_ptr_ptr let size = ctx.gpr[4] as u32; let out_ptr = ctx.gpr[5] as u32; match state.heap_alloc(size, mem) { Some(addr) => { if out_ptr != 0 { mem.write_u32(out_ptr, addr); } ctx.gpr[3] = 0; // SUCCESS } None => { ctx.gpr[3] = 0x8007_000E; // E_OUTOFMEMORY } } } // ===== User ===== fn xam_user_get_xuid(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) { // r3 = user_index, r4 = xuid_ptr let xuid_ptr = ctx.gpr[4] as u32; if xuid_ptr != 0 { mem.write_u64(xuid_ptr, 0); // No XUID } ctx.gpr[3] = 0; } fn xam_user_get_name(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) { // r3 = user_index, r4 = buffer, r5 = buffer_size let buffer = ctx.gpr[4] as u32; if buffer != 0 { mem.write_u8(buffer, 0); // Empty string } ctx.gpr[3] = 0; } fn xam_user_read_profile_settings(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { // Return error — no profile ctx.gpr[3] = 0x0000_048B; // ERROR_NOT_FOUND } // ===== System ===== fn xam_get_execution_id(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // r3 = execution_id_ptr_ptr — write pointer to execution info let ptr_ptr = ctx.gpr[3] as u32; if ptr_ptr != 0 { // Allocate and fill a fake XEX_EXECUTION_ID structure if let Some(exec_id_addr) = state.heap_alloc(0x18, mem) { mem.write_u32(exec_id_addr, 0x535107D4); // title_id (Project Sylpheed) mem.write_u32(exec_id_addr + 4, 0x2D2E2EEB); // media_id mem.write_u16(exec_id_addr + 8, 0); // version mem.write_u16(exec_id_addr + 10, 0); // base_version mem.write_u16(exec_id_addr + 12, 1); // disc_number mem.write_u16(exec_id_addr + 14, 1); // disc_count mem.write_u32(ptr_ptr, exec_id_addr); } } ctx.gpr[3] = 0; } fn xam_get_system_version(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0x2000_0000; // System version } // ===== Notify ===== fn xam_notify_create_listener(ctx: &mut PpcContext, _mem: &GuestMemory, state: &mut KernelState) { let handle = state.alloc_handle(); ctx.gpr[3] = handle as u64; } fn xnotify_get_next(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { // r3 = handle, r4 = id_ptr, r5 = param_ptr ctx.gpr[3] = 0; // FALSE (no notifications) } // ===== Session ===== fn xam_session_create_handle(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut KernelState) { // r3 = handle_ptr let handle_ptr = ctx.gpr[3] as u32; let handle = state.alloc_handle(); if handle_ptr != 0 { mem.write_u32(handle_ptr, handle); } ctx.gpr[3] = 0; } // ===== Locale ===== fn xget_avpack(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0x16; // HDMI } fn xget_game_region(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 0xFF; // All regions } fn xget_language(ctx: &mut PpcContext, _mem: &GuestMemory, _state: &mut KernelState) { ctx.gpr[3] = 1; // English } fn xget_video_mode(ctx: &mut PpcContext, mem: &GuestMemory, _state: &mut KernelState) { // r3 = video_mode_ptr let ptr = ctx.gpr[3] as u32; if ptr != 0 { mem.write_u32(ptr, 1280); // width mem.write_u32(ptr + 4, 720); // height mem.write_u32(ptr + 8, 0); // is_interlaced mem.write_u32(ptr + 12, 1); // is_widescreen mem.write_u32(ptr + 16, 60); // refresh_rate } ctx.gpr[3] = 0; }