diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS index b48a288..d377245 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/CHANGELOG.md b/CHANGELOG.md index 7736994..4e45763 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -73,38 +73,38 @@ # 0.3.0 -* On MacOS, the contents scale is updated when set_buffer() is called, to adapt when the window is on a new screen (#68). -* **Breaking:** Split the `GraphicsContext` type into `Context` and `Surface` (#64). -* On Web, cache the document in the `Context` type (#66). -* **Breaking:** Introduce a new "owned buffer" for no-copy presentation (#65). -* Enable support for multi-threaded WASM (#77). -* Fix buffer resizing on X11 (#69). -* Add a set of functions for handling buffer damage (#99). -* Add a `fetch()` function for getting the window contents (#104). -* Bump MSRV to 1.64 (#81). +- On MacOS, the contents scale is updated when set_buffer() is called, to adapt when the window is on a new screen (#68). +- **Breaking:** Split the `GraphicsContext` type into `Context` and `Surface` (#64). +- On Web, cache the document in the `Context` type (#66). +- **Breaking:** Introduce a new "owned buffer" for no-copy presentation (#65). +- Enable support for multi-threaded WASM (#77). +- Fix buffer resizing on X11 (#69). +- Add a set of functions for handling buffer damage (#99). +- Add a `fetch()` function for getting the window contents (#104). +- Bump MSRV to 1.64 (#81). # 0.2.1 -* Bump `windows-sys` to 0.48 +- Bump `windows-sys` to 0.48 # 0.2.0 -* Add support for Redox/Orbital. -* Add support for BSD distributions. -* Ported Windows backend from `winapi` to `windows-sys`. -* **Breaking:** Take a reference to a window instead of owning the window. -* Add a `from_raw` function for directly using raw handles. -* Improvements for Wayland support. -* Support for HiDPI on macOS. -* **Breaking:** Add feature flags for `x11` and `wayland` backends. -* Use static dispatch instead of dynamic dispatch for the backends. -* Add `libxcb` support to the X11 backend. -* Use X11 MIT-SHM extension, if available. +- Add support for Redox/Orbital. +- Add support for BSD distributions. +- Ported Windows backend from `winapi` to `windows-sys`. +- **Breaking:** Take a reference to a window instead of owning the window. +- Add a `from_raw` function for directly using raw handles. +- Improvements for Wayland support. +- Support for HiDPI on macOS. +- **Breaking:** Add feature flags for `x11` and `wayland` backends. +- Use static dispatch instead of dynamic dispatch for the backends. +- Add `libxcb` support to the X11 backend. +- Use X11 MIT-SHM extension, if available. # 0.1.1 -* Added WASM support (Thanks to [Liamolucko](https://github.com/Liamolucko)!) -* CALayer is now used for Mac OS backend, which is more flexible about what happens in the windowing library (Thanks to [lunixbochs](https://github.com/lunixbochs)!) +- Added WASM support (Thanks to [Liamolucko](https://github.com/Liamolucko)!) +- CALayer is now used for Mac OS backend, which is more flexible about what happens in the windowing library (Thanks to [lunixbochs](https://github.com/lunixbochs)!) # 0.1.0 diff --git a/Cargo.toml b/Cargo.toml index fdce087..153e692 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -28,6 +28,10 @@ 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] +bytemuck = "1.12.3" +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 +93,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 +118,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 be7084b..7c4a820 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,4 @@ -Overview -== +# Softbuffer Enables software rendering via drawing an image straight to a window. @@ -7,8 +6,7 @@ Softbuffer integrates with the [`raw-window-handle`](https://crates.io/crates/ra to allow writing pixels to a window in a cross-platform way while using the very high quality dedicated window management libraries that are available in the Rust ecosystem. -Alternatives -== +## Alternatives [minifb](https://crates.io/crates/minifb) also allows putting a 2D buffer/image on a window in a platform-independent way. Minifb's approach to doing window management itself, however, is problematic code duplication. We already have very high quality libraries for this in the Rust ecosystem @@ -24,27 +22,27 @@ hardware accelerated graphics stack in any way, and is thus more portable to ins hardware acceleration (e.g. VMs, older computers, computers with misconfigured drivers). Softbuffer should be used over pixels when its GPU-accelerated post-processing effects are not needed. -License & Credits -== +## License & Credits This library is dual-licensed under MIT or Apache-2.0, just like minifb and rust. Significant portions of code were taken from the minifb library to do platform-specific work. -Platform support: -== +## Platform support: + Some, but not all, platforms supported in [raw-window-handle](https://crates.io/crates/raw-window-handle) are supported by Softbuffer. Pull requests are welcome to add new platforms! **Nonetheless, all major desktop platforms that winit uses on desktop are supported.** For now, the priority for new platforms is: -1) to have at least one platform on each OS working (e.g. one of Win32 or WinRT, or one of Xlib, Xcb, and Wayland) and -2) for that one platform on each OS to be the one that winit uses. + +1. to have at least one platform on each OS working (e.g. one of Win32 or WinRT, or one of Xlib, Xcb, and Wayland) and +2. for that one platform on each OS to be the one that winit uses. (PRs will be accepted for any platform, even if it does not follow the above priority.) | Platform || |-----------|--| -|Android NDK|❌| +|Android NDK|✅| | AppKit |✅| | Orbital |✅| | UIKit |✅| @@ -59,13 +57,16 @@ For now, the priority for new platforms is: ❔: Immature\ ❌: Absent -WebAssembly ------------ +## WebAssembly To run an example with the web backend: `cargo run-wasm --example winit` -Example -== +## 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 use std::num::NonZeroU32; use std::rc::Rc; @@ -79,21 +80,27 @@ mod winit_app; fn main() { let event_loop = EventLoop::new().unwrap(); - let mut app = winit_app::WinitAppBuilder::with_init(|elwt| { - let window = { - let window = elwt.create_window(Window::default_attributes()); - Rc::new(window.unwrap()) - }; - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); - - (window, surface) - }).with_event_handler(|state, event, elwt| { - let (window, surface) = state; + let mut app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let window = { + let window = elwt.create_window(Window::default_attributes()); + Rc::new(window.unwrap()) + }; + let context = softbuffer::Context::new(window.clone()).unwrap(); + + (window, context) + }, + |_elwt, (window, context)| softbuffer::Surface::new(context, window.clone()).unwrap(), + ) + .with_event_handler(|(window, _context), surface, event, elwt| { elwt.set_control_flow(ControlFlow::Wait); match event { Event::WindowEvent { window_id, event: WindowEvent::RedrawRequested } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; let (width, height) = { let size = window.inner_size(); (size.width, size.height) @@ -132,8 +139,8 @@ fn main() { } ``` -MSRV Policy -== +## MSRV Policy + This crate's Minimum Supported Rust Version (MSRV) is **1.70**. Changes to the MSRV will be accompanied by a minor version bump. @@ -157,7 +164,6 @@ same MSRV policy. [`rust-windowing`]: https://github.com/rust-windowing -Changelog ---------- +## Changelog See the [changelog](CHANGELOG.md) for a list of this package's versions and the changes made in each version. diff --git a/examples/animation.rs b/examples/animation.rs index 6df2265..c075923 100644 --- a/examples/animation.rs +++ b/examples/animation.rs @@ -14,19 +14,23 @@ fn main() { let event_loop = EventLoop::new().unwrap(); let start = Instant::now(); - let app = winit_app::WinitAppBuilder::with_init(|event_loop| { - let window = winit_app::make_window(event_loop, |w| w); + let app = winit_app::WinitAppBuilder::with_init( + |event_loop| { + let window = winit_app::make_window(event_loop, |w| w); - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let context = softbuffer::Context::new(window.clone()).unwrap(); - let old_size = (0, 0); - let frames = pre_render_frames(0, 0); + let old_size = (0, 0); + let frames = pre_render_frames(0, 0); - (window, surface, old_size, frames) - }) - .with_event_handler(move |state, event, elwt| { - let (window, surface, old_size, frames) = state; + (window, context, old_size, frames) + }, + |_elwft, (window, context, _old_size, _frames)| { + softbuffer::Surface::new(context, window.clone()).unwrap() + }, + ) + .with_event_handler(move |state, surface, event, elwt| { + let (window, _context, old_size, frames) = state; elwt.set_control_flow(ControlFlow::Poll); @@ -35,6 +39,11 @@ fn main() { window_id, event: WindowEvent::Resized(size), } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("Resized fired before Resumed or after Suspended"); + return; + }; + if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) { @@ -45,6 +54,11 @@ fn main() { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + let size = window.inner_size(); if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) diff --git a/examples/fruit.rs b/examples/fruit.rs index cecd4af..0402e8a 100644 --- a/examples/fruit.rs +++ b/examples/fruit.rs @@ -14,28 +14,32 @@ fn main() { let event_loop = EventLoop::new().unwrap(); - let app = winit_app::WinitAppBuilder::with_init(move |elwt| { - let window = winit_app::make_window(elwt, |w| { - w.with_inner_size(winit::dpi::PhysicalSize::new(width, height)) - }); + let app = winit_app::WinitAppBuilder::with_init( + move |elwt| { + let window = winit_app::make_window(elwt, |w| { + w.with_inner_size(winit::dpi::PhysicalSize::new(width, height)) + }); - let context = softbuffer::Context::new(window.clone()).unwrap(); - let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let context = softbuffer::Context::new(window.clone()).unwrap(); - // Intentionally only set the size of the surface once, at creation. - // This is needed if the window chooses to ignore the size we passed in above, and for the - // platforms softbuffer supports that don't yet extract the size from the window. - surface - .resize( - NonZeroU32::new(width).unwrap(), - NonZeroU32::new(height).unwrap(), - ) - .unwrap(); - - (window, surface) - }) - .with_event_handler(move |state, event, elwt| { - let (window, surface) = state; + (window, context) + }, + move |_elwt, (window, context)| { + let mut surface = softbuffer::Surface::new(context, window.clone()).unwrap(); + // Intentionally only set the size of the surface once, at creation. + // This is needed if the window chooses to ignore the size we passed in above, and for the + // platforms softbuffer supports that don't yet extract the size from the window. + surface + .resize( + NonZeroU32::new(width).unwrap(), + NonZeroU32::new(height).unwrap(), + ) + .unwrap(); + surface + }, + ) + .with_event_handler(move |state, surface, event, elwt| { + let (window, _context) = state; elwt.set_control_flow(ControlFlow::Wait); match event { @@ -43,6 +47,11 @@ fn main() { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + let mut buffer = surface.buffer_mut().unwrap(); let width = fruit.width() as usize; for (x, y, pixel) in fruit.pixels() { diff --git a/examples/rectangle.rs b/examples/rectangle.rs index 41f8be4..b92f30b 100644 --- a/examples/rectangle.rs +++ b/examples/rectangle.rs @@ -25,20 +25,24 @@ fn redraw(buffer: &mut [u32], width: usize, height: usize, flag: bool) { fn main() { let event_loop = EventLoop::new().unwrap(); - let app = winit_app::WinitAppBuilder::with_init(|elwt| { - let window = winit_app::make_window(elwt, |w| { - w.with_title("Press space to show/hide a rectangle") - }); + let app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let window = winit_app::make_window(elwt, |w| { + w.with_title("Press space to show/hide a rectangle") + }); - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let context = softbuffer::Context::new(window.clone()).unwrap(); - let flag = false; + let flag = false; - (window, surface, flag) - }) - .with_event_handler(|state, event, elwt| { - let (window, surface, flag) = state; + (window, context, flag) + }, + |_elwt, (window, context, _flag)| { + softbuffer::Surface::new(context, window.clone()).unwrap() + }, + ) + .with_event_handler(|state, surface, event, elwt| { + let (window, _context, flag) = state; elwt.set_control_flow(ControlFlow::Wait); @@ -47,6 +51,11 @@ fn main() { window_id, event: WindowEvent::Resized(size), } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("Resized fired before Resumed or after Suspended"); + return; + }; + if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) { @@ -58,6 +67,10 @@ fn main() { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; // Grab the window's client area dimensions, and ensure they're valid let size = window.inner_size(); if let (Some(width), Some(height)) = diff --git a/examples/utils/winit_app.rs b/examples/utils/winit_app.rs index f427894..3b4cb4d 100644 --- a/examples/utils/winit_app.rs +++ b/examples/utils/winit_app.rs @@ -31,76 +31,94 @@ pub(crate) fn make_window( } /// Easily constructable winit application. -pub(crate) struct WinitApp { - /// Closure to initialize state. +pub(crate) struct WinitApp { + /// Closure to initialize `state`. init: Init, + /// Closure to initialize `surface_state`. + init_surface: InitSurface, + /// Closure to run on window events. event: Handler, /// Contained state. state: Option, + + /// Contained surface state. + surface_state: Option, } /// Builder that makes it so we don't have to name `T`. -pub(crate) struct WinitAppBuilder { - /// Closure to initialize state. +pub(crate) struct WinitAppBuilder { + /// Closure to initialize `state`. init: Init, + /// Closure to initialize `surface_state`. + init_surface: InitSurface, + /// Eat the type parameter. - _marker: PhantomData>, + _marker: PhantomData<(Option, Option)>, } -impl WinitAppBuilder +impl WinitAppBuilder where Init: FnMut(&ActiveEventLoop) -> T, + InitSurface: FnMut(&ActiveEventLoop, &mut T) -> S, { /// Create with an "init" closure. - pub(crate) fn with_init(init: Init) -> Self { + pub(crate) fn with_init(init: Init, init_surface: InitSurface) -> Self { Self { init, + init_surface, _marker: PhantomData, } } /// Build a new application. - pub(crate) fn with_event_handler(self, handler: F) -> WinitApp + pub(crate) fn with_event_handler(self, handler: F) -> WinitApp where - F: FnMut(&mut T, Event<()>, &ActiveEventLoop), + F: FnMut(&mut T, Option<&mut S>, Event<()>, &ActiveEventLoop), { - WinitApp::new(self.init, handler) + WinitApp::new(self.init, self.init_surface, handler) } } -impl WinitApp +impl WinitApp where Init: FnMut(&ActiveEventLoop) -> T, - Handler: FnMut(&mut T, Event<()>, &ActiveEventLoop), + InitSurface: FnMut(&ActiveEventLoop, &mut T) -> S, + Handler: FnMut(&mut T, Option<&mut S>, Event<()>, &ActiveEventLoop), { /// Create a new application. - pub(crate) fn new(init: Init, event: Handler) -> Self { + pub(crate) fn new(init: Init, init_surface: InitSurface, event: Handler) -> Self { Self { init, + init_surface, event, state: None, + surface_state: None, } } } -impl ApplicationHandler for WinitApp +impl ApplicationHandler + for WinitApp where Init: FnMut(&ActiveEventLoop) -> T, - Handler: FnMut(&mut T, Event<()>, &ActiveEventLoop), + InitSurface: FnMut(&ActiveEventLoop, &mut T) -> S, + Handler: FnMut(&mut T, Option<&mut S>, Event<()>, &ActiveEventLoop), { fn resumed(&mut self, el: &ActiveEventLoop) { debug_assert!(self.state.is_none()); - self.state = Some((self.init)(el)); + let mut state = (self.init)(el); + self.surface_state = Some((self.init_surface)(el, &mut state)); + self.state = Some(state); } fn suspended(&mut self, _event_loop: &ActiveEventLoop) { - let state = self.state.take(); - debug_assert!(state.is_some()); - drop(state); + let surface_state = self.surface_state.take(); + debug_assert!(surface_state.is_some()); + drop(surface_state); } fn window_event( @@ -110,12 +128,23 @@ where event: WindowEvent, ) { let state = self.state.as_mut().unwrap(); - (self.event)(state, Event::WindowEvent { window_id, event }, event_loop); + let surface_state = self.surface_state.as_mut(); + (self.event)( + state, + surface_state, + Event::WindowEvent { window_id, event }, + event_loop, + ); } fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { if let Some(state) = self.state.as_mut() { - (self.event)(state, Event::AboutToWait, event_loop); + (self.event)( + state, + self.surface_state.as_mut(), + Event::AboutToWait, + event_loop, + ); } } } diff --git a/examples/winit.rs b/examples/winit.rs index 3d3f261..27a3cef 100644 --- a/examples/winit.rs +++ b/examples/winit.rs @@ -6,19 +6,23 @@ 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()) +} - let app = winit_app::WinitAppBuilder::with_init(|elwt| { - let window = winit_app::make_window(elwt, |w| w); +pub(crate) fn entry(event_loop: EventLoop<()>) { + let app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let window = winit_app::make_window(elwt, |w| w); - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let context = softbuffer::Context::new(window.clone()).unwrap(); - (window, surface) - }) - .with_event_handler(|state, event, elwt| { - let (window, surface) = state; + (window, context) + }, + |_elwt, (window, context)| softbuffer::Surface::new(context, window.clone()).unwrap(), + ) + .with_event_handler(|(window, _context), surface, event, elwt| { elwt.set_control_flow(ControlFlow::Wait); match event { @@ -26,6 +30,11 @@ fn main() { window_id, event: WindowEvent::Resized(size), } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("Resized fired before Resumed or after Suspended"); + return; + }; + if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) { @@ -36,6 +45,10 @@ fn main() { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; let size = window.inner_size(); if let (Some(width), Some(height)) = (NonZeroU32::new(size.width), NonZeroU32::new(size.height)) diff --git a/examples/winit_android.rs b/examples/winit_android.rs new file mode 100644 index 0000000..375de37 --- /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 4c2be4d..3f6bce6 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}; @@ -19,16 +19,15 @@ mod ex { fn render_thread( window: Arc, - surface: Arc>, - do_render: mpsc::Receiver<()>, + do_render: mpsc::Receiver>>, done: mpsc::Sender<()>, ) { loop { println!("waiting for render..."); - if do_render.recv().is_err() { - // Main thread is dead. + let Ok(surface) = do_render.recv() else { + println!("main thread destroyed"); break; - } + }; // Perform the rendering. let mut surface = surface.lock().unwrap(); @@ -60,37 +59,40 @@ mod ex { } } - pub(super) fn entry() { - let event_loop = EventLoop::new().unwrap(); - - let app = winit_app::WinitAppBuilder::with_init(|elwt| { - let attributes = Window::default_attributes(); - #[cfg(target_arch = "wasm32")] - let attributes = - winit::platform::web::WindowAttributesExtWebSys::with_append(attributes, true); - let window = Arc::new(elwt.create_window(attributes).unwrap()); - - let context = softbuffer::Context::new(window.clone()).unwrap(); - let surface = { + pub fn entry(event_loop: EventLoop<()>) { + let app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let attributes = Window::default_attributes(); + #[cfg(target_arch = "wasm32")] + let attributes = + winit::platform::web::WindowAttributesExtWebSys::with_append(attributes, true); + let window = Arc::new(elwt.create_window(attributes).unwrap()); + + let context = softbuffer::Context::new(window.clone()).unwrap(); + + // Spawn a thread to handle rendering for this specific surface. The channels will + // be closed and the thread will be stopped whenever this surface (the returned + // context below) is dropped, so that it can all be recreated again (on Android) + // when a new surface is created. + let (start_render, do_render) = mpsc::channel(); + let (render_done, finish_render) = mpsc::channel(); + println!("starting thread..."); + std::thread::spawn({ + let window = window.clone(); + move || render_thread(window, do_render, render_done) + }); + + (window, context, start_render, finish_render) + }, + |_elwt, (window, context, _start_render, _finish_render)| { println!("making surface..."); - let surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); - Arc::new(Mutex::new(surface)) - }; - - // Spawn a thread to handle rendering. - let (start_render, do_render) = mpsc::channel(); - let (render_done, finish_render) = mpsc::channel(); - println!("starting thread..."); - std::thread::spawn({ - let window = window.clone(); - let surface = surface.clone(); - move || render_thread(window, surface, do_render, render_done) - }); - - (window, surface, start_render, finish_render) - }) - .with_event_handler(|state, event, elwt| { - let (window, _surface, start_render, finish_render) = state; + Arc::new(Mutex::new( + softbuffer::Surface::new(context, window.clone()).unwrap(), + )) + }, + ) + .with_event_handler(|state, surface, event, elwt| { + let (window, _context, start_render, finish_render) = state; elwt.set_control_flow(ControlFlow::Wait); match event { @@ -98,8 +100,12 @@ mod ex { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; // Start the render and then finish it. - start_render.send(()).unwrap(); + start_render.send(surface.clone()).unwrap(); finish_render.recv().unwrap(); } Event::WindowEvent { @@ -132,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 0000000..6ce92cb --- /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/examples/winit_wrong_sized_buffer.rs b/examples/winit_wrong_sized_buffer.rs index 95e3e07..0c58a11 100644 --- a/examples/winit_wrong_sized_buffer.rs +++ b/examples/winit_wrong_sized_buffer.rs @@ -12,24 +12,28 @@ const BUFFER_HEIGHT: usize = 128; fn main() { let event_loop = EventLoop::new().unwrap(); - let app = winit_app::WinitAppBuilder::with_init(|elwt| { - let window = winit_app::make_window(elwt, |w| w); + let app = winit_app::WinitAppBuilder::with_init( + |elwt| { + let window = winit_app::make_window(elwt, |w| w); - let context = softbuffer::Context::new(window.clone()).unwrap(); - let mut surface = softbuffer::Surface::new(&context, window.clone()).unwrap(); + let context = softbuffer::Context::new(window.clone()).unwrap(); - // Intentionally set the size of the surface to something different than the size of the window. - surface - .resize( - NonZeroU32::new(BUFFER_WIDTH as u32).unwrap(), - NonZeroU32::new(BUFFER_HEIGHT as u32).unwrap(), - ) - .unwrap(); - - (window, surface) - }) - .with_event_handler(|state, event, elwt| { - let (window, surface) = state; + (window, context) + }, + |_elwt, (window, context)| { + let mut surface = softbuffer::Surface::new(context, window.clone()).unwrap(); + // Intentionally set the size of the surface to something different than the size of the window. + surface + .resize( + NonZeroU32::new(BUFFER_WIDTH as u32).unwrap(), + NonZeroU32::new(BUFFER_HEIGHT as u32).unwrap(), + ) + .unwrap(); + surface + }, + ) + .with_event_handler(|state, surface, event, elwt| { + let (window, _context) = state; elwt.set_control_flow(ControlFlow::Wait); match event { @@ -37,6 +41,11 @@ fn main() { window_id, event: WindowEvent::RedrawRequested, } if window_id == window.id() => { + let Some(surface) = surface else { + eprintln!("RedrawRequested fired before Resumed or after Suspended"); + return; + }; + let mut buffer = surface.buffer_mut().unwrap(); for y in 0..BUFFER_HEIGHT { for x in 0..BUFFER_WIDTH { diff --git a/src/backend_dispatch.rs b/src/backend_dispatch.rs index 208f82c..a86832e 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 0000000..ce1f5a4 --- /dev/null +++ b/src/backends/android.rs @@ -0,0 +1,169 @@ +//! 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}, +}; +#[cfg(doc)] +use raw_window_handle::AndroidNdkWindowHandle; +use raw_window_handle::{HasDisplayHandle, HasWindowHandle, RawWindowHandle}; + +use crate::error::InitError; +use crate::{BufferInterface, Rect, SoftBufferError, SurfaceInterface}; + +/// The handle to a window for software buffering. +pub struct AndroidImpl { + native_window: NativeWindow, + window: W, + _display: PhantomData, +} + +impl SurfaceInterface for AndroidImpl { + type Context = D; + type Buffer<'a> = BufferImpl<'a, D, W> where Self: 'a; + + /// Create a new [`AndroidImpl`] from an [`AndroidNdkWindowHandle`]. + fn new(window: W, _display: &Self::Context) -> Result> { + 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. + // SAFETY: We have confirmed that the window handle is valid. + let native_window = unsafe { NativeWindow::clone_from_ptr(a.a_native_window.cast()) }; + + Ok(Self { + native_window, + _display: PhantomData, + window, + }) + } + + #[inline] + fn window(&self) -> &W { + &self.window + } + + /// Also changes the pixel format to [`HardwareBufferFormat::R8G8B8A8_UNORM`]. + 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 })?; + + self.native_window + .set_buffers_geometry( + width.into(), + height.into(), + // Default is typically R5G6B5 16bpp, switch to 32bpp + Some(HardwareBufferFormat::R8G8B8X8_UNORM), + ) + .map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to set buffer geometry on ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + }) + } + + fn buffer_mut(&mut self) -> Result, SoftBufferError> { + let native_window_buffer = self.native_window.lock(None).map_err(|err| { + SoftBufferError::PlatformError( + Some("Failed to lock ANativeWindow".to_owned()), + Some(Box::new(err)), + ) + })?; + + if !matches!( + native_window_buffer.format(), + // These are the only formats we support + HardwareBufferFormat::R8G8B8A8_UNORM | HardwareBufferFormat::R8G8B8X8_UNORM + ) { + return Err(SoftBufferError::PlatformError( + Some(format!( + "Unexpected buffer format {:?}, please call \ + .resize() first to change it to RGBx8888", + native_window_buffer.format() + )), + None, + )); + } + + let buffer = vec![0; native_window_buffer.width() * native_window_buffer.height()]; + + Ok(BufferImpl { + native_window_buffer, + buffer, + marker: PhantomData, + }) + } + + /// Fetch the buffer from the window. + fn fetch(&mut self) -> Result, SoftBufferError> { + Err(SoftBufferError::Unimplemented) + } +} + +pub struct BufferImpl<'a, D: ?Sized, W> { + native_window_buffer: NativeWindowBufferLockGuard<'a>, + buffer: Vec, + marker: PhantomData<(&'a D, &'a W)>, +} + +// TODO: Move to NativeWindowBufferLockGuard? +unsafe impl<'a, D, W> Send for BufferImpl<'a, D, W> {} + +impl<'a, D: HasDisplayHandle, W: HasWindowHandle> BufferInterface for BufferImpl<'a, D, W> { + #[inline] + fn pixels(&self) -> &[u32] { + &self.buffer + } + + #[inline] + fn pixels_mut(&mut self) -> &mut [u32] { + &mut self.buffer + } + + #[inline] + fn age(&self) -> u8 { + 0 + } + + // TODO: This function is pretty slow this way + fn present(mut self) -> Result<(), SoftBufferError> { + let input_lines = self.buffer.chunks(self.native_window_buffer.width()); + for (output, input) in self + .native_window_buffer + .lines() + // Unreachable as we checked before that this is a valid, mappable format + .unwrap() + .zip(input_lines) + { + // .lines() removed the stride + assert_eq!(output.len(), input.len() * 4); + + for (i, pixel) in input.iter().enumerate() { + // Swizzle colors from RGBX to BGR + let [b, g, r, _] = pixel.to_le_bytes(); + output[i * 4].write(b); + output[i * 4 + 1].write(g); + output[i * 4 + 2].write(r); + // TODO alpha? + } + } + Ok(()) + } + + fn present_with_damage(self, _damage: &[Rect]) -> Result<(), SoftBufferError> { + // TODO: Android requires the damage rect _at lock time_ + // Since we're faking the backing buffer _anyway_, we could even fake the surface lock + // and lock it here (if it doesn't influence timings). + self.present() + } +} diff --git a/src/backends/kms.rs b/src/backends/kms.rs index 901fc73..da0369b 100644 --- a/src/backends/kms.rs +++ b/src/backends/kms.rs @@ -43,16 +43,15 @@ impl ContextInterface for Arc where D: Sized, { - let fd = match display.display_handle()?.as_raw() { - RawDisplayHandle::Drm(drm) => drm.fd, - _ => return Err(InitError::Unsupported(display)), + let RawDisplayHandle::Drm(drm) = display.display_handle()?.as_raw() else { + return Err(InitError::Unsupported(display)); }; - if fd == -1 { + if drm.fd == -1 { return Err(SoftBufferError::IncompleteDisplayHandle.into()); } // SAFETY: Invariants guaranteed by the user. - let fd = unsafe { BorrowedFd::borrow_raw(fd) }; + let fd = unsafe { BorrowedFd::borrow_raw(drm.fd) }; Ok(Arc::new(KmsDisplayImpl { fd, @@ -139,13 +138,12 @@ impl SurfaceInterface fo /// Create a new KMS backend. fn new(window: W, display: &Arc>) -> Result> { // Make sure that the window handle is valid. - let plane_handle = match window.window_handle()?.as_raw() { - RawWindowHandle::Drm(drm) => match NonZeroU32::new(drm.plane) { - Some(handle) => plane::Handle::from(handle), - None => return Err(SoftBufferError::IncompleteWindowHandle.into()), - }, - _ => return Err(InitError::Unsupported(window)), + let RawWindowHandle::Drm(drm) = window.window_handle()?.as_raw() else { + return Err(InitError::Unsupported(window)); }; + let plane_handle = + NonZeroU32::new(drm.plane).ok_or(SoftBufferError::IncompleteWindowHandle)?; + let plane_handle = plane::Handle::from(plane_handle); let plane_info = display .get_plane(plane_handle) diff --git a/src/backends/mod.rs b/src/backends/mod.rs index f700b05..703401d 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/orbital.rs b/src/backends/orbital.rs index 96c3db1..52f3636 100644 --- a/src/backends/orbital.rs +++ b/src/backends/orbital.rs @@ -132,9 +132,8 @@ impl SurfaceInterface for Orbital fn new(window: W, _display: &D) -> Result> { let raw = window.window_handle()?.as_raw(); - let handle = match raw { - RawWindowHandle::Orbital(handle) => handle, - _ => return Err(InitError::Unsupported(window)), + let RawWindowHandle::Orbital(handle) = raw else { + return Err(InitError::Unsupported(window)); }; Ok(Self { diff --git a/src/backends/wayland/mod.rs b/src/backends/wayland/mod.rs index 2dda8d2..fe4cf36 100644 --- a/src/backends/wayland/mod.rs +++ b/src/backends/wayland/mod.rs @@ -45,12 +45,11 @@ impl ContextInterface for Arc w.display, - _ => return Err(InitError::Unsupported(display)), + let RawDisplayHandle::Wayland(w) = raw else { + return Err(InitError::Unsupported(display)); }; - let backend = unsafe { Backend::from_foreign_display(wayland_handle.as_ptr().cast()) }; + let backend = unsafe { Backend::from_foreign_display(w.display.as_ptr().cast()) }; let conn = Connection::from_backend(backend); let (globals, event_queue) = registry_queue_init(&conn).swbuf_err("Failed to make round trip to server")?; @@ -156,15 +155,14 @@ impl SurfaceInterface fn new(window: W, display: &Arc>) -> Result> { // Get the raw Wayland window. let raw = window.window_handle()?.as_raw(); - let wayland_handle = match raw { - RawWindowHandle::Wayland(w) => w.surface, - _ => return Err(InitError::Unsupported(window)), + let RawWindowHandle::Wayland(w) = raw else { + return Err(InitError::Unsupported(window)); }; let surface_id = unsafe { ObjectId::from_ptr( wl_surface::WlSurface::interface(), - wayland_handle.as_ptr().cast(), + w.surface.as_ptr().cast(), ) } .swbuf_err("Failed to create proxy for surface ID.")?; diff --git a/src/backends/web.rs b/src/backends/web.rs index 539e2d2..5c0e00f 100644 --- a/src/backends/web.rs +++ b/src/backends/web.rs @@ -26,10 +26,9 @@ pub struct WebDisplayImpl { impl ContextInterface for WebDisplayImpl { fn new(display: D) -> Result> { let raw = display.display_handle()?.as_raw(); - match raw { - RawDisplayHandle::Web(..) => {} - _ => return Err(InitError::Unsupported(display)), - } + let RawDisplayHandle::Web(..) = raw else { + return Err(InitError::Unsupported(display)); + }; let document = web_sys::window() .swbuf_err("`Window` is not present in this runtime")? diff --git a/src/backends/win32.rs b/src/backends/win32.rs index 084ad47..f044033 100644 --- a/src/backends/win32.rs +++ b/src/backends/win32.rs @@ -213,9 +213,8 @@ impl SurfaceInterface for Win32Im /// Create a new `Win32Impl` from a `Win32WindowHandle`. fn new(window: W, _display: &D) -> Result> { let raw = window.window_handle()?.as_raw(); - let handle = match raw { - RawWindowHandle::Win32(handle) => handle, - _ => return Err(crate::InitError::Unsupported(window)), + let RawWindowHandle::Win32(handle) = raw else { + return Err(crate::InitError::Unsupported(window)); }; // Get the handle to the device context. @@ -248,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 })?; diff --git a/src/backends/x11.rs b/src/backends/x11.rs index b01fb5f..bde94a7 100644 --- a/src/backends/x11.rs +++ b/src/backends/x11.rs @@ -193,10 +193,8 @@ impl SurfaceInterface fo let window_handle = match raw { RawWindowHandle::Xcb(xcb) => xcb, RawWindowHandle::Xlib(xlib) => { - let window = match NonZeroU32::new(xlib.window as u32) { - Some(window) => window, - None => return Err(SoftBufferError::IncompleteWindowHandle.into()), - }; + let window = NonZeroU32::new(xlib.window as u32) + .ok_or(SoftBufferError::IncompleteWindowHandle)?; let mut xcb_window_handle = XcbWindowHandle::new(window); xcb_window_handle.visual_id = NonZeroU32::new(xlib.visual_id as u32); xcb_window_handle @@ -702,26 +700,21 @@ impl ShmSegment { id.set_len(size as u64)?; // Map the shared memory to our file descriptor space. - let ptr = unsafe { - let ptr = mm::mmap( + let ptr = NonNull::new(unsafe { + mm::mmap( null_mut(), size, mm::ProtFlags::READ | mm::ProtFlags::WRITE, mm::MapFlags::SHARED, &id, 0, - )?; - - match NonNull::new(ptr.cast()) { - Some(ptr) => ptr, - None => { - return Err(io::Error::new( - io::ErrorKind::Other, - "unexpected null when mapping SHM segment", - )); - } - } - }; + )? + }) + .ok_or(io::Error::new( + io::ErrorKind::Other, + "unexpected null when mapping SHM segment", + ))? + .cast(); Ok(Self { id, diff --git a/src/lib.rs b/src/lib.rs index d19eafa..2a3fcd0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -196,13 +196,16 @@ impl HasWindowHandle for Surface /// - Web /// - AppKit /// - UIKit +/// +/// Buffer copies an channel swizzling happen on: +/// - Android pub struct Buffer<'a, D, W> { buffer_impl: BufferDispatch<'a, D, W>, _marker: PhantomData<(Arc, Cell<()>)>, } impl<'a, D: HasDisplayHandle, W: HasWindowHandle> Buffer<'a, D, W> { - /// Is age is the number of frames ago this buffer was last presented. So if the value is + /// `age` is the number of frames ago this buffer was last presented. So if the value is /// `1`, it is the same as the last frame, and if it is `2`, it is the same as the frame /// before that (for backends using double buffering). If the value is `0`, it is a new /// buffer that has unspecified contents.