use crate::{
event::{EventHandler, KeyCode, KeyMods, TouchPhase},
native::{
egl::{self, LibEgl},
NativeDisplayData,
},
};
use std::{cell::RefCell, sync::mpsc, thread, time::Duration};
pub use crate::native::gl::{self, *};
mod keycodes;
pub use ndk_sys;
pub mod ndk_utils;
#[no_mangle]
pub unsafe extern "C" fn JNI_OnLoad(
vm: *mut ndk_sys::JavaVM,
_: std::ffi::c_void,
) -> ndk_sys::jint {
VM = vm as *mut _ as _;
ndk_sys::JNI_VERSION_1_6 as _
}
extern "C" {
fn quad_main();
}
#[derive(Debug)]
enum Message {
SurfaceChanged {
width: i32,
height: i32,
},
SurfaceCreated {
window: *mut ndk_sys::ANativeWindow,
},
SurfaceDestroyed,
Touch {
phase: TouchPhase,
touch_id: u64,
x: f32,
y: f32,
},
Character {
character: u32,
},
KeyDown {
keycode: KeyCode,
},
KeyUp {
keycode: KeyCode,
},
Pause,
Resume,
Destroy,
Request(crate::native::Request),
}
unsafe impl Send for Message {}
thread_local! {
static MESSAGES_TX: RefCell<Option<mpsc::Sender<Message>>> = RefCell::new(None);
}
fn send_message(message: Message) {
MESSAGES_TX.with(|tx| {
let mut tx = tx.borrow_mut();
tx.as_mut().unwrap().send(message).unwrap();
})
}
pub static mut ACTIVITY: ndk_sys::jobject = std::ptr::null_mut();
static mut VM: *mut ndk_sys::JavaVM = std::ptr::null_mut();
pub unsafe fn console_debug(msg: *const ::core::ffi::c_char) {
ndk_sys::__android_log_write(
ndk_sys::android_LogPriority_ANDROID_LOG_DEBUG as _,
b"SAPP\0".as_ptr() as _,
msg,
);
}
pub unsafe fn console_info(msg: *const ::core::ffi::c_char) {
ndk_sys::__android_log_write(
ndk_sys::android_LogPriority_ANDROID_LOG_INFO as _,
b"SAPP\0".as_ptr() as _,
msg,
);
}
pub unsafe fn console_warn(msg: *const ::core::ffi::c_char) {
ndk_sys::__android_log_write(
ndk_sys::android_LogPriority_ANDROID_LOG_WARN as _,
b"SAPP\0".as_ptr() as _,
msg,
);
}
pub unsafe fn console_error(msg: *const ::core::ffi::c_char) {
ndk_sys::__android_log_write(
ndk_sys::android_LogPriority_ANDROID_LOG_ERROR as _,
b"SAPP\0".as_ptr() as _,
msg,
);
}
struct MainThreadState {
libegl: LibEgl,
egl_display: egl::EGLDisplay,
egl_config: egl::EGLConfig,
egl_context: egl::EGLContext,
surface: egl::EGLSurface,
window: *mut ndk_sys::ANativeWindow,
event_handler: Box<dyn EventHandler>,
quit: bool,
fullscreen: bool,
update_requested: bool,
keymods: KeyMods,
}
impl MainThreadState {
unsafe fn destroy_surface(&mut self) {
(self.libegl.eglMakeCurrent)(
self.egl_display,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
(self.libegl.eglDestroySurface)(self.egl_display, self.surface);
self.surface = std::ptr::null_mut();
}
unsafe fn update_surface(&mut self, window: *mut ndk_sys::ANativeWindow) {
if !self.window.is_null() {
ndk_sys::ANativeWindow_release(self.window);
}
self.window = window;
if self.surface.is_null() == false {
self.destroy_surface();
}
self.surface = (self.libegl.eglCreateWindowSurface)(
self.egl_display,
self.egl_config,
window as _,
std::ptr::null_mut(),
);
assert!(!self.surface.is_null());
let res = (self.libegl.eglMakeCurrent)(
self.egl_display,
self.surface,
self.surface,
self.egl_context,
);
assert!(res != 0);
}
fn process_message(&mut self, msg: Message) {
match msg {
Message::SurfaceCreated { window } => unsafe {
self.update_surface(window);
},
Message::SurfaceDestroyed => unsafe {
self.destroy_surface();
},
Message::SurfaceChanged { width, height } => {
{
let mut d = crate::native_display().lock().unwrap();
d.screen_width = width as _;
d.screen_height = height as _;
}
self.event_handler.resize_event(width as _, height as _);
}
Message::Touch {
phase,
touch_id,
x,
y,
} => {
self.event_handler.touch_event(phase, touch_id, x, y);
}
Message::Character { character } => {
if let Some(character) = char::from_u32(character) {
self.event_handler
.char_event(character, Default::default(), false);
}
}
Message::KeyDown { keycode } => {
match keycode {
KeyCode::LeftShift | KeyCode::RightShift => self.keymods.shift = true,
KeyCode::LeftControl | KeyCode::RightControl => self.keymods.ctrl = true,
KeyCode::LeftAlt | KeyCode::RightAlt => self.keymods.alt = true,
KeyCode::LeftSuper | KeyCode::RightSuper => self.keymods.logo = true,
_ => {}
}
self.event_handler
.key_down_event(keycode, self.keymods, false);
}
Message::KeyUp { keycode } => {
match keycode {
KeyCode::LeftShift | KeyCode::RightShift => self.keymods.shift = false,
KeyCode::LeftControl | KeyCode::RightControl => self.keymods.ctrl = false,
KeyCode::LeftAlt | KeyCode::RightAlt => self.keymods.alt = false,
KeyCode::LeftSuper | KeyCode::RightSuper => self.keymods.logo = false,
_ => {}
}
self.event_handler.key_up_event(keycode, self.keymods);
}
Message::Pause => self.event_handler.window_minimized_event(),
Message::Resume => {
if self.fullscreen {
unsafe {
let env = attach_jni_env();
set_full_screen(env, true);
}
}
self.event_handler.window_restored_event()
}
Message::Destroy => {
self.quit = true;
self.event_handler.quit_requested_event()
}
Message::Request(req) => self.process_request(req),
}
}
fn frame(&mut self) {
self.event_handler.update();
if self.surface.is_null() == false {
self.update_requested = false;
self.event_handler.draw();
unsafe {
(self.libegl.eglSwapBuffers)(self.egl_display, self.surface);
}
}
}
fn process_request(&mut self, request: crate::native::Request) {
use crate::native::Request::*;
match request {
ScheduleUpdate => {
self.update_requested = true;
}
SetFullscreen(fullscreen) => {
unsafe {
let env = attach_jni_env();
set_full_screen(env, fullscreen);
}
self.fullscreen = fullscreen;
}
ShowKeyboard(show) => unsafe {
let env = attach_jni_env();
ndk_utils::call_void_method!(env, ACTIVITY, "showKeyboard", "(Z)V", show as i32);
},
SetImePosition { .. } => {
}
SetImeEnabled(..) => {
}
_ => {}
}
}
}
pub unsafe fn attach_jni_env() -> *mut ndk_sys::JNIEnv {
let mut env: *mut ndk_sys::JNIEnv = std::ptr::null_mut();
let attach_current_thread = (**VM).AttachCurrentThread.unwrap();
let res = attach_current_thread(VM, &mut env, std::ptr::null_mut());
assert!(res == 0);
env
}
pub struct AndroidClipboard {}
impl AndroidClipboard {
pub fn new() -> AndroidClipboard {
AndroidClipboard {}
}
}
impl crate::native::Clipboard for AndroidClipboard {
fn get(&mut self) -> Option<String> {
unsafe {
let env = attach_jni_env();
let text = ndk_utils::call_object_method!(
env,
ACTIVITY,
"getClipboardText",
"()Ljava/lang/String;"
);
if text.is_null() {
return None;
}
let text = ndk_utils::get_utf_str!(env, text);
Some(text)
}
}
fn set(&mut self, data: &str) {
let data = std::ffi::CString::new(data).unwrap();
unsafe {
let env = attach_jni_env();
let new_string_utf = (**env).NewStringUTF.unwrap();
let jtext = new_string_utf(env, data.as_ptr());
ndk_utils::call_void_method!(
env,
ACTIVITY,
"setClipboardText",
"(Ljava/lang/String;)V",
jtext
);
}
}
}
pub unsafe fn run<F>(conf: crate::conf::Conf, f: F)
where
F: 'static + FnOnce() -> Box<dyn EventHandler>,
{
if conf.platform.android_panic_hook {
use std::ffi::CString;
use std::panic;
panic::set_hook(Box::new(|info| {
let msg = CString::new(format!("{info}")).unwrap_or_else(|_| {
CString::new(format!("MALFORMED ERROR MESSAGE {:?}", info.location())).unwrap()
});
console_error(msg.as_ptr());
}));
}
if conf.fullscreen {
let env = attach_jni_env();
set_full_screen(env, true);
}
struct SendHack<F>(F);
unsafe impl<F> Send for SendHack<F> {}
let f = SendHack(f);
let (tx, rx) = mpsc::channel();
let tx2 = tx.clone();
MESSAGES_TX.with(move |messages_tx| *messages_tx.borrow_mut() = Some(tx2));
thread::spawn(move || {
let mut libegl = LibEgl::try_load().expect("Cant load LibEGL");
let window = 'a: loop {
match rx.try_recv() {
Ok(Message::SurfaceCreated { window }) => {
break 'a window;
}
_ => {}
}
};
let (screen_width, screen_height) = 'a: loop {
match rx.try_recv() {
Ok(Message::SurfaceChanged { width, height }) => {
break 'a (width as f32, height as f32);
}
_ => {}
}
};
let (egl_context, egl_config, egl_display) = crate::native::egl::create_egl_context(
&mut libegl,
std::ptr::null_mut(),
conf.platform.framebuffer_alpha,
conf.sample_count,
)
.expect("Cant create EGL context");
assert!(!egl_display.is_null());
assert!(!egl_config.is_null());
crate::native::gl::load_gl_funcs(|proc| {
let name = std::ffi::CString::new(proc).unwrap();
(libegl.eglGetProcAddress)(name.as_ptr() as _)
});
let surface = (libegl.eglCreateWindowSurface)(
egl_display,
egl_config,
window as _,
std::ptr::null_mut(),
);
if (libegl.eglMakeCurrent)(egl_display, surface, surface, egl_context) == 0 {
panic!();
}
let clipboard = Box::new(AndroidClipboard::new());
let tx_fn = Box::new(move |req| tx.send(Message::Request(req)).unwrap());
crate::set_or_replace_display(NativeDisplayData {
high_dpi: conf.high_dpi,
blocking_event_loop: conf.platform.blocking_event_loop,
..NativeDisplayData::new(screen_width as _, screen_height as _, tx_fn, clipboard)
});
let event_handler = f.0();
let mut s = MainThreadState {
libegl,
egl_display,
egl_config,
egl_context,
surface,
window,
event_handler,
quit: false,
fullscreen: conf.fullscreen,
update_requested: true,
keymods: KeyMods {
shift: false,
ctrl: false,
alt: false,
logo: false,
},
};
let rx_timeout = conf
.platform
.sleep_interval_ms
.map(|sleep| Duration::from_millis(sleep as u64));
while !s.quit {
let block_on_wait = conf.platform.blocking_event_loop && !s.update_requested;
if block_on_wait {
match rx_recv(&rx, rx_timeout) {
Ok(msg) => s.process_message(msg),
Err(mpsc::RecvTimeoutError::Timeout) => s.update_requested = true,
Err(mpsc::RecvTimeoutError::Disconnected) => panic!(),
}
} else {
while let Ok(msg) = rx.try_recv() {
s.process_message(msg);
}
}
if !conf.platform.blocking_event_loop || s.update_requested {
s.frame();
}
thread::yield_now();
}
(s.libegl.eglMakeCurrent)(
s.egl_display,
std::ptr::null_mut(),
std::ptr::null_mut(),
std::ptr::null_mut(),
);
(s.libegl.eglDestroySurface)(s.egl_display, s.surface);
(s.libegl.eglDestroyContext)(s.egl_display, s.egl_context);
(s.libegl.eglTerminate)(s.egl_display);
});
}
fn rx_recv<T>(
rx: &mpsc::Receiver<T>,
timeout: Option<Duration>,
) -> Result<T, mpsc::RecvTimeoutError> {
match timeout {
Some(timeout) => rx.recv_timeout(timeout),
None => rx.recv().map_err(|_| mpsc::RecvTimeoutError::Disconnected),
}
}
#[no_mangle]
extern "C" fn jni_on_load(vm: *mut std::ffi::c_void) {
unsafe {
VM = vm as _;
}
}
unsafe fn create_native_window(surface: ndk_sys::jobject) -> *mut ndk_sys::ANativeWindow {
let env = attach_jni_env();
ndk_sys::ANativeWindow_fromSurface(env, surface)
}
#[no_mangle]
pub unsafe extern "C" fn Java_quad_1native_QuadNative_activityOnCreate(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
activity: ndk_sys::jobject,
) {
let env = attach_jni_env();
ACTIVITY = (**env).NewGlobalRef.unwrap()(env, activity);
quad_main();
}
#[no_mangle]
unsafe extern "C" fn Java_quad_1native_QuadNative_activityOnResume(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
) {
send_message(Message::Resume);
}
#[no_mangle]
unsafe extern "C" fn Java_quad_1native_QuadNative_activityOnPause(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
) {
send_message(Message::Pause);
}
#[no_mangle]
unsafe extern "C" fn Java_quad_1native_QuadNative_activityOnDestroy(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
) {
send_message(Message::Destroy);
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnSurfaceCreated(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
surface: ndk_sys::jobject,
) {
let window = unsafe { create_native_window(surface) };
send_message(Message::SurfaceCreated { window });
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnSurfaceDestroyed(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
) {
send_message(Message::SurfaceDestroyed);
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnSurfaceChanged(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
_: ndk_sys::jobject,
width: ndk_sys::jint,
height: ndk_sys::jint,
) {
send_message(Message::SurfaceChanged {
width: width as _,
height: height as _,
});
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnTouch(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
touch_id: ndk_sys::jint,
action: ndk_sys::jint,
x: ndk_sys::jfloat,
y: ndk_sys::jfloat,
) {
let phase = match action {
0 => TouchPhase::Moved,
1 => TouchPhase::Ended,
2 => TouchPhase::Started,
3 => TouchPhase::Cancelled,
x => panic!("Unsupported touch phase: {}", x),
};
send_message(Message::Touch {
phase,
touch_id: touch_id as _,
x: x as f32,
y: y as f32,
});
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnKeyDown(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
keycode: ndk_sys::jint,
) {
let keycode = keycodes::translate_keycode(keycode as _);
send_message(Message::KeyDown { keycode });
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnKeyUp(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
keycode: ndk_sys::jint,
) {
let keycode = keycodes::translate_keycode(keycode as _);
send_message(Message::KeyUp { keycode });
}
#[no_mangle]
extern "C" fn Java_quad_1native_QuadNative_surfaceOnCharacter(
_: *mut ndk_sys::JNIEnv,
_: ndk_sys::jobject,
character: ndk_sys::jint,
) {
send_message(Message::Character {
character: character as u32,
});
}
unsafe fn set_full_screen(env: *mut ndk_sys::JNIEnv, fullscreen: bool) {
ndk_utils::call_void_method!(env, ACTIVITY, "setFullScreen", "(Z)V", fullscreen as i32);
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct android_asset {
pub content: *mut ::core::ffi::c_char,
pub content_length: ::core::ffi::c_int,
}
extern "C" {
pub fn AAssetManager_fromJava(
env: *mut ndk_sys::JNIEnv,
assetManager: ndk_sys::jobject,
) -> *mut ndk_sys::AAssetManager;
}
pub(crate) unsafe fn load_asset(filepath: *const ::core::ffi::c_char, out: *mut android_asset) {
let env = attach_jni_env();
let get_method_id = (**env).GetMethodID.unwrap();
let get_object_class = (**env).GetObjectClass.unwrap();
let call_object_method = (**env).CallObjectMethod.unwrap();
let mid = (get_method_id)(
env,
get_object_class(env, ACTIVITY),
b"getAssets\0".as_ptr() as _,
b"()Landroid/content/res/AssetManager;\0".as_ptr() as _,
);
let asset_manager = (call_object_method)(env, ACTIVITY, mid);
let mgr = AAssetManager_fromJava(env, asset_manager);
let asset = ndk_sys::AAssetManager_open(mgr, filepath, ndk_sys::AASSET_MODE_BUFFER as _);
if asset.is_null() {
return;
}
let length = ndk_sys::AAsset_getLength64(asset);
let buffer = libc::malloc(length as _);
if ndk_sys::AAsset_read(asset, buffer, length as _) > 0 {
ndk_sys::AAsset_close(asset);
(*out).content_length = length as _;
(*out).content = buffer as _;
}
}