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() }