Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions templates/apps/rust-gpu-wgpu/.gitignore.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Rust
target/
**/*.rs.bk

# cargo-mobile2
.cargo/
/gen

# macOS
.DS_Store
42 changes: 42 additions & 0 deletions templates/apps/rust-gpu-wgpu/Cargo.toml.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
[package]
name = "{{app.name}}"
version = "0.1.0"
authors = ["{{author}}"]
edition = "2024"

[lib]
crate-type = ["staticlib", "cdylib", "rlib"]

[[bin]]
name = "{{app.name}}-desktop"
path = "gen/bin/desktop.rs"

[dependencies]
# shader crate
shaders = { path = "shaders" }

# API
wgpu = { version = "29.0.1", features = ["spirv"] }
pollster = "0.4.0"
log = "0.4.29"

# other
bytemuck = { version = "1.24.0", features = ["derive"] }
raw-window-handle = "0.6.2"
anyhow = "1.0.98"

# cargo-mobile2
[target.'cfg(target_os = "android")'.dependencies]
winit = { version = "0.30.13", features = ["android-native-activity"] }
android_logger = "0.15.1"

[target.'cfg(not(target_os = "android"))'.dependencies]
winit = "0.30.13"
env_logger = "0.11.8"

[build-dependencies]
# rust-gpu
spirv-builder = { version = "0.10.0-alpha.1" }

# other
anyhow = "1.0.98"
20 changes: 20 additions & 0 deletions templates/apps/rust-gpu-wgpu/build.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
use spirv_builder::{ShaderPanicStrategy, SpirvBuilder, SpirvMetadata};
use std::path::PathBuf;

pub fn main() -> anyhow::Result<()> {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let crate_path = [manifest_dir, "shaders"]
.iter()
.copied()
.collect::<PathBuf>();

let mut builder = SpirvBuilder::new(crate_path, "spirv-unknown-vulkan1.3");
builder.build_script.defaults = true;
builder.shader_panic_strategy = ShaderPanicStrategy::SilentExit;
builder.spirv_metadata = SpirvMetadata::Full;

let compile_result = builder.build()?;
let spv_path = compile_result.module.unwrap_single();
println!("cargo::rustc-env=SHADER_SPV_PATH={}", spv_path.display());
Ok(())
}
4 changes: 4 additions & 0 deletions templates/apps/rust-gpu-wgpu/gen/bin/desktop.rs.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
fn main() {
#[cfg(not(any(target_os = "android", target_os = "ios")))]
{{snake-case app.name}}::start_app();
}
3 changes: 3 additions & 0 deletions templates/apps/rust-gpu-wgpu/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly-2026-04-11"
components = ["rust-src", "rustc-dev", "llvm-tools"]
12 changes: 12 additions & 0 deletions templates/apps/rust-gpu-wgpu/shaders/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
[package]
name = "shaders"
version = "0.1.0"
edition = "2024"

[lints.rust]
unexpected_cfgs = { level = "allow", check-cfg = ['cfg(target_arch, values("spirv"))'] }

[dependencies]
spirv-std = { version = "0.10.0-alpha.1" }
glam = { version = "0.32.0", default-features = false }
bytemuck = { version = "1.24.0", features = ["derive"] }
3 changes: 3 additions & 0 deletions templates/apps/rust-gpu-wgpu/shaders/rust-toolchain.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
[toolchain]
channel = "nightly-2026-04-11"
components = ["rust-src", "rustc-dev", "llvm-tools"]
36 changes: 36 additions & 0 deletions templates/apps/rust-gpu-wgpu/shaders/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#![no_std]

use bytemuck::{Pod, Zeroable};
use core::f32::consts::PI;
use glam::{vec2, vec3, Vec3, Vec4};
#[cfg(target_arch = "spirv")]
use spirv_std::num_traits::Float;
use spirv_std::spirv;

#[derive(Copy, Clone, Pod, Zeroable)]
#[repr(C)]
pub struct ShaderConstants {
pub width: u32,
pub height: u32,
pub time: f32,
}

#[spirv(fragment)]
pub fn main_fs(vtx_color: Vec3, output: &mut Vec4) {
*output = Vec4::from((vtx_color, 1.));
}

#[spirv(vertex)]
pub fn main_vs(
#[spirv(vertex_index)] vert_id: i32,
#[spirv(descriptor_set = 0, binding = 0, storage_buffer)] constants: &ShaderConstants,
#[spirv(position)] vtx_pos: &mut Vec4,
vtx_color: &mut Vec3,
) {
let speed = 0.4;
let time = constants.time * speed + vert_id as f32 * (2. * PI * 120. / 360.);
let position = vec2(f32::sin(time), f32::cos(time));
*vtx_pos = Vec4::from((position, 0.0, 1.0));

*vtx_color = [vec3(1., 0., 0.), vec3(0., 1., 0.), vec3(0., 0., 1.)][vert_id as usize % 3];
}
30 changes: 30 additions & 0 deletions templates/apps/rust-gpu-wgpu/src/lib.rs.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
pub mod util;
pub mod wgpu_renderer;

use wgpu_renderer::App;
use winit::event_loop::EventLoop;

#[cfg(not(target_os = "android"))]
#[unsafe(no_mangle)]
#[inline(never)]
pub extern "C" fn start_app() {
env_logger::init();

let event_loop = EventLoop::new().unwrap();
event_loop.run_app(&mut App::default()).unwrap();
}

#[cfg(target_os = "android")]
#[unsafe(no_mangle)]
fn android_main(app: winit::platform::android::activity::AndroidApp) {
android_logger::init_once(
android_logger::Config::default()
.with_max_level(log::LevelFilter::Trace)
.with_tag("{{app.name}}"),
);

use winit::platform::android::EventLoopBuilderExtAndroid;

let event_loop = EventLoop::builder().with_android_app(app).build().unwrap();
event_loop.run_app(&mut App::default()).unwrap();
}
3 changes: 3 additions & 0 deletions templates/apps/rust-gpu-wgpu/src/util.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
pub fn enable_debug_layer() -> bool {
std::env::var("DEBUG_LAYER").is_ok_and(|e| !(e == "0" || e == "false"))
}
160 changes: 160 additions & 0 deletions templates/apps/rust-gpu-wgpu/src/wgpu_renderer/mod.rs.hbs
Original file line number Diff line number Diff line change
@@ -0,0 +1,160 @@
use crate::wgpu_renderer::renderer::MyRenderer;
use crate::wgpu_renderer::swapchain::MySwapchainManager;
use anyhow::Context;
use pollster::block_on;
use shaders::ShaderConstants;
use std::sync::Arc;
use std::time::Instant;
use winit::{
application::ApplicationHandler,
event::{ElementState, KeyEvent, WindowEvent},
event_loop::ActiveEventLoop,
keyboard::{Key, NamedKey},
window::{Window, WindowId},
};

mod render_pipeline;
mod renderer;
mod swapchain;

#[derive(Default)]
pub struct App(Option<State>);

impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
if self.0.is_none() {
self.0 = Some(block_on(State::new(event_loop)).unwrap());
}
}

fn window_event(&mut self, event_loop: &ActiveEventLoop, id: WindowId, event: WindowEvent) {
let state = self.0.as_mut().unwrap();
state.window_event(event_loop, id, event).unwrap();
}

// FIX(iOS): https://github.com/rust-windowing/winit/issues/3406
fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) {
event_loop.set_control_flow(winit::event_loop::ControlFlow::Wait);

#[cfg(target_os = "ios")]
{
if let Some(state) = self.0.as_mut() {
if state.ios_request_redraw {
state.window.request_redraw();
}

state.ios_request_redraw = false;
}
}
}
}

struct State {
start: Instant,
window: Arc<Window>,
renderer: MyRenderer,
swapchain: MySwapchainManager<'static>,
// FIX(iOS): https://github.com/rust-windowing/winit/issues/3406
#[cfg(target_os = "ios")]
ios_request_redraw: bool,
}

impl State {
async fn new(event_loop: &ActiveEventLoop) -> anyhow::Result<Self> {
let attributes = Window::default_attributes().with_title("{{app.stylized-name}}");

#[cfg(not(any(target_os = "android", target_os = "ios")))]
let attributes = attributes.with_inner_size(winit::dpi::LogicalSize::new(1280, 720));
let window = Arc::new(event_loop.create_window(attributes).unwrap());

let instance =
wgpu::Instance::new(wgpu::InstanceDescriptor::new_with_display_handle_from_env(
Box::new(event_loop.owned_display_handle()),
));
let surface = instance.create_surface(window.clone())?;
let adapter =
wgpu::util::initialize_adapter_from_env_or_default(&instance, Some(&surface)).await?;

let required_features = wgpu::Features::IMMEDIATES;
let required_limits = wgpu::Limits {
max_immediate_size: 128,
max_inter_stage_shader_variables: 15,
..Default::default()
};
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: None,
required_features,
required_limits,
experimental_features: wgpu::ExperimentalFeatures::disabled(),
memory_hints: wgpu::MemoryHints::Performance,
trace: Default::default(),
})
.await
.context("Failed to create device")?;

let swapchain = MySwapchainManager::new(
instance.clone(),
adapter.clone(),
device.clone(),
window.clone(),
surface,
);
let renderer = MyRenderer::new(device, queue, swapchain.format())?;
Ok(Self {
start: Instant::now(),
window,
swapchain,
renderer,
// FIX(iOS): https://github.com/rust-windowing/winit/issues/3406
#[cfg(target_os = "ios")]
ios_request_redraw: false,
})
}

fn window_event(
&mut self,
event_loop: &ActiveEventLoop,
_id: WindowId,
event: WindowEvent,
) -> anyhow::Result<()> {
match event {
WindowEvent::RedrawRequested => {
let time = self.start.elapsed().as_secs_f32();
let renderer = &self.renderer;
self.swapchain.render(|render_target| {
renderer.render(
&ShaderConstants {
time,
width: render_target.texture().width(),
height: render_target.texture().height(),
},
render_target,
)
})?;

// FIX(iOS): https://github.com/rust-windowing/winit/issues/3406
#[cfg(not(target_os = "ios"))]
self.window.request_redraw();

#[cfg(target_os = "ios")]
{
self.ios_request_redraw = true;
}
}
WindowEvent::KeyboardInput {
event:
KeyEvent {
logical_key: Key::Named(NamedKey::Escape),
state: ElementState::Pressed,
..
},
..
}
| WindowEvent::CloseRequested => event_loop.exit(),
WindowEvent::Resized(_) => self.swapchain.should_recreate(),
_ => (),
}
Ok(())
}
}
Loading