use bytemuck::{Pod, Zeroable};
use super::atlas;
use wgpu::util::DeviceExt;
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
pub struct QuadInstance {
pub pos: [f32; 2],
pub size: [f32; 2],
pub color: [f32; 4],
pub uv: [f32; 4],
}
pub const NO_ICON: [f32; 4] = [0.0; 4];
#[repr(C)]
#[derive(Clone, Copy, Pod, Zeroable)]
struct ScreenUniform {
size: [f32; 2],
_pad: [f32; 2],
}
const UNIT_QUAD: &[[f32; 2]] = &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]];
pub struct UiRenderer {
pipeline: wgpu::RenderPipeline,
unit_quad_buffer: wgpu::Buffer,
instance_buffer: wgpu::Buffer,
instance_capacity: usize,
screen_uniform_buffer: wgpu::Buffer,
bind_group: wgpu::BindGroup,
atlas: wgpu::Texture,
}
impl UiRenderer {
pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
label: Some("ui shader"),
source: wgpu::ShaderSource::Wgsl(include_str!("ui.wgsl").into()),
});
let screen_uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui screen uniform"),
size: std::mem::size_of::<ScreenUniform>() as u64,
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
label: Some("ui bind group layout"),
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,
},
wgpu::BindGroupLayoutEntry {
binding: 1,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Texture {
sample_type: wgpu::TextureSampleType::Float { filterable: true },
view_dimension: wgpu::TextureViewDimension::D2,
multisampled: false,
},
count: None,
},
wgpu::BindGroupLayoutEntry {
binding: 2,
visibility: wgpu::ShaderStages::FRAGMENT,
ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
count: None,
},
],
});
let atlas = device.create_texture(&wgpu::TextureDescriptor {
label: Some("ui atlas"),
size: wgpu::Extent3d {
width: atlas::ATLAS_SIZE,
height: atlas::ATLAS_SIZE,
depth_or_array_layers: 1,
},
mip_level_count: 1,
sample_count: 1,
dimension: wgpu::TextureDimension::D2,
format: wgpu::TextureFormat::Rgba8Unorm,
usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
view_formats: &[],
});
let atlas_view = atlas.create_view(&wgpu::TextureViewDescriptor::default());
let atlas_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
label: Some("ui atlas sampler"),
mag_filter: wgpu::FilterMode::Linear,
min_filter: wgpu::FilterMode::Linear,
..Default::default()
});
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("ui bind group"),
layout: &bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: screen_uniform_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: wgpu::BindingResource::TextureView(&atlas_view),
},
wgpu::BindGroupEntry {
binding: 2,
resource: wgpu::BindingResource::Sampler(&atlas_sampler),
},
],
});
let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("ui pipeline layout"),
bind_group_layouts: &[Some(&bind_group_layout)],
immediate_size: 0,
});
let unit_quad_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Vertex,
attributes: &wgpu::vertex_attr_array![0 => Float32x2],
};
let instance_layout = wgpu::VertexBufferLayout {
array_stride: std::mem::size_of::<QuadInstance>() as wgpu::BufferAddress,
step_mode: wgpu::VertexStepMode::Instance,
attributes: &wgpu::vertex_attr_array![
1 => Float32x2, 2 => Float32x2, 3 => Float32x4, 4 => Float32x4
],
};
let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
label: Some("ui pipeline"),
layout: Some(&pipeline_layout),
vertex: wgpu::VertexState {
module: &shader,
entry_point: Some("vs_main"),
buffers: &[Some(unit_quad_layout), Some(instance_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 {
topology: wgpu::PrimitiveTopology::TriangleStrip,
strip_index_format: None,
front_face: wgpu::FrontFace::Ccw,
cull_mode: None,
polygon_mode: wgpu::PolygonMode::Fill,
unclipped_depth: false,
conservative: false,
},
depth_stencil: None,
multisample: wgpu::MultisampleState::default(),
multiview_mask: None,
cache: None,
});
let unit_quad_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
label: Some("ui unit quad"),
contents: bytemuck::cast_slice(UNIT_QUAD),
usage: wgpu::BufferUsages::VERTEX,
});
let instance_capacity = 256;
let instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui instances"),
size: (instance_capacity * std::mem::size_of::<QuadInstance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
Self {
pipeline,
unit_quad_buffer,
instance_buffer,
instance_capacity,
screen_uniform_buffer,
bind_group,
atlas,
}
}
pub fn upload_atlas(&self, queue: &wgpu::Queue, atlas: &mut atlas::Atlas) {
let Some(pixels) = atlas.take_if_dirty() else {
return;
};
queue.write_texture(
self.atlas.as_image_copy(),
pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(atlas::ATLAS_SIZE * 4),
rows_per_image: Some(atlas::ATLAS_SIZE),
},
wgpu::Extent3d {
width: atlas::ATLAS_SIZE,
height: atlas::ATLAS_SIZE,
depth_or_array_layers: 1,
},
);
}
fn ensure_capacity(&mut self, device: &wgpu::Device, needed: usize) {
if needed <= self.instance_capacity {
return;
}
self.instance_capacity = needed.next_power_of_two();
self.instance_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("ui instances"),
size: (self.instance_capacity * std::mem::size_of::<QuadInstance>()) as u64,
usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
}
#[allow(clippy::too_many_arguments)]
pub fn render(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
encoder: &mut wgpu::CommandEncoder,
view: &wgpu::TextureView,
screen_width: f32,
screen_height: f32,
quads: &[QuadInstance],
) {
let screen_uniform = ScreenUniform {
size: [screen_width, screen_height],
_pad: [0.0, 0.0],
};
queue.write_buffer(
&self.screen_uniform_buffer,
0,
bytemuck::bytes_of(&screen_uniform),
);
if !quads.is_empty() {
self.ensure_capacity(device, quads.len());
queue.write_buffer(&self.instance_buffer, 0, bytemuck::cast_slice(quads));
}
let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("ui render pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view,
resolve_target: None,
depth_slice: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
if quads.is_empty() {
return;
}
pass.set_pipeline(&self.pipeline);
pass.set_bind_group(0, &self.bind_group, &[]);
pass.set_vertex_buffer(0, self.unit_quad_buffer.slice(..));
pass.set_vertex_buffer(1, self.instance_buffer.slice(..));
pass.draw(0..4, 0..quads.len() as u32);
}
}