1use 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 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 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 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}