Skip to main content

pfx_live/
renderer.rs

1use crate::canopy::{CanopyMode, Gobo, petal_emitters};
2use crate::crisp;
3use crate::demand::{self, DemandStats, Limits, Need, Next, Print, Region, Shown};
4use crate::effects::{
5    self, CreatureInstance, CreatureMesh, EffectCommands, EffectFrame, EffectTargets,
6    ParticleInstance, RoomHaze, VolumeKind, VolumeTimestamps, pack_emitter,
7};
8use crate::flat::look::{FlatLooks, Look, LookFrame};
9use crate::flat::{FlatPass, FlatScene};
10use crate::frame::{self, Frame, Instance, Matrix, MeshData, MeshHandle, OpaqueKey, Scene};
11use crate::glass;
12use crate::impostor::{ImpostorInstance, ImpostorParams, ImpostorPass};
13use crate::lights::{LightQuality, LocalLight};
14use crate::lod::{LodSet, LodStats};
15use crate::maps::{MapImages, TextureFilter};
16use crate::pipeline_cache::CacheLoad;
17use crate::probes::{Ambient, ContactDesc, ProbeLighting};
18use crate::reflect;
19use crate::shadow::{Quality, Shadows, View};
20use crate::sky::SkySource;
21use crate::stats::{FrameKind, FrameStats};
22use crate::taa::{self, GpuInputs};
23use crate::text::{GpuAtlas, PackedGlyph, QuadDraw, RichQuad, TextDraw, TextPass, TextSpace};
24use crate::warm::Warming;
25use pfx_geom::mesh::Mesh;
26use pfx_geom::tree::{PetalSource, Tree, tree_sway};
27use pfx_gpu::screens::{Budget, DynamicResolution};
28use pfx_gpu::{FrameTimings, Gpu, GpuProfiler, GpuRefusal, PassTiming, wgpu};
29use pfx_physics::{Emitter, ParticleKind, Wind};
30use pfx_post::{Bloom, Chain, Pass as PostPass, Style, Tape, Tone, dof, gpu::GpuChain};
31use pfx_text::Paragraph;
32use std::collections::VecDeque;
33use std::sync::Arc;
34use std::sync::atomic::{AtomicU64, Ordering};
35use std::sync::mpsc::{Receiver, TryRecvError};
36use wgpu::util::DeviceExt;
37
38pub const SCENE_COLOUR_MIPS: u32 = 7;
39pub const SHADOW_RANGE: f32 = 40.0;
40pub const TIMING_LAG: u64 = 2;
41
42#[derive(Clone, Debug, PartialEq)]
43pub struct Submitted {
44    pub frame: u64,
45    pub arrived: Vec<FrameTimings>,
46}
47
48#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
49pub struct TimingDrops {
50    pub ring: u64,
51    pub late: u64,
52}
53
54#[derive(Clone, Debug, PartialEq)]
55pub enum RendererError {
56    Refused(GpuRefusal),
57    Setup(String),
58}
59
60impl std::fmt::Display for RendererError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        match self {
63            Self::Refused(refusal) => write!(f, "{refusal}"),
64            Self::Setup(message) => write!(f, "{message}"),
65        }
66    }
67}
68
69impl std::error::Error for RendererError {}
70
71impl From<String> for RendererError {
72    fn from(message: String) -> Self {
73        Self::Setup(message)
74    }
75}
76
77impl From<GpuRefusal> for RendererError {
78    fn from(refusal: GpuRefusal) -> Self {
79        Self::Refused(refusal)
80    }
81}
82
83impl From<RendererError> for String {
84    fn from(error: RendererError) -> Self {
85        error.to_string()
86    }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub enum Finish {
91    Standard,
92    Style(Style),
93}
94
95#[derive(Clone, Copy, Debug, PartialEq)]
96pub enum Exposure {
97    Fixed(f32),
98    Auto { bias: f32 },
99}
100
101impl Exposure {
102    fn value(self, metered: f32) -> f32 {
103        match self {
104            Self::Fixed(value) => value,
105            Self::Auto { bias } => metered * 2.0_f32.powf(bias),
106        }
107    }
108}
109
110#[derive(Clone, Copy, Debug, PartialEq)]
111pub struct Picked {
112    pub x: u32,
113    pub y: u32,
114    pub id: u32,
115    pub depth: f32,
116}
117
118struct PickReadback {
119    buffer: wgpu::Buffer,
120    receiver: Receiver<Result<(), wgpu::BufferAsyncError>>,
121    x: u32,
122    y: u32,
123}
124
125pub struct TextItem<'a> {
126    pub atlas: &'a GpuAtlas,
127    pub paragraph: &'a Paragraph,
128    pub color: [f32; 4],
129    pub space: TextSpace,
130    pub id: u32,
131}
132pub struct TextQuadItem<'a> {
133    pub atlas: &'a GpuAtlas,
134    pub quads: &'a [RichQuad],
135    pub space: TextSpace,
136    pub icons: bool,
137    pub id: u32,
138}
139#[derive(Clone, Copy, Debug, PartialEq, Eq)]
140pub enum TextList {
141    Surface,
142    SurfaceQuads,
143    Overlay,
144    OverlayQuads,
145}
146#[derive(Clone, Copy, Debug, PartialEq)]
147pub struct TextBounds {
148    pub list: TextList,
149    pub index: usize,
150    pub id: u32,
151    pub rect: [f32; 4],
152}
153#[derive(Default)]
154pub struct Text<'a> {
155    pub surface: Vec<TextItem<'a>>,
156    pub overlay: Vec<TextItem<'a>>,
157    pub surface_quads: Vec<TextQuadItem<'a>>,
158    pub overlay_quads: Vec<TextQuadItem<'a>>,
159}
160pub struct Volume {
161    pub kind: VolumeKind,
162    pub lo: [f32; 3],
163    pub hi: [f32; 3],
164    pub color: [f32; 3],
165    pub density: f32,
166    pub anisotropy: f32,
167    pub time_scale: f32,
168}
169pub struct Liquid<'a> {
170    pub fluid: &'a wgpu::TextureView,
171    pub ripple: &'a wgpu::TextureView,
172}
173#[derive(Default)]
174pub struct Effects<'a> {
175    pub volume: Option<Volume>,
176    pub particles: &'a [ParticleInstance],
177    pub creatures: &'a [CreatureInstance],
178    pub creature_mesh: Option<&'a CreatureMesh>,
179    pub glass: &'a [glass::Surface],
180    pub liquid: Option<Liquid<'a>>,
181    pub props: &'a [ImpostorInstance],
182}
183
184pub const HISTORY_GAP: f32 = 0.25;
185
186#[derive(Clone, Debug)]
187pub struct History {
188    previous_time: Option<f32>,
189    last_view_projection: Option<Matrix>,
190    reset: bool,
191    max_gap: f32,
192}
193
194impl Default for History {
195    fn default() -> Self {
196        Self {
197            previous_time: None,
198            last_view_projection: None,
199            reset: false,
200            max_gap: HISTORY_GAP,
201        }
202    }
203}
204
205impl History {
206    pub fn invalidate(&mut self) {
207        self.reset = true;
208    }
209
210    pub fn set_max_gap(&mut self, seconds: f32) {
211        self.max_gap = if seconds.is_finite() {
212            seconds.max(0.0)
213        } else {
214            HISTORY_GAP
215        };
216    }
217
218    pub fn max_gap(&self) -> f32 {
219        self.max_gap
220    }
221
222    pub fn advance(
223        &mut self,
224        time: f32,
225        previous_view_projection: Matrix,
226        view_projection: Matrix,
227    ) -> bool {
228        let continuous = self
229            .previous_time
230            .is_some_and(|previous| time >= previous && time - previous <= self.max_gap);
231        let chained = self
232            .last_view_projection
233            .is_some_and(|last| close(last, previous_view_projection));
234        let valid = !self.reset && continuous && chained;
235        self.reset = false;
236        self.previous_time = Some(time);
237        self.last_view_projection = Some(view_projection);
238        valid
239    }
240
241    pub fn previous_time(&self) -> Option<f32> {
242        self.previous_time
243    }
244
245    pub fn bridge(&mut self, time: f32) {
246        if self.previous_time.is_some_and(|previous| time >= previous) {
247            self.previous_time = Some(time);
248        }
249    }
250
251    pub fn last_view_projection(&self) -> Option<Matrix> {
252        self.last_view_projection
253    }
254}
255
256fn close(a: Matrix, b: Matrix) -> bool {
257    let scale = a
258        .iter()
259        .flatten()
260        .chain(b.iter().flatten())
261        .fold(1.0_f32, |high, value| high.max(value.abs()));
262    a.iter()
263        .flatten()
264        .zip(b.iter().flatten())
265        .all(|(x, y)| (x - y).abs() <= scale * 1e-4)
266}
267
268struct Petals {
269    origin: [f32; 3],
270    sources: Vec<PetalSource>,
271    emitters: Vec<Emitter>,
272    seed: u32,
273    color: [f32; 3],
274    last_time: Option<f32>,
275}
276
277impl Petals {
278    fn new(tree: &Tree, origin: [f32; 3], rate: f32, color: [f32; 3]) -> Self {
279        let emitters = petal_emitters(tree, origin, 0.0, 0.0, [1.0, 0.0, 0.0], rate);
280        let count = emitters.len();
281        let sources = (0..count)
282            .map(|i| {
283                let index = ((i as f32 + 0.5) * tree.petal_sources.len() as f32 / count as f32)
284                    .floor() as usize;
285                tree.petal_sources[index.min(tree.petal_sources.len() - 1)]
286            })
287            .collect();
288        Self {
289            origin,
290            sources,
291            emitters,
292            seed: (tree.seed % 997) as u32 + 1,
293            color,
294            last_time: None,
295        }
296    }
297
298    fn step(&mut self, time: f32, wind: &crate::deform::WindFrame) -> Vec<ParticleInstance> {
299        let dt = self
300            .last_time
301            .map_or(0.0, |last| (time - last).clamp(0.0, 0.25));
302        self.last_time = Some(time);
303        let blow = Wind::new(self.seed, wind.direction, wind.strength);
304        let mut out = Vec::new();
305        for (emitter, source) in self.emitters.iter_mut().zip(&self.sources) {
306            let at = tree_sway(
307                source.position,
308                source.sway.pivot,
309                source.sway.level,
310                source.sway.stiffness,
311                wind.time,
312                wind.strength,
313                wind.direction,
314            );
315            emitter.set_origin([
316                at[0] + self.origin[0],
317                at[1] + self.origin[1],
318                at[2] + self.origin[2],
319            ]);
320            emitter.step(dt, time, &blow);
321            out.extend(pack_emitter(emitter, ParticleKind::Petal, time, self.color));
322        }
323        out
324    }
325}
326
327struct Motion {
328    layout: wgpu::BindGroupLayout,
329    pipeline: wgpu::RenderPipeline,
330    uniform: wgpu::Buffer,
331}
332
333struct Mips {
334    texture: wgpu::Texture,
335    view: wgpu::TextureView,
336    levels: Vec<wgpu::TextureView>,
337    extents: Vec<wgpu::Buffer>,
338    binds: Vec<wgpu::BindGroup>,
339    written: Option<[u32; 2]>,
340    pipeline: wgpu::RenderPipeline,
341}
342
343impl Mips {
344    fn bordered(&self, rect: [u32; 2], level: u32) -> [u32; 2] {
345        let size = self.texture.size();
346        [
347            (rect[0] + 1).min(crate::viewport::mip(size.width, level)),
348            (rect[1] + 1).min(crate::viewport::mip(size.height, level)),
349        ]
350    }
351
352    fn copy_scene(
353        &self,
354        encoder: &mut wgpu::CommandEncoder,
355        scene: &wgpu::Texture,
356        viewport: [u32; 2],
357    ) {
358        let [width, height] = viewport;
359        let copy =
360            |encoder: &mut wgpu::CommandEncoder, from: [u32; 2], to: [u32; 2], extent: [u32; 2]| {
361                encoder.copy_texture_to_texture(
362                    wgpu::TexelCopyTextureInfo {
363                        texture: scene,
364                        mip_level: 0,
365                        origin: wgpu::Origin3d {
366                            x: from[0],
367                            y: from[1],
368                            z: 0,
369                        },
370                        aspect: wgpu::TextureAspect::All,
371                    },
372                    wgpu::TexelCopyTextureInfo {
373                        texture: &self.texture,
374                        mip_level: 0,
375                        origin: wgpu::Origin3d {
376                            x: to[0],
377                            y: to[1],
378                            z: 0,
379                        },
380                        aspect: wgpu::TextureAspect::All,
381                    },
382                    wgpu::Extent3d {
383                        width: extent[0],
384                        height: extent[1],
385                        depth_or_array_layers: 1,
386                    },
387                );
388            };
389        copy(encoder, [0, 0], [0, 0], [width, height]);
390        let size = self.texture.size();
391        let right = width < size.width;
392        let below = height < size.height;
393        if right {
394            copy(encoder, [width - 1, 0], [width, 0], [1, height]);
395        }
396        if below {
397            copy(encoder, [0, height - 1], [0, height], [width, 1]);
398        }
399        if right && below {
400            copy(encoder, [width - 1, height - 1], [width, height], [1, 1]);
401        }
402    }
403}
404
405struct ExposureMeter {
406    layout: wgpu::BindGroupLayout,
407    pipeline: wgpu::ComputePipeline,
408    value: wgpu::Buffer,
409    readback: wgpu::Buffer,
410    extent: wgpu::Buffer,
411    written: Option<[u32; 2]>,
412    pending: Option<Receiver<Result<(), wgpu::BufferAsyncError>>>,
413    stale: bool,
414    exposure: f32,
415    last_time: Option<f32>,
416}
417
418impl ExposureMeter {
419    fn new(device: &wgpu::Device) -> Self {
420        let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
421            label: Some("live exposure layout"),
422            entries: &[
423                wgpu::BindGroupLayoutEntry {
424                    binding: 0,
425                    visibility: wgpu::ShaderStages::COMPUTE,
426                    ty: wgpu::BindingType::Texture {
427                        sample_type: wgpu::TextureSampleType::Float { filterable: false },
428                        view_dimension: wgpu::TextureViewDimension::D2,
429                        multisampled: false,
430                    },
431                    count: None,
432                },
433                wgpu::BindGroupLayoutEntry {
434                    binding: 1,
435                    visibility: wgpu::ShaderStages::COMPUTE,
436                    ty: wgpu::BindingType::Buffer {
437                        ty: wgpu::BufferBindingType::Storage { read_only: false },
438                        has_dynamic_offset: false,
439                        min_binding_size: None,
440                    },
441                    count: None,
442                },
443                wgpu::BindGroupLayoutEntry {
444                    binding: 2,
445                    visibility: wgpu::ShaderStages::COMPUTE,
446                    ty: wgpu::BindingType::Buffer {
447                        ty: wgpu::BufferBindingType::Uniform,
448                        has_dynamic_offset: false,
449                        min_binding_size: None,
450                    },
451                    count: None,
452                },
453            ],
454        });
455        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
456            label: Some("live exposure pipeline layout"),
457            bind_group_layouts: &[Some(&layout)],
458            immediate_size: 0,
459        });
460        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
461            label: Some("live exposure shader"),
462            source: wgpu::ShaderSource::Wgsl(EXPOSURE_WGSL.into()),
463        });
464        let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
465            label: Some("live exposure pipeline"),
466            layout: Some(&pipeline_layout),
467            module: &module,
468            entry_point: Some("main"),
469            compilation_options: wgpu::PipelineCompilationOptions::default(),
470            cache: None,
471        });
472        let value = device.create_buffer(&wgpu::BufferDescriptor {
473            label: Some("live log luminance"),
474            size: 4,
475            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
476            mapped_at_creation: false,
477        });
478        let readback = device.create_buffer(&wgpu::BufferDescriptor {
479            label: Some("live luminance readback"),
480            size: 4,
481            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
482            mapped_at_creation: false,
483        });
484        let extent = device.create_buffer(&wgpu::BufferDescriptor {
485            label: Some("live exposure extent"),
486            size: 16,
487            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
488            mapped_at_creation: false,
489        });
490        Self {
491            layout,
492            pipeline,
493            value,
494            readback,
495            extent,
496            written: None,
497            pending: None,
498            stale: false,
499            exposure: 1.0,
500            last_time: None,
501        }
502    }
503
504    fn advance(&mut self, time: f32, valid: bool) -> f32 {
505        let seconds = self
506            .last_time
507            .map_or(1.0 / 60.0, |last| (time - last).max(0.0));
508        self.last_time = Some(time);
509        if !valid {
510            self.exposure = 1.0;
511            self.stale = self.pending.is_some();
512        }
513        if let Some(receiver) = &self.pending {
514            match receiver.try_recv() {
515                Ok(Ok(())) => {
516                    let bytes = self
517                        .readback
518                        .slice(..)
519                        .get_mapped_range()
520                        .expect("a mapped range");
521                    let log_luminance = f32::from_le_bytes(bytes[..4].try_into().unwrap());
522                    drop(bytes);
523                    self.readback.unmap();
524                    self.pending = None;
525                    if valid && !self.stale && log_luminance.is_finite() {
526                        self.exposure = pfx_core::sky::adapt_exposure(
527                            self.exposure,
528                            &[log_luminance.exp()],
529                            seconds,
530                        );
531                    }
532                    self.stale = false;
533                }
534                Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
535                    self.pending = None;
536                    self.stale = false;
537                }
538                Err(TryRecvError::Empty) => {}
539            }
540        }
541        self.exposure
542    }
543
544    fn encode(
545        &mut self,
546        device: &wgpu::Device,
547        queue: &wgpu::Queue,
548        encoder: &mut wgpu::CommandEncoder,
549        source: &wgpu::TextureView,
550        viewport: [u32; 2],
551    ) {
552        if self.pending.is_some() {
553            return;
554        }
555        if self.written != Some(viewport) {
556            queue.write_buffer(
557                &self.extent,
558                0,
559                bytemuck::cast_slice(&[viewport[0], viewport[1], 0, 0]),
560            );
561            self.written = Some(viewport);
562        }
563        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
564            label: Some("live exposure bindings"),
565            layout: &self.layout,
566            entries: &[
567                wgpu::BindGroupEntry {
568                    binding: 0,
569                    resource: wgpu::BindingResource::TextureView(source),
570                },
571                wgpu::BindGroupEntry {
572                    binding: 1,
573                    resource: self.value.as_entire_binding(),
574                },
575                wgpu::BindGroupEntry {
576                    binding: 2,
577                    resource: self.extent.as_entire_binding(),
578                },
579            ],
580        });
581        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
582            label: Some("live exposure reduction"),
583            timestamp_writes: None,
584        });
585        pass.set_pipeline(&self.pipeline);
586        pass.set_bind_group(0, &bind, &[]);
587        pass.dispatch_workgroups(1, 1, 1);
588        drop(pass);
589        encoder.copy_buffer_to_buffer(&self.value, 0, &self.readback, 0, 4);
590    }
591
592    fn submitted(&mut self) {
593        if self.pending.is_some() {
594            return;
595        }
596        let (sender, receiver) = std::sync::mpsc::channel();
597        self.readback
598            .slice(..)
599            .map_async(wgpu::MapMode::Read, move |result| {
600                let _ = sender.send(result);
601            });
602        self.pending = Some(receiver);
603    }
604}
605
606const EXPOSURE_WGSL: &str = r#"
607@group(0) @binding(0) var source: texture_2d<f32>;
608@group(0) @binding(1) var<storage, read_write> output: array<f32>;
609@group(0) @binding(2) var<uniform> extent: vec4u;
610var<workgroup> values: array<f32, 256>;
611@compute @workgroup_size(256)
612fn main(@builtin(local_invocation_index) index: u32) {
613    let size = min(textureDimensions(source), extent.xy);
614    var total = 0.0;
615    for (var sample = 0u; sample < 4u; sample += 1u) {
616        let point = index * 4u + sample;
617        let x = min((point % 64u) * size.x / 64u + size.x / 128u, size.x - 1u);
618        let y = min((point / 64u) * size.y / 16u + size.y / 32u, size.y - 1u);
619        let rgb = textureLoad(source, vec2i(i32(x), i32(y)), 0).rgb;
620        total += log(max(dot(rgb, vec3f(0.2126, 0.7152, 0.0722)), 0.001));
621    }
622    values[index] = total;
623    workgroupBarrier();
624    var stride = 128u;
625    loop {
626        if (index < stride) { values[index] += values[index + stride]; }
627        workgroupBarrier();
628        if (stride == 1u) { break; }
629        stride /= 2u;
630    }
631    if (index == 0u) { output[0] = values[0] / 1024.0; }
632}
633"#;
634
635pub struct Renderer {
636    frame: Frame,
637    shadows: Shadows,
638    taa: taa::Pass,
639    crisp: crisp::Pass,
640    sharpen: f32,
641    text_after_taa: bool,
642    text: TextPass,
643    effects: effects::Effects,
644    reflections: reflect::Pass,
645    glass: glass::Pass,
646    impostors: Option<ImpostorPass>,
647    petals: Option<Petals>,
648    lod: LodSet,
649    post: GpuChain,
650    dof: dof::GpuDof,
651    lens: Option<dof::Lens>,
652    haze: Option<RoomHaze>,
653    exposure: ExposureMeter,
654    exposure_mode: Exposure,
655    custom_finish: bool,
656    tape: Option<Tape>,
657    pick_request: Option<(u32, u32)>,
658    pick_readback: Option<PickReadback>,
659    style: Finish,
660    output_format: wgpu::TextureFormat,
661    display: Option<(wgpu::Texture, wgpu::TextureView)>,
662    blit: Option<(wgpu::BindGroupLayout, wgpu::RenderPipeline)>,
663    depths: [(wgpu::Texture, wgpu::TextureView); 2],
664    depth_index: usize,
665    reactive: wgpu::Texture,
666    reactive_view: wgpu::TextureView,
667    depth_layout: wgpu::BindGroupLayout,
668    depth_pipeline: wgpu::ComputePipeline,
669    motion: Motion,
670    mips: Mips,
671    black: wgpu::TextureView,
672    profiler: GpuProfiler,
673    frames_submitted: u64,
674    frames_done: Arc<AtomicU64>,
675    submissions: VecDeque<(u64, wgpu::SubmissionIndex)>,
676    arrivals: Vec<FrameTimings>,
677    latest_timings: Option<FrameTimings>,
678    late_timings: u64,
679    stats: FrameStats,
680    resolution: Option<DynamicResolution>,
681    resolution_content: Option<[u32; 2]>,
682    resolution_frame: bool,
683    sky_hours: Vec<(u64, f32)>,
684    history: History,
685    history_valid: bool,
686    hour: f32,
687    probe_emission: f32,
688    pass_log: Vec<&'static str>,
689    text_bounds: Vec<TextBounds>,
690    flat: Option<FlatPass>,
691    flat_looks: Option<FlatLooks>,
692    plates: Option<crate::plates::Plates>,
693    plate_kit: Option<crate::plates::PlateKit>,
694    plate_casters: Vec<Instance>,
695    plate_mask: bool,
696    demand: Option<demand::Cache>,
697    revision: AtomicU64,
698    custom_local: Option<bool>,
699}
700
701pub struct GeometryReadback {
702    pub colour: Vec<[f32; 4]>,
703    pub depth: Vec<f32>,
704    pub normals: Vec<[f32; 3]>,
705}
706
707fn chain(style: Finish, encode: bool) -> Chain {
708    let mut chain = match style {
709        Finish::Standard => Chain {
710            passes: vec![
711                PostPass::Bloom(Bloom::neutral(0.0)),
712                PostPass::Tone(Tone::aces()),
713            ],
714            frame: 0,
715            seed: 0,
716        },
717        Finish::Style(style) => style.chain(),
718    };
719    chain.passes.insert(0, PostPass::Exposure(1.0));
720    if encode {
721        chain.passes.push(PostPass::Encode);
722    }
723    chain
724}
725
726fn plain_passes(passes: Vec<PassTiming>) -> Vec<PassTiming> {
727    let mut merged: Vec<PassTiming> = Vec::new();
728    for pass in passes {
729        let label = if pass.label.starts_with("post ") {
730            "post"
731        } else if pass.label.starts_with("volume ") {
732            "volume"
733        } else {
734            &pass.label
735        };
736        if let Some(existing) = merged.iter_mut().find(|item| item.label == label) {
737            existing.milliseconds += pass.milliseconds;
738        } else {
739            merged.push(PassTiming {
740                label: label.to_owned(),
741                milliseconds: pass.milliseconds,
742            });
743        }
744    }
745    merged.sort_by(|a, b| {
746        b.milliseconds
747            .total_cmp(&a.milliseconds)
748            .then_with(|| a.label.cmp(&b.label))
749    });
750    merged.truncate(16);
751    merged
752}
753
754const FULLSCREEN: &str = "@vertex fn vertex(@builtin(vertex_index) index: u32) -> @builtin(position) vec4f { let xy = array<vec2f, 3>(vec2f(-1.0, -1.0), vec2f(3.0, -1.0), vec2f(-1.0, 3.0)); return vec4f(xy[index], 0.0, 1.0); }";
755
756const MOTION_WGSL: &str = r#"
757struct MotionUniform {
758    inverse: mat4x4f,
759    current: mat4x4f,
760    previous: mat4x4f,
761    size: vec4f,
762}
763@group(0) @binding(0) var scene_depth: texture_depth_2d;
764@group(0) @binding(1) var scene_ids: texture_2d<u32>;
765@group(0) @binding(2) var<uniform> motion: MotionUniform;
766@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec2f {
767    let pixel = vec2i(position.xy);
768    if (textureLoad(scene_ids, pixel, 0).r != 0u) {
769        discard;
770    }
771    let z = textureLoad(scene_depth, pixel, 0);
772    let uv = position.xy * motion.size.zw;
773    let world = motion.inverse * vec4f(uv.x * 2.0 - 1.0, 1.0 - uv.y * 2.0, z, 1.0);
774    let point = vec4f(world.xyz / world.w, 1.0);
775    let now = motion.current * point;
776    let before = motion.previous * point;
777    let moved = (before.xy / before.w - now.xy / now.w) * vec2f(0.5, -0.5);
778    return select(moved, vec2f(0.0), abs(moved) < vec2f(1e-8));
779}
780"#;
781
782const DOWNSAMPLE_WGSL: &str = r#"
783@group(0) @binding(0) var source: texture_2d<f32>;
784@group(0) @binding(1) var source_sampler: sampler;
785@group(0) @binding(2) var<uniform> extent: vec4u;
786@fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {
787    let size = vec2f(textureDimensions(source)) * 0.5;
788    let pixel = min(position.xy, vec2f(extent.xy) - vec2f(0.5));
789    return textureSampleLevel(source, source_sampler, pixel / size, 0.0);
790}
791"#;
792
793pub fn motion_source() -> String {
794    format!("{FULLSCREEN}\n{MOTION_WGSL}")
795}
796
797pub fn downsample_source() -> String {
798    format!("{FULLSCREEN}\n{DOWNSAMPLE_WGSL}")
799}
800
801fn blit_pipeline(
802    device: &wgpu::Device,
803    format: wgpu::TextureFormat,
804) -> (wgpu::BindGroupLayout, wgpu::RenderPipeline) {
805    let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
806        label: Some("live output blit"),
807        entries: &[wgpu::BindGroupLayoutEntry {
808            binding: 0,
809            visibility: wgpu::ShaderStages::FRAGMENT,
810            ty: wgpu::BindingType::Texture {
811                sample_type: wgpu::TextureSampleType::Float { filterable: false },
812                view_dimension: wgpu::TextureViewDimension::D2,
813                multisampled: false,
814            },
815            count: None,
816        }],
817    });
818    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
819        label: Some("live output blit"),
820        bind_group_layouts: &[Some(&layout)],
821        immediate_size: 0,
822    });
823    let source = format!(
824        "{FULLSCREEN}\n@group(0) @binding(0) var source: texture_2d<f32>; @fragment fn fragment(@builtin(position) position: vec4f) -> @location(0) vec4f {{ return textureLoad(source, vec2i(position.xy), 0); }}"
825    );
826    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
827        label: Some("live output blit"),
828        source: wgpu::ShaderSource::Wgsl(source.into()),
829    });
830    let pipeline = fullscreen(
831        device,
832        &pipeline_layout,
833        &shader,
834        format,
835        "live output blit",
836    );
837    (layout, pipeline)
838}
839
840fn fullscreen(
841    device: &wgpu::Device,
842    layout: &wgpu::PipelineLayout,
843    shader: &wgpu::ShaderModule,
844    format: wgpu::TextureFormat,
845    label: &str,
846) -> wgpu::RenderPipeline {
847    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
848        label: Some(label),
849        layout: Some(layout),
850        vertex: wgpu::VertexState {
851            module: shader,
852            entry_point: Some("vertex"),
853            buffers: &[],
854            compilation_options: Default::default(),
855        },
856        primitive: Default::default(),
857        depth_stencil: None,
858        multisample: Default::default(),
859        fragment: Some(wgpu::FragmentState {
860            module: shader,
861            entry_point: Some("fragment"),
862            targets: &[Some(wgpu::ColorTargetState {
863                format,
864                blend: None,
865                write_mask: wgpu::ColorWrites::ALL,
866            })],
867            compilation_options: Default::default(),
868        }),
869        multiview_mask: None,
870        cache: None,
871    })
872}
873
874fn image(
875    device: &wgpu::Device,
876    width: u32,
877    height: u32,
878    format: wgpu::TextureFormat,
879    usage: wgpu::TextureUsages,
880    label: &str,
881) -> (wgpu::Texture, wgpu::TextureView) {
882    let texture = device.create_texture(&wgpu::TextureDescriptor {
883        label: Some(label),
884        size: wgpu::Extent3d {
885            width,
886            height,
887            depth_or_array_layers: 1,
888        },
889        mip_level_count: 1,
890        sample_count: 1,
891        dimension: wgpu::TextureDimension::D2,
892        format,
893        usage,
894        view_formats: &[],
895    });
896    let view = texture.create_view(&Default::default());
897    (texture, view)
898}
899
900fn linear_depths(
901    device: &wgpu::Device,
902    width: u32,
903    height: u32,
904) -> [(wgpu::Texture, wgpu::TextureView); 2] {
905    [0, 1].map(|_| {
906        image(
907            device,
908            width,
909            height,
910            wgpu::TextureFormat::R32Float,
911            wgpu::TextureUsages::STORAGE_BINDING
912                | wgpu::TextureUsages::TEXTURE_BINDING
913                | wgpu::TextureUsages::COPY_SRC,
914            "live linear depth",
915        )
916    })
917}
918
919fn depth_pipeline(device: &wgpu::Device) -> (wgpu::BindGroupLayout, wgpu::ComputePipeline) {
920    let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
921        label: Some("live linear depth"),
922        entries: &[
923            wgpu::BindGroupLayoutEntry {
924                binding: 0,
925                visibility: wgpu::ShaderStages::COMPUTE,
926                ty: wgpu::BindingType::Texture {
927                    sample_type: wgpu::TextureSampleType::Depth,
928                    view_dimension: wgpu::TextureViewDimension::D2,
929                    multisampled: false,
930                },
931                count: None,
932            },
933            wgpu::BindGroupLayoutEntry {
934                binding: 1,
935                visibility: wgpu::ShaderStages::COMPUTE,
936                ty: wgpu::BindingType::StorageTexture {
937                    access: wgpu::StorageTextureAccess::WriteOnly,
938                    format: wgpu::TextureFormat::R32Float,
939                    view_dimension: wgpu::TextureViewDimension::D2,
940                },
941                count: None,
942            },
943            wgpu::BindGroupLayoutEntry {
944                binding: 2,
945                visibility: wgpu::ShaderStages::COMPUTE,
946                ty: wgpu::BindingType::Buffer {
947                    ty: wgpu::BufferBindingType::Uniform,
948                    has_dynamic_offset: false,
949                    min_binding_size: None,
950                },
951                count: None,
952            },
953        ],
954    });
955    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
956        label: Some("live linear depth"),
957        bind_group_layouts: &[Some(&layout)],
958        immediate_size: 0,
959    });
960    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor { label: Some("live linear depth"), source: wgpu::ShaderSource::Wgsl("@group(0) @binding(0) var source: texture_depth_2d; @group(0) @binding(1) var destination: texture_storage_2d<r32float, write>; @group(0) @binding(2) var<uniform> limits: vec4f; @compute @workgroup_size(8, 8) fn main(@builtin(global_invocation_id) id: vec3u) { let size = textureDimensions(source); if (id.x >= size.x || id.y >= size.y) { return; } let z = textureLoad(source, id.xy, 0); let distance = limits.y / (z + limits.x); textureStore(destination, id.xy, vec4f(select(limits.z, distance, z < 1.0 && distance > 0.0), 0.0, 0.0, 0.0)); }".into()) });
961    let pipeline = device.create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
962        label: Some("live linear depth"),
963        layout: Some(&pipeline_layout),
964        module: &shader,
965        entry_point: Some("main"),
966        compilation_options: Default::default(),
967        cache: None,
968    });
969    (layout, pipeline)
970}
971
972fn motion_pass(device: &wgpu::Device) -> Motion {
973    let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
974        label: Some("live background motion"),
975        entries: &[
976            wgpu::BindGroupLayoutEntry {
977                binding: 0,
978                visibility: wgpu::ShaderStages::FRAGMENT,
979                ty: wgpu::BindingType::Texture {
980                    sample_type: wgpu::TextureSampleType::Depth,
981                    view_dimension: wgpu::TextureViewDimension::D2,
982                    multisampled: false,
983                },
984                count: None,
985            },
986            wgpu::BindGroupLayoutEntry {
987                binding: 1,
988                visibility: wgpu::ShaderStages::FRAGMENT,
989                ty: wgpu::BindingType::Texture {
990                    sample_type: wgpu::TextureSampleType::Uint,
991                    view_dimension: wgpu::TextureViewDimension::D2,
992                    multisampled: false,
993                },
994                count: None,
995            },
996            wgpu::BindGroupLayoutEntry {
997                binding: 2,
998                visibility: wgpu::ShaderStages::FRAGMENT,
999                ty: wgpu::BindingType::Buffer {
1000                    ty: wgpu::BufferBindingType::Uniform,
1001                    has_dynamic_offset: false,
1002                    min_binding_size: None,
1003                },
1004                count: None,
1005            },
1006        ],
1007    });
1008    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1009        label: Some("live background motion"),
1010        bind_group_layouts: &[Some(&layout)],
1011        immediate_size: 0,
1012    });
1013    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1014        label: Some("live background motion"),
1015        source: wgpu::ShaderSource::Wgsl(motion_source().into()),
1016    });
1017    let pipeline = fullscreen(
1018        device,
1019        &pipeline_layout,
1020        &shader,
1021        wgpu::TextureFormat::Rg16Float,
1022        "live background motion",
1023    );
1024    let uniform = device.create_buffer(&wgpu::BufferDescriptor {
1025        label: Some("live background motion"),
1026        size: 208,
1027        usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1028        mapped_at_creation: false,
1029    });
1030    Motion {
1031        layout,
1032        pipeline,
1033        uniform,
1034    }
1035}
1036
1037fn mips(device: &wgpu::Device, width: u32, height: u32) -> Mips {
1038    let levels_count = SCENE_COLOUR_MIPS.min(32 - width.max(height).leading_zeros());
1039    let texture = device.create_texture(&wgpu::TextureDescriptor {
1040        label: Some("live scene colour"),
1041        size: wgpu::Extent3d {
1042            width,
1043            height,
1044            depth_or_array_layers: 1,
1045        },
1046        mip_level_count: levels_count,
1047        sample_count: 1,
1048        dimension: wgpu::TextureDimension::D2,
1049        format: wgpu::TextureFormat::Rgba16Float,
1050        usage: wgpu::TextureUsages::TEXTURE_BINDING
1051            | wgpu::TextureUsages::RENDER_ATTACHMENT
1052            | wgpu::TextureUsages::COPY_DST,
1053        view_formats: &[],
1054    });
1055    let view = texture.create_view(&Default::default());
1056    let levels: Vec<wgpu::TextureView> = (0..levels_count)
1057        .map(|level| {
1058            texture.create_view(&wgpu::TextureViewDescriptor {
1059                base_mip_level: level,
1060                mip_level_count: Some(1),
1061                ..Default::default()
1062            })
1063        })
1064        .collect();
1065    let layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1066        label: Some("live scene colour mips"),
1067        entries: &[
1068            wgpu::BindGroupLayoutEntry {
1069                binding: 0,
1070                visibility: wgpu::ShaderStages::FRAGMENT,
1071                ty: wgpu::BindingType::Texture {
1072                    sample_type: wgpu::TextureSampleType::Float { filterable: true },
1073                    view_dimension: wgpu::TextureViewDimension::D2,
1074                    multisampled: false,
1075                },
1076                count: None,
1077            },
1078            wgpu::BindGroupLayoutEntry {
1079                binding: 1,
1080                visibility: wgpu::ShaderStages::FRAGMENT,
1081                ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1082                count: None,
1083            },
1084            wgpu::BindGroupLayoutEntry {
1085                binding: 2,
1086                visibility: wgpu::ShaderStages::FRAGMENT,
1087                ty: wgpu::BindingType::Buffer {
1088                    ty: wgpu::BufferBindingType::Uniform,
1089                    has_dynamic_offset: false,
1090                    min_binding_size: None,
1091                },
1092                count: None,
1093            },
1094        ],
1095    });
1096    let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
1097        label: Some("live scene colour mips"),
1098        bind_group_layouts: &[Some(&layout)],
1099        immediate_size: 0,
1100    });
1101    let shader = device.create_shader_module(wgpu::ShaderModuleDescriptor {
1102        label: Some("live scene colour mips"),
1103        source: wgpu::ShaderSource::Wgsl(downsample_source().into()),
1104    });
1105    let pipeline = fullscreen(
1106        device,
1107        &pipeline_layout,
1108        &shader,
1109        wgpu::TextureFormat::Rgba16Float,
1110        "live scene colour mips",
1111    );
1112    let sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1113        label: Some("live scene colour mips"),
1114        mag_filter: wgpu::FilterMode::Linear,
1115        min_filter: wgpu::FilterMode::Linear,
1116        ..Default::default()
1117    });
1118    let extents: Vec<wgpu::Buffer> = (0..levels_count)
1119        .map(|_| {
1120            device.create_buffer(&wgpu::BufferDescriptor {
1121                label: Some("live scene colour mip extent"),
1122                size: 16,
1123                usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1124                mapped_at_creation: false,
1125            })
1126        })
1127        .collect();
1128    let binds = (1..levels.len())
1129        .map(|level| {
1130            device.create_bind_group(&wgpu::BindGroupDescriptor {
1131                label: Some("live scene colour mips"),
1132                layout: &layout,
1133                entries: &[
1134                    wgpu::BindGroupEntry {
1135                        binding: 0,
1136                        resource: wgpu::BindingResource::TextureView(&levels[level - 1]),
1137                    },
1138                    wgpu::BindGroupEntry {
1139                        binding: 1,
1140                        resource: wgpu::BindingResource::Sampler(&sampler),
1141                    },
1142                    wgpu::BindGroupEntry {
1143                        binding: 2,
1144                        resource: extents[level].as_entire_binding(),
1145                    },
1146                ],
1147            })
1148        })
1149        .collect();
1150    Mips {
1151        texture,
1152        view,
1153        levels,
1154        extents,
1155        binds,
1156        written: None,
1157        pipeline,
1158    }
1159}
1160
1161pub fn invert(m: Matrix) -> Option<Matrix> {
1162    let a = |c: usize, r: usize| f64::from(m[c][r]);
1163    let cofactor = |c: usize, r: usize| {
1164        let rows: Vec<usize> = (0..4).filter(|&i| i != c).collect();
1165        let columns: Vec<usize> = (0..4).filter(|&i| i != r).collect();
1166        let minor = |i: usize, j: usize| a(columns[i], rows[j]);
1167        let value = minor(0, 0) * (minor(1, 1) * minor(2, 2) - minor(1, 2) * minor(2, 1))
1168            - minor(0, 1) * (minor(1, 0) * minor(2, 2) - minor(1, 2) * minor(2, 0))
1169            + minor(0, 2) * (minor(1, 0) * minor(2, 1) - minor(1, 1) * minor(2, 0));
1170        if (c + r).is_multiple_of(2) {
1171            value
1172        } else {
1173            -value
1174        }
1175    };
1176    let inverse: [[f64; 4]; 4] = std::array::from_fn(|c| std::array::from_fn(|r| cofactor(c, r)));
1177    let determinant: f64 = (0..4).map(|k| a(k, 0) * inverse[0][k]).sum();
1178    if !determinant.is_finite() || determinant.abs() < 1e-20 {
1179        return None;
1180    }
1181    Some(inverse.map(|column| column.map(|value| (value / determinant) as f32)))
1182}
1183
1184fn camera_view(scene: &Scene<'_>, width: u32, height: u32) -> View {
1185    let view = scene.camera.view;
1186    let projection = scene.camera.projection;
1187    let a = projection[2][2];
1188    let b = projection[3][2];
1189    View {
1190        eye: scene.camera.position,
1191        forward: [-view[0][2], -view[1][2], -view[2][2]],
1192        up: [view[0][1], view[1][1], view[2][1]],
1193        fov_y: 2.0 * (1.0 / projection[1][1]).atan(),
1194        aspect: width as f32 / height as f32,
1195        near: (b / a).abs().max(0.01),
1196        far: (b / (a + 1.0)).abs().clamp(1.0, SHADOW_RANGE),
1197    }
1198}
1199
1200fn solid_view(device: &wgpu::Device, queue: &wgpu::Queue, label: &str) -> wgpu::TextureView {
1201    device
1202        .create_texture_with_data(
1203            queue,
1204            &wgpu::TextureDescriptor {
1205                label: Some(label),
1206                size: wgpu::Extent3d {
1207                    width: 1,
1208                    height: 1,
1209                    depth_or_array_layers: 1,
1210                },
1211                mip_level_count: 1,
1212                sample_count: 1,
1213                dimension: wgpu::TextureDimension::D2,
1214                format: wgpu::TextureFormat::Rgba16Float,
1215                usage: wgpu::TextureUsages::TEXTURE_BINDING,
1216                view_formats: &[],
1217            },
1218            wgpu::util::TextureDataOrder::LayerMajor,
1219            &[0; 8],
1220        )
1221        .create_view(&Default::default())
1222}
1223
1224impl Renderer {
1225    pub fn new(gpu: Gpu, width: u32, height: u32) -> Result<Self, RendererError> {
1226        Self::new_with_output_format(gpu, width, height, wgpu::TextureFormat::Rgba16Float)
1227    }
1228
1229    pub fn new_with_output_format(
1230        gpu: Gpu,
1231        width: u32,
1232        height: u32,
1233        output_format: wgpu::TextureFormat,
1234    ) -> Result<Self, RendererError> {
1235        gpu.check_floor()?;
1236        if width == 0 || height == 0 {
1237            return Err(String::from("viewport is empty").into());
1238        }
1239        if !matches!(
1240            output_format,
1241            wgpu::TextureFormat::Rgba16Float
1242                | wgpu::TextureFormat::Rgba8UnormSrgb
1243                | wgpu::TextureFormat::Bgra8UnormSrgb
1244        ) {
1245            return Err(String::from("unsupported output format").into());
1246        }
1247        let device = &gpu.device;
1248        let shadows = Shadows::new(device, Quality::default());
1249        let mut taa = taa::Pass::new(device);
1250        taa.resize(device, width, height);
1251        let text = TextPass::new(device);
1252        let effects = effects::Effects::new(device, width, height, 4)?;
1253        let style = Finish::Standard;
1254        let post = GpuChain::new(
1255            chain(style, output_format == wgpu::TextureFormat::Rgba16Float),
1256            device,
1257            width,
1258            height,
1259        );
1260        let display = (output_format != wgpu::TextureFormat::Rgba16Float).then(|| {
1261            image(
1262                device,
1263                width,
1264                height,
1265                wgpu::TextureFormat::Rgba16Float,
1266                wgpu::TextureUsages::STORAGE_BINDING
1267                    | wgpu::TextureUsages::RENDER_ATTACHMENT
1268                    | wgpu::TextureUsages::TEXTURE_BINDING,
1269                "live display intermediate",
1270            )
1271        });
1272        let blit = display
1273            .as_ref()
1274            .map(|_| blit_pipeline(device, output_format));
1275        let (reactive, reactive_view) = image(
1276            device,
1277            width,
1278            height,
1279            wgpu::TextureFormat::R8Unorm,
1280            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
1281            "live reactive mask",
1282        );
1283        let (depth_layout, depth_pipeline) = depth_pipeline(device);
1284        let profiler = GpuProfiler::new(device, &gpu.queue);
1285        let black = solid_view(device, &gpu.queue, "live empty input");
1286        let mut renderer = Self {
1287            shadows,
1288            taa,
1289            crisp: crisp::Pass::new(device, width, height),
1290            sharpen: 0.0,
1291            text_after_taa: true,
1292            text,
1293            effects,
1294            reflections: reflect::Pass::new(device, width, height),
1295            glass: glass::Pass::new(device, width, height),
1296            impostors: None,
1297            petals: None,
1298            lod: LodSet::new(2_000_000),
1299            post,
1300            dof: dof::GpuDof::new(device, width, height),
1301            lens: None,
1302            haze: None,
1303            exposure: ExposureMeter::new(device),
1304            exposure_mode: Exposure::Auto { bias: 0.0 },
1305            custom_finish: false,
1306            tape: None,
1307            pick_request: None,
1308            pick_readback: None,
1309            style,
1310            output_format,
1311            display,
1312            blit,
1313            depths: linear_depths(device, width, height),
1314            depth_index: 0,
1315            reactive,
1316            reactive_view,
1317            depth_layout,
1318            depth_pipeline,
1319            motion: motion_pass(device),
1320            mips: mips(device, width, height),
1321            black,
1322            profiler,
1323            frames_submitted: 0,
1324            frames_done: Arc::new(AtomicU64::new(0)),
1325            submissions: VecDeque::new(),
1326            arrivals: Vec::new(),
1327            latest_timings: None,
1328            late_timings: 0,
1329            stats: FrameStats::from_env(),
1330            resolution: None,
1331            resolution_content: None,
1332            resolution_frame: false,
1333            sky_hours: Vec::new(),
1334            history: History::default(),
1335            history_valid: false,
1336            hour: 12.0,
1337            probe_emission: 0.0,
1338            pass_log: Vec::new(),
1339            text_bounds: Vec::new(),
1340            flat: None,
1341            flat_looks: None,
1342            plates: None,
1343            plate_kit: None,
1344            plate_casters: Vec::new(),
1345            plate_mask: false,
1346            demand: None,
1347            revision: AtomicU64::new(0),
1348            custom_local: None,
1349            frame: Frame::new(gpu, width, height)?,
1350        };
1351        renderer.frame.set_reflections_planned(true);
1352        Ok(renderer)
1353    }
1354
1355    pub fn gpu(&self) -> &Gpu {
1356        &self.frame.gpu
1357    }
1358
1359    pub fn size(&self) -> (u32, u32) {
1360        let [width, height] = self.frame.viewport();
1361        (width, height)
1362    }
1363
1364    pub fn capacity(&self) -> (u32, u32) {
1365        (self.frame.targets.width, self.frame.targets.height)
1366    }
1367
1368    pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
1369        let (capacity_width, capacity_height) = self.capacity();
1370        crate::viewport::check([width, height], [capacity_width, capacity_height])?;
1371        if self.size() == (width, height) {
1372            return Ok(());
1373        }
1374        if self.demand.is_some() && (width, height) != self.capacity() {
1375            return Err("frames on demand render at the full size; turn them off to render into a smaller viewport".into());
1376        }
1377        self.touch();
1378        self.set_viewport_parts(width, height)
1379    }
1380
1381    fn set_viewport_parts(&mut self, width: u32, height: u32) -> Result<(), String> {
1382        self.frame.set_viewport(width, height)?;
1383        self.taa.set_viewport(width, height)?;
1384        self.crisp.set_viewport(width, height)?;
1385        self.effects.set_viewport(width, height)?;
1386        self.reflections.set_viewport(width, height)?;
1387        self.glass.set_viewport(width, height)?;
1388        self.post.set_viewport(width, height)?;
1389        self.dof.set_viewport(width, height)?;
1390        self.text_bounds.clear();
1391        if self
1392            .pick_request
1393            .is_some_and(|(x, y)| x >= width || y >= height)
1394        {
1395            self.pick_request = None;
1396        }
1397        Ok(())
1398    }
1399
1400    pub fn history_restarts(&self) -> u64 {
1401        self.taa.restarts()
1402    }
1403
1404    pub fn target_textures(&self) -> Vec<wgpu::Texture> {
1405        let targets = &self.frame.targets;
1406        let mut textures = vec![
1407            targets.hdr.texture.clone(),
1408            targets.depth.clone(),
1409            targets.velocity.clone(),
1410            targets.ids.clone(),
1411            targets.normal_roughness.clone(),
1412            self.reactive.clone(),
1413            self.mips.texture.clone(),
1414            self.depths[0].0.clone(),
1415            self.depths[1].0.clone(),
1416        ];
1417        textures.extend(self.taa.textures());
1418        textures.extend(self.crisp.target().cloned());
1419        textures.extend(self.effects.textures());
1420        textures.extend(self.reflections.textures());
1421        textures.extend(self.glass.textures());
1422        textures.extend(self.display.as_ref().map(|(texture, _)| texture.clone()));
1423        textures.extend(self.flat.iter().flat_map(FlatPass::target_textures));
1424        textures
1425    }
1426
1427    fn post_chain(&self, finish: Chain) -> Result<GpuChain, String> {
1428        let (capacity_width, capacity_height) = self.capacity();
1429        let (width, height) = self.size();
1430        let mut post = GpuChain::new(
1431            finish,
1432            &self.frame.gpu.device,
1433            capacity_width,
1434            capacity_height,
1435        );
1436        post.set_viewport(width, height)?;
1437        Ok(post)
1438    }
1439
1440    pub fn output_format(&self) -> wgpu::TextureFormat {
1441        self.output_format
1442    }
1443
1444    pub fn last_pass_order(&self) -> &[&'static str] {
1445        &self.pass_log
1446    }
1447
1448    pub fn set_history_gap(&mut self, seconds: f32) {
1449        self.history.set_max_gap(seconds);
1450    }
1451
1452    pub fn history_valid(&self) -> bool {
1453        self.history_valid
1454    }
1455
1456    pub fn frame(&self) -> &Frame {
1457        &self.frame
1458    }
1459
1460    pub fn warm(&mut self, scenes: &[Scene<'_>]) -> Result<Warming, String> {
1461        let (width, height) = self.size();
1462        let mut keys: Vec<Vec<OpaqueKey>> = Vec::with_capacity(scenes.len());
1463        for scene in scenes {
1464            let fit = self
1465                .shadows
1466                .fit(&camera_view(scene, width, height), scene.sun.direction);
1467            keys.push(self.frame.opaque_keys(scene, Some((&self.shadows, &fit)))?);
1468        }
1469        let first = keys.first().cloned().unwrap_or_default();
1470        let later: Vec<OpaqueKey> = keys.into_iter().skip(1).flatten().collect();
1471        Ok(self.frame.warm(&first, &later))
1472    }
1473
1474    pub fn set_warm_workers(&mut self, workers: usize) {
1475        self.frame.set_warm_workers(workers);
1476    }
1477
1478    pub fn use_pipeline_cache(&mut self, path: &std::path::Path) -> CacheLoad {
1479        self.frame.use_pipeline_cache(path)
1480    }
1481
1482    pub fn save_pipeline_cache(&self) -> Result<Option<usize>, String> {
1483        self.frame.save_pipeline_cache()
1484    }
1485
1486    pub fn warm_opaque(&mut self, features: frame::OpaqueFeatures, lit: bool) {
1487        self.frame.warm_opaque(features, lit);
1488    }
1489
1490    pub fn set_finish(&mut self, mut finish: Chain) {
1491        self.touch();
1492        finish.passes.insert(0, PostPass::Exposure(1.0));
1493        if self.display.is_none() {
1494            finish.passes.push(PostPass::Encode);
1495        }
1496        self.custom_local = Some(demand::local_post(&finish));
1497        if let Ok(post) = self.post_chain(finish) {
1498            self.post = post;
1499        }
1500        self.custom_finish = true;
1501    }
1502
1503    pub fn set_tape(&mut self, tape: Option<Tape>) {
1504        if tape != self.tape {
1505            self.touch();
1506        }
1507        self.tape = tape;
1508        match tape {
1509            Some(tape) => self.post.set_tape(tape),
1510            None => self.post.reset_tape(),
1511        }
1512    }
1513
1514    pub fn clear_finish(&mut self) {
1515        self.touch();
1516        self.custom_finish = false;
1517        self.custom_local = None;
1518        if let Ok(post) = self.post_chain(chain(self.style, self.display.is_none())) {
1519            self.post = post;
1520        }
1521    }
1522
1523    pub fn set_text_after_taa(&mut self, after: bool) {
1524        if after != self.text_after_taa {
1525            self.touch();
1526        }
1527        self.text_after_taa = after;
1528    }
1529
1530    pub fn text_after_taa(&self) -> bool {
1531        self.text_after_taa
1532    }
1533
1534    pub fn set_sharpen(&mut self, strength: f32) -> Result<(), String> {
1535        let strength = crisp::check_strength(strength)?;
1536        if strength != self.sharpen {
1537            self.touch();
1538        }
1539        self.sharpen = strength;
1540        Ok(())
1541    }
1542
1543    pub fn sharpen(&self) -> f32 {
1544        self.sharpen
1545    }
1546
1547    pub fn set_texture_filter(&mut self, filter: TextureFilter) -> Result<(), String> {
1548        if filter != self.frame.texture_filter() {
1549            self.touch();
1550        }
1551        self.frame.set_texture_filter(filter)
1552    }
1553
1554    pub fn texture_filter(&self) -> TextureFilter {
1555        self.frame.texture_filter()
1556    }
1557
1558    pub fn set_lens(&mut self, lens: Option<dof::Lens>) {
1559        let lens = lens.filter(|lens| lens.enabled());
1560        if lens != self.lens {
1561            self.touch();
1562        }
1563        self.lens = lens;
1564    }
1565
1566    pub fn set_haze(&mut self, haze: Option<RoomHaze>) {
1567        if haze != self.haze {
1568            self.touch();
1569        }
1570        self.haze = haze;
1571    }
1572
1573    pub fn set_exposure(&mut self, exposure: Exposure) -> Result<(), String> {
1574        if exposure != self.exposure_mode {
1575            self.touch();
1576        }
1577        match exposure {
1578            Exposure::Fixed(value) if !value.is_finite() || value <= 0.0 => {
1579                return Err("fixed exposure must be finite and positive".into());
1580            }
1581            Exposure::Auto { bias }
1582                if !bias.is_finite()
1583                    || !(f32::MIN_POSITIVE..=f32::MAX / 8.0).contains(&2.0_f32.powf(bias)) =>
1584            {
1585                return Err("exposure bias must have a finite multiplier".into());
1586            }
1587            _ => {}
1588        }
1589        if self.exposure_mode != exposure {
1590            self.exposure.exposure = 1.0;
1591            self.exposure.stale = self.exposure.pending.is_some();
1592        }
1593        self.exposure_mode = exposure;
1594        Ok(())
1595    }
1596
1597    pub fn pick(&mut self, x: u32, y: u32) -> Result<(), String> {
1598        let (width, height) = self.size();
1599        if x >= width || y >= height {
1600            return Err("pick pixel is outside the viewport".into());
1601        }
1602        self.pick_request = Some((x, y));
1603        Ok(())
1604    }
1605
1606    pub fn picked(&mut self) -> Option<Picked> {
1607        let readback = self.pick_readback.as_ref()?;
1608        let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
1609        let result = readback.receiver.try_recv();
1610        match result {
1611            Ok(Ok(())) => {
1612                let bytes = readback
1613                    .buffer
1614                    .slice(..)
1615                    .get_mapped_range()
1616                    .expect("a mapped range");
1617                let id = u32::from_le_bytes(bytes[..4].try_into().unwrap());
1618                let depth = f32::from_le_bytes(bytes[256..260].try_into().unwrap());
1619                drop(bytes);
1620                readback.buffer.unmap();
1621                let picked = Picked {
1622                    x: readback.x,
1623                    y: readback.y,
1624                    id,
1625                    depth,
1626                };
1627                self.pick_readback = None;
1628                Some(picked)
1629            }
1630            Ok(Err(_)) | Err(TryRecvError::Disconnected) => {
1631                self.pick_readback = None;
1632                None
1633            }
1634            Err(TryRecvError::Empty) => None,
1635        }
1636    }
1637
1638    pub fn text_bounds(&self) -> &[TextBounds] {
1639        &self.text_bounds
1640    }
1641
1642    pub fn text_at(&self, x: f32, y: f32) -> Option<&TextBounds> {
1643        self.text_bounds.iter().rev().find(|bounds| {
1644            let [left, top, right, bottom] = bounds.rect;
1645            (left..right).contains(&x) && (top..bottom).contains(&y)
1646        })
1647    }
1648
1649    pub fn readback_geometry(&self) -> Result<GeometryReadback, String> {
1650        let targets = &self.frame.targets;
1651        let gpu = &self.frame.gpu;
1652        let [width, height] = self.frame.viewport();
1653        let colour = readback_texture(gpu, &targets.hdr.texture, width, height, 8)?;
1654        let depth = readback_texture(gpu, &targets.depth, width, height, 4)?;
1655        let normal = readback_texture(gpu, &targets.normal_roughness, width, height, 8)?;
1656        let colour = colour
1657            .chunks_exact(8)
1658            .map(|pixel| {
1659                std::array::from_fn(|i| {
1660                    half::f16::from_le_bytes(pixel[i * 2..i * 2 + 2].try_into().unwrap()).to_f32()
1661                })
1662            })
1663            .collect();
1664        let depth = depth
1665            .chunks_exact(4)
1666            .map(|pixel| f32::from_le_bytes(pixel.try_into().unwrap()))
1667            .collect();
1668        let normal = normal
1669            .chunks_exact(8)
1670            .map(|pixel| {
1671                std::array::from_fn(|i| {
1672                    half::f16::from_le_bytes(pixel[i * 2..i * 2 + 2].try_into().unwrap()).to_f32()
1673                })
1674            })
1675            .collect();
1676        Ok(GeometryReadback {
1677            colour,
1678            depth,
1679            normals: normal,
1680        })
1681    }
1682
1683    pub fn shadows(&self) -> &Shadows {
1684        &self.shadows
1685    }
1686
1687    pub fn set_shadow_quality(&mut self, quality: Quality) {
1688        self.touch();
1689        self.shadows.update_quality(&self.frame.gpu.device, quality);
1690    }
1691
1692    pub fn upload_mesh(&mut self, data: MeshData<'_>) -> Result<MeshHandle, String> {
1693        self.touch();
1694        self.frame.upload_mesh(data)
1695    }
1696
1697    pub fn upload_skinned_mesh(
1698        &mut self,
1699        data: MeshData<'_>,
1700        skin: crate::skin::SkinData<'_>,
1701    ) -> Result<MeshHandle, String> {
1702        self.touch();
1703        self.frame.upload_skinned_mesh(data, skin)
1704    }
1705
1706    pub fn set_poses(&mut self, poses: &[crate::skin::InstancePose<'_>]) -> Result<(), String> {
1707        if self.frame.same_poses(poses) {
1708            return Ok(());
1709        }
1710        self.touch();
1711        self.frame.set_poses(poses)
1712    }
1713
1714    pub fn set_surfaces(&mut self, surfaces: &[frame::InstanceSurface]) -> Result<(), String> {
1715        if self.frame.surfaces().as_slice() == surfaces {
1716            return Ok(());
1717        }
1718        self.touch();
1719        self.frame.set_surfaces(surfaces)
1720    }
1721
1722    pub fn release_mesh(&mut self, handle: MeshHandle) -> Result<(), String> {
1723        self.touch();
1724        self.frame.release_mesh(handle)
1725    }
1726
1727    pub fn replace_mesh(&mut self, handle: MeshHandle, data: MeshData<'_>) -> Result<(), String> {
1728        self.touch();
1729        self.frame.replace_mesh(handle, data)?;
1730        self.history.invalidate();
1731        Ok(())
1732    }
1733
1734    pub fn mesh_stats(&self) -> frame::MeshStats {
1735        let glass = self.glass.mesh_stats();
1736        frame::MeshStats {
1737            glass_live: glass.live,
1738            glass_slots: glass.slots,
1739            glass_buffers: glass.buffers,
1740            ..self.frame.mesh_stats()
1741        }
1742    }
1743
1744    pub fn glass_mesh_buffers(
1745        &self,
1746        handle: glass::MeshHandle,
1747    ) -> Option<(&wgpu::Buffer, &wgpu::Buffer, u32)> {
1748        self.glass.mesh_buffers(handle)
1749    }
1750
1751    pub fn upload_glass(&mut self, mesh: &Mesh) -> Result<glass::MeshHandle, String> {
1752        self.touch();
1753        self.glass.upload_mesh(&self.frame.gpu.device, mesh)
1754    }
1755
1756    pub fn release_glass(&mut self, handle: glass::MeshHandle) -> Result<(), String> {
1757        self.touch();
1758        self.glass.release_mesh(handle)
1759    }
1760
1761    pub fn replace_glass(&mut self, handle: glass::MeshHandle, mesh: &Mesh) -> Result<(), String> {
1762        self.touch();
1763        let gpu = &self.frame.gpu;
1764        self.glass
1765            .replace_mesh(&gpu.device, &gpu.queue, handle, mesh)?;
1766        self.history.invalidate();
1767        Ok(())
1768    }
1769
1770    pub fn lod_mut(&mut self) -> &mut LodSet {
1771        self.touch();
1772        &mut self.lod
1773    }
1774
1775    pub fn lod_stats(&self) -> LodStats {
1776        self.lod.stats()
1777    }
1778
1779    pub fn set_sky(&mut self, source: SkySource, hour: f32) -> Result<(), String> {
1780        self.touch();
1781        if !hour.is_finite() {
1782            return Err("hour must be finite".into());
1783        }
1784        self.frame.set_sky(source, true);
1785        self.sky_hours.clear();
1786        self.frame.set_probe_hour(hour)?;
1787        self.hour = hour;
1788        self.history.invalidate();
1789        Ok(())
1790    }
1791
1792    pub fn request_sky(&mut self, source: SkySource, hour: f32) -> Result<u64, String> {
1793        self.touch();
1794        if !hour.is_finite() {
1795            return Err("hour must be finite".into());
1796        }
1797        let request = self.frame.set_sky(source, false);
1798        self.sky_hours.push((request, hour));
1799        Ok(request)
1800    }
1801
1802    pub fn sky_settling(&self) -> bool {
1803        self.frame.sky().settling()
1804    }
1805
1806    pub fn wait_sky(&mut self) -> Result<bool, String> {
1807        match self.frame.sky_mut().wait_settled() {
1808            Some(shown) => {
1809                self.sky_shown(shown)?;
1810                Ok(true)
1811            }
1812            None => Ok(false),
1813        }
1814    }
1815
1816    fn sky_shown(&mut self, shown: u64) -> Result<(), String> {
1817        self.touch();
1818        if let Some(&(_, hour)) = self.sky_hours.iter().find(|(request, _)| *request == shown) {
1819            self.frame.set_probe_hour(hour)?;
1820            self.hour = hour;
1821        }
1822        self.sky_hours.retain(|(request, _)| *request > shown);
1823        self.history.invalidate();
1824        Ok(())
1825    }
1826
1827    pub fn set_probes(&mut self, probes: ProbeLighting) -> Result<(), String> {
1828        self.touch();
1829        self.frame.set_probes(probes);
1830        self.frame.set_probe_hour(self.hour)?;
1831        self.frame.set_probe_emission(self.probe_emission)
1832    }
1833
1834    pub fn set_ambient(&mut self, ambient: Ambient) -> Result<(), String> {
1835        self.touch();
1836        self.frame.set_ambient(ambient)?;
1837        self.history.invalidate();
1838        Ok(())
1839    }
1840
1841    pub fn set_contact(&mut self, desc: Option<ContactDesc>) -> Result<(), String> {
1842        self.touch();
1843        self.frame.set_contact(desc)?;
1844        self.history.invalidate();
1845        Ok(())
1846    }
1847
1848    pub fn write_contact(&self, values: &[f32]) -> Result<(), String> {
1849        self.touch();
1850        self.frame.write_contact(values)
1851    }
1852
1853    pub fn set_reflection_occlusion(&mut self, threshold: f32) -> Result<(), String> {
1854        self.touch();
1855        self.frame.set_reflection_occlusion(threshold)
1856    }
1857
1858    pub fn set_sky_visibility(&mut self, on: bool) {
1859        self.touch();
1860        self.frame.set_sky_visibility(on);
1861    }
1862
1863    pub fn set_probe_emission(&mut self, scale: f32) -> Result<(), String> {
1864        self.touch();
1865        self.frame.set_probe_emission(scale)?;
1866        self.probe_emission = scale;
1867        Ok(())
1868    }
1869
1870    pub fn set_lights(&mut self, lights: &[LocalLight]) -> Result<(), String> {
1871        self.touch();
1872        self.frame.set_lights(lights)
1873    }
1874
1875    pub fn set_light_quality(&mut self, quality: LightQuality) -> Result<(), String> {
1876        self.touch();
1877        self.frame.set_light_quality(quality)
1878    }
1879
1880    pub fn set_transmission(&mut self, tints: &[Option<[f32; 3]>]) -> Result<(), String> {
1881        self.touch();
1882        self.frame.set_transmission(tints)
1883    }
1884
1885    pub fn set_maps(&mut self, images: &MapImages<'_>) -> Result<(), String> {
1886        self.touch();
1887        self.frame.set_maps(images)
1888    }
1889
1890    pub fn set_detail(&mut self, atlas: &pfx_bake::detail::atlas::Atlas) -> Result<(), String> {
1891        self.touch();
1892        self.frame.set_detail(atlas)
1893    }
1894
1895    pub fn set_plates(
1896        &mut self,
1897        plate: Option<&pfx_bake::plate::Plate>,
1898        settings: crate::plates::PlateSettings,
1899    ) -> Result<(), String> {
1900        let plates = match plate {
1901            Some(plate) => {
1902                let kit = self.plate_kit();
1903                Some(crate::plates::Plates::with_kit(
1904                    &self.frame.gpu,
1905                    &kit,
1906                    plate,
1907                    settings,
1908                )?)
1909            }
1910            None => None,
1911        };
1912        self.swap_plates(plates);
1913        Ok(())
1914    }
1915
1916    pub fn plate_kit(&mut self) -> crate::plates::PlateKit {
1917        let gpu = &self.frame.gpu;
1918        let cache = self.frame.pipeline_cache();
1919        self.plate_kit
1920            .get_or_insert_with(|| crate::plates::PlateKit::new(gpu, cache))
1921            .clone()
1922    }
1923
1924    pub fn swap_plates(
1925        &mut self,
1926        plates: Option<crate::plates::Plates>,
1927    ) -> Option<crate::plates::Plates> {
1928        self.touch();
1929        let plates = plates.map(|mut plates| {
1930            plates.set_hour(self.hour);
1931            plates
1932        });
1933        let old = std::mem::replace(&mut self.plates, plates);
1934        self.taa.invalidate();
1935        old
1936    }
1937
1938    pub fn plates(&self) -> Option<&crate::plates::Plates> {
1939        self.plates.as_ref()
1940    }
1941
1942    pub fn set_plate_settings(&mut self, settings: crate::plates::PlateSettings) {
1943        if let Some(plates) = &mut self.plates
1944            && plates.settings() != settings
1945        {
1946            plates.set_settings(settings);
1947            self.touch();
1948        }
1949    }
1950
1951    pub fn set_plate_hour(&mut self, hour: f32) {
1952        if let Some(plates) = &mut self.plates
1953            && plates.hour().to_bits() != hour.to_bits()
1954        {
1955            plates.set_hour(hour);
1956            self.touch();
1957        }
1958    }
1959
1960    pub fn set_plate_casters(&mut self, casters: &[Instance]) {
1961        self.touch();
1962        self.plate_casters = casters
1963            .iter()
1964            .map(|caster| Instance {
1965                shadow_only: true,
1966                previous_model: caster.model,
1967                ..*caster
1968            })
1969            .collect();
1970    }
1971
1972    pub fn plate_casters(&self) -> &[Instance] {
1973        &self.plate_casters
1974    }
1975
1976    pub fn set_props(&mut self, props: Option<ImpostorPass>) {
1977        self.touch();
1978        self.impostors = props;
1979    }
1980
1981    pub fn set_tree(
1982        &mut self,
1983        tree: &Tree,
1984        origin: [f32; 3],
1985        receiver_y: f32,
1986        id: u32,
1987        petal_rate: f32,
1988        petal_color: [f32; 3],
1989    ) -> Result<MeshHandle, String> {
1990        self.touch();
1991        let handle = self.frame.set_tree(tree, origin, receiver_y, id)?;
1992        self.petals = Some(Petals::new(tree, origin, petal_rate, petal_color));
1993        Ok(handle)
1994    }
1995
1996    pub fn set_canopy_mode(&mut self, mode: CanopyMode) -> Result<(), String> {
1997        self.touch();
1998        self.frame.set_canopy_mode(mode)
1999    }
2000
2001    pub fn set_canopy_sharpness(&mut self, sharpness: f32) -> Result<(), String> {
2002        self.touch();
2003        self.frame.set_canopy_sharpness(sharpness)
2004    }
2005
2006    pub fn set_canopy_strength(&mut self, strength: f32) -> Result<(), String> {
2007        self.touch();
2008        self.frame.set_canopy_strength(strength)
2009    }
2010
2011    pub fn set_canopy_gobo(&mut self, gobo: Gobo) -> Result<(), String> {
2012        self.touch();
2013        self.frame.set_canopy_gobo(gobo)?;
2014        self.text.set_canopy_gobo(gobo);
2015        Ok(())
2016    }
2017
2018    pub fn volume_cuts(&self) -> effects::VolumeCuts {
2019        self.effects.volume_cuts()
2020    }
2021
2022    pub fn set_volume_cuts(&mut self, cuts: effects::VolumeCuts) {
2023        self.touch();
2024        let device = self.frame.gpu.device.clone();
2025        self.effects.set_volume_cuts(&device, cuts);
2026    }
2027
2028    pub fn resize(&mut self, width: u32, height: u32) -> Result<(), String> {
2029        self.touch();
2030        self.frame.resize(width, height)?;
2031        let owned_device = self.frame.gpu.device.clone();
2032        let device = &owned_device;
2033        self.taa.resize(device, width, height);
2034        self.crisp.resize(width, height);
2035        let cuts = self.effects.volume_cuts();
2036        self.effects = effects::Effects::new(device, width, height, 4)?;
2037        self.effects.set_volume_cuts(device, cuts);
2038        self.post.resize(width, height);
2039        self.dof.resize(width, height);
2040        self.reflections.resize(device, width, height);
2041        let meshes = self.glass.take_meshes();
2042        self.glass = glass::Pass::new(device, width, height);
2043        self.glass.set_meshes(meshes);
2044        if self.display.is_some() {
2045            self.display = Some(image(
2046                device,
2047                width,
2048                height,
2049                wgpu::TextureFormat::Rgba16Float,
2050                wgpu::TextureUsages::STORAGE_BINDING
2051                    | wgpu::TextureUsages::RENDER_ATTACHMENT
2052                    | wgpu::TextureUsages::TEXTURE_BINDING,
2053                "live display intermediate",
2054            ));
2055        }
2056        self.depths = linear_depths(device, width, height);
2057        self.mips = mips(device, width, height);
2058        (self.reactive, self.reactive_view) = image(
2059            device,
2060            width,
2061            height,
2062            wgpu::TextureFormat::R8Unorm,
2063            wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::TEXTURE_BINDING,
2064            "live reactive mask",
2065        );
2066        if let Some(limits) = self.frames_on_demand() {
2067            self.demand = Some(demand::Cache::new(device, [width, height], limits));
2068        }
2069        self.history.invalidate();
2070        self.history_valid = false;
2071        self.pass_log.clear();
2072        self.text_bounds.clear();
2073        self.pick_request = None;
2074        self.pick_readback = None;
2075        self.set_viewport_parts(width, height)
2076    }
2077
2078    pub fn render(
2079        &mut self,
2080        scene: &Scene<'_>,
2081        text: &Text<'_>,
2082        effects: &Effects<'_>,
2083        style: Finish,
2084        output: &wgpu::TextureView,
2085    ) -> Result<Vec<PassTiming>, String> {
2086        let frame = self.encode_frame(scene, text, effects, style, None, output, false)?;
2087        self.wait_for(frame)
2088    }
2089
2090    pub fn render_with_flat(
2091        &mut self,
2092        scene: &Scene<'_>,
2093        text: &Text<'_>,
2094        effects: &Effects<'_>,
2095        style: Finish,
2096        flat: &FlatScene<'_>,
2097        output: &wgpu::TextureView,
2098    ) -> Result<Vec<PassTiming>, String> {
2099        let frame = self.encode_frame(scene, text, effects, style, Some(flat), output, false)?;
2100        self.wait_for(frame)
2101    }
2102
2103    pub fn render_flat(
2104        &mut self,
2105        flat: &FlatScene<'_>,
2106        output: &wgpu::TextureView,
2107    ) -> Result<Vec<PassTiming>, String> {
2108        let frame = self.encode_flat_frame(flat, None, output)?;
2109        self.wait_for(frame)
2110    }
2111
2112    pub fn render_flat_look(
2113        &mut self,
2114        flat: &FlatScene<'_>,
2115        look: &LookFrame<'_>,
2116        output: &wgpu::TextureView,
2117    ) -> Result<Vec<PassTiming>, String> {
2118        let frame = self.encode_flat_frame(flat, Some(look), output)?;
2119        self.wait_for(frame)
2120    }
2121
2122    fn wait_for(&mut self, frame: u64) -> Result<Vec<PassTiming>, String> {
2123        self.frame
2124            .gpu
2125            .device
2126            .poll(wgpu::PollType::wait_indefinitely())
2127            .map_err(|error| format!("{error:?}"))?;
2128        self.gather_timings(self.frames_submitted.saturating_sub(1));
2129        Ok(std::mem::take(&mut self.arrivals)
2130            .into_iter()
2131            .find(|timings| timings.frame == frame)
2132            .map(|timings| timings.passes)
2133            .unwrap_or_default())
2134    }
2135
2136    pub fn submit(
2137        &mut self,
2138        scene: &Scene<'_>,
2139        text: &Text<'_>,
2140        effects: &Effects<'_>,
2141        style: Finish,
2142        output: &wgpu::TextureView,
2143    ) -> Result<Submitted, String> {
2144        let frame = self.encode_frame(scene, text, effects, style, None, output, false)?;
2145        Ok(self.submitted_frame(frame))
2146    }
2147
2148    pub fn submit_with_flat(
2149        &mut self,
2150        scene: &Scene<'_>,
2151        text: &Text<'_>,
2152        effects: &Effects<'_>,
2153        style: Finish,
2154        flat: &FlatScene<'_>,
2155        output: &wgpu::TextureView,
2156    ) -> Result<Submitted, String> {
2157        let frame = self.encode_frame(scene, text, effects, style, Some(flat), output, false)?;
2158        Ok(self.submitted_frame(frame))
2159    }
2160
2161    pub fn submit_flat(
2162        &mut self,
2163        flat: &FlatScene<'_>,
2164        output: &wgpu::TextureView,
2165    ) -> Result<Submitted, String> {
2166        let frame = self.encode_flat_frame(flat, None, output)?;
2167        Ok(self.submitted_frame(frame))
2168    }
2169
2170    pub fn submit_flat_look(
2171        &mut self,
2172        flat: &FlatScene<'_>,
2173        look: &LookFrame<'_>,
2174        output: &wgpu::TextureView,
2175    ) -> Result<Submitted, String> {
2176        let frame = self.encode_flat_frame(flat, Some(look), output)?;
2177        Ok(self.submitted_frame(frame))
2178    }
2179
2180    pub fn flat_looks(&self) -> Option<&FlatLooks> {
2181        self.flat_looks.as_ref()
2182    }
2183
2184    fn submitted_frame(&mut self, frame: u64) -> Submitted {
2185        let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
2186        self.gather_timings(self.frames_submitted.saturating_sub(1));
2187        Submitted {
2188            frame,
2189            arrived: std::mem::take(&mut self.arrivals),
2190        }
2191    }
2192
2193    pub fn flat_pass(&self) -> Option<&FlatPass> {
2194        self.flat.as_ref()
2195    }
2196
2197    pub fn warm_flat<'a>(
2198        &mut self,
2199        format: wgpu::TextureFormat,
2200        size: [u32; 2],
2201        looks: impl IntoIterator<Item = &'a Look>,
2202    ) -> Result<(), String> {
2203        if size[0] == 0 || size[1] == 0 {
2204            return Err("warm_flat needs a size of at least 1 by 1".into());
2205        }
2206        let device = self.frame.gpu.device.clone();
2207        let queue = self.frame.gpu.queue.clone();
2208        let hdr = wgpu::TextureFormat::Rgba16Float;
2209        let finishes = [
2210            (format, true, false),
2211            (hdr, true, false),
2212            (hdr, self.display.is_some(), true),
2213        ];
2214        let mut flat = self
2215            .flat
2216            .take()
2217            .unwrap_or_else(|| FlatPass::new(&device, &queue));
2218        flat.warm(&device, size, &finishes, true);
2219        self.flat = Some(flat);
2220        let mut set = self
2221            .flat_looks
2222            .take()
2223            .unwrap_or_else(|| FlatLooks::new(&device, &queue));
2224        let warmed = set.warm(&device, &queue, looks, size);
2225        self.flat_looks = Some(set);
2226        warmed
2227    }
2228
2229    pub fn flat_pipelines_made_on_render_thread(&self) -> usize {
2230        self.flat.as_ref().map_or(0, FlatPass::pipelines_made)
2231            + self
2232                .flat_looks
2233                .as_ref()
2234                .map_or(0, FlatLooks::pipelines_made)
2235    }
2236
2237    fn encode_flat_frame(
2238        &mut self,
2239        scene: &FlatScene<'_>,
2240        look: Option<&LookFrame<'_>>,
2241        output: &wgpu::TextureView,
2242    ) -> Result<u64, String> {
2243        let look = look.filter(|look| !look.is_identity());
2244        if look.is_some() && self.size() != self.capacity() {
2245            return Err("a flat look renders at the full size; set the viewport to the renderer's capacity first".into());
2246        }
2247        self.begin_frame(FrameKind::Full);
2248        self.pass_log.clear();
2249        self.text_bounds.clear();
2250        let (width, height) = self.size();
2251        let (capacity_width, capacity_height) = self.capacity();
2252        let owned_device = self.frame.gpu.device.clone();
2253        let device = &owned_device;
2254        let queue = self.frame.gpu.queue.clone();
2255        if let Some(tape) = self.tape {
2256            self.post.set_tape(tape);
2257        }
2258        let mut flat = self
2259            .flat
2260            .take()
2261            .unwrap_or_else(|| FlatPass::new(device, &queue));
2262        flat.set_capacity(Some([capacity_width, capacity_height]));
2263        if look.is_none() {
2264            let prepared = flat.prepare(device, &queue, scene, [width, height]);
2265            if let Err(error) = prepared {
2266                self.flat = Some(flat);
2267                return Err(error);
2268            }
2269        }
2270        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2271            label: Some("live flat"),
2272        });
2273        let clear = Some(
2274            scene
2275                .clear
2276                .map_or([0.0; 4], |colour| colour.premultiplied()),
2277        );
2278        let direct = !scene.post && self.display.is_none();
2279        let target = if direct {
2280            output.clone()
2281        } else {
2282            flat.colour_target(device, [capacity_width, capacity_height])
2283        };
2284        if let Some(look) = look {
2285            let mut looks = self
2286                .flat_looks
2287                .take()
2288                .unwrap_or_else(|| FlatLooks::new(device, &queue));
2289            let encoded = looks.encode(
2290                device,
2291                &queue,
2292                &mut encoder,
2293                &mut flat,
2294                scene,
2295                look,
2296                [width, height],
2297                &target,
2298                Some(&mut self.profiler),
2299            );
2300            self.flat_looks = Some(looks);
2301            if let Err(error) = encoded {
2302                self.flat = Some(flat);
2303                return Err(error);
2304            }
2305            self.pass_log.push("flat look");
2306        } else {
2307            flat.encode_colour(&mut encoder, &target, clear, Some(&mut self.profiler));
2308            self.pass_log.push("flat");
2309        }
2310        if !direct {
2311            if scene.post {
2312                let hdr = &self.frame.targets.hdr.view;
2313                flat.encode_finish(
2314                    device,
2315                    &mut encoder,
2316                    &target,
2317                    hdr,
2318                    wgpu::TextureFormat::Rgba16Float,
2319                    true,
2320                    false,
2321                    Some(&mut self.profiler),
2322                );
2323                self.pass_log.push("flat finish");
2324                let post_output = self.display.as_ref().map_or(output, |(_, view)| view);
2325                self.post.run_timed_exposed(
2326                    &mut encoder,
2327                    hdr,
2328                    None,
2329                    None,
2330                    post_output,
2331                    scene.frame,
2332                    scene.seed,
2333                    1.0,
2334                    Some(&mut self.profiler),
2335                );
2336                self.pass_log.push("post");
2337                if self.display.is_some() {
2338                    let display = self.display.as_ref().map(|(_, view)| view.clone());
2339                    if let Some(display) = display {
2340                        self.encode_blit(device, &mut encoder, &display, output);
2341                    }
2342                }
2343            } else {
2344                flat.encode_finish(
2345                    device,
2346                    &mut encoder,
2347                    &target,
2348                    output,
2349                    self.output_format,
2350                    true,
2351                    false,
2352                    Some(&mut self.profiler),
2353                );
2354                self.pass_log.push("flat finish");
2355            }
2356        }
2357        flat.encode_ids(
2358            &mut encoder,
2359            &self.frame.targets.ids_view,
2360            true,
2361            Some(&mut self.profiler),
2362        );
2363        self.pass_log.push("flat ids");
2364        self.flat = Some(flat);
2365        self.finish_encoder(device, &queue, encoder, false)
2366    }
2367
2368    pub fn frame_stats(&self) -> FrameStats {
2369        self.stats.clone()
2370    }
2371
2372    pub fn set_frame_stats(&mut self, stats: FrameStats) {
2373        self.stats = stats;
2374    }
2375
2376    fn begin_frame(&mut self, kind: FrameKind) {
2377        self.stats.begin(kind);
2378        self.resolution_frame = kind == FrameKind::Full;
2379    }
2380
2381    pub fn set_dynamic_resolution(&mut self, budget: Option<Budget>) -> Result<(), String> {
2382        self.resolution = budget.map(DynamicResolution::new).transpose()?;
2383        self.resolution_content = None;
2384        Ok(())
2385    }
2386
2387    pub fn dynamic_resolution(&self) -> Option<&DynamicResolution> {
2388        self.resolution.as_ref()
2389    }
2390
2391    pub fn apply_dynamic_resolution(&mut self, content: [u32; 2]) -> Result<(u32, u32), String> {
2392        let Some(resolution) = self.resolution.as_ref() else {
2393            return Ok(self.size());
2394        };
2395        let [width, height] = resolution
2396            .render_size(content)
2397            .ok_or("the content size must be at least 1 by 1")?;
2398        let [capacity_width, capacity_height] = resolution
2399            .capacity(content)
2400            .ok_or("the content size must be at least 1 by 1")?;
2401        self.resolution_content = Some(content);
2402        if self.capacity() != (capacity_width, capacity_height) {
2403            self.resize(capacity_width, capacity_height)?;
2404        }
2405        let (width, height) = (width.min(capacity_width), height.min(capacity_height));
2406        self.set_viewport(width, height)?;
2407        Ok((width, height))
2408    }
2409
2410    pub fn timings(&self) -> Option<&FrameTimings> {
2411        self.latest_timings.as_ref()
2412    }
2413
2414    pub fn timing_drops(&self) -> TimingDrops {
2415        TimingDrops {
2416            ring: self.profiler.dropped(),
2417            late: self.late_timings,
2418        }
2419    }
2420
2421    pub fn frames_submitted(&self) -> u64 {
2422        self.frames_submitted
2423    }
2424
2425    pub fn frames_in_flight(&self) -> u64 {
2426        self.frames_submitted
2427            .saturating_sub(self.frames_done.load(Ordering::Acquire))
2428    }
2429
2430    pub fn wait_frames(&mut self, in_flight: u64) -> Result<Vec<FrameTimings>, String> {
2431        let done = self.frames_done.load(Ordering::Acquire);
2432        while self
2433            .submissions
2434            .front()
2435            .is_some_and(|(frame, _)| *frame < done)
2436        {
2437            self.submissions.pop_front();
2438        }
2439        let waiting = self.submissions.len() as u64;
2440        if waiting > in_flight {
2441            let index = self.submissions[(waiting - in_flight - 1) as usize]
2442                .1
2443                .clone();
2444            self.frame
2445                .gpu
2446                .device
2447                .poll(wgpu::PollType::Wait {
2448                    submission_index: Some(index),
2449                    timeout: None,
2450                })
2451                .map_err(|error| format!("{error:?}"))?;
2452        } else {
2453            let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
2454        }
2455        self.gather_timings(self.frames_submitted.saturating_sub(1));
2456        Ok(std::mem::take(&mut self.arrivals))
2457    }
2458
2459    fn gather_timings(&mut self, current: u64) {
2460        for timings in self.profiler.gather() {
2461            if self.stats.enabled() || self.resolution.is_some() {
2462                let total = timings.passes.iter().map(|pass| pass.milliseconds).sum();
2463                self.stats.gpu(timings.frame, total);
2464                if let Some(resolution) = self.resolution.as_mut() {
2465                    resolution.observe(timings.frame, total);
2466                }
2467            }
2468            if current.saturating_sub(timings.frame) > TIMING_LAG {
2469                self.late_timings += 1;
2470                continue;
2471            }
2472            let timings = FrameTimings {
2473                frame: timings.frame,
2474                passes: plain_passes(timings.passes),
2475            };
2476            if self
2477                .latest_timings
2478                .as_ref()
2479                .is_none_or(|latest| latest.frame < timings.frame)
2480            {
2481                self.latest_timings = Some(timings.clone());
2482            }
2483            self.arrivals.push(timings);
2484        }
2485    }
2486
2487    #[allow(clippy::too_many_arguments)]
2488    fn encode_frame(
2489        &mut self,
2490        scene: &Scene<'_>,
2491        text: &Text<'_>,
2492        effects: &Effects<'_>,
2493        style: Finish,
2494        hud: Option<&FlatScene<'_>>,
2495        output: &wgpu::TextureView,
2496        cached: bool,
2497    ) -> Result<u64, String> {
2498        if !cached {
2499            self.begin_frame(FrameKind::Full);
2500        }
2501        self.pass_log.clear();
2502        let (width, height) = self.size();
2503        self.text.set_viewport(Some([width, height]));
2504        self.text_bounds = text_bounds(text, [width, height]);
2505        if !scene.time.is_finite() || scene.time < 0.0 {
2506            return Err("scene time must be finite and nonnegative".into());
2507        }
2508        if let Some(shown) = self.frame.sky_mut().swap_settled() {
2509            self.sky_shown(shown)?;
2510        }
2511        let frame_number = (scene.time * 60.0).round() as u32;
2512        let view_projection = frame::multiply(scene.camera.projection, scene.camera.view);
2513        let mut camera = scene.camera;
2514        if cached {
2515            if let Some(last) = self.history.last_view_projection() {
2516                camera.previous_view_projection = last;
2517            }
2518            self.history.bridge(scene.time);
2519        }
2520        let valid =
2521            self.history
2522                .advance(scene.time, camera.previous_view_projection, view_projection);
2523        self.history_valid = valid;
2524        if !valid {
2525            self.taa.invalidate();
2526        }
2527        let metered = if matches!(self.exposure_mode, Exposure::Auto { .. }) {
2528            self.exposure.advance(scene.time, valid)
2529        } else {
2530            1.0
2531        };
2532        let exposure = self.exposure_mode.value(metered);
2533        let owned_device = self.frame.gpu.device.clone();
2534        let device = &owned_device;
2535        let queue = self.frame.gpu.queue.clone();
2536        if style != self.style {
2537            if !self.custom_finish {
2538                self.post = self.post_chain(chain(style, self.display.is_none()))?;
2539            }
2540            self.style = style;
2541        }
2542        if let Some(tape) = self.tape {
2543            self.post.set_tape(tape);
2544        }
2545        let mut lod_instances = Vec::new();
2546        if self.lod.instance_mut(0).is_some() {
2547            self.lod.select(scene.camera, height);
2548            let mut lod = std::mem::replace(&mut self.lod, LodSet::new(0));
2549            let uploaded = lod.upload_selected(&mut self.frame);
2550            self.lod = lod;
2551            lod_instances = uploaded?;
2552            self.pass_log.push("lod");
2553        }
2554        let dynamic = scene.instances;
2555        let stand_ins: &[Instance] = if self.plates.is_some() {
2556            &self.plate_casters
2557        } else {
2558            &[]
2559        };
2560        let combined: Vec<Instance>;
2561        let instances = if lod_instances.is_empty() && stand_ins.is_empty() {
2562            scene.instances
2563        } else {
2564            combined = lod_instances
2565                .into_iter()
2566                .chain(scene.instances.iter().copied())
2567                .chain(stand_ins.iter().copied())
2568                .collect();
2569            &combined
2570        };
2571        let scene = Scene {
2572            camera,
2573            time: scene.time,
2574            seed: scene.seed,
2575            sun: scene.sun,
2576            instances,
2577            materials: scene.materials,
2578            deformers: scene.deformers,
2579            wind: scene.wind,
2580        };
2581        let jitter = taa::jitter_clip(frame_number, width, height);
2582        self.frame.set_jitter(jitter);
2583        let projection = frame::multiply(frame::jitter_matrix(jitter), scene.camera.projection);
2584        let jittered = frame::multiply(projection, scene.camera.view);
2585        let view = camera_view(&scene, width, height);
2586        let right = [
2587            scene.camera.view[0][0],
2588            scene.camera.view[1][0],
2589            scene.camera.view[2][0],
2590        ];
2591        let lens = [
2592            1.0 / scene.camera.projection[0][0],
2593            1.0 / scene.camera.projection[1][1],
2594        ];
2595        let shift = [scene.camera.projection[2][0], scene.camera.projection[2][1]];
2596        let sky_forward = if shift == [0.0; 2] {
2597            view.forward
2598        } else {
2599            std::array::from_fn(|k| {
2600                view.forward[k] + right[k] * lens[0] * shift[0] + view.up[k] * lens[1] * shift[1]
2601            })
2602        };
2603        self.frame.sky_mut().update(
2604            &queue,
2605            sky_forward,
2606            right.map(|value| value * lens[0]),
2607            view.up.map(|value| value * lens[1]),
2608        );
2609        self.frame
2610            .set_reflections(self.reflections.result().filter(|_| valid));
2611        let fit = self.shadows.fit(&view, scene.sun.direction);
2612        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2613            label: Some("live renderer"),
2614        });
2615        let shadow_props = self
2616            .impostors
2617            .as_ref()
2618            .filter(|_| !effects.props.is_empty());
2619        let cascades: Vec<Matrix> = fit
2620            .cascades
2621            .iter()
2622            .map(|cascade| cascade.view_proj.columns)
2623            .collect();
2624        let (fluid, ripple) = effects
2625            .liquid
2626            .as_ref()
2627            .map_or((&self.black, &self.black), |liquid| {
2628                (liquid.fluid, liquid.ripple)
2629            });
2630        let faces = self.frame.lights().faces();
2631        let light_faces = faces.len();
2632        let cast_layers: Vec<Matrix> = cascades
2633            .iter()
2634            .copied()
2635            .chain(faces.into_iter().map(|(_, matrix)| matrix))
2636            .collect();
2637        let glass_casting =
2638            self.glass
2639                .prepare_casts(device, &queue, &cast_layers, effects.glass, fluid, ripple)?;
2640        let glass = &self.glass;
2641        let mut glass_casts = |pass: &mut wgpu::RenderPass<'_>, layer: usize, _: bool| {
2642            glass.draw_casts(pass, layer);
2643        };
2644        self.frame.encode_with_casts(
2645            &scene,
2646            &mut encoder,
2647            Some((&mut self.shadows, &fit)),
2648            Some(&mut self.profiler),
2649            if glass_casting {
2650                Some(&mut glass_casts)
2651            } else {
2652                None
2653            },
2654            |gpu, encoder, shadows, fit| {
2655                if let Some(props) = shadow_props {
2656                    props.update_shadows(gpu, fit, self.hour, effects.props)?;
2657                    shadows.render_impostors(encoder, props, effects.props.len())?;
2658                }
2659                if let Some(mesh) = effects.creature_mesh
2660                    && !effects.creatures.is_empty()
2661                {
2662                    shadows.render_creatures(
2663                        gpu,
2664                        encoder,
2665                        fit,
2666                        mesh,
2667                        effects.creatures,
2668                        scene.time,
2669                    );
2670                }
2671                Ok(())
2672            },
2673        )?;
2674        if self.frame.has_tree() {
2675            self.pass_log.push("canopy cookie");
2676        }
2677        self.pass_log.push("shadows");
2678        if self.frame.lights().any_transmissive() || glass_casting {
2679            self.pass_log.push("shadow transmission");
2680        }
2681        if glass_casting && light_faces > 0 {
2682            self.pass_log.push("light transmission");
2683        }
2684        if shadow_props.is_some() {
2685            self.pass_log.push("impostor shadows");
2686        }
2687        if effects.creature_mesh.is_some() && !effects.creatures.is_empty() {
2688            self.pass_log.push("creature shadows");
2689        }
2690        if !self.frame.lights().shadowed().is_empty() {
2691            self.pass_log.push("light shadows");
2692        }
2693        self.pass_log.extend(["prepass", "opaque"]);
2694        if let Some(props) = &self.impostors
2695            && !effects.props.is_empty()
2696        {
2697            props.update(
2698                &self.frame.gpu,
2699                ImpostorParams {
2700                    view_projection: jittered,
2701                    camera_position: [
2702                        scene.camera.position[0],
2703                        scene.camera.position[1],
2704                        scene.camera.position[2],
2705                        0.0,
2706                    ],
2707                    camera_right: [right[0], right[1], right[2], 0.0],
2708                    camera_up: [view.up[0], view.up[1], view.up[2], 0.0],
2709                    settings: [0.0; 4],
2710                    source: [0.0; 4],
2711                },
2712                self.hour,
2713                effects.props,
2714            )?;
2715            let timing = self.profiler.pass("impostors");
2716            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2717                multiview_mask: None,
2718                label: Some("impostors"),
2719                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2720                    view: &self.frame.targets.hdr.view,
2721                    resolve_target: None,
2722                    depth_slice: None,
2723                    ops: wgpu::Operations {
2724                        load: wgpu::LoadOp::Load,
2725                        store: wgpu::StoreOp::Store,
2726                    },
2727                })],
2728                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2729                    view: &self.frame.targets.depth_view,
2730                    depth_ops: Some(wgpu::Operations {
2731                        load: wgpu::LoadOp::Load,
2732                        store: wgpu::StoreOp::Store,
2733                    }),
2734                    stencil_ops: None,
2735                }),
2736                timestamp_writes: self.profiler.render_writes(timing),
2737                occlusion_query_set: None,
2738            });
2739            crate::viewport::apply(&mut pass, [width, height]);
2740            props.draw(&mut pass, effects.props.len())?;
2741            drop(pass);
2742            self.pass_log.push("impostors");
2743        }
2744        if let Some(plates) = self.plates.as_mut() {
2745            let shadowed = plates.encode(
2746                &self.frame,
2747                (&self.shadows, &fit),
2748                &self.reactive_view,
2749                &mut encoder,
2750                &crate::plates::PlateFrame {
2751                    view_projection,
2752                    jittered,
2753                    previous: scene.camera.previous_view_projection,
2754                    view: scene.camera.view,
2755                    eye: scene.camera.position,
2756                    toward_sun: scene.sun.direction,
2757                    sun_light: scene
2758                        .sun
2759                        .colour
2760                        .map(|channel| channel * scene.sun.intensity),
2761                    sun_up: scene.sun.intensity > 0.0,
2762                    instances: dynamic,
2763                },
2764                Some(&mut self.profiler),
2765            )?;
2766            if shadowed {
2767                self.pass_log.push("plate shadows");
2768            }
2769            self.pass_log.push("plate composite");
2770            self.plate_mask = true;
2771        } else if self.plate_mask {
2772            encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2773                multiview_mask: None,
2774                label: Some("plate mask clear"),
2775                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2776                    view: &self.reactive_view,
2777                    resolve_target: None,
2778                    depth_slice: None,
2779                    ops: wgpu::Operations {
2780                        load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
2781                        store: wgpu::StoreOp::Store,
2782                    },
2783                })],
2784                depth_stencil_attachment: None,
2785                timestamp_writes: None,
2786                occlusion_query_set: None,
2787            });
2788            self.plate_mask = false;
2789        }
2790        self.depth_index = 1 - self.depth_index;
2791        let current_depth = &self.depths[self.depth_index].1;
2792        let depth_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
2793            label: Some("live depth limits"),
2794            contents: bytemuck::cast_slice(&[
2795                scene.camera.projection[2][2],
2796                scene.camera.projection[3][2],
2797                view.far,
2798                0.0,
2799            ]),
2800            usage: wgpu::BufferUsages::UNIFORM,
2801        });
2802        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2803            label: Some("live linear depth"),
2804            layout: &self.depth_layout,
2805            entries: &[
2806                wgpu::BindGroupEntry {
2807                    binding: 0,
2808                    resource: wgpu::BindingResource::TextureView(&self.frame.targets.depth_view),
2809                },
2810                wgpu::BindGroupEntry {
2811                    binding: 1,
2812                    resource: wgpu::BindingResource::TextureView(current_depth),
2813                },
2814                wgpu::BindGroupEntry {
2815                    binding: 2,
2816                    resource: depth_uniform.as_entire_binding(),
2817                },
2818            ],
2819        });
2820        let timing = self.profiler.pass("linear depth");
2821        {
2822            let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2823                label: Some("linear depth"),
2824                timestamp_writes: self.profiler.compute_writes(timing),
2825            });
2826            pass.set_pipeline(&self.depth_pipeline);
2827            pass.set_bind_group(0, &bind, &[]);
2828            pass.dispatch_workgroups(width.div_ceil(8), height.div_ceil(8), 1);
2829        }
2830        self.pass_log.push("linear depth");
2831        let inverse = invert(jittered).ok_or("camera projection is singular")?;
2832        let mut motion = Vec::with_capacity(52);
2833        for matrix in [
2834            inverse,
2835            view_projection,
2836            scene.camera.previous_view_projection,
2837        ] {
2838            motion.extend(matrix.iter().flatten().copied());
2839        }
2840        motion.extend([
2841            width as f32,
2842            height as f32,
2843            1.0 / width as f32,
2844            1.0 / height as f32,
2845        ]);
2846        queue.write_buffer(&self.motion.uniform, 0, bytemuck::cast_slice(&motion));
2847        let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
2848            label: Some("live background motion"),
2849            layout: &self.motion.layout,
2850            entries: &[
2851                wgpu::BindGroupEntry {
2852                    binding: 0,
2853                    resource: wgpu::BindingResource::TextureView(&self.frame.targets.depth_view),
2854                },
2855                wgpu::BindGroupEntry {
2856                    binding: 1,
2857                    resource: wgpu::BindingResource::TextureView(&self.frame.targets.ids_view),
2858                },
2859                wgpu::BindGroupEntry {
2860                    binding: 2,
2861                    resource: self.motion.uniform.as_entire_binding(),
2862                },
2863            ],
2864        });
2865        let timing = self.profiler.pass("background motion");
2866        {
2867            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2868                multiview_mask: None,
2869                label: Some("background motion"),
2870                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2871                    view: &self.frame.targets.velocity_view,
2872                    resolve_target: None,
2873                    depth_slice: None,
2874                    ops: wgpu::Operations {
2875                        load: wgpu::LoadOp::Load,
2876                        store: wgpu::StoreOp::Store,
2877                    },
2878                })],
2879                depth_stencil_attachment: None,
2880                timestamp_writes: self.profiler.render_writes(timing),
2881                occlusion_query_set: None,
2882            });
2883            crate::viewport::apply(&mut pass, [width, height]);
2884            pass.set_pipeline(&self.motion.pipeline);
2885            pass.set_bind_group(0, &bind, &[]);
2886            pass.draw(0..3, 0..1);
2887        }
2888        self.pass_log.push("background motion");
2889        let timing = self.profiler.pass("sky");
2890        {
2891            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2892                multiview_mask: None,
2893                label: Some("sky"),
2894                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2895                    view: &self.frame.targets.hdr.view,
2896                    resolve_target: None,
2897                    depth_slice: None,
2898                    ops: wgpu::Operations {
2899                        load: wgpu::LoadOp::Load,
2900                        store: wgpu::StoreOp::Store,
2901                    },
2902                })],
2903                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
2904                    view: &self.frame.targets.depth_view,
2905                    depth_ops: Some(wgpu::Operations {
2906                        load: wgpu::LoadOp::Load,
2907                        store: wgpu::StoreOp::Store,
2908                    }),
2909                    stencil_ops: None,
2910                }),
2911                timestamp_writes: self.profiler.render_writes(timing),
2912                occlusion_query_set: None,
2913            });
2914            crate::viewport::apply(&mut pass, [width, height]);
2915            self.frame.sky().draw(&mut pass);
2916        }
2917        self.pass_log.push("sky");
2918        self.mips.copy_scene(
2919            &mut encoder,
2920            &self.frame.targets.hdr.texture,
2921            [width, height],
2922        );
2923        let timing = self.profiler.pass("scene colour mips");
2924        let fresh = self.mips.written != Some([width, height]);
2925        self.mips.written = Some([width, height]);
2926        for level in 1..self.mips.levels.len() {
2927            let rect = [width, height].map(|size| crate::viewport::mip(size, level as u32));
2928            if fresh {
2929                queue.write_buffer(
2930                    &self.mips.extents[level],
2931                    0,
2932                    bytemuck::cast_slice(&[rect[0], rect[1], 0, 0]),
2933                );
2934            }
2935            let last = level + 1 == self.mips.levels.len();
2936            let writes = if level == 1 && last {
2937                self.profiler.render_writes(timing)
2938            } else if level == 1 {
2939                self.profiler
2940                    .render_writes(timing)
2941                    .map(|writes| wgpu::RenderPassTimestampWrites {
2942                        end_of_pass_write_index: None,
2943                        ..writes
2944                    })
2945            } else if last {
2946                self.profiler
2947                    .render_writes(timing)
2948                    .map(|writes| wgpu::RenderPassTimestampWrites {
2949                        beginning_of_pass_write_index: None,
2950                        ..writes
2951                    })
2952            } else {
2953                None
2954            };
2955            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2956                multiview_mask: None,
2957                label: Some("scene colour mip"),
2958                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2959                    view: &self.mips.levels[level],
2960                    resolve_target: None,
2961                    depth_slice: None,
2962                    ops: wgpu::Operations {
2963                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
2964                        store: wgpu::StoreOp::Store,
2965                    },
2966                })],
2967                depth_stencil_attachment: None,
2968                timestamp_writes: writes,
2969                occlusion_query_set: None,
2970            });
2971            crate::viewport::apply(&mut pass, self.mips.bordered(rect, level as u32));
2972            pass.set_pipeline(&self.mips.pipeline);
2973            pass.set_bind_group(0, &self.mips.binds[level - 1], &[]);
2974            pass.draw(0..3, 0..1);
2975        }
2976        self.pass_log.push("scene colour");
2977        let inverse_projection = invert(projection).ok_or("camera projection is singular")?;
2978        self.reflections.encode(
2979            device,
2980            &mut encoder,
2981            &reflect::Inputs {
2982                depth: current_depth,
2983                normal_roughness: &self.frame.targets.normal_roughness_view,
2984                colour: &self.mips.view,
2985                motion: &self.frame.targets.velocity_view,
2986                projection,
2987                inverse_projection,
2988                frame_index: frame_number,
2989                thickness: reflect::THICKNESS,
2990                history_valid: valid,
2991            },
2992            Some(&mut self.profiler),
2993        );
2994        self.pass_log.push("reflections");
2995        let mut reactive = &self.reactive_view;
2996        if glass::drawn(effects.glass) {
2997            let cube = self.frame.sky().cube_view();
2998            let caustic = self.frame.caustic();
2999            let content = self.frame.content();
3000            let caustic_view = caustic
3001                .as_ref()
3002                .map_or(&self.black, |caustic| content.view(caustic.slot));
3003            let (fluid, ripple) = effects
3004                .liquid
3005                .as_ref()
3006                .map_or((&self.black, &self.black), |liquid| {
3007                    (liquid.fluid, liquid.ripple)
3008                });
3009            self.glass.encode(
3010                device,
3011                &queue,
3012                &mut encoder,
3013                glass::Render {
3014                    camera: glass::Camera {
3015                        view: scene.camera.view,
3016                        projection,
3017                    },
3018                    surfaces: effects.glass,
3019                    inputs: glass::Inputs {
3020                        opaque_hdr_mips: &self.mips.view,
3021                        opaque_depth: &self.frame.targets.depth_view,
3022                        reflection_cube: &cube,
3023                        caustic: caustic_view,
3024                        caustic_projection: caustic
3025                            .as_ref()
3026                            .map(|caustic| caustic.projection(scene.sun.direction)),
3027                        fluid_height: fluid,
3028                        ripple_height: ripple,
3029                        mip_count: self.mips.levels.len() as u32,
3030                        sky_sh: self.frame.sky().lighting.sh,
3031                    },
3032                    output: &self.frame.targets.hdr.view,
3033                    profiler: Some(&mut self.profiler),
3034                },
3035            )?;
3036            reactive = self.glass.reactive();
3037            self.pass_log.push("glass");
3038        }
3039        let mut effect_frame = EffectFrame::new(width, height, scene.time, scene.seed);
3040        effect_frame.view_proj = jittered;
3041        effect_frame.eye = [
3042            scene.camera.position[0],
3043            scene.camera.position[1],
3044            scene.camera.position[2],
3045            0.0,
3046        ];
3047        effect_frame.right = [right[0], right[1], right[2], 0.0];
3048        effect_frame.up = [view.up[0], view.up[1], view.up[2], 0.0];
3049        effect_frame.forward = [view.forward[0], view.forward[1], view.forward[2], 0.0];
3050        effect_frame.lens = [lens[0], lens[1], 0.0, 0.0];
3051        effect_frame.sun = [
3052            scene.sun.direction[0],
3053            scene.sun.direction[1],
3054            scene.sun.direction[2],
3055            0.0,
3056        ];
3057        effect_frame.sun_color = [
3058            scene.sun.colour[0],
3059            scene.sun.colour[1],
3060            scene.sun.colour[2],
3061            scene.sun.intensity,
3062        ];
3063        let shadow_data = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
3064            label: Some("effects sun cascades"),
3065            contents: &self.shadows.sample_uniform(&fit),
3066            usage: wgpu::BufferUsages::UNIFORM,
3067        });
3068        let targets = EffectTargets {
3069            hdr: &self.frame.targets.hdr.view,
3070            depth: current_depth,
3071            shadow: Some((self.shadows.atlas_view(), &shadow_data)),
3072        };
3073        if let Some(haze) = self.haze {
3074            let mut room_frame = effect_frame;
3075            room_frame.set_room_haze(haze);
3076            self.effects.prepare_room(
3077                &mut EffectCommands {
3078                    device,
3079                    encoder: &mut encoder,
3080                },
3081                &haze,
3082            );
3083            let march = self.profiler.pass("haze march");
3084            let upsample = self.profiler.pass("haze upsample");
3085            self.effects.render_volume(
3086                &mut EffectCommands {
3087                    device,
3088                    encoder: &mut encoder,
3089                },
3090                &room_frame,
3091                targets,
3092                VolumeTimestamps {
3093                    march: self.profiler.render_writes(march),
3094                    upsample: self.profiler.render_writes(upsample),
3095                },
3096            );
3097            self.pass_log.extend(["haze march", "haze upsample"]);
3098        }
3099        if let Some(volume) = effects.volume.as_ref().filter(|_| !cached) {
3100            let mut volume_frame = effect_frame;
3101            volume_frame.set_volume(
3102                volume.kind,
3103                volume.lo,
3104                volume.hi,
3105                volume.color,
3106                volume.density,
3107                volume.anisotropy,
3108            );
3109            volume_frame.scale_time(volume.time_scale);
3110            let march = self.profiler.pass("volume march");
3111            let upsample = self.profiler.pass("volume upsample");
3112            self.effects.render_volume(
3113                &mut EffectCommands {
3114                    device,
3115                    encoder: &mut encoder,
3116                },
3117                &volume_frame,
3118                targets,
3119                VolumeTimestamps {
3120                    march: self.profiler.render_writes(march),
3121                    upsample: self.profiler.render_writes(upsample),
3122                },
3123            );
3124            self.pass_log.extend(["volume march", "volume upsample"]);
3125        }
3126        let petals = self
3127            .petals
3128            .as_mut()
3129            .map(|petals| petals.step(scene.time, &scene.wind.current))
3130            .unwrap_or_default();
3131        let own: &[ParticleInstance] = if cached { &[] } else { effects.particles };
3132        let particles: Vec<ParticleInstance>;
3133        let particles = if petals.is_empty() {
3134            own
3135        } else {
3136            particles = own.iter().copied().chain(petals).collect();
3137            &particles
3138        };
3139        if !particles.is_empty() {
3140            let timing = self.profiler.pass("particles");
3141            self.effects.render_particles(
3142                &mut EffectCommands {
3143                    device,
3144                    encoder: &mut encoder,
3145                },
3146                &effect_frame,
3147                targets,
3148                particles,
3149                self.profiler.render_writes(timing),
3150            );
3151            self.pass_log.push("particles");
3152        }
3153        if let Some(mesh) = effects.creature_mesh
3154            && !effects.creatures.is_empty()
3155        {
3156            let timing = self.profiler.pass("creatures");
3157            self.effects.render_creatures(
3158                &mut EffectCommands {
3159                    device,
3160                    encoder: &mut encoder,
3161                },
3162                &effect_frame,
3163                targets,
3164                mesh,
3165                effects.creatures,
3166                self.profiler.render_writes(timing),
3167            );
3168            self.pass_log.push("creatures");
3169        }
3170        let deformers = self.text.deformer_group(
3171            device,
3172            self.frame.deformer_buffer().buffer(),
3173            self.frame.deformer_buffer().plan_buffer(),
3174        );
3175        let surface_text = text
3176            .surface
3177            .iter()
3178            .any(|item| !item.paragraph.glyphs.is_empty())
3179            || text.surface_quads.iter().any(|item| !item.quads.is_empty());
3180        if surface_text && !self.text_after_taa {
3181            draw_surface_text(
3182                &mut self.text,
3183                &self.frame,
3184                &mut self.profiler,
3185                device,
3186                &mut encoder,
3187                text,
3188                &deformers,
3189                &self.frame.targets.hdr.view,
3190            )?;
3191            self.pass_log.push("surface text");
3192        }
3193        self.taa.resolve_timed(
3194            device,
3195            &queue,
3196            &mut encoder,
3197            &GpuInputs {
3198                colour: &self.frame.targets.hdr.view,
3199                depth: current_depth,
3200                motion: &self.frame.targets.velocity_view,
3201                reactive,
3202                id: Some(&self.frame.targets.ids_view),
3203                normal: Some(&self.frame.targets.normal_roughness_view),
3204                exposure,
3205            },
3206            &taa::Settings::default(),
3207            Some(&mut self.profiler),
3208        )?;
3209        self.pass_log.push("TAA");
3210        let resolved = self
3211            .taa
3212            .history_view()
3213            .ok_or("TAA output is empty")?
3214            .clone();
3215        let late_text = surface_text && self.text_after_taa;
3216        let resolved = if self.sharpen > 0.0 || late_text {
3217            let display = self.crisp.encode(
3218                device,
3219                &queue,
3220                &mut encoder,
3221                &resolved,
3222                self.sharpen,
3223                exposure,
3224                Some(&mut self.profiler),
3225            );
3226            self.pass_log.push(if self.sharpen > 0.0 {
3227                "sharpen"
3228            } else {
3229                "crisp copy"
3230            });
3231            if late_text {
3232                draw_surface_text(
3233                    &mut self.text,
3234                    &self.frame,
3235                    &mut self.profiler,
3236                    device,
3237                    &mut encoder,
3238                    text,
3239                    &deformers,
3240                    display,
3241                )?;
3242                self.pass_log.push("surface text");
3243            }
3244            display.clone()
3245        } else {
3246            resolved
3247        };
3248        let layered = cached && (effects.volume.is_some() || !effects.particles.is_empty());
3249        let resolved = if let Some(cache) = self.demand.as_ref().filter(|_| cached) {
3250            let whole = [Region::whole(width, height)];
3251            if layered {
3252                cache.encode_copy(
3253                    device,
3254                    &mut encoder,
3255                    &resolved,
3256                    &cache.still.view,
3257                    &whole,
3258                    "demand still",
3259                    &mut self.profiler,
3260                );
3261                self.pass_log.push("demand still");
3262                let layer = cache.layer.view.clone();
3263                cache.encode_copy(
3264                    device,
3265                    &mut encoder,
3266                    &resolved,
3267                    &layer,
3268                    &whole,
3269                    "demand layer",
3270                    &mut self.profiler,
3271                );
3272                let mut layer_frame = effect_frame;
3273                layer_frame.view_proj = view_projection;
3274                draw_layers(
3275                    device,
3276                    &mut encoder,
3277                    &mut self.effects,
3278                    &mut self.profiler,
3279                    effects,
3280                    &layer_frame,
3281                    EffectTargets {
3282                        hdr: &layer,
3283                        depth: current_depth,
3284                        shadow: Some((self.shadows.atlas_view(), &shadow_data)),
3285                    },
3286                    &mut self.pass_log,
3287                );
3288                layer
3289            } else {
3290                resolved
3291            }
3292        } else {
3293            resolved
3294        };
3295        let resolved = if let Some(lens) = self.lens {
3296            let timing = self.profiler.pass("depth of field");
3297            self.pass_log.push("depth of field");
3298            self.dof.render(
3299                &mut encoder,
3300                &resolved,
3301                current_depth,
3302                lens,
3303                dof::height_plane(scene.camera.view, scene.camera.projection),
3304                self.profiler.compute_writes(timing),
3305            )
3306        } else {
3307            &resolved
3308        };
3309        if matches!(self.exposure_mode, Exposure::Auto { .. }) {
3310            self.exposure
3311                .encode(device, &queue, &mut encoder, resolved, [width, height]);
3312        }
3313        let overlays = !text.overlay.is_empty() || !text.overlay_quads.is_empty() || hud.is_some();
3314        let colour = self
3315            .demand
3316            .as_ref()
3317            .filter(|_| cached)
3318            .map(|cache| cache.colour.view.clone());
3319        let post_output = match &colour {
3320            Some(view) => view,
3321            None => self.display.as_ref().map_or(output, |(_, view)| view),
3322        };
3323        self.post.run_timed_exposed(
3324            &mut encoder,
3325            resolved,
3326            Some(&self.frame.targets.depth_view),
3327            Some(&self.frame.targets.normal_roughness_view),
3328            post_output,
3329            frame_number,
3330            scene.seed,
3331            exposure,
3332            Some(&mut self.profiler),
3333        );
3334        self.pass_log.push("post");
3335        if let Some(cache) = self.demand.as_mut().filter(|_| cached) {
3336            cache.probe_depth(&mut encoder, &self.depths[self.depth_index].0);
3337            cache.encode_tiles(
3338                device,
3339                &mut encoder,
3340                &self.depths[self.depth_index].1,
3341                &mut self.profiler,
3342            );
3343            self.pass_log.push("demand tiles");
3344            let previous = cache.cached.as_ref().map(|cached| cached.exposure);
3345            cache.exposure_moving = previous
3346                .is_some_and(|previous| (previous - exposure).abs() > 1e-3 * exposure.abs());
3347            cache.cached = Some(demand::Cached {
3348                view: scene.camera.view,
3349                projection: scene.camera.projection,
3350                far: view.far,
3351                depth_index: self.depth_index,
3352                shadow: self.shadows.sample_uniform(&fit).to_vec(),
3353                frame_number,
3354                exposure,
3355            });
3356            cache.colour_layers = layered;
3357            cache.book.still = layered;
3358            cache.last = demand::Last::Colour;
3359            cache.overlaid = overlays;
3360            cache.overlay_print = None;
3361            if overlays {
3362                cache.encode_clear(&mut encoder);
3363            }
3364        }
3365        let overlay_target = match self.demand.as_ref().filter(|_| cached) {
3366            Some(cache) => cache.overlay.view.clone(),
3367            None => post_output.clone(),
3368        };
3369        for item in &text.overlay {
3370            self.text
3371                .encode_timed(
3372                    device,
3373                    &mut encoder,
3374                    TextDraw {
3375                        target: &overlay_target,
3376                        depth: None,
3377                        atlas: item.atlas,
3378                        paragraph: item.paragraph,
3379                        space: item.space,
3380                        color: item.color,
3381                        deformers: None,
3382                    },
3383                    Some(&mut self.profiler),
3384                )
3385                .map_err(str::to_owned)?;
3386        }
3387        for item in &text.overlay_quads {
3388            self.text
3389                .encode_quads_timed(
3390                    device,
3391                    &mut encoder,
3392                    QuadDraw {
3393                        target: &overlay_target,
3394                        depth: None,
3395                        atlas: item.atlas,
3396                        quads: item.quads,
3397                        space: item.space,
3398                        deformers: None,
3399                        icons: item.icons,
3400                    },
3401                    Some(&mut self.profiler),
3402                )
3403                .map_err(str::to_owned)?;
3404        }
3405        if !text.overlay.is_empty() || !text.overlay_quads.is_empty() {
3406            self.pass_log.push("overlay text");
3407        }
3408        let ids = &self.frame.targets.ids_view;
3409        if glass::drawn(effects.glass) && self.glass.has_ids() {
3410            self.glass
3411                .encode_ids(&mut encoder, ids, Some(&mut self.profiler));
3412            self.pass_log.push("glass ids");
3413        }
3414        let lit = self.frame.lit_bindings();
3415        let lit_for = |space: &TextSpace| {
3416            if space.is_lit() {
3417                lit.as_ref()
3418                    .map(Some)
3419                    .ok_or("frame lighting is not prepared")
3420            } else {
3421                Ok(None)
3422            }
3423        };
3424        for item in text.surface.iter().filter(|item| item.id != 0) {
3425            self.text
3426                .encode_ids(
3427                    device,
3428                    &mut encoder,
3429                    TextDraw {
3430                        target: ids,
3431                        depth: Some(&self.frame.targets.depth_view),
3432                        atlas: item.atlas,
3433                        paragraph: item.paragraph,
3434                        space: item.space,
3435                        color: item.color,
3436                        deformers: Some(&deformers),
3437                    },
3438                    lit_for(&item.space)?,
3439                    item.id,
3440                    Some(&mut self.profiler),
3441                )
3442                .map_err(str::to_owned)?;
3443        }
3444        for item in text.surface_quads.iter().filter(|item| item.id != 0) {
3445            self.text
3446                .encode_quad_ids(
3447                    device,
3448                    &mut encoder,
3449                    QuadDraw {
3450                        target: ids,
3451                        depth: Some(&self.frame.targets.depth_view),
3452                        atlas: item.atlas,
3453                        quads: item.quads,
3454                        space: item.space,
3455                        deformers: Some(&deformers),
3456                        icons: item.icons,
3457                    },
3458                    lit_for(&item.space)?,
3459                    item.id,
3460                    Some(&mut self.profiler),
3461                )
3462                .map_err(str::to_owned)?;
3463        }
3464        for item in text.overlay.iter().filter(|item| item.id != 0) {
3465            self.text
3466                .encode_ids(
3467                    device,
3468                    &mut encoder,
3469                    TextDraw {
3470                        target: ids,
3471                        depth: None,
3472                        atlas: item.atlas,
3473                        paragraph: item.paragraph,
3474                        space: item.space,
3475                        color: item.color,
3476                        deformers: None,
3477                    },
3478                    None,
3479                    item.id,
3480                    Some(&mut self.profiler),
3481                )
3482                .map_err(str::to_owned)?;
3483        }
3484        for item in text.overlay_quads.iter().filter(|item| item.id != 0) {
3485            self.text
3486                .encode_quad_ids(
3487                    device,
3488                    &mut encoder,
3489                    QuadDraw {
3490                        target: ids,
3491                        depth: None,
3492                        atlas: item.atlas,
3493                        quads: item.quads,
3494                        space: item.space,
3495                        deformers: None,
3496                        icons: item.icons,
3497                    },
3498                    None,
3499                    item.id,
3500                    Some(&mut self.profiler),
3501                )
3502                .map_err(str::to_owned)?;
3503        }
3504        if text.surface.iter().any(|item| item.id != 0)
3505            || text.surface_quads.iter().any(|item| item.id != 0)
3506            || text.overlay.iter().any(|item| item.id != 0)
3507            || text.overlay_quads.iter().any(|item| item.id != 0)
3508        {
3509            self.pass_log.push("text ids");
3510        }
3511        if let Some(hud) = hud {
3512            let mut flat = self
3513                .flat
3514                .take()
3515                .unwrap_or_else(|| FlatPass::new(device, &queue));
3516            let (capacity_width, capacity_height) = self.capacity();
3517            flat.set_capacity(Some([capacity_width, capacity_height]));
3518            let prepared = flat.prepare(device, &queue, hud, [width, height]);
3519            if let Err(error) = prepared {
3520                self.flat = Some(flat);
3521                return Err(error);
3522            }
3523            let target = flat.colour_target(device, [capacity_width, capacity_height]);
3524            flat.encode_colour(
3525                &mut encoder,
3526                &target,
3527                Some([0.0; 4]),
3528                Some(&mut self.profiler),
3529            );
3530            flat.encode_finish(
3531                device,
3532                &mut encoder,
3533                &target,
3534                &overlay_target,
3535                wgpu::TextureFormat::Rgba16Float,
3536                self.display.is_some(),
3537                true,
3538                Some(&mut self.profiler),
3539            );
3540            flat.encode_ids(&mut encoder, ids, false, Some(&mut self.profiler));
3541            self.flat = Some(flat);
3542            self.pass_log.extend(["flat", "flat finish", "flat ids"]);
3543        }
3544        let source = post_output.clone();
3545        if cached && overlays {
3546            if let Some(cache) = self.demand.as_mut() {
3547                cache.encode_composite(
3548                    device,
3549                    &mut encoder,
3550                    &source,
3551                    (output, self.output_format),
3552                    &mut self.profiler,
3553                );
3554                self.pass_log.push("composite");
3555            }
3556        } else if self.blit.is_some() {
3557            self.encode_blit(device, &mut encoder, &source, output);
3558        }
3559        self.finish_encoder(device, &queue, encoder, true)
3560    }
3561
3562    fn encode_blit(
3563        &mut self,
3564        device: &wgpu::Device,
3565        encoder: &mut wgpu::CommandEncoder,
3566        post_output: &wgpu::TextureView,
3567        output: &wgpu::TextureView,
3568    ) {
3569        if let Some((layout, pipeline)) = &self.blit {
3570            let bind = device.create_bind_group(&wgpu::BindGroupDescriptor {
3571                label: Some("live output blit"),
3572                layout,
3573                entries: &[wgpu::BindGroupEntry {
3574                    binding: 0,
3575                    resource: wgpu::BindingResource::TextureView(post_output),
3576                }],
3577            });
3578            let timing = self.profiler.pass("output blit");
3579            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3580                multiview_mask: None,
3581                label: Some("live output blit"),
3582                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
3583                    view: output,
3584                    resolve_target: None,
3585                    depth_slice: None,
3586                    ops: wgpu::Operations {
3587                        load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3588                        store: wgpu::StoreOp::Store,
3589                    },
3590                })],
3591                depth_stencil_attachment: None,
3592                timestamp_writes: self.profiler.render_writes(timing),
3593                occlusion_query_set: None,
3594            });
3595            let [width, height] = self.frame.viewport();
3596            crate::viewport::apply(&mut pass, [width, height]);
3597            pass.set_pipeline(pipeline);
3598            pass.set_bind_group(0, &bind, &[]);
3599            pass.draw(0..3, 0..1);
3600            self.pass_log.push("output blit");
3601        }
3602    }
3603
3604    fn finish_encoder(
3605        &mut self,
3606        device: &wgpu::Device,
3607        queue: &wgpu::Queue,
3608        mut encoder: wgpu::CommandEncoder,
3609        depth: bool,
3610    ) -> Result<u64, String> {
3611        let pick_readback = if self.pick_readback.is_none() {
3612            self.pick_request.take().map(|(x, y)| {
3613                let buffer = device.create_buffer(&wgpu::BufferDescriptor {
3614                    label: Some("live pick readback"),
3615                    size: 512,
3616                    usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
3617                    mapped_at_creation: false,
3618                });
3619                for (texture, offset) in [
3620                    (&self.frame.targets.ids, 0),
3621                    (&self.depths[self.depth_index].0, 256),
3622                ]
3623                .into_iter()
3624                .take(if depth { 2 } else { 1 })
3625                {
3626                    encoder.copy_texture_to_buffer(
3627                        wgpu::TexelCopyTextureInfo {
3628                            texture,
3629                            mip_level: 0,
3630                            origin: wgpu::Origin3d { x, y, z: 0 },
3631                            aspect: wgpu::TextureAspect::All,
3632                        },
3633                        wgpu::TexelCopyBufferInfo {
3634                            buffer: &buffer,
3635                            layout: wgpu::TexelCopyBufferLayout {
3636                                offset,
3637                                bytes_per_row: Some(256),
3638                                rows_per_image: Some(1),
3639                            },
3640                        },
3641                        wgpu::Extent3d {
3642                            width: 1,
3643                            height: 1,
3644                            depth_or_array_layers: 1,
3645                        },
3646                    );
3647                }
3648                (buffer, x, y)
3649            })
3650        } else {
3651            None
3652        };
3653        let number = self.frames_submitted;
3654        let _ = device.poll(wgpu::PollType::Poll);
3655        self.gather_timings(number);
3656        let slot = self.profiler.finish_frame(&mut encoder, number);
3657        let index = queue.submit(Some(encoder.finish()));
3658        let (width, height) = self.size();
3659        self.stats.submitted(number, [width, height]);
3660        if std::mem::take(&mut self.resolution_frame)
3661            && let Some(resolution) = self.resolution.as_mut()
3662        {
3663            let rendered = self
3664                .resolution_content
3665                .map_or(resolution.scale(), |content| {
3666                    let area = f64::from(width) * f64::from(height);
3667                    let full = f64::from(content[0]) * f64::from(content[1]);
3668                    (area / full).sqrt() as f32
3669                });
3670            resolution.begin_at(number, rendered);
3671        }
3672        self.frames_submitted += 1;
3673        let done = self.frames_done.clone();
3674        queue.on_submitted_work_done(move || {
3675            done.fetch_max(number + 1, Ordering::AcqRel);
3676        });
3677        self.submissions.push_back((number, index));
3678        if matches!(self.exposure_mode, Exposure::Auto { .. }) {
3679            self.exposure.submitted();
3680        }
3681        if let Some((buffer, x, y)) = pick_readback {
3682            let (sender, receiver) = std::sync::mpsc::channel();
3683            buffer
3684                .slice(..)
3685                .map_async(wgpu::MapMode::Read, move |result| {
3686                    let _ = sender.send(result);
3687                });
3688            self.pick_readback = Some(PickReadback {
3689                buffer,
3690                receiver,
3691                x,
3692                y,
3693            });
3694        }
3695        if let Some(slot) = slot {
3696            self.profiler.submitted(slot);
3697        }
3698        let done = self.frames_done.load(Ordering::Acquire);
3699        while self
3700            .submissions
3701            .front()
3702            .is_some_and(|(frame, _)| *frame < done)
3703        {
3704            self.submissions.pop_front();
3705        }
3706        Ok(number)
3707    }
3708}
3709
3710impl Renderer {
3711    fn touch(&self) {
3712        self.revision.fetch_add(1, Ordering::Relaxed);
3713    }
3714
3715    pub fn set_frames_on_demand(&mut self, limits: Option<Limits>) -> Result<(), String> {
3716        let Some(limits) = limits else {
3717            self.demand = None;
3718            return Ok(());
3719        };
3720        if self.display.is_none() {
3721            return Err("frames on demand need an sRGB output format".into());
3722        }
3723        if self.size() != self.capacity() {
3724            return Err("frames on demand render at the full size; set the viewport to the renderer's capacity first".into());
3725        }
3726        match &mut self.demand {
3727            Some(cache) => cache.limits = limits,
3728            None => {
3729                let (width, height) = self.size();
3730                self.demand = Some(demand::Cache::new(
3731                    &self.frame.gpu.device,
3732                    [width, height],
3733                    limits,
3734                ));
3735            }
3736        }
3737        Ok(())
3738    }
3739
3740    pub fn frames_on_demand(&self) -> Option<Limits> {
3741        self.demand.as_ref().map(|cache| cache.limits)
3742    }
3743
3744    pub fn demand_stats(&self) -> Option<DemandStats> {
3745        self.demand.as_ref().map(demand::Cache::stats)
3746    }
3747
3748    pub fn frame_needed(&self, next: &Next<'_>) -> Need {
3749        let Some(cache) = &self.demand else {
3750            return Need::Full;
3751        };
3752        demand::decide(&cache.book, &self.summarize(next), &cache.limits)
3753    }
3754
3755    pub fn submit_needed(
3756        &mut self,
3757        next: &Next<'_>,
3758        need: &Need,
3759        output: &wgpu::TextureView,
3760    ) -> Result<Option<Submitted>, String> {
3761        let Some(frame) = self.encode_needed(next, need, output)? else {
3762            return Ok(None);
3763        };
3764        Ok(Some(self.submitted_frame(frame)))
3765    }
3766
3767    pub fn render_needed(
3768        &mut self,
3769        next: &Next<'_>,
3770        need: &Need,
3771        output: &wgpu::TextureView,
3772    ) -> Result<Option<Vec<PassTiming>>, String> {
3773        let Some(frame) = self.encode_needed(next, need, output)? else {
3774            return Ok(None);
3775        };
3776        self.wait_for(frame).map(Some)
3777    }
3778
3779    pub fn present_last(&mut self, output: &wgpu::TextureView) -> Result<Submitted, String> {
3780        let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
3781        cache.cached.as_ref().ok_or("no frame has been drawn")?;
3782        let owned_device = self.frame.gpu.device.clone();
3783        let device = &owned_device;
3784        let queue = self.frame.gpu.queue.clone();
3785        self.begin_frame(FrameKind::Idle);
3786        self.pass_log.clear();
3787        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3788            label: Some("live present"),
3789        });
3790        self.encode_present(device, &mut encoder, output)?;
3791        let frame = self.finish_encoder(device, &queue, encoder, false)?;
3792        if let Some(cache) = &mut self.demand {
3793            cache.submitted();
3794        }
3795        Ok(self.submitted_frame(frame))
3796    }
3797
3798    fn encode_present(
3799        &mut self,
3800        device: &wgpu::Device,
3801        encoder: &mut wgpu::CommandEncoder,
3802        output: &wgpu::TextureView,
3803    ) -> Result<(), String> {
3804        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3805        let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
3806        let base = match cache.last {
3807            demand::Last::Warp(uniform) => {
3808                let depth = self.depths[cached.depth_index].1.clone();
3809                let colour = cache.colour.view.clone();
3810                cache.encode_warp(
3811                    device,
3812                    encoder,
3813                    &uniform,
3814                    &colour,
3815                    &depth,
3816                    (output, self.output_format),
3817                    false,
3818                    &mut self.profiler,
3819                );
3820                self.pass_log.push("reproject");
3821                return Ok(());
3822            }
3823            demand::Last::Colour => cache.colour.view.clone(),
3824            demand::Last::Scratch => cache.scratch.view.clone(),
3825        };
3826        if cache.overlaid {
3827            cache.encode_composite(
3828                device,
3829                encoder,
3830                &base,
3831                (output, self.output_format),
3832                &mut self.profiler,
3833            );
3834            self.pass_log.push("composite");
3835        } else {
3836            self.encode_blit(device, encoder, &base, output);
3837        }
3838        Ok(())
3839    }
3840
3841    fn refresh_overlay(
3842        &mut self,
3843        device: &wgpu::Device,
3844        queue: &wgpu::Queue,
3845        encoder: &mut wgpu::CommandEncoder,
3846        next: &Next<'_>,
3847        print: u64,
3848    ) -> Result<(), String> {
3849        let overlays = !next.text.overlay.is_empty()
3850            || !next.text.overlay_quads.is_empty()
3851            || next.hud.is_some();
3852        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3853        cache.overlaid = overlays;
3854        if !overlays {
3855            cache.overlay_print = None;
3856            return Ok(());
3857        }
3858        if cache.overlay_print == Some(print) {
3859            return Ok(());
3860        }
3861        cache.encode_clear(encoder);
3862        let target = cache.overlay.view.clone();
3863        cache.overlay_print = None;
3864        self.encode_overlay(device, queue, encoder, next, &target)?;
3865        if let Some(cache) = &mut self.demand {
3866            cache.overlay_print = Some(print);
3867        }
3868        Ok(())
3869    }
3870
3871    pub fn wake(&mut self, groups: u32) -> Result<Submitted, String> {
3872        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
3873        let owned_device = self.frame.gpu.device.clone();
3874        let device = &owned_device;
3875        let queue = self.frame.gpu.queue.clone();
3876        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
3877            label: Some("live wake"),
3878        });
3879        cache.encode_wake(device, &mut encoder, groups, &mut self.profiler);
3880        self.stats.discard();
3881        self.resolution_frame = false;
3882        self.pass_log.clear();
3883        self.pass_log.push("wake");
3884        let frame = self.finish_encoder(device, &queue, encoder, false)?;
3885        Ok(self.submitted_frame(frame))
3886    }
3887
3888    pub fn submit_bridge(
3889        &mut self,
3890        next: &Next<'_>,
3891        output: &wgpu::TextureView,
3892    ) -> Result<Option<Submitted>, String> {
3893        let Some(frame) = self.encode_bridge(next, output)? else {
3894            return Ok(None);
3895        };
3896        Ok(Some(self.submitted_frame(frame)))
3897    }
3898
3899    pub fn render_bridge(
3900        &mut self,
3901        next: &Next<'_>,
3902        output: &wgpu::TextureView,
3903    ) -> Result<Option<Vec<PassTiming>>, String> {
3904        let Some(frame) = self.encode_bridge(next, output)? else {
3905            return Ok(None);
3906        };
3907        self.wait_for(frame).map(Some)
3908    }
3909
3910    fn encode_bridge(
3911        &mut self,
3912        next: &Next<'_>,
3913        output: &wgpu::TextureView,
3914    ) -> Result<Option<u64>, String> {
3915        let Some(cache) = &self.demand else {
3916            return Ok(None);
3917        };
3918        let Some(cached) = &cache.cached else {
3919            return Ok(None);
3920        };
3921        let camera = next.scene.camera;
3922        if demand::motion(
3923            (&cached.view, &cached.projection, [0.0; 3]),
3924            (&camera.view, &camera.projection, [0.0; 3]),
3925            None,
3926        )
3927        .is_none()
3928        {
3929            return Ok(None);
3930        }
3931        let summary = self.summarize(next);
3932        self.begin_frame(FrameKind::Reproject);
3933        let frame = self.encode_reproject(next, &summary, output)?;
3934        if let Some(cache) = &mut self.demand {
3935            cache.submitted();
3936        }
3937        Ok(Some(frame))
3938    }
3939
3940    fn layer_reach(&self, style: Finish) -> Option<u32> {
3941        if self.lens.is_some() {
3942            return None;
3943        }
3944        if (self.custom_finish || style == self.style)
3945            && let Some(reach) = self.post.region_reach()
3946        {
3947            return Some(reach);
3948        }
3949        self.custom_local
3950            .unwrap_or_else(|| demand::local_post(&chain(style, false)))
3951            .then_some(0)
3952    }
3953
3954    fn summarize(&self, next: &Next<'_>) -> demand::Summary {
3955        let (width, height) = self.size();
3956        let camera = next.scene.camera;
3957        let view_projection = frame::multiply(camera.projection, camera.view);
3958        let mut world = Print::new();
3959        world.wide(self.revision.load(Ordering::Relaxed));
3960        world.wide(next.content);
3961        world.debug(&next.style);
3962        demand::print_scene(&mut world, next.scene);
3963        demand::print_effects(&mut world, next.effects);
3964        let mut overlay = Print::new();
3965        overlay.wide(next.overlay);
3966        overlay.word(u32::from(next.hud.is_some()));
3967        let mut anchors = Vec::new();
3968        demand::print_text(
3969            &mut world,
3970            &mut overlay,
3971            &mut anchors,
3972            next.text,
3973            &view_projection,
3974        );
3975        let auto = matches!(self.exposure_mode, Exposure::Auto { .. });
3976        let ticking = (next.effects.creature_mesh.is_some() && !next.effects.creatures.is_empty())
3977            || next.effects.liquid.is_some()
3978            || self.frame.has_tree()
3979            || next.scene.wind.current.strength > 0.0
3980            || self.tape.is_some_and(|tape| !tape.is_off())
3981            || self.sky_settling()
3982            || (auto
3983                && self
3984                    .demand
3985                    .as_ref()
3986                    .is_some_and(|cache| cache.exposure_moving));
3987        let reach = self.layer_reach(next.style);
3988        let whole = Region::whole(width, height);
3989        let mut layered = Vec::new();
3990        if let Some(volume) = &next.effects.volume
3991            && let Some(region) = demand::box_region(
3992                &view_projection,
3993                volume.lo,
3994                volume.hi,
3995                8 + demand::MARGIN + reach.unwrap_or(0),
3996                width,
3997                height,
3998            )
3999        {
4000            layered.push(region);
4001        }
4002        if let Some(region) = demand::particle_region(
4003            &view_projection,
4004            next.effects.particles,
4005            demand::MARGIN + reach.unwrap_or(0),
4006            width,
4007            height,
4008        ) {
4009            layered.push(region);
4010        }
4011        if reach.is_none() && !layered.is_empty() {
4012            layered = vec![whole];
4013        }
4014        demand::Summary {
4015            view: camera.view,
4016            projection: camera.projection,
4017            position: camera.position,
4018            size: [width, height],
4019            world: world.finish(),
4020            anchors,
4021            overlay: overlay.finish(),
4022            ticking,
4023            layered: demand::merge(&layered, width, height),
4024            moving: next
4025                .moving
4026                .iter()
4027                .map(|region| region.clip(width, height))
4028                .filter(|region| !region.is_empty())
4029                .collect(),
4030        }
4031    }
4032
4033    fn encode_needed(
4034        &mut self,
4035        next: &Next<'_>,
4036        need: &Need,
4037        output: &wgpu::TextureView,
4038    ) -> Result<Option<u64>, String> {
4039        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4040        let _ = self.frame.gpu.device.poll(wgpu::PollType::Poll);
4041        cache.gather();
4042        let need = if cache.cached.is_none() && need.draws() {
4043            &Need::Full
4044        } else {
4045            need
4046        };
4047        let summary = self.summarize(next);
4048        let (frame, kind) = match need {
4049            Need::None => return Ok(None),
4050            Need::Full => {
4051                self.begin_frame(FrameKind::Full);
4052                (
4053                    self.encode_frame(
4054                        next.scene,
4055                        next.text,
4056                        next.effects,
4057                        next.style,
4058                        next.hud,
4059                        output,
4060                        true,
4061                    )?,
4062                    Shown::Full,
4063                )
4064            }
4065            Need::Reproject => {
4066                self.begin_frame(FrameKind::Reproject);
4067                (
4068                    self.encode_reproject(next, &summary, output)?,
4069                    Shown::Reproject,
4070                )
4071            }
4072            Need::Partial(regions) => {
4073                self.begin_frame(FrameKind::Partial);
4074                (
4075                    self.encode_partial(next, &summary, regions, output)?,
4076                    Shown::Partial,
4077                )
4078            }
4079        };
4080        let summary = if kind == Shown::Full {
4081            self.summarize(next)
4082        } else {
4083            summary
4084        };
4085        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4086        cache.submitted();
4087        if kind == Shown::Full && cache.overlaid {
4088            cache.overlay_print = Some(summary.overlay);
4089        }
4090        let limits = cache.limits;
4091        demand::record(&mut cache.book, summary, kind, &limits);
4092        Ok(Some(frame))
4093    }
4094
4095    fn layer_frame(&self, scene: &Scene<'_>, view_projection: Matrix) -> EffectFrame {
4096        let (width, height) = self.size();
4097        let view = camera_view(scene, width, height);
4098        let camera = scene.camera;
4099        let right = [camera.view[0][0], camera.view[1][0], camera.view[2][0]];
4100        let mut frame = EffectFrame::new(width, height, scene.time, scene.seed);
4101        frame.view_proj = view_projection;
4102        frame.eye = [
4103            camera.position[0],
4104            camera.position[1],
4105            camera.position[2],
4106            0.0,
4107        ];
4108        frame.right = [right[0], right[1], right[2], 0.0];
4109        frame.up = [view.up[0], view.up[1], view.up[2], 0.0];
4110        frame.forward = [view.forward[0], view.forward[1], view.forward[2], 0.0];
4111        frame.lens = [
4112            1.0 / camera.projection[0][0],
4113            1.0 / camera.projection[1][1],
4114            0.0,
4115            0.0,
4116        ];
4117        frame.sun = [
4118            scene.sun.direction[0],
4119            scene.sun.direction[1],
4120            scene.sun.direction[2],
4121            0.0,
4122        ];
4123        frame.sun_color = [
4124            scene.sun.colour[0],
4125            scene.sun.colour[1],
4126            scene.sun.colour[2],
4127            scene.sun.intensity,
4128        ];
4129        frame
4130    }
4131
4132    #[allow(clippy::too_many_arguments)]
4133    fn encode_layer_post(
4134        &mut self,
4135        device: &wgpu::Device,
4136        encoder: &mut wgpu::CommandEncoder,
4137        next: &Next<'_>,
4138        depth: &wgpu::TextureView,
4139        target: &wgpu::TextureView,
4140        rects: Option<&[Region]>,
4141    ) -> Result<(), String> {
4142        let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
4143        let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4144        let camera = next.scene.camera;
4145        let view_projection = frame::multiply(camera.projection, camera.view);
4146        let layer_frame = self.layer_frame(next.scene, view_projection);
4147        let layer = cache.layer.view.clone();
4148        let shadow_data = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
4149            label: Some("demand sun cascades"),
4150            contents: &cached.shadow,
4151            usage: wgpu::BufferUsages::UNIFORM,
4152        });
4153        let (frame_number, exposure) = (cached.frame_number, cached.exposure);
4154        draw_layers(
4155            device,
4156            encoder,
4157            &mut self.effects,
4158            &mut self.profiler,
4159            next.effects,
4160            &layer_frame,
4161            EffectTargets {
4162                hdr: &layer,
4163                depth,
4164                shadow: Some((self.shadows.atlas_view(), &shadow_data)),
4165            },
4166            &mut self.pass_log,
4167        );
4168        let source = if let Some(lens) = self.lens {
4169            let timing = self.profiler.pass("depth of field");
4170            self.pass_log.push("depth of field");
4171            self.dof.render(
4172                encoder,
4173                &layer,
4174                depth,
4175                lens,
4176                dof::height_plane(camera.view, camera.projection),
4177                self.profiler.compute_writes(timing),
4178            )
4179        } else {
4180            &layer
4181        };
4182        match rects {
4183            Some(rects) => {
4184                let rects: Vec<[u32; 4]> = rects
4185                    .iter()
4186                    .map(|r| [r.x, r.y, r.x + r.width, r.y + r.height])
4187                    .collect();
4188                self.post.run_region(
4189                    encoder,
4190                    source,
4191                    Some(&self.frame.targets.depth_view),
4192                    Some(&self.frame.targets.normal_roughness_view),
4193                    target,
4194                    frame_number,
4195                    next.scene.seed,
4196                    exposure,
4197                    &rects,
4198                    Some(&mut self.profiler),
4199                );
4200            }
4201            None => self.post.run_timed_exposed(
4202                encoder,
4203                source,
4204                Some(&self.frame.targets.depth_view),
4205                Some(&self.frame.targets.normal_roughness_view),
4206                target,
4207                frame_number,
4208                next.scene.seed,
4209                exposure,
4210                Some(&mut self.profiler),
4211            ),
4212        }
4213        self.pass_log.push("post");
4214        Ok(())
4215    }
4216
4217    fn encode_overlay(
4218        &mut self,
4219        device: &wgpu::Device,
4220        queue: &wgpu::Queue,
4221        encoder: &mut wgpu::CommandEncoder,
4222        next: &Next<'_>,
4223        target: &wgpu::TextureView,
4224    ) -> Result<(), String> {
4225        let (width, height) = self.size();
4226        let text = next.text;
4227        for item in &text.overlay {
4228            self.text
4229                .encode_timed(
4230                    device,
4231                    encoder,
4232                    TextDraw {
4233                        target,
4234                        depth: None,
4235                        atlas: item.atlas,
4236                        paragraph: item.paragraph,
4237                        space: item.space,
4238                        color: item.color,
4239                        deformers: None,
4240                    },
4241                    Some(&mut self.profiler),
4242                )
4243                .map_err(str::to_owned)?;
4244        }
4245        for item in &text.overlay_quads {
4246            self.text
4247                .encode_quads_timed(
4248                    device,
4249                    encoder,
4250                    QuadDraw {
4251                        target,
4252                        depth: None,
4253                        atlas: item.atlas,
4254                        quads: item.quads,
4255                        space: item.space,
4256                        deformers: None,
4257                        icons: item.icons,
4258                    },
4259                    Some(&mut self.profiler),
4260                )
4261                .map_err(str::to_owned)?;
4262        }
4263        if !text.overlay.is_empty() || !text.overlay_quads.is_empty() {
4264            self.pass_log.push("overlay text");
4265        }
4266        if let Some(hud) = next.hud {
4267            let mut flat = self
4268                .flat
4269                .take()
4270                .unwrap_or_else(|| FlatPass::new(device, queue));
4271            let prepared = flat.prepare(device, queue, hud, [width, height]);
4272            if let Err(error) = prepared {
4273                self.flat = Some(flat);
4274                return Err(error);
4275            }
4276            let colour = flat.colour_target(device, [width, height]);
4277            flat.encode_colour(encoder, &colour, Some([0.0; 4]), Some(&mut self.profiler));
4278            flat.encode_finish(
4279                device,
4280                encoder,
4281                &colour,
4282                target,
4283                wgpu::TextureFormat::Rgba16Float,
4284                self.display.is_some(),
4285                true,
4286                Some(&mut self.profiler),
4287            );
4288            self.flat = Some(flat);
4289            self.pass_log.extend(["flat", "flat finish"]);
4290        }
4291        Ok(())
4292    }
4293
4294    fn encode_reproject(
4295        &mut self,
4296        next: &Next<'_>,
4297        summary: &demand::Summary,
4298        output: &wgpu::TextureView,
4299    ) -> Result<u64, String> {
4300        self.pass_log.clear();
4301        let (width, height) = self.size();
4302        self.text_bounds = text_bounds(next.text, [width, height]);
4303        let owned_device = self.frame.gpu.device.clone();
4304        let device = &owned_device;
4305        let queue = self.frame.gpu.queue.clone();
4306        let camera = next.scene.camera;
4307        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4308            label: Some("live reproject"),
4309        });
4310        self.refresh_overlay(device, &queue, &mut encoder, next, summary.overlay)?;
4311        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4312        let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4313        let mut uniform = demand::WarpUniform::new(
4314            (&cached.view, &cached.projection),
4315            (&camera.view, &camera.projection),
4316            [width, height],
4317            cached.far,
4318        )
4319        .ok_or("camera projection is singular")?;
4320        let depth = self.depths[cached.depth_index].1.clone();
4321        if !summary.layered.is_empty() || cache.colour_layers {
4322            let still = cache.still.view.clone();
4323            let layer = cache.layer.view.clone();
4324            let scratch = cache.scratch.view.clone();
4325            cache.encode_warp(
4326                device,
4327                &mut encoder,
4328                &uniform,
4329                &still,
4330                &depth,
4331                (&layer, wgpu::TextureFormat::Rgba16Float),
4332                true,
4333                &mut self.profiler,
4334            );
4335            cache.last = demand::Last::Scratch;
4336            let warped = cache.depth.view.clone();
4337            self.pass_log.push("reproject");
4338            self.encode_layer_post(device, &mut encoder, next, &warped, &scratch, None)?;
4339            self.encode_present(device, &mut encoder, output)?;
4340        } else {
4341            uniform.limits[3] = if cache.overlaid { 1.0 } else { 0.0 };
4342            cache.last = demand::Last::Warp(uniform);
4343            self.encode_present(device, &mut encoder, output)?;
4344        }
4345        self.finish_encoder(device, &queue, encoder, false)
4346    }
4347
4348    fn encode_partial(
4349        &mut self,
4350        next: &Next<'_>,
4351        summary: &demand::Summary,
4352        regions: &[Region],
4353        output: &wgpu::TextureView,
4354    ) -> Result<u64, String> {
4355        self.pass_log.clear();
4356        let (width, height) = self.size();
4357        self.text_bounds = text_bounds(next.text, [width, height]);
4358        let owned_device = self.frame.gpu.device.clone();
4359        let device = &owned_device;
4360        let queue = self.frame.gpu.queue.clone();
4361        let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
4362            label: Some("live partial"),
4363        });
4364        let cache = self.demand.as_mut().ok_or("frames on demand are off")?;
4365        let cached = cache.cached.as_ref().ok_or("no frame has been drawn")?;
4366        let depth = self.depths[cached.depth_index].1.clone();
4367        let mut layered = summary.layered.clone();
4368        if let Some(shown) = &cache.book.shown {
4369            layered.extend(shown.layered.iter().copied());
4370        }
4371        let layered: Vec<Region> = demand::merge(&layered, width, height)
4372            .into_iter()
4373            .filter(|region| regions.iter().any(|outer| outer.touches(region, 0)))
4374            .collect();
4375        let colour = cache.colour.view.clone();
4376        cache.last = demand::Last::Colour;
4377        if !layered.is_empty() {
4378            let still = cache.still.view.clone();
4379            let layer = cache.layer.view.clone();
4380            let scratch = cache.scratch.view.clone();
4381            cache.encode_copy(
4382                device,
4383                &mut encoder,
4384                &still,
4385                &layer,
4386                &layered,
4387                "partial still",
4388                &mut self.profiler,
4389            );
4390            cache.colour_layers = !summary.layered.is_empty() || cache.colour_layers;
4391            self.pass_log.push("partial still");
4392            self.encode_layer_post(device, &mut encoder, next, &depth, &scratch, Some(&layered))?;
4393            let cache = self.demand.as_ref().ok_or("frames on demand are off")?;
4394            cache.encode_copy(
4395                device,
4396                &mut encoder,
4397                &scratch,
4398                &colour,
4399                &layered,
4400                "partial colour",
4401                &mut self.profiler,
4402            );
4403            self.pass_log.push("partial colour");
4404        }
4405        self.refresh_overlay(device, &queue, &mut encoder, next, summary.overlay)?;
4406        self.encode_present(device, &mut encoder, output)?;
4407        self.finish_encoder(device, &queue, encoder, false)
4408    }
4409}
4410
4411#[allow(clippy::too_many_arguments)]
4412fn draw_layers(
4413    device: &wgpu::Device,
4414    encoder: &mut wgpu::CommandEncoder,
4415    pass: &mut effects::Effects,
4416    profiler: &mut GpuProfiler,
4417    effects: &Effects<'_>,
4418    frame: &EffectFrame,
4419    targets: EffectTargets<'_>,
4420    log: &mut Vec<&'static str>,
4421) {
4422    if let Some(volume) = &effects.volume {
4423        let mut volume_frame = *frame;
4424        volume_frame.set_volume(
4425            volume.kind,
4426            volume.lo,
4427            volume.hi,
4428            volume.color,
4429            volume.density,
4430            volume.anisotropy,
4431        );
4432        volume_frame.scale_time(volume.time_scale);
4433        let march = profiler.pass("volume march");
4434        let upsample = profiler.pass("volume upsample");
4435        pass.render_volume(
4436            &mut EffectCommands { device, encoder },
4437            &volume_frame,
4438            targets,
4439            VolumeTimestamps {
4440                march: profiler.render_writes(march),
4441                upsample: profiler.render_writes(upsample),
4442            },
4443        );
4444        log.extend(["volume march", "volume upsample"]);
4445    }
4446    if !effects.particles.is_empty() {
4447        let timing = profiler.pass("particles");
4448        pass.render_particles(
4449            &mut EffectCommands { device, encoder },
4450            frame,
4451            targets,
4452            effects.particles,
4453            profiler.render_writes(timing),
4454        );
4455        log.push("particles");
4456    }
4457}
4458
4459#[allow(clippy::too_many_arguments)]
4460fn draw_surface_text(
4461    pass: &mut TextPass,
4462    frame: &Frame,
4463    profiler: &mut GpuProfiler,
4464    device: &wgpu::Device,
4465    encoder: &mut wgpu::CommandEncoder,
4466    text: &Text<'_>,
4467    deformers: &wgpu::BindGroup,
4468    target: &wgpu::TextureView,
4469) -> Result<(), String> {
4470    for item in &text.surface {
4471        let draw = TextDraw {
4472            target,
4473            depth: Some(&frame.targets.depth_view),
4474            atlas: item.atlas,
4475            paragraph: item.paragraph,
4476            space: item.space,
4477            color: item.color,
4478            deformers: Some(deformers),
4479        };
4480        if item.space.is_lit() {
4481            let lit = frame
4482                .lit_bindings()
4483                .ok_or("frame lighting is not prepared")?;
4484            pass.encode_lit_timed(device, encoder, draw, &lit, Some(&mut *profiler))
4485        } else {
4486            pass.encode_timed(device, encoder, draw, Some(&mut *profiler))
4487        }
4488        .map_err(str::to_owned)?;
4489    }
4490    for item in &text.surface_quads {
4491        let draw = QuadDraw {
4492            target,
4493            depth: Some(&frame.targets.depth_view),
4494            atlas: item.atlas,
4495            quads: item.quads,
4496            space: item.space,
4497            deformers: Some(deformers),
4498            icons: item.icons,
4499        };
4500        if item.space.is_lit() {
4501            let lit = frame
4502                .lit_bindings()
4503                .ok_or("frame lighting is not prepared")?;
4504            pass.encode_quads_lit_timed(device, encoder, draw, &lit, Some(&mut *profiler))
4505        } else {
4506            pass.encode_quads_timed(device, encoder, draw, Some(&mut *profiler))
4507        }
4508        .map_err(str::to_owned)?;
4509    }
4510    Ok(())
4511}
4512
4513fn text_bounds(text: &Text<'_>, size: [u32; 2]) -> Vec<TextBounds> {
4514    let paragraphs = |list: TextList, items: &[TextItem<'_>]| {
4515        items
4516            .iter()
4517            .enumerate()
4518            .filter_map(|(index, item)| {
4519                crate::text::screen_bounds(
4520                    &item.space,
4521                    item.paragraph.glyphs.iter().map(PackedGlyph::from),
4522                    size,
4523                )
4524                .map(|rect| TextBounds {
4525                    list,
4526                    index,
4527                    id: item.id,
4528                    rect,
4529                })
4530            })
4531            .collect::<Vec<_>>()
4532    };
4533    let quads = |list: TextList, items: &[TextQuadItem<'_>]| {
4534        items
4535            .iter()
4536            .enumerate()
4537            .filter_map(|(index, item)| {
4538                crate::text::screen_bounds(
4539                    &item.space,
4540                    item.quads.iter().map(PackedGlyph::from),
4541                    size,
4542                )
4543                .map(|rect| TextBounds {
4544                    list,
4545                    index,
4546                    id: item.id,
4547                    rect,
4548                })
4549            })
4550            .collect::<Vec<_>>()
4551    };
4552    let mut bounds = paragraphs(TextList::Surface, &text.surface);
4553    bounds.extend(quads(TextList::SurfaceQuads, &text.surface_quads));
4554    bounds.extend(paragraphs(TextList::Overlay, &text.overlay));
4555    bounds.extend(quads(TextList::OverlayQuads, &text.overlay_quads));
4556    bounds
4557}
4558
4559fn readback_texture(
4560    gpu: &Gpu,
4561    texture: &wgpu::Texture,
4562    width: u32,
4563    height: u32,
4564    bytes_per_pixel: u32,
4565) -> Result<Vec<u8>, String> {
4566    let raw_row = width * bytes_per_pixel;
4567    let row =
4568        raw_row.div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT) * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
4569    let buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
4570        label: Some("live geometry readback"),
4571        size: u64::from(row) * u64::from(height),
4572        usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4573        mapped_at_creation: false,
4574    });
4575    let mut encoder = gpu
4576        .device
4577        .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4578            label: Some("live geometry readback"),
4579        });
4580    encoder.copy_texture_to_buffer(
4581        texture.as_image_copy(),
4582        wgpu::TexelCopyBufferInfo {
4583            buffer: &buffer,
4584            layout: wgpu::TexelCopyBufferLayout {
4585                offset: 0,
4586                bytes_per_row: Some(row),
4587                rows_per_image: Some(height),
4588            },
4589        },
4590        wgpu::Extent3d {
4591            width,
4592            height,
4593            depth_or_array_layers: 1,
4594        },
4595    );
4596    gpu.queue.submit(Some(encoder.finish()));
4597    let (sender, receiver) = std::sync::mpsc::channel();
4598    let slice = buffer.slice(..);
4599    slice.map_async(wgpu::MapMode::Read, move |result| {
4600        let _ = sender.send(result);
4601    });
4602    gpu.device
4603        .poll(wgpu::PollType::wait_indefinitely())
4604        .map_err(|error| format!("{error:?}"))?;
4605    receiver
4606        .recv()
4607        .map_err(|error| error.to_string())?
4608        .map_err(|error| error.to_string())?;
4609    let mapped = slice.get_mapped_range().expect("a mapped range");
4610    let mut bytes = Vec::with_capacity((raw_row * height) as usize);
4611    for padded in mapped.chunks_exact(row as usize) {
4612        bytes.extend_from_slice(&padded[..raw_row as usize]);
4613    }
4614    drop(mapped);
4615    buffer.unmap();
4616    Ok(bytes)
4617}
4618
4619#[cfg(test)]
4620mod tests {
4621    use super::*;
4622    use crate::frame::{Camera, SceneWind, Sun};
4623    use crate::sky::AnalyticSky;
4624    use pfx_geom::shapes::Shape;
4625    use pfx_gpu::OffscreenTarget;
4626    use pfx_materials::Material;
4627
4628    fn identity() -> Matrix {
4629        [
4630            [1.0, 0.0, 0.0, 0.0],
4631            [0.0, 1.0, 0.0, 0.0],
4632            [0.0, 0.0, 1.0, 0.0],
4633            [0.0, 0.0, 0.0, 1.0],
4634        ]
4635    }
4636
4637    fn perspective(aspect: f32) -> Matrix {
4638        let near = 0.1;
4639        let far = 100.0;
4640        let f = 1.0 / (40.0_f32.to_radians() * 0.5).tan();
4641        [
4642            [f / aspect, 0.0, 0.0, 0.0],
4643            [0.0, f, 0.0, 0.0],
4644            [0.0, 0.0, far / (near - far), -1.0],
4645            [0.0, 0.0, far * near / (near - far), 0.0],
4646        ]
4647    }
4648
4649    #[test]
4650    fn fixed_and_biased_exposure() {
4651        assert_eq!(Exposure::Fixed(0.93).value(4.0), 0.93);
4652        assert_eq!(Exposure::Auto { bias: 1.0 }.value(0.75), 1.5);
4653        assert_eq!(Exposure::Auto { bias: -1.0 }.value(0.75), 0.375);
4654    }
4655
4656    #[test]
4657    fn plain_passes_merge_post_and_keep_sixteen() {
4658        let mut passes = (0..16)
4659            .map(|index| PassTiming {
4660                label: format!("pass {index}"),
4661                milliseconds: index as f64,
4662            })
4663            .collect::<Vec<_>>();
4664        passes.extend(
4665            ["post 0:0", "post 1:0", "post 2:0", "post 3:0"].map(|label| PassTiming {
4666                label: label.to_owned(),
4667                milliseconds: 5.0,
4668            }),
4669        );
4670        passes.push(PassTiming {
4671            label: "volume march".into(),
4672            milliseconds: 0.2,
4673        });
4674        passes.push(PassTiming {
4675            label: "volume upsample".into(),
4676            milliseconds: 0.3,
4677        });
4678        let merged = plain_passes(passes);
4679        assert_eq!(merged.len(), 16);
4680        assert_eq!(merged[0].label, "post");
4681        assert_eq!(merged[0].milliseconds, 20.0);
4682        assert!(merged.iter().all(|pass| !pass.label.starts_with("post ")));
4683        assert!(!merged.iter().any(|pass| pass.label == "pass 0"));
4684    }
4685
4686    #[test]
4687    #[ignore = "needs a GPU; run under pgpu"]
4688    fn caller_finish_matches_gpu_chain() {
4689        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4690        let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
4691        let mut finish = Chain::new();
4692        finish.passes = vec![
4693            PostPass::Exposure(1.25),
4694            PostPass::Bloom(Bloom::neutral(0.2)),
4695            PostPass::Tone(Tone::aces()),
4696        ];
4697        renderer.set_finish(finish.clone());
4698        let mut expected = finish;
4699        expected.passes.insert(0, PostPass::Exposure(1.0));
4700        expected.passes.push(PostPass::Encode);
4701        let direct = GpuChain::new(expected, &renderer.gpu().device, 32, 32);
4702        let input = output(renderer.gpu(), 32, 32);
4703        let actual = output(renderer.gpu(), 32, 32);
4704        let reference = output(renderer.gpu(), 32, 32);
4705        let mut encoder = renderer
4706            .gpu()
4707            .device
4708            .create_command_encoder(&Default::default());
4709        {
4710            let _clear = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
4711                multiview_mask: None,
4712                label: Some("finish input"),
4713                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
4714                    view: &input.view,
4715                    resolve_target: None,
4716                    depth_slice: None,
4717                    ops: wgpu::Operations {
4718                        load: wgpu::LoadOp::Clear(wgpu::Color {
4719                            r: 1.5,
4720                            g: 0.7,
4721                            b: 0.2,
4722                            a: 1.0,
4723                        }),
4724                        store: wgpu::StoreOp::Store,
4725                    },
4726                })],
4727                depth_stencil_attachment: None,
4728                timestamp_writes: None,
4729                occlusion_query_set: None,
4730            });
4731        }
4732        renderer
4733            .post
4734            .run(&mut encoder, &input.view, None, None, &actual.view, 7, 9);
4735        direct.run(&mut encoder, &input.view, None, None, &reference.view, 7, 9);
4736        renderer.gpu().queue.submit(Some(encoder.finish()));
4737        assert_eq!(
4738            renderer.gpu().readback_rgba16(&actual).unwrap(),
4739            renderer.gpu().readback_rgba16(&reference).unwrap()
4740        );
4741    }
4742
4743    #[test]
4744    #[ignore = "needs a GPU; run under pgpu"]
4745    fn set_tape_overrides_the_finish_until_cleared() {
4746        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4747        let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
4748        assert!(renderer.post.tape().is_off());
4749        let mut finish = Chain::new();
4750        finish.passes.push(PostPass::Tone(Tone::aces()));
4751        finish.set_tape(Tape::forward(0.5));
4752        renderer.set_finish(finish);
4753        assert_eq!(renderer.post.tape(), Tape::forward(0.5));
4754        renderer.set_tape(Some(Tape::rewind(1.0)));
4755        assert_eq!(renderer.post.tape(), Tape::rewind(1.0));
4756        renderer.set_tape(None);
4757        assert_eq!(renderer.post.tape(), Tape::forward(0.5));
4758        renderer.clear_finish();
4759        renderer.set_tape(Some(Tape::rewind(0.3)));
4760        assert_eq!(renderer.post.tape(), Tape::rewind(0.3));
4761        renderer.set_tape(None);
4762        assert!(renderer.post.tape().is_off());
4763    }
4764
4765    fn sphere_still(size: u32) -> (Renderer, OffscreenTarget, Vec<Instance>, Vec<Material>) {
4766        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4767        let mut renderer = Renderer::new(gpu, size, size).unwrap();
4768        renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
4769        let shape = Shape::Ellipsoid {
4770            radius: 0.7,
4771            squash: 1.0,
4772        }
4773        .mesh(0.05);
4774        let mesh = renderer
4775            .upload_mesh(MeshData {
4776                positions: &shape.positions,
4777                normals: &shape.normals,
4778                tangents: &shape.tangents,
4779                uvs: &shape.uvs,
4780                uvs1: None,
4781                alpha: None,
4782                indices: &shape.indices,
4783            })
4784            .unwrap();
4785        let target = output(renderer.gpu(), size, size);
4786        let materials = vec![Material {
4787            base: [0.9; 3],
4788            metalness: 1.0,
4789            roughness: 0.3,
4790            ..Default::default()
4791        }];
4792        (
4793            renderer,
4794            target,
4795            vec![Instance::new(mesh, identity(), 0, 5)],
4796            materials,
4797        )
4798    }
4799
4800    fn still_scene<'a>(
4801        instances: &'a [Instance],
4802        materials: &'a [Material],
4803        number: u32,
4804    ) -> Scene<'a> {
4805        let shift = |n: u32| (n as f32 * 0.05).sin() * 0.3;
4806        let projection = perspective(1.0);
4807        let mut view = identity();
4808        view[3][0] = shift(number);
4809        view[3][2] = -3.0;
4810        let mut before = view;
4811        before[3][0] = shift(number.saturating_sub(1));
4812        Scene {
4813            camera: Camera {
4814                view,
4815                projection,
4816                previous_view_projection: frame::multiply(projection, before),
4817                position: [-view[3][0], 0.0, 3.0],
4818            },
4819            time: number as f32 / 60.0,
4820            seed: 11,
4821            sun: Sun {
4822                direction: [0.3, 0.8, 0.5],
4823                colour: [1.0; 3],
4824                intensity: 2.0,
4825            },
4826            instances,
4827            materials,
4828            deformers: &[],
4829            wind: SceneWind::default(),
4830        }
4831    }
4832
4833    fn varied_still(size: u32) -> (Renderer, OffscreenTarget, Vec<Instance>, Vec<Material>) {
4834        let (renderer, target, mut instances, materials) = sphere_still(size);
4835        let sphere = instances[0];
4836        let mut two_sided = sphere;
4837        two_sided.model = translated(-1.2, 0.0, 0.0);
4838        two_sided.previous_model = two_sided.model;
4839        two_sided.two_sided = true;
4840        two_sided.id = 6;
4841        let mut aged = sphere;
4842        aged.model = translated(1.2, 0.0, 0.0);
4843        aged.previous_model = aged.model;
4844        aged.age = 0.6;
4845        aged.id = 7;
4846        instances.extend([two_sided, aged]);
4847        (renderer, target, instances, materials)
4848    }
4849
4850    fn sorted(mut keys: Vec<OpaqueKey>) -> Vec<OpaqueKey> {
4851        keys.sort_by_key(|key| (key.features.flags(), key.lit, key.two_sided));
4852        keys
4853    }
4854
4855    fn draw_frames(
4856        renderer: &mut Renderer,
4857        target: &OffscreenTarget,
4858        instances: &[Instance],
4859        materials: &[Material],
4860        frames: u32,
4861    ) -> Vec<Vec<u16>> {
4862        (0..frames)
4863            .map(|number| {
4864                renderer
4865                    .render(
4866                        &still_scene(instances, materials, number),
4867                        &Text::default(),
4868                        &Effects::default(),
4869                        Finish::Standard,
4870                        &target.view,
4871                    )
4872                    .unwrap();
4873                renderer.gpu().readback_rgba16(target).unwrap()
4874            })
4875            .collect()
4876    }
4877
4878    #[test]
4879    #[ignore = "needs a GPU; run under pgpu"]
4880    fn warm_makes_the_opaque_pipelines_the_frames_draw_and_the_same_bytes() {
4881        let (mut warmed, target, instances, materials) = varied_still(64);
4882        let warming = warmed
4883            .warm(&[still_scene(&instances, &materials, 0)])
4884            .unwrap();
4885        let progress = warming.wait();
4886        assert!(warming.done() && warming.first_ready());
4887        assert_eq!(progress.failed, 0);
4888        assert_eq!(progress.total, 3);
4889        assert_eq!(progress.first_total, 3);
4890        assert!(warmed.frame().opaque_pipelines().len() <= 3);
4891        let warm_frames = draw_frames(&mut warmed, &target, &instances, &materials, 3);
4892        assert_eq!(warmed.frame().opaque_made_on_render_thread(), 0);
4893        let (mut cold, cold_target, cold_instances, cold_materials) = varied_still(64);
4894        let cold_frames = draw_frames(&mut cold, &cold_target, &cold_instances, &cold_materials, 3);
4895        assert_eq!(cold.frame().opaque_made_on_render_thread(), 3);
4896        assert_eq!(
4897            sorted(warmed.frame().opaque_pipelines()),
4898            sorted(cold.frame().opaque_pipelines())
4899        );
4900        for (number, (warm, cold)) in warm_frames.iter().zip(&cold_frames).enumerate() {
4901            assert!(warm == cold, "frame {number} differs after the warm");
4902        }
4903        let again = warmed
4904            .warm(&[still_scene(&instances, &materials, 3)])
4905            .unwrap();
4906        assert_eq!(again.progress().total, 0);
4907        assert!(again.done());
4908    }
4909
4910    #[test]
4911    #[ignore = "needs a GPU; run under pgpu"]
4912    fn later_scenes_warm_after_the_first_and_a_frame_waits_for_its_own() {
4913        let (mut renderer, target, instances, materials) = varied_still(64);
4914        let plain: Vec<Instance> = instances[..1].to_vec();
4915        renderer.set_warm_workers(1);
4916        let warming = renderer
4917            .warm(&[
4918                still_scene(&plain, &materials, 0),
4919                still_scene(&instances, &materials, 0),
4920            ])
4921            .unwrap();
4922        let started = warming.progress();
4923        assert_eq!((started.first_total, started.total), (1, 3));
4924        draw_frames(&mut renderer, &target, &plain, &materials, 1);
4925        assert_eq!(renderer.frame().opaque_made_on_render_thread(), 0);
4926        assert!(warming.first_ready());
4927        warming.wait();
4928        draw_frames(&mut renderer, &target, &instances, &materials, 2);
4929        assert_eq!(renderer.frame().opaque_made_on_render_thread(), 0);
4930        assert_eq!(renderer.frame().opaque_pipelines().len(), 3);
4931    }
4932
4933    #[test]
4934    #[ignore = "needs a GPU; run under pgpu"]
4935    fn the_first_frame_draws_the_same_bytes_through_the_reflection_pipelines() {
4936        let ssr = frame::OpaqueFeatures::named("ssr").unwrap();
4937        let (mut merged, target, instances, materials) = varied_still(64);
4938        let (mut split, split_target, split_instances, split_materials) = varied_still(64);
4939        split.frame().set_opaque_off(ssr);
4940        let first = draw_frames(&mut merged, &target, &instances, &materials, 1);
4941        let before = draw_frames(
4942            &mut split,
4943            &split_target,
4944            &split_instances,
4945            &split_materials,
4946            1,
4947        );
4948        assert!(merged.frame().opaque_drawn().iter().all(|f| f.ssr));
4949        assert!(split.frame().opaque_drawn().iter().all(|f| !f.ssr));
4950        assert!(first[0] == before[0], "the first frame changed");
4951        split.frame().set_opaque_off(frame::OpaqueFeatures::NONE);
4952        let made = merged.frame().opaque_made_on_render_thread();
4953        for number in 1..3 {
4954            let scene = still_scene(&instances, &materials, number);
4955            let split_scene = still_scene(&split_instances, &split_materials, number);
4956            for (renderer, scene, target) in [
4957                (&mut merged, &scene, &target),
4958                (&mut split, &split_scene, &split_target),
4959            ] {
4960                renderer
4961                    .render(
4962                        scene,
4963                        &Text::default(),
4964                        &Effects::default(),
4965                        Finish::Standard,
4966                        &target.view,
4967                    )
4968                    .unwrap();
4969            }
4970            assert!(
4971                merged.gpu().readback_rgba16(&target).unwrap()
4972                    == split.gpu().readback_rgba16(&split_target).unwrap(),
4973                "frame {number} differs"
4974            );
4975        }
4976        assert_eq!(merged.frame().opaque_made_on_render_thread(), made);
4977    }
4978
4979    fn cache_path(name: &str) -> std::path::PathBuf {
4980        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
4981            .join("../../tmp/pipeline-cache-tests")
4982            .join(name);
4983        let _ = std::fs::remove_file(&path);
4984        path
4985    }
4986
4987    fn caches(gpu: &Gpu) -> bool {
4988        gpu.device
4989            .features()
4990            .contains(wgpu::Features::PIPELINE_CACHE)
4991            && crate::pipeline_cache::cache_key(&gpu.info).is_some()
4992    }
4993
4994    #[test]
4995    #[ignore = "needs a GPU; run under pgpu"]
4996    fn a_pipeline_cache_round_trips_where_the_backend_keeps_one() {
4997        let path = cache_path("round-trip.bin");
4998        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4999        let supported = caches(&gpu);
5000        let mut renderer = Renderer::new(gpu, 32, 32).unwrap();
5001        let load = renderer.use_pipeline_cache(&path);
5002        if !supported {
5003            assert_eq!(load, CacheLoad::Unsupported);
5004            assert_eq!(renderer.save_pipeline_cache(), Ok(None));
5005            return;
5006        }
5007        assert_eq!(load, CacheLoad::Fresh);
5008        renderer.warm_opaque(frame::OpaqueFeatures::NONE, false);
5009        let saved = renderer.save_pipeline_cache().unwrap().unwrap();
5010        assert!(saved > 0);
5011        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5012        let mut again = Renderer::new(gpu, 32, 32).unwrap();
5013        assert_eq!(
5014            again.use_pipeline_cache(&path),
5015            CacheLoad::Loaded { bytes: saved }
5016        );
5017        again.warm_opaque(frame::OpaqueFeatures::NONE, false);
5018        let made = again.frame().opaque_made_on_render_thread();
5019        let pipelines = again.frame().opaque_pipelines().len();
5020        assert_eq!(made, pipelines);
5021        assert!(pipelines > 0);
5022        again.warm_opaque(frame::OpaqueFeatures::NONE, false);
5023        assert_eq!(again.frame().opaque_made_on_render_thread(), made);
5024        assert_eq!(again.frame().opaque_pipelines().len(), pipelines);
5025        let second = again.save_pipeline_cache().unwrap().unwrap();
5026        assert!(second > 0);
5027        assert_eq!(again.save_pipeline_cache().unwrap().unwrap(), second);
5028        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5029        let mut third = Renderer::new(gpu, 32, 32).unwrap();
5030        assert_eq!(
5031            third.use_pipeline_cache(&path),
5032            CacheLoad::Loaded { bytes: second }
5033        );
5034    }
5035
5036    #[test]
5037    #[ignore = "needs a GPU; run under pgpu"]
5038    fn an_invalid_pipeline_cache_is_ignored_and_replaced() {
5039        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5040        if !caches(&gpu) {
5041            return;
5042        }
5043        let key = crate::pipeline_cache::cache_key(&gpu.info).unwrap();
5044        let (mut renderer, target, instances, materials) = sphere_still(32);
5045        for (name, bytes) in [
5046            ("garbage.bin", b"not a cache at all".to_vec()),
5047            (
5048                "foreign.bin",
5049                crate::pipeline_cache::wrap(&key.replace("pito-engine", "older engine"), b"x"),
5050            ),
5051            ("damaged.bin", {
5052                let mut bytes = crate::pipeline_cache::wrap(&key, b"pipelines");
5053                let last = bytes.len() - 1;
5054                bytes[last] ^= 0x40;
5055                bytes
5056            }),
5057        ] {
5058            let path = cache_path(name);
5059            std::fs::create_dir_all(path.parent().unwrap()).unwrap();
5060            std::fs::write(&path, bytes).unwrap();
5061            assert!(
5062                matches!(renderer.use_pipeline_cache(&path), CacheLoad::Ignored(_)),
5063                "{name} was not ignored"
5064            );
5065        }
5066        draw_frames(&mut renderer, &target, &instances, &materials, 1);
5067        let saved = renderer.save_pipeline_cache().unwrap().unwrap();
5068        let path = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
5069            .join("../../tmp/pipeline-cache-tests/damaged.bin");
5070        assert_eq!(
5071            renderer.use_pipeline_cache(&path),
5072            CacheLoad::Loaded { bytes: saved }
5073        );
5074        draw_frames(&mut renderer, &target, &instances, &materials, 1);
5075    }
5076
5077    #[test]
5078    #[ignore = "needs a GPU; run under pgpu"]
5079    fn submitted_frames_match_blocking_frames_and_timings_arrive_late() {
5080        let frames = 120;
5081        let (mut blocking, blocking_target, instances, materials) = sphere_still(96);
5082        let mut timed = 0;
5083        for number in 0..frames {
5084            let timings = blocking
5085                .render(
5086                    &still_scene(&instances, &materials, number),
5087                    &Text::default(),
5088                    &Effects::default(),
5089                    Finish::Standard,
5090                    &blocking_target.view,
5091                )
5092                .unwrap();
5093            if !timings.is_empty() {
5094                timed += 1;
5095                assert_eq!(blocking.timings().unwrap().frame, u64::from(number));
5096                assert_eq!(blocking.timings().unwrap().passes, timings);
5097            }
5098        }
5099        let expected = blocking.gpu().readback_rgba16(&blocking_target).unwrap();
5100        let (mut renderer, target, instances, materials) = sphere_still(96);
5101        let mut arrived = std::collections::BTreeMap::new();
5102        let mut lags = [0u32; 3];
5103        let mut most_in_flight = 0;
5104        for number in 0..frames {
5105            let submitted = renderer
5106                .submit(
5107                    &still_scene(&instances, &materials, number),
5108                    &Text::default(),
5109                    &Effects::default(),
5110                    Finish::Standard,
5111                    &target.view,
5112                )
5113                .unwrap();
5114            assert_eq!(submitted.frame, u64::from(number));
5115            most_in_flight = most_in_flight.max(renderer.frames_in_flight());
5116            for timings in submitted.arrived {
5117                let lag = submitted.frame - timings.frame;
5118                assert!(
5119                    lag <= TIMING_LAG,
5120                    "frame {} arrived {lag} late",
5121                    timings.frame
5122                );
5123                lags[lag as usize] += 1;
5124                assert!(!timings.passes.is_empty());
5125                assert!(arrived.insert(timings.frame, timings).is_none());
5126            }
5127            if let Some(latest) = renderer.timings() {
5128                assert_eq!(Some(&latest.frame), arrived.keys().last());
5129            }
5130        }
5131        for timings in renderer.wait_frames(0).unwrap() {
5132            assert!(u64::from(frames - 1) - timings.frame <= TIMING_LAG);
5133            assert!(arrived.insert(timings.frame, timings).is_none());
5134        }
5135        assert_eq!(renderer.frames_in_flight(), 0);
5136        let drops = renderer.timing_drops();
5137        println!(
5138            "{frames} submitted frames: {} timings arrived (lag 0: {}, 1: {}, 2: {}), dropped {} with the ring full and {} late; at most {most_in_flight} frames in flight; {timed} blocking frames timed",
5139            arrived.len(),
5140            lags[0],
5141            lags[1],
5142            lags[2],
5143            drops.ring,
5144            drops.late
5145        );
5146        if timed > 0 {
5147            assert_eq!(
5148                arrived.len() as u64 + drops.ring + drops.late,
5149                u64::from(frames)
5150            );
5151            assert_eq!(
5152                renderer.timings().map(|latest| latest.frame),
5153                arrived.keys().last().copied()
5154            );
5155        }
5156        let actual = renderer.gpu().readback_rgba16(&target).unwrap();
5157        assert!(
5158            actual == expected,
5159            "submitted frames differ from rendered ones"
5160        );
5161        if timed == 0 {
5162            return;
5163        }
5164        let mut paced = Vec::new();
5165        for number in frames..frames + 60 {
5166            let submitted = renderer
5167                .submit(
5168                    &still_scene(&instances, &materials, number),
5169                    &Text::default(),
5170                    &Effects::default(),
5171                    Finish::Standard,
5172                    &target.view,
5173                )
5174                .unwrap();
5175            for timings in &submitted.arrived {
5176                assert!(submitted.frame - timings.frame <= TIMING_LAG);
5177            }
5178            paced.extend(submitted.arrived);
5179            for timings in renderer.wait_frames(2).unwrap() {
5180                assert!(submitted.frame - timings.frame <= TIMING_LAG);
5181                paced.push(timings);
5182            }
5183            assert!(renderer.frames_in_flight() <= 2);
5184        }
5185        paced.extend(renderer.wait_frames(0).unwrap());
5186        println!(
5187            "60 frames paced to two in flight: {} timings arrived, drops {:?}",
5188            paced.len(),
5189            renderer.timing_drops()
5190        );
5191        assert_eq!(
5192            paced
5193                .iter()
5194                .map(|timings| timings.frame)
5195                .collect::<Vec<_>>(),
5196            (u64::from(frames)..u64::from(frames) + 60).collect::<Vec<_>>()
5197        );
5198        assert_eq!(renderer.timing_drops(), drops);
5199    }
5200
5201    #[test]
5202    #[ignore = "needs a GPU; run under pgpu"]
5203    fn a_scene_without_a_volume_is_the_same_bytes_under_any_volume_cuts() {
5204        let mut shots = Vec::new();
5205        for cuts in [
5206            effects::VolumeCuts::default(),
5207            effects::VolumeCuts::FULL,
5208            effects::VolumeCuts {
5209                steps: 6,
5210                warp_octaves: 1,
5211                octaves: 1,
5212                early_out: true,
5213            },
5214        ] {
5215            let (mut renderer, target, instances, materials) = sphere_still(96);
5216            renderer.set_volume_cuts(cuts);
5217            assert_eq!(renderer.volume_cuts(), cuts);
5218            for number in 0..6 {
5219                renderer
5220                    .render(
5221                        &still_scene(&instances, &materials, number),
5222                        &Text::default(),
5223                        &Effects::default(),
5224                        Finish::Standard,
5225                        &target.view,
5226                    )
5227                    .unwrap();
5228            }
5229            assert!(!renderer.last_pass_order().contains(&"volume march"));
5230            shots.push(renderer.gpu().readback_rgba16(&target).unwrap());
5231        }
5232        assert!(shots[0] == shots[1] && shots[1] == shots[2]);
5233    }
5234
5235    #[test]
5236    #[ignore = "needs a GPU; run under pgpu"]
5237    fn a_stepped_sky_swaps_in_whole_and_renders_as_the_settled_one() {
5238        let old = AnalyticSky::new(
5239            pfx_core::daylight::Daylight {
5240                hour: 9.0,
5241                day: 172.0,
5242                latitude: 45.0,
5243                heading: 180.0,
5244            },
5245            pfx_core::daylight::REFERENCE_HOUR,
5246            2.5,
5247            [0.2, 0.25, 0.2],
5248        );
5249        let sky = |hour: f64| {
5250            SkySource::Analytic(AnalyticSky::new(
5251                pfx_core::daylight::Daylight {
5252                    hour,
5253                    day: 172.0,
5254                    latitude: 45.0,
5255                    heading: 180.0,
5256                },
5257                pfx_core::daylight::REFERENCE_HOUR,
5258                2.5,
5259                [0.2, 0.25, 0.2],
5260            ))
5261        };
5262        let sh_bits = |r: &Renderer| {
5263            r.frame()
5264                .sky()
5265                .lighting
5266                .sh
5267                .map(|band| band.map(f32::to_bits))
5268        };
5269        let (mut settled, settled_target, instances, materials) = sphere_still(64);
5270        settled.set_sky(SkySource::Analytic(old), 9.0).unwrap();
5271        let blocking = std::time::Instant::now();
5272        settled.set_sky(sky(15.0), 15.0).unwrap();
5273        let blocking_ms = blocking.elapsed().as_secs_f64() * 1000.0;
5274        let (mut stepped, stepped_target, _, _) = sphere_still(64);
5275        stepped.set_sky(SkySource::Analytic(old), 9.0).unwrap();
5276        let before = sh_bits(&stepped);
5277        let mut calls = Vec::new();
5278        let mut last = 0;
5279        for index in 0..20 {
5280            let started = std::time::Instant::now();
5281            last = stepped
5282                .request_sky(sky(10.0 + index as f64 * 0.25), 10.0 + index as f32 * 0.25)
5283                .unwrap();
5284            calls.push(started.elapsed().as_secs_f64() * 1000.0);
5285        }
5286        let started = std::time::Instant::now();
5287        let newest = stepped.request_sky(sky(15.0), 15.0).unwrap();
5288        calls.push(started.elapsed().as_secs_f64() * 1000.0);
5289        assert!(newest > last);
5290        assert!(stepped.sky_settling());
5291        let after = sh_bits(&settled);
5292        let mut number = 0;
5293        let mut whole = true;
5294        let settling = std::time::Instant::now();
5295        while stepped.sky_settling() && number < 600 {
5296            stepped
5297                .render(
5298                    &still_scene(&instances, &materials, number),
5299                    &Text::default(),
5300                    &Effects::default(),
5301                    Finish::Standard,
5302                    &stepped_target.view,
5303                )
5304                .unwrap();
5305            let now = sh_bits(&stepped);
5306            whole &= now == before || now == after;
5307            number += 1;
5308        }
5309        if stepped.sky_settling() {
5310            stepped.wait_sky().unwrap();
5311        }
5312        let settled_ms = settling.elapsed().as_secs_f64() * 1000.0;
5313        assert!(
5314            whole,
5315            "a frame showed a sky that was neither the old nor the new one"
5316        );
5317        assert_eq!(sh_bits(&stepped), after);
5318        assert_eq!(stepped.frame().sky().shown(), newest);
5319        assert_eq!(stepped.hour, 15.0);
5320        for number in 1000..1008 {
5321            for (renderer, target) in [
5322                (&mut settled, &settled_target),
5323                (&mut stepped, &stepped_target),
5324            ] {
5325                renderer
5326                    .render(
5327                        &still_scene(&instances, &materials, number),
5328                        &Text::default(),
5329                        &Effects::default(),
5330                        Finish::Standard,
5331                        &target.view,
5332                    )
5333                    .unwrap();
5334            }
5335        }
5336        let mean = calls.iter().sum::<f64>() / calls.len() as f64;
5337        let most = calls.iter().copied().fold(0.0, f64::max);
5338        println!(
5339            "request_sky on the calling thread: mean {mean:.4} ms, max {most:.4} ms over {} calls; the new sky swapped in after {number} frames and {settled_ms:.1} ms; set_sky with settle blocked {blocking_ms:.1} ms",
5340            calls.len()
5341        );
5342        assert!(mean < 1.0);
5343        assert!(
5344            settled.gpu().readback_rgba16(&settled_target).unwrap()
5345                == stepped.gpu().readback_rgba16(&stepped_target).unwrap(),
5346            "the stepped sky renders differently from the settled one"
5347        );
5348    }
5349
5350    #[test]
5351    #[ignore = "needs a GPU; run under pgpu"]
5352    fn pick_returns_id_and_depth_after_submission() {
5353        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5354        let mut renderer = Renderer::new(gpu, 64, 64).unwrap();
5355        renderer.shadows = Shadows::new(
5356            &renderer.gpu().device,
5357            Quality {
5358                resolution: 128,
5359                ..Default::default()
5360            },
5361        );
5362        let shape = Shape::RoundBox {
5363            half: [0.8; 3],
5364            radius: 0.05,
5365        }
5366        .mesh(0.2);
5367        let mesh = renderer
5368            .upload_mesh(MeshData {
5369                positions: &shape.positions,
5370                normals: &shape.normals,
5371                tangents: &shape.tangents,
5372                uvs: &shape.uvs,
5373                uvs1: None,
5374                alpha: None,
5375                indices: &shape.indices,
5376            })
5377            .unwrap();
5378        let instances = [Instance::new(mesh, identity(), 0, 37)];
5379        let mut view = identity();
5380        view[3][2] = -4.0;
5381        let projection = perspective(1.0);
5382        let camera = Camera {
5383            view,
5384            projection,
5385            previous_view_projection: frame::multiply(projection, view),
5386            position: [0.0, 0.0, 4.0],
5387        };
5388        let materials = [Material::default()];
5389        let target = output(renderer.gpu(), 64, 64);
5390        renderer.pick(32, 32).unwrap();
5391        let mut picked = None;
5392        for frame_number in 0..20 {
5393            let scene = Scene {
5394                camera,
5395                time: frame_number as f32 / 60.0,
5396                seed: 3,
5397                sun: Sun {
5398                    direction: [0.0, 1.0, 0.0],
5399                    colour: [1.0; 3],
5400                    intensity: 2.0,
5401                },
5402                instances: &instances,
5403                materials: &materials,
5404                deformers: &[],
5405                wind: SceneWind::default(),
5406            };
5407            renderer
5408                .render(
5409                    &scene,
5410                    &Text::default(),
5411                    &Effects::default(),
5412                    Finish::Standard,
5413                    &target.view,
5414                )
5415                .unwrap();
5416            picked = renderer.picked();
5417            if picked.is_some() {
5418                break;
5419            }
5420        }
5421        let picked = picked.expect("pick did not complete within twenty submitted frames");
5422        assert_eq!((picked.x, picked.y, picked.id), (32, 32, 37));
5423        assert!(picked.depth.is_finite() && picked.depth > 0.0 && picked.depth < 100.0);
5424    }
5425
5426    #[test]
5427    #[ignore = "needs a GPU; run under pgpu"]
5428    fn creature_depth_reaches_atlas_after_renderer_render() {
5429        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5430        let mut renderer = Renderer::new(gpu, 64, 64).unwrap();
5431        renderer.set_shadow_quality(Quality {
5432            resolution: 64,
5433            receiver: Some(crate::shadow::ReceiverBox {
5434                min: [-1.0, -0.1, -1.0],
5435                max: [1.0, 2.0, 1.0],
5436            }),
5437            caster_margin: 2.0,
5438            ..Quality::default()
5439        });
5440        let mesh = CreatureMesh::new(
5441            &renderer.gpu().device,
5442            &[
5443                effects::CreatureVertex {
5444                    position: [-0.4, 0.0, -0.4, 0.0],
5445                },
5446                effects::CreatureVertex {
5447                    position: [0.4, 0.0, -0.4, 0.0],
5448                },
5449                effects::CreatureVertex {
5450                    position: [0.0, 0.0, 0.4, 0.0],
5451                },
5452            ],
5453            &[0, 1, 2],
5454        );
5455        let bird = [CreatureInstance {
5456            position_scale: [0.0, 1.0, 0.0, 1.0],
5457            right: [1.0, 0.0, 0.0, 0.0],
5458            up: [0.0, 1.0, 0.0, 0.0],
5459            forward: [0.0, 0.0, 1.0, 0.0],
5460            motion: [0.0; 4],
5461        }];
5462        let mut view = identity();
5463        view[3][2] = -4.0;
5464        let projection = perspective(1.0);
5465        let scene = Scene {
5466            camera: Camera {
5467                view,
5468                projection,
5469                previous_view_projection: frame::multiply(projection, view),
5470                position: [0.0, 0.0, 4.0],
5471            },
5472            time: 0.0,
5473            seed: 5,
5474            sun: Sun {
5475                direction: [0.0, 1.0, 0.0],
5476                colour: [1.0; 3],
5477                intensity: 2.0,
5478            },
5479            instances: &[],
5480            materials: &[Material::default()],
5481            deformers: &[],
5482            wind: SceneWind::default(),
5483        };
5484        let output = output(renderer.gpu(), 64, 64);
5485        renderer
5486            .render(
5487                &scene,
5488                &Text::default(),
5489                &Effects {
5490                    creatures: &bird,
5491                    creature_mesh: Some(&mesh),
5492                    ..Default::default()
5493                },
5494                Finish::Standard,
5495                &output.view,
5496            )
5497            .unwrap();
5498        assert!(renderer.last_pass_order().contains(&"creature shadows"));
5499        let readback = renderer
5500            .gpu()
5501            .device
5502            .create_buffer(&wgpu::BufferDescriptor {
5503                label: Some("creature shadow readback"),
5504                size: 64 * 64 * 4 * crate::shadow::CASCADE_COUNT as u64,
5505                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5506                mapped_at_creation: false,
5507            });
5508        let mut encoder = renderer
5509            .gpu()
5510            .device
5511            .create_command_encoder(&Default::default());
5512        encoder.copy_texture_to_buffer(
5513            wgpu::TexelCopyTextureInfo {
5514                texture: renderer.shadows().atlas(),
5515                mip_level: 0,
5516                origin: wgpu::Origin3d::ZERO,
5517                aspect: wgpu::TextureAspect::DepthOnly,
5518            },
5519            wgpu::TexelCopyBufferInfo {
5520                buffer: &readback,
5521                layout: wgpu::TexelCopyBufferLayout {
5522                    offset: 0,
5523                    bytes_per_row: Some(256),
5524                    rows_per_image: Some(64),
5525                },
5526            },
5527            wgpu::Extent3d {
5528                width: 64,
5529                height: 64,
5530                depth_or_array_layers: crate::shadow::CASCADE_COUNT as u32,
5531            },
5532        );
5533        renderer.gpu().queue.submit(Some(encoder.finish()));
5534        let (send, receive) = std::sync::mpsc::channel();
5535        readback
5536            .slice(..)
5537            .map_async(wgpu::MapMode::Read, move |result| {
5538                send.send(result).unwrap();
5539            });
5540        renderer
5541            .gpu()
5542            .device
5543            .poll(wgpu::PollType::wait_indefinitely())
5544            .unwrap();
5545        receive.recv().unwrap().unwrap();
5546        let bytes = readback
5547            .slice(..)
5548            .get_mapped_range()
5549            .expect("a mapped range");
5550        assert!(
5551            bytes
5552                .chunks_exact(4)
5553                .any(|pixel| { f32::from_le_bytes(pixel.try_into().unwrap()) < 0.999 })
5554        );
5555    }
5556
5557    #[test]
5558    #[ignore = "needs a GPU; run under pgpu"]
5559    fn prop_shadow_reaches_cascade_atlas_after_renderer_render() {
5560        use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
5561        use pfx_bake::{Anchor, BakeScene};
5562        use pfx_load::Sky;
5563
5564        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5565        let shape = Shape::RoundBox {
5566            half: [0.5; 3],
5567            radius: 0.04,
5568        }
5569        .mesh(0.03);
5570        let triangles = triangles(&shape, [0.0; 3], 0);
5571        let bounds = bounds(&triangles, &[]).unwrap();
5572        let sky = Sky {
5573            width: 4,
5574            height: 2,
5575            texels: vec![[0.5; 4]; 8],
5576        };
5577        let baked = BakeScene {
5578            triangles,
5579            shapes: Vec::new(),
5580            materials: vec![Material::default()],
5581            anchors: vec![Anchor {
5582                hour: 12.0,
5583                sky: sky.clone(),
5584                sun: pfx_trace::Sun {
5585                    direction: [0.0, 1.0, 0.0],
5586                    color: [1.0; 3],
5587                    intensity: 2.0,
5588                },
5589            }],
5590        };
5591        let artifact = bake_prop(
5592            &gpu,
5593            &baked,
5594            &PropSpec {
5595                name: "shadow-test".into(),
5596                node: Some("box".into()),
5597                file: None,
5598                frames_per_side: 8,
5599                atlas_size: 256,
5600                hemisphere: false,
5601                anchors: vec![12.0],
5602            },
5603            bounds,
5604            1,
5605            5,
5606        )
5607        .unwrap();
5608        let mut renderer = Renderer::new(gpu, 160, 160).unwrap();
5609        renderer.shadows = Shadows::new(
5610            &renderer.gpu().device,
5611            Quality {
5612                resolution: 128,
5613                ..Default::default()
5614            },
5615        );
5616        renderer.set_sky(SkySource::Hdr(sky), 12.0).unwrap();
5617        let props = ImpostorPass::new(
5618            renderer.gpu(),
5619            artifact,
5620            wgpu::TextureFormat::Rgba16Float,
5621            wgpu::TextureFormat::Depth32Float,
5622            1,
5623        )
5624        .unwrap();
5625        renderer.set_props(Some(props));
5626        let output = output(renderer.gpu(), 160, 160);
5627        let mut view = identity();
5628        view[3][2] = -4.0;
5629        let projection = perspective(1.0);
5630        let camera = Camera {
5631            view,
5632            projection,
5633            previous_view_projection: frame::multiply(projection, view),
5634            position: [0.0, 0.0, 4.0],
5635        };
5636        let materials = [Material::default()];
5637        let scene = Scene {
5638            camera,
5639            time: 0.0,
5640            seed: 5,
5641            sun: Sun {
5642                direction: [0.0, 1.0, 0.0],
5643                colour: [1.0; 3],
5644                intensity: 2.0,
5645            },
5646            instances: &[],
5647            materials: &materials,
5648            deformers: &[],
5649            wind: SceneWind::default(),
5650        };
5651        let item = [ImpostorInstance::new(bounds.center, bounds.radius, 1, 0.0).unwrap()];
5652        renderer
5653            .render(
5654                &scene,
5655                &Text::default(),
5656                &Effects {
5657                    props: &item,
5658                    ..Default::default()
5659                },
5660                Finish::Standard,
5661                &output.view,
5662            )
5663            .unwrap();
5664        assert!(renderer.last_pass_order().contains(&"impostor shadows"));
5665        let read_mask = |renderer: &Renderer| {
5666            let size = 128u32;
5667            let buffer = renderer
5668                .gpu()
5669                .device
5670                .create_buffer(&wgpu::BufferDescriptor {
5671                    label: Some("renderer shadow readback"),
5672                    size: u64::from(size * size * 4 * crate::shadow::CASCADE_COUNT as u32),
5673                    usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
5674                    mapped_at_creation: false,
5675                });
5676            let mut encoder = renderer
5677                .gpu()
5678                .device
5679                .create_command_encoder(&Default::default());
5680            encoder.copy_texture_to_buffer(
5681                wgpu::TexelCopyTextureInfo {
5682                    texture: renderer.shadows.atlas(),
5683                    mip_level: 0,
5684                    origin: wgpu::Origin3d::ZERO,
5685                    aspect: wgpu::TextureAspect::DepthOnly,
5686                },
5687                wgpu::TexelCopyBufferInfo {
5688                    buffer: &buffer,
5689                    layout: wgpu::TexelCopyBufferLayout {
5690                        offset: 0,
5691                        bytes_per_row: Some(size * 4),
5692                        rows_per_image: Some(size),
5693                    },
5694                },
5695                wgpu::Extent3d {
5696                    width: size,
5697                    height: size,
5698                    depth_or_array_layers: crate::shadow::CASCADE_COUNT as u32,
5699                },
5700            );
5701            renderer.gpu().queue.submit(Some(encoder.finish()));
5702            let (send, receive) = std::sync::mpsc::channel();
5703            buffer
5704                .slice(..)
5705                .map_async(wgpu::MapMode::Read, move |result| {
5706                    send.send(result).unwrap();
5707                });
5708            renderer
5709                .gpu()
5710                .device
5711                .poll(wgpu::PollType::wait_indefinitely())
5712                .unwrap();
5713            receive.recv().unwrap().unwrap();
5714            let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
5715            let mask = mapped
5716                .chunks_exact(4)
5717                .map(|bytes| f32::from_le_bytes(bytes.try_into().unwrap()) < 0.999)
5718                .collect::<Vec<_>>();
5719            drop(mapped);
5720            buffer.unmap();
5721            mask
5722        };
5723        let full = read_mask(&renderer);
5724        assert!(full.iter().any(|&covered| covered));
5725        let mesh = renderer
5726            .upload_mesh(MeshData {
5727                positions: &shape.positions,
5728                normals: &shape.normals,
5729                tangents: &shape.tangents,
5730                uvs: &shape.uvs,
5731                uvs1: None,
5732                alpha: None,
5733                indices: &shape.indices,
5734            })
5735            .unwrap();
5736        let mut mesh_instance = Instance::new(mesh, identity(), 0, 1);
5737        mesh_instance.coverage = 0.5;
5738        let instances = [mesh_instance];
5739        let mixed_scene = Scene {
5740            time: 1.0 / 60.0,
5741            instances: &instances,
5742            ..scene
5743        };
5744        let mixed_prop = [ImpostorInstance::new(bounds.center, bounds.radius, 1, 0.5).unwrap()];
5745        renderer
5746            .render(
5747                &mixed_scene,
5748                &Text::default(),
5749                &Effects {
5750                    props: &mixed_prop,
5751                    ..Default::default()
5752                },
5753                Finish::Standard,
5754                &output.view,
5755            )
5756            .unwrap();
5757        let mixed = read_mask(&renderer);
5758        let overlap = full.iter().zip(&mixed).filter(|(a, b)| **a && **b).count();
5759        let union = full.iter().zip(&mixed).filter(|(a, b)| **a || **b).count();
5760        assert!(
5761            union > 0 && overlap * 10 >= union * 7,
5762            "crossfade shadow footprint {overlap}/{union}"
5763        );
5764    }
5765
5766    #[test]
5767    fn history_resets_on_a_cut() {
5768        let a = perspective(1.5);
5769        let mut b = a;
5770        b[3][0] = 0.4;
5771        let frame = |n: f32| n / 60.0;
5772        let mut history = History::default();
5773        assert!(!history.advance(frame(0.0), a, a));
5774        assert!(history.advance(frame(1.0), a, a));
5775        assert!(history.advance(frame(2.0), a, b));
5776        assert!(history.advance(frame(3.0), b, b));
5777        assert!(
5778            !history.advance(frame(4.0), a, a),
5779            "a camera that does not chain is a cut"
5780        );
5781        assert!(history.advance(frame(5.0), a, a));
5782        assert!(
5783            !history.advance(frame(30.0), a, a),
5784            "a jump in time is a cut"
5785        );
5786        assert!(history.advance(frame(31.0), a, a));
5787        history.invalidate();
5788        assert!(
5789            !history.advance(frame(32.0), a, a),
5790            "a resize or daylight jump resets"
5791        );
5792        assert!(history.advance(frame(33.0), a, a));
5793        assert_eq!(history.previous_time(), Some(frame(33.0)));
5794    }
5795
5796    #[test]
5797    fn history_follows_a_scripted_clock() {
5798        let a = perspective(1.5);
5799        let mut history = History::default();
5800        assert!(!history.advance(0.0, a, a));
5801        for step in 1..=30 {
5802            assert!(
5803                history.advance(step as f32 / 30.0, a, a),
5804                "a 30 fps clip keeps its history"
5805            );
5806        }
5807        assert!(history.advance(1.0, a, a), "a paused clock keeps it");
5808        assert!(
5809            history.advance(1.0 + 4.0 / 30.0, a, a),
5810            "a fast clip keeps it"
5811        );
5812        assert!(
5813            history.advance(1.2, a, a),
5814            "a dropped window frame keeps it"
5815        );
5816        assert!(!history.advance(1.1, a, a), "time going back is a cut");
5817        history.set_max_gap(0.05);
5818        assert!(
5819            !history.advance(1.2, a, a),
5820            "a gap over the setting is a cut"
5821        );
5822        assert!(history.advance(1.25, a, a));
5823        history.set_max_gap(f32::NAN);
5824        assert_eq!(history.max_gap(), HISTORY_GAP);
5825    }
5826
5827    #[test]
5828    fn inverse_and_fullscreen_shaders() {
5829        let mut view = identity();
5830        view[3] = [0.3, -0.2, -4.0, 1.0];
5831        let matrix = frame::multiply(perspective(1.7), view);
5832        let inverse = invert(matrix).unwrap();
5833        let product = frame::multiply(matrix, inverse);
5834        for (c, column) in product.iter().enumerate() {
5835            for (r, value) in column.iter().enumerate() {
5836                let expected = if c == r { 1.0 } else { 0.0 };
5837                assert!((value - expected).abs() < 1e-4, "{product:?}");
5838            }
5839        }
5840        assert!(invert([[0.0; 4]; 4]).is_none());
5841        for source in [motion_source(), downsample_source()] {
5842            let module = naga::front::wgsl::parse_str(&source).unwrap();
5843            naga::valid::Validator::new(
5844                naga::valid::ValidationFlags::all(),
5845                naga::valid::Capabilities::all(),
5846            )
5847            .validate(&module)
5848            .unwrap();
5849        }
5850    }
5851
5852    fn output(gpu: &Gpu, width: u32, height: u32) -> OffscreenTarget {
5853        let format = wgpu::TextureFormat::Rgba16Float;
5854        let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
5855            label: Some("prop output"),
5856            size: wgpu::Extent3d {
5857                width,
5858                height,
5859                depth_or_array_layers: 1,
5860            },
5861            mip_level_count: 1,
5862            sample_count: 1,
5863            dimension: wgpu::TextureDimension::D2,
5864            format,
5865            usage: wgpu::TextureUsages::RENDER_ATTACHMENT
5866                | wgpu::TextureUsages::TEXTURE_BINDING
5867                | wgpu::TextureUsages::STORAGE_BINDING
5868                | wgpu::TextureUsages::COPY_SRC,
5869            view_formats: &[],
5870        });
5871        let view = texture.create_view(&Default::default());
5872        OffscreenTarget {
5873            texture,
5874            view,
5875            format,
5876            width,
5877            height,
5878        }
5879    }
5880
5881    fn triangles(mesh: &Mesh, offset: [f32; 3], material: u32) -> Vec<pfx_trace::bvh::Triangle> {
5882        mesh.indices
5883            .chunks_exact(3)
5884            .map(|tri| pfx_trace::bvh::Triangle {
5885                vertices: [tri[0], tri[1], tri[2]].map(|index| {
5886                    [0, 1, 2].map(|axis| mesh.positions[index as usize][axis] + offset[axis])
5887                }),
5888                material,
5889            })
5890            .collect()
5891    }
5892
5893    #[test]
5894    #[ignore = "needs a GPU; run under pgpu"]
5895    fn prop_matches_its_mesh_under_the_sky() {
5896        use pfx_bake::impostor::{PropSpec, bake_prop, bounds};
5897        use pfx_bake::{Anchor, BakeScene};
5898        use pfx_load::Sky;
5899        let width = 3840;
5900        let height = 2160;
5901        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5902        let box_mesh = Shape::RoundBox {
5903            half: [0.8, 0.4, 0.6],
5904            radius: 0.12,
5905        }
5906        .mesh(0.004);
5907        let sphere_mesh = Shape::Ellipsoid {
5908            radius: 0.24,
5909            squash: 1.0,
5910        }
5911        .mesh(0.004);
5912        let sphere_offset = [0.0, 0.62, 0.0];
5913        let mut tris = triangles(&box_mesh, [0.0; 3], 0);
5914        tris.extend(triangles(&sphere_mesh, sphere_offset, 1));
5915        let materials = vec![
5916            Material {
5917                base: [0.75, 0.16, 0.08],
5918                roughness: 0.7,
5919                ..Default::default()
5920            },
5921            Material {
5922                base: [0.9; 3],
5923                metalness: 1.0,
5924                roughness: 0.22,
5925                ..Default::default()
5926            },
5927        ];
5928        let sky = Sky {
5929            width: 4,
5930            height: 2,
5931            texels: vec![[0.3, 0.4, 0.6, 1.0]; 8],
5932        };
5933        let sun = [0.4, 0.8, 0.4];
5934        let scene = BakeScene {
5935            triangles: tris,
5936            shapes: Vec::new(),
5937            materials: materials.clone(),
5938            anchors: vec![Anchor {
5939                hour: 12.0,
5940                sky: sky.clone(),
5941                sun: pfx_trace::Sun {
5942                    direction: sun,
5943                    color: [1.0; 3],
5944                    intensity: 2.0,
5945                },
5946            }],
5947        };
5948        let prop_bounds = bounds(&scene.triangles, &scene.shapes).unwrap();
5949        let spec = PropSpec {
5950            name: "rounded-box-chrome".into(),
5951            node: Some("test".into()),
5952            file: None,
5953            frames_per_side: 16,
5954            atlas_size: 2048,
5955            hemisphere: false,
5956            anchors: vec![12.0],
5957        };
5958        let artifact = bake_prop(&gpu, &scene, &spec, prop_bounds, 512, 7).unwrap();
5959        let mut renderer = Renderer::new(gpu, width, height).unwrap();
5960        renderer.set_sky(SkySource::Hdr(sky), 12.0).unwrap();
5961        let props = ImpostorPass::new(
5962            renderer.gpu(),
5963            artifact,
5964            wgpu::TextureFormat::Rgba16Float,
5965            wgpu::TextureFormat::Depth32Float,
5966            4,
5967        )
5968        .unwrap();
5969        renderer.set_props(Some(props));
5970        let box_handle = renderer
5971            .upload_mesh(MeshData {
5972                positions: &box_mesh.positions,
5973                normals: &box_mesh.normals,
5974                tangents: &box_mesh.tangents,
5975                uvs: &box_mesh.uvs,
5976                uvs1: None,
5977                alpha: None,
5978                indices: &box_mesh.indices,
5979            })
5980            .unwrap();
5981        let sphere_handle = renderer
5982            .upload_mesh(MeshData {
5983                positions: &sphere_mesh.positions,
5984                normals: &sphere_mesh.normals,
5985                tangents: &sphere_mesh.tangents,
5986                uvs: &sphere_mesh.uvs,
5987                uvs1: None,
5988                alpha: None,
5989                indices: &sphere_mesh.indices,
5990            })
5991            .unwrap();
5992        let mut sphere_model = identity();
5993        sphere_model[3] = [0.0, sphere_offset[1], 0.0, 1.0];
5994        let meshes = [
5995            Instance::new(box_handle, identity(), 0, 1),
5996            Instance::new(sphere_handle, sphere_model, 1, 2),
5997        ];
5998        let card = [ImpostorInstance::new(prop_bounds.center, prop_bounds.radius, 3, 0.0).unwrap()];
5999        let look = |eye: [f32; 3]| {
6000            let d = [
6001                prop_bounds.center[0] - eye[0],
6002                prop_bounds.center[1] - eye[1],
6003                prop_bounds.center[2] - eye[2],
6004            ];
6005            let l = (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt();
6006            let forward = d.map(|v| v / l);
6007            let r = [-forward[2], 0.0, forward[0]];
6008            let rl = (r[0] * r[0] + r[2] * r[2]).sqrt();
6009            let right = r.map(|v| v / rl);
6010            let up = [
6011                right[1] * forward[2] - right[2] * forward[1],
6012                right[2] * forward[0] - right[0] * forward[2],
6013                right[0] * forward[1] - right[1] * forward[0],
6014            ];
6015            let dot = |a: [f32; 3], b: [f32; 3]| a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
6016            let view = [
6017                [right[0], up[0], -forward[0], 0.0],
6018                [right[1], up[1], -forward[1], 0.0],
6019                [right[2], up[2], -forward[2], 0.0],
6020                [-dot(right, eye), -dot(up, eye), dot(forward, eye), 1.0],
6021            ];
6022            let projection = perspective(width as f32 / height as f32);
6023            Camera {
6024                view,
6025                projection,
6026                previous_view_projection: frame::multiply(projection, view),
6027                position: eye,
6028            }
6029        };
6030        let views = [
6031            ("head-on", look([0.0, prop_bounds.center[1], 20.0])),
6032            ("oblique", look([8.4, prop_bounds.center[1] + 5.3, 17.6])),
6033        ];
6034        let target = output(renderer.gpu(), width, height);
6035        let mut start = 0;
6036        let mut results = Vec::new();
6037        for (name, camera) in views {
6038            let mut capture =
6039                |renderer: &mut Renderer, instances: &[Instance], props: &[ImpostorInstance]| {
6040                    for number in start..start + 16 {
6041                        let scene = Scene {
6042                            camera,
6043                            time: number as f32 / 60.0,
6044                            seed: 7,
6045                            sun: Sun {
6046                                direction: sun,
6047                                colour: [1.0; 3],
6048                                intensity: 2.0,
6049                            },
6050                            instances,
6051                            materials: &materials,
6052                            deformers: &[],
6053                            wind: SceneWind::default(),
6054                        };
6055                        renderer
6056                            .render(
6057                                &scene,
6058                                &Text::default(),
6059                                &Effects {
6060                                    props,
6061                                    ..Default::default()
6062                                },
6063                                Finish::Standard,
6064                                &target.view,
6065                            )
6066                            .unwrap();
6067                    }
6068                    start += 100;
6069                    renderer
6070                        .gpu()
6071                        .readback_rgba16(&target)
6072                        .unwrap()
6073                        .chunks_exact(4)
6074                        .map(|pixel| [0, 1, 2].map(|c| half::f16::from_bits(pixel[c]).to_f32()))
6075                        .collect::<Vec<_>>()
6076                };
6077            let background = capture(&mut renderer, &[], &[]);
6078            let mesh = capture(&mut renderer, &meshes, &[]);
6079            let prop = capture(&mut renderer, &[], &card);
6080            let differs = |a: [f32; 3], b: [f32; 3]| (0..3).any(|c| (a[c] - b[c]).abs() > 0.004);
6081            let mask: Vec<bool> = (0..mesh.len())
6082                .map(|i| differs(mesh[i], background[i]) || differs(prop[i], background[i]))
6083                .collect();
6084            let clip = frame::transform(
6085                frame::multiply(camera.projection, camera.view),
6086                [sphere_offset[0], sphere_offset[1], sphere_offset[2], 1.0],
6087            );
6088            let centre = [
6089                (clip[0] / clip[3] * 0.5 + 0.5) * width as f32,
6090                (0.5 - clip[1] / clip[3] * 0.5) * height as f32,
6091            ];
6092            let reach = 0.24 * 1.15 * camera.projection[1][1] * height as f32 * 0.5 / clip[3];
6093            let on_sphere = |i: usize| {
6094                let x = (i % width as usize) as f32 + 0.5 - centre[0];
6095                let y = (i / width as usize) as f32 + 0.5 - centre[1];
6096                x * x + y * y <= reach * reach
6097            };
6098            let mean = |keep: &dyn Fn(usize) -> bool| {
6099                let picked: Vec<usize> = (0..mesh.len()).filter(|i| mask[*i] && keep(*i)).collect();
6100                let sum = picked
6101                    .iter()
6102                    .map(|&i| (0..3).map(|c| (mesh[i][c] - prop[i][c]).abs()).sum::<f32>())
6103                    .sum::<f32>();
6104                (sum / (picked.len().max(1) * 3) as f32, picked.len())
6105            };
6106            let (error, count) = mean(&|_| true);
6107            let (box_error, box_count) = mean(&|i| !on_sphere(i));
6108            let (chrome_error, chrome_count) = mean(&|i| on_sphere(i));
6109            let both: Vec<usize> = (0..mesh.len())
6110                .filter(|&i| {
6111                    !on_sphere(i)
6112                        && differs(mesh[i], background[i])
6113                        && differs(prop[i], background[i])
6114                })
6115                .collect();
6116            let bias = [0, 1, 2].map(|c| {
6117                both.iter().map(|&i| prop[i][c] - mesh[i][c]).sum::<f32>()
6118                    / both.len().max(1) as f32
6119            });
6120            let interior = both
6121                .iter()
6122                .map(|&i| (0..3).map(|c| (mesh[i][c] - prop[i][c]).abs()).sum::<f32>())
6123                .sum::<f32>()
6124                / (both.len().max(1) * 3) as f32;
6125            println!(
6126                "{name}: box bias where both cover {bias:?}, MAE {interior:.4} over {} pixels",
6127                both.len()
6128            );
6129            println!(
6130                "{name}: rounded box MAE {box_error:.4} over {box_count} pixels, chrome sphere MAE {chrome_error:.4} over {chrome_count} pixels"
6131            );
6132            let covered = |image: &[[f32; 3]]| -> Vec<bool> {
6133                (0..image.len())
6134                    .map(|i| differs(image[i], background[i]))
6135                    .collect()
6136            };
6137            let speckles = |cover: &[bool]| {
6138                let w = width as usize;
6139                let h = height as usize;
6140                let mut lonely = 0;
6141                for y in 1..h - 1 {
6142                    for x in 1..w - 1 {
6143                        let here = cover[y * w + x];
6144                        let same = [(-1i32, 0i32), (1, 0), (0, -1), (0, 1)]
6145                            .iter()
6146                            .filter(|(dx, dy)| {
6147                                cover[(y as i32 + dy) as usize * w + (x as i32 + dx) as usize]
6148                                    == here
6149                            })
6150                            .count();
6151                        if same == 0 {
6152                            lonely += 1;
6153                        }
6154                    }
6155                }
6156                lonely
6157            };
6158            let mesh_speckles = speckles(&covered(&mesh));
6159            let prop_speckles = speckles(&covered(&prop));
6160            println!(
6161                "{name}: object-only display MAE {error:.4} over {count} pixels; isolated edge pixels: mesh {mesh_speckles}, prop {prop_speckles}"
6162            );
6163            results.push((name, error, count, mesh_speckles, prop_speckles, interior));
6164            let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp");
6165            std::fs::create_dir_all(&root).unwrap();
6166            let file =
6167                std::fs::File::create(root.join(format!("prop-against-mesh-{name}.png"))).unwrap();
6168            let mut encoder = png::Encoder::new(file, width * 2, height);
6169            encoder.set_color(png::ColorType::Rgb);
6170            encoder.set_depth(png::BitDepth::Eight);
6171            let mut bytes = Vec::with_capacity((width * 2 * height * 3) as usize);
6172            for y in 0..height as usize {
6173                for image in [&mesh, &prop] {
6174                    for x in 0..width as usize {
6175                        bytes.extend(
6176                            image[y * width as usize + x]
6177                                .map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8),
6178                        );
6179                    }
6180                }
6181            }
6182            encoder
6183                .write_header()
6184                .unwrap()
6185                .write_image_data(&bytes)
6186                .unwrap();
6187        }
6188        let (_, _, count, mesh_speckles, prop_speckles, _) = results[0];
6189        assert!(count > 20_000, "{count} object pixels");
6190        for (name, _, _, _, _, box_error) in &results {
6191            assert!(
6192                *box_error <= 0.02,
6193                "{name}: rounded box interior MAE {box_error:.4}"
6194            );
6195        }
6196        assert!(
6197            prop_speckles <= mesh_speckles + count / 2000,
6198            "speckled prop edges"
6199        );
6200    }
6201
6202    const PICK_SIZE: u32 = 128;
6203    const FLOOR_ID: u32 = 3;
6204    const PANEL_ID: u32 = 4;
6205    const GLASS_PIXEL: (u32, u32) = (27, 64);
6206    const TEXT_PIXEL: (u32, u32) = (99, 64);
6207    const PANEL_PIXEL: (u32, u32) = (121, 64);
6208    const OVERLAY_PIXEL: (u32, u32) = (64, 112);
6209    const FLOOR_PIXEL: (u32, u32) = (64, 20);
6210
6211    struct IdShot {
6212        colour: Vec<u16>,
6213        ids: Vec<u32>,
6214        picked: Picked,
6215        bounds: Vec<TextBounds>,
6216        at_overlay: Option<TextBounds>,
6217        passes: Vec<&'static str>,
6218    }
6219
6220    impl IdShot {
6221        fn id(&self, (x, y): (u32, u32)) -> u32 {
6222            self.ids[(y * PICK_SIZE + x) as usize]
6223        }
6224    }
6225
6226    fn translated(x: f32, y: f32, z: f32) -> Matrix {
6227        let mut model = identity();
6228        model[3] = [x, y, z, 1.0];
6229        model
6230    }
6231
6232    fn upload_shape(renderer: &mut Renderer, mesh: &Mesh) -> MeshHandle {
6233        renderer
6234            .upload_mesh(MeshData {
6235                positions: &mesh.positions,
6236                normals: &mesh.normals,
6237                tangents: &mesh.tangents,
6238                uvs: &mesh.uvs,
6239                uvs1: None,
6240                alpha: None,
6241                indices: &mesh.indices,
6242            })
6243            .unwrap()
6244    }
6245
6246    fn solid_atlas() -> pfx_text::Atlas {
6247        let level = |size: u32| pfx_text::MipLevel {
6248            width: size,
6249            height: size,
6250            bytes: vec![255; (size * size) as usize],
6251        };
6252        pfx_text::Atlas {
6253            channels: 1,
6254            levels: vec![level(8), level(4), level(2), level(1)],
6255        }
6256    }
6257
6258    fn solid_quad(center: [f32; 2], half: f32, color: [f32; 4]) -> RichQuad {
6259        RichQuad {
6260            rect: [center[0] - half, center[1] - half, 2.0 * half, 2.0 * half],
6261            uv: [0.0, 0.0, 1.0, 1.0],
6262            origin: center,
6263            alpha: 1.0,
6264            clip: [-1000.0, -1000.0, 1000.0, 1000.0],
6265            turn: [0.0; 3],
6266            color,
6267        }
6268    }
6269
6270    fn id_shot(
6271        glass_id: u32,
6272        text_id: u32,
6273        overlay_id: u32,
6274        lit: bool,
6275        pick: (u32, u32),
6276    ) -> IdShot {
6277        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6278        let mut renderer = Renderer::new(gpu, PICK_SIZE, PICK_SIZE).unwrap();
6279        renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6280        renderer.shadows = Shadows::new(
6281            &renderer.gpu().device,
6282            Quality {
6283                resolution: 256,
6284                ..Default::default()
6285            },
6286        );
6287        let floor = Shape::RoundBox {
6288            half: [3.0, 3.0, 0.1],
6289            radius: 0.02,
6290        }
6291        .mesh(0.5);
6292        let panel = Shape::RoundBox {
6293            half: [0.6, 0.6, 0.05],
6294            radius: 0.02,
6295        }
6296        .mesh(0.2);
6297        let slab = Shape::RoundBox {
6298            half: [0.5, 0.5, 0.2],
6299            radius: 0.05,
6300        }
6301        .mesh(0.2);
6302        let floor = upload_shape(&mut renderer, &floor);
6303        let panel = upload_shape(&mut renderer, &panel);
6304        let slab = renderer.upload_glass(&slab).unwrap();
6305        let instances = [
6306            Instance::new(floor, translated(0.0, 0.0, -1.0), 0, FLOOR_ID),
6307            Instance::new(panel, translated(0.8, 0.0, 0.0), 0, PANEL_ID),
6308        ];
6309        let materials = [Material {
6310            base: [0.6; 3],
6311            roughness: 0.6,
6312            ..Default::default()
6313        }];
6314        let glass = [glass::Surface {
6315            mesh: slab,
6316            model: translated(-0.8, 0.0, 0.0),
6317            material: pfx_materials::fixtures::glass(),
6318            liquid: false,
6319            fluid_height: 0.0,
6320            ripple_height: 0.0,
6321            caustic_strength: 0.0,
6322            tinted: None,
6323            id: glass_id,
6324            casts_shadow: false,
6325            shadow_only: false,
6326            clip: [[0.0; 4]; 2],
6327        }];
6328        let atlas = GpuAtlas::new(
6329            &renderer.gpu().device,
6330            &renderer.gpu().queue,
6331            &solid_atlas(),
6332        )
6333        .unwrap();
6334        let ink = [solid_quad([0.0, 0.0], 30.0, [0.1, 0.1, 0.1, 1.0])];
6335        let badge = [solid_quad(
6336            [OVERLAY_PIXEL.0 as f32, OVERLAY_PIXEL.1 as f32],
6337            8.0,
6338            [1.0, 0.8, 0.2, 1.0],
6339        )];
6340        let mut view = identity();
6341        view[3][2] = -4.0;
6342        let projection = perspective(1.0);
6343        let camera = Camera {
6344            view,
6345            projection,
6346            previous_view_projection: frame::multiply(projection, view),
6347            position: [0.0, 0.0, 4.0],
6348        };
6349        let text_model = [
6350            [0.01, 0.0, 0.0, 0.0],
6351            [0.0, -0.01, 0.0, 0.0],
6352            [0.0, 0.0, 1.0, 0.0],
6353            [0.8, 0.0, 0.08, 1.0],
6354        ];
6355        let model_view_projection = frame::multiply(frame::multiply(projection, view), text_model);
6356        let text_space = if lit {
6357            TextSpace::LitSurface {
6358                model_view_projection,
6359                model: text_model,
6360            }
6361        } else {
6362            TextSpace::Surface {
6363                model_view_projection,
6364            }
6365        };
6366        let text = Text {
6367            surface_quads: vec![TextQuadItem {
6368                atlas: &atlas,
6369                quads: &ink,
6370                space: text_space,
6371                icons: false,
6372                id: text_id,
6373            }],
6374            overlay_quads: vec![TextQuadItem {
6375                atlas: &atlas,
6376                quads: &badge,
6377                space: TextSpace::Overlay {
6378                    width: PICK_SIZE,
6379                    height: PICK_SIZE,
6380                },
6381                icons: false,
6382                id: overlay_id,
6383            }],
6384            ..Default::default()
6385        };
6386        let effects = Effects {
6387            glass: &glass,
6388            ..Default::default()
6389        };
6390        let target = output(renderer.gpu(), PICK_SIZE, PICK_SIZE);
6391        renderer.pick(pick.0, pick.1).unwrap();
6392        for number in 0..4 {
6393            let scene = Scene {
6394                camera,
6395                time: number as f32 / 60.0,
6396                seed: 5,
6397                sun: Sun {
6398                    direction: [0.3, 0.5, 0.8],
6399                    colour: [1.0; 3],
6400                    intensity: 2.0,
6401                },
6402                instances: &instances,
6403                materials: &materials,
6404                deformers: &[],
6405                wind: SceneWind::default(),
6406            };
6407            renderer
6408                .render(&scene, &text, &effects, Finish::Standard, &target.view)
6409                .unwrap();
6410        }
6411        let colour = renderer.gpu().readback_rgba16(&target).unwrap();
6412        let ids = readback_texture(
6413            renderer.gpu(),
6414            &renderer.frame().targets.ids,
6415            PICK_SIZE,
6416            PICK_SIZE,
6417            4,
6418        )
6419        .unwrap()
6420        .chunks_exact(4)
6421        .map(|bytes| u32::from_le_bytes(bytes.try_into().unwrap()))
6422        .collect();
6423        let mut picked = None;
6424        for _ in 0..100 {
6425            picked = renderer.picked();
6426            if picked.is_some() {
6427                break;
6428            }
6429            let _ = renderer
6430                .gpu()
6431                .device
6432                .poll(wgpu::PollType::wait_indefinitely());
6433        }
6434        IdShot {
6435            colour,
6436            ids,
6437            picked: picked.expect("the pick never came back"),
6438            bounds: renderer.text_bounds().to_vec(),
6439            at_overlay: renderer
6440                .text_at(OVERLAY_PIXEL.0 as f32 + 0.5, OVERLAY_PIXEL.1 as f32 + 0.5)
6441                .copied(),
6442            passes: renderer.last_pass_order().to_vec(),
6443        }
6444    }
6445
6446    #[derive(Clone, Copy, PartialEq)]
6447    enum Caster {
6448        Nothing,
6449        Copy { moving: bool },
6450        Glass,
6451    }
6452
6453    const CAST_SIZE: u32 = 256;
6454
6455    struct CastShot {
6456        colour: Vec<u16>,
6457        passes: Vec<&'static str>,
6458        transmission: f64,
6459        cascades: f64,
6460        opaque: f64,
6461    }
6462
6463    fn cast_glass() -> Material {
6464        Material {
6465            base: [0.5, 0.62, 0.95],
6466            ..pfx_materials::fixtures::glass()
6467        }
6468    }
6469
6470    fn cast_camera() -> Camera {
6471        let mut view = identity();
6472        view[3][2] = -4.0;
6473        let projection = perspective(1.0);
6474        Camera {
6475            view,
6476            projection,
6477            previous_view_projection: frame::multiply(projection, view),
6478            position: [0.0, 0.0, 4.0],
6479        }
6480    }
6481
6482    fn cast_pixel(world: [f32; 3]) -> usize {
6483        let camera = cast_camera();
6484        let clip = frame::transform(
6485            frame::multiply(camera.projection, camera.view),
6486            [world[0], world[1], world[2], 1.0],
6487        );
6488        let x = ((clip[0] / clip[3] * 0.5 + 0.5) * CAST_SIZE as f32) as usize;
6489        let y = ((0.5 - clip[1] / clip[3] * 0.5) * CAST_SIZE as f32) as usize;
6490        y * CAST_SIZE as usize + x
6491    }
6492
6493    fn cast_shot(caster: Caster, frames: usize) -> CastShot {
6494        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6495        let mut renderer = Renderer::new(gpu, CAST_SIZE, CAST_SIZE).unwrap();
6496        renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6497        let floor = Shape::RoundBox {
6498            half: [3.0, 3.0, 0.1],
6499            radius: 0.02,
6500        }
6501        .mesh(0.5);
6502        let slab = Shape::RoundBox {
6503            half: [0.5, 0.5, 0.2],
6504            radius: 0.05,
6505        }
6506        .mesh(0.2);
6507        let floor = upload_shape(&mut renderer, &floor);
6508        let glass_mesh = renderer.upload_glass(&slab).unwrap();
6509        let mut instances = vec![Instance::new(floor, translated(0.0, 0.0, -1.0), 0, 1)];
6510        let glass_model = translated(-0.8, 0.0, 0.0);
6511        if let Caster::Copy { moving } = caster {
6512            let copy = upload_shape(&mut renderer, &slab);
6513            let mut instance = Instance::new(copy, glass_model, 1, 2);
6514            instance.shadow_only = true;
6515            if moving {
6516                instance.previous_model[3][1] = 1.0e-9;
6517            }
6518            instances.push(instance);
6519            renderer
6520                .set_transmission(&[None, Some(glass::cast_tint(&cast_glass()))])
6521                .unwrap();
6522        }
6523        let materials = [
6524            Material {
6525                base: [0.6; 3],
6526                roughness: 0.6,
6527                ..Default::default()
6528            },
6529            Material::default(),
6530        ];
6531        let glass = [glass::Surface {
6532            mesh: glass_mesh,
6533            model: glass_model,
6534            material: cast_glass(),
6535            liquid: false,
6536            fluid_height: 0.0,
6537            ripple_height: 0.0,
6538            caustic_strength: 0.0,
6539            tinted: None,
6540            id: 0,
6541            casts_shadow: caster == Caster::Glass,
6542            shadow_only: false,
6543            clip: [[0.0; 4]; 2],
6544        }];
6545        let effects = Effects {
6546            glass: &glass,
6547            ..Default::default()
6548        };
6549        let target = output(renderer.gpu(), CAST_SIZE, CAST_SIZE);
6550        let mut sums = [0.0; 3];
6551        let mut timed = 0;
6552        let mut turns = pfx_gpu::pace::Turns::default();
6553        for number in 0..frames {
6554            let scene = Scene {
6555                camera: cast_camera(),
6556                time: number as f32 / 60.0,
6557                seed: 5,
6558                sun: Sun {
6559                    direction: [0.3, 0.5, 0.8],
6560                    colour: [1.0; 3],
6561                    intensity: 2.0,
6562                },
6563                instances: &instances,
6564                materials: &materials,
6565                deformers: &[],
6566                wind: SceneWind::default(),
6567            };
6568            let timings = renderer
6569                .render(
6570                    &scene,
6571                    &Text::default(),
6572                    &effects,
6573                    Finish::Standard,
6574                    &target.view,
6575                )
6576                .unwrap();
6577            turns.add(pfx_gpu::pace::gpu_ms(&timings).unwrap_or(f64::NAN));
6578            if number < 4 || timings.is_empty() {
6579                continue;
6580            }
6581            timed += 1;
6582            for timing in &timings {
6583                let slot = if timing.label == "shadow transmission" {
6584                    0
6585                } else if timing.label.starts_with("shadow cascade") {
6586                    1
6587                } else if timing.label == "opaque PBR" {
6588                    2
6589                } else {
6590                    continue;
6591                };
6592                sums[slot] += timing.milliseconds;
6593            }
6594        }
6595        let mean = |sum: f64| sum / f64::from(timed.max(1));
6596        CastShot {
6597            colour: renderer.gpu().readback_rgba16(&target).unwrap(),
6598            passes: renderer.last_pass_order().to_vec(),
6599            transmission: mean(sums[0]),
6600            cascades: mean(sums[1]),
6601            opaque: mean(sums[2]),
6602        }
6603    }
6604
6605    fn rgb(colour: &[u16], at: usize) -> [f32; 3] {
6606        [0, 1, 2].map(|k| half::f16::from_bits(colour[at * 4 + k]).to_f32())
6607    }
6608
6609    #[test]
6610    fn a_glass_cast_tint_is_transmission_times_base_or_beer_lambert_over_its_thickness() {
6611        let clear = Material {
6612            base: [0.5, 0.8, 1.0],
6613            transmission: 0.9,
6614            ..Material::default()
6615        };
6616        let tint = glass::cast_tint(&clear);
6617        for (k, expected) in [0.45, 0.72, 0.9].into_iter().enumerate() {
6618            assert!((tint[k] - expected).abs() < 1e-6, "{tint:?}");
6619        }
6620        let thick = Material {
6621            thickness: 0.3,
6622            absorption: 4.0,
6623            ..clear
6624        };
6625        let beer = pfx_materials::beer(0.3, [0.5, 0.8, 1.0], 4.0);
6626        let tint = glass::cast_tint(&thick);
6627        for k in 0..3 {
6628            assert!((tint[k] - beer[k] * 0.9).abs() < 1e-6, "{tint:?}");
6629        }
6630        assert_eq!(tint[2], 0.9);
6631        let unthick = Material {
6632            absorption: 4.0,
6633            ..clear
6634        };
6635        assert_eq!(glass::cast_tint(&unthick), glass::cast_tint(&clear));
6636        let bright = Material {
6637            base: [2.0, 0.5, -1.0],
6638            transmission: 1.0,
6639            ..Material::default()
6640        };
6641        assert_eq!(glass::cast_tint(&bright), [1.0, 0.5, 0.0]);
6642    }
6643
6644    #[test]
6645    #[ignore = "needs a GPU; run under pgpu"]
6646    fn glass_casts_the_shadow_its_shadow_only_copy_cast() {
6647        let nothing = cast_shot(Caster::Nothing, 6);
6648        let copy = cast_shot(Caster::Copy { moving: false }, 6);
6649        let glass = cast_shot(Caster::Glass, 6);
6650        assert!(!nothing.passes.contains(&"shadow transmission"));
6651        assert!(glass.passes.contains(&"shadow transmission"));
6652        let shade = cast_pixel([-1.1, -0.9, -0.9]);
6653        let open = cast_pixel([1.5, -0.9, -0.9]);
6654        let ratio = |shot: &CastShot| {
6655            let (lit, shaded) = (rgb(&shot.colour, open), rgb(&shot.colour, shade));
6656            [0, 1, 2].map(|k| shaded[k] / lit[k])
6657        };
6658        println!(
6659            "under the slab / open floor: nothing {:?}, copy {:?}, glass {:?}",
6660            ratio(&nothing),
6661            ratio(&copy),
6662            ratio(&glass)
6663        );
6664        let glassed = ratio(&glass);
6665        assert!(glassed[2] > glassed[0] + 0.1, "the shadow is not tinted");
6666        assert!(
6667            glassed[0] < ratio(&nothing)[0] - 0.1,
6668            "the glass casts no shadow"
6669        );
6670        let mut worst = 0.0f32;
6671        let mut differing = 0;
6672        for (a, b) in copy
6673            .colour
6674            .chunks_exact(4)
6675            .zip(glass.colour.chunks_exact(4))
6676        {
6677            let gap = (0..3)
6678                .map(|k| {
6679                    (half::f16::from_bits(a[k]).to_f32() - half::f16::from_bits(b[k]).to_f32())
6680                        .abs()
6681                })
6682                .fold(0.0, f32::max);
6683            worst = worst.max(gap);
6684            differing += usize::from(gap > 0.0);
6685        }
6686        println!("copy against glass: {differing} pixels differ, worst {worst:.5}");
6687        assert!(worst <= 1.0 / 255.0, "worst {worst}");
6688    }
6689
6690    #[test]
6691    #[ignore = "needs a GPU; run under pgpu"]
6692    fn glass_casting_costs_about_what_its_copy_costs() {
6693        for (name, caster) in [
6694            ("nothing", Caster::Nothing),
6695            ("still copy", Caster::Copy { moving: false }),
6696            ("moving copy", Caster::Copy { moving: true }),
6697            ("glass", Caster::Glass),
6698        ] {
6699            let shot = cast_shot(caster, 64);
6700            println!(
6701                "{name}: transmission {:.4} ms, cascades {:.4} ms, opaque {:.4} ms",
6702                shot.transmission, shot.cascades, shot.opaque
6703            );
6704        }
6705    }
6706
6707    #[derive(Clone, Copy, Debug, PartialEq)]
6708    enum Card {
6709        Plain,
6710        Cut,
6711        Grid,
6712    }
6713
6714    const CARD_IMAGE: u32 = 256;
6715    const CARD_CELLS: u32 = 128;
6716
6717    fn card_alpha(u: f32, v: f32) -> f32 {
6718        let r = ((u - 0.5).powi(2) + (v - 0.5).powi(2)).sqrt();
6719        if (0.25..=0.45).contains(&r) { 1.0 } else { 0.0 }
6720    }
6721
6722    fn card_mesh(renderer: &mut Renderer, card: Card, half: f32) -> MeshHandle {
6723        let cells = if card == Card::Grid { CARD_CELLS } else { 1 };
6724        let side = cells + 1;
6725        let mut positions = Vec::new();
6726        let mut uvs = Vec::new();
6727        let mut alpha = Vec::new();
6728        for j in 0..side {
6729            for i in 0..side {
6730                let uv = [i as f32 / cells as f32, j as f32 / cells as f32];
6731                positions.push([(uv[0] * 2.0 - 1.0) * half, 0.0, (uv[1] * 2.0 - 1.0) * half]);
6732                uvs.push(uv);
6733                alpha.push(card_alpha(uv[0], uv[1]));
6734            }
6735        }
6736        let mut indices = Vec::new();
6737        for j in 0..cells {
6738            for i in 0..cells {
6739                let a = j * side + i;
6740                indices.extend([a, a + side, a + 1, a + 1, a + side, a + side + 1]);
6741            }
6742        }
6743        let count = positions.len();
6744        renderer
6745            .upload_mesh(MeshData {
6746                positions: &positions,
6747                normals: &vec![[0.0, 1.0, 0.0]; count],
6748                tangents: &vec![[1.0, 0.0, 0.0, 1.0]; count],
6749                uvs: &uvs,
6750                uvs1: None,
6751                alpha: (card == Card::Grid).then_some(alpha.as_slice()),
6752                indices: &indices,
6753            })
6754            .unwrap()
6755    }
6756
6757    fn card_costs(card: Card) -> [f64; 3] {
6758        let (width, height) = (3840, 2160);
6759        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6760        let mut renderer = Renderer::new(gpu, width, height).unwrap();
6761        renderer.set_exposure(Exposure::Fixed(1.0)).unwrap();
6762        let floor = card_mesh(&mut renderer, Card::Plain, 4.0);
6763        let mesh = card_mesh(&mut renderer, card, 0.3);
6764        let texels: Vec<u8> = (0..CARD_IMAGE * CARD_IMAGE)
6765            .flat_map(|index| {
6766                let u = ((index % CARD_IMAGE) as f32 + 0.5) / CARD_IMAGE as f32;
6767                let v = ((index / CARD_IMAGE) as f32 + 0.5) / CARD_IMAGE as f32;
6768                [220, 60, 40, (card_alpha(u, v) * 255.0) as u8]
6769            })
6770            .collect();
6771        renderer
6772            .frame()
6773            .set_content(0, CARD_IMAGE, CARD_IMAGE, crate::maps::ContentFormat::Srgb8)
6774            .unwrap();
6775        renderer
6776            .frame()
6777            .write_content(0, [0, 0, CARD_IMAGE, CARD_IMAGE], &texels)
6778            .unwrap();
6779        let mut instances = vec![Instance::new(floor, identity(), 0, 1)];
6780        for row in 0..4 {
6781            for column in 0..6 {
6782                let model = translated(
6783                    (column as f32 - 2.5) * 0.8,
6784                    0.8 + 0.05 * row as f32,
6785                    (row as f32 - 1.5) * 0.8,
6786                );
6787                let mut instance = Instance::new(mesh, model, 1, instances.len() as u32 + 1);
6788                instance.previous_model[3][1] += 1.0e-6;
6789                instances.push(instance);
6790            }
6791        }
6792        let mut surfaces = vec![
6793            frame::InstanceSurface {
6794                cutout: card == Card::Cut,
6795                ..Default::default()
6796            };
6797            instances.len()
6798        ];
6799        surfaces[0].cutout = false;
6800        renderer.frame().set_surfaces(&surfaces).unwrap();
6801        let materials = [
6802            Material {
6803                base: [0.6; 3],
6804                roughness: 0.7,
6805                ..Default::default()
6806            },
6807            Material {
6808                base: [0.9; 3],
6809                roughness: 0.5,
6810                content_layer: pfx_materials::ContentLayer {
6811                    slot: 0,
6812                    ..Default::default()
6813                },
6814                ..Default::default()
6815            },
6816        ];
6817        let view = [
6818            [1.0, 0.0, 0.0, 0.0],
6819            [0.0, 0.0, 1.0, 0.0],
6820            [0.0, -1.0, 0.0, 0.0],
6821            [0.0, 0.0, -4.0, 1.0],
6822        ];
6823        let projection = perspective(width as f32 / height as f32);
6824        let camera = Camera {
6825            view,
6826            projection,
6827            previous_view_projection: frame::multiply(projection, view),
6828            position: [0.0, 4.0, 0.0],
6829        };
6830        let target = output(renderer.gpu(), width, height);
6831        let mut sums = [0.0; 3];
6832        let mut timed = 0;
6833        let mut turns = pfx_gpu::pace::Turns::default();
6834        for number in 0..40 {
6835            let scene = Scene {
6836                camera,
6837                time: number as f32 / 60.0,
6838                seed: 5,
6839                sun: Sun {
6840                    direction: [0.8, 0.6, 0.0],
6841                    colour: [1.0; 3],
6842                    intensity: 3.0,
6843                },
6844                instances: &instances,
6845                materials: &materials,
6846                deformers: &[],
6847                wind: SceneWind::default(),
6848            };
6849            let timings = renderer
6850                .render(
6851                    &scene,
6852                    &Text::default(),
6853                    &Effects::default(),
6854                    Finish::Standard,
6855                    &target.view,
6856                )
6857                .unwrap();
6858            turns.add(pfx_gpu::pace::gpu_ms(&timings).unwrap_or(f64::NAN));
6859            if number < 8 || timings.is_empty() {
6860                continue;
6861            }
6862            timed += 1;
6863            for timing in &timings {
6864                let slot = if timing.label == "depth and velocity" {
6865                    0
6866                } else if timing.label == "opaque PBR" {
6867                    1
6868                } else if timing.label.starts_with("shadow cascade") {
6869                    2
6870                } else {
6871                    continue;
6872                };
6873                sums[slot] += timing.milliseconds;
6874            }
6875        }
6876        sums.map(|sum| sum / f64::from(timed.max(1)))
6877    }
6878
6879    #[test]
6880    #[ignore = "needs a GPU; run under pgpu"]
6881    fn card_scene_costs_with_content_cutouts_and_vertex_alpha_grids() {
6882        for card in [Card::Plain, Card::Cut, Card::Grid] {
6883            let [depth, opaque, cascades] = card_costs(card);
6884            println!(
6885                "{card:?} cards at 3840x2160: depth and velocity {depth:.4} ms, opaque {opaque:.4} ms, cascades {cascades:.4} ms"
6886            );
6887        }
6888    }
6889
6890    #[test]
6891    #[ignore = "needs a GPU; run under pgpu"]
6892    fn glass_with_an_id_picks_over_the_floor_and_glass_without_one_lets_it_through() {
6893        let picked = id_shot(9, 0, 0, false, GLASS_PIXEL);
6894        assert_eq!(picked.picked.id, 9);
6895        assert_eq!(picked.id(GLASS_PIXEL), 9);
6896        assert_eq!(picked.id(FLOOR_PIXEL), FLOOR_ID);
6897        assert_eq!(picked.id(PANEL_PIXEL), PANEL_ID);
6898        assert!(picked.passes.contains(&"glass ids"));
6899        let through = id_shot(0, 0, 0, false, GLASS_PIXEL);
6900        assert_eq!(through.picked.id, FLOOR_ID);
6901        assert_eq!(through.id(GLASS_PIXEL), FLOOR_ID);
6902        assert!(!through.passes.contains(&"glass ids"));
6903        let slab = picked.ids.iter().filter(|id| **id == 9).count();
6904        let changed = picked
6905            .ids
6906            .iter()
6907            .zip(&through.ids)
6908            .filter(|(a, b)| a != b)
6909            .count();
6910        assert!(slab > 400, "the slab covers only {slab} pixels");
6911        assert_eq!(changed, slab, "ids changed outside the slab");
6912    }
6913
6914    #[test]
6915    #[ignore = "needs a GPU; run under pgpu"]
6916    fn surface_text_picks_as_its_own_id_on_its_panel() {
6917        for lit in [false, true] {
6918            let shot = id_shot(0, 21, 0, lit, TEXT_PIXEL);
6919            assert_eq!(shot.picked.id, 21, "lit {lit}");
6920            assert_eq!(shot.id(TEXT_PIXEL), 21, "lit {lit}");
6921            assert_eq!(shot.id(PANEL_PIXEL), PANEL_ID, "lit {lit}");
6922            assert!(shot.passes.contains(&"text ids"));
6923            let plain = id_shot(0, 0, 0, lit, TEXT_PIXEL);
6924            assert_eq!(plain.picked.id, PANEL_ID, "lit {lit}");
6925            assert!(!plain.passes.contains(&"text ids"));
6926        }
6927    }
6928
6929    #[test]
6930    #[ignore = "needs a GPU; run under pgpu"]
6931    fn overlay_text_picks_over_a_mesh_and_its_bounds_come_back_on_the_cpu() {
6932        let shot = id_shot(0, 21, 33, false, OVERLAY_PIXEL);
6933        assert_eq!(shot.picked.id, 33);
6934        assert_eq!(shot.id(OVERLAY_PIXEL), 33);
6935        assert_eq!(shot.id(FLOOR_PIXEL), FLOOR_ID);
6936        let plain = id_shot(0, 21, 0, false, OVERLAY_PIXEL);
6937        assert_eq!(plain.picked.id, FLOOR_ID);
6938        let overlay = shot.at_overlay.expect("no text bounds under the overlay");
6939        assert_eq!(
6940            (overlay.list, overlay.index, overlay.id),
6941            (TextList::OverlayQuads, 0, 33)
6942        );
6943        let close = |a: [f32; 4], b: [f32; 4]| a.iter().zip(b).all(|(a, b)| (a - b).abs() < 0.01);
6944        assert!(
6945            close(overlay.rect, [56.0, 104.0, 72.0, 120.0]),
6946            "{:?}",
6947            overlay.rect
6948        );
6949        let surface = shot
6950            .bounds
6951            .iter()
6952            .find(|bounds| bounds.list == TextList::SurfaceQuads)
6953            .expect("no surface text bounds");
6954        assert_eq!(surface.id, 21);
6955        let [left, top, right, bottom] = surface.rect;
6956        let (x, y) = (TEXT_PIXEL.0 as f32, TEXT_PIXEL.1 as f32);
6957        assert!(
6958            left < x && x < right && top < y && y < bottom,
6959            "{:?}",
6960            surface.rect
6961        );
6962        let covered = shot.ids.iter().enumerate().filter(|(_, id)| **id == 21);
6963        for (index, _) in covered {
6964            let px = (index as u32 % PICK_SIZE) as f32 + 0.5;
6965            let py = (index as u32 / PICK_SIZE) as f32 + 0.5;
6966            assert!(
6967                left <= px && px <= right && top <= py && py <= bottom,
6968                "text id at ({px}, {py}) outside its bounds {:?}",
6969                surface.rect
6970            );
6971        }
6972    }
6973
6974    #[test]
6975    #[ignore = "needs a GPU; run under pgpu"]
6976    fn id_writes_leave_the_frames_colour_bytes_unchanged() {
6977        for lit in [false, true] {
6978            let before = id_shot(0, 0, 0, lit, GLASS_PIXEL);
6979            let after = id_shot(9, 21, 33, lit, GLASS_PIXEL);
6980            assert_eq!(after.id(GLASS_PIXEL), 9);
6981            assert_eq!(after.id(TEXT_PIXEL), 21);
6982            assert_eq!(after.id(OVERLAY_PIXEL), 33);
6983            let differ = before
6984                .colour
6985                .iter()
6986                .zip(&after.colour)
6987                .filter(|(a, b)| a != b)
6988                .count();
6989            assert_eq!(differ, 0, "lit {lit}: {differ} colour channels changed");
6990        }
6991    }
6992
6993    fn glass_slab(half: f32, step: f32) -> Mesh {
6994        Shape::RoundBox {
6995            half: [half, half, 0.2],
6996            radius: 0.05,
6997        }
6998        .mesh(step)
6999    }
7000
7001    fn render_glass(
7002        renderer: &mut Renderer,
7003        handle: glass::MeshHandle,
7004        target: &OffscreenTarget,
7005    ) -> Result<(), String> {
7006        let mut view = identity();
7007        view[3][2] = -4.0;
7008        let projection = perspective(1.0);
7009        let camera = Camera {
7010            view,
7011            projection,
7012            previous_view_projection: frame::multiply(projection, view),
7013            position: [0.0, 0.0, 4.0],
7014        };
7015        let materials = [Material::default()];
7016        let glass = [glass::Surface {
7017            mesh: handle,
7018            model: identity(),
7019            material: pfx_materials::fixtures::glass(),
7020            liquid: false,
7021            fluid_height: 0.0,
7022            ripple_height: 0.0,
7023            caustic_strength: 0.0,
7024            tinted: None,
7025            id: 0,
7026            casts_shadow: false,
7027            shadow_only: false,
7028            clip: [[0.0; 4]; 2],
7029        }];
7030        let scene = Scene {
7031            camera,
7032            time: 0.0,
7033            seed: 5,
7034            sun: Sun {
7035                direction: [0.3, 0.5, 0.8],
7036                colour: [1.0; 3],
7037                intensity: 2.0,
7038            },
7039            instances: &[],
7040            materials: &materials,
7041            deformers: &[],
7042            wind: SceneWind::default(),
7043        };
7044        let effects = Effects {
7045            glass: &glass,
7046            ..Default::default()
7047        };
7048        renderer
7049            .render(
7050                &scene,
7051                &Text::default(),
7052                &effects,
7053                Finish::Standard,
7054                &target.view,
7055            )
7056            .map(|_| ())
7057    }
7058
7059    #[test]
7060    #[ignore = "needs a GPU; run under pgpu"]
7061    fn glass_meshes_release_replace_and_keep_their_handles_through_a_resize() {
7062        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7063        let mut renderer = Renderer::new(gpu, PICK_SIZE, PICK_SIZE).unwrap();
7064        let target = output(renderer.gpu(), PICK_SIZE, PICK_SIZE);
7065        let small = glass_slab(0.3, 0.3);
7066        let large = glass_slab(0.5, 0.1);
7067        let keep = renderer.upload_glass(&small).unwrap();
7068        let flat = renderer.mesh_stats();
7069        assert_eq!(
7070            (flat.glass_live, flat.glass_slots, flat.glass_buffers),
7071            (1, 1, 2)
7072        );
7073        render_glass(&mut renderer, keep, &target).unwrap();
7074        for cycle in 0..1000 {
7075            let (first, second) = if cycle % 2 == 0 {
7076                (&small, &large)
7077            } else {
7078                (&large, &small)
7079            };
7080            let handle = renderer.upload_glass(first).unwrap();
7081            assert_eq!(renderer.mesh_stats().glass_live, 2);
7082            renderer.replace_glass(handle, second).unwrap();
7083            renderer.replace_glass(handle, first).unwrap();
7084            renderer.release_glass(handle).unwrap();
7085            assert!(renderer.release_glass(handle).is_err());
7086            assert!(renderer.replace_glass(handle, first).is_err());
7087            assert!(renderer.glass_mesh_buffers(handle).is_none());
7088            let stats = renderer.mesh_stats();
7089            assert_eq!(
7090                (stats.glass_live, stats.glass_slots, stats.glass_buffers),
7091                (1, 2, 2),
7092                "cycle {cycle}"
7093            );
7094        }
7095        let before = renderer.glass_mesh_buffers(keep).unwrap().0.clone();
7096        renderer.replace_glass(keep, &small).unwrap();
7097        assert!(*renderer.glass_mesh_buffers(keep).unwrap().0 == before);
7098        renderer.replace_glass(keep, &large).unwrap();
7099        assert_eq!(renderer.mesh_stats().glass_buffers, 2);
7100        renderer.replace_glass(keep, &small).unwrap();
7101        assert_eq!(
7102            renderer.glass_mesh_buffers(keep).unwrap().2,
7103            small.indices.len() as u32
7104        );
7105        renderer.resize(96, 96).unwrap();
7106        let resized = renderer.mesh_stats();
7107        assert_eq!(
7108            (
7109                resized.glass_live,
7110                resized.glass_slots,
7111                resized.glass_buffers
7112            ),
7113            (1, 2, 2)
7114        );
7115        let target = output(renderer.gpu(), 96, 96);
7116        render_glass(&mut renderer, keep, &target).unwrap();
7117        renderer.release_glass(keep).unwrap();
7118        let error = render_glass(&mut renderer, keep, &target).unwrap_err();
7119        assert!(error.contains("released"), "{error}");
7120        let end = renderer.mesh_stats();
7121        assert_eq!(
7122            (end.glass_live, end.glass_buffers),
7123            (0, 0),
7124            "the last glass mesh was released"
7125        );
7126    }
7127
7128    fn flat_board() -> (crate::flat::ShadowCurve, Vec<crate::flat::Draw>) {
7129        use crate::flat::{CurvePoint, Draw, ShadowCurve, Srgba, Stroke};
7130        let curve = ShadowCurve::new(
7131            Srgba::hex(0x2b3442),
7132            vec![
7133                CurvePoint {
7134                    elevation: 1.0,
7135                    offset: 4.0,
7136                    sigma: 5.0,
7137                    alpha: 0.22,
7138                },
7139                CurvePoint {
7140                    elevation: 2.0,
7141                    offset: 12.0,
7142                    sigma: 16.0,
7143                    alpha: 0.26,
7144                },
7145            ],
7146        )
7147        .unwrap();
7148        let draws = vec![
7149            Draw::rect([48.0, 40.0], [64.0, 40.0], 8.0)
7150                .fill(Srgba::WHITE)
7151                .elevation(1.0)
7152                .id(7),
7153            Draw::circle([30.0, 40.0], 9.0)
7154                .fill(Srgba::hex(0xe2614c))
7155                .elevation(1.0)
7156                .shadow(crate::flat::Shadow::None)
7157                .id(8),
7158            Draw::rect([70.0, 70.0], [40.0, 30.0], 10.0)
7159                .vertical(Srgba::hex(0xffd36b), Srgba::hex(0xf0a92e))
7160                .stroke(Stroke::solid(Srgba::hex(0xc4840c), 2.0))
7161                .elevation(2.0)
7162                .id(9),
7163        ];
7164        (curve, draws)
7165    }
7166
7167    fn flat_scene<'a>(
7168        curve: &'a crate::flat::ShadowCurve,
7169        draws: &'a [crate::flat::Draw],
7170    ) -> crate::flat::FlatScene<'a> {
7171        crate::flat::FlatScene {
7172            layout: [96.0, 96.0],
7173            clear: Some(crate::flat::Srgba::hex(0xeceff3)),
7174            curve,
7175            light: crate::flat::Light::default(),
7176            draws,
7177            groups: &[],
7178            text: &[],
7179            icons: None,
7180            sprites: None,
7181            environment: None,
7182            post: false,
7183            frame: 0,
7184            seed: 0,
7185        }
7186    }
7187
7188    #[test]
7189    #[ignore = "needs a GPU; run under pgpu"]
7190    fn existing_scenes_render_the_same_bytes_beside_a_flat_hud() {
7191        let (mut plain, plain_target, instances, materials) = sphere_still(96);
7192        let (mut beside, beside_target, _, _) = sphere_still(96);
7193        let (curve, _) = flat_board();
7194        let empty = flat_scene(&curve, &[]);
7195        for number in 0..4 {
7196            let scene = still_scene(&instances, &materials, number);
7197            plain
7198                .render(
7199                    &scene,
7200                    &Text::default(),
7201                    &Effects::default(),
7202                    Finish::Standard,
7203                    &plain_target.view,
7204                )
7205                .unwrap();
7206            beside
7207                .render_with_flat(
7208                    &scene,
7209                    &Text::default(),
7210                    &Effects::default(),
7211                    Finish::Standard,
7212                    &empty,
7213                    &beside_target.view,
7214                )
7215                .unwrap();
7216        }
7217        assert!(!plain.last_pass_order().contains(&"flat"));
7218        assert!(beside.last_pass_order().contains(&"flat"));
7219        let expected = plain.gpu().readback_rgba16(&plain_target).unwrap();
7220        let actual = beside.gpu().readback_rgba16(&beside_target).unwrap();
7221        assert!(expected == actual, "an empty flat HUD changed the frame");
7222        let (curve, draws) = flat_board();
7223        let hud = flat_scene(&curve, &draws);
7224        let scene = still_scene(&instances, &materials, 4);
7225        beside
7226            .render_with_flat(
7227                &scene,
7228                &Text::default(),
7229                &Effects::default(),
7230                Finish::Standard,
7231                &hud,
7232                &beside_target.view,
7233            )
7234            .unwrap();
7235        let with_hud = beside.gpu().readback_rgba16(&beside_target).unwrap();
7236        let at = |pixels: &[u16], x: usize, y: usize| -> [f32; 4] {
7237            std::array::from_fn(|c| half::f16::from_bits(pixels[(y * 96 + x) * 4 + c]).to_f32())
7238        };
7239        assert!(at(&with_hud, 48, 30)[..3].iter().all(|&c| c > 0.995));
7240        let red = at(&with_hud, 30, 40);
7241        assert!(
7242            (red[0] - 0xe2 as f32 / 255.0).abs() < 2e-3
7243                && (red[1] - 0x61 as f32 / 255.0).abs() < 2e-3
7244        );
7245    }
7246
7247    #[test]
7248    #[ignore = "needs a GPU; run under pgpu"]
7249    fn a_flat_scene_renders_alone_in_every_output_format_and_picks() {
7250        let (curve, draws) = flat_board();
7251        let scene = flat_scene(&curve, &draws);
7252        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7253        let mut direct = Renderer::new(gpu, 96, 96).unwrap();
7254        let target = output(direct.gpu(), 96, 96);
7255        direct.render_flat(&scene, &target.view).unwrap();
7256        assert_eq!(direct.last_pass_order(), ["flat", "flat ids"]);
7257        let gamma = direct
7258            .gpu()
7259            .readback_rgba16(&target)
7260            .unwrap()
7261            .chunks_exact(4)
7262            .map(|p| std::array::from_fn::<f32, 4, _>(|c| half::f16::from_bits(p[c]).to_f32()))
7263            .collect::<Vec<_>>();
7264        let twin = crate::flat::cpu::render(&scene, None, [96, 96]).unwrap();
7265        for (a, b) in gamma.iter().zip(&twin) {
7266            for c in 0..4 {
7267                assert!((a[c] - b[c]).abs() < 2.5 / 255.0, "{a:?} vs {b:?}");
7268            }
7269        }
7270        assert!(gamma[30 * 96 + 48][..3].iter().all(|&c| c > 0.995));
7271        direct.pick(30, 40).unwrap();
7272        direct.render_flat(&scene, &target.view).unwrap();
7273        let picked = direct.picked().unwrap();
7274        assert_eq!((picked.id, picked.depth), (8, 0.0));
7275        direct.pick(70, 70).unwrap();
7276        direct.render_flat(&scene, &target.view).unwrap();
7277        assert_eq!(direct.picked().unwrap().id, 9);
7278        direct.pick(2, 2).unwrap();
7279        direct.render_flat(&scene, &target.view).unwrap();
7280        assert_eq!(direct.picked().unwrap().id, 0);
7281
7282        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7283        let mut srgb =
7284            Renderer::new_with_output_format(gpu, 96, 96, wgpu::TextureFormat::Rgba8UnormSrgb)
7285                .unwrap();
7286        let srgb_target = srgb
7287            .gpu()
7288            .offscreen(96, 96, wgpu::TextureFormat::Rgba8UnormSrgb)
7289            .unwrap();
7290        srgb.render_flat(&scene, &srgb_target.view).unwrap();
7291        assert_eq!(srgb.last_pass_order(), ["flat", "flat finish", "flat ids"]);
7292        let bytes = srgb.gpu().readback_rgba8(&srgb_target).unwrap();
7293        for (pixel, expected) in bytes.chunks_exact(4).zip(&gamma) {
7294            for c in 0..3 {
7295                let want = (expected[c].clamp(0.0, 1.0) * 255.0).round();
7296                assert!(
7297                    (f32::from(pixel[c]) - want).abs() <= 1.0,
7298                    "{pixel:?} vs {expected:?}"
7299                );
7300            }
7301        }
7302
7303        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7304        let mut posted = Renderer::new(gpu, 96, 96).unwrap();
7305        posted.set_finish(pfx_post::Chain {
7306            passes: Vec::new(),
7307            frame: 0,
7308            seed: 0,
7309        });
7310        let posted_target = output(posted.gpu(), 96, 96);
7311        let mut through_post = scene;
7312        through_post.post = true;
7313        posted
7314            .render_flat(&through_post, &posted_target.view)
7315            .unwrap();
7316        assert_eq!(
7317            posted.last_pass_order(),
7318            ["flat", "flat finish", "post", "flat ids"]
7319        );
7320        let finished = posted.gpu().readback_rgba16(&posted_target).unwrap();
7321        for (pixel, expected) in finished.chunks_exact(4).zip(&gamma) {
7322            for c in 0..3 {
7323                let value = half::f16::from_bits(pixel[c]).to_f32();
7324                assert!(
7325                    (value - expected[c]).abs() < 1.5 / 255.0,
7326                    "{value} vs {expected:?}"
7327                );
7328            }
7329        }
7330        posted.set_tape(Some(pfx_post::Tape::forward(1.0)));
7331        let mut glitched = through_post;
7332        glitched.frame = 7;
7333        posted.render_flat(&glitched, &posted_target.view).unwrap();
7334        let taped = posted.gpu().readback_rgba16(&posted_target).unwrap();
7335        assert!(
7336            taped != finished,
7337            "the tape glitch did not reach the flat frame"
7338        );
7339    }
7340}