From e8aef14347cc9ae1ec4d0e3d36a202094888d184 Mon Sep 17 00:00:00 2001 From: Marijn Suijten Date: Wed, 11 Nov 2020 16:32:02 +0100 Subject: [PATCH] Add Android support to wgpu example (#215) * examples/wgpu: Handle escape to exit (same as ash example) * examples/wgpu: Set up for use on Android * examples/wgpu: Convert #[cfg] blocks to cfg_if * examples/wgpu: Wait for events instead of busy-looping The image currently does not change and the OS will notify us when to redraw (ie. after window resizes). This is going to save power especially on mobile devices. As soon as interactive or animating visuals are introduced to this example redraws can be requested with `window.request_redraw()`. * examples/wgpu: Create swapchain in ::Resume on Android * docs: Add Android to supported operating systems * ci: Build test cross-compilation for Android * HACK: ci: Create Android symlink without spaces * ci: Set legacy ANDROID_HOME because ndk-build prefers deprecated var --- .github/workflows/ci.yaml | 26 +++- CONTRIBUTING.md | 2 +- Cargo.lock | 2 + docs/src/platform-support.md | 1 + examples/runners/wgpu/Cargo.toml | 9 +- examples/runners/wgpu/src/lib.rs | 205 ++++++++++++++++++++++++++++++ examples/runners/wgpu/src/main.rs | 156 +---------------------- 7 files changed, 243 insertions(+), 158 deletions(-) create mode 100644 examples/runners/wgpu/src/lib.rs diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index 6471111672..fbbdf69d8e 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -11,7 +11,6 @@ jobs: name: Test strategy: matrix: - os: [macOS-latest, ubuntu-latest, windows-latest] include: - os: ubuntu-latest target: x86_64-unknown-linux-gnu @@ -64,6 +63,31 @@ jobs: shell: bash run: .github/workflows/test.sh ${{ runner.os }} + - name: Install cargo-apk + run: cargo install cargo-apk + + - if: runner.os == 'Windows' + name: Create symlink to Android SDK/NDK without spaces + run: | + $oldAndroidPath = $env:ANDROID_HOME + $sdk_root = "C:\Android" + New-Item -Path $sdk_root -ItemType SymbolicLink -Value $oldAndroidPath + + echo "ANDROID_SDK_ROOT=$sdk_root" >> $env:GITHUB_ENV + echo "ANDROID_NDK_ROOT=$sdk_root\ndk-bundle" >> $env:GITHUB_ENV + + # Update legacy path for ndk-build: + echo "ANDROID_HOME=$sdk_root" >> $env:GITHUB_ENV + + # Unset legacy paths: + echo "ANDROID_NDK_HOME=" >> $env:GITHUB_ENV + echo "ANDROID_NDK_PATH=" >> $env:GITHUB_ENV + + - name: Compile WGPU example for Android + run: | + rustup target add aarch64-linux-android + cargo apk build --manifest-path examples/runners/wgpu/Cargo.toml --features use-installed-tools --no-default-features + lint: name: Lint runs-on: ubuntu-latest diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index db4d166121..049515ed58 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -83,7 +83,7 @@ Examples of what would require an RFC: #### Life-cycle 1. You file a [major change proposal][mcp-template] outlining the changes and the motivation for it. -2. A member of the team will review the proposal and tag it with the appropiate label. +2. A member of the team will review the proposal and tag it with the appropriate label. 2.1. `mcp: accepted` means that the MCP has been accepted and is ready for a pull request implementing it. 2.2. `mcp: rfc needed` means that the MCP has been accepted as something the team would like but needs a full RFC before the implementation. 2.3 Closing an issue means that the MCP has rejected. diff --git a/Cargo.lock b/Cargo.lock index e071a2f4bd..2d22d09d06 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -602,9 +602,11 @@ dependencies = [ name = "example-runner-wgpu" version = "0.1.0" dependencies = [ + "cfg-if 1.0.0", "console_error_panic_hook", "console_log", "futures", + "ndk-glue", "spirv-builder", "wasm-bindgen-futures", "web-sys", diff --git a/docs/src/platform-support.md b/docs/src/platform-support.md index 45f5661ab9..689f2c6bb2 100644 --- a/docs/src/platform-support.md +++ b/docs/src/platform-support.md @@ -12,6 +12,7 @@ The `rust-gpu` project currently supports a limited number of platforms and grap | Windows | 10+ | Primary | | | Linux | Ubuntu 18.04+ | Primary | | | macOS | Catalina (10.15)+ | Secondary | Using [MoltenVK] +| Android | Tested 10-11 | Secondary | | [MoltenVK]: https://github.com/KhronosGroup/MoltenVK diff --git a/examples/runners/wgpu/Cargo.toml b/examples/runners/wgpu/Cargo.toml index 8e2cfd5784..62ae3580bc 100644 --- a/examples/runners/wgpu/Cargo.toml +++ b/examples/runners/wgpu/Cargo.toml @@ -6,6 +6,9 @@ edition = "2018" license = "MIT OR Apache-2.0" publish = false +[lib] +crate-type = ["lib", "cdylib"] + # See rustc_codegen_spirv/Cargo.toml for details on these features [features] default = ["use-compiled-tools"] @@ -13,13 +16,17 @@ use-installed-tools = ["spirv-builder/use-installed-tools"] use-compiled-tools = ["spirv-builder/use-compiled-tools"] [dependencies] -wgpu = "0.6.0" +cfg-if = "1.0.0" futures = { version = "0.3", default-features = false, features = ["std", "executor"] } +wgpu = "0.6.0" winit = { version = "0.23", features = ["web-sys"] } [build-dependencies] spirv-builder = { path = "../../../crates/spirv-builder", default-features = false } +[target.'cfg(target_os = "android")'.dependencies] +ndk-glue = "0.2" + [target.'cfg(not(target_arch = "wasm32"))'.dependencies] wgpu-subscriber = "0.1.0" diff --git a/examples/runners/wgpu/src/lib.rs b/examples/runners/wgpu/src/lib.rs new file mode 100644 index 0000000000..1f0ae00271 --- /dev/null +++ b/examples/runners/wgpu/src/lib.rs @@ -0,0 +1,205 @@ +use winit::{ + event::{Event, KeyboardInput, VirtualKeyCode, WindowEvent}, + event_loop::{ControlFlow, EventLoop}, + window::Window, +}; + +async fn run(event_loop: EventLoop<()>, window: Window, swapchain_format: wgpu::TextureFormat) { + let size = window.inner_size(); + let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); + + // Wait for Resumed event on Android; the surface is only needed early to + // find an adapter that can render to this surface. + let mut surface = if cfg!(target_os = "android") { + None + } else { + Some(unsafe { instance.create_surface(&window) }) + }; + + let adapter = instance + .request_adapter(&wgpu::RequestAdapterOptions { + power_preference: wgpu::PowerPreference::default(), + // Request an adapter which can render to our surface + compatible_surface: surface.as_ref(), + }) + .await + .expect("Failed to find an appropriate adapter"); + + // Create the logical device and command queue + let (device, queue) = adapter + .request_device( + &wgpu::DeviceDescriptor { + features: wgpu::Features::empty(), + limits: wgpu::Limits::default(), + shader_validation: true, + }, + None, + ) + .await + .expect("Failed to create device"); + + // Load the shaders from disk + let module = device.create_shader_module(wgpu::include_spirv!(env!("sky_shader.spv"))); + + let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { + label: None, + bind_group_layouts: &[], + push_constant_ranges: &[], + }); + + let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { + label: None, + layout: Some(&pipeline_layout), + vertex_stage: wgpu::ProgrammableStageDescriptor { + module: &module, + entry_point: "main_vs", + }, + fragment_stage: Some(wgpu::ProgrammableStageDescriptor { + module: &module, + entry_point: "main_fs", + }), + // Use the default rasterizer state: no culling, no depth bias + rasterization_state: None, + primitive_topology: wgpu::PrimitiveTopology::TriangleList, + color_states: &[swapchain_format.into()], + depth_stencil_state: None, + vertex_state: wgpu::VertexStateDescriptor { + index_format: wgpu::IndexFormat::Uint16, + vertex_buffers: &[], + }, + sample_count: 1, + sample_mask: !0, + alpha_to_coverage_enabled: false, + }); + + let mut sc_desc = wgpu::SwapChainDescriptor { + usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, + format: swapchain_format, + width: size.width, + height: size.height, + present_mode: wgpu::PresentMode::Mailbox, + }; + + let mut swap_chain = surface + .as_ref() + .map(|surface| device.create_swap_chain(&surface, &sc_desc)); + + event_loop.run(move |event, _, control_flow| { + // Have the closure take ownership of the resources. + // `event_loop.run` never returns, therefore we must do this to ensure + // the resources are properly cleaned up. + let _ = (&instance, &adapter, &module, &pipeline_layout); + + *control_flow = ControlFlow::Wait; + match event { + Event::Resumed => { + let s = unsafe { instance.create_surface(&window) }; + swap_chain = Some(device.create_swap_chain(&s, &sc_desc)); + surface = Some(s); + } + Event::Suspended => { + surface = None; + swap_chain = None; + } + Event::WindowEvent { + event: WindowEvent::Resized(size), + .. + } => { + // Recreate the swap chain with the new size + sc_desc.width = size.width; + sc_desc.height = size.height; + if let Some(surface) = &surface { + swap_chain = Some(device.create_swap_chain(surface, &sc_desc)); + } + } + Event::RedrawRequested(_) => { + if let Some(swap_chain) = &mut swap_chain { + let frame = swap_chain + .get_current_frame() + .expect("Failed to acquire next swap chain texture") + .output; + let mut encoder = device + .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); + { + let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { + color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { + attachment: &frame.view, + resolve_target: None, + ops: wgpu::Operations { + load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), + store: true, + }, + }], + depth_stencil_attachment: None, + }); + rpass.set_pipeline(&render_pipeline); + rpass.draw(0..3, 0..1); + } + + queue.submit(Some(encoder.finish())); + } + } + Event::WindowEvent { + event: WindowEvent::CloseRequested, + .. + } => *control_flow = ControlFlow::Exit, + Event::WindowEvent { + event: + WindowEvent::KeyboardInput { + input: + KeyboardInput { + virtual_keycode: Some(VirtualKeyCode::Escape), + .. + }, + .. + }, + .. + } => *control_flow = ControlFlow::Exit, + _ => {} + } + }); +} + +#[cfg_attr(target_os = "android", ndk_glue::main(backtrace = "on"))] +pub fn main() { + let event_loop = EventLoop::new(); + let window = winit::window::WindowBuilder::new() + .with_title("Rust GPU - wgpu") + .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0)) + .build(&event_loop) + .unwrap(); + + cfg_if::cfg_if! { + if #[cfg(target_arch = "wasm32")] { + std::panic::set_hook(Box::new(console_error_panic_hook::hook)); + console_log::init().expect("could not initialize logger"); + use winit::platform::web::WindowExtWebSys; + // On wasm, append the canvas to the document body + web_sys::window() + .and_then(|win| win.document()) + .and_then(|doc| doc.body()) + .and_then(|body| { + body.append_child(&web_sys::Element::from(window.canvas())) + .ok() + }) + .expect("couldn't append canvas to document body"); + // Temporarily avoid srgb formats for the swapchain on the web + wasm_bindgen_futures::spawn_local(run( + event_loop, + window, + wgpu::TextureFormat::Bgra8Unorm, + )); + } else { + wgpu_subscriber::initialize_default_subscriber(None); + futures::executor::block_on(run( + event_loop, + window, + if cfg!(target_os = "android") { + wgpu::TextureFormat::Rgba8UnormSrgb + } else { + wgpu::TextureFormat::Bgra8UnormSrgb + }, + )); + } + } +} diff --git a/examples/runners/wgpu/src/main.rs b/examples/runners/wgpu/src/main.rs index 0ec58b3906..c91dd9dc84 100644 --- a/examples/runners/wgpu/src/main.rs +++ b/examples/runners/wgpu/src/main.rs @@ -1,157 +1,3 @@ -use winit::{ - event::{Event, WindowEvent}, - event_loop::{ControlFlow, EventLoop}, - window::Window, -}; - -async fn run(event_loop: EventLoop<()>, window: Window, swapchain_format: wgpu::TextureFormat) { - let size = window.inner_size(); - let instance = wgpu::Instance::new(wgpu::BackendBit::PRIMARY); - let surface = unsafe { instance.create_surface(&window) }; - let adapter = instance - .request_adapter(&wgpu::RequestAdapterOptions { - power_preference: wgpu::PowerPreference::default(), - // Request an adapter which can render to our surface - compatible_surface: Some(&surface), - }) - .await - .expect("Failed to find an appropiate adapter"); - - // Create the logical device and command queue - let (device, queue) = adapter - .request_device( - &wgpu::DeviceDescriptor { - features: wgpu::Features::empty(), - limits: wgpu::Limits::default(), - shader_validation: true, - }, - None, - ) - .await - .expect("Failed to create device"); - - // Load the shaders from disk - let module = device.create_shader_module(wgpu::include_spirv!(env!("sky_shader.spv"))); - - let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { - label: None, - bind_group_layouts: &[], - push_constant_ranges: &[], - }); - - let render_pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { - label: None, - layout: Some(&pipeline_layout), - vertex_stage: wgpu::ProgrammableStageDescriptor { - module: &module, - entry_point: "main_vs", - }, - fragment_stage: Some(wgpu::ProgrammableStageDescriptor { - module: &module, - entry_point: "main_fs", - }), - // Use the default rasterizer state: no culling, no depth bias - rasterization_state: None, - primitive_topology: wgpu::PrimitiveTopology::TriangleList, - color_states: &[swapchain_format.into()], - depth_stencil_state: None, - vertex_state: wgpu::VertexStateDescriptor { - index_format: wgpu::IndexFormat::Uint16, - vertex_buffers: &[], - }, - sample_count: 1, - sample_mask: !0, - alpha_to_coverage_enabled: false, - }); - - let mut sc_desc = wgpu::SwapChainDescriptor { - usage: wgpu::TextureUsage::OUTPUT_ATTACHMENT, - format: swapchain_format, - width: size.width, - height: size.height, - present_mode: wgpu::PresentMode::Mailbox, - }; - - let mut swap_chain = device.create_swap_chain(&surface, &sc_desc); - - event_loop.run(move |event, _, control_flow| { - // Have the closure take ownership of the resources. - // `event_loop.run` never returns, therefore we must do this to ensure - // the resources are properly cleaned up. - let _ = (&instance, &adapter, &module, &pipeline_layout); - - *control_flow = ControlFlow::Poll; - match event { - Event::WindowEvent { - event: WindowEvent::Resized(size), - .. - } => { - // Recreate the swap chain with the new size - sc_desc.width = size.width; - sc_desc.height = size.height; - swap_chain = device.create_swap_chain(&surface, &sc_desc); - } - Event::RedrawRequested(_) => { - let frame = swap_chain - .get_current_frame() - .expect("Failed to acquire next swap chain texture") - .output; - let mut encoder = - device.create_command_encoder(&wgpu::CommandEncoderDescriptor { label: None }); - { - let mut rpass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { - color_attachments: &[wgpu::RenderPassColorAttachmentDescriptor { - attachment: &frame.view, - resolve_target: None, - ops: wgpu::Operations { - load: wgpu::LoadOp::Clear(wgpu::Color::GREEN), - store: true, - }, - }], - depth_stencil_attachment: None, - }); - rpass.set_pipeline(&render_pipeline); - rpass.draw(0..3, 0..1); - } - - queue.submit(Some(encoder.finish())); - } - Event::WindowEvent { - event: WindowEvent::CloseRequested, - .. - } => *control_flow = ControlFlow::Exit, - _ => {} - } - }); -} - fn main() { - let event_loop = EventLoop::new(); - let window = winit::window::WindowBuilder::new() - .with_title("Rust GPU - wgpu") - .with_inner_size(winit::dpi::LogicalSize::new(1280.0, 720.0)) - .build(&event_loop) - .unwrap(); - #[cfg(not(target_arch = "wasm32"))] - { - wgpu_subscriber::initialize_default_subscriber(None); - // Temporarily avoid srgb formats for the swapchain on the web - futures::executor::block_on(run(event_loop, window, wgpu::TextureFormat::Bgra8UnormSrgb)); - } - #[cfg(target_arch = "wasm32")] - { - std::panic::set_hook(Box::new(console_error_panic_hook::hook)); - console_log::init().expect("could not initialize logger"); - use winit::platform::web::WindowExtWebSys; - // On wasm, append the canvas to the document body - web_sys::window() - .and_then(|win| win.document()) - .and_then(|doc| doc.body()) - .and_then(|body| { - body.append_child(&web_sys::Element::from(window.canvas())) - .ok() - }) - .expect("couldn't append canvas to document body"); - wasm_bindgen_futures::spawn_local(run(event_loop, window, wgpu::TextureFormat::Bgra8Unorm)); - } + example_runner_wgpu::main() }