1use std::collections::HashMap;
14
15use crate::application::Application;
16use crate::blend::BlendMode;
17use crate::texture::render_texture::RenderTexture;
18
19const TEMPLATE: &str = include_str!("../../shaders/advanced_blend.wgsl");
20const PLACEHOLDER: &str = "// __BLEND_FN_PLACEHOLDER__";
21
22pub(crate) struct AdvancedBlendPipelines {
24 pipelines: HashMap<BlendMode, wgpu::RenderPipeline>,
25 bind_group_layout: wgpu::BindGroupLayout,
26 sampler: wgpu::Sampler,
27}
28
29impl AdvancedBlendPipelines {
30 pub(crate) fn new(app: &Application, output_format: wgpu::TextureFormat) -> Self {
31 let device = app.device();
32
33 let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
34 label: Some("wisp::advanced_blend bg layout"),
35 entries: &[
36 wgpu::BindGroupLayoutEntry {
37 binding: 0,
38 visibility: wgpu::ShaderStages::FRAGMENT,
39 ty: wgpu::BindingType::Texture {
40 sample_type: wgpu::TextureSampleType::Float { filterable: true },
41 view_dimension: wgpu::TextureViewDimension::D2,
42 multisampled: false,
43 },
44 count: None,
45 },
46 wgpu::BindGroupLayoutEntry {
47 binding: 1,
48 visibility: wgpu::ShaderStages::FRAGMENT,
49 ty: wgpu::BindingType::Texture {
50 sample_type: wgpu::TextureSampleType::Float { filterable: true },
51 view_dimension: wgpu::TextureViewDimension::D2,
52 multisampled: false,
53 },
54 count: None,
55 },
56 wgpu::BindGroupLayoutEntry {
57 binding: 2,
58 visibility: wgpu::ShaderStages::FRAGMENT,
59 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
60 count: None,
61 },
62 ],
63 });
64
65 let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
66 label: Some("wisp::advanced_blend pipeline layout"),
67 bind_group_layouts: &[&bind_group_layout],
68 push_constant_ranges: &[],
69 });
70
71 let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
72 label: Some("wisp::advanced_blend sampler"),
73 address_mode_u: wgpu::AddressMode::ClampToEdge,
74 address_mode_v: wgpu::AddressMode::ClampToEdge,
75 address_mode_w: wgpu::AddressMode::ClampToEdge,
76 mag_filter: wgpu::FilterMode::Linear,
77 min_filter: wgpu::FilterMode::Linear,
78 mipmap_filter: wgpu::FilterMode::Nearest,
79 ..Default::default()
80 });
81
82 let mut pipelines = HashMap::new();
83 for mode in BlendMode::all() {
84 if !mode.is_advanced() {
85 continue;
86 }
87 let body = blend_fn_body(mode);
88 let wgsl = TEMPLATE.replace(PLACEHOLDER, body);
89 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
90 label: Some("wisp::advanced_blend shader"),
91 source: wgpu::ShaderSource::Wgsl(wgsl.into()),
92 });
93
94 let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
95 label: Some("wisp::advanced_blend pipeline"),
96 layout: Some(&pipeline_layout),
97 vertex: wgpu::VertexState {
98 module: &shader,
99 entry_point: Some("main_vs"),
100 buffers: &[],
101 compilation_options: wgpu::PipelineCompilationOptions::default(),
102 },
103 fragment: Some(wgpu::FragmentState {
104 module: &shader,
105 entry_point: Some("main_fs"),
106 targets: &[Some(wgpu::ColorTargetState {
107 format: output_format,
108 blend: Some(wgpu::BlendState::REPLACE),
111 write_mask: wgpu::ColorWrites::ALL,
112 })],
113 compilation_options: wgpu::PipelineCompilationOptions::default(),
114 }),
115 primitive: wgpu::PrimitiveState::default(),
116 depth_stencil: None,
117 multisample: wgpu::MultisampleState::default(),
118 multiview: None,
119 cache: None,
120 });
121 pipelines.insert(mode, pipeline);
122 }
123
124 Self {
125 pipelines,
126 bind_group_layout,
127 sampler,
128 }
129 }
130
131 pub(crate) fn apply(
135 &self,
136 app: &Application,
137 mode: BlendMode,
138 backdrop: &RenderTexture,
139 foreground: &RenderTexture,
140 output: &RenderTexture,
141 ) {
142 debug_assert!(
143 mode.is_advanced(),
144 "AdvancedBlendPipelines::apply called with native mode {mode:?}"
145 );
146 let pipeline = self
147 .pipelines
148 .get(&mode)
149 .unwrap_or_else(|| panic!("no pipeline registered for {mode:?}"));
150
151 let bg = app.device().create_bind_group(&wgpu::BindGroupDescriptor {
152 label: Some("wisp::advanced_blend bg"),
153 layout: &self.bind_group_layout,
154 entries: &[
155 wgpu::BindGroupEntry {
156 binding: 0,
157 resource: wgpu::BindingResource::TextureView(backdrop.view()),
158 },
159 wgpu::BindGroupEntry {
160 binding: 1,
161 resource: wgpu::BindingResource::TextureView(foreground.view()),
162 },
163 wgpu::BindGroupEntry {
164 binding: 2,
165 resource: wgpu::BindingResource::Sampler(&self.sampler),
166 },
167 ],
168 });
169
170 let mut encoder = app
171 .device()
172 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
173 label: Some("wisp::advanced_blend encoder"),
174 });
175
176 {
177 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
178 label: Some("wisp::advanced_blend pass"),
179 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
180 view: output.view(),
181 resolve_target: None,
182 ops: wgpu::Operations {
183 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
184 store: wgpu::StoreOp::Store,
185 },
186 })],
187 depth_stencil_attachment: None,
188 timestamp_writes: None,
189 occlusion_query_set: None,
190 });
191 pass.set_pipeline(pipeline);
192 pass.set_bind_group(0, &bg, &[]);
193 pass.draw(0..3, 0..1);
194 }
195
196 app.queue().submit(std::iter::once(encoder.finish()));
197 }
198}
199
200#[must_use]
205#[allow(
206 clippy::too_many_lines,
207 reason = "20 advanced blend modes × multi-line WGSL bodies; flat match reads better than 20 helper fns"
208)]
209fn blend_fn_body(mode: BlendMode) -> &'static str {
210 match mode {
211 BlendMode::Darken => {
213 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
214 return min(base, blend);
215 }"
216 }
217 BlendMode::Lighten => {
218 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
219 return max(base, blend);
220 }"
221 }
222 BlendMode::Difference => {
223 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
224 return abs(base - blend);
225 }"
226 }
227 BlendMode::Exclusion => {
228 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
229 return base + blend - 2.0 * base * blend;
230 }"
231 }
232 BlendMode::Negation => {
233 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
234 return vec3<f32>(1.0) - abs(vec3<f32>(1.0) - base - blend);
235 }"
236 }
237 BlendMode::LinearDodge => {
238 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
239 return clamp(base + blend, vec3<f32>(0.0), vec3<f32>(1.0));
240 }"
241 }
242 BlendMode::LinearBurn => {
243 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
244 return clamp(base + blend - vec3<f32>(1.0), vec3<f32>(0.0), vec3<f32>(1.0));
245 }"
246 }
247 BlendMode::LinearLight => {
248 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
249 return clamp(base + 2.0 * blend - vec3<f32>(1.0), vec3<f32>(0.0), vec3<f32>(1.0));
250 }"
251 }
252 BlendMode::Divide => {
253 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
254 let safe = max(blend, vec3<f32>(1e-6));
255 return clamp(base / safe, vec3<f32>(0.0), vec3<f32>(1.0));
256 }"
257 }
258
259 BlendMode::Overlay => {
261 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
262 let dark = 2.0 * base * blend;
263 let light = vec3<f32>(1.0) - 2.0 * (vec3<f32>(1.0) - base) * (vec3<f32>(1.0) - blend);
264 return select(light, dark, base < vec3<f32>(0.5));
265 }"
266 }
267 BlendMode::HardLight => {
268 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
269 let dark = 2.0 * base * blend;
270 let light = vec3<f32>(1.0) - 2.0 * (vec3<f32>(1.0) - base) * (vec3<f32>(1.0) - blend);
271 return select(light, dark, blend < vec3<f32>(0.5));
272 }"
273 }
274 BlendMode::SoftLight => {
275 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
276 let two_blend = 2.0 * blend;
277 let darken = base - (vec3<f32>(1.0) - two_blend) * base * (vec3<f32>(1.0) - base);
278 let one = vec3<f32>(1.0);
279 let denom = select(
280 sqrt(base),
281 ((16.0 * base - 12.0) * base + 3.0) * base,
282 base <= vec3<f32>(0.25)
283 );
284 let lighten = base + (two_blend - one) * (denom - base);
285 return select(lighten, darken, blend <= vec3<f32>(0.5));
286 }"
287 }
288 BlendMode::PinLight => {
289 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
290 let dark = min(base, 2.0 * blend);
291 let light = max(base, 2.0 * blend - vec3<f32>(1.0));
292 return select(light, dark, blend < vec3<f32>(0.5));
293 }"
294 }
295 BlendMode::HardMix => {
296 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
297 // VividLight thresholded: ≥ 0.5 → 1, < 0.5 → 0.
298 return step(vec3<f32>(0.5), base + blend - vec3<f32>(0.5));
299 }"
300 }
301 BlendMode::VividLight => {
302 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
303 let two_blend = 2.0 * blend;
304 let safe_lo = max(two_blend, vec3<f32>(1e-6));
305 let burn = vec3<f32>(1.0) - min(vec3<f32>(1.0), (vec3<f32>(1.0) - base) / safe_lo);
306 let safe_hi = max(vec3<f32>(1.0) - (two_blend - vec3<f32>(1.0)), vec3<f32>(1e-6));
307 let dodge = min(vec3<f32>(1.0), base / safe_hi);
308 return select(dodge, burn, blend < vec3<f32>(0.5));
309 }"
310 }
311 BlendMode::ColorBurn => {
312 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
313 let safe = max(blend, vec3<f32>(1e-6));
314 let result = vec3<f32>(1.0) - min(vec3<f32>(1.0), (vec3<f32>(1.0) - base) / safe);
315 return select(result, vec3<f32>(0.0), blend <= vec3<f32>(0.0));
316 }"
317 }
318 BlendMode::ColorDodge => {
319 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
320 let safe = max(vec3<f32>(1.0) - blend, vec3<f32>(1e-6));
321 let result = min(vec3<f32>(1.0), base / safe);
322 return select(result, vec3<f32>(1.0), blend >= vec3<f32>(1.0));
323 }"
324 }
325
326 BlendMode::Saturation => {
328 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
329 return set_lum(set_sat(base, sat(blend)), lum(base));
330 }"
331 }
332 BlendMode::Color => {
333 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
334 return set_lum(blend, lum(base));
335 }"
336 }
337 BlendMode::Luminosity => {
338 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
339 return set_lum(base, lum(blend));
340 }"
341 }
342
343 BlendMode::Normal
345 | BlendMode::Multiply
346 | BlendMode::Add
347 | BlendMode::Screen
348 | BlendMode::Subtract
349 | BlendMode::Min
350 | BlendMode::Max
351 | BlendMode::Erase => {
352 "fn blend_fn(base: vec3<f32>, blend: vec3<f32>) -> vec3<f32> {
353 return blend;
354 }"
355 }
356 }
357}
358
359#[cfg(test)]
360mod tests {
361 use super::*;
362
363 #[test]
364 fn every_advanced_mode_has_a_blend_fn() {
365 for mode in BlendMode::all().filter(|m| m.is_advanced()) {
370 let body = blend_fn_body(mode);
371 let wgsl = TEMPLATE.replace(PLACEHOLDER, body);
372 assert!(
373 wgsl.contains("fn blend_fn"),
374 "missing blend_fn for {mode:?}"
375 );
376 assert!(
377 !wgsl.contains(PLACEHOLDER),
378 "placeholder left in WGSL for {mode:?}"
379 );
380 }
381 }
382}