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