1use super::RenderNodeCpu;
2
3#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
7#[repr(u32)]
8pub enum BlendMode {
9 #[default]
11 Normal = 0,
12 Multiply = 1,
14 Screen = 2,
16 Overlay = 3,
18 SoftLight = 4,
20 HardLight = 5,
22 ColorDodge = 6,
24 ColorBurn = 7,
26 Difference = 8,
28 Exclusion = 9,
30 Add = 10,
32 Subtract = 11,
34 Darken = 12,
36 Lighten = 13,
38 Hue = 14,
40 Saturation = 15,
42 Color = 16,
44 Luminosity = 17,
46}
47
48#[cfg(feature = "wgpu")]
51struct BlendPipeline {
52 render_pipeline: wgpu::RenderPipeline,
53 bind_group_layout: wgpu::BindGroupLayout,
54 sampler: wgpu::Sampler,
55 uniform_buf: wgpu::Buffer,
56}
57
58pub struct BlendModeNode {
65 pub mode: BlendMode,
67 pub opacity: f32,
69 pub overlay_rgba: Vec<u8>,
71 pub overlay_width: u32,
73 pub overlay_height: u32,
75 #[cfg(feature = "wgpu")]
76 pipeline: std::sync::OnceLock<BlendPipeline>,
77}
78
79impl BlendModeNode {
80 #[must_use]
81 pub fn new(
82 mode: BlendMode,
83 opacity: f32,
84 overlay_rgba: Vec<u8>,
85 overlay_width: u32,
86 overlay_height: u32,
87 ) -> Self {
88 Self {
89 mode,
90 opacity,
91 overlay_rgba,
92 overlay_width,
93 overlay_height,
94 #[cfg(feature = "wgpu")]
95 pipeline: std::sync::OnceLock::new(),
96 }
97 }
98}
99
100#[allow(clippy::many_single_char_names, clippy::float_cmp)]
103fn rgb_to_hsl(r: f32, g: f32, b: f32) -> [f32; 3] {
104 let max_c = r.max(g).max(b);
105 let min_c = r.min(g).min(b);
106 let l = (max_c + min_c) * 0.5;
107 if (max_c - min_c).abs() < 1e-6 {
108 return [0.0, 0.0, l];
109 }
110 let delta = max_c - min_c;
111 let s = if l < 0.5 {
112 delta / (max_c + min_c)
113 } else {
114 delta / (2.0 - max_c - min_c)
115 };
116 let h = if max_c == r {
117 let raw = (g - b) / delta;
118 if g >= b { raw } else { raw + 6.0 }
119 } else if max_c == g {
120 (b - r) / delta + 2.0
121 } else {
122 (r - g) / delta + 4.0
123 } / 6.0;
124 [h, s, l]
125}
126
127fn hue_to_rgb_cpu(p: f32, q: f32, t_in: f32) -> f32 {
128 let t = t_in.rem_euclid(1.0);
129 if t < 1.0 / 6.0 {
130 return p + (q - p) * 6.0 * t;
131 }
132 if t < 0.5 {
133 return q;
134 }
135 if t < 2.0 / 3.0 {
136 return p + (q - p) * (2.0 / 3.0 - t) * 6.0;
137 }
138 p
139}
140
141#[allow(clippy::many_single_char_names)]
142fn hsl_to_rgb(h: f32, s: f32, l: f32) -> [f32; 3] {
143 if s.abs() < 1e-6 {
144 return [l, l, l];
145 }
146 let q = if l < 0.5 {
147 l * (1.0 + s)
148 } else {
149 l + s - l * s
150 };
151 let p = 2.0 * l - q;
152 [
153 hue_to_rgb_cpu(p, q, h + 1.0 / 3.0),
154 hue_to_rgb_cpu(p, q, h),
155 hue_to_rgb_cpu(p, q, h - 1.0 / 3.0),
156 ]
157}
158
159fn overlay_ch(b: f32, o: f32) -> f32 {
160 if b < 0.5 {
161 2.0 * b * o
162 } else {
163 1.0 - 2.0 * (1.0 - b) * (1.0 - o)
164 }
165}
166
167fn soft_light_d(b: f32) -> f32 {
168 if b <= 0.25 {
169 ((16.0 * b - 12.0) * b + 4.0) * b
170 } else {
171 b.sqrt()
172 }
173}
174
175fn soft_light_ch(b: f32, o: f32) -> f32 {
176 if o <= 0.5 {
177 b - (1.0 - 2.0 * o) * b * (1.0 - b)
178 } else {
179 b + (2.0 * o - 1.0) * (soft_light_d(b) - b)
180 }
181}
182
183#[allow(clippy::many_single_char_names)]
184fn blend_rgb(mode: BlendMode, base: [f32; 3], ov: [f32; 3]) -> [f32; 3] {
185 let [br, bg, bb] = base;
186 let [or, og, ob] = ov;
187 match mode {
188 BlendMode::Normal => ov,
189 BlendMode::Multiply => [br * or, bg * og, bb * ob],
190 BlendMode::Screen => [
191 1.0 - (1.0 - br) * (1.0 - or),
192 1.0 - (1.0 - bg) * (1.0 - og),
193 1.0 - (1.0 - bb) * (1.0 - ob),
194 ],
195 BlendMode::Overlay => [overlay_ch(br, or), overlay_ch(bg, og), overlay_ch(bb, ob)],
196 BlendMode::SoftLight => [
197 soft_light_ch(br, or),
198 soft_light_ch(bg, og),
199 soft_light_ch(bb, ob),
200 ],
201 BlendMode::HardLight => [overlay_ch(or, br), overlay_ch(og, bg), overlay_ch(ob, bb)],
202 BlendMode::ColorDodge => [
203 (br / (1.0 - or + 1e-4)).clamp(0.0, 1.0),
204 (bg / (1.0 - og + 1e-4)).clamp(0.0, 1.0),
205 (bb / (1.0 - ob + 1e-4)).clamp(0.0, 1.0),
206 ],
207 BlendMode::ColorBurn => [
208 (1.0 - (1.0 - br) / (or + 1e-4)).clamp(0.0, 1.0),
209 (1.0 - (1.0 - bg) / (og + 1e-4)).clamp(0.0, 1.0),
210 (1.0 - (1.0 - bb) / (ob + 1e-4)).clamp(0.0, 1.0),
211 ],
212 BlendMode::Difference => [(br - or).abs(), (bg - og).abs(), (bb - ob).abs()],
213 BlendMode::Exclusion => [
214 br + or - 2.0 * br * or,
215 bg + og - 2.0 * bg * og,
216 bb + ob - 2.0 * bb * ob,
217 ],
218 BlendMode::Add => [
219 (br + or).clamp(0.0, 1.0),
220 (bg + og).clamp(0.0, 1.0),
221 (bb + ob).clamp(0.0, 1.0),
222 ],
223 BlendMode::Subtract => [
224 (br - or).clamp(0.0, 1.0),
225 (bg - og).clamp(0.0, 1.0),
226 (bb - ob).clamp(0.0, 1.0),
227 ],
228 BlendMode::Darken => [br.min(or), bg.min(og), bb.min(ob)],
229 BlendMode::Lighten => [br.max(or), bg.max(og), bb.max(ob)],
230 BlendMode::Hue => {
231 let [_bh, bs, bl] = rgb_to_hsl(br, bg, bb);
232 let [oh, _, _] = rgb_to_hsl(or, og, ob);
233 hsl_to_rgb(oh, bs, bl)
234 }
235 BlendMode::Saturation => {
236 let [bh, bs, bl] = rgb_to_hsl(br, bg, bb);
237 let [_, os, _] = rgb_to_hsl(or, og, ob);
238 let _ = bs;
239 hsl_to_rgb(bh, os, bl)
240 }
241 BlendMode::Color => {
242 let [_, _, bl] = rgb_to_hsl(br, bg, bb);
243 let [oh, os, _] = rgb_to_hsl(or, og, ob);
244 hsl_to_rgb(oh, os, bl)
245 }
246 BlendMode::Luminosity => {
247 let [bh, bs, _] = rgb_to_hsl(br, bg, bb);
248 let [_, _, ol] = rgb_to_hsl(or, og, ob);
249 hsl_to_rgb(bh, bs, ol)
250 }
251 }
252}
253
254impl RenderNodeCpu for BlendModeNode {
255 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
256 fn process_cpu(&self, rgba: &mut [u8], _w: u32, _h: u32) {
257 if self.overlay_rgba.len() != rgba.len() {
258 log::warn!(
259 "BlendModeNode::process_cpu skipped: size mismatch base={} overlay={}",
260 rgba.len(),
261 self.overlay_rgba.len()
262 );
263 return;
264 }
265 for (base, ov) in rgba
266 .chunks_exact_mut(4)
267 .zip(self.overlay_rgba.chunks_exact(4))
268 {
269 let br = f32::from(base[0]) / 255.0;
270 let bg = f32::from(base[1]) / 255.0;
271 let bb = f32::from(base[2]) / 255.0;
272 let or = f32::from(ov[0]) / 255.0;
273 let og = f32::from(ov[1]) / 255.0;
274 let ob = f32::from(ov[2]) / 255.0;
275 let oa = f32::from(ov[3]) / 255.0;
276
277 let [rr, rg, rb] = blend_rgb(self.mode, [br, bg, bb], [or, og, ob]);
278 let eff_alpha = oa * self.opacity;
279 let out_r = (br + (rr - br) * eff_alpha).clamp(0.0, 1.0);
280 let out_g = (bg + (rg - bg) * eff_alpha).clamp(0.0, 1.0);
281 let out_b = (bb + (rb - bb) * eff_alpha).clamp(0.0, 1.0);
282 base[0] = (out_r * 255.0 + 0.5) as u8;
283 base[1] = (out_g * 255.0 + 0.5) as u8;
284 base[2] = (out_b * 255.0 + 0.5) as u8;
285 }
286 }
287}
288
289#[cfg(feature = "wgpu")]
292impl BlendModeNode {
293 #[allow(clippy::too_many_lines)]
294 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &BlendPipeline {
295 self.pipeline.get_or_init(|| {
296 let device = &ctx.device;
297 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
298 label: Some("Blend shader"),
299 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/blend.wgsl").into()),
300 });
301 let bgl = two_tex_sampler_uniform_bgl(device, "Blend");
302 let render_pipeline = fullscreen_pipeline(device, &shader, "Blend", &bgl);
303 let sampler = linear_sampler(device, "Blend");
304 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
305 label: Some("Blend uniforms"),
306 size: 16,
307 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
308 mapped_at_creation: false,
309 });
310 BlendPipeline {
311 render_pipeline,
312 bind_group_layout: bgl,
313 sampler,
314 uniform_buf,
315 }
316 })
317 }
318}
319
320#[cfg(feature = "wgpu")]
321impl super::RenderNode for BlendModeNode {
322 fn input_count(&self) -> usize {
323 2
324 }
325
326 fn process(
327 &self,
328 inputs: &[&wgpu::Texture],
329 outputs: &[&wgpu::Texture],
330 ctx: &crate::context::RenderContext,
331 ) {
332 let Some(tex_base) = inputs.first() else {
333 log::warn!("BlendModeNode::process called with no inputs");
334 return;
335 };
336 let Some(output) = outputs.first() else {
337 log::warn!("BlendModeNode::process called with no outputs");
338 return;
339 };
340 let pd = self.get_or_create_pipeline(ctx);
341
342 let ov_tex = upload_rgba_texture(
344 ctx,
345 &self.overlay_rgba,
346 self.overlay_width,
347 self.overlay_height,
348 "Blend overlay",
349 );
350
351 let mode_bytes = (self.mode as u32).to_le_bytes();
353 let opac_bytes = self.opacity.to_le_bytes();
354 let uniforms: [u8; 16] = [
355 mode_bytes[0],
356 mode_bytes[1],
357 mode_bytes[2],
358 mode_bytes[3],
359 opac_bytes[0],
360 opac_bytes[1],
361 opac_bytes[2],
362 opac_bytes[3],
363 0,
364 0,
365 0,
366 0,
367 0,
368 0,
369 0,
370 0,
371 ];
372 ctx.queue.write_buffer(&pd.uniform_buf, 0, &uniforms);
373
374 let base_view = tex_base.create_view(&wgpu::TextureViewDescriptor::default());
375 let ov_view = ov_tex.create_view(&wgpu::TextureViewDescriptor::default());
376 let out_view = output.create_view(&wgpu::TextureViewDescriptor::default());
377
378 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
379 label: Some("Blend BG"),
380 layout: &pd.bind_group_layout,
381 entries: &[
382 wgpu::BindGroupEntry {
383 binding: 0,
384 resource: wgpu::BindingResource::TextureView(&base_view),
385 },
386 wgpu::BindGroupEntry {
387 binding: 1,
388 resource: wgpu::BindingResource::TextureView(&ov_view),
389 },
390 wgpu::BindGroupEntry {
391 binding: 2,
392 resource: wgpu::BindingResource::Sampler(&pd.sampler),
393 },
394 wgpu::BindGroupEntry {
395 binding: 3,
396 resource: pd.uniform_buf.as_entire_binding(),
397 },
398 ],
399 });
400
401 submit_render_pass(ctx, &pd.render_pipeline, &bind_group, &out_view, "Blend");
402 }
403}
404
405#[cfg(feature = "wgpu")]
408struct TransformPipeline {
409 render_pipeline: wgpu::RenderPipeline,
410 bind_group_layout: wgpu::BindGroupLayout,
411 sampler: wgpu::Sampler,
412 uniform_buf: wgpu::Buffer,
413}
414
415pub struct TransformNode {
423 pub translate: [f32; 2],
425 pub rotate: f32,
427 pub scale: [f32; 2],
429 #[cfg(feature = "wgpu")]
430 pipeline: std::sync::OnceLock<TransformPipeline>,
431}
432
433impl TransformNode {
434 #[must_use]
435 pub fn new(translate: [f32; 2], rotate: f32, scale: [f32; 2]) -> Self {
436 Self {
437 translate,
438 rotate,
439 scale,
440 #[cfg(feature = "wgpu")]
441 pipeline: std::sync::OnceLock::new(),
442 }
443 }
444}
445
446impl Default for TransformNode {
447 fn default() -> Self {
448 Self::new([0.0, 0.0], 0.0, [1.0, 1.0])
449 }
450}
451
452impl RenderNodeCpu for TransformNode {
453 fn process_cpu(&self, _rgba: &mut [u8], _w: u32, _h: u32) {
454 }
456}
457
458#[cfg(feature = "wgpu")]
459impl TransformNode {
460 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &TransformPipeline {
461 self.pipeline.get_or_init(|| {
462 let device = &ctx.device;
463 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
464 label: Some("Transform shader"),
465 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/transform.wgsl").into()),
466 });
467 let bgl = one_tex_sampler_uniform_bgl(device, "Transform");
468 let render_pipeline = fullscreen_pipeline(device, &shader, "Transform", &bgl);
469 let sampler = linear_sampler(device, "Transform");
470 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
472 label: Some("Transform uniforms"),
473 size: 32,
474 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
475 mapped_at_creation: false,
476 });
477 TransformPipeline {
478 render_pipeline,
479 bind_group_layout: bgl,
480 sampler,
481 uniform_buf,
482 }
483 })
484 }
485}
486
487#[cfg(feature = "wgpu")]
488impl super::RenderNode for TransformNode {
489 fn process(
490 &self,
491 inputs: &[&wgpu::Texture],
492 outputs: &[&wgpu::Texture],
493 ctx: &crate::context::RenderContext,
494 ) {
495 let Some(input) = inputs.first() else {
496 log::warn!("TransformNode::process called with no inputs");
497 return;
498 };
499 let Some(output) = outputs.first() else {
500 log::warn!("TransformNode::process called with no outputs");
501 return;
502 };
503 let pd = self.get_or_create_pipeline(ctx);
504
505 let uniforms = pack_f32(&[
507 self.translate[0],
508 self.translate[1],
509 self.rotate,
510 0.0,
511 self.scale[0],
512 self.scale[1],
513 0.0,
514 0.0,
515 ]);
516 ctx.queue.write_buffer(&pd.uniform_buf, 0, &uniforms);
517
518 let in_view = input.create_view(&wgpu::TextureViewDescriptor::default());
519 let out_view = output.create_view(&wgpu::TextureViewDescriptor::default());
520
521 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
522 label: Some("Transform BG"),
523 layout: &pd.bind_group_layout,
524 entries: &[
525 wgpu::BindGroupEntry {
526 binding: 0,
527 resource: wgpu::BindingResource::TextureView(&in_view),
528 },
529 wgpu::BindGroupEntry {
530 binding: 1,
531 resource: wgpu::BindingResource::Sampler(&pd.sampler),
532 },
533 wgpu::BindGroupEntry {
534 binding: 2,
535 resource: pd.uniform_buf.as_entire_binding(),
536 },
537 ],
538 });
539 submit_render_pass(
540 ctx,
541 &pd.render_pipeline,
542 &bind_group,
543 &out_view,
544 "Transform",
545 );
546 }
547}
548
549#[cfg(feature = "wgpu")]
552struct ChromaKeyPipeline {
553 render_pipeline: wgpu::RenderPipeline,
554 bind_group_layout: wgpu::BindGroupLayout,
555 sampler: wgpu::Sampler,
556 uniform_buf: wgpu::Buffer,
557}
558
559pub struct ChromaKeyNode {
567 pub key_color: [f32; 3],
569 pub tolerance: f32,
571 pub softness: f32,
573 #[cfg(feature = "wgpu")]
574 pipeline: std::sync::OnceLock<ChromaKeyPipeline>,
575}
576
577impl ChromaKeyNode {
578 #[must_use]
579 pub fn new(key_color: [f32; 3], tolerance: f32, softness: f32) -> Self {
580 Self {
581 key_color,
582 tolerance,
583 softness,
584 #[cfg(feature = "wgpu")]
585 pipeline: std::sync::OnceLock::new(),
586 }
587 }
588}
589
590fn bt709_luma(r: f32, g: f32, b: f32) -> f32 {
593 0.2126 * r + 0.7152 * g + 0.0722 * b
594}
595
596fn chroma_dist_cpu(pixel: [f32; 3], key: [f32; 3]) -> f32 {
597 let pl = bt709_luma(pixel[0], pixel[1], pixel[2]);
598 let kl = bt709_luma(key[0], key[1], key[2]);
599 let dp = [pixel[0] - pl, pixel[1] - pl, pixel[2] - pl];
600 let dk = [key[0] - kl, key[1] - kl, key[2] - kl];
601 let d = [dp[0] - dk[0], dp[1] - dk[1], dp[2] - dk[2]];
602 (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
603}
604
605fn smoothstep(edge0: f32, edge1: f32, x: f32) -> f32 {
606 let t = ((x - edge0) / (edge1 - edge0)).clamp(0.0, 1.0);
607 t * t * (3.0 - 2.0 * t)
608}
609
610impl RenderNodeCpu for ChromaKeyNode {
611 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
612 fn process_cpu(&self, rgba: &mut [u8], _w: u32, _h: u32) {
613 for pixel in rgba.chunks_exact_mut(4) {
614 let r = f32::from(pixel[0]) / 255.0;
615 let g = f32::from(pixel[1]) / 255.0;
616 let b = f32::from(pixel[2]) / 255.0;
617 let a = f32::from(pixel[3]) / 255.0;
618 let dist = chroma_dist_cpu([r, g, b], self.key_color);
619 let alpha_factor = smoothstep(
620 self.tolerance - self.softness,
621 self.tolerance + self.softness,
622 dist,
623 );
624 pixel[3] = ((a * alpha_factor).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
625 }
626 }
627}
628
629#[cfg(feature = "wgpu")]
630impl ChromaKeyNode {
631 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &ChromaKeyPipeline {
632 self.pipeline.get_or_init(|| {
633 let device = &ctx.device;
634 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
635 label: Some("ChromaKey shader"),
636 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/chroma_key.wgsl").into()),
637 });
638 let bgl = one_tex_sampler_uniform_bgl(device, "ChromaKey");
639 let render_pipeline = fullscreen_pipeline(device, &shader, "ChromaKey", &bgl);
640 let sampler = linear_sampler(device, "ChromaKey");
641 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
643 label: Some("ChromaKey uniforms"),
644 size: 32,
645 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
646 mapped_at_creation: false,
647 });
648 ChromaKeyPipeline {
649 render_pipeline,
650 bind_group_layout: bgl,
651 sampler,
652 uniform_buf,
653 }
654 })
655 }
656}
657
658#[cfg(feature = "wgpu")]
659impl super::RenderNode for ChromaKeyNode {
660 fn process(
661 &self,
662 inputs: &[&wgpu::Texture],
663 outputs: &[&wgpu::Texture],
664 ctx: &crate::context::RenderContext,
665 ) {
666 let Some(input) = inputs.first() else {
667 log::warn!("ChromaKeyNode::process called with no inputs");
668 return;
669 };
670 let Some(output) = outputs.first() else {
671 log::warn!("ChromaKeyNode::process called with no outputs");
672 return;
673 };
674 let pd = self.get_or_create_pipeline(ctx);
675
676 let uniforms = pack_f32(&[
677 self.key_color[0],
678 self.key_color[1],
679 self.key_color[2],
680 self.tolerance,
681 self.softness,
682 0.0,
683 0.0,
684 0.0,
685 ]);
686 ctx.queue.write_buffer(&pd.uniform_buf, 0, &uniforms);
687
688 let in_view = input.create_view(&wgpu::TextureViewDescriptor::default());
689 let out_view = output.create_view(&wgpu::TextureViewDescriptor::default());
690
691 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
692 label: Some("ChromaKey BG"),
693 layout: &pd.bind_group_layout,
694 entries: &[
695 wgpu::BindGroupEntry {
696 binding: 0,
697 resource: wgpu::BindingResource::TextureView(&in_view),
698 },
699 wgpu::BindGroupEntry {
700 binding: 1,
701 resource: wgpu::BindingResource::Sampler(&pd.sampler),
702 },
703 wgpu::BindGroupEntry {
704 binding: 2,
705 resource: pd.uniform_buf.as_entire_binding(),
706 },
707 ],
708 });
709 submit_render_pass(
710 ctx,
711 &pd.render_pipeline,
712 &bind_group,
713 &out_view,
714 "ChromaKey",
715 );
716 }
717}
718
719#[cfg(feature = "wgpu")]
722struct MaskPipeline {
723 render_pipeline: wgpu::RenderPipeline,
724 bind_group_layout: wgpu::BindGroupLayout,
725 sampler: wgpu::Sampler,
726 uniform_buf: wgpu::Buffer,
727}
728
729#[cfg(feature = "wgpu")]
730fn create_mask_pipeline(ctx: &crate::context::RenderContext) -> MaskPipeline {
731 let device = &ctx.device;
732 let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
733 label: Some("Mask shader"),
734 source: wgpu::ShaderSource::Wgsl(include_str!("../shaders/mask.wgsl").into()),
735 });
736 let bgl = two_tex_sampler_uniform_bgl(device, "Mask");
737 let render_pipeline = fullscreen_pipeline(device, &shader, "Mask", &bgl);
738 let sampler = linear_sampler(device, "Mask");
739 let uniform_buf = device.create_buffer(&wgpu::BufferDescriptor {
740 label: Some("Mask uniforms"),
741 size: 16,
742 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
743 mapped_at_creation: false,
744 });
745 MaskPipeline {
746 render_pipeline,
747 bind_group_layout: bgl,
748 sampler,
749 uniform_buf,
750 }
751}
752
753#[cfg(feature = "wgpu")]
754fn submit_mask_pass(
755 ctx: &crate::context::RenderContext,
756 pd: &MaskPipeline,
757 base_tex: &wgpu::Texture,
758 mask_tex: &wgpu::Texture,
759 output_tex: &wgpu::Texture,
760 mode: u32,
761 label: &str,
762) {
763 let mode_bytes = mode.to_le_bytes();
764 let uniforms: [u8; 16] = [
765 mode_bytes[0],
766 mode_bytes[1],
767 mode_bytes[2],
768 mode_bytes[3],
769 0,
770 0,
771 0,
772 0,
773 0,
774 0,
775 0,
776 0,
777 0,
778 0,
779 0,
780 0,
781 ];
782 ctx.queue.write_buffer(&pd.uniform_buf, 0, &uniforms);
783
784 let base_view = base_tex.create_view(&wgpu::TextureViewDescriptor::default());
785 let mask_view = mask_tex.create_view(&wgpu::TextureViewDescriptor::default());
786 let out_view = output_tex.create_view(&wgpu::TextureViewDescriptor::default());
787
788 let bind_group = ctx.device.create_bind_group(&wgpu::BindGroupDescriptor {
789 label: Some(label),
790 layout: &pd.bind_group_layout,
791 entries: &[
792 wgpu::BindGroupEntry {
793 binding: 0,
794 resource: wgpu::BindingResource::TextureView(&base_view),
795 },
796 wgpu::BindGroupEntry {
797 binding: 1,
798 resource: wgpu::BindingResource::TextureView(&mask_view),
799 },
800 wgpu::BindGroupEntry {
801 binding: 2,
802 resource: wgpu::BindingResource::Sampler(&pd.sampler),
803 },
804 wgpu::BindGroupEntry {
805 binding: 3,
806 resource: pd.uniform_buf.as_entire_binding(),
807 },
808 ],
809 });
810 submit_render_pass(ctx, &pd.render_pipeline, &bind_group, &out_view, label);
811}
812
813pub struct ShapeMaskNode {
820 pub mask_rgba: Vec<u8>,
822 pub mask_width: u32,
824 pub mask_height: u32,
826 #[cfg(feature = "wgpu")]
827 pipeline: std::sync::OnceLock<MaskPipeline>,
828}
829
830impl ShapeMaskNode {
831 #[must_use]
832 pub fn new(mask_rgba: Vec<u8>, mask_width: u32, mask_height: u32) -> Self {
833 Self {
834 mask_rgba,
835 mask_width,
836 mask_height,
837 #[cfg(feature = "wgpu")]
838 pipeline: std::sync::OnceLock::new(),
839 }
840 }
841}
842
843impl RenderNodeCpu for ShapeMaskNode {
844 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
845 fn process_cpu(&self, rgba: &mut [u8], _w: u32, _h: u32) {
846 if self.mask_rgba.len() != rgba.len() {
847 return;
848 }
849 for (base, mask) in rgba.chunks_exact_mut(4).zip(self.mask_rgba.chunks_exact(4)) {
850 let keep = if mask[3] > 1 { 1.0_f32 } else { 0.0_f32 };
851 let a = f32::from(base[3]) / 255.0;
852 base[3] = ((a * keep).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
853 }
854 }
855}
856
857#[cfg(feature = "wgpu")]
858impl ShapeMaskNode {
859 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &MaskPipeline {
860 self.pipeline.get_or_init(|| create_mask_pipeline(ctx))
861 }
862}
863
864#[cfg(feature = "wgpu")]
865impl super::RenderNode for ShapeMaskNode {
866 fn input_count(&self) -> usize {
867 2
868 }
869
870 fn process(
871 &self,
872 inputs: &[&wgpu::Texture],
873 outputs: &[&wgpu::Texture],
874 ctx: &crate::context::RenderContext,
875 ) {
876 let Some(base_tex) = inputs.first() else {
877 log::warn!("ShapeMaskNode::process called with no inputs");
878 return;
879 };
880 let Some(output) = outputs.first() else {
881 log::warn!("ShapeMaskNode::process called with no outputs");
882 return;
883 };
884 let pd = self.get_or_create_pipeline(ctx);
885 let mask_tex = upload_rgba_texture(
886 ctx,
887 &self.mask_rgba,
888 self.mask_width,
889 self.mask_height,
890 "ShapeMask mask",
891 );
892 submit_mask_pass(ctx, pd, base_tex, &mask_tex, output, 0, "ShapeMask BG");
893 }
894}
895
896pub struct LumaMaskNode {
902 pub mask_rgba: Vec<u8>,
904 pub mask_width: u32,
906 pub mask_height: u32,
908 #[cfg(feature = "wgpu")]
909 pipeline: std::sync::OnceLock<MaskPipeline>,
910}
911
912impl LumaMaskNode {
913 #[must_use]
914 pub fn new(mask_rgba: Vec<u8>, mask_width: u32, mask_height: u32) -> Self {
915 Self {
916 mask_rgba,
917 mask_width,
918 mask_height,
919 #[cfg(feature = "wgpu")]
920 pipeline: std::sync::OnceLock::new(),
921 }
922 }
923}
924
925impl RenderNodeCpu for LumaMaskNode {
926 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
927 fn process_cpu(&self, rgba: &mut [u8], _w: u32, _h: u32) {
928 if self.mask_rgba.len() != rgba.len() {
929 return;
930 }
931 for (base, mask) in rgba.chunks_exact_mut(4).zip(self.mask_rgba.chunks_exact(4)) {
932 let mr = f32::from(mask[0]) / 255.0;
933 let mg = f32::from(mask[1]) / 255.0;
934 let mb = f32::from(mask[2]) / 255.0;
935 let luma = bt709_luma(mr, mg, mb);
936 let ba = f32::from(base[3]) / 255.0;
937 base[3] = ((ba * luma).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
938 }
939 }
940}
941
942#[cfg(feature = "wgpu")]
943impl LumaMaskNode {
944 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &MaskPipeline {
945 self.pipeline.get_or_init(|| create_mask_pipeline(ctx))
946 }
947}
948
949#[cfg(feature = "wgpu")]
950impl super::RenderNode for LumaMaskNode {
951 fn input_count(&self) -> usize {
952 2
953 }
954
955 fn process(
956 &self,
957 inputs: &[&wgpu::Texture],
958 outputs: &[&wgpu::Texture],
959 ctx: &crate::context::RenderContext,
960 ) {
961 let Some(base_tex) = inputs.first() else {
962 log::warn!("LumaMaskNode::process called with no inputs");
963 return;
964 };
965 let Some(output) = outputs.first() else {
966 log::warn!("LumaMaskNode::process called with no outputs");
967 return;
968 };
969 let pd = self.get_or_create_pipeline(ctx);
970 let mask_tex = upload_rgba_texture(
971 ctx,
972 &self.mask_rgba,
973 self.mask_width,
974 self.mask_height,
975 "LumaMask mask",
976 );
977 submit_mask_pass(ctx, pd, base_tex, &mask_tex, output, 1, "LumaMask BG");
978 }
979}
980
981pub struct AlphaMatteNode {
988 pub background_rgba: Vec<u8>,
990 pub background_width: u32,
992 pub background_height: u32,
994 #[cfg(feature = "wgpu")]
995 pipeline: std::sync::OnceLock<MaskPipeline>,
996}
997
998impl AlphaMatteNode {
999 #[must_use]
1000 pub fn new(background_rgba: Vec<u8>, background_width: u32, background_height: u32) -> Self {
1001 Self {
1002 background_rgba,
1003 background_width,
1004 background_height,
1005 #[cfg(feature = "wgpu")]
1006 pipeline: std::sync::OnceLock::new(),
1007 }
1008 }
1009}
1010
1011impl RenderNodeCpu for AlphaMatteNode {
1012 #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
1013 fn process_cpu(&self, rgba: &mut [u8], _w: u32, _h: u32) {
1014 if self.background_rgba.len() != rgba.len() {
1015 return;
1016 }
1017 for (fg, bg) in rgba
1018 .chunks_exact_mut(4)
1019 .zip(self.background_rgba.chunks_exact(4))
1020 {
1021 let fa = f32::from(fg[3]) / 255.0;
1022 let ba = f32::from(bg[3]) / 255.0;
1023 for ch in 0..3 {
1024 let fc = f32::from(fg[ch]) / 255.0;
1025 let bc = f32::from(bg[ch]) / 255.0;
1026 fg[ch] = ((fc * fa + bc * (1.0 - fa)).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1027 }
1028 fg[3] = ((fa + ba * (1.0 - fa)).clamp(0.0, 1.0) * 255.0 + 0.5) as u8;
1029 }
1030 }
1031}
1032
1033#[cfg(feature = "wgpu")]
1034impl AlphaMatteNode {
1035 fn get_or_create_pipeline(&self, ctx: &crate::context::RenderContext) -> &MaskPipeline {
1036 self.pipeline.get_or_init(|| create_mask_pipeline(ctx))
1037 }
1038}
1039
1040#[cfg(feature = "wgpu")]
1041impl super::RenderNode for AlphaMatteNode {
1042 fn input_count(&self) -> usize {
1043 2
1044 }
1045
1046 fn process(
1047 &self,
1048 inputs: &[&wgpu::Texture],
1049 outputs: &[&wgpu::Texture],
1050 ctx: &crate::context::RenderContext,
1051 ) {
1052 let Some(fg_tex) = inputs.first() else {
1053 log::warn!("AlphaMatteNode::process called with no inputs");
1054 return;
1055 };
1056 let Some(output) = outputs.first() else {
1057 log::warn!("AlphaMatteNode::process called with no outputs");
1058 return;
1059 };
1060 let pd = self.get_or_create_pipeline(ctx);
1061 let bg_tex = upload_rgba_texture(
1062 ctx,
1063 &self.background_rgba,
1064 self.background_width,
1065 self.background_height,
1066 "AlphaMatte bg",
1067 );
1068 submit_mask_pass(ctx, pd, fg_tex, &bg_tex, output, 2, "AlphaMatte BG");
1069 }
1070}
1071
1072#[cfg(test)]
1075mod tests {
1076 use super::*;
1077
1078 #[test]
1081 fn blend_mode_multiply_should_produce_product_of_base_and_overlay() {
1082 let grey50 = vec![128u8, 128, 128, 255];
1084 let node = BlendModeNode::new(BlendMode::Multiply, 1.0, grey50.clone(), 1, 1);
1085 let mut rgba = grey50;
1086 node.process_cpu(&mut rgba, 1, 1);
1087 let expected = (128.0_f32 / 255.0 * 128.0 / 255.0 * 255.0 + 0.5) as u8;
1089 let diff = (rgba[0] as i32 - expected as i32).abs();
1090 assert!(
1091 diff <= 1,
1092 "Multiply 50%×50% grey: expected ~{expected}, got {}",
1093 rgba[0]
1094 );
1095 }
1096
1097 #[test]
1098 fn blend_mode_screen_should_be_brighter_than_either_input() {
1099 let base = vec![100u8, 100, 100, 255];
1100 let overlay = vec![150u8, 150, 150, 255];
1101 let node = BlendModeNode::new(BlendMode::Screen, 1.0, overlay, 1, 1);
1102 let mut rgba = base;
1103 node.process_cpu(&mut rgba, 1, 1);
1104 assert!(
1105 rgba[0] > 150,
1106 "Screen must be brighter than max input; got {}",
1107 rgba[0]
1108 );
1109 }
1110
1111 #[test]
1112 fn blend_mode_normal_at_full_opacity_should_replace_base_with_overlay() {
1113 let base = vec![50u8, 50, 50, 255];
1114 let overlay = vec![200u8, 100, 50, 255];
1115 let node = BlendModeNode::new(BlendMode::Normal, 1.0, overlay, 1, 1);
1116 let mut rgba = base;
1117 node.process_cpu(&mut rgba, 1, 1);
1118 assert!(
1119 (rgba[0] as i32 - 200).abs() <= 1,
1120 "R should match overlay; got {}",
1121 rgba[0]
1122 );
1123 assert!(
1124 (rgba[1] as i32 - 100).abs() <= 1,
1125 "G should match overlay; got {}",
1126 rgba[1]
1127 );
1128 }
1129
1130 #[test]
1131 fn blend_mode_normal_at_zero_opacity_should_leave_base_unchanged() {
1132 let base = vec![50u8, 80, 120, 255];
1133 let overlay = vec![200u8, 200, 200, 255];
1134 let node = BlendModeNode::new(BlendMode::Normal, 0.0, overlay, 1, 1);
1135 let mut rgba = base.clone();
1136 node.process_cpu(&mut rgba, 1, 1);
1137 assert!(
1138 (rgba[0] as i32 - 50).abs() <= 1,
1139 "R should match base; got {}",
1140 rgba[0]
1141 );
1142 }
1143
1144 #[test]
1145 fn blend_mode_difference_of_equal_pixels_should_be_black() {
1146 let grey = vec![128u8, 128, 128, 255];
1147 let node = BlendModeNode::new(BlendMode::Difference, 1.0, grey.clone(), 1, 1);
1148 let mut rgba = grey;
1149 node.process_cpu(&mut rgba, 1, 1);
1150 assert!(
1151 rgba[0] <= 1,
1152 "Difference of same pixel must be ~black; got {}",
1153 rgba[0]
1154 );
1155 }
1156
1157 #[test]
1158 fn blend_mode_add_should_clamp_at_white() {
1159 let bright = vec![200u8, 200, 200, 255];
1160 let node = BlendModeNode::new(BlendMode::Add, 1.0, bright.clone(), 1, 1);
1161 let mut rgba = bright;
1162 node.process_cpu(&mut rgba, 1, 1);
1163 assert_eq!(rgba[0], 255, "Add of two bright values must clamp to 255");
1164 }
1165
1166 #[test]
1167 fn blend_mode_darken_should_return_minimum_channel() {
1168 let base = vec![100u8, 200, 50, 255];
1169 let overlay = vec![150u8, 50, 100, 255];
1170 let node = BlendModeNode::new(BlendMode::Darken, 1.0, overlay, 1, 1);
1171 let mut rgba = base;
1172 node.process_cpu(&mut rgba, 1, 1);
1173 assert!(
1174 (rgba[0] as i32 - 100).abs() <= 1,
1175 "Darken R: min(100,150)=100; got {}",
1176 rgba[0]
1177 );
1178 assert!(
1179 (rgba[1] as i32 - 50).abs() <= 1,
1180 "Darken G: min(200,50)=50; got {}",
1181 rgba[1]
1182 );
1183 assert!(
1184 (rgba[2] as i32 - 50).abs() <= 1,
1185 "Darken B: min(50,100)=50; got {}",
1186 rgba[2]
1187 );
1188 }
1189
1190 #[test]
1191 fn blend_mode_size_mismatch_should_be_noop() {
1192 let overlay = vec![200u8; 8];
1193 let node = BlendModeNode::new(BlendMode::Normal, 1.0, overlay, 2, 1);
1194 let original = vec![50u8, 80, 120, 255];
1195 let mut rgba = original.clone();
1196 node.process_cpu(&mut rgba, 1, 1);
1197 assert_eq!(rgba, original, "size mismatch must leave base unchanged");
1198 }
1199
1200 #[test]
1203 fn transform_node_cpu_path_should_be_passthrough() {
1204 let node = TransformNode::new([0.1, 0.0], 0.0, [2.0, 2.0]);
1205 let original = vec![10u8, 20, 30, 255];
1206 let mut rgba = original.clone();
1207 node.process_cpu(&mut rgba, 1, 1);
1208 assert_eq!(rgba, original, "TransformNode CPU must be a no-op");
1209 }
1210
1211 #[test]
1212 fn transform_node_default_should_be_identity() {
1213 let node = TransformNode::default();
1214 assert_eq!(node.translate, [0.0, 0.0]);
1215 assert_eq!(node.rotate, 0.0);
1216 assert_eq!(node.scale, [1.0, 1.0]);
1217 }
1218
1219 #[test]
1222 fn chroma_key_node_pure_green_should_become_transparent() {
1223 let mut rgba = vec![0u8, 255, 0, 255]; let node = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.1, 0.05);
1225 node.process_cpu(&mut rgba, 1, 1);
1226 assert_eq!(
1227 rgba[3], 0,
1228 "pure green key must produce fully transparent alpha"
1229 );
1230 }
1231
1232 #[test]
1233 fn chroma_key_node_non_key_colour_should_stay_opaque() {
1234 let mut rgba = vec![255u8, 0, 0, 255]; let node = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.1, 0.05);
1236 node.process_cpu(&mut rgba, 1, 1);
1237 assert!(
1238 rgba[3] > 200,
1239 "non-key colour must stay opaque; got alpha={}",
1240 rgba[3]
1241 );
1242 }
1243
1244 #[test]
1245 fn chroma_key_node_tolerances_should_control_threshold() {
1246 let mut rgba_tight = vec![0u8, 100, 0, 255]; let mut rgba_loose = rgba_tight.clone();
1249 let node_tight = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.05, 0.01);
1250 let node_loose = ChromaKeyNode::new([0.0, 1.0, 0.0], 0.8, 0.1);
1251 node_tight.process_cpu(&mut rgba_tight, 1, 1);
1252 node_loose.process_cpu(&mut rgba_loose, 1, 1);
1253 assert!(
1254 rgba_loose[3] < rgba_tight[3],
1255 "loose tolerance must key more aggressively than tight"
1256 );
1257 }
1258
1259 #[test]
1262 fn shape_mask_node_opaque_mask_should_keep_base_alpha() {
1263 let mask = vec![0u8, 0, 0, 255]; let node = ShapeMaskNode::new(mask, 1, 1);
1265 let mut rgba = vec![128u8, 128, 128, 200];
1266 node.process_cpu(&mut rgba, 1, 1);
1267 assert!(
1268 (rgba[3] as i32 - 200).abs() <= 1,
1269 "opaque mask must preserve base alpha"
1270 );
1271 }
1272
1273 #[test]
1274 fn shape_mask_node_transparent_mask_should_zero_alpha() {
1275 let mask = vec![255u8, 255, 255, 0]; let node = ShapeMaskNode::new(mask, 1, 1);
1277 let mut rgba = vec![128u8, 128, 128, 255];
1278 node.process_cpu(&mut rgba, 1, 1);
1279 assert_eq!(rgba[3], 0, "transparent mask must produce zero alpha");
1280 }
1281
1282 #[test]
1285 fn luma_mask_node_white_mask_should_preserve_alpha() {
1286 let mask = vec![255u8, 255, 255, 255]; let node = LumaMaskNode::new(mask, 1, 1);
1288 let mut rgba = vec![100u8, 100, 100, 200];
1289 node.process_cpu(&mut rgba, 1, 1);
1290 assert!(
1291 (rgba[3] as i32 - 200).abs() <= 2,
1292 "white mask preserves alpha"
1293 );
1294 }
1295
1296 #[test]
1297 fn luma_mask_node_black_mask_should_zero_alpha() {
1298 let mask = vec![0u8, 0, 0, 255]; let node = LumaMaskNode::new(mask, 1, 1);
1300 let mut rgba = vec![100u8, 100, 100, 255];
1301 node.process_cpu(&mut rgba, 1, 1);
1302 assert_eq!(rgba[3], 0, "black mask must zero out alpha");
1303 }
1304
1305 #[test]
1308 fn alpha_matte_node_opaque_fg_should_replace_background() {
1309 let bg = vec![50u8, 50, 50, 255];
1310 let node = AlphaMatteNode::new(bg, 1, 1);
1311 let mut fg = vec![200u8, 100, 50, 255]; node.process_cpu(&mut fg, 1, 1);
1313 assert!(
1314 (fg[0] as i32 - 200).abs() <= 1,
1315 "opaque fg must dominate; got {}",
1316 fg[0]
1317 );
1318 }
1319
1320 #[test]
1321 fn alpha_matte_node_transparent_fg_should_show_background() {
1322 let bg = vec![50u8, 80, 120, 255];
1323 let node = AlphaMatteNode::new(bg, 1, 1);
1324 let mut fg = vec![200u8, 200, 200, 0]; node.process_cpu(&mut fg, 1, 1);
1326 assert!(
1327 (fg[0] as i32 - 50).abs() <= 1,
1328 "transparent fg must show bg; got {}",
1329 fg[0]
1330 );
1331 }
1332
1333 #[test]
1336 fn all_blend_mode_variants_should_compile() {
1337 let modes = [
1338 BlendMode::Normal,
1339 BlendMode::Multiply,
1340 BlendMode::Screen,
1341 BlendMode::Overlay,
1342 BlendMode::SoftLight,
1343 BlendMode::HardLight,
1344 BlendMode::ColorDodge,
1345 BlendMode::ColorBurn,
1346 BlendMode::Difference,
1347 BlendMode::Exclusion,
1348 BlendMode::Add,
1349 BlendMode::Subtract,
1350 BlendMode::Darken,
1351 BlendMode::Lighten,
1352 BlendMode::Hue,
1353 BlendMode::Saturation,
1354 BlendMode::Color,
1355 BlendMode::Luminosity,
1356 ];
1357 assert_eq!(modes.len(), 18);
1358 }
1359}
1360
1361#[cfg(feature = "wgpu")]
1364pub(crate) fn linear_sampler(device: &wgpu::Device, label: &str) -> wgpu::Sampler {
1365 device.create_sampler(&wgpu::SamplerDescriptor {
1366 label: Some(&format!("{label} sampler")),
1367 address_mode_u: wgpu::AddressMode::ClampToEdge,
1368 address_mode_v: wgpu::AddressMode::ClampToEdge,
1369 mag_filter: wgpu::FilterMode::Linear,
1370 min_filter: wgpu::FilterMode::Linear,
1371 ..Default::default()
1372 })
1373}
1374
1375#[cfg(feature = "wgpu")]
1377pub(crate) fn one_tex_sampler_uniform_bgl(
1378 device: &wgpu::Device,
1379 label: &str,
1380) -> wgpu::BindGroupLayout {
1381 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1382 label: Some(&format!("{label} BGL")),
1383 entries: &[
1384 wgpu::BindGroupLayoutEntry {
1385 binding: 0,
1386 visibility: wgpu::ShaderStages::FRAGMENT,
1387 ty: wgpu::BindingType::Texture {
1388 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1389 view_dimension: wgpu::TextureViewDimension::D2,
1390 multisampled: false,
1391 },
1392 count: None,
1393 },
1394 wgpu::BindGroupLayoutEntry {
1395 binding: 1,
1396 visibility: wgpu::ShaderStages::FRAGMENT,
1397 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1398 count: None,
1399 },
1400 wgpu::BindGroupLayoutEntry {
1401 binding: 2,
1402 visibility: wgpu::ShaderStages::FRAGMENT,
1403 ty: wgpu::BindingType::Buffer {
1404 ty: wgpu::BufferBindingType::Uniform,
1405 has_dynamic_offset: false,
1406 min_binding_size: None,
1407 },
1408 count: None,
1409 },
1410 ],
1411 })
1412}
1413
1414#[cfg(feature = "wgpu")]
1416pub(crate) fn two_tex_sampler_uniform_bgl(
1417 device: &wgpu::Device,
1418 label: &str,
1419) -> wgpu::BindGroupLayout {
1420 device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1421 label: Some(&format!("{label} BGL")),
1422 entries: &[
1423 wgpu::BindGroupLayoutEntry {
1424 binding: 0,
1425 visibility: wgpu::ShaderStages::FRAGMENT,
1426 ty: wgpu::BindingType::Texture {
1427 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1428 view_dimension: wgpu::TextureViewDimension::D2,
1429 multisampled: false,
1430 },
1431 count: None,
1432 },
1433 wgpu::BindGroupLayoutEntry {
1434 binding: 1,
1435 visibility: wgpu::ShaderStages::FRAGMENT,
1436 ty: wgpu::BindingType::Texture {
1437 sample_type: wgpu::TextureSampleType::Float { filterable: true },
1438 view_dimension: wgpu::TextureViewDimension::D2,
1439 multisampled: false,
1440 },
1441 count: None,
1442 },
1443 wgpu::BindGroupLayoutEntry {
1444 binding: 2,
1445 visibility: wgpu::ShaderStages::FRAGMENT,
1446 ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1447 count: None,
1448 },
1449 wgpu::BindGroupLayoutEntry {
1450 binding: 3,
1451 visibility: wgpu::ShaderStages::FRAGMENT,
1452 ty: wgpu::BindingType::Buffer {
1453 ty: wgpu::BufferBindingType::Uniform,
1454 has_dynamic_offset: false,
1455 min_binding_size: None,
1456 },
1457 count: None,
1458 },
1459 ],
1460 })
1461}
1462
1463#[cfg(feature = "wgpu")]
1464pub(crate) fn fullscreen_pipeline(
1465 device: &wgpu::Device,
1466 shader: &wgpu::ShaderModule,
1467 label: &str,
1468 bgl: &wgpu::BindGroupLayout,
1469) -> wgpu::RenderPipeline {
1470 let layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1471 label: Some(&format!("{label} layout")),
1472 bind_group_layouts: &[Some(bgl)],
1473 immediate_size: 0,
1474 });
1475 device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1476 label: Some(&format!("{label} pipeline")),
1477 layout: Some(&layout),
1478 vertex: wgpu::VertexState {
1479 module: shader,
1480 entry_point: Some("vs_main"),
1481 buffers: &[],
1482 compilation_options: wgpu::PipelineCompilationOptions::default(),
1483 },
1484 fragment: Some(wgpu::FragmentState {
1485 module: shader,
1486 entry_point: Some("fs_main"),
1487 targets: &[Some(wgpu::ColorTargetState {
1488 format: wgpu::TextureFormat::Rgba8Unorm,
1489 blend: None,
1490 write_mask: wgpu::ColorWrites::ALL,
1491 })],
1492 compilation_options: wgpu::PipelineCompilationOptions::default(),
1493 }),
1494 primitive: wgpu::PrimitiveState::default(),
1495 depth_stencil: None,
1496 multisample: wgpu::MultisampleState::default(),
1497 multiview_mask: None,
1498 cache: None,
1499 })
1500}
1501
1502#[cfg(feature = "wgpu")]
1503pub(crate) fn upload_rgba_texture(
1504 ctx: &crate::context::RenderContext,
1505 data: &[u8],
1506 width: u32,
1507 height: u32,
1508 label: &str,
1509) -> wgpu::Texture {
1510 let tex = ctx.device.create_texture(&wgpu::TextureDescriptor {
1511 label: Some(label),
1512 size: wgpu::Extent3d {
1513 width,
1514 height,
1515 depth_or_array_layers: 1,
1516 },
1517 mip_level_count: 1,
1518 sample_count: 1,
1519 dimension: wgpu::TextureDimension::D2,
1520 format: wgpu::TextureFormat::Rgba8Unorm,
1521 usage: wgpu::TextureUsages::COPY_DST | wgpu::TextureUsages::TEXTURE_BINDING,
1522 view_formats: &[],
1523 });
1524 ctx.queue.write_texture(
1525 wgpu::TexelCopyTextureInfo {
1526 texture: &tex,
1527 mip_level: 0,
1528 origin: wgpu::Origin3d::ZERO,
1529 aspect: wgpu::TextureAspect::All,
1530 },
1531 data,
1532 wgpu::TexelCopyBufferLayout {
1533 offset: 0,
1534 bytes_per_row: Some(width * 4),
1535 rows_per_image: None,
1536 },
1537 wgpu::Extent3d {
1538 width,
1539 height,
1540 depth_or_array_layers: 1,
1541 },
1542 );
1543 tex
1544}
1545
1546#[cfg(feature = "wgpu")]
1547pub(crate) fn submit_render_pass(
1548 ctx: &crate::context::RenderContext,
1549 pipeline: &wgpu::RenderPipeline,
1550 bind_group: &wgpu::BindGroup,
1551 out_view: &wgpu::TextureView,
1552 label: &str,
1553) {
1554 let mut encoder = ctx
1555 .device
1556 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1557 label: Some(&format!("{label} encoder")),
1558 });
1559 {
1560 let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1561 label: Some(&format!("{label} pass")),
1562 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1563 view: out_view,
1564 resolve_target: None,
1565 depth_slice: None,
1566 ops: wgpu::Operations {
1567 load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
1568 store: wgpu::StoreOp::Store,
1569 },
1570 })],
1571 depth_stencil_attachment: None,
1572 timestamp_writes: None,
1573 occlusion_query_set: None,
1574 multiview_mask: None,
1575 });
1576 pass.set_pipeline(pipeline);
1577 pass.set_bind_group(0, bind_group, &[]);
1578 pass.draw(0..6, 0..1);
1579 }
1580 ctx.queue.submit(std::iter::once(encoder.finish()));
1581}
1582
1583#[cfg(feature = "wgpu")]
1584fn pack_f32(values: &[f32]) -> Vec<u8> {
1585 values.iter().flat_map(|f| f.to_le_bytes()).collect()
1586}