Skip to main content

wisp/render/
sprite_pipeline.rs

1//! Instanced sprite pipeline — M0.9.
2//!
3//! Per-instance attributes: 4×4 model matrix (4 × `Float32x4`), tint
4//! (`Float32x4`), anchor (`Float32x2`). Six vertices per instance form a
5//! unit-quad in `[0, 1]²`; the vertex shader subtracts `anchor` then applies
6//! the model.
7
8use std::collections::{HashMap, HashSet};
9
10use bytemuck::{Pod, Zeroable};
11use glam::Mat4;
12use wgpu::util::DeviceExt;
13
14use crate::application::Application;
15use crate::blend::BlendMode;
16use crate::color::Color;
17use crate::render::blend_pipeline::BlendPipelineMap;
18use crate::render::scene_walk::walk_visible_subtree;
19use crate::scene::{Node, NodeId, Sprite, Stage};
20use crate::texture::Texture;
21
22#[repr(C)]
23#[derive(Clone, Copy, Pod, Zeroable)]
24pub(crate) struct SpriteInstance {
25    pub model: [[f32; 4]; 4],
26    pub tint: [f32; 4],
27    pub anchor: [f32; 2],
28    pub _padding: [f32; 2],
29}
30
31const ATTR_LAYOUT: [wgpu::VertexAttribute; 6] = wgpu::vertex_attr_array![
32    0 => Float32x4,
33    1 => Float32x4,
34    2 => Float32x4,
35    3 => Float32x4,
36    4 => Float32x4,
37    5 => Float32x2,
38];
39
40pub(crate) struct SpritePipeline {
41    pipelines: BlendPipelineMap,
42    texture_layout: wgpu::BindGroupLayout,
43}
44
45impl SpritePipeline {
46    pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
47        let device = app.device();
48        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
49            label: Some("wisp::sprite"),
50            source: wgpu::ShaderSource::Wgsl(include_str!("../../shaders/sprite.wgsl").into()),
51        });
52
53        let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
54            label: Some("wisp::sprite texture layout"),
55            entries: &[
56                wgpu::BindGroupLayoutEntry {
57                    binding: 0,
58                    visibility: wgpu::ShaderStages::FRAGMENT,
59                    ty: wgpu::BindingType::Texture {
60                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
61                        view_dimension: wgpu::TextureViewDimension::D2,
62                        multisampled: false,
63                    },
64                    count: None,
65                },
66                wgpu::BindGroupLayoutEntry {
67                    binding: 1,
68                    visibility: wgpu::ShaderStages::FRAGMENT,
69                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
70                    count: None,
71                },
72            ],
73        });
74
75        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
76            label: Some("wisp::sprite pipeline layout"),
77            bind_group_layouts: &[&texture_layout],
78            push_constant_ranges: &[],
79        });
80
81        // Build one pipeline per native blend mode — eight in total.
82        // The vertex/fragment/layout setup is shared; only the
83        // `BlendState` differs.
84        let pipelines = BlendPipelineMap::new(|blend| {
85            device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
86                label: Some("wisp::sprite pipeline"),
87                layout: Some(&pipeline_layout),
88                vertex: wgpu::VertexState {
89                    module: &shader,
90                    entry_point: Some("main_vs"),
91                    buffers: &[wgpu::VertexBufferLayout {
92                        array_stride: std::mem::size_of::<SpriteInstance>() as wgpu::BufferAddress,
93                        step_mode: wgpu::VertexStepMode::Instance,
94                        attributes: &ATTR_LAYOUT,
95                    }],
96                    compilation_options: wgpu::PipelineCompilationOptions::default(),
97                },
98                fragment: Some(wgpu::FragmentState {
99                    module: &shader,
100                    entry_point: Some("main_fs"),
101                    targets: &[Some(wgpu::ColorTargetState {
102                        format: output_format,
103                        blend: Some(blend),
104                        write_mask: wgpu::ColorWrites::ALL,
105                    })],
106                    compilation_options: wgpu::PipelineCompilationOptions::default(),
107                }),
108                primitive: wgpu::PrimitiveState::default(),
109                depth_stencil: None,
110                multisample: wgpu::MultisampleState::default(),
111                multiview: None,
112                cache: None,
113            })
114        });
115
116        Self {
117            pipelines,
118            texture_layout,
119        }
120    }
121
122    /// Traverse `stage` in pre-order, accumulate sprite instances grouped by
123    /// `(texture id, blend mode)`, then draw one batch per group.
124    ///
125    /// Returns the number of draw calls emitted and total sprites rendered.
126    pub(crate) fn draw_stage(
127        &self,
128        app: &Application,
129        pass: &mut wgpu::RenderPass<'_>,
130        stage: &Stage,
131    ) -> (u32, u32) {
132        self.draw_subtree(app, pass, stage, stage.root(), &HashSet::new())
133    }
134
135    /// Render only the subtree rooted at `start`. Nodes whose IDs are
136    /// in `exclude` (and their descendants) are skipped — used by the
137    /// auto-dispatch advanced-blend renderer to walk the scene "minus"
138    /// the advanced-blend subtrees.
139    pub(crate) fn draw_subtree(
140        &self,
141        app: &Application,
142        pass: &mut wgpu::RenderPass<'_>,
143        stage: &Stage,
144        start: NodeId,
145        exclude: &HashSet<NodeId>,
146    ) -> (u32, u32) {
147        let batches = collect_batches(stage, start, exclude);
148        let mut draw_calls = 0u32;
149        let mut sprites_drawn = 0u32;
150
151        for batch in &batches {
152            let count = u32::try_from(batch.instances.len()).expect("instance count fits in u32");
153            sprites_drawn = sprites_drawn.saturating_add(count);
154            let buffer = app
155                .device()
156                .create_buffer_init(&wgpu::util::BufferInitDescriptor {
157                    label: Some("wisp::sprite instances"),
158                    contents: bytemuck::cast_slice(&batch.instances),
159                    usage: wgpu::BufferUsages::VERTEX,
160                });
161
162            let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
163                label: Some("wisp::sprite texture bg"),
164                layout: &self.texture_layout,
165                entries: &[
166                    wgpu::BindGroupEntry {
167                        binding: 0,
168                        resource: wgpu::BindingResource::TextureView(batch.texture.view()),
169                    },
170                    wgpu::BindGroupEntry {
171                        binding: 1,
172                        resource: wgpu::BindingResource::Sampler(batch.texture.sampler()),
173                    },
174                ],
175            });
176
177            pass.set_pipeline(self.pipelines.get(batch.blend_mode));
178            pass.set_bind_group(0, &bg, &[]);
179            pass.set_vertex_buffer(0, buffer.slice(..));
180            pass.draw(0..6, 0..count);
181            draw_calls += 1;
182        }
183
184        (draw_calls, sprites_drawn)
185    }
186}
187
188struct Batch {
189    texture: Texture,
190    blend_mode: BlendMode,
191    instances: Vec<SpriteInstance>,
192}
193
194fn collect_batches(stage: &Stage, start: NodeId, exclude: &HashSet<NodeId>) -> Vec<Batch> {
195    type Key = (usize, BlendMode);
196    let mut grouped: HashMap<Key, (Texture, BlendMode, Vec<SpriteInstance>)> = HashMap::new();
197    let mut order: Vec<Key> = Vec::new();
198
199    walk_visible_subtree(stage, start, exclude, |_id, node, world| {
200        if let Node::Sprite(sprite) = node {
201            push_sprite_instance(sprite, world, &mut grouped, &mut order);
202        }
203    });
204
205    order
206        .into_iter()
207        .filter_map(|key| {
208            grouped
209                .remove(&key)
210                .map(|(texture, blend_mode, instances)| Batch {
211                    texture,
212                    blend_mode,
213                    instances,
214                })
215        })
216        .collect()
217}
218
219fn push_sprite_instance(
220    sprite: &Sprite,
221    world: Mat4,
222    grouped: &mut HashMap<(usize, BlendMode), (Texture, BlendMode, Vec<SpriteInstance>)>,
223    order: &mut Vec<(usize, BlendMode)>,
224) {
225    let key = (sprite.texture.id(), sprite.container.blend_mode);
226    let alpha_tint = with_premultiplied_alpha(sprite.tint, sprite.container.alpha);
227    let entry = grouped.entry(key).or_insert_with(|| {
228        order.push(key);
229        (
230            sprite.texture.clone(),
231            sprite.container.blend_mode,
232            Vec::new(),
233        )
234    });
235    entry.2.push(SpriteInstance {
236        model: world.to_cols_array_2d(),
237        tint: [alpha_tint.r, alpha_tint.g, alpha_tint.b, alpha_tint.a],
238        anchor: [sprite.anchor.x, sprite.anchor.y],
239        _padding: [0.0, 0.0],
240    });
241}
242
243fn with_premultiplied_alpha(tint: Color, alpha: f32) -> Color {
244    Color {
245        r: tint.r,
246        g: tint.g,
247        b: tint.b,
248        a: tint.a * alpha,
249    }
250}