Skip to main content

game_toolkit_gfx/
primitives.rs

1use bytemuck::{Pod, Zeroable};
2use wgpu::util::DeviceExt;
3
4use crate::target::Targets;
5
6#[repr(C)]
7#[derive(Copy, Clone, Pod, Zeroable)]
8struct PrimVertex {
9    pos: [f32; 2],
10}
11
12const QUAD_VERTS: &[PrimVertex] = &[
13    PrimVertex { pos: [0.0, 0.0] },
14    PrimVertex { pos: [1.0, 0.0] },
15    PrimVertex { pos: [1.0, 1.0] },
16    PrimVertex { pos: [0.0, 1.0] },
17];
18const QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
19
20#[repr(C)]
21#[derive(Copy, Clone, Pod, Zeroable, Debug)]
22pub struct CircleInstance {
23    pub center: [f32; 2],
24    pub radius: f32,
25    /// `0.0` = filled disk; `>0` = ring of that pixel thickness (outer = radius).
26    pub thickness: f32,
27    pub color: [f32; 4],
28}
29
30pub(crate) struct PrimitiveBatcher {
31    quad_vb: wgpu::Buffer,
32    quad_ib: wgpu::Buffer,
33    instance_vb: wgpu::Buffer,
34    capacity: usize,
35    pipeline: wgpu::RenderPipeline,
36    pending: Vec<(i16, CircleInstance)>,
37}
38
39impl PrimitiveBatcher {
40    pub fn new(
41        device: &wgpu::Device,
42        surface_format: wgpu::TextureFormat,
43        camera_bgl: &wgpu::BindGroupLayout,
44        sample_count: u32,
45        depth_format: Option<wgpu::TextureFormat>,
46    ) -> Self {
47        let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
48            label: Some("prim.quad_vb"),
49            contents: bytemuck::cast_slice(QUAD_VERTS),
50            usage: wgpu::BufferUsages::VERTEX,
51        });
52        let quad_ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
53            label: Some("prim.quad_ib"),
54            contents: bytemuck::cast_slice(QUAD_INDICES),
55            usage: wgpu::BufferUsages::INDEX,
56        });
57        let capacity = 1024usize;
58        let instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
59            label: Some("prim.circle_instances"),
60            size: (capacity * size_of::<CircleInstance>()) as u64,
61            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
62            mapped_at_creation: false,
63        });
64
65        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
66            label: Some("prim.circle.shader"),
67            source: wgpu::ShaderSource::Wgsl(
68                concat!(include_str!("srgb.wgsl"), include_str!("circle.wgsl")).into(),
69            ),
70        });
71        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
72            label: Some("prim.layout"),
73            bind_group_layouts: &[Some(camera_bgl)],
74            immediate_size: 0,
75        });
76
77        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
78            label: Some("prim.circle.pipeline"),
79            layout: Some(&layout),
80            vertex: wgpu::VertexState {
81                module: &shader,
82                entry_point: Some("vs_main"),
83                compilation_options: Default::default(),
84                buffers: &[
85                    Some(wgpu::VertexBufferLayout {
86                        array_stride: size_of::<PrimVertex>() as u64,
87                        step_mode: wgpu::VertexStepMode::Vertex,
88                        attributes: &wgpu::vertex_attr_array![0 => Float32x2],
89                    }),
90                    Some(wgpu::VertexBufferLayout {
91                        array_stride: size_of::<CircleInstance>() as u64,
92                        step_mode: wgpu::VertexStepMode::Instance,
93                        attributes: &wgpu::vertex_attr_array![
94                            2 => Float32x2,
95                            3 => Float32,
96                            4 => Float32,
97                            5 => Float32x4,
98                        ],
99                    }),
100                ],
101            },
102            fragment: Some(wgpu::FragmentState {
103                module: &shader,
104                entry_point: Some("fs_main"),
105                compilation_options: Default::default(),
106                targets: &[Some(wgpu::ColorTargetState {
107                    format: surface_format,
108                    blend: Some(wgpu::BlendState::ALPHA_BLENDING),
109                    write_mask: wgpu::ColorWrites::ALL,
110                })],
111            }),
112            primitive: wgpu::PrimitiveState::default(),
113            depth_stencil: depth_format.map(crate::target::no_write_depth),
114            multisample: crate::target::multisample(sample_count),
115            multiview_mask: None,
116            cache: None,
117        });
118
119        Self {
120            quad_vb,
121            quad_ib,
122            instance_vb,
123            capacity,
124            pipeline,
125            pending: Vec::new(),
126        }
127    }
128
129    pub fn push(&mut self, layer: i16, inst: CircleInstance) {
130        self.pending.push((layer, inst));
131    }
132
133    /// Record every layer that has pending circles, for cross-batcher interleaving.
134    pub fn collect_layers(&self, out: &mut std::collections::BTreeSet<i16>) {
135        out.extend(self.pending.iter().map(|(layer, _)| *layer));
136    }
137
138    /// Sort all pending circles by layer and upload them in one write. Must run before any
139    /// [`PrimitiveBatcher::draw_layer`]; see [`SpriteBatcher::upload`] for why the buffer is
140    /// written exactly once per frame rather than per layer pass.
141    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
142        if self.pending.is_empty() {
143            return;
144        }
145        self.pending.sort_by_key(|(layer, _)| *layer);
146        if self.pending.len() > self.capacity {
147            self.capacity = self.pending.len().next_power_of_two();
148            self.instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
149                label: Some("prim.circle_instances"),
150                size: (self.capacity * size_of::<CircleInstance>()) as u64,
151                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
152                mapped_at_creation: false,
153            });
154        }
155        let flat: Vec<CircleInstance> = self.pending.iter().map(|(_, c)| *c).collect();
156        queue.write_buffer(&self.instance_vb, 0, bytemuck::cast_slice(&flat));
157    }
158
159    /// Draw the circles on `layer` (already uploaded by [`PrimitiveBatcher::upload`]) into the
160    /// already-cleared target.
161    pub fn draw_layer(
162        &self,
163        layer: i16,
164        encoder: &mut wgpu::CommandEncoder,
165        targets: &Targets<'_>,
166        camera_bg: &wgpu::BindGroup,
167    ) {
168        // `pending` is sorted by layer, so this layer's circles are a contiguous range.
169        let lo = self.pending.partition_point(|(l, _)| *l < layer);
170        let hi = self.pending.partition_point(|(l, _)| *l <= layer);
171        if lo == hi {
172            return;
173        }
174
175        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
176            label: Some("prim.circle.pass"),
177            color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Load))],
178            depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Load),
179            occlusion_query_set: None,
180            timestamp_writes: None,
181            multiview_mask: None,
182        });
183        pass.set_pipeline(&self.pipeline);
184        pass.set_bind_group(0, camera_bg, &[]);
185        pass.set_vertex_buffer(0, self.quad_vb.slice(..));
186        pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
187        pass.set_vertex_buffer(1, self.instance_vb.slice(..));
188        pass.draw_indexed(0..6, 0, lo as u32..hi as u32);
189    }
190
191    pub fn clear(&mut self) {
192        self.pending.clear();
193    }
194}