Skip to main content

pfx_post/look/
gpu.rs

1use super::{Backdrop, Blend, Crt, Dither, Place, Quantise, UpscaleMap, Vignette, wipe_front};
2use pfx_gpu::GpuProfiler;
3
4pub const FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
5pub const SLOT: u64 = 512;
6pub const PALETTE_BYTES: u64 = 8192;
7const ALIGN: u64 = 256;
8const GLOW_REACH: f32 = 2.0;
9const GLOW_FACTOR_MAX: u32 = 4;
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12enum Kind {
13    Backdrop,
14    Quantise,
15    Upscale,
16    Vignette,
17    Mix,
18    Copy,
19    Bright,
20    Blur,
21    Crt,
22}
23
24const KINDS: [(Kind, &str); 9] = [
25    (Kind::Backdrop, "look_backdrop"),
26    (Kind::Quantise, "look_quantise"),
27    (Kind::Upscale, "look_upscale"),
28    (Kind::Vignette, "look_vignette"),
29    (Kind::Mix, "look_mix"),
30    (Kind::Copy, "look_copy"),
31    (Kind::Bright, "look_bright"),
32    (Kind::Blur, "look_blur"),
33    (Kind::Crt, "look_crt"),
34];
35
36#[derive(Clone, Copy)]
37pub struct Surface<'a> {
38    pub view: &'a wgpu::TextureView,
39    pub size: [u32; 2],
40}
41
42struct Job {
43    kind: Kind,
44    label: &'static str,
45    uniform: u64,
46    palette: Option<u64>,
47    source: wgpu::TextureView,
48    aux: Option<wgpu::TextureView>,
49    target: wgpu::TextureView,
50}
51
52type Pooled = (wgpu::Texture, wgpu::TextureView, [u32; 2]);
53
54pub struct LookGpu {
55    layout: wgpu::BindGroupLayout,
56    palette_layout: wgpu::BindGroupLayout,
57    pipelines: Vec<(Kind, wgpu::RenderPipeline)>,
58    sampler: wgpu::Sampler,
59    empty: wgpu::TextureView,
60    buffer: Option<wgpu::Buffer>,
61    capacity: u64,
62    cursor: u64,
63    flushed: u64,
64    staging: Vec<u8>,
65    jobs: Vec<Job>,
66    targets: Vec<Option<Pooled>>,
67    glow: [Option<Pooled>; 2],
68}
69
70pub fn entries(palette: bool) -> Vec<wgpu::BindGroupLayoutEntry> {
71    let texture = |binding| wgpu::BindGroupLayoutEntry {
72        binding,
73        visibility: wgpu::ShaderStages::FRAGMENT,
74        ty: wgpu::BindingType::Texture {
75            sample_type: wgpu::TextureSampleType::Float { filterable: true },
76            view_dimension: wgpu::TextureViewDimension::D2,
77            multisampled: false,
78        },
79        count: None,
80    };
81    let uniform = |binding, size| wgpu::BindGroupLayoutEntry {
82        binding,
83        visibility: wgpu::ShaderStages::FRAGMENT,
84        ty: wgpu::BindingType::Buffer {
85            ty: wgpu::BufferBindingType::Uniform,
86            has_dynamic_offset: true,
87            min_binding_size: wgpu::BufferSize::new(size),
88        },
89        count: None,
90    };
91    let mut entries = vec![
92        uniform(0, SLOT),
93        texture(1),
94        texture(2),
95        wgpu::BindGroupLayoutEntry {
96            binding: 3,
97            visibility: wgpu::ShaderStages::FRAGMENT,
98            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
99            count: None,
100        },
101    ];
102    if palette {
103        entries.push(uniform(4, PALETTE_BYTES));
104    }
105    entries
106}
107
108fn pooled(device: &wgpu::Device, label: &str, size: [u32; 2]) -> Pooled {
109    let texture = device.create_texture(&wgpu::TextureDescriptor {
110        label: Some(label),
111        size: wgpu::Extent3d {
112            width: size[0].max(1),
113            height: size[1].max(1),
114            depth_or_array_layers: 1,
115        },
116        mip_level_count: 1,
117        sample_count: 1,
118        dimension: wgpu::TextureDimension::D2,
119        format: FORMAT,
120        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
121            | wgpu::TextureUsages::TEXTURE_BINDING
122            | wgpu::TextureUsages::COPY_SRC
123            | wgpu::TextureUsages::COPY_DST,
124        view_formats: &[],
125    });
126    let view = texture.create_view(&Default::default());
127    (texture, view, size)
128}
129
130fn header(target: [u32; 2], place: &Place, source: [u32; 2]) -> [[f32; 4]; 3] {
131    [
132        [
133            target[0] as f32,
134            target[1] as f32,
135            1.0 / target[0] as f32,
136            1.0 / target[1] as f32,
137        ],
138        [place.scale, place.offset[0], place.offset[1], 0.0],
139        [
140            place.layout[0],
141            place.layout[1],
142            source[0] as f32,
143            source[1] as f32,
144        ],
145    ]
146}
147
148fn unplaced(size: [u32; 2]) -> Place {
149    Place {
150        layout: [size[0] as f32, size[1] as f32],
151        scale: 1.0,
152        offset: [0.0; 2],
153    }
154}
155
156pub fn glow_factor(sigma: f32) -> u32 {
157    let wanted = (sigma / GLOW_REACH).ceil().max(1.0) as u32;
158    wanted.next_power_of_two().min(GLOW_FACTOR_MAX)
159}
160
161impl LookGpu {
162    pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
163        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
164            label: Some("look"),
165            entries: &entries(false),
166        });
167        let palette_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
168            label: Some("look palette"),
169            entries: &entries(true),
170        });
171        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
172            label: Some("look"),
173            source: wgpu::ShaderSource::Wgsl(super::shader::source().into()),
174        });
175        let plain = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
176            label: Some("look"),
177            bind_group_layouts: &[Some(&layout)],
178            immediate_size: 0,
179        });
180        let with_palette = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
181            label: Some("look palette"),
182            bind_group_layouts: &[Some(&palette_layout)],
183            immediate_size: 0,
184        });
185        let pipelines = KINDS
186            .iter()
187            .map(|&(kind, entry)| {
188                let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
189                    label: Some(entry),
190                    layout: Some(if kind == Kind::Quantise {
191                        &with_palette
192                    } else {
193                        &plain
194                    }),
195                    vertex: wgpu::VertexState {
196                        module: &module,
197                        entry_point: Some("look_vertex"),
198                        buffers: &[],
199                        compilation_options: Default::default(),
200                    },
201                    primitive: Default::default(),
202                    depth_stencil: None,
203                    multisample: Default::default(),
204                    fragment: Some(wgpu::FragmentState {
205                        module: &module,
206                        entry_point: Some(entry),
207                        targets: &[Some(wgpu::ColorTargetState {
208                            format: FORMAT,
209                            blend: None,
210                            write_mask: wgpu::ColorWrites::ALL,
211                        })],
212                        compilation_options: Default::default(),
213                    }),
214                    multiview_mask: None,
215                    cache: None,
216                });
217                (kind, pipeline)
218            })
219            .collect();
220        let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
221            label: Some("look"),
222            mag_filter: wgpu::FilterMode::Linear,
223            min_filter: wgpu::FilterMode::Linear,
224            ..Default::default()
225        });
226        let empty = pooled(device, "look empty", [1, 1]);
227        queue.write_texture(
228            empty.0.as_image_copy(),
229            &[0u8; 8],
230            wgpu::TexelCopyBufferLayout {
231                offset: 0,
232                bytes_per_row: Some(8),
233                rows_per_image: Some(1),
234            },
235            wgpu::Extent3d {
236                width: 1,
237                height: 1,
238                depth_or_array_layers: 1,
239            },
240        );
241        Self {
242            layout,
243            palette_layout,
244            pipelines,
245            sampler,
246            empty: empty.1,
247            buffer: None,
248            capacity: 0,
249            cursor: 0,
250            flushed: 0,
251            staging: Vec::new(),
252            jobs: Vec::new(),
253            targets: Vec::new(),
254            glow: [None, None],
255        }
256    }
257
258    pub fn pipelines(&self) -> usize {
259        self.pipelines.len()
260    }
261
262    pub fn begin(&mut self) {
263        self.cursor = 0;
264        self.flushed = 0;
265        self.staging.clear();
266        self.jobs.clear();
267    }
268
269    pub fn target(
270        &mut self,
271        device: &wgpu::Device,
272        slot: usize,
273        size: [u32; 2],
274    ) -> wgpu::TextureView {
275        if self.targets.len() <= slot {
276            self.targets.resize_with(slot + 1, || None);
277        }
278        let entry = &mut self.targets[slot];
279        if entry.as_ref().is_none_or(|pooled| pooled.2 != size) {
280            *entry = Some(pooled(device, "look target", size));
281        }
282        entry.as_ref().unwrap().1.clone()
283    }
284
285    pub fn job_count(&self) -> usize {
286        self.jobs.len()
287    }
288
289    fn push_bytes(&mut self, bytes: &[u8]) -> u64 {
290        let at = self.cursor;
291        let local = (at - self.flushed) as usize;
292        self.staging.resize(local, 0);
293        self.staging.extend_from_slice(bytes);
294        self.cursor = (at + bytes.len() as u64).div_ceil(ALIGN) * ALIGN;
295        at
296    }
297
298    fn push_uniform(&mut self, head: [[f32; 4]; 3], params: &[[f32; 4]]) -> u64 {
299        let mut slot = [[0.0f32; 4]; (SLOT / 16) as usize];
300        slot[..3].copy_from_slice(&head);
301        slot[3..3 + params.len()].copy_from_slice(params);
302        self.push_bytes(bytemuck::cast_slice(&slot))
303    }
304
305    #[allow(clippy::too_many_arguments)]
306    fn job(
307        &mut self,
308        kind: Kind,
309        label: &'static str,
310        uniform: u64,
311        palette: Option<u64>,
312        source: &wgpu::TextureView,
313        aux: Option<&wgpu::TextureView>,
314        target: &wgpu::TextureView,
315    ) {
316        self.jobs.push(Job {
317            kind,
318            label,
319            uniform,
320            palette,
321            source: source.clone(),
322            aux: aux.cloned(),
323            target: target.clone(),
324        });
325    }
326
327    pub fn backdrop(
328        &mut self,
329        source: Surface<'_>,
330        target: Surface<'_>,
331        backdrop: &Backdrop,
332        place: &Place,
333    ) {
334        let mut p = [[0.0f32; 4]; 18];
335        if let Some(gradient) = &backdrop.gradient {
336            p[..12].copy_from_slice(&gradient.packed());
337        }
338        p[12] = [
339            f32::from(u8::from(backdrop.colour.is_some())),
340            f32::from(u8::from(backdrop.grid.is_some())),
341            0.0,
342            0.0,
343        ];
344        if let Some(grid) = &backdrop.grid {
345            p[13] = [
346                grid.spacing[0],
347                grid.spacing[1],
348                grid.origin[0],
349                grid.origin[1],
350            ];
351            let major = grid.major.filter(|major| major.every > 0);
352            p[14] = [
353                grid.width,
354                major.map_or(0.0, |major| major.every as f32),
355                major.map_or(0.0, |major| major.width),
356                0.0,
357            ];
358            p[15] = grid.colour;
359            p[16] = major.map_or([0.0; 4], |major| major.colour);
360        }
361        p[17] = backdrop.colour.unwrap_or([0.0; 4]);
362        let uniform = self.push_uniform(header(target.size, place, source.size), &p);
363        self.job(
364            Kind::Backdrop,
365            "look backdrop",
366            uniform,
367            None,
368            source.view,
369            None,
370            target.view,
371        );
372    }
373
374    pub fn quantise(&mut self, source: Surface<'_>, target: Surface<'_>, quantise: &Quantise) {
375        let (order, spread) = match quantise.dither {
376            Dither::None => (0, 0.0),
377            Dither::Bayer { order, spread } => (order, spread),
378        };
379        let p = [[
380            quantise.palette.len() as f32,
381            order as f32,
382            spread,
383            order.max(1).trailing_zeros() as f32,
384        ]];
385        let uniform =
386            self.push_uniform(header(target.size, &unplaced(target.size), source.size), &p);
387        let palette = self.push_bytes(bytemuck::cast_slice(&quantise.palette.packed()));
388        self.job(
389            Kind::Quantise,
390            "look quantise",
391            uniform,
392            Some(palette),
393            source.view,
394            None,
395            target.view,
396        );
397    }
398
399    pub fn upscale(
400        &mut self,
401        source: Surface<'_>,
402        target: Surface<'_>,
403        map: &UpscaleMap,
404        clear: [f32; 4],
405    ) {
406        let p = [
407            [
408                map.offset[0] as f32,
409                map.offset[1] as f32,
410                map.extent[0] as f32,
411                map.extent[1] as f32,
412            ],
413            clear,
414        ];
415        let uniform =
416            self.push_uniform(header(target.size, &unplaced(target.size), map.source), &p);
417        self.job(
418            Kind::Upscale,
419            "look upscale",
420            uniform,
421            None,
422            source.view,
423            None,
424            target.view,
425        );
426    }
427
428    pub fn vignette(
429        &mut self,
430        source: Surface<'_>,
431        target: Surface<'_>,
432        vignette: &Vignette,
433        place: &Place,
434    ) {
435        let p = [
436            [
437                vignette.centre[0],
438                vignette.centre[1],
439                vignette.radius[0],
440                vignette.radius[1],
441            ],
442            [vignette.falloff, vignette.floor, 0.0, 0.0],
443            vignette.colour,
444        ];
445        let uniform = self.push_uniform(header(target.size, place, source.size), &p);
446        self.job(
447            Kind::Vignette,
448            "look vignette",
449            uniform,
450            None,
451            source.view,
452            None,
453            target.view,
454        );
455    }
456
457    #[allow(clippy::too_many_arguments)]
458    pub fn mix(
459        &mut self,
460        from: Surface<'_>,
461        to: Surface<'_>,
462        target: Surface<'_>,
463        blend: &Blend,
464        progress: f32,
465        place: &Place,
466    ) {
467        let progress = progress.clamp(0.0, 1.0);
468        let p = match *blend {
469            Blend::Crossfade => [[progress, 0.0, 0.0, 0.0], [0.0; 4]],
470            Blend::Wipe { angle, softness } => {
471                let (front, along) = wipe_front(angle, softness, progress, place.area(target.size));
472                [
473                    [progress, 1.0, softness.max(1e-4), 0.0],
474                    [along[0], along[1], front, 0.0],
475                ]
476            }
477        };
478        let uniform = self.push_uniform(header(target.size, place, from.size), &p);
479        self.job(
480            Kind::Mix,
481            "look mix",
482            uniform,
483            None,
484            from.view,
485            Some(to.view),
486            target.view,
487        );
488    }
489
490    pub fn copy(&mut self, source: Surface<'_>, target: Surface<'_>) {
491        let uniform = self.push_uniform(
492            header(target.size, &unplaced(target.size), source.size),
493            &[],
494        );
495        self.job(
496            Kind::Copy,
497            "look copy",
498            uniform,
499            None,
500            source.view,
501            None,
502            target.view,
503        );
504    }
505
506    pub fn crt(
507        &mut self,
508        device: &wgpu::Device,
509        source: Surface<'_>,
510        target: Surface<'_>,
511        crt: &Crt,
512        place: &Place,
513    ) {
514        let mut glow_view = None;
515        let mut tint = [0.0; 4];
516        if let Some(glow) = crt
517            .glow
518            .filter(|glow| glow.strength > 0.0 && glow.radius > 0.0)
519        {
520            let sigma = glow.radius * place.scale;
521            let factor = glow_factor(sigma);
522            let reduced = [
523                source.size[0].div_ceil(factor),
524                source.size[1].div_ceil(factor),
525            ];
526            for (index, slot) in self.glow.iter_mut().enumerate() {
527                if slot.as_ref().is_none_or(|pooled| pooled.2 != reduced) {
528                    *slot = Some(pooled(
529                        device,
530                        if index == 0 {
531                            "look glow"
532                        } else {
533                            "look glow blur"
534                        },
535                        reduced,
536                    ));
537                }
538            }
539            let a = self.glow[0].as_ref().unwrap().1.clone();
540            let b = self.glow[1].as_ref().unwrap().1.clone();
541            let bright = self.push_uniform(
542                header(reduced, &unplaced(reduced), source.size),
543                &[[factor as f32, glow.threshold, 0.0, 0.0]],
544            );
545            self.job(
546                Kind::Bright,
547                "look glow",
548                bright,
549                None,
550                source.view,
551                None,
552                &a,
553            );
554            let sigma_reduced = sigma / factor as f32;
555            let reach = (3.0 * sigma_reduced).ceil().max(1.0);
556            for (direction, from, to) in [([1.0, 0.0], &a, &b), ([0.0, 1.0], &b, &a)] {
557                let uniform = self.push_uniform(
558                    header(reduced, &unplaced(reduced), reduced),
559                    &[[direction[0], direction[1], sigma_reduced, reach]],
560                );
561                self.job(Kind::Blur, "look glow", uniform, None, from, None, to);
562            }
563            let strength = glow.strength * glow.tint[3];
564            tint = [
565                glow.tint[0] * strength,
566                glow.tint[1] * strength,
567                glow.tint[2] * strength,
568                1.0,
569            ];
570            glow_view = Some(a);
571        }
572        let lines = crt
573            .scanlines
574            .map_or([0.0; 4], |lines| [lines.period, lines.strength, 1.0, 0.0]);
575        let curve = crt.curvature.map_or([0.0, 0.0, 0.0, 0.0], |curve| {
576            [curve.amount, curve.corner, 1.0, 0.0]
577        });
578        let shift = crt
579            .aberration
580            .map_or(0.0, |aberration| aberration.shift * place.scale);
581        let bezel = crt.curvature.map_or([0.0; 4], |curve| curve.bezel);
582        let p = [[curve[0], curve[1], curve[2], shift.max(0.0)], bezel, tint];
583        let uniform = self.push_uniform(
584            header(target.size, place, source.size),
585            &[lines, p[0], p[1], p[2]],
586        );
587        self.job(
588            Kind::Crt,
589            "look crt",
590            uniform,
591            None,
592            source.view,
593            glow_view.as_ref(),
594            target.view,
595        );
596    }
597
598    pub fn flush(
599        &mut self,
600        device: &wgpu::Device,
601        queue: &wgpu::Queue,
602        encoder: &mut wgpu::CommandEncoder,
603        mut profiler: Option<&mut GpuProfiler>,
604    ) {
605        if self.jobs.is_empty() {
606            return;
607        }
608        let end = self.cursor.max(self.flushed + self.staging.len() as u64);
609        if self.buffer.is_none() || end > self.capacity {
610            self.capacity = end.next_power_of_two().max(64 * 1024);
611            self.buffer = Some(device.create_buffer(&wgpu::BufferDescriptor {
612                label: Some("look uniforms"),
613                size: self.capacity,
614                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
615                mapped_at_creation: false,
616            }));
617        }
618        let buffer = self.buffer.clone().unwrap();
619        if !self.staging.is_empty() {
620            let padded = self.staging.len().div_ceil(4) * 4;
621            self.staging.resize(padded, 0);
622            queue.write_buffer(&buffer, self.flushed, &self.staging);
623        }
624        self.staging.clear();
625        self.flushed = self.cursor;
626        for job in std::mem::take(&mut self.jobs) {
627            let mut entries = vec![
628                wgpu::BindGroupEntry {
629                    binding: 0,
630                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
631                        buffer: &buffer,
632                        offset: 0,
633                        size: wgpu::BufferSize::new(SLOT),
634                    }),
635                },
636                wgpu::BindGroupEntry {
637                    binding: 1,
638                    resource: wgpu::BindingResource::TextureView(&job.source),
639                },
640                wgpu::BindGroupEntry {
641                    binding: 2,
642                    resource: wgpu::BindingResource::TextureView(
643                        job.aux.as_ref().unwrap_or(&self.empty),
644                    ),
645                },
646                wgpu::BindGroupEntry {
647                    binding: 3,
648                    resource: wgpu::BindingResource::Sampler(&self.sampler),
649                },
650            ];
651            if job.palette.is_some() {
652                entries.push(wgpu::BindGroupEntry {
653                    binding: 4,
654                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
655                        buffer: &buffer,
656                        offset: 0,
657                        size: wgpu::BufferSize::new(PALETTE_BYTES),
658                    }),
659                });
660            }
661            let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
662                label: Some(job.label),
663                layout: if job.palette.is_some() {
664                    &self.palette_layout
665                } else {
666                    &self.layout
667                },
668                entries: &entries,
669            });
670            let timing = profiler
671                .as_deref_mut()
672                .and_then(|profiler| profiler.pass(job.label));
673            let writes = profiler
674                .as_deref()
675                .and_then(|profiler| profiler.render_writes(timing));
676            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
677                multiview_mask: None,
678                label: Some(job.label),
679                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
680                    view: &job.target,
681                    resolve_target: None,
682                    depth_slice: None,
683                    ops: wgpu::Operations {
684                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
685                        store: wgpu::StoreOp::Store,
686                    },
687                })],
688                depth_stencil_attachment: None,
689                timestamp_writes: writes,
690                occlusion_query_set: None,
691            });
692            let pipeline = &self
693                .pipelines
694                .iter()
695                .find(|(kind, _)| *kind == job.kind)
696                .unwrap()
697                .1;
698            pass.set_pipeline(pipeline);
699            let offsets = [job.uniform as u32, job.palette.unwrap_or(0) as u32];
700            let count = if job.palette.is_some() { 2 } else { 1 };
701            pass.set_bind_group(0, &group, &offsets[..count]);
702            pass.draw(0..3, 0..1);
703        }
704    }
705}