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;