diff --git a/crates/xenia-app/tests/golden/sylpheed_n200m.json b/crates/xenia-app/tests/golden/sylpheed_n200m.json index 4c70278..2b9a5c7 100644 --- a/crates/xenia-app/tests/golden/sylpheed_n200m.json +++ b/crates/xenia-app/tests/golden/sylpheed_n200m.json @@ -1,9 +1,9 @@ { - "instructions": 200000239, - "imports": 575447, + "instructions": 200000203, + "imports": 575647, "unimpl": 0, - "draws": 3165, - "swaps": 895, + "draws": 3208, + "swaps": 910, "unique_render_targets": 2, "shader_blobs_live": 6, "texture_cache_entries": 1 diff --git a/crates/xenia-kernel/src/xam.rs b/crates/xenia-kernel/src/xam.rs index 9950e45..6ad9111 100644 --- a/crates/xenia-kernel/src/xam.rs +++ b/crates/xenia-kernel/src/xam.rs @@ -125,6 +125,29 @@ fn gamepad_key(state: &xenia_hid::GamepadState) -> u128 { u128::from_be_bytes(bytes) } +/// Whether user 0 has a default always-connected, idle (no-buttons) virtual +/// controller. **Default ON** — matches canary, whose default input drivers +/// (xinput / winkey) present a connected pad, so a title screen advances to its +/// "Press A" input-poll state instead of stalling forever on +/// DEVICE_NOT_CONNECTED (a console always exposes controller slots; a real +/// gamepad under `--ui` supplies actual button state, otherwise the pad is idle). +/// Set `XENIA_NO_PAD=1` to present NO controller (reproduces the raw headless +/// no-HID trajectory). +fn virtual_pad_enabled() -> bool { + use std::sync::OnceLock; + static V: OnceLock = OnceLock::new(); + *V.get_or_init(|| { + !std::env::var("XENIA_NO_PAD") + .map(|v| v == "1" || v.eq_ignore_ascii_case("true")) + .unwrap_or(false) + }) +} + +/// True iff user 0 has a controller: a real host gamepad, or the default pad. +fn user_connected(state: &KernelState, user: u32) -> bool { + state.ui.as_ref().is_some_and(|ui| ui.is_connected(user)) || (user == 0 && virtual_pad_enabled()) +} + fn xam_input_get_capabilities( ctx: &mut PpcContext, mem: &GuestMemory, @@ -133,8 +156,7 @@ fn xam_input_get_capabilities( // 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 { + if !user_connected(state, user) { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } @@ -146,15 +168,16 @@ fn xam_input_get_state(ctx: &mut PpcContext, mem: &GuestMemory, state: &mut Kern // 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) { + if !user_connected(state, user) { ctx.gpr[3] = xenia_hid::errors::DEVICE_NOT_CONNECTED as u64; return; } - let gamepad = ui.snapshot_gamepad(); + // Real host gamepad if present, else the idle virtual pad (XENIA_VIRTUAL_PAD). + let gamepad = state + .ui + .as_ref() + .map(|ui| ui.snapshot_gamepad()) + .unwrap_or_default(); let key = gamepad_key(&gamepad); if key != state.last_input_bytes { state.input_packet_number = state.input_packet_number.wrapping_add(1);