Skip to main content

pfx_live/
upscale.rs

1use bytemuck::{Pod, Zeroable};
2use pfx_gpu::GpuProfiler;
3use pfx_gpu::window::Rect;
4use pfx_post::color::srgb_channel;
5
6use crate::flat::look::{FlatLooks, Look, LookFrame};
7use crate::flat::{FlatPass, FlatScene};
8
9pub const WGSL: &str = r#"
10struct Upscale {
11    content: vec4f,
12    source: vec2f,
13    mode: u32,
14    transfer: u32,
15    bars: vec4f,
16    backdrop: u32,
17    pad0: u32,
18    pad1: u32,
19    pad2: u32,
20}
21
22@group(0) @binding(0) var upscale_source: texture_2d<f32>;
23@group(0) @binding(1) var<uniform> upscale: Upscale;
24
25@vertex
26fn upscale_vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f {
27    let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0));
28    return vec4f(xy[index], 0.0, 1.0);
29}
30
31fn upscale_texel(p: vec2i) -> vec4f {
32    let last = vec2i(upscale.source) - vec2i(1);
33    return textureLoad(upscale_source, clamp(p, vec2i(0), last), 0);
34}
35
36fn upscale_weights(t: f32) -> vec4f {
37    let t2 = t * t;
38    let t3 = t2 * t;
39    return vec4f(
40        0.5 * (-t3 + 2.0 * t2 - t),
41        0.5 * (3.0 * t3 - 5.0 * t2 + 2.0),
42        0.5 * (-3.0 * t3 + 4.0 * t2 + t),
43        0.5 * (t3 - t2),
44    );
45}
46
47fn upscale_decode(c: vec3f) -> vec3f {
48    return select(pow((c + vec3f(0.055)) / 1.055, vec3f(2.4)), c / 12.92, c <= vec3f(0.04045));
49}
50
51fn upscale_encode(c: vec3f) -> vec3f {
52    let v = max(c, vec3f(0.0));
53    return select(1.055 * pow(v, vec3f(1.0 / 2.4)) - vec3f(0.055), v * 12.92, v <= vec3f(0.0031308));
54}
55
56fn upscale_transfer(c: vec4f) -> vec4f {
57    if (upscale.transfer == 1u) {
58        return vec4f(upscale_decode(c.rgb), c.a);
59    }
60    if (upscale.transfer == 2u) {
61        return vec4f(upscale_encode(c.rgb), c.a);
62    }
63    return c;
64}
65
66@fragment
67fn upscale_fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
68    let c = upscale.content;
69    let p = position.xy;
70    if (p.x < c.x || p.y < c.y || p.x >= c.x + c.z || p.y >= c.y + c.w) {
71        if (upscale.backdrop == 1u) {
72            discard;
73        }
74        return upscale.bars;
75    }
76    return upscale_transfer(upscale_filtered(p));
77}
78
79fn upscale_filtered(p: vec2f) -> vec4f {
80    let c = upscale.content;
81    let uv = (p - c.xy) / c.zw * upscale.source - vec2f(0.5);
82    if (upscale.mode == 1u) {
83        return upscale_texel(vec2i(floor(uv + vec2f(0.5))));
84    }
85    let base = floor(uv);
86    let f = uv - base;
87    let wx = upscale_weights(f.x);
88    let wy = upscale_weights(f.y);
89    let b = vec2i(base);
90    var sum = vec4f(0.0);
91    for (var j = 0; j < 4; j++) {
92        var row = vec4f(0.0);
93        for (var i = 0; i < 4; i++) {
94            row += upscale_texel(b + vec2i(i - 1, j - 1)) * wx[i];
95        }
96        sum += row * wy[j];
97    }
98    let t00 = upscale_texel(b);
99    let t10 = upscale_texel(b + vec2i(1, 0));
100    let t01 = upscale_texel(b + vec2i(0, 1));
101    let t11 = upscale_texel(b + vec2i(1, 1));
102    let lo = min(min(t00, t10), min(t01, t11));
103    let hi = max(max(t00, t10), max(t01, t11));
104    return clamp(sum, lo, hi);
105}
106"#;
107
108#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
109#[repr(u32)]
110pub enum Filter {
111    #[default]
112    Sharp,
113    Nearest,
114}
115
116#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
117#[repr(u32)]
118pub enum Transfer {
119    #[default]
120    Copy,
121    Decode,
122    Encode,
123}
124
125impl Transfer {
126    pub fn between(source: wgpu::TextureFormat, output: wgpu::TextureFormat) -> Self {
127        match (source.is_srgb(), output.is_srgb()) {
128            (false, true) => Transfer::Decode,
129            (true, false) => Transfer::Encode,
130            _ => Transfer::Copy,
131        }
132    }
133}
134
135#[derive(Clone, Copy, Debug, Pod, Zeroable)]
136#[repr(C)]
137struct Uniform {
138    content: [f32; 4],
139    source: [f32; 2],
140    mode: u32,
141    transfer: u32,
142    bars: [f32; 4],
143    backdrop: u32,
144    pad: [u32; 3],
145}
146
147pub struct Compose<'a> {
148    pub source: &'a wgpu::TextureView,
149    pub source_format: wgpu::TextureFormat,
150    pub source_size: [u32; 2],
151    pub output: &'a wgpu::TextureView,
152    pub output_format: wgpu::TextureFormat,
153    pub output_size: [u32; 2],
154    pub content: Rect,
155    pub bars: [f32; 4],
156    pub filter: Filter,
157    pub backdrop: Option<&'a FlatScene<'a>>,
158}
159
160pub struct Overlay<'a> {
161    pub scene: &'a FlatScene<'a>,
162    pub look: Option<&'a LookFrame<'a>>,
163    pub output: &'a wgpu::TextureView,
164    pub output_format: wgpu::TextureFormat,
165    pub output_size: [u32; 2],
166    pub content: Rect,
167}
168
169struct Source {
170    texture: wgpu::Texture,
171    view: wgpu::TextureView,
172    size: [u32; 2],
173    format: wgpu::TextureFormat,
174}
175
176pub struct Upscale {
177    layout: wgpu::BindGroupLayout,
178    pipeline_layout: wgpu::PipelineLayout,
179    module: wgpu::ShaderModule,
180    pipelines: Vec<(wgpu::TextureFormat, wgpu::RenderPipeline)>,
181    uniform: wgpu::Buffer,
182    group: Option<(wgpu::TextureView, wgpu::BindGroup)>,
183    source: Option<Source>,
184    flat: Option<FlatPass>,
185    backdrop: Option<FlatPass>,
186    looks: Option<FlatLooks>,
187}
188
189impl Upscale {
190    pub fn new(device: &wgpu::Device) -> Self {
191        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
192            label: Some("upscale"),
193            entries: &[
194                wgpu::BindGroupLayoutEntry {
195                    binding: 0,
196                    visibility: wgpu::ShaderStages::FRAGMENT,
197                    ty: wgpu::BindingType::Texture {
198                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
199                        view_dimension: wgpu::TextureViewDimension::D2,
200                        multisampled: false,
201                    },
202                    count: None,
203                },
204                wgpu::BindGroupLayoutEntry {
205                    binding: 1,
206                    visibility: wgpu::ShaderStages::FRAGMENT,
207                    ty: wgpu::BindingType::Buffer {
208                        ty: wgpu::BufferBindingType::Uniform,
209                        has_dynamic_offset: false,
210                        min_binding_size: None,
211                    },
212                    count: None,
213                },
214            ],
215        });
216        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
217            label: Some("upscale"),
218            bind_group_layouts: &[Some(&layout)],
219            immediate_size: 0,
220        });
221        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
222            label: Some("upscale"),
223            source: wgpu::ShaderSource::Wgsl(WGSL.into()),
224        });
225        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
226            label: Some("upscale"),
227            size: std::mem::size_of::<Uniform>() as u64,
228            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
229            mapped_at_creation: false,
230        });
231        Self {
232            layout,
233            pipeline_layout,
234            module,
235            pipelines: Vec::new(),
236            uniform,
237            group: None,
238            source: None,
239            flat: None,
240            backdrop: None,
241            looks: None,
242        }
243    }
244
245    pub fn warm(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) {
246        self.pipeline(device, format);
247    }
248
249    pub fn warm_overlay<'a>(
250        &mut self,
251        device: &wgpu::Device,
252        queue: &wgpu::Queue,
253        format: wgpu::TextureFormat,
254        size: [u32; 2],
255        looks: impl IntoIterator<Item = &'a Look>,
256    ) -> Result<(), String> {
257        if size[0] == 0 || size[1] == 0 {
258            return Err("warming the overlay needs a size of at least 1 by 1".into());
259        }
260        self.pipeline(device, format);
261        self.flat
262            .get_or_insert_with(|| FlatPass::new(device, queue))
263            .warm(device, size, &[(format, format.is_srgb(), true)], true);
264        self.looks
265            .get_or_insert_with(|| FlatLooks::new(device, queue))
266            .warm(device, queue, looks, size)
267    }
268
269    pub fn overlay_pipelines_made(&self) -> usize {
270        self.flat.as_ref().map_or(0, FlatPass::pipelines_made)
271            + self.looks.as_ref().map_or(0, FlatLooks::pipelines_made)
272    }
273
274    pub fn source_target(
275        &mut self,
276        device: &wgpu::Device,
277        size: [u32; 2],
278        format: wgpu::TextureFormat,
279    ) -> Result<wgpu::TextureView, String> {
280        if size[0] == 0 || size[1] == 0 {
281            return Err("the render size must be at least 1 by 1".into());
282        }
283        let fresh = self
284            .source
285            .as_ref()
286            .is_some_and(|source| source.size == size && source.format == format);
287        if !fresh {
288            let mut usage = wgpu::TextureUsages::RENDER_ATTACHMENT
289                | wgpu::TextureUsages::TEXTURE_BINDING
290                | wgpu::TextureUsages::COPY_SRC;
291            if format == wgpu::TextureFormat::Rgba16Float {
292                usage |= wgpu::TextureUsages::STORAGE_BINDING;
293            }
294            let texture = device.create_texture(&wgpu::TextureDescriptor {
295                label: Some("upscale source"),
296                size: wgpu::Extent3d {
297                    width: size[0],
298                    height: size[1],
299                    depth_or_array_layers: 1,
300                },
301                mip_level_count: 1,
302                sample_count: 1,
303                dimension: wgpu::TextureDimension::D2,
304                format,
305                usage,
306                view_formats: &[],
307            });
308            let view = texture.create_view(&Default::default());
309            self.source = Some(Source {
310                texture,
311                view,
312                size,
313                format,
314            });
315        }
316        self.source
317            .as_ref()
318            .map(|source| source.view.clone())
319            .ok_or_else(|| "the render target is missing".to_owned())
320    }
321
322    pub fn source_texture(&self) -> Option<&wgpu::Texture> {
323        self.source.as_ref().map(|source| &source.texture)
324    }
325
326    fn pipeline(&mut self, device: &wgpu::Device, format: wgpu::TextureFormat) -> usize {
327        if let Some(index) = self.pipelines.iter().position(|(made, _)| *made == format) {
328            return index;
329        }
330        let pipeline = device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
331            label: Some("upscale"),
332            layout: Some(&self.pipeline_layout),
333            vertex: wgpu::VertexState {
334                module: &self.module,
335                entry_point: Some("upscale_vertex"),
336                buffers: &[],
337                compilation_options: Default::default(),
338            },
339            primitive: Default::default(),
340            depth_stencil: None,
341            multisample: Default::default(),
342            fragment: Some(wgpu::FragmentState {
343                module: &self.module,
344                entry_point: Some("upscale_fragment"),
345                targets: &[Some(wgpu::ColorTargetState {
346                    format,
347                    blend: None,
348                    write_mask: wgpu::ColorWrites::ALL,
349                })],
350                compilation_options: Default::default(),
351            }),
352            multiview_mask: None,
353            cache: None,
354        });
355        self.pipelines.push((format, pipeline));
356        self.pipelines.len() - 1
357    }
358
359    pub fn encode(
360        &mut self,
361        device: &wgpu::Device,
362        queue: &wgpu::Queue,
363        encoder: &mut wgpu::CommandEncoder,
364        compose: &Compose<'_>,
365        mut profiler: Option<&mut GpuProfiler>,
366    ) -> Result<(), String> {
367        check(compose)?;
368        let index = self.pipeline(device, compose.output_format);
369        let content = compose.content;
370        let uniform = Uniform {
371            content: [content.x, content.y, content.width, content.height],
372            source: [compose.source_size[0] as f32, compose.source_size[1] as f32],
373            mode: compose.filter as u32,
374            transfer: Transfer::between(compose.source_format, compose.output_format) as u32,
375            bars: bar_value(compose.bars, compose.output_format),
376            backdrop: u32::from(compose.backdrop.is_some()),
377            pad: [0; 3],
378        };
379        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&uniform));
380        if self
381            .group
382            .as_ref()
383            .is_none_or(|(source, _)| source != compose.source)
384        {
385            let group = device.create_bind_group(&wgpu::BindGroupDescriptor {
386                label: Some("upscale"),
387                layout: &self.layout,
388                entries: &[
389                    wgpu::BindGroupEntry {
390                        binding: 0,
391                        resource: wgpu::BindingResource::TextureView(compose.source),
392                    },
393                    wgpu::BindGroupEntry {
394                        binding: 1,
395                        resource: self.uniform.as_entire_binding(),
396                    },
397                ],
398            });
399            self.group = Some((compose.source.clone(), group));
400        }
401        if let Some(backdrop) = compose.backdrop {
402            let flat = self
403                .backdrop
404                .get_or_insert_with(|| FlatPass::new(device, queue));
405            flat.prepare(device, queue, backdrop, compose.output_size)?;
406            let target = flat.colour_target(device, compose.output_size);
407            let [r, g, b, a] = compose.bars;
408            flat.encode_colour(
409                encoder,
410                &target,
411                Some([r * a, g * a, b * a, a]),
412                profiler.as_deref_mut(),
413            );
414            flat.encode_finish(
415                device,
416                encoder,
417                &target,
418                compose.output,
419                compose.output_format,
420                compose.output_format.is_srgb(),
421                false,
422                profiler.as_deref_mut(),
423            );
424        }
425        let timing = profiler
426            .as_deref_mut()
427            .and_then(|profiler| profiler.pass("upscale"));
428        let writes = profiler
429            .as_deref()
430            .and_then(|profiler| profiler.render_writes(timing));
431        {
432            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
433                multiview_mask: None,
434                label: Some("upscale"),
435                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
436                    view: compose.output,
437                    resolve_target: None,
438                    depth_slice: None,
439                    ops: wgpu::Operations {
440                        load: if compose.backdrop.is_some() {
441                            wgpu::LoadOp::Load
442                        } else {
443                            wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT)
444                        },
445                        store: wgpu::StoreOp::Store,
446                    },
447                })],
448                depth_stencil_attachment: None,
449                timestamp_writes: writes,
450                occlusion_query_set: None,
451            });
452            pass.set_pipeline(&self.pipelines[index].1);
453            pass.set_bind_group(0, self.group.as_ref().map(|(_, group)| group), &[]);
454            pass.draw(0..3, 0..1);
455        }
456        Ok(())
457    }
458
459    pub fn encode_overlay(
460        &mut self,
461        device: &wgpu::Device,
462        queue: &wgpu::Queue,
463        encoder: &mut wgpu::CommandEncoder,
464        overlay: &Overlay<'_>,
465        mut profiler: Option<&mut GpuProfiler>,
466    ) -> Result<(), String> {
467        if overlay.output_size[0] == 0 || overlay.output_size[1] == 0 {
468            return Err("the overlay needs an output of at least 1 by 1".into());
469        }
470        let flat = self
471            .flat
472            .get_or_insert_with(|| FlatPass::new(device, queue));
473        let look = overlay.look.filter(|look| !look.is_identity());
474        if look.is_none() {
475            flat.prepare(device, queue, overlay.scene, overlay.output_size)?;
476        }
477        let Some(clip) = clip(overlay.content, overlay.output_size) else {
478            return Ok(());
479        };
480        flat.set_clip(Some(clip));
481        let target = flat.colour_target(device, overlay.output_size);
482        if let Some(look) = look {
483            self.looks
484                .get_or_insert_with(|| FlatLooks::new(device, queue))
485                .encode(
486                    device,
487                    queue,
488                    encoder,
489                    flat,
490                    overlay.scene,
491                    look,
492                    overlay.output_size,
493                    &target,
494                    profiler.as_deref_mut(),
495                )?;
496        } else {
497            flat.encode_colour(encoder, &target, Some([0.0; 4]), profiler.as_deref_mut());
498        }
499        flat.encode_finish(
500            device,
501            encoder,
502            &target,
503            overlay.output,
504            overlay.output_format,
505            overlay.output_format.is_srgb(),
506            true,
507            profiler,
508        );
509        Ok(())
510    }
511}
512
513pub fn clip(content: Rect, output_size: [u32; 2]) -> Option<[u32; 4]> {
514    let edge = |from: f32, limit: u32| ((from - 0.5).ceil().max(0.0) as u32).min(limit);
515    let x0 = edge(content.x, output_size[0]);
516    let y0 = edge(content.y, output_size[1]);
517    let x1 = edge(content.x + content.width, output_size[0]);
518    let y1 = edge(content.y + content.height, output_size[1]);
519    (x1 > x0 && y1 > y0).then_some([x0, y0, x1 - x0, y1 - y0])
520}
521
522fn check(compose: &Compose<'_>) -> Result<(), String> {
523    let [sw, sh] = compose.source_size;
524    let [ow, oh] = compose.output_size;
525    if sw == 0 || sh == 0 || ow == 0 || oh == 0 {
526        return Err("the upscale needs a source and an output of at least 1 by 1".into());
527    }
528    let c = compose.content;
529    let finite = [c.x, c.y, c.width, c.height].iter().all(|v| v.is_finite());
530    if !finite || c.width <= 0.0 || c.height <= 0.0 {
531        return Err("the content rect must be finite and not empty".into());
532    }
533    if compose
534        .bars
535        .iter()
536        .any(|channel| !(0.0..=1.0).contains(channel))
537    {
538        return Err("the bar colour's channels must be from 0 to 1".into());
539    }
540    Ok(())
541}
542
543pub fn bar_value(bars: [f32; 4], format: wgpu::TextureFormat) -> [f32; 4] {
544    let [r, g, b, a] = bars;
545    let channel = |c: f32| if format.is_srgb() { srgb_channel(c) } else { c } * a;
546    [channel(r), channel(g), channel(b), a]
547}
548
549fn weights(t: f32) -> [f32; 4] {
550    let t2 = t * t;
551    let t3 = t2 * t;
552    [
553        0.5 * (-t3 + 2.0 * t2 - t),
554        0.5 * (3.0 * t3 - 5.0 * t2 + 2.0),
555        0.5 * (-3.0 * t3 + 4.0 * t2 + t),
556        0.5 * (t3 - t2),
557    ]
558}
559
560pub fn cpu(
561    source: &[[f32; 4]],
562    source_size: [u32; 2],
563    output_size: [u32; 2],
564    content: Rect,
565    bars: [f32; 4],
566    filter: Filter,
567) -> Vec<[f32; 4]> {
568    let [sw, sh] = source_size.map(|v| v as i32);
569    let texel = |x: i32, y: i32| source[(y.clamp(0, sh - 1) * sw + x.clamp(0, sw - 1)) as usize];
570    let mut out = Vec::with_capacity((output_size[0] * output_size[1]) as usize);
571    for y in 0..output_size[1] {
572        for x in 0..output_size[0] {
573            let p = [x as f32 + 0.5, y as f32 + 0.5];
574            if p[0] < content.x
575                || p[1] < content.y
576                || p[0] >= content.x + content.width
577                || p[1] >= content.y + content.height
578            {
579                out.push(bars);
580                continue;
581            }
582            let u = (p[0] - content.x) / content.width * sw as f32 - 0.5;
583            let v = (p[1] - content.y) / content.height * sh as f32 - 0.5;
584            if filter == Filter::Nearest {
585                out.push(texel((u + 0.5).floor() as i32, (v + 0.5).floor() as i32));
586                continue;
587            }
588            let (bx, by) = (u.floor(), v.floor());
589            let (wx, wy) = (weights(u - bx), weights(v - by));
590            let (bx, by) = (bx as i32, by as i32);
591            let mut sum = [0.0f32; 4];
592            for (j, wy) in wy.iter().enumerate() {
593                let mut row = [0.0f32; 4];
594                for (i, wx) in wx.iter().enumerate() {
595                    let t = texel(bx + i as i32 - 1, by + j as i32 - 1);
596                    for k in 0..4 {
597                        row[k] += t[k] * wx;
598                    }
599                }
600                for k in 0..4 {
601                    sum[k] += row[k] * wy;
602                }
603            }
604            let corners = [
605                texel(bx, by),
606                texel(bx + 1, by),
607                texel(bx, by + 1),
608                texel(bx + 1, by + 1),
609            ];
610            out.push(std::array::from_fn(|k| {
611                let lo = corners.iter().map(|t| t[k]).fold(f32::INFINITY, f32::min);
612                let hi = corners
613                    .iter()
614                    .map(|t| t[k])
615                    .fold(f32::NEG_INFINITY, f32::max);
616                sum[k].clamp(lo, hi)
617            }));
618        }
619    }
620    out
621}
622
623#[cfg(test)]
624mod tests;