1use std::path::PathBuf;
19use std::sync::Arc;
20
21use crate::pipeline_helpers::{blend_state_for, load_wgsl};
22
23pub const MAX_ACTIVE_SPRITES: usize = 64;
27
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
32pub enum SpriteBlendKind {
33 Alpha,
34 Additive,
35}
36
37#[repr(C)]
41#[derive(Debug, Clone, Copy, Default)]
42pub struct SpriteUniform {
43 center_size: [f32; 4],
48 rot_pad: [f32; 4],
50 rgba: [f32; 4],
52}
53
54unsafe impl bytemuck::Pod for SpriteUniform {}
56unsafe impl bytemuck::Zeroable for SpriteUniform {}
57
58#[derive(Debug, Clone, Copy)]
63pub struct SpriteFrame {
64 pub texture_index: u32,
65 pub x: f32,
66 pub y: f32,
67 pub sx: f32,
68 pub sy: f32,
69 pub rot: f32,
70 pub rgba: [f32; 4],
71 pub blend: SpriteBlendKind,
72 pub burn: bool,
73}
74
75pub struct SpriteDrawCmd<'a> {
78 pub uniform: SpriteUniform,
79 pub texture_view: &'a wgpu::TextureView,
80 pub blend: SpriteBlendKind,
81 pub burn: bool,
87}
88
89pub struct SpriteTexture {
91 pub texture: wgpu::Texture,
92 pub view: wgpu::TextureView,
93 pub width: u32,
94 pub height: u32,
95 pub name: String,
98}
99
100pub struct SpritePool {
105 textures: Vec<Arc<SpriteTexture>>,
106 fallback: Arc<SpriteTexture>,
107 sampler: wgpu::Sampler,
108}
109
110impl SpritePool {
111 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
115 let fallback = Arc::new(make_fallback(device, queue));
116 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
117 label: Some("Sprite Sampler"),
118 address_mode_u: wgpu::AddressMode::ClampToEdge,
119 address_mode_v: wgpu::AddressMode::ClampToEdge,
120 mag_filter: wgpu::FilterMode::Linear,
121 min_filter: wgpu::FilterMode::Linear,
122 ..Default::default()
123 });
124 Self {
125 textures: Vec::new(),
126 fallback,
127 sampler,
128 }
129 }
130
131 pub fn sampler(&self) -> &wgpu::Sampler {
134 &self.sampler
135 }
136
137 pub fn len(&self) -> usize {
139 self.textures.len()
140 }
141
142 pub fn is_empty(&self) -> bool {
143 self.textures.is_empty()
144 }
145
146 pub fn get_or_fallback(&self, idx: u32) -> &Arc<SpriteTexture> {
150 self.textures.get(idx as usize).unwrap_or(&self.fallback)
151 }
152
153 pub fn load_from_defs(
162 &mut self,
163 device: &wgpu::Device,
164 queue: &wgpu::Queue,
165 dir: &std::path::Path,
166 def_imgs: &[String],
167 ) {
168 self.textures.clear();
169 for img in def_imgs {
170 let path = dir.join(img);
171 match load_sprite_file(device, queue, &path) {
172 Ok(tex) => self.textures.push(Arc::new(tex)),
173 Err(e) => {
174 log::warn!(
175 "sprite texture {} unloadable ({}); using fallback",
176 path.display(),
177 e
178 );
179 self.textures.push(Arc::clone(&self.fallback));
180 }
181 }
182 }
183 }
184}
185
186pub fn default_sprite_dirs() -> Vec<PathBuf> {
189 let mut out = Vec::new();
190 if let Some(home) = std::env::var_os("HOME") {
191 let home = PathBuf::from(home);
192 out.push(home.join(".local/share/onedrop/sprites"));
193 out.push(home.join(".config/onedrop/sprites"));
194 out.push(home.join("Music/milkdrop/sprites"));
195 }
196 if let Some(xdg) = std::env::var_os("XDG_DATA_HOME") {
197 out.push(PathBuf::from(xdg).join("onedrop/sprites"));
198 }
199 out
200}
201
202pub fn pick_first_existing(dirs: &[PathBuf]) -> Option<PathBuf> {
204 dirs.iter().find(|p| p.is_dir()).cloned()
205}
206
207pub struct SpritePipeline {
211 pipeline_alpha: wgpu::RenderPipeline,
212 pipeline_additive: wgpu::RenderPipeline,
213 bgl: wgpu::BindGroupLayout,
214 uniform_buffer: wgpu::Buffer,
219 uniform_stride: u64,
220}
221
222impl SpritePipeline {
223 pub fn new(device: &wgpu::Device, format: wgpu::TextureFormat) -> Self {
224 let shader = load_wgsl(
225 device,
226 "Sprite Shader",
227 include_str!("../shaders/sprite.wgsl"),
228 );
229
230 let bgl = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
231 label: Some("Sprite BGL"),
232 entries: &[
233 wgpu::BindGroupLayoutEntry {
234 binding: 0,
235 visibility: wgpu::ShaderStages::VERTEX_FRAGMENT,
236 ty: wgpu::BindingType::Buffer {
237 ty: wgpu::BufferBindingType::Uniform,
238 has_dynamic_offset: true,
239 min_binding_size: wgpu::BufferSize::new(
240 std::mem::size_of::<SpriteUniform>() as u64,
241 ),
242 },
243 count: None,
244 },
245 wgpu::BindGroupLayoutEntry {
246 binding: 1,
247 visibility: wgpu::ShaderStages::FRAGMENT,
248 ty: wgpu::BindingType::Texture {
249 sample_type: wgpu::TextureSampleType::Float { filterable: true },
250 view_dimension: wgpu::TextureViewDimension::D2,
251 multisampled: false,
252 },
253 count: None,
254 },
255 wgpu::BindGroupLayoutEntry {
256 binding: 2,
257 visibility: wgpu::ShaderStages::FRAGMENT,
258 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
259 count: None,
260 },
261 ],
262 });
263
264 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
265 label: Some("Sprite Layout"),
266 bind_group_layouts: &[Some(&bgl)],
267 immediate_size: 0,
268 });
269
270 let make_pipeline = |label: &str, additive: bool| -> wgpu::RenderPipeline {
271 let blend = blend_state_for(additive);
272 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
273 label: Some(label),
274 layout: Some(&layout),
275 vertex: wgpu::VertexState {
276 module: &shader,
277 entry_point: Some("vs_main"),
278 buffers: &[],
279 compilation_options: Default::default(),
280 },
281 fragment: Some(wgpu::FragmentState {
282 module: &shader,
283 entry_point: Some("fs_main"),
284 targets: &[Some(wgpu::ColorTargetState {
285 format,
286 blend: Some(blend),
287 write_mask: wgpu::ColorWrites::ALL,
288 })],
289 compilation_options: Default::default(),
290 }),
291 primitive: wgpu::PrimitiveState {
292 topology: wgpu::PrimitiveTopology::TriangleList,
293 ..Default::default()
294 },
295 depth_stencil: None,
296 multisample: wgpu::MultisampleState::default(),
297 multiview_mask: None,
298 cache: None,
299 })
300 };
301
302 let pipeline_alpha = make_pipeline("Sprite Pipeline (alpha)", false);
303 let pipeline_additive = make_pipeline("Sprite Pipeline (additive)", true);
304
305 let uniform_stride = 256u64;
309 let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
310 label: Some("Sprite Uniforms"),
311 size: uniform_stride * MAX_ACTIVE_SPRITES as u64,
312 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
313 mapped_at_creation: false,
314 });
315
316 Self {
317 pipeline_alpha,
318 pipeline_additive,
319 bgl,
320 uniform_buffer,
321 uniform_stride,
322 }
323 }
324
325 pub fn record(
329 &self,
330 encoder: &mut wgpu::CommandEncoder,
331 queue: &wgpu::Queue,
332 device: &wgpu::Device,
333 target: &wgpu::TextureView,
334 sampler: &wgpu::Sampler,
335 cmds: &[SpriteDrawCmd<'_>],
336 ) {
337 if cmds.is_empty() {
338 return;
339 }
340 let n = cmds.len().min(MAX_ACTIVE_SPRITES);
341 let mut staging = vec![0u8; self.uniform_stride as usize * n];
346 for (i, cmd) in cmds.iter().take(n).enumerate() {
347 let start = i * self.uniform_stride as usize;
348 let bytes = bytemuck::bytes_of(&cmd.uniform);
349 staging[start..start + bytes.len()].copy_from_slice(bytes);
350 }
351 queue.write_buffer(&self.uniform_buffer, 0, &staging);
352
353 let mut bind_groups: Vec<wgpu::BindGroup> = Vec::with_capacity(n);
358 for cmd in cmds.iter().take(n) {
359 bind_groups.push(
360 device.create_bind_group(&wgpu::BindGroupDescriptor {
361 label: Some("Sprite BG (per-draw)"),
362 layout: &self.bgl,
363 entries: &[
364 wgpu::BindGroupEntry {
365 binding: 0,
366 resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
367 buffer: &self.uniform_buffer,
368 offset: 0,
369 size: wgpu::BufferSize::new(
370 std::mem::size_of::<SpriteUniform>() as u64
371 ),
372 }),
373 },
374 wgpu::BindGroupEntry {
375 binding: 1,
376 resource: wgpu::BindingResource::TextureView(cmd.texture_view),
377 },
378 wgpu::BindGroupEntry {
379 binding: 2,
380 resource: wgpu::BindingResource::Sampler(sampler),
381 },
382 ],
383 }),
384 );
385 }
386
387 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
388 label: Some("Sprite Pass"),
389 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
390 view: target,
391 resolve_target: None,
392 ops: wgpu::Operations {
393 load: wgpu::LoadOp::Load,
394 store: wgpu::StoreOp::Store,
395 },
396 depth_slice: None,
397 })],
398 depth_stencil_attachment: None,
399 timestamp_writes: None,
400 occlusion_query_set: None,
401 multiview_mask: None,
402 });
403
404 for (i, cmd) in cmds.iter().take(n).enumerate() {
405 let pipeline = match cmd.blend {
406 SpriteBlendKind::Alpha => &self.pipeline_alpha,
407 SpriteBlendKind::Additive => &self.pipeline_additive,
408 };
409 pass.set_pipeline(pipeline);
410 pass.set_bind_group(
411 0,
412 Some(&bind_groups[i]),
413 &[i as u32 * self.uniform_stride as u32],
414 );
415 pass.draw(0..6, 0..1);
416 }
417 }
418}
419
420#[allow(clippy::too_many_arguments)]
426pub fn build_sprite_uniform(
427 x: f32,
428 y: f32,
429 sx: f32,
430 sy: f32,
431 rot: f32,
432 rgba: [f32; 4],
433 texture_w: u32,
434 texture_h: u32,
435 render_w: u32,
436 render_h: u32,
437) -> SpriteUniform {
438 let base = 0.33;
443 let tex_aspect = (texture_w.max(1) as f32) / (texture_h.max(1) as f32);
444 let render_aspect = (render_w.max(1) as f32) / (render_h.max(1) as f32);
445 let size_x = sx * base * tex_aspect / render_aspect;
447 let size_y = sy * base;
448 SpriteUniform {
449 center_size: [x, y, size_x, size_y],
450 rot_pad: [rot, 0.0, 0.0, 0.0],
451 rgba,
452 }
453}
454
455fn make_fallback(device: &wgpu::Device, queue: &wgpu::Queue) -> SpriteTexture {
456 upload_sprite_rgba8(device, queue, "__fallback_transparent", 1, 1, &[0, 0, 0, 0])
457}
458
459fn load_sprite_file(
460 device: &wgpu::Device,
461 queue: &wgpu::Queue,
462 path: &std::path::Path,
463) -> Result<SpriteTexture, image::ImageError> {
464 let img = image::open(path)?;
465 let rgba = img.to_rgba8();
466 let (w, h) = rgba.dimensions();
467 let name = path
468 .file_stem()
469 .and_then(|s| s.to_str())
470 .map(|s| s.to_ascii_lowercase())
471 .unwrap_or_default();
472 Ok(upload_sprite_rgba8(device, queue, &name, w, h, &rgba))
473}
474
475fn upload_sprite_rgba8(
476 device: &wgpu::Device,
477 queue: &wgpu::Queue,
478 name: &str,
479 width: u32,
480 height: u32,
481 bytes: &[u8],
482) -> SpriteTexture {
483 let texture = device.create_texture(&wgpu::TextureDescriptor {
484 label: Some(&format!("Sprite {name}")),
485 size: wgpu::Extent3d {
486 width,
487 height,
488 depth_or_array_layers: 1,
489 },
490 mip_level_count: 1,
491 sample_count: 1,
492 dimension: wgpu::TextureDimension::D2,
493 format: wgpu::TextureFormat::Rgba8Unorm,
494 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
495 view_formats: &[],
496 });
497 queue.write_texture(
498 wgpu::TexelCopyTextureInfo {
499 texture: &texture,
500 mip_level: 0,
501 origin: wgpu::Origin3d::ZERO,
502 aspect: wgpu::TextureAspect::All,
503 },
504 bytes,
505 wgpu::TexelCopyBufferLayout {
506 offset: 0,
507 bytes_per_row: Some(width * 4),
508 rows_per_image: Some(height),
509 },
510 wgpu::Extent3d {
511 width,
512 height,
513 depth_or_array_layers: 1,
514 },
515 );
516 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
517 SpriteTexture {
518 texture,
519 view,
520 width,
521 height,
522 name: name.to_string(),
523 }
524}
525
526#[cfg(test)]
527mod tests {
528 use super::*;
529
530 #[test]
531 fn build_uniform_packs_aspect_correctly() {
532 let u = build_sprite_uniform(0.5, 0.5, 1.0, 1.0, 0.0, [1.0; 4], 100, 50, 1000, 500);
535 assert!((u.center_size[2] - u.center_size[3]).abs() < 1e-6);
536 }
537
538 #[test]
539 fn build_uniform_handles_zero_dims() {
540 let u = build_sprite_uniform(0.5, 0.5, 1.0, 1.0, 0.0, [1.0; 4], 0, 0, 0, 0);
542 assert!(u.center_size[2].is_finite());
543 assert!(u.center_size[3].is_finite());
544 }
545
546 #[test]
547 fn build_uniform_writes_rot_to_first_lane() {
548 let u = build_sprite_uniform(0.5, 0.5, 1.0, 1.0, 0.7, [1.0; 4], 100, 100, 100, 100);
549 assert!((u.rot_pad[0] - 0.7).abs() < 1e-6);
550 }
551}