connect to native code (#955)

* connect to native code

* more

* more

* more

* more

* more

* more

* more

* more
This commit is contained in:
Logan McNaughton
2026-05-26 15:03:00 +02:00
committed by GitHub
parent 9d7b13d4c9
commit 9c9f1a3000
8 changed files with 189 additions and 89 deletions
@@ -18,7 +18,15 @@
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".N64Activity"></activity>
<activity android:name=".N64Activity"
android:launchMode="singleTop"
android:configChanges="layoutDirection|locale|grammaticalGender|fontScale|fontWeightAdjustment|orientation|uiMode|screenLayout|screenSize|smallestScreenSize|keyboard|keyboardHidden|navigation"
android:preferMinimalPostProcessing="true"
android:exported="false">
<intent-filter>
<action android:name="android.hardware.usb.action.USB_DEVICE_ATTACHED" />
</intent-filter>
</activity>
</application>
</manifest>
@@ -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<String> = arrayOf(
"SDL3",
"SDL3_ttf",
"SDL3_image",
"gopher64"
)
override fun getMainFunction(): String = "gopher64_sdl_main"
override fun getArguments(): Array<String> {
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()
}
}
}
+1 -80
View File
@@ -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<String>,
#[arg(short, long)]
fullscreen: bool,
#[arg(long)]
overclock: Option<bool>,
#[arg(long)]
disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
netplay_player_number: Option<u8>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
configure_input_profile: Option<String>,
#[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<i32>,
#[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<String>,
#[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<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
port: Option<usize>,
#[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<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
ra_username: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
ra_password: Option<String>,
}
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 {
+84
View File
@@ -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<reqwest::Client> = std::sync::LazyLock::new(|| {
reqwest::Client::builder()
@@ -28,6 +29,85 @@ pub static WEB_CLIENT: std::sync::LazyLock<reqwest::Client> = 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<String>,
#[arg(short, long)]
pub fullscreen: bool,
#[arg(long)]
pub overclock: Option<bool>,
#[arg(long)]
pub disable_expansion_pak: Option<bool>,
#[arg(long, value_name = "CHEATS_FILE", hide = true)]
pub cheats: Option<String>,
#[arg(long, value_name = "NETPLAY_PEER_ADDR", hide = true)]
pub netplay_peer_addr: Option<String>,
#[arg(long, value_name = "NETPLAY_PLAYER_NUMBER", hide = true)]
pub netplay_player_number: Option<u8>,
#[arg(
long,
value_name = "PROFILE_NAME",
help = "Create a new input profile (keyboard/gamepad mappings)"
)]
pub configure_input_profile: Option<String>,
#[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<i32>,
#[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<String>,
#[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<i32>,
#[arg(
long,
value_name = "PORT",
help = "Valid values: 1-4. To be used alongside --bind-input-profile and --assign-controller"
)]
pub port: Option<usize>,
#[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<u32>,
#[arg(
long = "ra-username",
value_name = "USERNAME",
help = "Username for RetroAchievements"
)]
pub ra_username: Option<String>,
#[arg(
long = "ra-password",
value_name = "PASSWORD",
help = "Password for RetroAchievements"
)]
pub ra_password: Option<String>,
}
#[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(),
);
}
}
+50 -3
View File
@@ -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<String> {
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::<AndroidIntent>(&intent_data)? };
+1 -1
View File
@@ -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 {
+10 -2
View File
@@ -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,
+4 -2
View File
@@ -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);