//! This example shows basic usage of wgpu-hal by rendering //! a ton of moving sprites, each with a separate texture and draw call. extern crate wgpu_hal as hal; use hal::{ Adapter as _, CommandEncoder as _, Device as _, Instance as _, Queue as _, Surface as _, }; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use winit::{ event::{ElementState, Event, KeyEvent, WindowEvent}, event_loop::ControlFlow, keyboard::{Key, NamedKey}, }; use std::{ borrow::{Borrow, Cow}, iter, mem::size_of, ptr, time::Instant, }; const MAX_BUNNIES: usize = 1 << 20; const BUNNY_SIZE: f32 = 0.15 * 256.0; const GRAVITY: f32 = -9.8 * 100.0; const MAX_VELOCITY: f32 = 750.0; const DESIRED_MAX_LATENCY: u32 = 2; #[repr(C)] #[derive(Clone, Copy)] struct Globals { mvp: [[f32; 4]; 4], size: [f32; 2], pad: [f32; 2], } #[repr(C, align(256))] #[derive(Clone, Copy)] struct Locals { position: [f32; 2], velocity: [f32; 2], color: u32, _pad: u32, } struct ExecutionContext { encoder: A::CommandEncoder, fence: A::Fence, fence_value: hal::FenceValue, used_views: Vec, used_cmd_bufs: Vec, frames_recorded: usize, } impl ExecutionContext { unsafe fn wait_and_clear(&mut self, device: &A::Device) { device.wait(&self.fence, self.fence_value, !0).unwrap(); self.encoder.reset_all(self.used_cmd_bufs.drain(..)); for view in self.used_views.drain(..) { device.destroy_texture_view(view); } self.frames_recorded = 0; } } #[allow(dead_code)] struct Example { instance: A::Instance, adapter: A::Adapter, surface: A::Surface, surface_format: wgt::TextureFormat, device: A::Device, queue: A::Queue, global_group: A::BindGroup, local_group: A::BindGroup, global_group_layout: A::BindGroupLayout, local_group_layout: A::BindGroupLayout, pipeline_layout: A::PipelineLayout, shader: A::ShaderModule, pipeline: A::RenderPipeline, bunnies: Vec, local_buffer: A::Buffer, local_alignment: u32, global_buffer: A::Buffer, sampler: A::Sampler, texture: A::Texture, texture_view: A::TextureView, contexts: Vec>, context_index: usize, extent: [u32; 2], start: Instant, } impl Example { fn init(window: &winit::window::Window) -> Result> { let instance_desc = hal::InstanceDescriptor { name: "example", flags: wgt::InstanceFlags::from_build_config().with_env(), // Can't rely on having DXC available, so use FXC instead dx12_shader_compiler: wgt::Dx12Compiler::Fxc, gles_minor_version: wgt::Gles3MinorVersion::default(), }; let instance = unsafe { A::Instance::init(&instance_desc)? }; let surface = { let raw_window_handle = window.window_handle()?.as_raw(); let raw_display_handle = window.display_handle()?.as_raw(); unsafe { instance .create_surface(raw_display_handle, raw_window_handle) .unwrap() } }; let (adapter, capabilities) = unsafe { let mut adapters = instance.enumerate_adapters(Some(&surface)); if adapters.is_empty() { return Err("no adapters found".into()); } let exposed = adapters.swap_remove(0); (exposed.adapter, exposed.capabilities) }; let surface_caps = unsafe { adapter.surface_capabilities(&surface) } .ok_or("failed to get surface capabilities")?; log::info!("Surface caps: {:#?}", surface_caps); let hal::OpenDevice { device, queue } = unsafe { adapter .open( wgt::Features::empty(), &wgt::Limits::default(), &wgt::MemoryHints::default(), ) .unwrap() }; let window_size: (u32, u32) = window.inner_size().into(); let surface_config = hal::SurfaceConfiguration { maximum_frame_latency: DESIRED_MAX_LATENCY.clamp( *surface_caps.maximum_frame_latency.start(), *surface_caps.maximum_frame_latency.end(), ), present_mode: wgt::PresentMode::Fifo, composite_alpha_mode: wgt::CompositeAlphaMode::Opaque, format: wgt::TextureFormat::Bgra8UnormSrgb, extent: wgt::Extent3d { width: window_size.0, height: window_size.1, depth_or_array_layers: 1, }, usage: hal::TextureUses::COLOR_TARGET, view_formats: vec![], }; unsafe { surface.configure(&device, &surface_config).unwrap(); }; let naga_shader = { let shader_file = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("examples") .join("halmark") .join("shader.wgsl"); let source = std::fs::read_to_string(shader_file).unwrap(); let module = naga::front::wgsl::Frontend::new().parse(&source).unwrap(); let info = naga::valid::Validator::new( naga::valid::ValidationFlags::all(), naga::valid::Capabilities::empty(), ) .validate(&module) .unwrap(); hal::NagaShader { module: Cow::Owned(module), info, debug_source: None, } }; let shader_desc = hal::ShaderModuleDescriptor { label: None, runtime_checks: false, }; let shader = unsafe { device .create_shader_module(&shader_desc, hal::ShaderInput::Naga(naga_shader)) .unwrap() }; let global_bgl_desc = hal::BindGroupLayoutDescriptor { label: None, flags: hal::BindGroupLayoutFlags::empty(), entries: &[ wgt::BindGroupLayoutEntry { binding: 0, visibility: wgt::ShaderStages::VERTEX, ty: wgt::BindingType::Buffer { ty: wgt::BufferBindingType::Uniform, has_dynamic_offset: false, min_binding_size: wgt::BufferSize::new(size_of::() as _), }, count: None, }, wgt::BindGroupLayoutEntry { binding: 1, visibility: wgt::ShaderStages::FRAGMENT, ty: wgt::BindingType::Texture { sample_type: wgt::TextureSampleType::Float { filterable: true }, view_dimension: wgt::TextureViewDimension::D2, multisampled: false, }, count: None, }, wgt::BindGroupLayoutEntry { binding: 2, visibility: wgt::ShaderStages::FRAGMENT, ty: wgt::BindingType::Sampler(wgt::SamplerBindingType::Filtering), count: None, }, ], }; let global_group_layout = unsafe { device.create_bind_group_layout(&global_bgl_desc).unwrap() }; let local_bgl_desc = hal::BindGroupLayoutDescriptor { label: None, flags: hal::BindGroupLayoutFlags::empty(), entries: &[wgt::BindGroupLayoutEntry { binding: 0, visibility: wgt::ShaderStages::VERTEX, ty: wgt::BindingType::Buffer { ty: wgt::BufferBindingType::Uniform, has_dynamic_offset: true, min_binding_size: wgt::BufferSize::new(size_of::() as _), }, count: None, }], }; let local_group_layout = unsafe { device.create_bind_group_layout(&local_bgl_desc).unwrap() }; let pipeline_layout_desc = hal::PipelineLayoutDescriptor { label: None, flags: hal::PipelineLayoutFlags::empty(), bind_group_layouts: &[&global_group_layout, &local_group_layout], push_constant_ranges: &[], }; let pipeline_layout = unsafe { device .create_pipeline_layout(&pipeline_layout_desc) .unwrap() }; let constants = naga::back::PipelineConstants::default(); let pipeline_desc = hal::RenderPipelineDescriptor { label: None, layout: &pipeline_layout, vertex_stage: hal::ProgrammableStage { module: &shader, entry_point: "vs_main", constants: &constants, zero_initialize_workgroup_memory: true, }, vertex_buffers: &[], fragment_stage: Some(hal::ProgrammableStage { module: &shader, entry_point: "fs_main", constants: &constants, zero_initialize_workgroup_memory: true, }), primitive: wgt::PrimitiveState { topology: wgt::PrimitiveTopology::TriangleStrip, ..wgt::PrimitiveState::default() }, depth_stencil: None, multisample: wgt::MultisampleState::default(), color_targets: &[Some(wgt::ColorTargetState { format: surface_config.format, blend: Some(wgt::BlendState::ALPHA_BLENDING), write_mask: wgt::ColorWrites::default(), })], multiview: None, cache: None, }; let pipeline = unsafe { device.create_render_pipeline(&pipeline_desc).unwrap() }; let texture_data = [0xFFu8; 4]; let staging_buffer_desc = hal::BufferDescriptor { label: Some("stage"), size: texture_data.len() as wgt::BufferAddress, usage: hal::BufferUses::MAP_WRITE | hal::BufferUses::COPY_SRC, memory_flags: hal::MemoryFlags::TRANSIENT | hal::MemoryFlags::PREFER_COHERENT, }; let staging_buffer = unsafe { device.create_buffer(&staging_buffer_desc).unwrap() }; unsafe { let mapping = device .map_buffer(&staging_buffer, 0..staging_buffer_desc.size) .unwrap(); ptr::copy_nonoverlapping( texture_data.as_ptr(), mapping.ptr.as_ptr(), texture_data.len(), ); device.unmap_buffer(&staging_buffer); assert!(mapping.is_coherent); } let texture_desc = hal::TextureDescriptor { label: None, size: wgt::Extent3d { width: 1, height: 1, depth_or_array_layers: 1, }, mip_level_count: 1, sample_count: 1, dimension: wgt::TextureDimension::D2, format: wgt::TextureFormat::Rgba8UnormSrgb, usage: hal::TextureUses::COPY_DST | hal::TextureUses::RESOURCE, memory_flags: hal::MemoryFlags::empty(), view_formats: vec![], }; let texture = unsafe { device.create_texture(&texture_desc).unwrap() }; let cmd_encoder_desc = hal::CommandEncoderDescriptor { label: None, queue: &queue, }; let mut cmd_encoder = unsafe { device.create_command_encoder(&cmd_encoder_desc).unwrap() }; unsafe { cmd_encoder.begin_encoding(Some("init")).unwrap() }; { let buffer_barrier = hal::BufferBarrier { buffer: &staging_buffer, usage: hal::BufferUses::empty()..hal::BufferUses::COPY_SRC, }; let texture_barrier1 = hal::TextureBarrier { texture: &texture, range: wgt::ImageSubresourceRange::default(), usage: hal::TextureUses::UNINITIALIZED..hal::TextureUses::COPY_DST, }; let texture_barrier2 = hal::TextureBarrier { texture: &texture, range: wgt::ImageSubresourceRange::default(), usage: hal::TextureUses::COPY_DST..hal::TextureUses::RESOURCE, }; let copy = hal::BufferTextureCopy { buffer_layout: wgt::ImageDataLayout { offset: 0, bytes_per_row: Some(4), rows_per_image: None, }, texture_base: hal::TextureCopyBase { origin: wgt::Origin3d::ZERO, mip_level: 0, array_layer: 0, aspect: hal::FormatAspects::COLOR, }, size: hal::CopyExtent { width: 1, height: 1, depth: 1, }, }; unsafe { cmd_encoder.transition_buffers(iter::once(buffer_barrier)); cmd_encoder.transition_textures(iter::once(texture_barrier1)); cmd_encoder.copy_buffer_to_texture(&staging_buffer, &texture, iter::once(copy)); cmd_encoder.transition_textures(iter::once(texture_barrier2)); } } let sampler_desc = hal::SamplerDescriptor { label: None, address_modes: [wgt::AddressMode::ClampToEdge; 3], mag_filter: wgt::FilterMode::Linear, min_filter: wgt::FilterMode::Nearest, mipmap_filter: wgt::FilterMode::Nearest, lod_clamp: 0.0..32.0, compare: None, anisotropy_clamp: 1, border_color: None, }; let sampler = unsafe { device.create_sampler(&sampler_desc).unwrap() }; let globals = Globals { // cgmath::ortho() projection mvp: [ [2.0 / window_size.0 as f32, 0.0, 0.0, 0.0], [0.0, 2.0 / window_size.1 as f32, 0.0, 0.0], [0.0, 0.0, 1.0, 0.0], [-1.0, -1.0, 0.0, 1.0], ], size: [BUNNY_SIZE; 2], pad: [0.0; 2], }; let global_buffer_desc = hal::BufferDescriptor { label: Some("global"), size: size_of::() as wgt::BufferAddress, usage: hal::BufferUses::MAP_WRITE | hal::BufferUses::UNIFORM, memory_flags: hal::MemoryFlags::PREFER_COHERENT, }; let global_buffer = unsafe { let buffer = device.create_buffer(&global_buffer_desc).unwrap(); let mapping = device .map_buffer(&buffer, 0..global_buffer_desc.size) .unwrap(); ptr::copy_nonoverlapping( &globals as *const Globals as *const u8, mapping.ptr.as_ptr(), size_of::(), ); device.unmap_buffer(&buffer); assert!(mapping.is_coherent); buffer }; let local_alignment = wgt::math::align_to( size_of::() as u32, capabilities.limits.min_uniform_buffer_offset_alignment, ); let local_buffer_desc = hal::BufferDescriptor { label: Some("local"), size: (MAX_BUNNIES as wgt::BufferAddress) * (local_alignment as wgt::BufferAddress), usage: hal::BufferUses::MAP_WRITE | hal::BufferUses::UNIFORM, memory_flags: hal::MemoryFlags::PREFER_COHERENT, }; let local_buffer = unsafe { device.create_buffer(&local_buffer_desc).unwrap() }; let view_desc = hal::TextureViewDescriptor { label: None, format: texture_desc.format, dimension: wgt::TextureViewDimension::D2, usage: hal::TextureUses::RESOURCE, range: wgt::ImageSubresourceRange::default(), }; let texture_view = unsafe { device.create_texture_view(&texture, &view_desc).unwrap() }; let global_group = { let global_buffer_binding = hal::BufferBinding { buffer: &global_buffer, offset: 0, size: None, }; let texture_binding = hal::TextureBinding { view: &texture_view, usage: hal::TextureUses::RESOURCE, }; let global_group_desc = hal::BindGroupDescriptor { label: Some("global"), layout: &global_group_layout, buffers: &[global_buffer_binding], samplers: &[&sampler], textures: &[texture_binding], acceleration_structures: &[], entries: &[ hal::BindGroupEntry { binding: 0, resource_index: 0, count: 1, }, hal::BindGroupEntry { binding: 1, resource_index: 0, count: 1, }, hal::BindGroupEntry { binding: 2, resource_index: 0, count: 1, }, ], }; unsafe { device.create_bind_group(&global_group_desc).unwrap() } }; let local_group = { let local_buffer_binding = hal::BufferBinding { buffer: &local_buffer, offset: 0, size: wgt::BufferSize::new(size_of::() as _), }; let local_group_desc = hal::BindGroupDescriptor { label: Some("local"), layout: &local_group_layout, buffers: &[local_buffer_binding], samplers: &[], textures: &[], acceleration_structures: &[], entries: &[hal::BindGroupEntry { binding: 0, resource_index: 0, count: 1, }], }; unsafe { device.create_bind_group(&local_group_desc).unwrap() } }; let init_fence_value = 1; let fence = unsafe { let mut fence = device.create_fence().unwrap(); let init_cmd = cmd_encoder.end_encoding().unwrap(); queue .submit(&[&init_cmd], &[], (&mut fence, init_fence_value)) .unwrap(); device.wait(&fence, init_fence_value, !0).unwrap(); device.destroy_buffer(staging_buffer); cmd_encoder.reset_all(iter::once(init_cmd)); fence }; Ok(Example { instance, surface, surface_format: surface_config.format, adapter, device, queue, pipeline_layout, shader, pipeline, global_group, local_group, global_group_layout, local_group_layout, bunnies: Vec::new(), local_buffer, local_alignment, global_buffer, sampler, texture, texture_view, contexts: vec![ExecutionContext { encoder: cmd_encoder, fence, fence_value: init_fence_value + 1, used_views: Vec::new(), used_cmd_bufs: Vec::new(), frames_recorded: 0, }], context_index: 0, extent: [window_size.0, window_size.1], start: Instant::now(), }) } fn is_empty(&self) -> bool { self.bunnies.is_empty() } fn exit(mut self) { unsafe { { let ctx = &mut self.contexts[self.context_index]; self.queue .submit(&[], &[], (&mut ctx.fence, ctx.fence_value)) .unwrap(); } for mut ctx in self.contexts { ctx.wait_and_clear(&self.device); self.device.destroy_command_encoder(ctx.encoder); self.device.destroy_fence(ctx.fence); } self.device.destroy_bind_group(self.local_group); self.device.destroy_bind_group(self.global_group); self.device.destroy_buffer(self.local_buffer); self.device.destroy_buffer(self.global_buffer); self.device.destroy_texture_view(self.texture_view); self.device.destroy_texture(self.texture); self.device.destroy_sampler(self.sampler); self.device.destroy_shader_module(self.shader); self.device.destroy_render_pipeline(self.pipeline); self.device .destroy_bind_group_layout(self.local_group_layout); self.device .destroy_bind_group_layout(self.global_group_layout); self.device.destroy_pipeline_layout(self.pipeline_layout); self.surface.unconfigure(&self.device); self.device.exit(self.queue); drop(self.surface); drop(self.adapter); } } fn update(&mut self, event: winit::event::WindowEvent) { if let winit::event::WindowEvent::KeyboardInput { event: KeyEvent { logical_key: Key::Named(NamedKey::Space), state: ElementState::Pressed, .. }, .. } = event { let spawn_count = 64 + self.bunnies.len() / 2; let elapsed = self.start.elapsed(); let color = elapsed.as_nanos() as u32; println!( "Spawning {} bunnies, total at {}", spawn_count, self.bunnies.len() + spawn_count ); for i in 0..spawn_count { let random = ((elapsed.as_nanos() * (i + 1) as u128) & 0xFF) as f32 / 255.0; let speed = random * MAX_VELOCITY - (MAX_VELOCITY * 0.5); self.bunnies.push(Locals { position: [0.0, 0.5 * (self.extent[1] as f32)], velocity: [speed, 0.0], color, _pad: 0, }); } } } fn render(&mut self) { let delta = 0.01; for bunny in self.bunnies.iter_mut() { bunny.position[0] += bunny.velocity[0] * delta; bunny.position[1] += bunny.velocity[1] * delta; bunny.velocity[1] += GRAVITY * delta; if (bunny.velocity[0] > 0.0 && bunny.position[0] + 0.5 * BUNNY_SIZE > self.extent[0] as f32) || (bunny.velocity[0] < 0.0 && bunny.position[0] - 0.5 * BUNNY_SIZE < 0.0) { bunny.velocity[0] *= -1.0; } if bunny.velocity[1] < 0.0 && bunny.position[1] < 0.5 * BUNNY_SIZE { bunny.velocity[1] *= -1.0; } } if !self.bunnies.is_empty() { let size = self.bunnies.len() * self.local_alignment as usize; unsafe { let mapping = self .device .map_buffer(&self.local_buffer, 0..size as wgt::BufferAddress) .unwrap(); ptr::copy_nonoverlapping( self.bunnies.as_ptr() as *const u8, mapping.ptr.as_ptr(), size, ); assert!(mapping.is_coherent); self.device.unmap_buffer(&self.local_buffer); } } let ctx = &mut self.contexts[self.context_index]; let surface_tex = unsafe { self.surface .acquire_texture(None, &ctx.fence) .unwrap() .unwrap() .texture }; let target_barrier0 = hal::TextureBarrier { texture: surface_tex.borrow(), range: wgt::ImageSubresourceRange::default(), usage: hal::TextureUses::UNINITIALIZED..hal::TextureUses::COLOR_TARGET, }; unsafe { ctx.encoder.begin_encoding(Some("frame")).unwrap(); ctx.encoder.transition_textures(iter::once(target_barrier0)); } let surface_view_desc = hal::TextureViewDescriptor { label: None, format: self.surface_format, dimension: wgt::TextureViewDimension::D2, usage: hal::TextureUses::COLOR_TARGET, range: wgt::ImageSubresourceRange::default(), }; let surface_tex_view = unsafe { self.device .create_texture_view(surface_tex.borrow(), &surface_view_desc) .unwrap() }; let pass_desc = hal::RenderPassDescriptor { label: None, extent: wgt::Extent3d { width: self.extent[0], height: self.extent[1], depth_or_array_layers: 1, }, sample_count: 1, color_attachments: &[Some(hal::ColorAttachment { target: hal::Attachment { view: &surface_tex_view, usage: hal::TextureUses::COLOR_TARGET, }, resolve_target: None, ops: hal::AttachmentOps::STORE, clear_value: wgt::Color { r: 0.1, g: 0.2, b: 0.3, a: 1.0, }, })], depth_stencil_attachment: None, multiview: None, timestamp_writes: None, occlusion_query_set: None, }; unsafe { ctx.encoder.begin_render_pass(&pass_desc); ctx.encoder.set_render_pipeline(&self.pipeline); ctx.encoder .set_bind_group(&self.pipeline_layout, 0, &self.global_group, &[]); } for i in 0..self.bunnies.len() { let offset = (i as wgt::DynamicOffset) * (self.local_alignment as wgt::DynamicOffset); unsafe { ctx.encoder .set_bind_group(&self.pipeline_layout, 1, &self.local_group, &[offset]); ctx.encoder.draw(0, 4, 0, 1); } } ctx.frames_recorded += 1; let target_barrier1 = hal::TextureBarrier { texture: surface_tex.borrow(), range: wgt::ImageSubresourceRange::default(), usage: hal::TextureUses::COLOR_TARGET..hal::TextureUses::PRESENT, }; unsafe { ctx.encoder.end_render_pass(); ctx.encoder.transition_textures(iter::once(target_barrier1)); } unsafe { let cmd_buf = ctx.encoder.end_encoding().unwrap(); self.queue .submit( &[&cmd_buf], &[&surface_tex], (&mut ctx.fence, ctx.fence_value), ) .unwrap(); self.queue.present(&self.surface, surface_tex).unwrap(); ctx.used_cmd_bufs.push(cmd_buf); ctx.used_views.push(surface_tex_view); }; log::debug!("Context switch from {}", self.context_index); let old_fence_value = ctx.fence_value; if self.contexts.len() == 1 { let hal_desc = hal::CommandEncoderDescriptor { label: None, queue: &self.queue, }; self.contexts.push(unsafe { ExecutionContext { encoder: self.device.create_command_encoder(&hal_desc).unwrap(), fence: self.device.create_fence().unwrap(), fence_value: 0, used_views: Vec::new(), used_cmd_bufs: Vec::new(), frames_recorded: 0, } }); } self.context_index = (self.context_index + 1) % self.contexts.len(); let next = &mut self.contexts[self.context_index]; unsafe { next.wait_and_clear(&self.device); } next.fence_value = old_fence_value + 1; } } cfg_if::cfg_if! { // Apple + Metal if #[cfg(all(any(target_os = "macos", target_os = "ios"), feature = "metal"))] { type Api = hal::api::Metal; } // Wasm + Vulkan else if #[cfg(all(not(target_arch = "wasm32"), feature = "vulkan"))] { type Api = hal::api::Vulkan; } // Windows + DX12 else if #[cfg(all(windows, feature = "dx12"))] { type Api = hal::api::Dx12; } // Anything + GLES else if #[cfg(feature = "gles")] { type Api = hal::api::Gles; } // Fallback else { type Api = hal::api::Empty; } } fn main() { env_logger::init(); let event_loop = winit::event_loop::EventLoop::new().unwrap(); let window = winit::window::WindowBuilder::new() .with_title("hal-bunnymark") .build(&event_loop) .unwrap(); let example_result = Example::::init(&window); let mut example = Some(example_result.expect("Selected backend is not supported")); println!("Press space to spawn bunnies."); let mut last_frame_inst = Instant::now(); let (mut frame_count, mut accum_time) = (0, 0.0); event_loop .run(move |event, target| { let _ = &window; // force ownership by the closure target.set_control_flow(ControlFlow::Poll); match event { Event::LoopExiting => { example.take().unwrap().exit(); } Event::WindowEvent { event, .. } => match event { WindowEvent::KeyboardInput { event: KeyEvent { logical_key: Key::Named(NamedKey::Escape), state: ElementState::Pressed, .. }, .. } | WindowEvent::CloseRequested => target.exit(), WindowEvent::RedrawRequested => { let ex = example.as_mut().unwrap(); { accum_time += last_frame_inst.elapsed().as_secs_f32(); last_frame_inst = Instant::now(); frame_count += 1; if frame_count == 100 && !ex.is_empty() { println!( "Avg frame time {}ms", accum_time * 1000.0 / frame_count as f32 ); accum_time = 0.0; frame_count = 0; } } ex.render(); window.request_redraw(); } _ => { example.as_mut().unwrap().update(event); } }, _ => {} } }) .unwrap(); }