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}