onedrop_renderer/
blend_renderer.rs

1//! Blend renderer for double-preset visualization.
2//!
3//! This module implements rendering of two presets simultaneously with
4//! 27 different blending patterns.
5
6use crate::error::Result;
7use std::sync::Arc;
8use wgpu::{Device, Queue, TextureView};
9
10/// Blend renderer for double-presets.
11pub struct BlendRenderer {
12    device: Arc<Device>,
13    queue: Arc<Queue>,
14    pipeline: wgpu::RenderPipeline,
15    bind_group_layout: wgpu::BindGroupLayout,
16    uniform_buffer: wgpu::Buffer,
17    sampler: wgpu::Sampler,
18    /// Cached bind group key and bind group for texture pair
19    cached_bind_group: Option<CachedBindGroup>,
20}
21
22/// Holds cached bind group data
23struct CachedBindGroup {
24    key_a: usize,
25    key_b: usize,
26    bind_group: wgpu::BindGroup,
27}
28
29#[repr(C)]
30#[derive(Copy, Clone, Debug, bytemuck::Pod, bytemuck::Zeroable)]
31struct BlendUniforms {
32    blend_pattern: u32,
33    blend_amount: f32,
34    time: f32,
35    _padding: f32,
36}
37
38impl BlendRenderer {
39    /// Create a new blend renderer.
40    pub fn new(
41        device: Arc<Device>,
42        queue: Arc<Queue>,
43        texture_format: wgpu::TextureFormat,
44    ) -> Result<Self> {
45        // Create uniform buffer
46        let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
47            label: Some("Blend Uniform Buffer"),
48            size: std::mem::size_of::<BlendUniforms>() as u64,
49            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
50            mapped_at_creation: false,
51        });
52
53        // Create sampler
54        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
55            label: Some("Blend Sampler"),
56            address_mode_u: wgpu::AddressMode::ClampToEdge,
57            address_mode_v: wgpu::AddressMode::ClampToEdge,
58            address_mode_w: wgpu::AddressMode::ClampToEdge,
59            mag_filter: wgpu::FilterMode::Linear,
60            min_filter: wgpu::FilterMode::Linear,
61            mipmap_filter: wgpu::MipmapFilterMode::Linear,
62            ..Default::default()
63        });
64
65        // Create bind group layout
66        let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
67            label: Some("Blend Bind Group Layout"),
68            entries: &[
69                // Texture A
70                wgpu::BindGroupLayoutEntry {
71                    binding: 0,
72                    visibility: wgpu::ShaderStages::FRAGMENT,
73                    ty: wgpu::BindingType::Texture {
74                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
75                        view_dimension: wgpu::TextureViewDimension::D2,
76                        multisampled: false,
77                    },
78                    count: None,
79                },
80                // Texture B
81                wgpu::BindGroupLayoutEntry {
82                    binding: 1,
83                    visibility: wgpu::ShaderStages::FRAGMENT,
84                    ty: wgpu::BindingType::Texture {
85                        sample_type: wgpu::TextureSampleType::Float { filterable: true },
86                        view_dimension: wgpu::TextureViewDimension::D2,
87                        multisampled: false,
88                    },
89                    count: None,
90                },
91                // Sampler
92                wgpu::BindGroupLayoutEntry {
93                    binding: 2,
94                    visibility: wgpu::ShaderStages::FRAGMENT,
95                    ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
96                    count: None,
97                },
98                // Uniforms
99                wgpu::BindGroupLayoutEntry {
100                    binding: 3,
101                    visibility: wgpu::ShaderStages::FRAGMENT,
102                    ty: wgpu::BindingType::Buffer {
103                        ty: wgpu::BufferBindingType::Uniform,
104                        has_dynamic_offset: false,
105                        min_binding_size: None,
106                    },
107                    count: None,
108                },
109            ],
110        });
111
112        // Load shader
113        let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
114            label: Some("Blend Shader"),
115            source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blend.wgsl").into()),
116        });
117
118        // Create pipeline layout
119        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
120            label: Some("Blend Pipeline Layout"),
121            bind_group_layouts: &[Some(&bind_group_layout)],
122            immediate_size: 0,
123        });
124
125        // Create render pipeline
126        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
127            label: Some("Blend Pipeline"),
128            layout: Some(&pipeline_layout),
129            vertex: wgpu::VertexState {
130                module: &shader,
131                entry_point: Some("vs_main"),
132                buffers: &[],
133                compilation_options: Default::default(),
134            },
135            fragment: Some(wgpu::FragmentState {
136                module: &shader,
137                entry_point: Some("fs_main"),
138                targets: &[Some(wgpu::ColorTargetState {
139                    format: texture_format,
140                    blend: Some(wgpu::BlendState::REPLACE),
141                    write_mask: wgpu::ColorWrites::ALL,
142                })],
143                compilation_options: Default::default(),
144            }),
145            primitive: wgpu::PrimitiveState {
146                topology: wgpu::PrimitiveTopology::TriangleList,
147                strip_index_format: None,
148                front_face: wgpu::FrontFace::Ccw,
149                cull_mode: None,
150                polygon_mode: wgpu::PolygonMode::Fill,
151                unclipped_depth: false,
152                conservative: false,
153            },
154            depth_stencil: None,
155            multisample: wgpu::MultisampleState {
156                count: 1,
157                mask: !0,
158                alpha_to_coverage_enabled: false,
159            },
160            multiview_mask: None,
161            cache: None,
162        });
163
164        Ok(Self {
165            device,
166            queue,
167            pipeline,
168            bind_group_layout,
169            uniform_buffer,
170            sampler,
171            cached_bind_group: None,
172        })
173    }
174
175    /// Check if cached bind group matches texture pair
176    fn is_cached(&self, key_a: usize, key_b: usize) -> bool {
177        if let Some(ref cached) = self.cached_bind_group {
178            cached.key_a == key_a && cached.key_b == key_b
179        } else {
180            false
181        }
182    }
183
184    /// Get or create a cached bind group for the texture pair.
185    fn get_cached_bind_group(&self) -> Option<&wgpu::BindGroup> {
186        self.cached_bind_group.as_ref().map(|c| &c.bind_group)
187    }
188
189    /// Create and cache a new bind group for the texture pair.
190    fn create_and_cache_bind_group(&mut self, texture_a: &TextureView, texture_b: &TextureView) {
191        // Use pointer addresses as cache keys (texture views are immutable)
192        let key_a = texture_a as *const _ as usize;
193        let key_b = texture_b as *const _ as usize;
194
195        // Create new bind group
196        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
197            label: Some("Blend Bind Group"),
198            layout: &self.bind_group_layout,
199            entries: &[
200                wgpu::BindGroupEntry {
201                    binding: 0,
202                    resource: wgpu::BindingResource::TextureView(texture_a),
203                },
204                wgpu::BindGroupEntry {
205                    binding: 1,
206                    resource: wgpu::BindingResource::TextureView(texture_b),
207                },
208                wgpu::BindGroupEntry {
209                    binding: 2,
210                    resource: wgpu::BindingResource::Sampler(&self.sampler),
211                },
212                wgpu::BindGroupEntry {
213                    binding: 3,
214                    resource: self.uniform_buffer.as_entire_binding(),
215                },
216            ],
217        });
218
219        // Cache for future use
220        self.cached_bind_group = Some(CachedBindGroup {
221            key_a,
222            key_b,
223            bind_group,
224        });
225    }
226
227    /// Render blended output.
228    pub fn render(
229        &mut self,
230        texture_a: &TextureView,
231        texture_b: &TextureView,
232        output: &TextureView,
233        blend_pattern: u32,
234        blend_amount: f32,
235        time: f32,
236    ) -> Result<()> {
237        // Update uniforms
238        let uniforms = BlendUniforms {
239            blend_pattern,
240            blend_amount,
241            time,
242            _padding: 0.0,
243        };
244        self.queue
245            .write_buffer(&self.uniform_buffer, 0, bytemuck::cast_slice(&[uniforms]));
246
247        // Check if we need to create a new bind group
248        let key_a = texture_a as *const _ as usize;
249        let key_b = texture_b as *const _ as usize;
250
251        if !self.is_cached(key_a, key_b) {
252            self.create_and_cache_bind_group(texture_a, texture_b);
253        }
254
255        let bind_group = self
256            .get_cached_bind_group()
257            .expect("bind group should be cached");
258
259        // Create command encoder
260        let mut encoder = self
261            .device
262            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
263                label: Some("Blend Encoder"),
264            });
265
266        // Render pass
267        {
268            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
269                label: Some("Blend Render Pass"),
270                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
271                    view: output,
272                    depth_slice: None,
273                    resolve_target: None,
274                    ops: wgpu::Operations {
275                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
276                        store: wgpu::StoreOp::Store,
277                    },
278                })],
279                depth_stencil_attachment: None,
280                timestamp_writes: None,
281                occlusion_query_set: None,
282                multiview_mask: None,
283            });
284
285            render_pass.set_pipeline(&self.pipeline);
286            render_pass.set_bind_group(0, bind_group, &[]);
287            render_pass.draw(0..3, 0..1);
288        }
289
290        // Submit commands
291        self.queue.submit(std::iter::once(encoder.finish()));
292
293        Ok(())
294    }
295}
296
297#[cfg(test)]
298mod tests {
299    use super::*;
300
301    #[test]
302    fn test_blend_uniforms_size() {
303        assert_eq!(std::mem::size_of::<BlendUniforms>(), 16);
304    }
305}