mirror of
https://github.com/gopher64/gopher64.git
synced 2026-07-11 01:25:20 +02:00
connect to native code (#955)
* connect to native code * more * more * more * more * more * more * more * more
This commit is contained in:
@@ -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
@@ -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 {
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user