use std::error::Error; use bytemuck::{Pod, Zeroable}; use raw_window_handle::{HasDisplayHandle, HasWindowHandle}; use ruin_ui::{Color, DisplayItem, Rect, SceneSnapshot}; use wgpu::util::DeviceExt; #[repr(C)] #[derive(Clone, Copy, Pod, Zeroable)] struct Vertex { position: [f32; 2], color: [f32; 4], } const VERTEX_ATTRIBUTES: [wgpu::VertexAttribute; 2] = wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x4]; impl Vertex { const LAYOUT: wgpu::VertexBufferLayout<'static> = wgpu::VertexBufferLayout { array_stride: std::mem::size_of::() as u64, step_mode: wgpu::VertexStepMode::Vertex, attributes: &VERTEX_ATTRIBUTES, }; fn layout() -> wgpu::VertexBufferLayout<'static> { Self::LAYOUT } } #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum RenderError { Lost, Outdated, Timeout, Occluded, Validation, } pub struct WgpuSceneRenderer { surface: wgpu::Surface<'static>, device: wgpu::Device, queue: wgpu::Queue, config: wgpu::SurfaceConfiguration, pipeline: wgpu::RenderPipeline, } impl WgpuSceneRenderer { pub fn new( target: impl HasDisplayHandle + HasWindowHandle + Send + Sync + 'static, width: u32, height: u32, ) -> Result> { let instance = wgpu::Instance::new(wgpu::InstanceDescriptor::new_without_display_handle()); let surface = instance.create_surface(target)?; let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions { power_preference: wgpu::PowerPreference::HighPerformance, force_fallback_adapter: false, compatible_surface: Some(&surface), }))?; let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor::default()))?; let caps = surface.get_capabilities(&adapter); let format = caps .formats .iter() .copied() .find(wgpu::TextureFormat::is_srgb) .unwrap_or(caps.formats[0]); let config = wgpu::SurfaceConfiguration { usage: wgpu::TextureUsages::RENDER_ATTACHMENT, format, width: width.max(1), height: height.max(1), present_mode: wgpu::PresentMode::AutoVsync, desired_maximum_frame_latency: 2, alpha_mode: caps.alpha_modes[0], view_formats: vec![], }; surface.configure(&device, &config); let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("ruin-ui-renderer-wgpu-shader"), source: wgpu::ShaderSource::Wgsl( r#" struct VertexIn { @location(0) position: vec2, @location(1) color: vec4, }; struct VertexOut { @builtin(position) position: vec4, @location(0) color: vec4, }; @vertex fn vs_main(input: VertexIn) -> VertexOut { var out: VertexOut; out.position = vec4(input.position, 0.0, 1.0); out.color = input.color; return out; } @fragment fn fs_main(input: VertexOut) -> @location(0) vec4 { return input.color; } "# .into(), ), }); let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("ruin-ui-renderer-wgpu-pipeline-layout"), bind_group_layouts: &[], immediate_size: 0, }); let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor { label: Some("ruin-ui-renderer-wgpu-pipeline"), layout: Some(&pipeline_layout), vertex: wgpu::VertexState { module: &shader, entry_point: Some("vs_main"), buffers: &[Vertex::layout()], compilation_options: wgpu::PipelineCompilationOptions::default(), }, fragment: Some(wgpu::FragmentState { module: &shader, entry_point: Some("fs_main"), targets: &[Some(wgpu::ColorTargetState { format, blend: Some(wgpu::BlendState::ALPHA_BLENDING), write_mask: wgpu::ColorWrites::ALL, })], compilation_options: wgpu::PipelineCompilationOptions::default(), }), primitive: wgpu::PrimitiveState::default(), depth_stencil: None, multisample: wgpu::MultisampleState::default(), multiview_mask: None, cache: None, }); Ok(Self { surface, device, queue, config, pipeline, }) } pub fn size(&self) -> (u32, u32) { (self.config.width, self.config.height) } pub fn resize(&mut self, width: u32, height: u32) { self.config.width = width.max(1); self.config.height = height.max(1); self.surface.configure(&self.device, &self.config); } pub fn render(&mut self, scene: &SceneSnapshot) -> Result<(), RenderError> { let vertices = build_vertices(scene); let frame = match self.surface.get_current_texture() { wgpu::CurrentSurfaceTexture::Success(frame) | wgpu::CurrentSurfaceTexture::Suboptimal(frame) => frame, wgpu::CurrentSurfaceTexture::Lost => return Err(RenderError::Lost), wgpu::CurrentSurfaceTexture::Outdated => return Err(RenderError::Outdated), wgpu::CurrentSurfaceTexture::Timeout => return Err(RenderError::Timeout), wgpu::CurrentSurfaceTexture::Occluded => return Err(RenderError::Occluded), wgpu::CurrentSurfaceTexture::Validation => return Err(RenderError::Validation), }; let view = frame .texture .create_view(&wgpu::TextureViewDescriptor::default()); let vertex_buffer = self .device .create_buffer_init(&wgpu::util::BufferInitDescriptor { label: Some("ruin-ui-renderer-wgpu-vertices"), contents: bytemuck::cast_slice(&vertices), usage: wgpu::BufferUsages::VERTEX, }); let mut encoder = self .device .create_command_encoder(&wgpu::CommandEncoderDescriptor { label: Some("ruin-ui-renderer-wgpu-encoder"), }); { let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("ruin-ui-renderer-wgpu-pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: &view, depth_slice: None, resolve_target: None, ops: wgpu::Operations { load: wgpu::LoadOp::Clear(wgpu::Color { r: 0.03, g: 0.04, b: 0.08, a: 1.0, }), store: wgpu::StoreOp::Store, }, })], depth_stencil_attachment: None, timestamp_writes: None, occlusion_query_set: None, multiview_mask: None, }); if !vertices.is_empty() { pass.set_pipeline(&self.pipeline); pass.set_vertex_buffer(0, vertex_buffer.slice(..)); pass.draw(0..vertices.len() as u32, 0..1); } } self.queue.submit([encoder.finish()]); frame.present(); Ok(()) } } fn build_vertices(scene: &SceneSnapshot) -> Vec { let width = scene.logical_size.width.max(1.0); let height = scene.logical_size.height.max(1.0); let mut vertices = Vec::new(); for item in &scene.items { if let DisplayItem::Quad(quad) = item { push_quad_vertices(&mut vertices, quad.rect, quad.color, width, height); } } vertices } fn push_quad_vertices( vertices: &mut Vec, rect: Rect, color: Color, width: f32, height: f32, ) { let left = to_ndc_x(rect.origin.x, width); let right = to_ndc_x(rect.origin.x + rect.size.width, width); let top = to_ndc_y(rect.origin.y, height); let bottom = to_ndc_y(rect.origin.y + rect.size.height, height); let color = [ color.r as f32 / 255.0, color.g as f32 / 255.0, color.b as f32 / 255.0, color.a as f32 / 255.0, ]; vertices.extend_from_slice(&[ Vertex { position: [left, top], color, }, Vertex { position: [right, top], color, }, Vertex { position: [right, bottom], color, }, Vertex { position: [left, top], color, }, Vertex { position: [right, bottom], color, }, Vertex { position: [left, bottom], color, }, ]); } fn to_ndc_x(x: f32, width: f32) -> f32 { (x / width) * 2.0 - 1.0 } fn to_ndc_y(y: f32, height: f32) -> f32 { 1.0 - (y / height) * 2.0 } #[cfg(test)] mod tests { use super::build_vertices; use ruin_ui::{Color, PreparedText, Rect, SceneSnapshot, UiSize}; #[test] fn quad_scenes_expand_to_six_vertices_per_quad() { let mut scene = SceneSnapshot::new(1, UiSize::new(100.0, 50.0)); scene.push_quad( Rect::new(0.0, 0.0, 100.0, 50.0), Color::rgb(0x11, 0x22, 0x33), ); scene.push_quad( Rect::new(10.0, 10.0, 20.0, 20.0), Color::rgb(0x44, 0x55, 0x66), ); scene.push_text(PreparedText { text: "ignored".into(), font_size: 16.0, color: Color::rgb(0xFF, 0xFF, 0xFF), glyphs: Vec::new(), }); let vertices = build_vertices(&scene); assert_eq!(vertices.len(), 12); } }