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
This commit is contained in:
Marijn Suijten 2020-11-11 16:32:02 +01:00 committed by GitHub
parent 86da42f2d7
commit e8aef14347
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 243 additions and 158 deletions

View File

@ -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

View File

@ -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.

2
Cargo.lock generated
View File

@ -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",

View File

@ -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

View File

@ -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"

View File

@ -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
},
));
}
}
}

View File

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