1use bytemuck::{Pod, Zeroable};
11use onedrop_codegen::{ShaderUniforms, USER_TEXTURE_SLOTS};
12use wgpu::util::DeviceExt;
13
14use crate::error::Result;
15use crate::user_warp_pipeline::{UserWarpPipeline, WarpAuxViews};
16use crate::warp_mesh::WarpMesh;
17
18#[repr(C)]
23#[derive(Clone, Copy, Debug, Pod, Zeroable)]
24pub struct WarpVertex {
25 pub pos_clip: [f32; 2],
26 pub uv_warp: [f32; 2],
27}
28
29#[repr(C)]
30#[derive(Clone, Copy, Debug, Pod, Zeroable)]
31struct WarpUniforms {
32 decay: f32,
33 aspect_x: f32,
34 aspect_y: f32,
35 flags: u32,
38}
39
40pub struct WarpPipeline {
41 pipeline: wgpu::RenderPipeline,
42 bind_group_layout: wgpu::BindGroupLayout,
43 bind_group: wgpu::BindGroup,
44 sampler: wgpu::Sampler,
45 uniforms_buffer: wgpu::Buffer,
46
47 vertex_buffer: wgpu::Buffer,
48 index_buffer: wgpu::Buffer,
49 index_count: u32,
50 vertex_count: u32,
51
52 user_pipeline: Option<UserWarpPipeline>,
56 target_format: wgpu::TextureFormat,
59}
60
61impl WarpPipeline {
62 pub fn new(
63 device: &wgpu::Device,
64 target_format: wgpu::TextureFormat,
65 prev_texture_view: &wgpu::TextureView,
66 mesh: &WarpMesh,
67 ) -> Result<Self> {
68 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
70 label: Some("Warp Sampler"),
71 address_mode_u: wgpu::AddressMode::ClampToEdge,
72 address_mode_v: wgpu::AddressMode::ClampToEdge,
73 address_mode_w: wgpu::AddressMode::ClampToEdge,
74 mag_filter: wgpu::FilterMode::Linear,
75 min_filter: wgpu::FilterMode::Linear,
76 mipmap_filter: wgpu::MipmapFilterMode::Linear,
77 ..Default::default()
78 });
79
80 let uniforms = WarpUniforms {
81 decay: 0.98,
82 aspect_x: 1.0,
83 aspect_y: 1.0,
84 flags: 0,
85 };
86 let uniforms_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
87 label: Some("Warp Uniforms"),
88 contents: bytemuck::bytes_of(&uniforms),
89 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
90 });
91
92 let initial_vertices: Vec<WarpVertex> = mesh
94 .vertices
95 .iter()
96 .map(|v| WarpVertex {
97 pos_clip: v.pos_clip,
98 uv_warp: v.uv_orig,
99 })
100 .collect();
101 let vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
102 label: Some("Warp Vertex Buffer"),
103 contents: bytemuck::cast_slice(&initial_vertices),
104 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
105 });
106
107 let index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
108 label: Some("Warp Index Buffer"),
109 contents: bytemuck::cast_slice(&mesh.indices),
110 usage: wgpu::BufferUsages::INDEX,
111 });
112
113 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
114 label: Some("Warp Bind Group Layout"),
115 entries: &[
116 wgpu::BindGroupLayoutEntry {
117 binding: 0,
118 visibility: wgpu::ShaderStages::FRAGMENT,
119 ty: wgpu::BindingType::Buffer {
120 ty: wgpu::BufferBindingType::Uniform,
121 has_dynamic_offset: false,
122 min_binding_size: None,
123 },
124 count: None,
125 },
126 wgpu::BindGroupLayoutEntry {
127 binding: 1,
128 visibility: wgpu::ShaderStages::FRAGMENT,
129 ty: wgpu::BindingType::Texture {
130 sample_type: wgpu::TextureSampleType::Float { filterable: true },
131 view_dimension: wgpu::TextureViewDimension::D2,
132 multisampled: false,
133 },
134 count: None,
135 },
136 wgpu::BindGroupLayoutEntry {
137 binding: 2,
138 visibility: wgpu::ShaderStages::FRAGMENT,
139 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
140 count: None,
141 },
142 ],
143 });
144
145 let bind_group = Self::create_bind_group(
146 device,
147 &bind_group_layout,
148 &uniforms_buffer,
149 prev_texture_view,
150 &sampler,
151 );
152
153 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
154 label: Some("Warp Shader"),
155 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/warp.wgsl").into()),
156 });
157
158 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
159 label: Some("Warp Pipeline Layout"),
160 bind_group_layouts: &[Some(&bind_group_layout)],
161 immediate_size: 0,
162 });
163
164 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
165 label: Some("Warp Pipeline"),
166 layout: Some(&pipeline_layout),
167 vertex: wgpu::VertexState {
168 module: &shader,
169 entry_point: Some("vs_main"),
170 buffers: &[wgpu::VertexBufferLayout {
171 array_stride: std::mem::size_of::<WarpVertex>() as wgpu::BufferAddress,
172 step_mode: wgpu::VertexStepMode::Vertex,
173 attributes: &[
174 wgpu::VertexAttribute {
175 offset: 0,
176 shader_location: 0,
177 format: wgpu::VertexFormat::Float32x2,
178 },
179 wgpu::VertexAttribute {
180 offset: std::mem::size_of::<[f32; 2]>() as wgpu::BufferAddress,
181 shader_location: 1,
182 format: wgpu::VertexFormat::Float32x2,
183 },
184 ],
185 }],
186 compilation_options: Default::default(),
187 },
188 fragment: Some(wgpu::FragmentState {
189 module: &shader,
190 entry_point: Some("fs_main"),
191 targets: &[Some(wgpu::ColorTargetState {
192 format: target_format,
193 blend: Some(wgpu::BlendState::REPLACE),
194 write_mask: wgpu::ColorWrites::ALL,
195 })],
196 compilation_options: Default::default(),
197 }),
198 primitive: wgpu::PrimitiveState {
199 topology: wgpu::PrimitiveTopology::TriangleList,
200 ..Default::default()
201 },
202 depth_stencil: None,
203 multisample: wgpu::MultisampleState::default(),
204 multiview_mask: None,
205 cache: None,
206 });
207
208 Ok(Self {
209 pipeline,
210 bind_group_layout,
211 bind_group,
212 sampler,
213 uniforms_buffer,
214 vertex_buffer,
215 index_buffer,
216 index_count: mesh.indices.len() as u32,
217 vertex_count: mesh.vertices.len() as u32,
218 user_pipeline: None,
219 target_format,
220 })
221 }
222
223 pub fn set_user_shader_with_plan(
232 &mut self,
233 device: &wgpu::Device,
234 queue: &wgpu::Queue,
235 wrapped_wgsl: &str,
236 prev_view: &wgpu::TextureView,
237 aux: &WarpAuxViews,
238 user_views: [Option<wgpu::TextureView>; USER_TEXTURE_SLOTS],
239 ) -> Result<()> {
240 let user = UserWarpPipeline::new(
241 device,
242 queue,
243 self.target_format,
244 wrapped_wgsl,
245 prev_view,
246 aux,
247 user_views,
248 )?;
249 self.user_pipeline = Some(user);
250 Ok(())
251 }
252
253 pub fn reset_to_default(&mut self) {
256 self.user_pipeline = None;
257 }
258
259 pub fn has_user_shader(&self) -> bool {
262 self.user_pipeline.is_some()
263 }
264
265 pub fn update_user_uniforms(&self, queue: &wgpu::Queue, uniforms: &ShaderUniforms) {
269 if let Some(u) = &self.user_pipeline {
270 u.update_uniforms(queue, uniforms);
271 }
272 }
273
274 pub fn rebind_user_textures(
279 &mut self,
280 device: &wgpu::Device,
281 prev_view: &wgpu::TextureView,
282 aux: &WarpAuxViews,
283 ) {
284 if let Some(u) = self.user_pipeline.as_mut() {
285 u.rebind_textures(device, prev_view, aux);
286 }
287 }
288
289 fn create_bind_group(
290 device: &wgpu::Device,
291 layout: &wgpu::BindGroupLayout,
292 uniforms_buffer: &wgpu::Buffer,
293 prev_texture_view: &wgpu::TextureView,
294 sampler: &wgpu::Sampler,
295 ) -> wgpu::BindGroup {
296 device.create_bind_group(&wgpu::BindGroupDescriptor {
297 label: Some("Warp Bind Group"),
298 layout,
299 entries: &[
300 wgpu::BindGroupEntry {
301 binding: 0,
302 resource: uniforms_buffer.as_entire_binding(),
303 },
304 wgpu::BindGroupEntry {
305 binding: 1,
306 resource: wgpu::BindingResource::TextureView(prev_texture_view),
307 },
308 wgpu::BindGroupEntry {
309 binding: 2,
310 resource: wgpu::BindingResource::Sampler(sampler),
311 },
312 ],
313 })
314 }
315
316 pub fn rebind_prev_texture(
319 &mut self,
320 device: &wgpu::Device,
321 prev_texture_view: &wgpu::TextureView,
322 ) {
323 self.bind_group = Self::create_bind_group(
324 device,
325 &self.bind_group_layout,
326 &self.uniforms_buffer,
327 prev_texture_view,
328 &self.sampler,
329 );
330 }
331
332 pub fn update_uniforms(&self, queue: &wgpu::Queue, decay: f32, aspect: f32, flags: u32) {
333 let aspect_x = if aspect > 1.0 { 1.0 / aspect } else { 1.0 };
334 let aspect_y = if aspect < 1.0 { aspect } else { 1.0 };
335 let uniforms = WarpUniforms {
336 decay,
337 aspect_x,
338 aspect_y,
339 flags,
340 };
341 queue.write_buffer(&self.uniforms_buffer, 0, bytemuck::bytes_of(&uniforms));
342 }
343
344 pub fn rebuild_mesh(&mut self, device: &wgpu::Device, mesh: &WarpMesh) {
349 let initial_vertices: Vec<WarpVertex> = mesh
350 .vertices
351 .iter()
352 .map(|v| WarpVertex {
353 pos_clip: v.pos_clip,
354 uv_warp: v.uv_orig,
355 })
356 .collect();
357 self.vertex_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
358 label: Some("Warp Vertex Buffer"),
359 contents: bytemuck::cast_slice(&initial_vertices),
360 usage: wgpu::BufferUsages::VERTEX | wgpu::BufferUsages::COPY_DST,
361 });
362 self.index_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
363 label: Some("Warp Index Buffer"),
364 contents: bytemuck::cast_slice(&mesh.indices),
365 usage: wgpu::BufferUsages::INDEX,
366 });
367 self.vertex_count = mesh.vertices.len() as u32;
368 self.index_count = mesh.indices.len() as u32;
369 }
370
371 pub fn update_vertices(&self, queue: &wgpu::Queue, vertices: &[WarpVertex]) {
375 debug_assert_eq!(
376 vertices.len() as u32,
377 self.vertex_count,
378 "warp vertex count mismatch"
379 );
380 queue.write_buffer(&self.vertex_buffer, 0, bytemuck::cast_slice(vertices));
381 }
382
383 pub fn render(&self, encoder: &mut wgpu::CommandEncoder, output_view: &wgpu::TextureView) {
390 if let Some(user) = &self.user_pipeline {
391 user.render(
392 encoder,
393 output_view,
394 &self.vertex_buffer,
395 &self.index_buffer,
396 self.index_count,
397 );
398 return;
399 }
400 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
401 label: Some("Warp Render Pass"),
402 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
403 view: output_view,
404 depth_slice: None,
405 resolve_target: None,
406 ops: wgpu::Operations {
407 load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
408 store: wgpu::StoreOp::Store,
409 },
410 })],
411 depth_stencil_attachment: None,
412 timestamp_writes: None,
413 occlusion_query_set: None,
414 multiview_mask: None,
415 });
416
417 pass.set_pipeline(&self.pipeline);
418 pass.set_bind_group(0, &self.bind_group, &[]);
419 pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
420 pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
421 pass.draw_indexed(0..self.index_count, 0, 0..1);
422 }
423
424 pub fn vertex_count(&self) -> u32 {
425 self.vertex_count
426 }
427}
428
429#[cfg(test)]
430mod tests {
431 use super::*;
432 use crate::chain_textures::ChainTextures;
433 use crate::config::RenderConfig;
434 use crate::gpu_context::GpuContext;
435 use crate::warp_mesh::WarpMesh;
436
437 #[test]
438 fn pipeline_instantiates() {
439 let cfg = RenderConfig::default();
440 let gpu = pollster::block_on(GpuContext::new(cfg)).unwrap();
441 let chain_tex = ChainTextures::new(&gpu.device, &gpu.config);
442 let mesh = WarpMesh::new(8, 6, 16.0 / 9.0);
443 let p = WarpPipeline::new(
444 &gpu.device,
445 gpu.config.texture_format.to_wgpu(),
446 &chain_tex.prev_texture_view,
447 &mesh,
448 );
449 assert!(p.is_ok());
450 }
451
452 #[test]
453 fn warp_vertex_size_is_16() {
454 assert_eq!(std::mem::size_of::<WarpVertex>(), 16);
456 }
457
458 #[test]
464 fn warp_pass_applies_decay_to_prev_sample() {
465 use crate::config::TextureFormat;
466
467 let cfg = RenderConfig {
468 width: 64,
469 height: 64,
470 texture_format: TextureFormat::Bgra8Unorm,
471 ..Default::default()
472 };
473 let gpu = pollster::block_on(GpuContext::new(cfg)).unwrap();
474 let chain_tex = ChainTextures::new(&gpu.device, &gpu.config);
475 let mesh = WarpMesh::new(8, 6, 1.0);
476 let pipeline = WarpPipeline::new(
477 &gpu.device,
478 gpu.config.texture_format.to_wgpu(),
479 &chain_tex.prev_texture_view,
480 &mesh,
481 )
482 .unwrap();
483
484 let pixels: Vec<u8> = (0..(64 * 64))
486 .flat_map(|_| [200u8, 200, 200, 255])
487 .collect();
488 gpu.queue.write_texture(
489 wgpu::TexelCopyTextureInfo {
490 texture: &chain_tex.prev_texture,
491 mip_level: 0,
492 origin: wgpu::Origin3d::ZERO,
493 aspect: wgpu::TextureAspect::All,
494 },
495 &pixels,
496 wgpu::TexelCopyBufferLayout {
497 offset: 0,
498 bytes_per_row: Some(64 * 4),
499 rows_per_image: Some(64),
500 },
501 wgpu::Extent3d {
502 width: 64,
503 height: 64,
504 depth_or_array_layers: 1,
505 },
506 );
507
508 let read_center = |decay: f32, flags: u32| -> u8 {
509 pipeline.update_uniforms(&gpu.queue, decay, 1.0, flags);
510
511 let mut encoder = gpu.device.create_command_encoder(&Default::default());
512 pipeline.render(&mut encoder, &chain_tex.render_texture_view);
513 gpu.queue.submit(std::iter::once(encoder.finish()));
514
515 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
516 let unpadded_row: u32 = 64 * 4;
517 let padded_row = unpadded_row.div_ceil(align) * align;
518 let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
519 label: Some("Warp Decay Test Staging"),
520 size: (padded_row * 64) as u64,
521 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
522 mapped_at_creation: false,
523 });
524 let mut e2 = gpu.device.create_command_encoder(&Default::default());
525 e2.copy_texture_to_buffer(
526 wgpu::TexelCopyTextureInfo {
527 texture: &chain_tex.render_texture,
528 mip_level: 0,
529 origin: wgpu::Origin3d::ZERO,
530 aspect: wgpu::TextureAspect::All,
531 },
532 wgpu::TexelCopyBufferInfo {
533 buffer: &staging,
534 layout: wgpu::TexelCopyBufferLayout {
535 offset: 0,
536 bytes_per_row: Some(padded_row),
537 rows_per_image: Some(64),
538 },
539 },
540 wgpu::Extent3d {
541 width: 64,
542 height: 64,
543 depth_or_array_layers: 1,
544 },
545 );
546 gpu.queue.submit(std::iter::once(e2.finish()));
547 let (tx, rx) = std::sync::mpsc::channel();
548 staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
549 let _ = tx.send(r);
550 });
551 gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok();
552 rx.recv().unwrap().unwrap();
553 let view = staging.slice(..).get_mapped_range();
554 let off = (32u32 * padded_row + 32 * 4) as usize;
555 let g = view[off + 1];
556 drop(view);
557 staging.unmap();
558 g
559 };
560
561 let pass = read_center(1.0, 0);
563 assert!(
564 (pass as i32 - 200).abs() <= 4,
565 "decay=1.0 should pass through, got {pass} (expected ~200)"
566 );
567
568 let half = read_center(0.5, 0);
570 assert!(
571 (half as i32 - 100).abs() <= 4,
572 "decay=0.5 should halve, got {half} (expected ~100)"
573 );
574
575 let zero = read_center(0.0, 0);
577 assert!(zero <= 4, "decay=0.0 should produce black, got {zero}");
578 }
579
580 #[test]
586 fn warp_darken_center_attenuates_center_more_than_corner() {
587 use crate::config::TextureFormat;
588
589 let cfg = RenderConfig {
590 width: 64,
591 height: 64,
592 texture_format: TextureFormat::Bgra8Unorm,
593 ..Default::default()
594 };
595 let gpu = pollster::block_on(GpuContext::new(cfg)).unwrap();
596 let chain_tex = ChainTextures::new(&gpu.device, &gpu.config);
597 let mesh = WarpMesh::new(8, 6, 1.0);
598 let pipeline = WarpPipeline::new(
599 &gpu.device,
600 gpu.config.texture_format.to_wgpu(),
601 &chain_tex.prev_texture_view,
602 &mesh,
603 )
604 .unwrap();
605
606 let pixels: Vec<u8> = (0..(64 * 64))
607 .flat_map(|_| [200u8, 200, 200, 255])
608 .collect();
609 gpu.queue.write_texture(
610 wgpu::TexelCopyTextureInfo {
611 texture: &chain_tex.prev_texture,
612 mip_level: 0,
613 origin: wgpu::Origin3d::ZERO,
614 aspect: wgpu::TextureAspect::All,
615 },
616 &pixels,
617 wgpu::TexelCopyBufferLayout {
618 offset: 0,
619 bytes_per_row: Some(64 * 4),
620 rows_per_image: Some(64),
621 },
622 wgpu::Extent3d {
623 width: 64,
624 height: 64,
625 depth_or_array_layers: 1,
626 },
627 );
628
629 let read_pixels = |flags: u32| -> (u8, u8) {
630 pipeline.update_uniforms(&gpu.queue, 1.0, 1.0, flags);
631
632 let mut encoder = gpu.device.create_command_encoder(&Default::default());
633 pipeline.render(&mut encoder, &chain_tex.render_texture_view);
634 gpu.queue.submit(std::iter::once(encoder.finish()));
635
636 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
637 let unpadded_row: u32 = 64 * 4;
638 let padded_row = unpadded_row.div_ceil(align) * align;
639 let staging = gpu.device.create_buffer(&wgpu::BufferDescriptor {
640 label: Some("DarkenCenter Test Staging"),
641 size: (padded_row * 64) as u64,
642 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
643 mapped_at_creation: false,
644 });
645 let mut e2 = gpu.device.create_command_encoder(&Default::default());
646 e2.copy_texture_to_buffer(
647 wgpu::TexelCopyTextureInfo {
648 texture: &chain_tex.render_texture,
649 mip_level: 0,
650 origin: wgpu::Origin3d::ZERO,
651 aspect: wgpu::TextureAspect::All,
652 },
653 wgpu::TexelCopyBufferInfo {
654 buffer: &staging,
655 layout: wgpu::TexelCopyBufferLayout {
656 offset: 0,
657 bytes_per_row: Some(padded_row),
658 rows_per_image: Some(64),
659 },
660 },
661 wgpu::Extent3d {
662 width: 64,
663 height: 64,
664 depth_or_array_layers: 1,
665 },
666 );
667 gpu.queue.submit(std::iter::once(e2.finish()));
668 let (tx, rx) = std::sync::mpsc::channel();
669 staging.slice(..).map_async(wgpu::MapMode::Read, move |r| {
670 let _ = tx.send(r);
671 });
672 gpu.device.poll(wgpu::PollType::wait_indefinitely()).ok();
673 rx.recv().unwrap().unwrap();
674 let view = staging.slice(..).get_mapped_range();
675 let center_off = (32u32 * padded_row + 32 * 4) as usize;
676 let corner_off = (2u32 * padded_row + 2 * 4) as usize;
677 let center_g = view[center_off + 1];
678 let corner_g = view[corner_off + 1];
679 drop(view);
680 staging.unmap();
681 (center_g, corner_g)
682 };
683
684 let (off_center, off_corner) = read_pixels(0);
686 assert!(
687 (off_center as i32 - off_corner as i32).abs() <= 2,
688 "flag off: centre {off_center} vs corner {off_corner} should be ~equal"
689 );
690
691 let (on_center, on_corner) = read_pixels(2);
694 assert!(
695 (on_corner as i32 - off_corner as i32).abs() <= 2,
696 "flag on: corner {on_corner} should still be ~{off_corner}"
697 );
698 assert!(
699 on_center + 2 < off_center,
700 "flag on: centre {on_center} should be darker than off-state {off_center}"
701 );
702 }
703}