1#[cfg(test)]
2mod tests;
3
4use super::{
5 Draw, Fill, Fit, FlatPass, FlatScene, FlatText, Shadow, ShadowCurve, Shape, Srgba, TokenKey,
6};
7use pfx_gpu::{GpuProfiler, wgpu};
8pub use pfx_post::look::{
9 Aberration, Backdrop, Blend, Crt, Curvature, Dither, Glow, Gradient, GradientShape, Grid,
10 Interpolation, Major, Palette, Place, Quantise, Scanlines, Upscale, UpscaleMap, Vignette,
11};
12use pfx_post::look::{LookGpu, gpu::Surface, mix_oklab};
13use std::collections::BTreeMap;
14
15pub const LOW_RES_MAX: u32 = 4096;
16
17#[derive(Clone, Debug, Default, PartialEq)]
18pub struct TokenSet {
19 tokens: BTreeMap<String, Srgba>,
20}
21
22impl TokenSet {
23 pub fn new() -> Self {
24 Self::default()
25 }
26
27 pub fn with(mut self, key: impl Into<String>, colour: Srgba) -> Self {
28 self.insert(key, colour);
29 self
30 }
31
32 pub fn insert(&mut self, key: impl Into<String>, colour: Srgba) {
33 self.tokens.insert(key.into(), colour);
34 }
35
36 pub fn get(&self, key: &str) -> Option<Srgba> {
37 self.tokens.get(key).copied()
38 }
39
40 pub fn iter(&self) -> impl Iterator<Item = (&str, Srgba)> {
41 self.tokens
42 .iter()
43 .map(|(key, colour)| (key.as_str(), *colour))
44 }
45
46 pub fn len(&self) -> usize {
47 self.tokens.len()
48 }
49
50 pub fn is_empty(&self) -> bool {
51 self.tokens.is_empty()
52 }
53
54 pub fn mix(&self, other: &TokenSet, t: f32) -> TokenSet {
55 let t = t.clamp(0.0, 1.0);
56 let mut out = self.clone();
57 for (key, colour) in &other.tokens {
58 let mixed = match self.tokens.get(key) {
59 Some(start) if t <= 0.0 => *start,
60 Some(_) if t >= 1.0 => *colour,
61 Some(start) => Srgba(mix_oklab(start.0, colour.0, t)),
62 None => *colour,
63 };
64 out.tokens.insert(key.clone(), mixed);
65 }
66 out
67 }
68}
69
70impl<K: Into<String>> FromIterator<(K, Srgba)> for TokenSet {
71 fn from_iter<I: IntoIterator<Item = (K, Srgba)>>(iter: I) -> Self {
72 let mut set = Self::new();
73 for (key, colour) in iter {
74 set.insert(key, colour);
75 }
76 set
77 }
78}
79
80fn byte_key(colour: Srgba) -> [u8; 3] {
81 std::array::from_fn(|i| (colour.0[i].clamp(0.0, 1.0) * 255.0).round() as u8)
82}
83
84#[derive(Clone, Debug, Default, PartialEq)]
85pub struct Recolour {
86 map: Vec<([u8; 3], Srgba, f32)>,
87 keyed: BTreeMap<TokenKey, (Srgba, f32)>,
88}
89
90fn swapped(colour: Srgba, to: Srgba, base_alpha: f32) -> Srgba {
91 let alpha = if base_alpha > 0.0 {
92 colour.0[3] * to.0[3] / base_alpha
93 } else {
94 to.0[3]
95 };
96 Srgba([to.0[0], to.0[1], to.0[2], alpha.clamp(0.0, 1.0)])
97}
98
99impl Recolour {
100 pub fn new(base: &TokenSet, swap: &TokenSet) -> Self {
101 let mut map: Vec<([u8; 3], Srgba, f32)> = Vec::new();
102 for (key, from) in base.iter() {
103 let Some(to) = swap.get(key) else {
104 continue;
105 };
106 let at = byte_key(from);
107 if map.iter().any(|(existing, _, _)| *existing == at) || from == to {
108 continue;
109 }
110 map.push((at, to, from.0[3]));
111 }
112 map.sort_by_key(|entry| entry.0);
113 let mut keyed = BTreeMap::new();
114 for (key, to) in swap.iter() {
115 let from = base.get(key);
116 if from == Some(to) {
117 continue;
118 }
119 keyed.insert(TokenKey::new(key), (to, from.map_or(1.0, |from| from.0[3])));
120 }
121 Self { map, keyed }
122 }
123
124 pub fn is_identity(&self) -> bool {
125 self.map.is_empty() && self.keyed.is_empty()
126 }
127
128 pub fn pick(&self, colour: Srgba, key: Option<TokenKey>) -> Srgba {
129 match key {
130 None => self.colour(colour),
131 Some(key) => match self.keyed.get(&key) {
132 Some(&(to, base_alpha)) => swapped(colour, to, base_alpha),
133 None => colour,
134 },
135 }
136 }
137
138 pub fn colour(&self, colour: Srgba) -> Srgba {
139 let at = byte_key(colour);
140 match self.map.binary_search_by_key(&at, |entry| entry.0) {
141 Ok(index) => {
142 let (_, to, base_alpha) = self.map[index];
143 swapped(colour, to, base_alpha)
144 }
145 Err(_) => colour,
146 }
147 }
148
149 pub fn gradient(&self, gradient: &Gradient) -> Gradient {
150 let stops: Vec<(f32, [f32; 4])> = gradient
151 .stops()
152 .map(|(offset, colour)| (offset, self.colour(Srgba(colour)).0))
153 .collect();
154 Gradient::new(gradient.shape, &stops)
155 .map(|recoloured| recoloured.space(gradient.space))
156 .unwrap_or(*gradient)
157 }
158
159 pub fn draw(&self, draw: &mut Draw) {
160 let roles = draw.roles;
161 draw.fill = match draw.fill {
162 Fill::None => Fill::None,
163 Fill::Solid(colour) => Fill::Solid(self.pick(colour, roles.fill)),
164 Fill::Vertical(top, bottom) => Fill::Vertical(
165 self.pick(top, roles.fill),
166 self.pick(bottom, roles.fill_end),
167 ),
168 Fill::Gradient(gradient) => Fill::Gradient(self.gradient(&gradient)),
169 Fill::Image(image) => Fill::Image(image),
170 };
171 if let Some(stroke) = draw.stroke.as_mut() {
172 stroke.colour = self.pick(stroke.colour, roles.stroke);
173 }
174 if let Some(pattern) = draw.pattern.as_mut() {
175 pattern.colour = self.pick(pattern.colour, roles.pattern);
176 }
177 if let Shadow::Glow(glow) = &mut draw.shadow {
178 glow.colour = self.pick(glow.colour, roles.glow);
179 }
180 }
181}
182
183#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
184pub enum LowText {
185 #[default]
186 Pixelated,
187 Crisp,
188}
189
190#[derive(Clone, Copy, Debug, PartialEq)]
191pub struct LowRes {
192 pub size: [u32; 2],
193 pub upscale: Upscale,
194 pub text: LowText,
195}
196
197impl LowRes {
198 pub fn new(size: [u32; 2]) -> Self {
199 Self {
200 size,
201 upscale: Upscale::Integer,
202 text: LowText::Pixelated,
203 }
204 }
205
206 pub fn upscale(mut self, upscale: Upscale) -> Self {
207 self.upscale = upscale;
208 self
209 }
210
211 pub fn text(mut self, text: LowText) -> Self {
212 self.text = text;
213 self
214 }
215}
216
217#[derive(Clone, Copy, Debug, PartialEq)]
218pub struct ShadowTint {
219 pub colour: Option<Srgba>,
220 pub alpha: f32,
221}
222
223impl Default for ShadowTint {
224 fn default() -> Self {
225 Self {
226 colour: None,
227 alpha: 1.0,
228 }
229 }
230}
231
232impl ShadowTint {
233 pub fn new() -> Self {
234 Self::default()
235 }
236
237 pub fn colour(mut self, colour: Srgba) -> Self {
238 self.colour = Some(colour);
239 self
240 }
241
242 pub fn alpha(mut self, alpha: f32) -> Self {
243 self.alpha = alpha;
244 self
245 }
246
247 pub fn apply(&self, curve: Srgba) -> Srgba {
248 let colour = self.colour.unwrap_or(curve);
249 colour.alpha((colour.0[3] * self.alpha).clamp(0.0, 1.0))
250 }
251}
252
253#[derive(Clone, Debug, PartialEq)]
254pub enum LookPass {
255 LowRes(LowRes),
256 Tokens(TokenSet),
257 Shadow(ShadowTint),
258 Backdrop(Backdrop),
259 Quantise(Quantise),
260 Vignette(Vignette),
261 Crt(Crt),
262}
263
264#[derive(Clone, Debug, Default, PartialEq)]
265pub struct Look {
266 pub passes: Vec<LookPass>,
267}
268
269impl Look {
270 pub fn new() -> Self {
271 Self::default()
272 }
273
274 pub fn with(mut self, pass: LookPass) -> Self {
275 self.passes.push(pass);
276 self
277 }
278
279 pub fn is_identity(&self) -> bool {
280 self.passes.is_empty()
281 }
282
283 pub fn low_res(&self) -> Option<&LowRes> {
284 self.passes.iter().find_map(|pass| match pass {
285 LookPass::LowRes(low) => Some(low),
286 _ => None,
287 })
288 }
289
290 pub fn tokens(&self) -> Option<&TokenSet> {
291 self.passes.iter().find_map(|pass| match pass {
292 LookPass::Tokens(tokens) => Some(tokens),
293 _ => None,
294 })
295 }
296
297 pub fn shadow(&self) -> Option<&ShadowTint> {
298 self.passes.iter().find_map(|pass| match pass {
299 LookPass::Shadow(tint) => Some(tint),
300 _ => None,
301 })
302 }
303
304 pub fn backdrop(&self) -> Option<&Backdrop> {
305 self.passes.iter().find_map(|pass| match pass {
306 LookPass::Backdrop(backdrop) => Some(backdrop),
307 _ => None,
308 })
309 }
310
311 pub fn validate(&self) -> Result<(), String> {
312 let count = |test: fn(&LookPass) -> bool| self.passes.iter().filter(|p| test(p)).count();
313 if count(|p| matches!(p, LookPass::LowRes(_))) > 1
314 || count(|p| matches!(p, LookPass::Tokens(_))) > 1
315 || count(|p| matches!(p, LookPass::Backdrop(_))) > 1
316 || count(|p| matches!(p, LookPass::Shadow(_))) > 1
317 {
318 return Err(
319 "a look holds at most one low resolution, token set, shadow tint and backdrop"
320 .into(),
321 );
322 }
323 if let Some(tint) = self.shadow()
324 && !(tint.alpha.is_finite() && tint.alpha >= 0.0)
325 {
326 return Err("a shadow tint's alpha scale is finite and zero or more".into());
327 }
328 if let Some(low) = self.low_res()
329 && (low.size.contains(&0) || low.size.iter().any(|&side| side > LOW_RES_MAX))
330 {
331 return Err(format!(
332 "a low resolution is 1 to {LOW_RES_MAX} pixels a side"
333 ));
334 }
335 Ok(())
336 }
337
338 fn same_pixels(&self, other: &Look) -> bool {
339 let pixels = |look: &Look| {
340 look.passes
341 .iter()
342 .filter(|pass| !matches!(pass, LookPass::Tokens(_) | LookPass::Shadow(_)))
343 .cloned()
344 .collect::<Vec<_>>()
345 };
346 pixels(self) == pixels(other)
347 }
348}
349
350#[derive(Clone, Copy, Debug, Default, PartialEq)]
351pub enum Easing {
352 #[default]
353 Linear,
354 In,
355 Out,
356 InOut,
357 Steps(u32),
358}
359
360impl Easing {
361 pub fn at(self, t: f32) -> f32 {
362 let t = t.clamp(0.0, 1.0);
363 match self {
364 Easing::Linear => t,
365 Easing::In => t * t * t,
366 Easing::Out => 1.0 - (1.0 - t).powi(3),
367 Easing::InOut => {
368 if t < 0.5 {
369 4.0 * t * t * t
370 } else {
371 1.0 - (-2.0 * t + 2.0).powi(3) * 0.5
372 }
373 }
374 Easing::Steps(steps) => {
375 let steps = steps.max(1) as f32;
376 (t * steps).floor().min(steps) / steps
377 }
378 }
379 }
380}
381
382#[derive(Clone, Copy, Debug, PartialEq)]
383pub struct Transition {
384 pub duration: f32,
385 pub easing: Easing,
386 pub blend: Blend,
387}
388
389impl Transition {
390 pub const INSTANT: Self = Self {
391 duration: 0.0,
392 easing: Easing::Linear,
393 blend: Blend::Crossfade,
394 };
395
396 pub fn crossfade(duration: f32, easing: Easing) -> Self {
397 Self {
398 duration,
399 easing,
400 blend: Blend::Crossfade,
401 }
402 }
403
404 pub fn wipe(duration: f32, easing: Easing, angle: f32, softness: f32) -> Self {
405 Self {
406 duration,
407 easing,
408 blend: Blend::Wipe { angle, softness },
409 }
410 }
411}
412
413#[derive(Clone, Copy, Debug, Default, PartialEq)]
414pub enum Motion {
415 #[default]
416 Full,
417 Reduced {
418 longest: f32,
419 },
420}
421
422impl Motion {
423 pub fn apply(self, transition: Transition) -> Transition {
424 match self {
425 Motion::Full => transition,
426 Motion::Reduced { longest } => Transition {
427 duration: transition.duration.min(longest.max(0.0)),
428 easing: Easing::Linear,
429 blend: Blend::Crossfade,
430 },
431 }
432 }
433}
434
435#[derive(Clone, Debug)]
436pub struct LookStack {
437 base: TokenSet,
438 current: Look,
439 previous: Option<Look>,
440 transition: Transition,
441 start: f64,
442 pub motion: Motion,
443}
444
445#[derive(Clone, Copy, Debug)]
446pub struct LookFrame<'a> {
447 pub from: &'a Look,
448 pub to: &'a Look,
449 pub progress: f32,
450 pub blend: Blend,
451 pub base: &'a TokenSet,
452}
453
454impl<'a> LookFrame<'a> {
455 pub fn still(look: &'a Look, base: &'a TokenSet) -> Self {
456 Self {
457 from: look,
458 to: look,
459 progress: 1.0,
460 blend: Blend::Crossfade,
461 base,
462 }
463 }
464
465 pub fn settled(&self) -> bool {
466 self.progress >= 1.0 || std::ptr::eq(self.from, self.to) || self.from == self.to
467 }
468
469 pub fn is_identity(&self) -> bool {
470 self.to.is_identity() && (self.settled() || self.from.is_identity())
471 }
472}
473
474impl LookStack {
475 pub fn new(look: Look) -> Result<Self, String> {
476 look.validate()?;
477 Ok(Self {
478 base: TokenSet::new(),
479 current: look,
480 previous: None,
481 transition: Transition::INSTANT,
482 start: 0.0,
483 motion: Motion::Full,
484 })
485 }
486
487 pub fn base(mut self, tokens: TokenSet) -> Self {
488 self.base = tokens;
489 self
490 }
491
492 pub fn motion(mut self, motion: Motion) -> Self {
493 self.motion = motion;
494 self
495 }
496
497 pub fn current(&self) -> &Look {
498 &self.current
499 }
500
501 pub fn tokens(&self) -> &TokenSet {
502 &self.base
503 }
504
505 pub fn progress(&self, time: f64) -> f32 {
506 let transition = self.motion.apply(self.transition);
507 if self.previous.is_none() || transition.duration <= 0.0 {
508 return 1.0;
509 }
510 let t = ((time - self.start) / f64::from(transition.duration)).clamp(0.0, 1.0) as f32;
511 transition.easing.at(t)
512 }
513
514 pub fn set(&mut self, look: Look) -> Result<(), String> {
515 look.validate()?;
516 self.current = look;
517 self.previous = None;
518 Ok(())
519 }
520
521 pub fn switch(&mut self, look: Look, time: f64, transition: Transition) -> Result<(), String> {
522 look.validate()?;
523 if !(transition.duration.is_finite() && transition.duration >= 0.0) || !time.is_finite() {
524 return Err("a transition's time and duration must be finite and nonnegative".into());
525 }
526 let progress = self.progress(time);
527 let shown = match self.previous.take() {
528 Some(previous) if progress < 0.5 => previous,
529 _ => std::mem::take(&mut self.current),
530 };
531 self.previous = Some(shown);
532 self.current = look;
533 self.transition = transition;
534 self.start = time;
535 Ok(())
536 }
537
538 pub fn frame(&self, time: f64) -> LookFrame<'_> {
539 let progress = self.progress(time);
540 match &self.previous {
541 Some(previous) if progress < 1.0 => LookFrame {
542 from: previous,
543 to: &self.current,
544 progress,
545 blend: self.motion.apply(self.transition).blend,
546 base: &self.base,
547 },
548 _ => LookFrame::still(&self.current, &self.base),
549 }
550 }
551}
552
553pub fn text_keys(draws: &[Draw], count: usize) -> Vec<Option<TokenKey>> {
554 let mut keys = vec![None; count];
555 for draw in draws {
556 if let Shape::Text(index) = draw.shape
557 && let Some(slot) = keys.get_mut(index)
558 && slot.is_none()
559 {
560 *slot = draw.roles.fill;
561 }
562 }
563 keys
564}
565
566pub struct SceneCopy<'a> {
567 pub draws: Vec<Draw>,
568 pub text: Vec<FlatText<'a>>,
569 pub curve: ShadowCurve,
570 pub clear: Option<Srgba>,
571}
572
573impl<'a> SceneCopy<'a> {
574 pub fn recoloured(scene: &FlatScene<'a>, recolour: &Recolour) -> Self {
575 let mut draws = scene.draws.to_vec();
576 let mut text = scene.text.to_vec();
577 let mut curve = scene.curve.clone();
578 let mut clear = scene.clear;
579 if !recolour.is_identity() {
580 let keys = text_keys(&draws, text.len());
581 for draw in &mut draws {
582 recolour.draw(draw);
583 }
584 for (item, key) in text.iter_mut().zip(keys) {
585 item.colour = recolour.pick(item.colour, key);
586 }
587 curve.colour = recolour.colour(curve.colour);
588 clear = clear.map(|colour| recolour.colour(colour));
589 }
590 Self {
591 draws,
592 text,
593 curve,
594 clear,
595 }
596 }
597
598 pub fn scene<'b>(&'b self, base: &FlatScene<'a>) -> FlatScene<'b>
599 where
600 'a: 'b,
601 {
602 FlatScene {
603 layout: base.layout,
604 clear: self.clear,
605 curve: &self.curve,
606 light: base.light,
607 draws: &self.draws,
608 groups: base.groups,
609 text: &self.text,
610 icons: base.icons,
611 sprites: None,
612 environment: base.environment,
613 post: base.post,
614 frame: base.frame,
615 seed: base.seed,
616 }
617 }
618
619 fn keep(&mut self, text: bool) {
620 for draw in &mut self.draws {
621 if matches!(draw.shape, Shape::Text(_)) != text {
622 draw.opacity = 0.0;
623 }
624 }
625 }
626
627 fn place_text(&mut self, layout_axes: [f32; 2], shift: [f32; 2]) {
628 let affine = super::multiply(
629 super::translation(shift[0], shift[1]),
630 super::scaling(layout_axes[0], layout_axes[1]),
631 );
632 for draw in &mut self.draws {
633 if matches!(draw.shape, Shape::Text(_)) {
634 draw.transform = super::multiply(affine, draw.transform);
635 }
636 }
637 }
638}
639
640fn place_of(fit: &Fit) -> Place {
641 Place {
642 layout: fit.layout,
643 scale: fit.scale,
644 offset: fit.offset,
645 }
646}
647
648fn side_tokens(frame: &LookFrame<'_>, look: &Look, mixed: bool) -> TokenSet {
649 let own = |look: &Look| {
650 let mut tokens = frame.base.clone();
651 for (key, colour) in look.tokens().into_iter().flat_map(TokenSet::iter) {
652 tokens.insert(key, colour);
653 }
654 tokens
655 };
656 if mixed {
657 own(frame.from).mix(&own(frame.to), frame.progress)
658 } else {
659 own(look)
660 }
661}
662
663fn side_shadow(frame: &LookFrame<'_>, look: &Look, mixed: bool, curve: Srgba) -> Srgba {
664 let own = |look: &Look| look.shadow().map_or(curve, |tint| tint.apply(curve));
665 if !mixed {
666 return own(look);
667 }
668 let (from, to) = (own(frame.from), own(frame.to));
669 if from == to {
670 return to;
671 }
672 let t = frame.progress.clamp(0.0, 1.0);
673 if t <= 0.0 {
674 from
675 } else if t >= 1.0 {
676 to
677 } else {
678 Srgba(mix_oklab(from.0, to.0, t))
679 }
680}
681
682fn warm_slots<'a>(
683 looks: impl IntoIterator<Item = &'a Look>,
684 size: [u32; 2],
685) -> Result<Vec<(usize, [u32; 2])>, String> {
686 let mut slots: Vec<(usize, [u32; 2])> = Vec::new();
687 let mut want = |slot: usize, render: [u32; 2]| {
688 if !slots.contains(&(slot, render)) {
689 slots.push((slot, render));
690 }
691 };
692 for look in looks {
693 look.validate()?;
694 if look.is_identity() {
695 continue;
696 }
697 let low = look.low_res().copied();
698 let render = low.map_or(size, |low| low.size);
699 let crisp = low.is_some_and(|low| low.text == LowText::Crisp);
700 want(0, render);
701 if crisp {
702 want(1, size);
703 }
704 if low.is_some() {
705 want(2, render);
706 if crisp {
707 want(3, size);
708 }
709 }
710 }
711 Ok(slots)
712}
713
714pub struct FlatLooks {
715 gpu: LookGpu,
716 passes: Vec<Option<FlatPass>>,
717 stages: u32,
718}
719
720enum Stage<'l> {
721 Backdrop(&'l Backdrop),
722 Quantise(&'l Quantise),
723 Vignette(&'l Vignette),
724 Upscale(UpscaleMap),
725 Text,
726 Crt(&'l Crt),
727}
728
729impl FlatLooks {
730 pub fn new(device: &wgpu::Device, queue: &wgpu::Queue) -> Self {
731 Self {
732 gpu: LookGpu::new(device, queue),
733 passes: Vec::new(),
734 stages: 0,
735 }
736 }
737
738 pub fn stages(&self) -> u32 {
739 self.stages
740 }
741
742 pub fn pipelines_made(&self) -> usize {
743 self.gpu.pipelines()
744 + self
745 .passes
746 .iter()
747 .flatten()
748 .map(FlatPass::pipelines_made)
749 .sum::<usize>()
750 }
751
752 pub fn warm<'a>(
753 &mut self,
754 device: &wgpu::Device,
755 queue: &wgpu::Queue,
756 looks: impl IntoIterator<Item = &'a Look>,
757 size: [u32; 2],
758 ) -> Result<(), String> {
759 let slots = warm_slots(looks, size)?;
760 for (slot, render) in slots {
761 self.pass(device, queue, slot)
762 .warm(device, render, &[], false);
763 }
764 Ok(())
765 }
766
767 fn pass(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, slot: usize) -> &mut FlatPass {
768 if self.passes.len() <= slot {
769 self.passes.resize_with(slot + 1, || None);
770 }
771 self.passes[slot].get_or_insert_with(|| FlatPass::new(device, queue))
772 }
773
774 #[allow(clippy::too_many_arguments)]
775 pub fn encode(
776 &mut self,
777 device: &wgpu::Device,
778 queue: &wgpu::Queue,
779 encoder: &mut wgpu::CommandEncoder,
780 main: &mut FlatPass,
781 scene: &FlatScene<'_>,
782 frame: &LookFrame<'_>,
783 size: [u32; 2],
784 target: &wgpu::TextureView,
785 mut profiler: Option<&mut GpuProfiler>,
786 ) -> Result<Fit, String> {
787 frame.from.validate()?;
788 frame.to.validate()?;
789 self.gpu.begin();
790 self.stages = 0;
791 let settled = frame.settled();
792 let shared =
793 !settled && frame.blend == Blend::Crossfade && frame.from.same_pixels(frame.to);
794 let sides: Vec<&Look> = if settled || shared {
795 vec![frame.to]
796 } else {
797 vec![frame.from, frame.to]
798 };
799 let mixed = !settled && frame.blend == Blend::Crossfade;
800 let copies: Vec<SceneCopy<'_>> = sides
801 .iter()
802 .map(|look| {
803 let tokens = side_tokens(frame, look, mixed);
804 let mut copy = SceneCopy::recoloured(scene, &Recolour::new(frame.base, &tokens));
805 if look.backdrop().is_some() {
806 copy.clear = None;
807 }
808 copy.curve.colour = side_shadow(frame, look, mixed, copy.curve.colour);
809 copy
810 })
811 .collect();
812 let last = sides.len() - 1;
813 let fit = main.prepare(device, queue, &copies[last].scene(scene), size)?;
814 let single = sides.len() == 1;
815 let mut results = Vec::new();
816 for (index, look) in sides.iter().enumerate() {
817 let output = single.then_some(target);
818 let result = self.encode_side(
819 device,
820 queue,
821 encoder,
822 main,
823 scene,
824 &copies[index],
825 look,
826 index,
827 index == last,
828 &fit,
829 size,
830 output,
831 profiler.as_deref_mut(),
832 )?;
833 results.push(result);
834 }
835 if !single {
836 self.gpu.mix(
837 Surface {
838 view: &results[0],
839 size,
840 },
841 Surface {
842 view: &results[1],
843 size,
844 },
845 Surface { view: target, size },
846 &frame.blend,
847 frame.progress,
848 &place_of(&fit),
849 );
850 self.stages += 1;
851 }
852 self.gpu.flush(device, queue, encoder, profiler);
853 Ok(fit)
854 }
855
856 #[allow(clippy::too_many_arguments)]
857 fn encode_side(
858 &mut self,
859 device: &wgpu::Device,
860 queue: &wgpu::Queue,
861 encoder: &mut wgpu::CommandEncoder,
862 main: &mut FlatPass,
863 scene: &FlatScene<'_>,
864 copy: &SceneCopy<'_>,
865 look: &Look,
866 index: usize,
867 main_side: bool,
868 fit: &Fit,
869 size: [u32; 2],
870 output: Option<&wgpu::TextureView>,
871 mut profiler: Option<&mut GpuProfiler>,
872 ) -> Result<wgpu::TextureView, String> {
873 let low = look.low_res().copied();
874 let render = low.map_or(size, |low| low.size);
875 let low_fit = Fit::new(scene.layout, render)?;
876 let map = low
877 .map(|low| UpscaleMap::new(low.size, size, low.upscale))
878 .transpose()?;
879 let crisp = low.is_some_and(|low| low.text == LowText::Crisp);
880 let mut stages = Vec::new();
881 if let Some(backdrop) = look.backdrop() {
882 stages.push(Stage::Backdrop(backdrop));
883 }
884 let mut upscaled = map.is_none();
885 for pass in &look.passes {
886 match pass {
887 LookPass::Quantise(quantise) => stages.push(Stage::Quantise(quantise)),
888 LookPass::Vignette(vignette) => stages.push(Stage::Vignette(vignette)),
889 LookPass::Crt(crt) => {
890 if !upscaled {
891 stages.push(Stage::Upscale(map.unwrap()));
892 if crisp {
893 stages.push(Stage::Text);
894 }
895 upscaled = true;
896 }
897 if !crt.is_empty() {
898 stages.push(Stage::Crt(crt));
899 }
900 }
901 _ => {}
902 }
903 }
904 if !upscaled {
905 stages.push(Stage::Upscale(map.unwrap()));
906 if crisp {
907 stages.push(Stage::Text);
908 }
909 }
910 let clear = copy.clear.map_or([0.0; 4], |colour| colour.premultiplied());
911 let base_slot = index * 4;
912 let first_view = match (output, stages.is_empty()) {
913 (Some(output), true) => output.clone(),
914 _ => self.gpu.target(device, base_slot, render),
915 };
916 let reuse_main = main_side && low.is_none();
917 self.gpu
918 .flush(device, queue, encoder, profiler.as_deref_mut());
919 let flat_scene;
920 let flat: &mut FlatPass = if reuse_main {
921 main
922 } else {
923 let mut colour = SceneCopy {
924 draws: copy.draws.clone(),
925 text: copy.text.clone(),
926 curve: copy.curve.clone(),
927 clear: copy.clear,
928 };
929 if crisp {
930 colour.keep(false);
931 }
932 flat_scene = colour;
933 let pass = self.pass(device, queue, index * 2);
934 pass.prepare(device, queue, &flat_scene.scene(scene), render)?;
935 pass
936 };
937 flat.encode_colour(encoder, &first_view, Some(clear), profiler.as_deref_mut());
938 let mut current = first_view;
939 let mut current_size = render;
940 let mut place = place_of(&low_fit);
941 let mut toggle = 1;
942 let count = stages.len();
943 for (at, stage) in stages.into_iter().enumerate() {
944 let next_size = match stage {
945 Stage::Upscale(map) => map.target,
946 _ => current_size,
947 };
948 let next = match (output, at + 1 == count) {
949 (Some(output), true) => output.clone(),
950 _ => {
951 let slot = base_slot
952 + if next_size == render && low.is_some() {
953 toggle
954 } else {
955 2 + toggle
956 };
957 toggle ^= 1;
958 self.gpu.target(device, slot, next_size)
959 }
960 };
961 let from = Surface {
962 view: ¤t,
963 size: current_size,
964 };
965 let to = Surface {
966 view: &next,
967 size: next_size,
968 };
969 match stage {
970 Stage::Backdrop(backdrop) => self.gpu.backdrop(from, to, backdrop, &place),
971 Stage::Quantise(quantise) => self.gpu.quantise(from, to, quantise),
972 Stage::Vignette(vignette) => self.gpu.vignette(from, to, vignette, &place),
973 Stage::Crt(crt) => self.gpu.crt(device, from, to, crt, &place),
974 Stage::Upscale(map) => {
975 self.gpu.upscale(from, to, &map, clear);
976 let k = map.scale();
977 place = Place {
978 layout: scene.layout,
979 scale: low_fit.scale * k[0],
980 offset: [
981 map.offset[0] as f32 + low_fit.offset[0] * k[0],
982 map.offset[1] as f32 + low_fit.offset[1] * k[1],
983 ],
984 };
985 let mut text = SceneCopy {
986 draws: copy.draws.clone(),
987 text: copy.text.clone(),
988 curve: copy.curve.clone(),
989 clear: None,
990 };
991 if crisp {
992 text.keep(true);
993 text.place_text(
994 [
995 low_fit.scale * k[0] / fit.scale,
996 low_fit.scale * k[1] / fit.scale,
997 ],
998 [
999 (map.offset[0] as f32 + low_fit.offset[0] * k[0] - fit.offset[0])
1000 / fit.scale,
1001 (map.offset[1] as f32 + low_fit.offset[1] * k[1] - fit.offset[1])
1002 / fit.scale,
1003 ],
1004 );
1005 let pass = self.pass(device, queue, index * 2 + 1);
1006 pass.prepare(device, queue, &text.scene(scene), size)?;
1007 }
1008 }
1009 Stage::Text => {
1010 self.gpu
1011 .flush(device, queue, encoder, profiler.as_deref_mut());
1012 if let Some(Some(pass)) = self.passes.get(index * 2 + 1) {
1013 pass.encode_colour(encoder, ¤t, None, profiler.as_deref_mut());
1014 }
1015 self.stages += 1;
1016 continue;
1017 }
1018 }
1019 self.stages += 1;
1020 current = next;
1021 current_size = next_size;
1022 }
1023 if let Some(output) = output
1024 && current_size == size
1025 && count > 0
1026 && ¤t != output
1027 {
1028 self.gpu.copy(
1029 Surface {
1030 view: ¤t,
1031 size,
1032 },
1033 Surface { view: output, size },
1034 );
1035 current = output.clone();
1036 }
1037 Ok(current)
1038 }
1039}