From 9c9f1a300099954109bfae27dc2236da0664af0c Mon Sep 17 00:00:00 2001 From: Logan McNaughton <848146+loganmc10@users.noreply.github.com> Date: Tue, 26 May 2026 15:03:00 +0200 Subject: [PATCH] connect to native code (#955) * connect to native code * more * more * more * more * more * more * more * more --- .../app/src/main/AndroidManifest.xml | 10 ++- .../github/gopher64/gopher64/N64Activity.kt | 30 +++++++ src/lib.rs | 81 +----------------- src/ui.rs | 84 +++++++++++++++++++ src/ui/android.rs | 53 +++++++++++- src/ui/gui.rs | 2 +- src/ui/input.rs | 12 ++- src/ui/video.rs | 6 +- 8 files changed, 189 insertions(+), 89 deletions(-) diff --git a/android-project/app/src/main/AndroidManifest.xml b/android-project/app/src/main/AndroidManifest.xml index 292d6682..cb6793af 100644 --- a/android-project/app/src/main/AndroidManifest.xml +++ b/android-project/app/src/main/AndroidManifest.xml @@ -18,7 +18,15 @@ - + + + + + \ No newline at end of file diff --git a/android-project/app/src/main/java/io/github/gopher64/gopher64/N64Activity.kt b/android-project/app/src/main/java/io/github/gopher64/gopher64/N64Activity.kt index ec37fe0d..2fabc26f 100644 --- a/android-project/app/src/main/java/io/github/gopher64/gopher64/N64Activity.kt +++ b/android-project/app/src/main/java/io/github/gopher64/gopher64/N64Activity.kt @@ -3,10 +3,40 @@ package io.github.gopher64.gopher64 import org.libsdl.app.SDLActivity class N64Activity : SDLActivity() { + companion object { + const val CONFIGURE_INPUT_PROFILE = 2 + } + override fun getLibraries(): Array = arrayOf( "SDL3", "SDL3_ttf", "SDL3_image", "gopher64" ) + + override fun getMainFunction(): String = "gopher64_sdl_main" + + override fun getArguments(): Array { + val intent = intent ?: return super.getArguments() + val request_code = intent.getIntExtra("request_code", 0) + if (request_code == CONFIGURE_INPUT_PROFILE) { + val profile = intent.getStringExtra("profile_name") ?: return super.getArguments() + val deadzone = intent.getIntExtra("deadzone", -1) + val args = mutableListOf( + "--configure-input-profile", + profile, + ) + if (intent.getBooleanExtra("dinput", false)) { + args.add("--use-dinput") + } + if (deadzone != -1) { + args.add("--deadzone") + args.add(deadzone.toString()) + } + setResult(RESULT_OK) // so that the profiles are updated in the GUI + return args.toTypedArray() + } else { + return super.getArguments() + } + } } diff --git a/src/lib.rs b/src/lib.rs index a519a13c..eb6e1049 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -14,85 +14,6 @@ use std::io::Error; #[cfg(target_os = "android")] use ui::android; -/// N64 emulator -#[derive(Parser, Debug)] -#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })] -struct Args { - game: Option, - #[arg(short, long)] - fullscreen: bool, - #[arg(long)] - overclock: Option, - #[arg(long)] - disable_expansion_pak: Option, - #[arg(long, value_name = "CHEATS_FILE", hide = true)] - cheats: Option, - #[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)] - netplay_peer_addr: Option, - #[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)] - netplay_player_number: Option, - #[arg( - long, - value_name = "PROFILE_NAME", - help = "Create a new input profile (keyboard/gamepad mappings)" - )] - configure_input_profile: Option, - #[arg(long, help = "Use DirectInput when configuring a new input profile")] - use_dinput: bool, - #[arg( - long, - value_name = "DEADZONE_PERCENTAGE", - help = "Used along with --configure-input-profile to set the deadzone for analog sticks" - )] - deadzone: Option, - #[arg( - long, - value_name = "PROFILE_NAME", - help = "Must also specify --port. Used to bind a previously created profile to a port" - )] - bind_input_profile: Option, - #[arg( - long, - help = "Lists connected controllers which can be used in --assign-controller" - )] - list_controllers: bool, - #[arg( - long, - value_name = "CONTROLLER_NUMBER", - help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port" - )] - assign_controller: Option, - #[arg( - long, - value_name = "PORT", - help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller" - )] - port: Option, - #[arg( - long, - help = "Clear all input profile bindings and controller assignments" - )] - clear_input_bindings: bool, - #[arg( - long, - value_name = "SLOT", - help = "Load savestate from slot 0-9 when starting the game" - )] - load_state: Option, - #[arg( - long = "ra-username", - value_name = "USERNAME", - help = "Username for RetroAchievements" - )] - ra_username: Option, - #[arg( - long = "ra-password", - value_name = "PASSWORD", - help = "Password for RetroAchievements" - )] - ra_password: Option, -} - pub async fn run() -> std::io::Result<()> { let dirs = ui::get_dirs(); @@ -103,7 +24,7 @@ pub async fn run() -> std::io::Result<()> { ui::sdl_hints(); - let args = Args::parse(); + let args = ui::Args::parse(); if let Some(game) = args.game { let file_path = std::path::Path::new(&game).to_path_buf(); let Some(rom_contents) = device::get_rom_contents(&file_path) else { diff --git a/src/ui.rs b/src/ui.rs index 2d6449b9..2bf4d42c 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -16,6 +16,7 @@ pub mod usb; pub mod video; #[cfg(all(feature = "gui", not(target_os = "android")))] pub mod vru; +use clap::Parser; pub static WEB_CLIENT: std::sync::LazyLock = std::sync::LazyLock::new(|| { reqwest::Client::builder() @@ -28,6 +29,85 @@ pub static WEB_CLIENT: std::sync::LazyLock = std::sync::LazyLoc .unwrap() }); +/// N64 emulator +#[derive(Parser, Debug)] +#[command(author, version=env!("GIT_DESCRIBE"), about, long_about = None, arg_required_else_help = if cfg!(feature = "gui") { false } else { true })] +pub struct Args { + pub game: Option, + #[arg(short, long)] + pub fullscreen: bool, + #[arg(long)] + pub overclock: Option, + #[arg(long)] + pub disable_expansion_pak: Option, + #[arg(long, value_name = "CHEATS_FILE", hide = true)] + pub cheats: Option, + #[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)] + pub netplay_peer_addr: Option, + #[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)] + pub netplay_player_number: Option, + #[arg( + long, + value_name = "PROFILE_NAME", + help = "Create a new input profile (keyboard/gamepad mappings)" + )] + pub configure_input_profile: Option, + #[arg(long, help = "Use DirectInput when configuring a new input profile")] + pub use_dinput: bool, + #[arg( + long, + value_name = "DEADZONE_PERCENTAGE", + help = "Used along with --configure-input-profile to set the deadzone for analog sticks" + )] + pub deadzone: Option, + #[arg( + long, + value_name = "PROFILE_NAME", + help = "Must also specify --port. Used to bind a previously created profile to a port" + )] + pub bind_input_profile: Option, + #[arg( + long, + help = "Lists connected controllers which can be used in --assign-controller" + )] + pub list_controllers: bool, + #[arg( + long, + value_name = "CONTROLLER_NUMBER", + help = "Must also specify --port. Used to assign a controller listed in --list-controllers to a port" + )] + pub assign_controller: Option, + #[arg( + long, + value_name = "PORT", + help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller" + )] + pub port: Option, + #[arg( + long, + help = "Clear all input profile bindings and controller assignments" + )] + pub clear_input_bindings: bool, + #[arg( + long, + value_name = "SLOT", + help = "Load savestate from slot 0-9 when starting the game" + )] + pub load_state: Option, + #[arg( + long = "ra-username", + value_name = "USERNAME", + help = "Username for RetroAchievements" + )] + pub ra_username: Option, + #[arg( + long = "ra-password", + value_name = "PASSWORD", + help = "Password for RetroAchievements" + )] + pub ra_password: Option, +} + #[derive(Clone)] pub struct Dirs { pub config_dir: std::path::PathBuf, @@ -93,6 +173,10 @@ pub fn sdl_hints() { sdl3_sys::everything::SDL_HINT_JOYSTICK_ALLOW_BACKGROUND_EVENTS, hint.as_ptr(), ); + sdl3_sys::everything::SDL_SetHint( + sdl3_sys::everything::SDL_HINT_ANDROID_ALLOW_RECREATE_ACTIVITY, + hint.as_ptr(), + ); } } diff --git a/src/ui/android.rs b/src/ui/android.rs index e7bcc648..5db00f69 100644 --- a/src/ui/android.rs +++ b/src/ui/android.rs @@ -1,4 +1,5 @@ use crate::ui; +use clap::Parser; use jni::objects::{JClass, JObject, JString}; use jni::refs::Global; use jni::sys::jint; @@ -141,7 +142,50 @@ pub struct ControllerInfo { pub descriptor: String, } -pub fn configure_input_profile(profile_name: slint::SharedString, dinput: bool, deadzone: i32) { +fn argv_to_strings(argc: std::ffi::c_int, argv: *mut *mut std::ffi::c_char) -> Vec { + if argc <= 0 || argv.is_null() { + return Vec::new(); + } + unsafe { + (0..argc as usize) + .map(|i| { + std::ffi::CStr::from_ptr(*argv.add(i)) + .to_string_lossy() + .into_owned() + }) + .collect() + } +} + +#[unsafe(no_mangle)] +#[tokio::main(worker_threads = 4)] +pub async extern "C" fn gopher64_sdl_main( + argc: std::ffi::c_int, + argv: *mut *mut std::ffi::c_char, +) -> std::ffi::c_int { + ui::sdl_hints(); + + let raw = argv_to_strings(argc, argv); + let args = ui::Args::try_parse_from(raw).unwrap(); + if let Some(profile) = args.configure_input_profile { + let mut config = ui::config::Config::new(); + ui::input::configure_input_profile( + &mut config, + profile, + args.use_dinput, + args.deadzone.unwrap_or(ui::input::DEADZONE_DEFAULT), + ); + + ui::sdl_close(); + } + 0 +} + +pub fn spawn_configure_input_profile( + profile_name: slint::SharedString, + dinput: bool, + deadzone: i32, +) { if let Some(vm) = get_vm() { if let Err(err) = vm.attach_current_thread(|env| { start_n64_activity_on_jvm(env, profile_name.to_string(), dinput, deadzone) @@ -166,6 +210,7 @@ fn start_n64_activity_on_jvm( let package_name = JString::from_str(env, "io.github.gopher64.gopher64")?; let class_name = JString::from_str(env, "io.github.gopher64.gopher64.N64Activity")?; + let request_code_key = JString::from_str(env, "request_code")?; let profile_name_key = JString::from_str(env, "profile_name")?; let profile_name_value = JString::from_str(env, profile_name)?; let dinput_key = JString::from_str(env, "dinput")?; @@ -174,7 +219,8 @@ fn start_n64_activity_on_jvm( .set_class_name(env, &package_name, &class_name)? .put_extra_string(env, &profile_name_key, &profile_name_value)? .put_extra_boolean(env, &dinput_key, dinput)? - .put_extra_int(env, &deadzone_key, deadzone)?; + .put_extra_int(env, &deadzone_key, deadzone)? + .put_extra_int(env, &request_code_key, CONFIGURE_INPUT_PROFILE)?; activity .as_ref() @@ -445,11 +491,12 @@ pub extern "system" fn Java_io_github_gopher64_gopher64_SlintActivity_nativeOnAc intent_data: JObject<'caller>, ) { let outcome = unowned_env.with_env(|env| -> Result<_, jni::errors::Error> { - if result_code == AndroidActivity::RESULT_OK(env)? && !intent_data.is_null() { + if result_code == AndroidActivity::RESULT_OK(env)? { match request_code { REQUEST_SELECT_ROM => { if let Ok(mut tx_lock) = SELECT_ROM_TX.lock() && let Some(tx) = tx_lock.take() + && !intent_data.is_null() { let result_intent = unsafe { env.as_cast_raw::(&intent_data)? }; diff --git a/src/ui/gui.rs b/src/ui/gui.rs index 55d2d432..0be86406 100644 --- a/src/ui/gui.rs +++ b/src/ui/gui.rs @@ -355,7 +355,7 @@ fn controller_window(app: &AppWindow, config: &ui::config::Config) { handle.set_show_input_profile(false); #[cfg(target_os = "android")] - ui::android::configure_input_profile(profile_name, dinput, deadzone); + ui::android::spawn_configure_input_profile(profile_name, dinput, deadzone); #[cfg(not(target_os = "android"))] tokio::spawn(async move { diff --git a/src/ui/input.rs b/src/ui/input.rs index 3c621680..b3bb47e6 100644 --- a/src/ui/input.rs +++ b/src/ui/input.rs @@ -665,12 +665,16 @@ pub fn configure_input_profile( let title = std::ffi::CString::new("configure input profile").unwrap(); let mut window: *mut sdl3_sys::video::SDL_Window = std::ptr::null_mut(); let mut renderer: *mut sdl3_sys::render::SDL_Renderer = std::ptr::null_mut(); + #[cfg(target_os = "android")] + let window_flags = sdl3_sys::video::SDL_WINDOW_FULLSCREEN; + #[cfg(not(target_os = "android"))] + let window_flags = sdl3_sys::video::SDL_WindowFlags(0); if !unsafe { sdl3_sys::render::SDL_CreateWindowAndRenderer( title.as_ptr(), 852, 480, - sdl3_sys::video::SDL_WindowFlags(0), + window_flags, &mut window, &mut renderer, ) @@ -782,7 +786,11 @@ pub fn configure_input_profile( ); let mut event: sdl3_sys::events::SDL_Event = Default::default(); while !key_set && unsafe { sdl3_sys::events::SDL_WaitEventTimeout(&mut event, 100) } { - if event.event_type() == sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED { + if event.event_type() == sdl3_sys::events::SDL_EVENT_WINDOW_CLOSE_REQUESTED + || (event.event_type() == sdl3_sys::events::SDL_EVENT_KEY_DOWN + && unsafe { event.key.scancode } + == sdl3_sys::scancode::SDL_SCANCODE_AC_BACK) + { close_input_profile_window( open_joysticks, open_controllers, diff --git a/src/ui/video.rs b/src/ui/video.rs index 8b7c58e0..7e844325 100644 --- a/src/ui/video.rs +++ b/src/ui/video.rs @@ -280,10 +280,12 @@ pub fn draw_text( h: (h - sdl3_ttf_sys::ttf::TTF_GetFontHeight(font)) as f32, }; sdl3_sys::render::SDL_RenderTexture(renderer, image_texture, std::ptr::null(), &rect); + let (mut text_w, mut text_h) = (0, 0); + sdl3_ttf_sys::ttf::TTF_GetTextSize(ttf_text, &mut text_w, &mut text_h); sdl3_ttf_sys::ttf::TTF_DrawRendererText( ttf_text, - 20.0, - (h - sdl3_ttf_sys::ttf::TTF_GetFontHeight(font)) as f32, + (w / 2 - text_w / 2) as f32, + (h - text_h) as f32, ); sdl3_sys::render::SDL_RenderPresent(renderer); sdl3_ttf_sys::ttf::TTF_DestroyText(ttf_text);