Skip to content

Instantly share code, notes, and snippets.

@matthewjberger
Created August 19, 2024 10:49
Show Gist options
  • Save matthewjberger/602f7eb2453f7ed21de04dd25cd2e1c6 to your computer and use it in GitHub Desktop.
Save matthewjberger/602f7eb2453f7ed21de04dd25cd2e1c6 to your computer and use it in GitHub Desktop.
Standalone Winit Wgpu/Triangle

Quickstart:

# native
cargo run -r

# web
trunk serve --open features webgpu
[package]
name = "app"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib", "rlib"]
path = "src/lib.rs"
name = "app_core"
[dependencies]
bytemuck = { version = "1.17.0", features = ["derive"] }
egui = { git = "https://github.com/emilk/egui", rev = "9a1e358a144b5d2af9d03a80257c34883f57cf0b" }
egui-wgpu = { git = "https://github.com/emilk/egui", features = [
"winit",
], rev = "9a1e358a144b5d2af9d03a80257c34883f57cf0b" }
env_logger = "0.11.5"
log = "0.4.22"
winit = "0.30.5"
nalgebra-glm = { version = "0.19.0", features = [
"convert-bytemuck",
"serde-serialize",
] }
wgpu = { version = "22.1.0", default-features = false }
futures = "0.3.30"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
egui-winit = { git = "https://github.com/emilk/egui", rev = "9a1e358a144b5d2af9d03a80257c34883f57cf0b" }
pollster = "0.3.0"
[target.'cfg(target_arch = "wasm32")'.dependencies]
console_error_panic_hook = "0.1.7"
console_log = "1.0.0"
egui-winit = { git = "https://github.com/emilk/egui", default-features = false, rev = "9a1e358a144b5d2af9d03a80257c34883f57cf0b" }
wasm-bindgen = "0.2.93"
wasm-bindgen-futures = "0.4.43"
web-sys = { version = "0.3.70", features = [
"Window",
"Document",
"HtmlElement",
"Node",
"Text",
] }
web-time = "1.1.0"
[features]
default = ["wgpu/default"]
webgl = ["wgpu/webgl"]
webgpu = ["wgpu/webgpu"]
use std::sync::Arc;
use winit::{
application::ApplicationHandler,
dpi::PhysicalSize,
event::WindowEvent,
window::{Theme, Window},
};
#[cfg(target_arch = "wasm32")]
use futures::channel::oneshot::Receiver;
#[cfg(not(target_arch = "wasm32"))]
pub use std::time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
pub use web_time::{Duration, Instant};
#[cfg(target_arch = "wasm32")]
use wasm_bindgen::prelude::*;
#[derive(Default)]
pub struct App {
window: Option<Arc<Window>>,
renderer: Option<Renderer<'static>>,
gui_state: Option<egui_winit::State>,
last_render_time: Option<Instant>,
#[cfg(target_arch = "wasm32")]
renderer_receiver: Option<Receiver<Renderer<'static>>>,
last_size: (u32, u32),
}
impl ApplicationHandler for App {
fn resumed(&mut self, event_loop: &winit::event_loop::ActiveEventLoop) {
let mut attributes = Window::default_attributes();
#[cfg(not(target_arch = "wasm32"))]
{
attributes = attributes.with_title("Standalone Winit/Wgpu Example");
}
#[allow(unused_assignments)]
#[cfg(target_arch = "wasm32")]
let (mut canvas_width, mut canvas_height) = (0, 0);
#[cfg(target_arch = "wasm32")]
{
use winit::platform::web::WindowAttributesExtWebSys;
let canvas = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("canvas")
.unwrap()
.dyn_into::<web_sys::HtmlCanvasElement>()
.unwrap();
canvas_width = canvas.width();
canvas_height = canvas.height();
self.last_size = (canvas_width, canvas_height);
attributes = attributes.with_canvas(Some(canvas));
}
if let Ok(window) = event_loop.create_window(attributes) {
let first_window_handle = self.window.is_none();
let window_handle = std::sync::Arc::new(window);
self.window = Some(window_handle.clone());
if first_window_handle {
#[cfg(not(target_arch = "wasm32"))]
{
let inner_size = window_handle.inner_size();
self.last_size = (inner_size.width, inner_size.height);
}
let gui_context = egui::Context::default();
gui_context.set_pixels_per_point(window_handle.scale_factor() as f32);
let viewport_id = gui_context.viewport_id();
let gui_state = egui_winit::State::new(
gui_context,
viewport_id,
&window_handle,
Some(window_handle.scale_factor() as _),
Some(Theme::Dark),
None,
);
#[cfg(not(target_arch = "wasm32"))]
let (width, height) = (
window_handle.inner_size().width,
window_handle.inner_size().height,
);
#[cfg(not(target_arch = "wasm32"))]
{
env_logger::init();
let renderer = pollster::block_on(async move {
Renderer::new(window_handle.clone(), width, height).await
});
self.renderer = Some(renderer);
}
#[cfg(target_arch = "wasm32")]
{
let (sender, receiver) = futures::channel::oneshot::channel();
self.renderer_receiver = Some(receiver);
std::panic::set_hook(Box::new(console_error_panic_hook::hook));
console_log::init().expect("Failed to initialize logger!");
log::info!("Canvas dimensions: ({canvas_width} x {canvas_height})");
wasm_bindgen_futures::spawn_local(async move {
let renderer =
Renderer::new(window_handle.clone(), canvas_width, canvas_height).await;
if sender.send(renderer).is_err() {
log::error!("Failed to create and send renderer!");
}
});
}
self.gui_state = Some(gui_state);
self.last_render_time = Some(Instant::now());
}
}
}
fn window_event(
&mut self,
event_loop: &winit::event_loop::ActiveEventLoop,
_window_id: winit::window::WindowId,
event: winit::event::WindowEvent,
) {
#[cfg(target_arch = "wasm32")]
{
let mut renderer_received = false;
if let Some(receiver) = self.renderer_receiver.as_mut() {
if let Ok(Some(renderer)) = receiver.try_recv() {
self.renderer = Some(renderer);
renderer_received = true;
}
}
if renderer_received {
self.renderer_receiver = None;
}
}
let (Some(gui_state), Some(renderer), Some(window), Some(last_render_time)) = (
self.gui_state.as_mut(),
self.renderer.as_mut(),
self.window.as_ref(),
self.last_render_time.as_mut(),
) else {
return;
};
// Receive gui window event
if gui_state.on_window_event(window, &event).consumed {
return;
}
// If the gui didn't consume the event, handle it
match event {
WindowEvent::KeyboardInput {
event:
winit::event::KeyEvent {
physical_key: winit::keyboard::PhysicalKey::Code(key_code),
..
},
..
} => {
// Exit by pressing the escape key
if matches!(key_code, winit::keyboard::KeyCode::Escape) {
event_loop.exit();
}
}
WindowEvent::Resized(PhysicalSize { width, height }) => {
let (width, height) = ((width).max(1), (height).max(1));
log::info!("Resizing renderer surface to: ({width}, {height})");
renderer.resize(width, height);
self.last_size = (width, height);
}
WindowEvent::CloseRequested => {
log::info!("Close requested. Exiting...");
event_loop.exit();
}
WindowEvent::RedrawRequested => {
let now = Instant::now();
let delta_time = now - *last_render_time;
*last_render_time = now;
let gui_input = gui_state.take_egui_input(window);
gui_state.egui_ctx().begin_frame(gui_input);
let egui_winit::egui::FullOutput {
textures_delta,
shapes,
pixels_per_point,
..
} = gui_state.egui_ctx().end_frame();
let paint_jobs = gui_state.egui_ctx().tessellate(shapes, pixels_per_point);
let screen_descriptor = {
let (width, height) = self.last_size;
egui_wgpu::ScreenDescriptor {
size_in_pixels: [width, height],
pixels_per_point: window.scale_factor() as f32,
}
};
renderer.render_frame(screen_descriptor, paint_jobs, textures_delta, delta_time);
}
_ => (),
}
window.request_redraw();
}
}
pub struct Renderer<'window> {
gpu: Gpu<'window>,
depth_texture_view: wgpu::TextureView,
egui_renderer: egui_wgpu::Renderer,
scene: Scene,
}
impl<'window> Renderer<'window> {
const DEPTH_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Depth32Float;
pub async fn new(
window: impl Into<wgpu::SurfaceTarget<'window>>,
width: u32,
height: u32,
) -> Self {
let gpu = Gpu::new_async(window, width, height).await;
let depth_texture_view = gpu.create_depth_texture(width, height);
let egui_renderer = egui_wgpu::Renderer::new(
&gpu.device,
gpu.surface_config.format,
Some(Self::DEPTH_FORMAT),
1,
false,
);
let scene = Scene::new(&gpu.device, gpu.surface_format);
Self {
gpu,
depth_texture_view,
egui_renderer,
scene,
}
}
pub fn resize(&mut self, width: u32, height: u32) {
self.gpu.resize(width, height);
self.depth_texture_view = self.gpu.create_depth_texture(width, height);
}
pub fn render_frame(
&mut self,
screen_descriptor: egui_wgpu::ScreenDescriptor,
paint_jobs: Vec<egui::epaint::ClippedPrimitive>,
textures_delta: egui::TexturesDelta,
delta_time: crate::Duration,
) {
let delta_time = delta_time.as_secs_f32();
self.scene
.update(&self.gpu.queue, self.gpu.aspect_ratio(), delta_time);
for (id, image_delta) in &textures_delta.set {
self.egui_renderer
.update_texture(&self.gpu.device, &self.gpu.queue, *id, image_delta);
}
for id in &textures_delta.free {
self.egui_renderer.free_texture(id);
}
let mut encoder = self
.gpu
.device
.create_command_encoder(&wgpu::CommandEncoderDescriptor {
label: Some("Render Encoder"),
});
self.egui_renderer.update_buffers(
&self.gpu.device,
&self.gpu.queue,
&mut encoder,
&paint_jobs,
&screen_descriptor,
);
let surface_texture = self
.gpu
.surface
.get_current_texture()
.expect("Failed to get surface texture!");
let surface_texture_view =
surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor {
label: wgpu::Label::default(),
aspect: wgpu::TextureAspect::default(),
format: Some(self.gpu.surface_format),
dimension: None,
base_mip_level: 0,
mip_level_count: None,
base_array_layer: 0,
array_layer_count: None,
});
encoder.insert_debug_marker("Render scene");
// This scope around the crate::render_pass prevents the
// crate::render_pass from holding a borrow to the encoder,
// which would prevent calling `.finish()` in
// preparation for queue submission.
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("Render Pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: &surface_texture_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(wgpu::Color {
r: 0.19,
g: 0.24,
b: 0.42,
a: 1.0,
}),
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
view: &self.depth_texture_view,
depth_ops: Some(wgpu::Operations {
load: wgpu::LoadOp::Clear(1.0),
store: wgpu::StoreOp::Store,
}),
stencil_ops: None,
}),
timestamp_writes: None,
occlusion_query_set: None,
});
self.scene.render(&mut render_pass);
self.egui_renderer
.render(&mut render_pass, &paint_jobs, &screen_descriptor);
}
self.gpu.queue.submit(std::iter::once(encoder.finish()));
surface_texture.present();
}
}
pub struct Gpu<'window> {
pub surface: wgpu::Surface<'window>,
pub device: wgpu::Device,
pub queue: wgpu::Queue,
pub surface_config: wgpu::SurfaceConfiguration,
pub surface_format: wgpu::TextureFormat,
}
impl<'window> Gpu<'window> {
pub fn aspect_ratio(&self) -> f32 {
self.surface_config.width as f32 / self.surface_config.height.max(1) as f32
}
pub fn resize(&mut self, width: u32, height: u32) {
self.surface_config.width = width;
self.surface_config.height = height;
self.surface.configure(&self.device, &self.surface_config);
}
pub fn create_depth_texture(&self, width: u32, height: u32) -> wgpu::TextureView {
let texture = self.device.create_texture(
&(wgpu::TextureDescriptor {
label: Some("Depth Texture"),
size: wgpu::Extent3d {
width,
height,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Depth32Float,
usage: wgpu::TextureUsages::RENDER_ATTACHMENT
| wgpu::TextureUsages::TEXTURE_BINDING,
view_formats: &[],
}),
);
texture.create_view(&wgpu::TextureViewDescriptor {
label: None,
format: Some(wgpu::TextureFormat::Depth32Float),
dimension: Some(wgpu::TextureViewDimension::D2),
aspect: wgpu::TextureAspect::All,
base_mip_level: 0,
base_array_layer: 0,
array_layer_count: None,
mip_level_count: None,
})
}
pub async fn new_async(
window: impl Into<wgpu::SurfaceTarget<'window>>,
width: u32,
height: u32,
) -> Self {
let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
backends: wgpu::util::backend_bits_from_env().unwrap_or_else(wgpu::Backends::all),
..Default::default()
});
let surface = instance.create_surface(window).unwrap();
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: wgpu::PowerPreference::default(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.expect("Failed to request adapter!");
let (device, queue) = {
log::info!("WGPU Adapter Features: {:#?}", adapter.features());
adapter
.request_device(
&wgpu::DeviceDescriptor {
label: Some("WGPU Device"),
#[cfg(not(target_arch = "wasm32"))]
required_features: wgpu::Features::default(),
#[cfg(all(target_arch = "wasm32", feature = "webgpu"))]
required_features: wgpu::Features::all_webgpu_mask(),
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
required_features: wgpu::Features::default(),
#[cfg(not(target_arch = "wasm32"))]
required_limits: wgpu::Limits {
max_texture_dimension_2d: 4096, // Allow higher resolutions on native
..wgpu::Limits::downlevel_defaults()
},
#[cfg(all(target_arch = "wasm32", feature = "webgpu"))]
required_limits: wgpu::Limits::default(),
#[cfg(all(target_arch = "wasm32", feature = "webgl"))]
required_limits: wgpu::Limits::downlevel_webgl2_defaults(),
memory_hints: wgpu::MemoryHints::default(),
},
None,
)
.await
.expect("Failed to request a device!")
};
let surface_capabilities = surface.get_capabilities(&adapter);
// This assumes an sRGB surface texture
let surface_format = surface_capabilities
.formats
.iter()
.copied()
.find(|f| !f.is_srgb()) // egui wants a non-srgb surface texture
.unwrap_or(surface_capabilities.formats[0]);
let surface_config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width,
height,
present_mode: surface_capabilities.present_modes[0],
alpha_mode: surface_capabilities.alpha_modes[0],
view_formats: vec![],
desired_maximum_frame_latency: 2,
};
surface.configure(&device, &surface_config);
Self {
surface,
device,
queue,
surface_config,
surface_format,
}
}
}
struct Scene {
pub model: nalgebra_glm::Mat4,
pub vertex_buffer: wgpu::Buffer,
pub index_buffer: wgpu::Buffer,
pub uniform: UniformBinding,
pub pipeline: wgpu::RenderPipeline,
}
impl Scene {
pub fn new(device: &wgpu::Device, surface_format: wgpu::TextureFormat) -> Self {
let vertex_buffer = wgpu::util::DeviceExt::create_buffer_init(
device,
&wgpu::util::BufferInitDescriptor {
label: Some("Vertex Buffer"),
contents: bytemuck::cast_slice(&VERTICES),
usage: wgpu::BufferUsages::VERTEX,
},
);
let index_buffer = wgpu::util::DeviceExt::create_buffer_init(
device,
&wgpu::util::BufferInitDescriptor {
label: Some("index Buffer"),
contents: bytemuck::cast_slice(&INDICES),
usage: wgpu::BufferUsages::INDEX,
},
);
let uniform = UniformBinding::new(device);
let pipeline = Self::create_pipeline(device, surface_format, &uniform);
Self {
model: nalgebra_glm::Mat4::identity(),
uniform,
pipeline,
vertex_buffer,
index_buffer,
}
}
pub fn render<'rpass>(&'rpass self, renderpass: &mut wgpu::RenderPass<'rpass>) {
renderpass.set_pipeline(&self.pipeline);
renderpass.set_bind_group(0, &self.uniform.bind_group, &[]);
renderpass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
renderpass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
renderpass.draw_indexed(0..(INDICES.len() as _), 0, 0..1);
}
pub fn update(&mut self, queue: &wgpu::Queue, aspect_ratio: f32, delta_time: f32) {
let projection =
nalgebra_glm::perspective_lh_zo(aspect_ratio, 80_f32.to_radians(), 0.1, 1000.0);
let view = nalgebra_glm::look_at_lh(
&nalgebra_glm::vec3(0.0, 0.0, 3.0),
&nalgebra_glm::vec3(0.0, 0.0, 0.0),
&nalgebra_glm::Vec3::y(),
);
self.model = nalgebra_glm::rotate(
&self.model,
30_f32.to_radians() * delta_time,
&nalgebra_glm::Vec3::y(),
);
self.uniform.update_buffer(
queue,
0,
UniformBuffer {
mvp: projection * view * self.model,
},
);
}
fn create_pipeline(
device: &wgpu::Device,
surface_format: wgpu::TextureFormat,
uniform: &UniformBinding,
) -> wgpu::RenderPipeline {
let shader_module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: None,
source: wgpu::ShaderSource::Wgsl(std::borrow::Cow::Borrowed(SHADER_SOURCE)),
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: None,
bind_group_layouts: &[&uniform.bind_group_layout],
push_constant_ranges: &[],
});
device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: None,
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader_module,
entry_point: "vertex_main",
buffers: &[Vertex::description(&Vertex::vertex_attributes())],
compilation_options: Default::default(),
},
primitive: wgpu::PrimitiveState {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: Some(wgpu::IndexFormat::Uint32),
front_face: wgpu::FrontFace::Cw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
conservative: false,
unclipped_depth: false,
},
depth_stencil: Some(wgpu::DepthStencilState {
format: Renderer::DEPTH_FORMAT,
depth_write_enabled: true,
depth_compare: wgpu::CompareFunction::Less,
stencil: wgpu::StencilState::default(),
bias: wgpu::DepthBiasState::default(),
}),
multisample: wgpu::MultisampleState {
count: 1,
mask: !0,
alpha_to_coverage_enabled: false,
},
fragment: Some(wgpu::FragmentState {
module: &shader_module,
entry_point: "fragment_main",
targets: &[Some(wgpu::ColorTargetState {
format: surface_format,
blend: Some(wgpu::BlendState::ALPHA_BLENDING),
write_mask: wgpu::ColorWrites::ALL,
})],
compilation_options: Default::default(),
}),
multiview: None,
cache: None,
})
}
}
#[repr(C)]
#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
struct Vertex {
position: [f32; 4],
color: [f32; 4],
}
impl Vertex {
pub fn vertex_attributes() -> Vec<wgpu::VertexAttribute> {
wgpu::vertex_attr_array![0 => Float32x4, 1 => Float32x4].to_vec()
}
pub fn description(attributes: &[wgpu::VertexAttribute]) -> wgpu::VertexBufferLayout {
wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<Vertex>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes,
}
}
}
#[repr(C)]
#[derive(Default, Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
struct UniformBuffer {
mvp: nalgebra_glm::Mat4,
}
struct UniformBinding {
pub buffer: wgpu::Buffer,
pub bind_group: wgpu::BindGroup,
pub bind_group_layout: wgpu::BindGroupLayout,
}
impl UniformBinding {
pub fn new(device: &wgpu::Device) -> Self {
let buffer = wgpu::util::DeviceExt::create_buffer_init(
device,
&wgpu::util::BufferInitDescriptor {
label: Some("Uniform Buffer"),
contents: bytemuck::cast_slice(&[UniformBuffer::default()]),
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
},
);
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
entries: &[wgpu::BindGroupLayoutEntry {
binding: 0,
visibility: wgpu::ShaderStages::VERTEX,
ty: wgpu::BindingType::Buffer {
ty: wgpu::BufferBindingType::Uniform,
has_dynamic_offset: false,
min_binding_size: None,
},
count: None,
}],
label: Some("uniform_bind_group_layout"),
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
layout: &bind_group_layout,
entries: &[wgpu::BindGroupEntry {
binding: 0,
resource: buffer.as_entire_binding(),
}],
label: Some("uniform_bind_group"),
});
Self {
buffer,
bind_group,
bind_group_layout,
}
}
pub fn update_buffer(
&mut self,
queue: &wgpu::Queue,
offset: wgpu::BufferAddress,
uniform_buffer: UniformBuffer,
) {
queue.write_buffer(
&self.buffer,
offset,
bytemuck::cast_slice(&[uniform_buffer]),
)
}
}
const VERTICES: [Vertex; 3] = [
Vertex {
position: [1.0, -1.0, 0.0, 1.0],
color: [1.0, 0.0, 0.0, 1.0],
},
Vertex {
position: [-1.0, -1.0, 0.0, 1.0],
color: [0.0, 1.0, 0.0, 1.0],
},
Vertex {
position: [0.0, 1.0, 0.0, 1.0],
color: [0.0, 0.0, 1.0, 1.0],
},
];
const INDICES: [u32; 3] = [0, 1, 2]; // Clockwise winding order
const SHADER_SOURCE: &str = "
struct Uniform {
mvp: mat4x4<f32>,
};
@group(0) @binding(0)
var<uniform> ubo: Uniform;
struct VertexInput {
@location(0) position: vec4<f32>,
@location(1) color: vec4<f32>,
};
struct VertexOutput {
@builtin(position) position: vec4<f32>,
@location(0) color: vec4<f32>,
};
@vertex
fn vertex_main(vert: VertexInput) -> VertexOutput {
var out: VertexOutput;
out.color = vert.color;
out.position = ubo.mvp * vert.position;
return out;
};
@fragment
fn fragment_main(in: VertexOutput) -> @location(0) vec4<f32> {
return vec4<f32>(in.color);
}
";
fn main() -> Result<(), Box<dyn std::error::Error>> {
let event_loop = winit::event_loop::EventLoop::builder().build()?;
event_loop.set_control_flow(winit::event_loop::ControlFlow::Poll);
let mut app = app_core::App::default();
event_loop.run_app(&mut app)?;
Ok(())
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment