From 56432034af18ddf6e2fe08ba6c44e3ed1b162b02 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Wed, 10 Jul 2024 14:37:35 +0200 Subject: [PATCH] Initial Android backend --- .github/CODEOWNERS | 3 + Cargo.toml | 17 +++ README.md | 6 +- examples/winit.rs | 5 +- examples/winit_android.rs | 18 +++ examples/winit_multithread.rs | 10 +- examples/winit_multithread_android.rs | 18 +++ src/backend_dispatch.rs | 2 + src/backends/android.rs | 154 ++++++++++++++++++++++++++ src/backends/mod.rs | 2 + src/backends/win32.rs | 4 +- 11 files changed, 230 insertions(+), 9 deletions(-) create mode 100644 examples/winit_android.rs create mode 100644 examples/winit_multithread_android.rs create mode 100644 src/backends/android.rs diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b48a2883..d3772452 100644 --- a/.github/CODEOWNERS +++ b/.github/CODEOWNERS @@ -1,3 +1,6 @@ +# Android +/src/android.rs @MarijnS95 + # Apple platforms /src/cg.rs @madsmtm diff --git a/Cargo.toml b/Cargo.toml index fdce0873..57a963ec 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,9 @@ x11-dlopen = ["tiny-xlib/dlopen", "x11rb/dl-libxcb"] log = "0.4.17" raw_window_handle = { package = "raw-window-handle", version = "0.6", features = ["std"] } +[target.'cfg(target_os = "android")'.dependencies] +ndk = "0.9.0" + [target.'cfg(all(unix, not(any(target_vendor = "apple", target_os = "android", target_os = "redox"))))'.dependencies] as-raw-xcb-connection = { version = "1.0.0", optional = true } bytemuck = { version = "1.12.3", optional = true } @@ -89,6 +92,10 @@ criterion = { version = "0.4.0", default-features = false, features = ["cargo_be web-time = "1.0.0" winit = "0.30.0" +[target.'cfg(target_os = "android")'.dev-dependencies] +winit = { version = "0.30.0", features = ["android-native-activity"] } +android-activity = "0.6" + [dev-dependencies.image] version = "0.25.0" # Disable rayon on web @@ -110,6 +117,16 @@ members = [ "run-wasm", ] +[[example]] +# Run with `cargo apk r --example winit_android` +name = "winit_android" +crate-type = ["cdylib"] + +[[example]] +# Run with `cargo apk r --example winit_multithread_android` +name = "winit_multithread_android" +crate-type = ["cdylib"] + [package.metadata.docs.rs] all-features = true rustdoc-args = ["--cfg", "docsrs"] diff --git a/README.md b/README.md index 96355a18..7c4a820a 100644 --- a/README.md +++ b/README.md @@ -42,7 +42,7 @@ For now, the priority for new platforms is: | Platform || |-----------|--| -|Android NDK|❌| +|Android NDK|✅| | AppKit |✅| | Orbital |✅| | UIKit |✅| @@ -61,6 +61,10 @@ For now, the priority for new platforms is: To run an example with the web backend: `cargo run-wasm --example winit` +## Android + +To run the Android-specific example on an Android phone: `cargo apk r --example winit_android` or `cargo apk r --example winit_multithread_android`. + ## Example ```rust,no_run diff --git a/examples/winit.rs b/examples/winit.rs index 09c6c100..27a3cefb 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -6,9 +6,12 @@ use winit::keyboard::{Key, NamedKey}; #[path = "utils/winit_app.rs"] mod winit_app; +#[cfg(not(target_os = "android"))] fn main() { - let event_loop = EventLoop::new().unwrap(); + entry(EventLoop::new().unwrap()) +} +pub(crate) fn entry(event_loop: EventLoop<()>) { let app = winit_app::WinitAppBuilder::with_init( |elwt| { let window = winit_app::make_window(elwt, |w| w); diff --git a/examples/winit_android.rs b/examples/winit_android.rs new file mode 100644 index 00000000..375de37d --- /dev/null +++ b/examples/winit_android.rs @@ -0,0 +1,18 @@ +#![cfg(target_os = "android")] + +use winit::event_loop::EventLoop; +pub use winit::platform::android::{activity::AndroidApp, EventLoopBuilderExtAndroid}; + +#[path = "winit.rs"] +mod desktop_example; + +/// Run with `cargo apk r --example winit_android` +#[no_mangle] +fn android_main(app: AndroidApp) { + let mut builder = EventLoop::builder(); + + // Install the Android event loop extension if necessary. + builder.with_android_app(app); + + desktop_example::entry(builder.build().unwrap()) +} diff --git a/examples/winit_multithread.rs b/examples/winit_multithread.rs index c0d208d0..3f6bce62 100644 --- a/examples/winit_multithread.rs +++ b/examples/winit_multithread.rs @@ -5,7 +5,7 @@ mod winit_app; #[cfg(not(target_family = "wasm"))] -mod ex { +pub mod ex { use std::num::NonZeroU32; use std::sync::{mpsc, Arc, Mutex}; use winit::event::{Event, KeyEvent, WindowEvent}; @@ -59,9 +59,7 @@ mod ex { } } - pub(super) fn entry() { - let event_loop = EventLoop::new().unwrap(); - + pub fn entry(event_loop: EventLoop<()>) { let app = winit_app::WinitAppBuilder::with_init( |elwt| { let attributes = Window::default_attributes(); @@ -140,6 +138,8 @@ mod ex { } } +#[cfg(not(target_os = "android"))] fn main() { - ex::entry(); + use winit::event_loop::EventLoop; + ex::entry(EventLoop::new().unwrap()) } diff --git a/examples/winit_multithread_android.rs b/examples/winit_multithread_android.rs new file mode 100644 index 00000000..6ce92cbe --- /dev/null +++ b/examples/winit_multithread_android.rs @@ -0,0 +1,18 @@ +#![cfg(target_os = "android")] + +use winit::event_loop::EventLoop; +pub use winit::platform::android::{activity::AndroidApp, EventLoopBuilderExtAndroid}; + +#[path = "winit_multithread.rs"] +mod desktop_example; + +/// Run with `cargo apk r --example winit_android` +#[no_mangle] +fn android_main(app: AndroidApp) { + let mut builder = EventLoop::builder(); + + // Install the Android event loop extension if necessary. + builder.with_android_app(app); + + desktop_example::ex::entry(builder.build().unwrap()) +} diff --git a/src/backend_dispatch.rs b/src/backend_dispatch.rs index 208f82cd..a86832ea 100644 --- a/src/backend_dispatch.rs +++ b/src/backend_dispatch.rs @@ -178,6 +178,8 @@ macro_rules! make_dispatch { make_dispatch! { => + #[cfg(target_os = "android")] + Android(D, backends::android::AndroidImpl, backends::android::BufferImpl<'a, D, W>), #[cfg(x11_platform)] X11(Arc>, backends::x11::X11Impl, backends::x11::BufferImpl<'a, D, W>), #[cfg(wayland_platform)] diff --git a/src/backends/android.rs b/src/backends/android.rs new file mode 100644 index 00000000..995e03a5 --- /dev/null +++ b/src/backends/android.rs @@ -0,0 +1,154 @@ +//! Implementation of software buffering for Android. + +use std::marker::PhantomData; +use std::num::{NonZeroI32, NonZeroU32}; + +use ndk::{ + hardware_buffer_format::HardwareBufferFormat, + native_window::{NativeWindow, NativeWindowBufferLockGuard}, +}; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; + +use crate::error::InitError; +use crate::{Rect, SoftBufferError}; + +/// The handle to a window for software buffering. +pub struct AndroidImpl { + native_window: NativeWindow, + + _display: PhantomData, + + /// The pointer to the window object. + /// + /// This is pretty useless because it gives us a pointer to [`NativeWindow`] that we have to increase the refcount on. + /// Alternatively we can use [`NativeWindow::from_ptr()`] wrapped in [`std::mem::ManuallyDrop`] + window: W, +} + +// TODO: Current system doesn't require a trait to be implemented here, even though it exists. +impl AndroidImpl { + /// Create a new [`AndroidImpl`] from an [`AndroidNdkWindowHandle`]. + /// + /// # Safety + /// + /// The [`AndroidNdkWindowHandle`] must be a valid window handle. + // TODO: That's lame, why can't we get an AndroidNdkWindowHandle directly here + pub(crate) fn new(window: W, _display: &D) -> Result> { + // Get the raw Android window (surface). + let raw = window.window_handle()?.as_raw(); + let RawWindowHandle::AndroidNdk(a) = raw else { + return Err(InitError::Unsupported(window)); + }; + + // Acquire a new owned reference to the window, that will be freed on drop. + let native_window = unsafe { NativeWindow::clone_from_ptr(a.a_native_window.cast()) }; + + Ok(Self { + native_window, + // _display: DisplayHandle::borrow_raw(raw_window_handle::RawDisplayHandle::Android( + // AndroidDisplayHandle, + // )), + _display: PhantomData, + window, + }) + } + + #[inline] + pub fn window(&self) -> &W { + &self.window + } + + /// Also changes the pixel format to [`HardwareBufferFormat::R8G8B8A8_UNORM`]. + pub fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { + let (width, height) = (|| { + let width = NonZeroI32::try_from(width).ok()?; + let height = NonZeroI32::try_from(height).ok()?; + Some((width, height)) + })() + .ok_or(SoftBufferError::SizeOutOfRange { width, height })?; + + // Do not change the format. + self.native_window + .set_buffers_geometry( + width.into(), + height.into(), + // Default is typically R5G6B5 16bpp, switch to 32bpp + Some(HardwareBufferFormat::R8G8B8A8_UNORM), + ) + .map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to set buffer geometry on ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + }) + } + + pub fn buffer_mut(&mut self) -> Result, SoftBufferError> { + let lock_guard = self.native_window.lock(None).map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to lock ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + })?; + + assert_eq!( + lock_guard.format().bytes_per_pixel(), + Some(4), + "Unexpected buffer format {:?}, please call .resize() first to change it to RGBA8888", + lock_guard.format() + ); + + Ok(BufferImpl(lock_guard, PhantomData, PhantomData)) + } + + /// Fetch the buffer from the window. + pub fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} + +pub struct BufferImpl<'a, D: ?Sized, W>( + NativeWindowBufferLockGuard<'a>, + PhantomData<&'a D>, + PhantomData<&'a W>, +); + +// TODO: Move to NativeWindowBufferLockGuard? +unsafe impl<'a, D, W> Send for BufferImpl<'a, D, W> {} + +impl<'a, D: HasDisplayHandle + ?Sized, W: HasWindowHandle> BufferImpl<'a, D, W> { + #[inline] + pub fn pixels(&self) -> &[u32] { + todo!() + // unsafe { + // std::slice::from_raw_parts( + // self.0.bits().cast_const().cast(), + // (self.0.stride() * self.0.height()) as usize, + // ) + // } + } + + #[inline] + pub fn pixels_mut(&mut self) -> &mut [u32] { + let bytes = self.0.bytes().expect("Nonplanar format"); + unsafe { + std::slice::from_raw_parts_mut( + bytes.as_mut_ptr().cast(), + bytes.len() / std::mem::size_of::(), + ) + } + } + + pub fn age(&self) -> u8 { + 0 + } + + pub fn present(self) -> Result<(), SoftBufferError> { + // Dropping the guard automatically unlocks and posts it + Ok(()) + } + + pub fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} diff --git a/src/backends/mod.rs b/src/backends/mod.rs index f700b05a..703401d0 100644 --- a/src/backends/mod.rs +++ b/src/backends/mod.rs @@ -1,6 +1,8 @@ use crate::{ContextInterface, InitError}; use raw_window_handle::HasDisplayHandle; +#[cfg(target_os = "android")] +pub(crate) mod android; #[cfg(target_vendor = "apple")] pub(crate) mod cg; #[cfg(kms_platform)] diff --git a/src/backends/win32.rs b/src/backends/win32.rs index 8d7108b9..f0440331 100644 --- a/src/backends/win32.rs +++ b/src/backends/win32.rs @@ -247,8 +247,8 @@ impl SurfaceInterface for Win32Im fn resize(&mut self, width: NonZeroU32, height: NonZeroU32) -> Result<(), SoftBufferError> { let (width, height) = (|| { - let width = NonZeroI32::new(i32::try_from(width.get()).ok()?)?; - let height = NonZeroI32::new(i32::try_from(height.get()).ok()?)?; + let width = NonZeroI32::try_from(width).ok()?; + let height = NonZeroI32::try_from(height).ok()?; Some((width, height)) })() .ok_or(SoftBufferError::SizeOutOfRange { width, height })?;