onedrop_renderer/
per_pixel_pipeline.rs

1//! Per-pixel shader execution pipeline
2//!
3//! Executes per-pixel equations on the GPU using dynamically compiled shaders.
4
5use crate::error::{RenderError, Result};
6use wgpu::util::DeviceExt;
7
8/// Per-pixel variables uniform buffer
9#[repr(C)]
10#[derive(Debug, Copy, Clone, bytemuck::Pod, bytemuck::Zeroable)]
11pub struct PixelVarsUniform {
12    // Coordinates
13    pub x: f32,
14    pub y: f32,
15    pub rad: f32,
16    pub ang: f32,
17
18    // Audio
19    pub bass: f32,
20    pub mid: f32,
21    pub treb: f32,
22    pub bass_att: f32,
23    pub mid_att: f32,
24    pub treb_att: f32,
25
26    // Time
27    pub time: f32,
28    pub frame: f32,
29    pub fps: f32,
30
31    // Padding for alignment
32    pub _padding: f32,
33
34    // Custom variables (64 floats as 16 vec4s)
35    pub q: [[f32; 4]; 16],
36}
37
38impl Default for PixelVarsUniform {
39    fn default() -> Self {
40        Self {
41            x: 0.0,
42            y: 0.0,
43            rad: 0.0,
44            ang: 0.0,
45            bass: 0.0,
46            mid: 0.0,
47            treb: 0.0,
48            bass_att: 0.0,
49            mid_att: 0.0,
50            treb_att: 0.0,
51            time: 0.0,
52            frame: 0.0,
53            fps: 60.0,
54            _padding: 0.0,
55            q: [[0.0; 4]; 16],
56        }
57    }
58}
59
60/// Per-pixel rendering pipeline
61#[allow(dead_code)]
62pub struct PerPixelPipeline {
63    device: wgpu::Device,
64    queue: wgpu::Queue,
65
66    // Pipeline state
67    render_pipeline: Option<wgpu::RenderPipeline>,
68    bind_group_layout: wgpu::BindGroupLayout,
69    bind_group: Option<wgpu::BindGroup>,
70
71    // Uniform buffer
72    vars_buffer: wgpu::Buffer,
73    vars: PixelVarsUniform,
74
75    // Textures
76    input_texture: Option<wgpu::Texture>,
77    output_texture: Option<wgpu::Texture>,
78    sampler: wgpu::Sampler,
79
80    // Resolution
81    width: u32,
82    height: u32,
83}
84
85impl PerPixelPipeline {
86    pub fn new(device: wgpu::Device, queue: wgpu::Queue, width: u32, height: u32) -> Result<Self> {
87        // Create uniform buffer
88        let vars = PixelVarsUniform::default();
89        let vars_buffer = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
90            label: Some("Per-Pixel Vars Buffer"),
91            contents: bytemuck::cast_slice(&[vars]),
92            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
93        });
94
95        // Create bind group layout
96        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
97            label: Some("Per-Pixel Bind Group Layout"),
98            entries: &[
99                // Uniform buffer
100                wgpu::BindGroupLayoutEntry {
101                    binding: 0,
102                    visibility: wgpu::ShaderStages::FRAGMENT,
103                    ty: wgpu::BindingType::Buffer {
104                        ty: wgpu::BufferBindingType::Uniform,
105                        has_dynamic_offset: false,
106                        min_binding_size: None,
107                    },
108                    count: None,
109                },
110                // Sampler
111                wgpu::BindGroupLayoutEntry {
112                    binding: 1,
113                    visibility: wgpu::ShaderStages::FRAGMENT,
114                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
115                    count: None,
116                },
117                // Input texture
118                wgpu::BindGroupLayoutEntry {
119                    binding: 2,
120                    visibility: wgpu::ShaderStages::FRAGMENT,
121                    ty: wgpu::BindingType::Texture {
122                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
123                        view_dimension: wgpu::TextureViewDimension::D2,
124                        multisampled: false,
125                    },
126                    count: None,
127                },
128            ],
129        });
130
131        // Create sampler
132        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
133            label: Some("Per-Pixel Sampler"),
134            address_mode_u: wgpu::AddressMode::ClampToEdge,
135            address_mode_v: wgpu::AddressMode::ClampToEdge,
136            address_mode_w: wgpu::AddressMode::ClampToEdge,
137            mag_filter: wgpu::FilterMode::Linear,
138            min_filter: wgpu::FilterMode::Linear,
139            mipmap_filter: wgpu::MipmapFilterMode::Nearest,
140            ..Default::default()
141        });
142
143        Ok(Self {
144            device,
145            queue,
146            render_pipeline: None,
147            bind_group_layout,
148            bind_group: None,
149            vars_buffer,
150            vars,
151            input_texture: None,
152            output_texture: None,
153            sampler,
154            width,
155            height,
156        })
157    }
158
159    /// Set the shader module from compiled WGSL
160    pub fn set_shader(&mut self, shader_module: &wgpu::ShaderModule) -> Result<()> {
161        let pipeline_layout = self
162            .device
163            .create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
164                label: Some("Per-Pixel Pipeline Layout"),
165                bind_group_layouts: &[Some(&self.bind_group_layout)],
166                immediate_size: 0,
167            });
168
169        self.render_pipeline = Some(self.device.create_render_pipeline(
170            &wgpu::RenderPipelineDescriptor {
171                label: Some("Per-Pixel Render Pipeline"),
172                layout: Some(&pipeline_layout),
173                vertex: wgpu::VertexState {
174                    module: shader_module,
175                    entry_point: Some("vs_main"),
176                    buffers: &[],
177                    compilation_options: Default::default(),
178                },
179                fragment: Some(wgpu::FragmentState {
180                    module: shader_module,
181                    entry_point: Some("fs_main"),
182                    targets: &[Some(wgpu::ColorTargetState {
183                        format: wgpu::TextureFormat::Rgba8Unorm,
184                        blend: Some(wgpu::BlendState::REPLACE),
185                        write_mask: wgpu::ColorWrites::ALL,
186                    })],
187                    compilation_options: Default::default(),
188                }),
189                primitive: wgpu::PrimitiveState {
190                    topology: wgpu::PrimitiveTopology::TriangleList,
191                    strip_index_format: None,
192                    front_face: wgpu::FrontFace::Ccw,
193                    cull_mode: None,
194                    unclipped_depth: false,
195                    polygon_mode: wgpu::PolygonMode::Fill,
196                    conservative: false,
197                },
198                depth_stencil: None,
199                multisample: wgpu::MultisampleState {
200                    count: 1,
201                    mask: !0,
202                    alpha_to_coverage_enabled: false,
203                },
204                multiview_mask: None,
205                cache: None,
206            },
207        ));
208
209        Ok(())
210    }
211
212    /// Update uniform variables
213    pub fn update_vars(&mut self, vars: PixelVarsUniform) {
214        self.vars = vars;
215        self.queue
216            .write_buffer(&self.vars_buffer, 0, bytemuck::cast_slice(&[self.vars]));
217    }
218
219    /// Set input texture
220    pub fn set_input_texture(&mut self, texture: wgpu::Texture) {
221        self.input_texture = Some(texture);
222        self.update_bind_group();
223    }
224
225    /// Update bind group with current textures
226    fn update_bind_group(&mut self) {
227        if let Some(ref input_texture) = self.input_texture {
228            let input_view = input_texture.create_view(&wgpu::TextureViewDescriptor::default());
229
230            self.bind_group = Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
231                label: Some("Per-Pixel Bind Group"),
232                layout: &self.bind_group_layout,
233                entries: &[
234                    wgpu::BindGroupEntry {
235                        binding: 0,
236                        resource: self.vars_buffer.as_entire_binding(),
237                    },
238                    wgpu::BindGroupEntry {
239                        binding: 1,
240                        resource: wgpu::BindingResource::Sampler(&self.sampler),
241                    },
242                    wgpu::BindGroupEntry {
243                        binding: 2,
244                        resource: wgpu::BindingResource::TextureView(&input_view),
245                    },
246                ],
247            }));
248        }
249    }
250
251    /// Render per-pixel effects
252    pub fn render(&mut self, output_view: &wgpu::TextureView) -> Result<()> {
253        let pipeline = self
254            .render_pipeline
255            .as_ref()
256            .ok_or_else(|| RenderError::RenderFailed("No shader set".to_string()))?;
257
258        let bind_group = self
259            .bind_group
260            .as_ref()
261            .ok_or_else(|| RenderError::RenderFailed("No bind group".to_string()))?;
262
263        let mut encoder = self
264            .device
265            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
266                label: Some("Per-Pixel Render Encoder"),
267            });
268
269        {
270            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
271                label: Some("Per-Pixel Render Pass"),
272                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
273                    view: output_view,
274                    depth_slice: None,
275                    resolve_target: None,
276                    ops: wgpu::Operations {
277                        load: wgpu::LoadOp::Load,
278                        store: wgpu::StoreOp::Store,
279                    },
280                })],
281                depth_stencil_attachment: None,
282                timestamp_writes: None,
283                occlusion_query_set: None,
284                multiview_mask: None,
285            });
286
287            render_pass.set_pipeline(pipeline);
288            render_pass.set_bind_group(0, bind_group, &[]);
289            render_pass.draw(0..6, 0..1); // Full-screen quad (2 triangles)
290        }
291
292        self.queue.submit(Some(encoder.finish()));
293
294        Ok(())
295    }
296}
297
298#[cfg(test)]
299mod tests {
300    use super::*;
301
302    #[test]
303    fn test_pixel_vars_size() {
304        // Verify struct size is correct (312 bytes)
305        assert_eq!(std::mem::size_of::<PixelVarsUniform>(), 312);
306    }
307
308    #[test]
309    fn test_pixel_vars_default() {
310        let vars = PixelVarsUniform::default();
311        assert_eq!(vars.fps, 60.0);
312        assert_eq!(vars.time, 0.0);
313    }
314}