mirror of
https://github.com/EmbarkStudios/rust-gpu.git
synced 2024-11-21 22:34:34 +00:00
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:
parent
86da42f2d7
commit
e8aef14347
26
.github/workflows/ci.yaml
vendored
26
.github/workflows/ci.yaml
vendored
@ -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
|
||||
|
@ -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
2
Cargo.lock
generated
@ -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",
|
||||
|
@ -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
|
||||
|
||||
|
@ -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"
|
||||
|
||||
|
205
examples/runners/wgpu/src/lib.rs
Normal file
205
examples/runners/wgpu/src/lib.rs
Normal 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
|
||||
},
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
@ -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()
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user