390 lines
13 KiB
Rust
390 lines
13 KiB
Rust
use jni::objects::{JByteArray, JClass, JIntArray, JObject, JObjectArray, JString};
|
|
use jni::sys::{jboolean, jbyte, jfloat, jint, jlong, jobject};
|
|
use jni::JNIEnv;
|
|
use ndk::native_window::NativeWindow;
|
|
use raw_window_handle::{
|
|
AndroidDisplayHandle, AndroidNdkWindowHandle, RawDisplayHandle, RawWindowHandle,
|
|
};
|
|
use std::ffi::c_void;
|
|
use std::ptr::NonNull;
|
|
use std::sync::Once;
|
|
|
|
use crate::viewport::ViewportHandle;
|
|
|
|
macro_rules! dbg_log {
|
|
($($arg:tt)*) => {
|
|
#[cfg(debug_assertions)]
|
|
log::info!($($arg)*);
|
|
};
|
|
}
|
|
|
|
/// owns the inner viewport plus the ANativeWindow whose lifetime backs the wgpu surface.
|
|
struct AndroidViewport {
|
|
inner: ViewportHandle,
|
|
_window: NativeWindow,
|
|
}
|
|
|
|
/// initializes android logger and the ndk_context singleton consumed by cpal's AAudio backend.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_initContext<'a>(
|
|
env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
activity: JObject<'a>,
|
|
) {
|
|
static ONCE: Once = Once::new();
|
|
ONCE.call_once(|| {
|
|
#[cfg(debug_assertions)]
|
|
android_logger::init_once(
|
|
android_logger::Config::default()
|
|
.with_max_level(log::LevelFilter::Debug)
|
|
.with_tag("yr_crystals"),
|
|
);
|
|
redirect_stdio_to_logcat();
|
|
let vm = match env.get_java_vm() {
|
|
Ok(vm) => vm,
|
|
Err(_) => return,
|
|
};
|
|
let activity_ref = env.new_global_ref(&activity).ok();
|
|
if let Some(activity_ref) = activity_ref {
|
|
unsafe {
|
|
ndk_context::initialize_android_context(
|
|
vm.get_java_vm_pointer() as *mut c_void,
|
|
activity_ref.as_raw() as *mut c_void,
|
|
);
|
|
}
|
|
std::mem::forget(activity_ref);
|
|
}
|
|
dbg_log!("ndk_context initialized");
|
|
});
|
|
}
|
|
|
|
/// builds the viewport from a Surface jobject, holding the ANativeWindow for the surface's life.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportCreate<'a>(
|
|
env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
surface: JObject<'a>,
|
|
width: jfloat,
|
|
height: jfloat,
|
|
scale: jfloat,
|
|
) -> jlong {
|
|
let scale_f = (scale as f32).max(1.0);
|
|
let window = match unsafe { NativeWindow::from_surface(env.get_raw(), surface.as_raw()) } {
|
|
Some(w) => w,
|
|
None => {
|
|
dbg_log!("ANativeWindow_fromSurface returned null");
|
|
return 0;
|
|
}
|
|
};
|
|
let win_ptr = match NonNull::new(window.ptr().as_ptr() as *mut c_void) {
|
|
Some(p) => p,
|
|
None => return 0,
|
|
};
|
|
let raw_window = RawWindowHandle::AndroidNdk(AndroidNdkWindowHandle::new(win_ptr));
|
|
let raw_display = RawDisplayHandle::Android(AndroidDisplayHandle::new());
|
|
let inner = match ViewportHandle::new_from_raw(
|
|
raw_window,
|
|
raw_display,
|
|
width as f32,
|
|
height as f32,
|
|
scale_f,
|
|
) {
|
|
Some(v) => v,
|
|
None => {
|
|
dbg_log!("ViewportHandle::new_from_raw returned None");
|
|
return 0;
|
|
}
|
|
};
|
|
dbg_log!("viewportCreate ok: {}x{}@{}", width, height, scale);
|
|
let boxed = Box::new(AndroidViewport {
|
|
inner,
|
|
_window: window,
|
|
});
|
|
Box::into_raw(boxed) as jlong
|
|
}
|
|
|
|
/// drops the viewport and releases the ANativeWindow reference.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportDestroy<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
) {
|
|
if handle == 0 {
|
|
return;
|
|
}
|
|
unsafe { drop(Box::from_raw(handle as *mut AndroidViewport)) };
|
|
}
|
|
|
|
/// renders one frame from the choreographer callback.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportRender<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
av.inner.render_frame();
|
|
}
|
|
|
|
/// reconfigures the wgpu surface to the given bounds and scale.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportResize<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
width: jfloat,
|
|
height: jfloat,
|
|
scale: jfloat,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
av.inner.resize_px(width as f32, height as f32, (scale as f32).max(1.0));
|
|
}
|
|
|
|
/// forwards a single touch lifecycle event into the viewport gesture pipeline.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportTouchEvent<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
x: jfloat,
|
|
y: jfloat,
|
|
pressed: jboolean,
|
|
moved: jboolean,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
av.inner.push_touch(x as f32, y as f32, pressed != 0, moved != 0);
|
|
}
|
|
|
|
/// forwards an Android KeyEvent into the viewport with optional unicode text.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportKeyEvent<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
key: jint,
|
|
modifiers: jint,
|
|
pressed: jboolean,
|
|
text: JString<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let utf8 = if text.is_null() {
|
|
None
|
|
} else {
|
|
env.get_string(&text).ok().map(|s| String::from(s))
|
|
};
|
|
av.inner.push_key_event(key as u32, utf8, modifiers as u32, pressed != 0);
|
|
}
|
|
|
|
/// resolves a SAF picked folder path into a library scan request.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFolder<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
path: JString<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let Ok(path_str) = env.get_string(&path) else { return };
|
|
av.inner.apply_picked_folder(String::from(path_str).into());
|
|
}
|
|
|
|
/// loads a single picked audio file path as a one-track library.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFile<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
path: JString<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let Ok(path_str) = env.get_string(&path) else { return };
|
|
av.inner.apply_picked_file(String::from(path_str).into());
|
|
}
|
|
|
|
/// passes a batch of picked file paths to the library in a single call.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportApplyPickedFiles<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
paths: JObjectArray<'a>,
|
|
) {
|
|
let Ok(count) = env.get_array_length(&paths) else { return };
|
|
dbg_log!("viewportApplyPickedFiles: enter count={count} handle={handle:#x}");
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else {
|
|
dbg_log!("viewportApplyPickedFiles: null handle");
|
|
return;
|
|
};
|
|
let mut out = Vec::with_capacity(count as usize);
|
|
for i in 0..count {
|
|
let Ok(elem) = env.get_object_array_element(&paths, i) else { continue };
|
|
let jstr: JString = elem.into();
|
|
let Ok(s) = env.get_string(&jstr) else { continue };
|
|
let s = String::from(s);
|
|
dbg_log!("viewportApplyPickedFiles: path[{i}] = {s}");
|
|
out.push(std::path::PathBuf::from(s));
|
|
}
|
|
dbg_log!("viewportApplyPickedFiles: forwarding {} paths to App", out.len());
|
|
av.inner.apply_picked_files(out);
|
|
dbg_log!("viewportApplyPickedFiles: returned from App");
|
|
}
|
|
|
|
/// drives the import progress strip, clearing on a total of zero.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetLibraryProgress<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
current: jint,
|
|
total: jint,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
av.inner.set_library_progress(current.max(0) as u32, total.max(0) as u32);
|
|
}
|
|
|
|
/// shows or clears the modal coordinator-wait overlay; null/empty message hides the overlay.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetCoordinatingMessage<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
msg: JString<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let s = if msg.is_null() {
|
|
None
|
|
} else {
|
|
env.get_string(&msg).ok().map(String::from).filter(|s| !s.is_empty())
|
|
};
|
|
av.inner.set_coordinating_message(s);
|
|
}
|
|
|
|
/// seeds the sidebar with placeholder rows of titles plus track-number tags ahead of export completion.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetPendingTitles<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
titles: JObjectArray<'a>,
|
|
track_numbers: JIntArray<'a>,
|
|
) {
|
|
let Ok(count) = env.get_array_length(&titles) else { return };
|
|
dbg_log!("viewportSetPendingTitles: count={count}");
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else {
|
|
dbg_log!("viewportSetPendingTitles: null handle");
|
|
return;
|
|
};
|
|
let mut tns = vec![0i32; count as usize];
|
|
let _ = env.get_int_array_region(&track_numbers, 0, &mut tns);
|
|
let mut out = Vec::with_capacity(count as usize);
|
|
for i in 0..count {
|
|
let title = env
|
|
.get_object_array_element(&titles, i)
|
|
.ok()
|
|
.map(|e| {
|
|
let js: JString = e.into();
|
|
env.get_string(&js).ok().map(String::from).unwrap_or_default()
|
|
})
|
|
.unwrap_or_default();
|
|
let title = if title.is_empty() {
|
|
format!("Track {}", i + 1)
|
|
} else {
|
|
title
|
|
};
|
|
let tn_raw = tns[i as usize];
|
|
let tn = if tn_raw <= 0 { None } else { Some(tn_raw as u32) };
|
|
out.push((title, tn));
|
|
}
|
|
av.inner.set_pending_titles(out);
|
|
}
|
|
|
|
/// fills in the file path for a placeholder track at completion of the export.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetTrackPath<'a>(
|
|
mut env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
idx: jint,
|
|
path: JString<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let Ok(s) = env.get_string(&path) else { return };
|
|
let s = String::from(s);
|
|
dbg_log!("viewportSetTrackPath: idx={idx} path={s}");
|
|
av.inner.set_track_path(idx.max(0) as usize, std::path::PathBuf::from(s));
|
|
}
|
|
|
|
/// hands raw JPEG/PNG artwork bytes to the art decode worker for a given track index.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportSetTrackArt<'a>(
|
|
env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
idx: jint,
|
|
bytes: JByteArray<'a>,
|
|
) {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return };
|
|
let Ok(len) = env.get_array_length(&bytes) else { return };
|
|
dbg_log!("viewportSetTrackArt: idx={idx} len={len}");
|
|
if len <= 0 { return; }
|
|
let mut buf = vec![0i8; len as usize];
|
|
if env.get_byte_array_region(&bytes, 0, &mut buf).is_err() { return; }
|
|
let owned: Vec<u8> = buf.into_iter().map(|b: jbyte| b as u8).collect();
|
|
av.inner.set_track_art_bytes(idx.max(0) as usize, owned);
|
|
}
|
|
|
|
/// drains the pending picker request flag: 0 idle, 1 folder, 2 file.
|
|
#[unsafe(no_mangle)]
|
|
pub extern "system" fn Java_org_elseif_yrxtals_NativeBridge_viewportTakePendingPick<'a>(
|
|
_env: JNIEnv<'a>,
|
|
_class: JClass<'a>,
|
|
handle: jlong,
|
|
) -> jint {
|
|
let Some(av) = (unsafe { (handle as *mut AndroidViewport).as_mut() }) else { return 0 };
|
|
av.inner.take_pending_pick() as jint
|
|
}
|
|
|
|
/// dup2s stdout and stderr onto a pipe and pumps each line into logcat under the `yr_crystals.io` tag.
|
|
fn redirect_stdio_to_logcat() {
|
|
use std::io::{BufRead, BufReader};
|
|
use std::os::fd::FromRawFd;
|
|
let mut fds: [libc::c_int; 2] = [0, 0];
|
|
let rc = unsafe { libc::pipe(fds.as_mut_ptr()) };
|
|
if rc != 0 {
|
|
return;
|
|
}
|
|
let (rfd, wfd) = (fds[0], fds[1]);
|
|
unsafe {
|
|
libc::dup2(wfd, libc::STDOUT_FILENO);
|
|
libc::dup2(wfd, libc::STDERR_FILENO);
|
|
libc::close(wfd);
|
|
}
|
|
std::thread::Builder::new()
|
|
.name("yr_crystals.stdio_to_logcat".into())
|
|
.spawn(move || {
|
|
let file = unsafe { std::fs::File::from_raw_fd(rfd) };
|
|
let reader = BufReader::new(file);
|
|
for line in reader.lines().flatten() {
|
|
log::info!(target: "yr_crystals.io", "{}", line);
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
|
|
/// nullability check on a JObject through its raw pointer.
|
|
trait JObjectExt {
|
|
fn is_null(&self) -> bool;
|
|
}
|
|
impl<'a> JObjectExt for JObject<'a> {
|
|
fn is_null(&self) -> bool {
|
|
self.as_raw() as *const jobject == std::ptr::null()
|
|
}
|
|
}
|
|
impl<'a> JObjectExt for JString<'a> {
|
|
fn is_null(&self) -> bool {
|
|
let o: &JObject = self.as_ref();
|
|
o.is_null()
|
|
}
|
|
}
|