Skip to main content

game_toolkit_gfx/
sprite.rs

1use std::collections::HashMap;
2
3use bytemuck::{Pod, Zeroable};
4use wgpu::util::DeviceExt;
5
6use crate::color::Color;
7use crate::target::Targets;
8use crate::texture::{TextureId, TextureRegistry};
9
10#[repr(C)]
11#[derive(Copy, Clone, Pod, Zeroable)]
12struct QuadVertex {
13    pos: [f32; 2],
14    uv: [f32; 2],
15}
16
17const QUAD_VERTS: &[QuadVertex] = &[
18    QuadVertex {
19        pos: [0.0, 0.0],
20        uv: [0.0, 0.0],
21    },
22    QuadVertex {
23        pos: [1.0, 0.0],
24        uv: [1.0, 0.0],
25    },
26    QuadVertex {
27        pos: [1.0, 1.0],
28        uv: [1.0, 1.0],
29    },
30    QuadVertex {
31        pos: [0.0, 1.0],
32        uv: [0.0, 1.0],
33    },
34];
35const QUAD_INDICES: &[u16] = &[0, 1, 2, 0, 2, 3];
36
37#[repr(C)]
38#[derive(Copy, Clone, Pod, Zeroable, Debug)]
39pub struct SpriteInstance {
40    pub pos: [f32; 2],
41    pub size: [f32; 2],
42    pub uv_min: [f32; 2],
43    pub uv_max: [f32; 2],
44    pub color: [f32; 4],
45    pub rotation: f32,
46    /// Padding to a 16-byte multiple. `_pad[0]` doubles as the CPU-side marker for
47    /// [`SpriteInstance::with_pixel_region`]; leave it zeroed.
48    pub _pad: [f32; 3],
49}
50
51/// `_pad[0]` value marking `uv_min` / `uv_max` as pixels still to be divided by the texture size.
52const PIXEL_REGION: f32 = 1.0;
53
54impl SpriteInstance {
55    pub fn at(pos: [f32; 2], size: [f32; 2]) -> Self {
56        Self {
57            pos,
58            size,
59            uv_min: [0.0, 0.0],
60            uv_max: [1.0, 1.0],
61            color: [1.0; 4],
62            rotation: 0.0,
63            _pad: [0.0; 3],
64        }
65    }
66    pub fn with_color(mut self, c: impl Into<Color>) -> Self {
67        self.color = c.into().to_array();
68        self
69    }
70    pub fn with_rotation(mut self, r: f32) -> Self {
71        self.rotation = r;
72        self
73    }
74    /// Sample the UV rectangle `min..max` (`[0.0, 0.0]..[1.0, 1.0]` is the whole texture).
75    /// Beyond `0..1` the texture tiles only if it was set to [`crate::TextureWrap::Repeat`]
76    /// (or `MirrorRepeat`) with `Graphics::set_texture_wrap`; by default it clamps to the
77    /// edge texels.
78    pub fn with_uv(mut self, min: [f32; 2], max: [f32; 2]) -> Self {
79        self.uv_min = min;
80        self.uv_max = max;
81        self
82    }
83    /// Sample the `size` pixel rectangle at `min` (top-left, in pixels) of a texture that is
84    /// `tex_size` pixels large, e.g. one sprite of an atlas. Get `tex_size` from
85    /// `Graphics::texture_size` / `Painter::texture_size`, which never reports a zero side;
86    /// a zero `tex_size` side would produce non-finite UVs and is a debug-build panic.
87    pub fn with_region(self, min: [f32; 2], size: [f32; 2], tex_size: [u32; 2]) -> Self {
88        debug_assert!(
89            tex_size[0] > 0 && tex_size[1] > 0,
90            "with_region: zero-sized texture {tex_size:?}"
91        );
92        let [tw, th] = [tex_size[0] as f32, tex_size[1] as f32];
93        self.with_uv(
94            [min[0] / tw, min[1] / th],
95            [(min[0] + size[0]) / tw, (min[1] + size[1]) / th],
96        )
97    }
98    /// Sample the `size` pixel rectangle at `min` (top-left, in pixels) of whatever texture
99    /// this is drawn with, like [`with_region`](Self::with_region) but without passing the
100    /// texture size: [`crate::Painter::sprite_ex`] looks it up when the sprite is drawn, and
101    /// draws nothing for an unknown texture. Combine freely with [`flip_x`](Self::flip_x) /
102    /// [`flip_y`](Self::flip_y), and draw the instance only through `sprite_ex`.
103    pub fn with_pixel_region(mut self, min: [f32; 2], size: [f32; 2]) -> Self {
104        self.uv_min = min;
105        self.uv_max = [min[0] + size[0], min[1] + size[1]];
106        self._pad[0] = PIXEL_REGION;
107        self
108    }
109
110    /// Whether the UVs are a [`with_pixel_region`](Self::with_pixel_region) still in pixels.
111    pub(crate) fn has_pixel_region(&self) -> bool {
112        self._pad[0] == PIXEL_REGION
113    }
114
115    /// Turn a pixel region into UVs for a texture of `tex_size` pixels.
116    pub(crate) fn resolve_pixel_region(mut self, tex_size: [u32; 2]) -> Self {
117        let [tw, th] = [tex_size[0].max(1) as f32, tex_size[1].max(1) as f32];
118        self.uv_min = [self.uv_min[0] / tw, self.uv_min[1] / th];
119        self.uv_max = [self.uv_max[0] / tw, self.uv_max[1] / th];
120        self._pad[0] = 0.0;
121        self
122    }
123
124    /// Mirror horizontally by swapping the u bounds. Works with any UV range or region;
125    /// apply it after [`with_uv`](Self::with_uv) / [`with_region`](Self::with_region).
126    pub fn flip_x(mut self) -> Self {
127        std::mem::swap(&mut self.uv_min[0], &mut self.uv_max[0]);
128        self
129    }
130    /// Mirror vertically by swapping the v bounds. Works with any UV range or region;
131    /// apply it after [`with_uv`](Self::with_uv) / [`with_region`](Self::with_region).
132    pub fn flip_y(mut self) -> Self {
133        std::mem::swap(&mut self.uv_min[1], &mut self.uv_max[1]);
134        self
135    }
136}
137
138#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
139pub enum BlendMode {
140    Alpha,
141    Additive,
142    Premultiplied,
143}
144
145impl BlendMode {
146    pub(crate) const ALL: [Self; 3] = [Self::Alpha, Self::Additive, Self::Premultiplied];
147
148    /// The wgpu blend state every batcher uses for this mode.
149    pub(crate) fn state(self) -> wgpu::BlendState {
150        match self {
151            Self::Alpha => wgpu::BlendState::ALPHA_BLENDING,
152            Self::Premultiplied => wgpu::BlendState::PREMULTIPLIED_ALPHA_BLENDING,
153            Self::Additive => wgpu::BlendState {
154                color: wgpu::BlendComponent {
155                    src_factor: wgpu::BlendFactor::SrcAlpha,
156                    dst_factor: wgpu::BlendFactor::One,
157                    operation: wgpu::BlendOperation::Add,
158                },
159                alpha: wgpu::BlendComponent::OVER,
160            },
161        }
162    }
163
164    pub(crate) fn label(self) -> &'static str {
165        match self {
166            Self::Alpha => "alpha",
167            Self::Additive => "add",
168            Self::Premultiplied => "premul",
169        }
170    }
171}
172
173#[derive(Copy, Clone, PartialEq, Eq, Hash)]
174struct BatchKey {
175    texture: TextureId,
176    layer: i16,
177    blend: BlendMode,
178}
179
180pub(crate) struct SpriteBatcher {
181    quad_vb: wgpu::Buffer,
182    quad_ib: wgpu::Buffer,
183    instance_vb: wgpu::Buffer,
184    instance_capacity: usize,
185    pipelines: HashMap<BlendMode, wgpu::RenderPipeline>,
186    pending: Vec<(BatchKey, SpriteInstance)>,
187}
188
189impl SpriteBatcher {
190    pub fn new(
191        device: &wgpu::Device,
192        surface_format: wgpu::TextureFormat,
193        camera_bgl: &wgpu::BindGroupLayout,
194        texture_bgl: &wgpu::BindGroupLayout,
195        sample_count: u32,
196        depth_format: Option<wgpu::TextureFormat>,
197    ) -> Self {
198        let multisample = crate::target::multisample(sample_count);
199        let depth_stencil = depth_format.map(crate::target::no_write_depth);
200        let quad_vb = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
201            label: Some("sprite.quad_vb"),
202            contents: bytemuck::cast_slice(QUAD_VERTS),
203            usage: wgpu::BufferUsages::VERTEX,
204        });
205        let quad_ib = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
206            label: Some("sprite.quad_ib"),
207            contents: bytemuck::cast_slice(QUAD_INDICES),
208            usage: wgpu::BufferUsages::INDEX,
209        });
210
211        let instance_capacity = 4096usize;
212        let instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
213            label: Some("sprite.instances"),
214            size: (instance_capacity * size_of::<SpriteInstance>()) as u64,
215            usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
216            mapped_at_creation: false,
217        });
218
219        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
220            label: Some("sprite.shader"),
221            source: wgpu::ShaderSource::Wgsl(
222                concat!(include_str!("srgb.wgsl"), include_str!("sprite.wgsl")).into(),
223            ),
224        });
225
226        let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
227            label: Some("sprite.layout"),
228            bind_group_layouts: &[Some(camera_bgl), Some(texture_bgl)],
229            immediate_size: 0,
230        });
231
232        let make_pipeline = |blend: wgpu::BlendState, label: &str| {
233            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
234                label: Some(label),
235                layout: Some(&layout),
236                vertex: wgpu::VertexState {
237                    module: &shader,
238                    entry_point: Some("vs_main"),
239                    compilation_options: Default::default(),
240                    buffers: &[
241                        Some(wgpu::VertexBufferLayout {
242                            array_stride: size_of::<QuadVertex>() as u64,
243                            step_mode: wgpu::VertexStepMode::Vertex,
244                            attributes: &wgpu::vertex_attr_array![0 => Float32x2, 1 => Float32x2],
245                        }),
246                        Some(wgpu::VertexBufferLayout {
247                            array_stride: size_of::<SpriteInstance>() as u64,
248                            step_mode: wgpu::VertexStepMode::Instance,
249                            attributes: &wgpu::vertex_attr_array![
250                                2 => Float32x2,
251                                3 => Float32x2,
252                                4 => Float32x2,
253                                5 => Float32x2,
254                                6 => Float32x4,
255                                7 => Float32,
256                            ],
257                        }),
258                    ],
259                },
260                fragment: Some(wgpu::FragmentState {
261                    module: &shader,
262                    entry_point: Some("fs_main"),
263                    compilation_options: Default::default(),
264                    targets: &[Some(wgpu::ColorTargetState {
265                        format: surface_format,
266                        blend: Some(blend),
267                        write_mask: wgpu::ColorWrites::ALL,
268                    })],
269                }),
270                primitive: wgpu::PrimitiveState::default(),
271                depth_stencil: depth_stencil.clone(),
272                multisample,
273                multiview_mask: None,
274                cache: None,
275            })
276        };
277
278        let pipelines = BlendMode::ALL
279            .into_iter()
280            .map(|blend| {
281                let label = format!("sprite.{}", blend.label());
282                (blend, make_pipeline(blend.state(), &label))
283            })
284            .collect();
285
286        Self {
287            quad_vb,
288            quad_ib,
289            instance_vb,
290            instance_capacity,
291            pipelines,
292            pending: Vec::new(),
293        }
294    }
295
296    pub fn draw(&mut self, tex: TextureId, layer: i16, blend: BlendMode, inst: SpriteInstance) {
297        self.pending.push((
298            BatchKey {
299                texture: tex,
300                layer,
301                blend,
302            },
303            inst,
304        ));
305    }
306
307    /// Record every layer that has pending sprites, for cross-batcher interleaving.
308    pub fn collect_layers(&self, out: &mut std::collections::BTreeSet<i16>) {
309        out.extend(self.pending.iter().map(|(k, _)| k.layer));
310    }
311
312    /// Sort all pending sprites by layer (then blend, then texture) and upload them to the
313    /// instance buffer in one write. Must run before any [`SpriteBatcher::draw_layer`]: the
314    /// buffer is written once per frame because `queue.write_buffer` does not interleave with
315    /// encoder passes, so a per-pass write would clobber the earlier layers' data.
316    pub fn upload(&mut self, device: &wgpu::Device, queue: &wgpu::Queue) {
317        if self.pending.is_empty() {
318            return;
319        }
320        self.pending.sort_by(|a, b| {
321            a.0.layer
322                .cmp(&b.0.layer)
323                .then((a.0.blend as u8).cmp(&(b.0.blend as u8)))
324                .then(a.0.texture.0.cmp(&b.0.texture.0))
325        });
326        if self.pending.len() > self.instance_capacity {
327            self.instance_capacity = self.pending.len().next_power_of_two();
328            self.instance_vb = device.create_buffer(&wgpu::BufferDescriptor {
329                label: Some("sprite.instances"),
330                size: (self.instance_capacity * size_of::<SpriteInstance>()) as u64,
331                usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
332                mapped_at_creation: false,
333            });
334        }
335        let flat: Vec<SpriteInstance> = self.pending.iter().map(|(_, i)| *i).collect();
336        queue.write_buffer(&self.instance_vb, 0, bytemuck::cast_slice(&flat));
337    }
338
339    /// Draw the sprites on `layer` (already uploaded by [`SpriteBatcher::upload`]) into the
340    /// already-cleared target, grouped by blend then texture to minimize state changes.
341    pub fn draw_layer(
342        &self,
343        layer: i16,
344        encoder: &mut wgpu::CommandEncoder,
345        targets: &Targets<'_>,
346        camera_bg: &wgpu::BindGroup,
347        textures: &TextureRegistry,
348    ) {
349        // `pending` is sorted by layer, so this layer's sprites are a contiguous range.
350        let lo = self.pending.partition_point(|(k, _)| k.layer < layer);
351        let hi = self.pending.partition_point(|(k, _)| k.layer <= layer);
352        if lo == hi {
353            return;
354        }
355
356        let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
357            label: Some("sprite.pass"),
358            color_attachments: &[Some(targets.color_attachment(wgpu::LoadOp::Load))],
359            depth_stencil_attachment: targets.depth_attachment(wgpu::LoadOp::Load),
360            occlusion_query_set: None,
361            timestamp_writes: None,
362            multiview_mask: None,
363        });
364
365        pass.set_bind_group(0, camera_bg, &[]);
366        pass.set_vertex_buffer(0, self.quad_vb.slice(..));
367        pass.set_index_buffer(self.quad_ib.slice(..), wgpu::IndexFormat::Uint16);
368        pass.set_vertex_buffer(1, self.instance_vb.slice(..));
369
370        let mut i = lo;
371        while i < hi {
372            let key = self.pending[i].0;
373            let start = i;
374            while i < hi
375                && self.pending[i].0.blend == key.blend
376                && self.pending[i].0.texture == key.texture
377            {
378                i += 1;
379            }
380            let count = (i - start) as u32;
381            pass.set_pipeline(&self.pipelines[&key.blend]);
382            pass.set_bind_group(1, textures.bind_group(key.texture), &[]);
383            pass.draw_indexed(0..6, 0, (start as u32)..(start as u32 + count));
384        }
385    }
386
387    pub fn clear(&mut self) {
388        self.pending.clear();
389    }
390}
391
392#[cfg(test)]
393mod tests {
394    use super::*;
395
396    #[test]
397    fn pixel_region_resolves_like_with_region() {
398        let tex = [64, 32];
399        let px = SpriteInstance::at([0.0; 2], [8.0; 2]).with_pixel_region([16.0, 8.0], [16.0, 8.0]);
400        assert!(px.has_pixel_region());
401        let resolved = px.resolve_pixel_region(tex);
402        let direct =
403            SpriteInstance::at([0.0; 2], [8.0; 2]).with_region([16.0, 8.0], [16.0, 8.0], tex);
404        assert!(!resolved.has_pixel_region());
405        assert_eq!(resolved.uv_min, direct.uv_min);
406        assert_eq!(resolved.uv_max, direct.uv_max);
407    }
408
409    #[test]
410    fn flip_before_resolving_a_pixel_region() {
411        let inst = SpriteInstance::at([0.0; 2], [8.0; 2])
412            .with_pixel_region([0.0, 0.0], [16.0, 16.0])
413            .flip_x()
414            .resolve_pixel_region([32, 32]);
415        assert_eq!(inst.uv_min, [0.5, 0.0]);
416        assert_eq!(inst.uv_max, [0.0, 0.5]);
417    }
418
419    #[test]
420    fn with_region_maps_pixels_to_uvs() {
421        let inst = SpriteInstance::at([0.0, 0.0], [32.0, 16.0]).with_region(
422            [64.0, 32.0],
423            [32.0, 16.0],
424            [256, 128],
425        );
426        assert_eq!(inst.uv_min, [0.25, 0.25]);
427        assert_eq!(inst.uv_max, [0.375, 0.375]);
428    }
429
430    #[test]
431    #[cfg(debug_assertions)]
432    #[should_panic(expected = "zero-sized texture")]
433    fn with_region_rejects_a_zero_sized_texture_in_debug_builds() {
434        let _ =
435            SpriteInstance::at([0.0, 0.0], [1.0, 1.0]).with_region([0.0, 0.0], [1.0, 1.0], [0, 64]);
436    }
437
438    #[test]
439    fn flip_x_swaps_u_bounds_only() {
440        let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0])
441            .with_uv([0.1, 0.2], [0.3, 0.4])
442            .flip_x();
443        assert_eq!(inst.uv_min, [0.3, 0.2]);
444        assert_eq!(inst.uv_max, [0.1, 0.4]);
445    }
446
447    #[test]
448    fn flip_y_swaps_v_bounds_only() {
449        let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0])
450            .with_uv([0.1, 0.2], [0.3, 0.4])
451            .flip_y();
452        assert_eq!(inst.uv_min, [0.1, 0.4]);
453        assert_eq!(inst.uv_max, [0.3, 0.2]);
454    }
455
456    #[test]
457    fn flipping_twice_restores_the_region() {
458        let inst = SpriteInstance::at([0.0, 0.0], [1.0, 1.0]).with_region(
459            [8.0, 8.0],
460            [16.0, 16.0],
461            [64, 64],
462        );
463        let back = inst.flip_x().flip_y().flip_x().flip_y();
464        assert_eq!(back.uv_min, inst.uv_min);
465        assert_eq!(back.uv_max, inst.uv_max);
466    }
467}