Skip to main content

pfx_live/flat/look/
mod.rs

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: &current,
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, &current, 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            && &current != output
1027        {
1028            self.gpu.copy(
1029                Surface {
1030                    view: &current,
1031                    size,
1032                },
1033                Surface { view: output, size },
1034            );
1035            current = output.clone();
1036        }
1037        Ok(current)
1038    }
1039}