Skip to main content

pfx_live/
frame.rs

1use crate::canopy::{Canopy, CanopyFrame, CanopyMode, Gobo, canopy_light_wgsl};
2use crate::deform::{self, Deformer, DeformerBuffer, DeformerId, WindFrame};
3use crate::lights::{CASTER_BYTES, CASTER_SLOTS, LIGHTS_WGSL, LightQuality, Lights, LocalLight};
4use crate::lod::LOD_WGSL;
5use crate::maps::{
6    self, CAUSTIC_BINDING, CONTENT_BINDING, CONTENT_SLOTS, Caustic, ContentFormat, ContentSlots,
7    GpuMapMaterial, MapImages, MapIndices, MapTextures,
8};
9use crate::pipeline_cache::{CacheLoad, PipelineCache};
10use crate::probes::{
11    Ambient, ContactDesc, ContactField, ContactUniform, PROBE_WGSL, ProbeLighting,
12};
13use crate::ranges::FreeRanges;
14use crate::reflect;
15use crate::shadow::{CASCADE_COUNT, Fit, SAMPLE_UNIFORM_BYTES, SHADOW_WGSL, Shadows};
16use crate::skin::{
17    InstancePose, NO_SKIN, PoseLayout, SKIN_BINDING, SKIN_WGSL, SkinData, SkinStore,
18};
19use crate::sky::{AnalyticSky, SkyPass, SkySource};
20use crate::warm::{self, Arrival, Warming};
21use bytemuck::{Pod, Zeroable};
22use pfx_bake::detail::atlas::Atlas;
23use pfx_bake::reflection::{
24    ProbeRecord, ReflectionArray, ReflectionArtifact, nearest_two, upload_reflections,
25};
26use pfx_core::daylight::{Daylight, REFERENCE_HOUR};
27use pfx_core::sky::SUN_RADIUS;
28use pfx_geom::tree::{SwayVertex, TREE_SWAY_WGSL, Tree};
29use pfx_gpu::{Gpu, GpuProfiler, OffscreenTarget, PassTiming};
30use pfx_materials::{BRDF, Material, PackedMaterial};
31use pfx_physics::BEND_WGSL;
32use std::cell::{Cell, RefCell};
33use std::collections::{HashMap, HashSet};
34use std::hash::{Hash, Hasher};
35use std::num::NonZeroU64;
36use std::ops::{Deref, Range};
37use std::sync::Arc;
38use std::sync::atomic::{AtomicUsize, Ordering};
39use std::sync::mpsc::{Receiver, Sender, channel};
40use wgpu::util::DeviceExt;
41
42pub type Matrix = [[f32; 4]; 4];
43
44pub const NO_SWAY: u32 = u32::MAX;
45pub const SKY_EDGE: u32 = 128;
46pub const CONTACT_BINDING: u32 = CONTENT_BINDING + CONTENT_SLOTS as u32 + 1;
47pub const NORMAL_FORMAT: wgpu::TextureFormat = wgpu::TextureFormat::Rgba16Float;
48
49pub(crate) const MESH_SLOT_BITS: u32 = 20;
50pub(crate) const MESH_SLOT_MASK: u32 = (1 << MESH_SLOT_BITS) - 1;
51pub(crate) const MESH_GENERATION_LAST: u32 = u32::MAX >> MESH_SLOT_BITS;
52
53#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
54pub struct MeshHandle(pub u32);
55
56impl MeshHandle {
57    pub fn slot(self) -> usize {
58        (self.0 & MESH_SLOT_MASK) as usize
59    }
60
61    pub fn generation(self) -> u32 {
62        self.0 >> MESH_SLOT_BITS
63    }
64
65    fn pack(slot: usize, generation: u32) -> Self {
66        Self(slot as u32 | generation << MESH_SLOT_BITS)
67    }
68}
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub struct MeshStats {
72    pub live: usize,
73    pub slots: usize,
74    pub buffers: usize,
75    pub glass_live: usize,
76    pub glass_slots: usize,
77    pub glass_buffers: usize,
78}
79
80pub struct MeshData<'a> {
81    pub positions: &'a [[f32; 3]],
82    pub normals: &'a [[f32; 3]],
83    pub tangents: &'a [[f32; 4]],
84    pub uvs: &'a [[f32; 2]],
85    pub uvs1: Option<&'a [[f32; 2]]>,
86    pub alpha: Option<&'a [f32]>,
87    pub indices: &'a [u32],
88}
89
90pub struct MeshBuffers<'a> {
91    pub vertices: &'a wgpu::Buffer,
92    pub positions: &'a wgpu::Buffer,
93    pub indices: &'a wgpu::Buffer,
94    pub count: u32,
95}
96
97#[derive(Clone, Copy)]
98pub struct Camera {
99    pub view: Matrix,
100    pub projection: Matrix,
101    pub previous_view_projection: Matrix,
102    pub position: [f32; 3],
103}
104
105#[derive(Clone, Copy)]
106pub struct Sun {
107    pub direction: [f32; 3],
108    pub colour: [f32; 3],
109    pub intensity: f32,
110}
111
112impl Sun {
113    pub fn from_daylight(daylight: Daylight, reference_hour: f64) -> Self {
114        let sun = daylight.sun();
115        let light = daylight.light(reference_hour);
116        Self {
117            direction: sun.y_up.map(|value| value as f32),
118            colour: light.colour.map(|value| value as f32),
119            intensity: light.intensity as f32,
120        }
121    }
122}
123
124#[derive(Clone, Copy)]
125pub struct Instance {
126    pub mesh: MeshHandle,
127    pub model: Matrix,
128    pub previous_model: Matrix,
129    pub material: u32,
130    pub id: u32,
131    pub opacity: f32,
132    pub alpha_cutoff: f32,
133    pub deformer: DeformerId,
134    pub age: f32,
135    pub coverage: f32,
136    pub shadow_only: bool,
137    pub casts_shadow: bool,
138    pub two_sided: bool,
139}
140
141impl Instance {
142    pub fn new(mesh: MeshHandle, model: Matrix, material: u32, id: u32) -> Self {
143        Self {
144            mesh,
145            model,
146            previous_model: model,
147            material,
148            id,
149            opacity: 1.0,
150            alpha_cutoff: 0.5,
151            deformer: DeformerId::NONE,
152            age: 0.0,
153            coverage: 1.0,
154            shadow_only: false,
155            casts_shadow: true,
156            two_sided: false,
157        }
158    }
159}
160
161#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
162pub enum ContentFace {
163    #[default]
164    Both,
165    Front,
166    Back,
167}
168
169impl ContentFace {
170    fn code(self) -> u32 {
171        match self {
172            Self::Both => 0,
173            Self::Front => 1,
174            Self::Back => 2,
175        }
176    }
177
178    pub fn shows(self, front: bool) -> bool {
179        match self {
180            Self::Both => true,
181            Self::Front => front,
182            Self::Back => !front,
183        }
184    }
185}
186
187#[derive(Clone, Copy, Debug, PartialEq)]
188pub struct InstanceSurface {
189    pub clip: [[f32; 4]; 2],
190    pub uv_offset: [f32; 2],
191    pub uv_scale: [f32; 2],
192    pub crop: [f32; 4],
193    pub face: ContentFace,
194    pub contact: bool,
195    pub authored_ambient: bool,
196    pub cutout: bool,
197    pub reflection_occlusion: f32,
198}
199
200impl Default for InstanceSurface {
201    fn default() -> Self {
202        Self {
203            clip: [[0.0; 4]; 2],
204            uv_offset: [0.0; 2],
205            uv_scale: [1.0; 2],
206            crop: [0.0, 0.0, 1.0, 1.0],
207            face: ContentFace::Both,
208            contact: false,
209            authored_ambient: false,
210            cutout: false,
211            reflection_occlusion: 1.0,
212        }
213    }
214}
215
216impl InstanceSurface {
217    pub fn clipped(&self) -> bool {
218        self.clip
219            .iter()
220            .any(|plane| plane[..3].iter().any(|&value| value != 0.0))
221    }
222
223    pub fn cuts(&self, point: [f32; 3]) -> bool {
224        self.clip.iter().any(|plane| {
225            plane[..3].iter().any(|&value| value != 0.0)
226                && plane[0] * point[0] + plane[1] * point[1] + plane[2] * point[2] > plane[3]
227        })
228    }
229
230    pub fn content_uv(&self, uv: [f32; 2]) -> Option<[f32; 2]> {
231        pfx_materials::content_uv(uv, self.uv_offset, self.uv_scale, self.crop)
232    }
233
234    fn valid(&self) -> bool {
235        self.clip.iter().flatten().all(|value| value.is_finite())
236            && self.uv_offset.iter().all(|value| value.is_finite())
237            && self.uv_scale.iter().all(|value| value.is_finite())
238            && self.crop.iter().all(|value| value.is_finite())
239            && (0.0..=1.0).contains(&self.reflection_occlusion)
240    }
241
242    fn occlusion_release(&self) -> u32 {
243        ((1.0 - self.reflection_occlusion) * 255.0).round() as u32
244    }
245}
246
247#[derive(Clone, Copy, Debug, PartialEq)]
248pub struct SceneWind {
249    pub current: WindFrame,
250    pub previous: WindFrame,
251}
252
253impl Default for SceneWind {
254    fn default() -> Self {
255        let calm = WindFrame {
256            time: 0.0,
257            strength: 0.0,
258            direction: [1.0, 0.0, 0.0],
259        };
260        Self {
261            current: calm,
262            previous: calm,
263        }
264    }
265}
266
267pub struct Scene<'a> {
268    pub camera: Camera,
269    pub time: f32,
270    pub seed: u32,
271    pub sun: Sun,
272    pub instances: &'a [Instance],
273    pub materials: &'a [Material],
274    pub deformers: &'a [Deformer],
275    pub wind: SceneWind,
276}
277
278pub struct FrameTargets {
279    pub hdr: OffscreenTarget,
280    pub depth: wgpu::Texture,
281    pub depth_view: wgpu::TextureView,
282    pub velocity: wgpu::Texture,
283    pub velocity_view: wgpu::TextureView,
284    pub ids: wgpu::Texture,
285    pub ids_view: wgpu::TextureView,
286    pub normal_roughness: wgpu::Texture,
287    pub normal_roughness_view: wgpu::TextureView,
288    pub width: u32,
289    pub height: u32,
290}
291
292pub(crate) struct CountedBuffer {
293    buffer: wgpu::Buffer,
294    live: Arc<AtomicUsize>,
295}
296
297impl CountedBuffer {
298    pub(crate) fn new(buffer: wgpu::Buffer, live: &Arc<AtomicUsize>) -> Self {
299        live.fetch_add(1, Ordering::Relaxed);
300        Self {
301            buffer,
302            live: live.clone(),
303        }
304    }
305}
306
307impl Deref for CountedBuffer {
308    type Target = wgpu::Buffer;
309
310    fn deref(&self) -> &wgpu::Buffer {
311        &self.buffer
312    }
313}
314
315impl Drop for CountedBuffer {
316    fn drop(&mut self) {
317        self.live.fetch_sub(1, Ordering::Relaxed);
318    }
319}
320
321struct Mesh {
322    vertices: CountedBuffer,
323    positions: CountedBuffer,
324    indices: CountedBuffer,
325    vertex_capacity: usize,
326    index_capacity: usize,
327    count: u32,
328    sway: u32,
329    sway_len: u32,
330    skin: u32,
331    skin_len: u32,
332    skin_joints: u32,
333}
334
335struct MeshSlot {
336    generation: u32,
337    mesh: Option<Mesh>,
338}
339
340fn live_mesh(meshes: &[MeshSlot], handle: MeshHandle) -> Option<&Mesh> {
341    let slot = meshes.get(handle.slot())?;
342    if slot.generation == handle.generation() {
343        slot.mesh.as_ref()
344    } else {
345        None
346    }
347}
348
349fn check_mesh(data: &MeshData<'_>) -> Result<(), String> {
350    let count = data.positions.len();
351    if count == 0 || data.indices.is_empty() || !data.indices.len().is_multiple_of(3) {
352        return Err("mesh needs vertices and triangle indices".into());
353    }
354    if data.normals.len() != count
355        || data.tangents.len() != count
356        || data.uvs.len() != count
357        || data.uvs1.is_some_and(|uvs1| uvs1.len() != count)
358        || data.alpha.is_some_and(|alpha| alpha.len() != count)
359    {
360        return Err("mesh attributes have different vertex counts".into());
361    }
362    if data.indices.iter().any(|&index| index as usize >= count) {
363        return Err("mesh index is outside the vertex buffer".into());
364    }
365    Ok(())
366}
367
368fn mesh_vertices(data: &MeshData<'_>) -> Vec<Vertex> {
369    (0..data.positions.len())
370        .map(|index| {
371            let uv1 = data.uvs1.map_or([0.0; 2], |uvs1| uvs1[index]);
372            Vertex {
373                position: [
374                    data.positions[index][0],
375                    data.positions[index][1],
376                    data.positions[index][2],
377                    1.0,
378                ],
379                normal: [
380                    data.normals[index][0],
381                    data.normals[index][1],
382                    data.normals[index][2],
383                    uv1[1],
384                ],
385                tangent: data.tangents[index],
386                uv_alpha: [
387                    data.uvs[index][0],
388                    data.uvs[index][1],
389                    data.alpha.map_or(1.0, |alpha| alpha[index]),
390                    uv1[0],
391                ],
392            }
393        })
394        .collect()
395}
396
397fn mesh_buffer(
398    device: &wgpu::Device,
399    live: &Arc<AtomicUsize>,
400    label: &str,
401    contents: &[u8],
402    usage: wgpu::BufferUsages,
403) -> CountedBuffer {
404    CountedBuffer::new(
405        device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
406            label: Some(label),
407            contents,
408            usage: usage | wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::STORAGE,
409        }),
410        live,
411    )
412}
413
414#[derive(Clone, Debug, PartialEq, Eq)]
415pub struct Batch {
416    pub mesh: MeshHandle,
417    pub two_sided: bool,
418    pub moving: bool,
419    pub clipped: bool,
420    pub cut: bool,
421    pub shadow_only: bool,
422    pub casts_shadow: bool,
423    pub range: Range<u32>,
424}
425
426#[repr(C)]
427#[derive(Clone, Copy, Pod, Zeroable)]
428struct Vertex {
429    position: [f32; 4],
430    normal: [f32; 4],
431    tangent: [f32; 4],
432    uv_alpha: [f32; 4],
433}
434
435#[repr(C)]
436#[derive(Clone, Copy, Pod, Zeroable)]
437struct GpuSway {
438    pivot_level: [f32; 4],
439    stiffness: [f32; 4],
440}
441
442#[repr(C)]
443#[derive(Clone, Copy, Pod, Zeroable)]
444struct GpuInstance {
445    model: Matrix,
446    previous_model: Matrix,
447    normal: [[f32; 4]; 3],
448    meta: [u32; 4],
449    extra: [u32; 4],
450    clip: [[f32; 4]; 2],
451    content_uv: [f32; 4],
452    content_crop: [f32; 4],
453    content_face: [u32; 4],
454    skin: [u32; 4],
455}
456
457#[repr(C)]
458#[derive(Clone, Copy, Pod, Zeroable)]
459struct Uniforms {
460    view_projection: Matrix,
461    motion_view_projection: Matrix,
462    previous_view_projection: Matrix,
463    view: Matrix,
464    camera_position: [f32; 4],
465    sun_direction: [f32; 4],
466    sun_colour_intensity: [f32; 4],
467    time_size: [f32; 4],
468    flags: [u32; 4],
469    wind: [f32; 4],
470    wind_direction: [f32; 4],
471    previous_wind_direction: [f32; 4],
472}
473
474#[repr(C)]
475#[derive(Clone, Copy, Pod, Zeroable)]
476struct SkyLight {
477    sh: [[f32; 4]; 9],
478    info: [f32; 4],
479}
480
481#[repr(C)]
482#[derive(Clone, Copy, Pod, Zeroable)]
483struct GpuReflectionProbe {
484    center: [f32; 3],
485    layer: i32,
486    box_min: [f32; 3],
487    priority: f32,
488    box_max: [f32; 3],
489    fade: f32,
490}
491
492impl From<ProbeRecord> for GpuReflectionProbe {
493    fn from(value: ProbeRecord) -> Self {
494        Self {
495            center: value.center,
496            layer: value.layer,
497            box_min: value.box_min,
498            priority: value.priority,
499            box_max: value.box_max,
500            fade: value.fade,
501        }
502    }
503}
504
505#[repr(C)]
506#[derive(Clone, Copy, Pod, Zeroable)]
507struct ReflectionSelection {
508    first: GpuReflectionProbe,
509    second: GpuReflectionProbe,
510    weights_mip: [f32; 4],
511}
512
513fn local_candidates(records: &[ProbeRecord], camera: [f32; 3]) -> [usize; 2] {
514    let chosen = nearest_two(records, camera);
515    if chosen[0].1 > 0.0 {
516        let second = if chosen[1].1 > 0.0 {
517            chosen[1].0
518        } else {
519            chosen[0].0
520        };
521        return [chosen[0].0, second];
522    }
523    let mut order: Vec<(usize, f32)> = records
524        .iter()
525        .enumerate()
526        .map(|(index, record)| {
527            let outside = (0..3)
528                .map(|axis| {
529                    let below = record.box_min[axis] - camera[axis];
530                    let above = camera[axis] - record.box_max[axis];
531                    below.max(above).max(0.0).powi(2)
532                })
533                .sum::<f32>();
534            (index, outside)
535        })
536        .collect();
537    order.sort_by(|a, b| a.1.total_cmp(&b.1).then(a.0.cmp(&b.0)));
538    let first = order.first().map_or(0, |entry| entry.0);
539    [first, order.get(1).map_or(first, |entry| entry.0)]
540}
541
542struct LocalReflections {
543    artifacts: Vec<ReflectionArtifact>,
544    array: ReflectionArray,
545}
546
547struct Foliage {
548    canopy: Canopy,
549    origin: [f32; 3],
550    receiver_y: f32,
551    id: u32,
552    prepass: wgpu::RenderPipeline,
553    opaque: wgpu::RenderPipeline,
554}
555
556#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
557pub struct OpaqueFeatures {
558    pub sun_shadow: bool,
559    pub soft_search: bool,
560    pub soft_filter: bool,
561    pub cascade_blend: bool,
562    pub probes: bool,
563    pub local_reflections: bool,
564    pub ssr: bool,
565    pub canopy: bool,
566    pub contact: bool,
567    pub authored_ambient: bool,
568    pub caustics: bool,
569    pub content: bool,
570    pub maps: bool,
571    pub detail: bool,
572    pub ageing: bool,
573    pub fine_noise: bool,
574    pub uv1: bool,
575    pub content_cutout: bool,
576    pub lacquer: bool,
577    pub procedural_lacquer: bool,
578    pub scratch: bool,
579    pub grime: bool,
580    pub wood: bool,
581    pub wall: bool,
582    pub fibre: bool,
583    pub crinkle: bool,
584    pub procedural_detail: bool,
585    pub film: bool,
586}
587
588const OPAQUE_FEATURES: usize = 28;
589
590impl OpaqueFeatures {
591    pub const NAMES: [&'static str; OPAQUE_FEATURES] = [
592        "sun_shadow",
593        "soft_search",
594        "soft_filter",
595        "cascade_blend",
596        "probes",
597        "local_reflections",
598        "ssr",
599        "canopy",
600        "contact",
601        "authored_ambient",
602        "caustics",
603        "content",
604        "maps",
605        "detail",
606        "ageing",
607        "fine_noise",
608        "uv1",
609        "content_cutout",
610        "lacquer",
611        "procedural_lacquer",
612        "scratch",
613        "grime",
614        "wood",
615        "wall",
616        "fibre",
617        "crinkle",
618        "procedural_detail",
619        "film",
620    ];
621
622    pub const ALL: Self = Self::from_flags([true; OPAQUE_FEATURES]);
623
624    pub const NONE: Self = Self::from_flags([false; OPAQUE_FEATURES]);
625
626    pub const fn from_flags(flags: [bool; OPAQUE_FEATURES]) -> Self {
627        Self {
628            sun_shadow: flags[0],
629            soft_search: flags[1],
630            soft_filter: flags[2],
631            cascade_blend: flags[3],
632            probes: flags[4],
633            local_reflections: flags[5],
634            ssr: flags[6],
635            canopy: flags[7],
636            contact: flags[8],
637            authored_ambient: flags[9],
638            caustics: flags[10],
639            content: flags[11],
640            maps: flags[12],
641            detail: flags[13],
642            ageing: flags[14],
643            fine_noise: flags[15],
644            uv1: flags[16],
645            content_cutout: flags[17],
646            lacquer: flags[18],
647            procedural_lacquer: flags[19],
648            scratch: flags[20],
649            grime: flags[21],
650            wood: flags[22],
651            wall: flags[23],
652            fibre: flags[24],
653            crinkle: flags[25],
654            procedural_detail: flags[26],
655            film: flags[27],
656        }
657    }
658
659    pub const fn flags(self) -> [bool; OPAQUE_FEATURES] {
660        [
661            self.sun_shadow,
662            self.soft_search,
663            self.soft_filter,
664            self.cascade_blend,
665            self.probes,
666            self.local_reflections,
667            self.ssr,
668            self.canopy,
669            self.contact,
670            self.authored_ambient,
671            self.caustics,
672            self.content,
673            self.maps,
674            self.detail,
675            self.ageing,
676            self.fine_noise,
677            self.uv1,
678            self.content_cutout,
679            self.lacquer,
680            self.procedural_lacquer,
681            self.scratch,
682            self.grime,
683            self.wood,
684            self.wall,
685            self.fibre,
686            self.crinkle,
687            self.procedural_detail,
688            self.film,
689        ]
690    }
691
692    pub fn named(name: &str) -> Option<Self> {
693        let index = Self::NAMES.iter().position(|known| *known == name)?;
694        let mut flags = [false; OPAQUE_FEATURES];
695        flags[index] = true;
696        Some(Self::from_flags(flags))
697    }
698
699    pub fn union(self, other: Self) -> Self {
700        let (a, b) = (self.flags(), other.flags());
701        Self::from_flags(std::array::from_fn(|i| a[i] || b[i]))
702    }
703
704    pub fn without(self, other: Self) -> Self {
705        let (a, b) = (self.flags(), other.flags());
706        Self::from_flags(std::array::from_fn(|i| a[i] && !b[i]))
707    }
708
709    pub fn names(self) -> Vec<&'static str> {
710        Self::NAMES
711            .iter()
712            .zip(self.flags())
713            .filter_map(|(name, on)| on.then_some(*name))
714            .collect()
715    }
716
717    fn constants(self) -> Vec<(String, f64)> {
718        Self::NAMES
719            .iter()
720            .zip(self.flags())
721            .map(|(name, on)| (format!("use_{name}"), f64::from(u8::from(on))))
722            .collect()
723    }
724}
725
726#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
727pub struct OpaqueKey {
728    pub features: OpaqueFeatures,
729    pub lit: bool,
730    pub two_sided: bool,
731}
732
733impl OpaqueKey {
734    pub fn both_sides(features: OpaqueFeatures, lit: bool) -> [Self; 2] {
735        [false, true].map(|two_sided| Self {
736            features,
737            lit,
738            two_sided,
739        })
740    }
741}
742
743pub(crate) fn opaque_pipeline(
744    device: &wgpu::Device,
745    layout: &wgpu::PipelineLayout,
746    module: &wgpu::ShaderModule,
747    cache: Option<&wgpu::PipelineCache>,
748    key: OpaqueKey,
749) -> wgpu::RenderPipeline {
750    let constants = key.features.constants();
751    let constants: Vec<(&str, f64)> = constants
752        .iter()
753        .map(|(name, value)| (name.as_str(), *value))
754        .collect();
755    let buffers = [vertex_layout()];
756    pipeline_with(
757        device,
758        PipelineSpec {
759            label: if key.lit {
760                "live opaque PBR with local lights"
761            } else {
762                "live opaque PBR"
763            },
764            layout,
765            module,
766            vertex: "vertex_main",
767            fragment: Some(if key.lit {
768                "opaque_lit_main"
769            } else {
770                "opaque_main"
771            }),
772            buffers: &buffers,
773            targets: &opaque_targets(),
774            cull: (!key.two_sided).then_some(wgpu::Face::Back),
775            write_depth: false,
776            cache,
777        },
778        &constants,
779    )
780}
781
782struct CutPipelines {
783    prepass: [wgpu::RenderPipeline; 2],
784    shadow: wgpu::RenderPipeline,
785}
786
787struct Pipelines {
788    prepass: [wgpu::RenderPipeline; 2],
789    shadow: [wgpu::RenderPipeline; 2],
790    cut: Option<CutPipelines>,
791    cut_layout: wgpu::PipelineLayout,
792    transmit: wgpu::RenderPipeline,
793    layout: wgpu::PipelineLayout,
794    module: wgpu::ShaderModule,
795    opaque: HashMap<OpaqueKey, wgpu::RenderPipeline>,
796    cache: Option<PipelineCache>,
797    pending: HashSet<OpaqueKey>,
798    arrivals: Receiver<Arrival>,
799    sender: Sender<Arrival>,
800    made_here: usize,
801    workers: usize,
802}
803
804impl Pipelines {
805    fn collect(&mut self) {
806        while let Ok(arrival) = self.arrivals.try_recv() {
807            self.arrive(arrival);
808        }
809    }
810
811    fn retarget(&mut self, module: wgpu::ShaderModule) {
812        while !self.pending.is_empty() {
813            match self.arrivals.recv() {
814                Ok((key, _)) => {
815                    self.pending.remove(&key);
816                }
817                Err(_) => break,
818            }
819        }
820        self.opaque.clear();
821        self.module = module;
822    }
823
824    fn arrive(&mut self, (key, made): Arrival) {
825        self.pending.remove(&key);
826        if let Some(made) = made {
827            self.opaque.entry(key).or_insert(made);
828        }
829    }
830
831    fn ensure_cut(&mut self, device: &wgpu::Device) {
832        if self.cut.is_some() {
833            return;
834        }
835        let buffers = [vertex_layout()];
836        let prepass = prepass_pipelines(device, &self.layout, &self.module, true);
837        let shadow = pipeline(
838            device,
839            PipelineSpec {
840                label: "live cut shadow casters",
841                layout: &self.cut_layout,
842                module: &self.module,
843                vertex: "shadow_cut_vertex",
844                fragment: Some("shadow_cut_fragment"),
845                buffers: &buffers,
846                targets: &[],
847                cull: None,
848                write_depth: true,
849                cache: None,
850            },
851        );
852        self.cut = Some(CutPipelines { prepass, shadow });
853    }
854
855    fn ensure_opaque(&mut self, device: &wgpu::Device, key: OpaqueKey) {
856        self.collect();
857        while !self.opaque.contains_key(&key) && self.pending.contains(&key) {
858            match self.arrivals.recv() {
859                Ok(arrival) => self.arrive(arrival),
860                Err(_) => break,
861            }
862        }
863        if self.opaque.contains_key(&key) {
864            return;
865        }
866        let made = opaque_pipeline(
867            device,
868            &self.layout,
869            &self.module,
870            self.cache.as_ref().map(PipelineCache::cache),
871            key,
872        );
873        self.made_here += 1;
874        self.opaque.insert(key, made);
875    }
876
877    fn warm(&mut self, device: &wgpu::Device, first: &[OpaqueKey], later: &[OpaqueKey]) -> Warming {
878        self.collect();
879        let mut queued: Vec<OpaqueKey> = Vec::new();
880        let mut first_count = 0;
881        for (index, key) in first.iter().chain(later).enumerate() {
882            if self.opaque.contains_key(key) || self.pending.contains(key) || queued.contains(key) {
883                continue;
884            }
885            queued.push(*key);
886            if index < first.len() {
887                first_count += 1;
888            }
889        }
890        self.pending.extend(queued.iter().copied());
891        warm::spawn(
892            warm::Job {
893                device: device.clone(),
894                layout: self.layout.clone(),
895                module: self.module.clone(),
896                cache: self.cache.as_ref().map(|cache| cache.cache().clone()),
897            },
898            queued,
899            first_count,
900            self.workers,
901            self.sender.clone(),
902        )
903    }
904}
905
906struct Fallbacks {
907    black: wgpu::TextureView,
908    local_probes: wgpu::TextureView,
909    canopy_params: wgpu::Buffer,
910    linear: wgpu::Sampler,
911}
912
913pub struct Frame {
914    pub gpu: Gpu,
915    pub targets: FrameTargets,
916    meshes: Vec<MeshSlot>,
917    free_meshes: Vec<usize>,
918    mesh_buffers_live: Arc<AtomicUsize>,
919    mesh_epoch: u64,
920    sway_free: FreeRanges,
921    uniform: wgpu::Buffer,
922    instances: wgpu::Buffer,
923    materials: wgpu::Buffer,
924    map_materials: wgpu::Buffer,
925    sway: wgpu::Buffer,
926    sway_data: Vec<GpuSway>,
927    skins: SkinStore,
928    instance_capacity: usize,
929    material_capacity: usize,
930    scene_layout: wgpu::BindGroupLayout,
931    scene_group: wgpu::BindGroup,
932    shadow_group: wgpu::BindGroup,
933    shadow_layout: wgpu::BindGroupLayout,
934    shadow_uniform: wgpu::Buffer,
935    _shadow_atlas: wgpu::Texture,
936    _shadow_view: wgpu::TextureView,
937    _shadow_sampler: wgpu::Sampler,
938    cascade_uniform: wgpu::Buffer,
939    cascade_group: wgpu::BindGroup,
940    cascade_layout: wgpu::BindGroupLayout,
941    cascade_align: u32,
942    lights: Lights,
943    lighting_layout: wgpu::BindGroupLayout,
944    lighting_group: Option<wgpu::BindGroup>,
945    sky_light: wgpu::Buffer,
946    ambient: Ambient,
947    ambient_light: wgpu::Buffer,
948    contact: Option<ContactField>,
949    contact_uniform: wgpu::Buffer,
950    reflection_selection: wgpu::Buffer,
951    local_reflections: RefCell<Option<LocalReflections>>,
952    deformers: DeformerBuffer,
953    maps: MapTextures,
954    map_layers: [usize; 4],
955    content: RefCell<ContentSlots>,
956    content_epoch: Cell<u64>,
957    caustic: RefCell<Option<Caustic>>,
958    surfaces: RefCell<Vec<InstanceSurface>>,
959    probes: ProbeLighting,
960    sky: SkyPass,
961    ssr: Option<reflect::Reflections>,
962    viewport: [u32; 2],
963    foliage: Option<Foliage>,
964    fallbacks: Fallbacks,
965    pipelines: Pipelines,
966    canopy_gobo: Gobo,
967    shadow_sample: [u8; SAMPLE_UNIFORM_BYTES],
968    instance_features: Vec<OpaqueFeatures>,
969    reflections_planned: bool,
970    opaque_on: Cell<OpaqueFeatures>,
971    opaque_off: Cell<OpaqueFeatures>,
972    opaque_drawn: Vec<OpaqueFeatures>,
973    jitter: [f32; 2],
974    profiler: GpuProfiler,
975}
976
977pub fn multiply(a: Matrix, b: Matrix) -> Matrix {
978    let mut result = [[0.0; 4]; 4];
979    for column in 0..4 {
980        for row in 0..4 {
981            result[column][row] = (0..4).map(|k| a[k][row] * b[column][k]).sum();
982        }
983    }
984    result
985}
986
987pub fn transform(matrix: Matrix, point: [f32; 4]) -> [f32; 4] {
988    std::array::from_fn(|row| {
989        (0..4)
990            .map(|column| matrix[column][row] * point[column])
991            .sum()
992    })
993}
994
995pub fn velocity_uv(current: [f32; 4], previous: [f32; 4]) -> [f32; 2] {
996    if current[3].abs() < 1e-8 || previous[3].abs() < 1e-8 {
997        return [0.0; 2];
998    }
999    [
1000        (previous[0] / previous[3] - current[0] / current[3]) * 0.5,
1001        (current[1] / current[3] - previous[1] / previous[3]) * 0.5,
1002    ]
1003}
1004
1005pub fn velocity_pixels(current: [f32; 4], previous: [f32; 4], size: [f32; 2]) -> [f32; 2] {
1006    let uv = velocity_uv(current, previous);
1007    [uv[0] * size[0], uv[1] * size[1]]
1008}
1009
1010pub fn jitter_matrix(jitter: [f32; 2]) -> Matrix {
1011    let mut shift = [[0.0; 4]; 4];
1012    for (i, column) in shift.iter_mut().enumerate() {
1013        column[i] = 1.0;
1014    }
1015    shift[3][0] = jitter[0];
1016    shift[3][1] = jitter[1];
1017    shift
1018}
1019
1020fn same_bits(a: Matrix, b: Matrix) -> bool {
1021    a.iter()
1022        .flatten()
1023        .zip(b.iter().flatten())
1024        .all(|(x, y)| x.to_bits() == y.to_bits())
1025}
1026
1027pub fn batches(
1028    instances: &[Instance],
1029    swaying: impl Fn(MeshHandle) -> bool,
1030    clipped: impl Fn(usize) -> bool,
1031) -> Vec<Batch> {
1032    surface_batches(instances, swaying, clipped, |_| false)
1033}
1034
1035pub fn surface_batches(
1036    instances: &[Instance],
1037    swaying: impl Fn(MeshHandle) -> bool,
1038    clipped: impl Fn(usize) -> bool,
1039    cut: impl Fn(usize) -> bool,
1040) -> Vec<Batch> {
1041    let mut out: Vec<Batch> = Vec::new();
1042    for (index, instance) in instances.iter().enumerate() {
1043        let two_sided = instance.two_sided || instance.deformer != DeformerId::NONE;
1044        let moving = instance.model != instance.previous_model
1045            || instance.deformer != DeformerId::NONE
1046            || swaying(instance.mesh);
1047        let clipped = clipped(index);
1048        let cut = cut(index);
1049        let index = index as u32;
1050        if let Some(last) = out.last_mut()
1051            && last.mesh == instance.mesh
1052            && last.two_sided == two_sided
1053            && last.moving == moving
1054            && last.clipped == clipped
1055            && last.cut == cut
1056            && last.shadow_only == instance.shadow_only
1057            && last.casts_shadow == instance.casts_shadow
1058            && last.range.end == index
1059        {
1060            last.range.end += 1;
1061            continue;
1062        }
1063        out.push(Batch {
1064            mesh: instance.mesh,
1065            two_sided,
1066            moving,
1067            clipped,
1068            cut,
1069            shadow_only: instance.shadow_only,
1070            casts_shadow: instance.casts_shadow,
1071            range: index..index + 1,
1072        });
1073    }
1074    out
1075}
1076
1077fn opaque_instance_features(
1078    instance: &GpuInstance,
1079    material: &GpuMapMaterial,
1080    filmed: bool,
1081) -> OpaqueFeatures {
1082    let packed = material.seeds[2] == 3 && material.indices[3] >= 0;
1083    let unbaked = material.seeds[3] == 0;
1084    let layered = material
1085        .layers
1086        .iter()
1087        .any(|layer| layer[0] != 0.0 && layer[2] != 0.0);
1088    let bump = material.seeds[2] == 1 && material.factors[3] != 0.0;
1089    let lacquered = !packed
1090        && material
1091            .layers
1092            .iter()
1093            .any(|layer| layer[0] == 18.0 && layer[2] != 0.0);
1094    let carries = |kinds: &[f32]| {
1095        unbaked
1096            && !packed
1097            && material
1098                .layers
1099                .iter()
1100                .any(|layer| kinds.contains(&layer[0]) && layer[2] != 0.0)
1101    };
1102    let unplaced = material.content[0] < -0.5;
1103    OpaqueFeatures {
1104        contact: instance.content_face[2] & 1 != 0,
1105        caustics: instance.content_face[2] & 2 != 0,
1106        authored_ambient: instance.content_face[3] != 0,
1107        content: !unplaced,
1108        maps: material.indices.iter().any(|&index| index >= 0),
1109        detail: unbaked && ((!packed && layered) || bump),
1110        lacquer: unbaked && (lacquered || bump),
1111        scratch: carries(&[17.0]),
1112        grime: carries(&[5.0]),
1113        wood: carries(&[3.0]),
1114        wall: carries(&[4.0]),
1115        fibre: carries(&[1.0, 11.0]),
1116        crinkle: carries(&[2.0]),
1117        ageing: f32::from_bits(instance.extra[2]) > 0.0,
1118        uv1: material.seeds[3] >= 2 && material.packed[1] != 0.0,
1119        content_cutout: instance.content_face[2] & 4 != 0,
1120        film: filmed,
1121        ..OpaqueFeatures::NONE
1122    }
1123}
1124
1125fn scene_instance_features(
1126    packed: &[GpuInstance],
1127    map_materials: &[GpuMapMaterial],
1128    materials: &[Material],
1129) -> Vec<OpaqueFeatures> {
1130    packed
1131        .iter()
1132        .map(|instance| {
1133            let index = instance.meta[0] as usize;
1134            opaque_instance_features(
1135                instance,
1136                &map_materials[index],
1137                materials[index].thin_film_amount > 0.0,
1138            )
1139        })
1140        .collect()
1141}
1142
1143pub fn shadow_batches(batches: &[Batch]) -> Vec<&Batch> {
1144    batches.iter().filter(|batch| batch.casts_shadow).collect()
1145}
1146
1147pub fn transmission_batches<'a>(
1148    batches: &'a [Batch],
1149    instances: &[Instance],
1150    transmissive: impl Fn(u32) -> bool,
1151) -> Vec<&'a Batch> {
1152    batches
1153        .iter()
1154        .filter(|batch| {
1155            batch.casts_shadow
1156                && batch
1157                    .range
1158                    .clone()
1159                    .any(|index| transmissive(instances[index as usize].material))
1160        })
1161        .collect()
1162}
1163
1164#[allow(clippy::too_many_arguments)]
1165fn draw_casters(
1166    pass: &mut wgpu::RenderPass<'_>,
1167    casters: &[&Batch],
1168    statics: bool,
1169    deformers: &wgpu::BindGroup,
1170    lighting: Option<&wgpu::BindGroup>,
1171    pipelines: &Pipelines,
1172    meshes: &[MeshSlot],
1173) {
1174    pass.set_bind_group(2, deformers, &[]);
1175    if casters.iter().any(|batch| batch.cut)
1176        && let Some(lighting) = lighting
1177    {
1178        pass.set_bind_group(3, lighting, &[]);
1179    }
1180    let mut current = None;
1181    for batch in casters.iter().filter(|batch| batch.moving != statics) {
1182        let kind = if batch.cut {
1183            2
1184        } else {
1185            usize::from(batch.clipped)
1186        };
1187        if current != Some(kind) {
1188            pass.set_pipeline(match &pipelines.cut {
1189                Some(cut) if batch.cut => &cut.shadow,
1190                _ => &pipelines.shadow[usize::from(batch.clipped)],
1191            });
1192            current = Some(kind);
1193        }
1194        let Some(mesh) = live_mesh(meshes, batch.mesh) else {
1195            continue;
1196        };
1197        pass.set_vertex_buffer(0, mesh.vertices.slice(..));
1198        pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
1199        pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
1200    }
1201}
1202
1203fn content_row_key(material: &Material) -> (bool, i32) {
1204    let layer = material.content_layer;
1205    (layer.active(), layer.slot)
1206}
1207
1208fn mix_key(key: u64, other: u64) -> u64 {
1209    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1210    key.hash(&mut hasher);
1211    other.hash(&mut hasher);
1212    hasher.finish()
1213}
1214
1215pub fn static_key(instances: &[Instance], batches: &[Batch], surfaces: &[InstanceSurface]) -> u64 {
1216    let mut hasher = std::collections::hash_map::DefaultHasher::new();
1217    for batch in shadow_batches(batches)
1218        .into_iter()
1219        .filter(|batch| !batch.moving)
1220    {
1221        batch.mesh.hash(&mut hasher);
1222        for index in batch.range.start as usize..batch.range.end as usize {
1223            for value in instances[index].model.iter().flatten() {
1224                value.to_bits().hash(&mut hasher);
1225            }
1226            if batch.clipped {
1227                for value in surfaces[index].clip.iter().flatten() {
1228                    value.to_bits().hash(&mut hasher);
1229                }
1230            }
1231        }
1232    }
1233    hasher.finish()
1234}
1235
1236pub fn sky_irradiance(sh: &[[f32; 3]; 9], normal: [f32; 3]) -> [f32; 3] {
1237    let length = (normal[0] * normal[0] + normal[1] * normal[1] + normal[2] * normal[2])
1238        .sqrt()
1239        .max(1e-8);
1240    let d = normal.map(|value| value / length);
1241    let basis = [
1242        0.282095,
1243        0.488603 * d[1],
1244        0.488603 * d[2],
1245        0.488603 * d[0],
1246        1.092548 * d[0] * d[1],
1247        1.092548 * d[1] * d[2],
1248        0.315392 * (3.0 * d[2] * d[2] - 1.0),
1249        1.092548 * d[0] * d[2],
1250        0.546274 * (d[0] * d[0] - d[1] * d[1]),
1251    ];
1252    [0, 1, 2].map(|channel| {
1253        (0..9)
1254            .map(|i| sh[i][channel] * basis[i])
1255            .sum::<f32>()
1256            .max(0.0)
1257    })
1258}
1259
1260pub fn ambient_irradiance(
1261    grid: Option<[f32; 3]>,
1262    sh: &[[f32; 3]; 9],
1263    normal: [f32; 3],
1264) -> [f32; 3] {
1265    match grid {
1266        Some(value) if value[0] >= 0.0 => value,
1267        _ => sky_irradiance(sh, normal),
1268    }
1269}
1270
1271fn cross(a: [f32; 3], b: [f32; 3]) -> [f32; 3] {
1272    [
1273        a[1] * b[2] - a[2] * b[1],
1274        a[2] * b[0] - a[0] * b[2],
1275        a[0] * b[1] - a[1] * b[0],
1276    ]
1277}
1278
1279fn normal_columns(model: Matrix) -> Result<[[f32; 4]; 3], String> {
1280    let a = [model[0][0], model[0][1], model[0][2]];
1281    let b = [model[1][0], model[1][1], model[1][2]];
1282    let c = [model[2][0], model[2][1], model[2][2]];
1283    let columns = [cross(b, c), cross(c, a), cross(a, b)];
1284    let determinant = a.iter().zip(columns[0]).map(|(x, y)| x * y).sum::<f32>();
1285    if !determinant.is_finite() || determinant.abs() < 1e-8 {
1286        return Err("instance model has a singular normal transform".into());
1287    }
1288    Ok(columns.map(|v| {
1289        [
1290            v[0] / determinant,
1291            v[1] / determinant,
1292            v[2] / determinant,
1293            0.0,
1294        ]
1295    }))
1296}
1297
1298pub fn shader_source() -> String {
1299    shader_source_with(&Gobo::default())
1300}
1301
1302pub fn shader_source_with(gobo: &Gobo) -> String {
1303    [
1304        BRDF,
1305        SHADOW_WGSL,
1306        LOD_WGSL,
1307        &deform_source(),
1308        SKIN_WGSL,
1309        TREE_SWAY_WGSL,
1310        pfx_materials::NOISE,
1311        pfx_materials::AGEING,
1312        maps::MAPS_WGSL,
1313        &maps::content_wgsl(),
1314        PROBE_WGSL,
1315        reflect::PROBE_WGSL,
1316        &canopy_light_wgsl(gobo),
1317        SHADER,
1318        LIGHTS_WGSL,
1319    ]
1320    .join("\n")
1321}
1322
1323pub fn deform_source() -> String {
1324    format!(
1325        "{}\n{}\n{}",
1326        pfx_geom::strip::ROLL_WGSL,
1327        BEND_WGSL.replace("pcg(", "bend_pcg("),
1328        deform::DEFORM_WGSL
1329    )
1330}
1331
1332pub fn card_source() -> String {
1333    format!("{SHADOW_WGSL}\n{LOD_WGSL}\n{CARD_SHADER}")
1334}
1335
1336fn target(
1337    device: &wgpu::Device,
1338    width: u32,
1339    height: u32,
1340    format: wgpu::TextureFormat,
1341    label: &str,
1342) -> (wgpu::Texture, wgpu::TextureView) {
1343    let texture = device.create_texture(&wgpu::TextureDescriptor {
1344        label: Some(label),
1345        size: wgpu::Extent3d {
1346            width,
1347            height,
1348            depth_or_array_layers: 1,
1349        },
1350        mip_level_count: 1,
1351        sample_count: 1,
1352        dimension: wgpu::TextureDimension::D2,
1353        format,
1354        usage: wgpu::TextureUsages::RENDER_ATTACHMENT
1355            | wgpu::TextureUsages::TEXTURE_BINDING
1356            | wgpu::TextureUsages::COPY_SRC,
1357        view_formats: &[],
1358    });
1359    let view = texture.create_view(&Default::default());
1360    (texture, view)
1361}
1362
1363fn storage(device: &wgpu::Device, label: &str, bytes: usize) -> wgpu::Buffer {
1364    device.create_buffer(&wgpu::BufferDescriptor {
1365        label: Some(label),
1366        size: bytes.max(16) as u64,
1367        usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
1368        mapped_at_creation: false,
1369    })
1370}
1371
1372fn buffer_entry(
1373    binding: u32,
1374    visibility: wgpu::ShaderStages,
1375    ty: wgpu::BufferBindingType,
1376) -> wgpu::BindGroupLayoutEntry {
1377    wgpu::BindGroupLayoutEntry {
1378        binding,
1379        visibility,
1380        ty: wgpu::BindingType::Buffer {
1381            ty,
1382            has_dynamic_offset: false,
1383            min_binding_size: None,
1384        },
1385        count: None,
1386    }
1387}
1388
1389fn texture_entry(
1390    binding: u32,
1391    view_dimension: wgpu::TextureViewDimension,
1392) -> wgpu::BindGroupLayoutEntry {
1393    wgpu::BindGroupLayoutEntry {
1394        binding,
1395        visibility: wgpu::ShaderStages::FRAGMENT,
1396        ty: wgpu::BindingType::Texture {
1397            sample_type: wgpu::TextureSampleType::Float { filterable: true },
1398            view_dimension,
1399            multisampled: false,
1400        },
1401        count: None,
1402    }
1403}
1404
1405fn sampler_entry(binding: u32) -> wgpu::BindGroupLayoutEntry {
1406    wgpu::BindGroupLayoutEntry {
1407        binding,
1408        visibility: wgpu::ShaderStages::FRAGMENT,
1409        ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Filtering),
1410        count: None,
1411    }
1412}
1413
1414pub fn scene_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1415    let read = wgpu::BufferBindingType::Storage { read_only: true };
1416    vec![
1417        buffer_entry(
1418            0,
1419            wgpu::ShaderStages::VERTEX_FRAGMENT,
1420            wgpu::BufferBindingType::Uniform,
1421        ),
1422        buffer_entry(1, wgpu::ShaderStages::VERTEX_FRAGMENT, read),
1423        buffer_entry(2, wgpu::ShaderStages::FRAGMENT, read),
1424        buffer_entry(3, wgpu::ShaderStages::VERTEX, read),
1425        buffer_entry(SKIN_BINDING, wgpu::ShaderStages::VERTEX, read),
1426    ]
1427}
1428
1429pub fn shadow_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1430    vec![
1431        buffer_entry(
1432            0,
1433            wgpu::ShaderStages::FRAGMENT,
1434            wgpu::BufferBindingType::Uniform,
1435        ),
1436        wgpu::BindGroupLayoutEntry {
1437            binding: 1,
1438            visibility: wgpu::ShaderStages::FRAGMENT,
1439            ty: wgpu::BindingType::Texture {
1440                sample_type: wgpu::TextureSampleType::Depth,
1441                view_dimension: wgpu::TextureViewDimension::D2Array,
1442                multisampled: false,
1443            },
1444            count: None,
1445        },
1446        wgpu::BindGroupLayoutEntry {
1447            binding: 2,
1448            visibility: wgpu::ShaderStages::FRAGMENT,
1449            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
1450            count: None,
1451        },
1452        wgpu::BindGroupLayoutEntry {
1453            binding: 4,
1454            visibility: wgpu::ShaderStages::FRAGMENT,
1455            ty: wgpu::BindingType::Texture {
1456                sample_type: wgpu::TextureSampleType::Uint,
1457                view_dimension: wgpu::TextureViewDimension::D2Array,
1458                multisampled: false,
1459            },
1460            count: None,
1461        },
1462        buffer_entry(
1463            5,
1464            wgpu::ShaderStages::FRAGMENT,
1465            wgpu::BufferBindingType::Uniform,
1466        ),
1467        wgpu::BindGroupLayoutEntry {
1468            binding: 6,
1469            visibility: wgpu::ShaderStages::FRAGMENT,
1470            ty: wgpu::BindingType::Texture {
1471                sample_type: wgpu::TextureSampleType::Depth,
1472                view_dimension: wgpu::TextureViewDimension::D2Array,
1473                multisampled: false,
1474            },
1475            count: None,
1476        },
1477        wgpu::BindGroupLayoutEntry {
1478            binding: 7,
1479            visibility: wgpu::ShaderStages::FRAGMENT,
1480            ty: wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison),
1481            count: None,
1482        },
1483    ]
1484}
1485
1486pub fn cascade_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1487    vec![
1488        wgpu::BindGroupLayoutEntry {
1489            binding: 3,
1490            visibility: wgpu::ShaderStages::VERTEX,
1491            ty: wgpu::BindingType::Buffer {
1492                ty: wgpu::BufferBindingType::Uniform,
1493                has_dynamic_offset: true,
1494                min_binding_size: NonZeroU64::new(CASTER_BYTES),
1495            },
1496            count: None,
1497        },
1498        buffer_entry(
1499            8,
1500            wgpu::ShaderStages::VERTEX_FRAGMENT,
1501            wgpu::BufferBindingType::Storage { read_only: true },
1502        ),
1503    ]
1504}
1505
1506pub fn deformer_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1507    (0..2)
1508        .map(|binding| {
1509            buffer_entry(
1510                binding,
1511                wgpu::ShaderStages::VERTEX,
1512                wgpu::BufferBindingType::Storage { read_only: true },
1513            )
1514        })
1515        .collect()
1516}
1517
1518pub fn card_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1519    vec![
1520        buffer_entry(
1521            0,
1522            wgpu::ShaderStages::VERTEX_FRAGMENT,
1523            wgpu::BufferBindingType::Uniform,
1524        ),
1525        buffer_entry(
1526            1,
1527            wgpu::ShaderStages::VERTEX,
1528            wgpu::BufferBindingType::Storage { read_only: true },
1529        ),
1530        texture_entry(2, wgpu::TextureViewDimension::D2),
1531        sampler_entry(3),
1532    ]
1533}
1534
1535pub fn lighting_entries() -> Vec<wgpu::BindGroupLayoutEntry> {
1536    let read = wgpu::BufferBindingType::Storage { read_only: true };
1537    let uniform = wgpu::BufferBindingType::Uniform;
1538    let fragment = wgpu::ShaderStages::FRAGMENT;
1539    vec![
1540        texture_entry(0, wgpu::TextureViewDimension::D2Array),
1541        texture_entry(1, wgpu::TextureViewDimension::D2Array),
1542        texture_entry(2, wgpu::TextureViewDimension::D2Array),
1543        texture_entry(3, wgpu::TextureViewDimension::D2Array),
1544        sampler_entry(4),
1545        buffer_entry(5, fragment, read),
1546        buffer_entry(6, fragment, read),
1547        buffer_entry(7, fragment, read),
1548        buffer_entry(8, fragment, uniform),
1549        texture_entry(9, wgpu::TextureViewDimension::D2),
1550        sampler_entry(10),
1551        buffer_entry(11, fragment, uniform),
1552        buffer_entry(12, fragment, uniform),
1553        texture_entry(13, wgpu::TextureViewDimension::Cube),
1554        texture_entry(14, wgpu::TextureViewDimension::CubeArray),
1555        sampler_entry(15),
1556        texture_entry(16, wgpu::TextureViewDimension::D2),
1557        buffer_entry(17, fragment, uniform),
1558        texture_entry(CONTENT_BINDING, wgpu::TextureViewDimension::D2),
1559        texture_entry(CONTENT_BINDING + 1, wgpu::TextureViewDimension::D2),
1560        texture_entry(CONTENT_BINDING + 2, wgpu::TextureViewDimension::D2),
1561        texture_entry(CONTENT_BINDING + 3, wgpu::TextureViewDimension::D2),
1562        sampler_entry(CONTENT_BINDING + CONTENT_SLOTS as u32),
1563        texture_entry(CONTACT_BINDING, wgpu::TextureViewDimension::D2),
1564        buffer_entry(CONTACT_BINDING + 1, fragment, uniform),
1565        buffer_entry(CONTACT_BINDING + 2, fragment, uniform),
1566        texture_entry(reflect::GUIDE_BINDING, wgpu::TextureViewDimension::D2),
1567        buffer_entry(CAUSTIC_BINDING, fragment, uniform),
1568    ]
1569}
1570
1571fn layout(
1572    device: &wgpu::Device,
1573    label: &str,
1574    entries: &[wgpu::BindGroupLayoutEntry],
1575) -> wgpu::BindGroupLayout {
1576    device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
1577        label: Some(label),
1578        entries,
1579    })
1580}
1581
1582fn vertex_layout() -> Option<wgpu::VertexBufferLayout<'static>> {
1583    const ATTRIBUTES: [wgpu::VertexAttribute; 4] = wgpu::vertex_attr_array![
1584        0 => Float32x4,
1585        1 => Float32x4,
1586        2 => Float32x4,
1587        3 => Float32x4,
1588    ];
1589    Some(wgpu::VertexBufferLayout {
1590        array_stride: std::mem::size_of::<Vertex>() as u64,
1591        step_mode: wgpu::VertexStepMode::Vertex,
1592        attributes: &ATTRIBUTES,
1593    })
1594}
1595
1596fn prepass_targets() -> [Option<wgpu::ColorTargetState>; 2] {
1597    [
1598        Some(wgpu::ColorTargetState {
1599            format: wgpu::TextureFormat::Rg16Float,
1600            blend: None,
1601            write_mask: wgpu::ColorWrites::ALL,
1602        }),
1603        Some(wgpu::ColorTargetState {
1604            format: wgpu::TextureFormat::R32Uint,
1605            blend: None,
1606            write_mask: wgpu::ColorWrites::ALL,
1607        }),
1608    ]
1609}
1610
1611fn opaque_targets() -> [Option<wgpu::ColorTargetState>; 2] {
1612    [
1613        Some(wgpu::ColorTargetState {
1614            format: wgpu::TextureFormat::Rgba16Float,
1615            blend: None,
1616            write_mask: wgpu::ColorWrites::ALL,
1617        }),
1618        Some(wgpu::ColorTargetState {
1619            format: NORMAL_FORMAT,
1620            blend: None,
1621            write_mask: wgpu::ColorWrites::ALL,
1622        }),
1623    ]
1624}
1625
1626struct PipelineSpec<'a> {
1627    label: &'a str,
1628    layout: &'a wgpu::PipelineLayout,
1629    module: &'a wgpu::ShaderModule,
1630    vertex: &'a str,
1631    fragment: Option<&'a str>,
1632    buffers: &'a [Option<wgpu::VertexBufferLayout<'a>>],
1633    targets: &'a [Option<wgpu::ColorTargetState>],
1634    cull: Option<wgpu::Face>,
1635    write_depth: bool,
1636    cache: Option<&'a wgpu::PipelineCache>,
1637}
1638
1639fn prepass_pipelines(
1640    device: &wgpu::Device,
1641    layout: &wgpu::PipelineLayout,
1642    module: &wgpu::ShaderModule,
1643    cut: bool,
1644) -> [wgpu::RenderPipeline; 2] {
1645    let buffers = [vertex_layout()];
1646    [Some(wgpu::Face::Back), None].map(|cull| {
1647        pipeline_with(
1648            device,
1649            PipelineSpec {
1650                label: "live depth and velocity",
1651                layout,
1652                module,
1653                vertex: "vertex_main",
1654                fragment: Some("prepass_main"),
1655                buffers: &buffers,
1656                targets: &prepass_targets(),
1657                cull,
1658                write_depth: true,
1659                cache: None,
1660            },
1661            &[("use_content_cutout", f64::from(u8::from(cut)))],
1662        )
1663    })
1664}
1665
1666fn pipeline(device: &wgpu::Device, spec: PipelineSpec<'_>) -> wgpu::RenderPipeline {
1667    pipeline_with(device, spec, &[])
1668}
1669
1670fn pipeline_with(
1671    device: &wgpu::Device,
1672    spec: PipelineSpec<'_>,
1673    constants: &[(&str, f64)],
1674) -> wgpu::RenderPipeline {
1675    device.create_render_pipeline(&wgpu::RenderPipelineDescriptor {
1676        label: Some(spec.label),
1677        layout: Some(spec.layout),
1678        vertex: wgpu::VertexState {
1679            module: spec.module,
1680            entry_point: Some(spec.vertex),
1681            buffers: spec.buffers,
1682            compilation_options: Default::default(),
1683        },
1684        primitive: wgpu::PrimitiveState {
1685            cull_mode: spec.cull,
1686            ..Default::default()
1687        },
1688        depth_stencil: Some(wgpu::DepthStencilState {
1689            format: wgpu::TextureFormat::Depth32Float,
1690            depth_write_enabled: Some(spec.write_depth),
1691            depth_compare: Some(if spec.write_depth {
1692                wgpu::CompareFunction::Less
1693            } else {
1694                wgpu::CompareFunction::LessEqual
1695            }),
1696            stencil: Default::default(),
1697            bias: Default::default(),
1698        }),
1699        multisample: Default::default(),
1700        fragment: spec.fragment.map(|entry| wgpu::FragmentState {
1701            module: spec.module,
1702            entry_point: Some(entry),
1703            targets: spec.targets,
1704            compilation_options: wgpu::PipelineCompilationOptions {
1705                constants,
1706                ..Default::default()
1707            },
1708        }),
1709        multiview_mask: None,
1710        cache: spec.cache,
1711    })
1712}
1713
1714fn default_sky() -> SkySource {
1715    SkySource::Analytic(AnalyticSky::new(
1716        Daylight {
1717            hour: 12.0,
1718            day: Daylight::DAY,
1719            latitude: Daylight::LATITUDE,
1720            heading: Daylight::HEADING,
1721        },
1722        REFERENCE_HOUR,
1723        2.5,
1724        [0.3; 3],
1725    ))
1726}
1727
1728fn new_targets(gpu: &Gpu, width: u32, height: u32) -> Result<FrameTargets, String> {
1729    let hdr = gpu.offscreen(width, height, wgpu::TextureFormat::Rgba16Float)?;
1730    let device = &gpu.device;
1731    let (depth, depth_view) = target(
1732        device,
1733        width,
1734        height,
1735        wgpu::TextureFormat::Depth32Float,
1736        "live depth",
1737    );
1738    let (velocity, velocity_view) = target(
1739        device,
1740        width,
1741        height,
1742        wgpu::TextureFormat::Rg16Float,
1743        "live velocity",
1744    );
1745    let (ids, ids_view) = target(
1746        device,
1747        width,
1748        height,
1749        wgpu::TextureFormat::R32Uint,
1750        "live ids",
1751    );
1752    let (normal_roughness, normal_roughness_view) =
1753        target(device, width, height, NORMAL_FORMAT, "live normals");
1754    Ok(FrameTargets {
1755        hdr,
1756        depth,
1757        depth_view,
1758        velocity,
1759        velocity_view,
1760        ids,
1761        ids_view,
1762        normal_roughness,
1763        normal_roughness_view,
1764        width,
1765        height,
1766    })
1767}
1768
1769fn solid(
1770    device: &wgpu::Device,
1771    queue: &wgpu::Queue,
1772    layers: u32,
1773    dimension: wgpu::TextureViewDimension,
1774    label: &str,
1775) -> wgpu::TextureView {
1776    let texture = device.create_texture_with_data(
1777        queue,
1778        &wgpu::TextureDescriptor {
1779            label: Some(label),
1780            size: wgpu::Extent3d {
1781                width: 1,
1782                height: 1,
1783                depth_or_array_layers: layers,
1784            },
1785            mip_level_count: 1,
1786            sample_count: 1,
1787            dimension: wgpu::TextureDimension::D2,
1788            format: wgpu::TextureFormat::Rgba16Float,
1789            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1790            view_formats: &[],
1791        },
1792        wgpu::util::TextureDataOrder::LayerMajor,
1793        &vec![0; 8 * layers as usize],
1794    );
1795    texture.create_view(&wgpu::TextureViewDescriptor {
1796        dimension: Some(dimension),
1797        ..Default::default()
1798    })
1799}
1800
1801impl Frame {
1802    pub fn new(gpu: Gpu, width: u32, height: u32) -> Result<Self, String> {
1803        if width == 0 || height == 0 {
1804            return Err("viewport is empty".into());
1805        }
1806        let targets = new_targets(&gpu, width, height)?;
1807        let device = &gpu.device;
1808        let uniform = device.create_buffer(&wgpu::BufferDescriptor {
1809            label: Some("live frame uniforms"),
1810            size: std::mem::size_of::<Uniforms>() as u64,
1811            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1812            mapped_at_creation: false,
1813        });
1814        let instances = storage(device, "live instances", std::mem::size_of::<GpuInstance>());
1815        let materials = storage(device, "live materials", PackedMaterial::SIZE);
1816        let map_materials = storage(
1817            device,
1818            "live map materials",
1819            std::mem::size_of::<GpuMapMaterial>(),
1820        );
1821        let sway = storage(device, "live sway", std::mem::size_of::<GpuSway>());
1822        let skins = SkinStore::new(device);
1823        let scene_layout = layout(device, "live scene layout", &scene_entries());
1824        let scene_group = Self::make_scene_group(
1825            device,
1826            &scene_layout,
1827            [&uniform, &instances, &materials, &sway, skins.buffer()],
1828        );
1829        let shadow_layout = layout(device, "live shadow seam", &shadow_entries());
1830        let mut fallback_sample = [0u8; SAMPLE_UNIFORM_BYTES];
1831        fallback_sample[60..64].copy_from_slice(&1.0f32.to_le_bytes());
1832        let shadow_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1833            label: Some("live shadow sample"),
1834            contents: &fallback_sample,
1835            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1836        });
1837        let shadow_atlas = device.create_texture(&wgpu::TextureDescriptor {
1838            label: Some("live fallback shadow"),
1839            size: wgpu::Extent3d {
1840                width: 1,
1841                height: 1,
1842                depth_or_array_layers: 3,
1843            },
1844            mip_level_count: 1,
1845            sample_count: 1,
1846            dimension: wgpu::TextureDimension::D2,
1847            format: wgpu::TextureFormat::Depth32Float,
1848            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::RENDER_ATTACHMENT,
1849            view_formats: &[],
1850        });
1851        let shadow_view = shadow_atlas.create_view(&wgpu::TextureViewDescriptor {
1852            dimension: Some(wgpu::TextureViewDimension::D2Array),
1853            ..Default::default()
1854        });
1855        let shadow_sampler = device.create_sampler(&wgpu::SamplerDescriptor {
1856            compare: Some(wgpu::CompareFunction::LessEqual),
1857            ..Default::default()
1858        });
1859        let lights = Lights::new(device);
1860        let shadow_group = Self::make_shadow_group(
1861            device,
1862            &shadow_layout,
1863            &shadow_uniform,
1864            &shadow_view,
1865            &shadow_sampler,
1866            lights.empty_transmission(),
1867            &lights,
1868        );
1869        let cascade_layout = layout(device, "live cascade", &cascade_entries());
1870        let cascade_align = device.limits().min_uniform_buffer_offset_alignment.max(256);
1871        let cascade_uniform = device.create_buffer(&wgpu::BufferDescriptor {
1872            label: Some("live cascades"),
1873            size: u64::from(cascade_align) * CASTER_SLOTS as u64,
1874            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1875            mapped_at_creation: false,
1876        });
1877        let cascade_group =
1878            Self::make_cascade_group(device, &cascade_layout, &cascade_uniform, lights.table());
1879        let lighting_layout = layout(device, "live lighting", &lighting_entries());
1880        let sky_light = device.create_buffer(&wgpu::BufferDescriptor {
1881            label: Some("live sky light"),
1882            size: std::mem::size_of::<SkyLight>() as u64,
1883            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1884            mapped_at_creation: false,
1885        });
1886        let ambient_light = device.create_buffer(&wgpu::BufferDescriptor {
1887            label: Some("live authored ambient"),
1888            size: std::mem::size_of::<SkyLight>() as u64,
1889            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1890            mapped_at_creation: false,
1891        });
1892        let contact_uniform = device.create_buffer_init(&wgpu::util::BufferInitDescriptor {
1893            label: Some("live contact field"),
1894            contents: bytemuck::bytes_of(&ContactUniform::off()),
1895            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1896        });
1897        let reflection_selection = device.create_buffer(&wgpu::BufferDescriptor {
1898            label: Some("live reflection selection"),
1899            size: std::mem::size_of::<ReflectionSelection>() as u64,
1900            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1901            mapped_at_creation: false,
1902        });
1903        let deformers = DeformerBuffer::new(device);
1904        let maps = MapTextures::new(device, &gpu.queue, &MapImages::default())?;
1905        let content = ContentSlots::new(device, &gpu.queue);
1906        let probes = ProbeLighting::fallback(device, &gpu.queue, [-1.0; 3]);
1907        let mut sky = SkyPass::new(
1908            device,
1909            &gpu.queue,
1910            wgpu::TextureFormat::Rgba16Float,
1911            Some(wgpu::TextureFormat::Depth32Float),
1912            default_sky(),
1913            SKY_EDGE,
1914        );
1915        while !sky.lighting.complete() {
1916            sky.update(
1917                &gpu.queue,
1918                [0.0, 0.0, -1.0],
1919                [1.0, 0.0, 0.0],
1920                [0.0, 1.0, 0.0],
1921            );
1922        }
1923        let fallbacks = Fallbacks {
1924            black: solid(
1925                device,
1926                &gpu.queue,
1927                1,
1928                wgpu::TextureViewDimension::D2,
1929                "live black",
1930            ),
1931            local_probes: solid(
1932                device,
1933                &gpu.queue,
1934                6,
1935                wgpu::TextureViewDimension::CubeArray,
1936                "live local probes",
1937            ),
1938            canopy_params: device.create_buffer(&wgpu::BufferDescriptor {
1939                label: Some("live canopy fallback"),
1940                size: 96,
1941                usage: wgpu::BufferUsages::UNIFORM,
1942                mapped_at_creation: false,
1943            }),
1944            linear: device.create_sampler(&wgpu::SamplerDescriptor {
1945                label: Some("live reflection sampler"),
1946                mag_filter: wgpu::FilterMode::Linear,
1947                min_filter: wgpu::FilterMode::Linear,
1948                mipmap_filter: wgpu::MipmapFilterMode::Linear,
1949                ..Default::default()
1950            }),
1951        };
1952        let pipelines = Self::make_pipelines(
1953            device,
1954            [
1955                &scene_layout,
1956                &shadow_layout,
1957                &deformers.layout,
1958                &lighting_layout,
1959            ],
1960            &cascade_layout,
1961        );
1962        let profiler = GpuProfiler::new(device, &gpu.queue);
1963        Ok(Self {
1964            targets,
1965            meshes: Vec::new(),
1966            uniform,
1967            instances,
1968            materials,
1969            map_materials,
1970            sway,
1971            sway_data: Vec::new(),
1972            skins,
1973            free_meshes: Vec::new(),
1974            mesh_buffers_live: Arc::new(AtomicUsize::new(0)),
1975            mesh_epoch: 0,
1976            sway_free: FreeRanges::default(),
1977            instance_capacity: 1,
1978            material_capacity: 1,
1979            scene_layout,
1980            scene_group,
1981            shadow_group,
1982            shadow_layout,
1983            shadow_uniform,
1984            _shadow_atlas: shadow_atlas,
1985            _shadow_view: shadow_view,
1986            _shadow_sampler: shadow_sampler,
1987            cascade_uniform,
1988            cascade_group,
1989            cascade_layout,
1990            cascade_align,
1991            lights,
1992            lighting_layout,
1993            lighting_group: None,
1994            sky_light,
1995            ambient: Ambient::Sky,
1996            ambient_light,
1997            contact: None,
1998            contact_uniform,
1999            reflection_selection,
2000            local_reflections: RefCell::new(None),
2001            deformers,
2002            maps,
2003            map_layers: [0; 4],
2004            content: RefCell::new(content),
2005            content_epoch: Cell::new(0),
2006            caustic: RefCell::new(None),
2007            surfaces: RefCell::new(Vec::new()),
2008            probes,
2009            sky,
2010            ssr: None,
2011            viewport: [width, height],
2012            foliage: None,
2013            fallbacks,
2014            pipelines,
2015            canopy_gobo: Gobo::default(),
2016            shadow_sample: fallback_sample,
2017            instance_features: Vec::new(),
2018            reflections_planned: false,
2019            opaque_on: Cell::new(OpaqueFeatures::NONE),
2020            opaque_off: Cell::new(OpaqueFeatures::NONE),
2021            opaque_drawn: Vec::new(),
2022            jitter: [0.0; 2],
2023            profiler,
2024            gpu,
2025        })
2026    }
2027
2028    fn make_pipelines(
2029        device: &wgpu::Device,
2030        layouts: [&wgpu::BindGroupLayout; 4],
2031        cascade: &wgpu::BindGroupLayout,
2032    ) -> Pipelines {
2033        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2034            label: Some("live frame layout"),
2035            bind_group_layouts: &layouts.map(Some),
2036            immediate_size: 0,
2037        });
2038        let shadow_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2039            label: Some("live shadow layout"),
2040            bind_group_layouts: &[Some(layouts[0]), Some(cascade), Some(layouts[2])],
2041            immediate_size: 0,
2042        });
2043        let cut_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2044            label: Some("live cut shadow layout"),
2045            bind_group_layouts: &[
2046                Some(layouts[0]),
2047                Some(cascade),
2048                Some(layouts[2]),
2049                Some(layouts[3]),
2050            ],
2051            immediate_size: 0,
2052        });
2053        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2054            label: Some("live frame shader"),
2055            source: wgpu::ShaderSource::Wgsl(shader_source().into()),
2056        });
2057        let buffers = [vertex_layout()];
2058        let prepass = prepass_pipelines(device, &pipeline_layout, &module, false);
2059        let shadow = [
2060            ("shadow_vertex", None),
2061            ("shadow_clip_vertex", Some("shadow_clip_fragment")),
2062        ]
2063        .map(|(vertex, fragment)| {
2064            pipeline(
2065                device,
2066                PipelineSpec {
2067                    label: "live shadow casters",
2068                    layout: &shadow_layout,
2069                    module: &module,
2070                    vertex,
2071                    fragment,
2072                    buffers: &buffers,
2073                    targets: &[],
2074                    cull: None,
2075                    write_depth: true,
2076                    cache: None,
2077                },
2078            )
2079        });
2080        let transmit = pipeline(
2081            device,
2082            PipelineSpec {
2083                label: "live transmissive casters",
2084                layout: &shadow_layout,
2085                module: &module,
2086                vertex: "transmit_vertex",
2087                fragment: Some("transmit_fragment"),
2088                buffers: &buffers,
2089                targets: &[Some(wgpu::ColorTargetState {
2090                    format: crate::shadow::TRANSMISSION_FORMAT,
2091                    blend: None,
2092                    write_mask: wgpu::ColorWrites::ALL,
2093                })],
2094                cull: None,
2095                write_depth: true,
2096                cache: None,
2097            },
2098        );
2099        let (sender, arrivals) = channel();
2100        Pipelines {
2101            prepass,
2102            shadow,
2103            cut: None,
2104            cut_layout,
2105            transmit,
2106            layout: pipeline_layout,
2107            module,
2108            opaque: HashMap::new(),
2109            cache: None,
2110            pending: HashSet::new(),
2111            arrivals,
2112            sender,
2113            made_here: 0,
2114            workers: warm::default_workers(),
2115        }
2116    }
2117
2118    fn make_cascade_group(
2119        device: &wgpu::Device,
2120        layout: &wgpu::BindGroupLayout,
2121        uniform: &wgpu::Buffer,
2122        tints: &wgpu::Buffer,
2123    ) -> wgpu::BindGroup {
2124        device.create_bind_group(&wgpu::BindGroupDescriptor {
2125            label: Some("live cascades"),
2126            layout,
2127            entries: &[
2128                wgpu::BindGroupEntry {
2129                    binding: 3,
2130                    resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
2131                        buffer: uniform,
2132                        offset: 0,
2133                        size: NonZeroU64::new(CASTER_BYTES),
2134                    }),
2135                },
2136                wgpu::BindGroupEntry {
2137                    binding: 8,
2138                    resource: tints.as_entire_binding(),
2139                },
2140            ],
2141        })
2142    }
2143
2144    fn make_scene_group(
2145        device: &wgpu::Device,
2146        layout: &wgpu::BindGroupLayout,
2147        buffers: [&wgpu::Buffer; 5],
2148    ) -> wgpu::BindGroup {
2149        let entries: Vec<wgpu::BindGroupEntry<'_>> = buffers
2150            .iter()
2151            .enumerate()
2152            .map(|(binding, buffer)| wgpu::BindGroupEntry {
2153                binding: binding as u32,
2154                resource: buffer.as_entire_binding(),
2155            })
2156            .collect();
2157        device.create_bind_group(&wgpu::BindGroupDescriptor {
2158            label: Some("live scene"),
2159            layout,
2160            entries: &entries,
2161        })
2162    }
2163
2164    fn rebuild_scene_group(&mut self) {
2165        self.scene_group = Self::make_scene_group(
2166            &self.gpu.device,
2167            &self.scene_layout,
2168            [
2169                &self.uniform,
2170                &self.instances,
2171                &self.materials,
2172                &self.sway,
2173                self.skins.buffer(),
2174            ],
2175        );
2176    }
2177
2178    fn make_shadow_group(
2179        device: &wgpu::Device,
2180        layout: &wgpu::BindGroupLayout,
2181        uniform: &wgpu::Buffer,
2182        view: &wgpu::TextureView,
2183        sampler: &wgpu::Sampler,
2184        transmission: &wgpu::TextureView,
2185        lights: &Lights,
2186    ) -> wgpu::BindGroup {
2187        device.create_bind_group(&wgpu::BindGroupDescriptor {
2188            label: Some("live shadow sample"),
2189            layout,
2190            entries: &[
2191                wgpu::BindGroupEntry {
2192                    binding: 0,
2193                    resource: uniform.as_entire_binding(),
2194                },
2195                wgpu::BindGroupEntry {
2196                    binding: 1,
2197                    resource: wgpu::BindingResource::TextureView(view),
2198                },
2199                wgpu::BindGroupEntry {
2200                    binding: 2,
2201                    resource: wgpu::BindingResource::Sampler(sampler),
2202                },
2203                wgpu::BindGroupEntry {
2204                    binding: 4,
2205                    resource: wgpu::BindingResource::TextureView(transmission),
2206                },
2207                wgpu::BindGroupEntry {
2208                    binding: 5,
2209                    resource: lights.uniform().as_entire_binding(),
2210                },
2211                wgpu::BindGroupEntry {
2212                    binding: 6,
2213                    resource: wgpu::BindingResource::TextureView(lights.faces_view()),
2214                },
2215                wgpu::BindGroupEntry {
2216                    binding: 7,
2217                    resource: wgpu::BindingResource::Sampler(lights.sampler()),
2218                },
2219            ],
2220        })
2221    }
2222
2223    pub fn set_shadows(&mut self, shadows: &Shadows, fit: &Fit) {
2224        self.shadow_sample = shadows.sample_uniform(fit);
2225        self.gpu
2226            .queue
2227            .write_buffer(&self.shadow_uniform, 0, &self.shadow_sample);
2228        self.shadow_group = Self::make_shadow_group(
2229            &self.gpu.device,
2230            &self.shadow_layout,
2231            &self.shadow_uniform,
2232            shadows.atlas_view(),
2233            shadows.sampler(),
2234            shadows
2235                .transmission_view()
2236                .unwrap_or(self.lights.empty_transmission()),
2237            &self.lights,
2238        );
2239    }
2240
2241    pub fn set_probe_corner_floor(&self, floor: f32) -> Result<(), String> {
2242        self.probes.set_corner_floor(&self.gpu.queue, floor)
2243    }
2244
2245    pub fn probe_corner_floor(&self) -> f32 {
2246        self.probes.corner_floor()
2247    }
2248
2249    pub fn set_reflection_occlusion(&mut self, threshold: f32) -> Result<(), String> {
2250        self.probes
2251            .set_reflection_occlusion(&self.gpu.queue, threshold)
2252    }
2253
2254    pub fn set_sky_visibility(&mut self, on: bool) {
2255        self.probes.set_sky_visibility(&self.gpu.queue, on);
2256    }
2257
2258    pub fn set_opaque_on(&self, on: OpaqueFeatures) {
2259        self.opaque_on.set(on);
2260    }
2261
2262    pub fn set_opaque_off(&self, off: OpaqueFeatures) {
2263        self.opaque_off.set(off);
2264    }
2265
2266    pub fn warm_opaque(&mut self, features: OpaqueFeatures, lit: bool) {
2267        for key in OpaqueKey::both_sides(features, lit) {
2268            self.pipelines.ensure_opaque(&self.gpu.device, key);
2269        }
2270    }
2271
2272    pub fn warm(&mut self, first: &[OpaqueKey], later: &[OpaqueKey]) -> Warming {
2273        self.pipelines.warm(&self.gpu.device, first, later)
2274    }
2275
2276    pub fn set_warm_workers(&mut self, workers: usize) {
2277        self.pipelines.workers = workers.max(1);
2278    }
2279
2280    pub fn opaque_pipelines(&self) -> Vec<OpaqueKey> {
2281        self.pipelines.opaque.keys().copied().collect()
2282    }
2283
2284    pub fn opaque_made_on_render_thread(&self) -> usize {
2285        self.pipelines.made_here
2286    }
2287
2288    pub fn use_pipeline_cache(&mut self, path: &std::path::Path) -> CacheLoad {
2289        let (cache, load) = PipelineCache::open(&self.gpu.device, &self.gpu.info, path);
2290        if cache.is_some() {
2291            self.pipelines.cache = cache;
2292        }
2293        load
2294    }
2295
2296    pub(crate) fn pipeline_cache(&self) -> Option<&wgpu::PipelineCache> {
2297        self.pipelines.cache.as_ref().map(PipelineCache::cache)
2298    }
2299
2300    pub fn save_pipeline_cache(&self) -> Result<Option<usize>, String> {
2301        self.pipelines
2302            .cache
2303            .as_ref()
2304            .map_or(Ok(None), |cache| cache.save().map(Some))
2305    }
2306
2307    pub fn set_reflections_planned(&mut self, planned: bool) {
2308        self.reflections_planned = planned;
2309    }
2310
2311    pub fn opaque_keys(
2312        &self,
2313        scene: &Scene<'_>,
2314        shadows: Option<(&Shadows, &Fit)>,
2315    ) -> Result<Vec<OpaqueKey>, String> {
2316        if scene.materials.is_empty() {
2317            return Err("scene needs at least one material".into());
2318        }
2319        let surfaces = self.scene_surfaces(scene.instances.len());
2320        let packed = self.pack_instances(scene, &surfaces, &self.skins.layout())?;
2321        let map_materials = self.pack_map_materials(scene);
2322        let instance_features = scene_instance_features(&packed, &map_materials, scene.materials);
2323        let batches = surface_batches(
2324            scene.instances,
2325            |mesh| self.deforms(mesh),
2326            |index| surfaces[index].clipped(),
2327            |index| surfaces[index].cutout,
2328        );
2329        let sample = shadows.map_or(self.shadow_sample, |(shadows, fit)| {
2330            shadows.sample_uniform(fit)
2331        });
2332        let features = self.batch_features(
2333            &batches,
2334            self.scene_features_from(&sample),
2335            &instance_features,
2336        );
2337        let lit = self.lights.extended();
2338        let mut keys: Vec<OpaqueKey> = Vec::new();
2339        for (batch, features) in batches.iter().zip(features) {
2340            let key = OpaqueKey {
2341                features,
2342                lit,
2343                two_sided: batch.two_sided,
2344            };
2345            if !batch.shadow_only && !keys.contains(&key) {
2346                keys.push(key);
2347            }
2348        }
2349        Ok(keys)
2350    }
2351
2352    pub fn opaque_drawn(&self) -> &[OpaqueFeatures] {
2353        &self.opaque_drawn
2354    }
2355
2356    pub fn scene_features(&self) -> OpaqueFeatures {
2357        self.scene_features_from(&self.shadow_sample)
2358    }
2359
2360    fn scene_features_from(&self, sample: &[u8; SAMPLE_UNIFORM_BYTES]) -> OpaqueFeatures {
2361        let read = |offset: usize| {
2362            f32::from_le_bytes(sample[offset..offset + 4].try_into().expect("four bytes"))
2363        };
2364        let unshadowed = read(60) > 0.5;
2365        let shadowed = !unshadowed;
2366        let soft = shadowed && read(256) > 0.0;
2367        OpaqueFeatures {
2368            sun_shadow: shadowed,
2369            soft_search: soft,
2370            soft_filter: soft,
2371            cascade_blend: shadowed && read(28) > 0.0,
2372            probes: self.probes.live(),
2373            local_reflections: self.local_reflections.borrow().is_some(),
2374            ssr: self.ssr.is_some() || self.reflections_planned,
2375            canopy: self.foliage.is_some(),
2376            ..OpaqueFeatures::NONE
2377        }
2378    }
2379
2380    pub fn opaque_features(&self, batches: &[Batch]) -> Vec<OpaqueFeatures> {
2381        self.batch_features(batches, self.scene_features(), &self.instance_features)
2382    }
2383
2384    fn batch_features(
2385        &self,
2386        batches: &[Batch],
2387        scene: OpaqueFeatures,
2388        instance_features: &[OpaqueFeatures],
2389    ) -> Vec<OpaqueFeatures> {
2390        batches
2391            .iter()
2392            .map(|batch| {
2393                let used = batch
2394                    .range
2395                    .clone()
2396                    .filter_map(|index| instance_features.get(index as usize))
2397                    .fold(scene, |all, features| all.union(*features));
2398                used.union(self.opaque_on.get())
2399                    .without(self.opaque_off.get())
2400            })
2401            .collect()
2402    }
2403
2404    fn set_unshadowed(&mut self) {
2405        self.shadow_group = Self::make_shadow_group(
2406            &self.gpu.device,
2407            &self.shadow_layout,
2408            &self.shadow_uniform,
2409            &self._shadow_view,
2410            &self._shadow_sampler,
2411            self.lights.empty_transmission(),
2412            &self.lights,
2413        );
2414    }
2415
2416    pub fn set_lights(&mut self, lights: &[LocalLight]) -> Result<(), String> {
2417        self.lights.set(lights)
2418    }
2419
2420    pub fn set_light_quality(&mut self, quality: LightQuality) -> Result<(), String> {
2421        self.lights.set_quality(quality)
2422    }
2423
2424    pub fn set_transmission(&mut self, tints: &[Option<[f32; 3]>]) -> Result<(), String> {
2425        self.lights.set_transmission(tints)
2426    }
2427
2428    pub fn lights(&self) -> &Lights {
2429        &self.lights
2430    }
2431
2432    pub fn set_probe_emission(&mut self, scale: f32) -> Result<(), String> {
2433        self.probes.set_emission(&self.gpu.queue, scale)
2434    }
2435
2436    pub fn set_jitter(&mut self, jitter: [f32; 2]) {
2437        self.jitter = jitter;
2438    }
2439
2440    pub fn set_reflections(&mut self, history: Option<reflect::Reflections>) {
2441        self.ssr = history;
2442    }
2443
2444    pub fn set_local_reflections(
2445        &self,
2446        artifacts: Option<Vec<ReflectionArtifact>>,
2447        hour: f32,
2448    ) -> Result<(), String> {
2449        let reflections = artifacts
2450            .map(|artifacts| {
2451                let array =
2452                    upload_reflections(&self.gpu.device, &self.gpu.queue, &artifacts, hour)?;
2453                Ok::<_, String>(LocalReflections { artifacts, array })
2454            })
2455            .transpose()?;
2456        *self.local_reflections.borrow_mut() = reflections;
2457        Ok(())
2458    }
2459
2460    pub fn set_local_reflection_hour(&self, hour: f32) -> Result<(), String> {
2461        let mut reflections = self.local_reflections.borrow_mut();
2462        if let Some(ref mut current) = *reflections {
2463            current.array =
2464                upload_reflections(&self.gpu.device, &self.gpu.queue, &current.artifacts, hour)?;
2465        }
2466        Ok(())
2467    }
2468
2469    pub fn set_sky(&mut self, source: SkySource, settle: bool) -> u64 {
2470        if !settle {
2471            return self.sky.request_source(source);
2472        }
2473        self.sky
2474            .set_source(&self.gpu.device, &self.gpu.queue, source);
2475        while !self.sky.lighting.complete() {
2476            self.sky.update(
2477                &self.gpu.queue,
2478                [0.0, 0.0, -1.0],
2479                [1.0, 0.0, 0.0],
2480                [0.0, 1.0, 0.0],
2481            );
2482        }
2483        self.sky.shown()
2484    }
2485
2486    pub fn sky(&self) -> &SkyPass {
2487        &self.sky
2488    }
2489
2490    pub fn sky_mut(&mut self) -> &mut SkyPass {
2491        &mut self.sky
2492    }
2493
2494    pub fn set_ambient(&mut self, ambient: Ambient) -> Result<(), String> {
2495        if !ambient.valid() {
2496            return Err("an ambient must be finite, and a constant nonnegative".into());
2497        }
2498        self.ambient = ambient;
2499        Ok(())
2500    }
2501
2502    pub fn ambient(&self) -> Ambient {
2503        self.ambient
2504    }
2505
2506    pub fn set_contact(&mut self, desc: Option<ContactDesc>) -> Result<(), String> {
2507        let uniform = match desc {
2508            Some(desc) => {
2509                self.contact = Some(ContactField::new(&self.gpu.device, &self.gpu.queue, desc)?);
2510                ContactUniform::on(&desc)
2511            }
2512            None => {
2513                self.contact = None;
2514                ContactUniform::off()
2515            }
2516        };
2517        self.gpu
2518            .queue
2519            .write_buffer(&self.contact_uniform, 0, bytemuck::bytes_of(&uniform));
2520        Ok(())
2521    }
2522
2523    pub fn contact(&self) -> Option<&ContactField> {
2524        self.contact.as_ref()
2525    }
2526
2527    pub fn write_contact(&self, values: &[f32]) -> Result<(), String> {
2528        match &self.contact {
2529            Some(field) => field.write(&self.gpu.queue, values),
2530            None => Err("no contact field is set".into()),
2531        }
2532    }
2533
2534    pub fn set_probes(&mut self, probes: ProbeLighting) {
2535        self.probes = probes;
2536        self.probes.set_fallback(&self.gpu.queue, [-1.0; 3]);
2537    }
2538
2539    pub fn set_probe_hour(&mut self, hour: f32) -> Result<(), String> {
2540        self.set_local_reflection_hour(hour)?;
2541        if self.probes.manifest.is_none() {
2542            return Ok(());
2543        }
2544        self.probes
2545            .set_hour(&self.gpu.device, &self.gpu.queue, hour)
2546    }
2547
2548    pub fn probes(&self) -> &ProbeLighting {
2549        &self.probes
2550    }
2551
2552    pub fn set_maps(&mut self, images: &MapImages<'_>) -> Result<(), String> {
2553        let filter = self.maps.filter;
2554        self.maps = MapTextures::new(&self.gpu.device, &self.gpu.queue, images)?;
2555        self.maps.set_filter(&self.gpu.device, filter);
2556        self.map_layers = [
2557            images.base.len(),
2558            images.normal.len(),
2559            images.roughness.len(),
2560            images.metal.len(),
2561        ];
2562        Ok(())
2563    }
2564
2565    pub fn set_detail(&mut self, atlas: &Atlas) -> Result<(), String> {
2566        let filter = self.maps.filter;
2567        self.maps = MapTextures::detail(&self.gpu.device, &self.gpu.queue, atlas)?;
2568        self.maps.set_filter(&self.gpu.device, filter);
2569        let parts = atlas.parts.len();
2570        self.map_layers = [parts, parts, parts, 0];
2571        Ok(())
2572    }
2573
2574    pub fn set_texture_filter(&mut self, filter: maps::TextureFilter) -> Result<(), String> {
2575        filter.check()?;
2576        let anisotropy = self.maps.filter.anisotropy;
2577        self.maps.set_filter(&self.gpu.device, filter);
2578        if filter.anisotropy != anisotropy {
2579            self.content
2580                .borrow_mut()
2581                .set_filter(&self.gpu.device, filter);
2582        }
2583        Ok(())
2584    }
2585
2586    pub fn texture_filter(&self) -> maps::TextureFilter {
2587        self.maps.filter
2588    }
2589
2590    pub fn maps(&self) -> &MapTextures {
2591        &self.maps
2592    }
2593
2594    pub fn set_content(
2595        &self,
2596        slot: usize,
2597        width: u32,
2598        height: u32,
2599        format: ContentFormat,
2600    ) -> Result<(), String> {
2601        self.content_changed();
2602        self.content
2603            .borrow_mut()
2604            .allocate(&self.gpu.device, slot, width, height, format)
2605    }
2606
2607    pub fn write_content(&self, slot: usize, rect: [u32; 4], bytes: &[u8]) -> Result<(), String> {
2608        self.content_changed();
2609        self.content
2610            .borrow_mut()
2611            .write(&self.gpu.device, &self.gpu.queue, slot, rect, bytes)
2612    }
2613
2614    fn content_changed(&self) {
2615        self.content_epoch
2616            .set(self.content_epoch.get().wrapping_add(1));
2617    }
2618
2619    pub fn copy_content(
2620        &self,
2621        encoder: &mut wgpu::CommandEncoder,
2622        slot: usize,
2623        source: &wgpu::Texture,
2624    ) -> Result<maps::ContentCopy, String> {
2625        self.content_changed();
2626        self.content
2627            .borrow_mut()
2628            .copy(&self.gpu.device, encoder, slot, source)
2629    }
2630
2631    pub fn regenerate_content(
2632        &self,
2633        encoder: &mut wgpu::CommandEncoder,
2634        slot: usize,
2635    ) -> Result<(), String> {
2636        self.content_changed();
2637        self.content.borrow_mut().regenerate(encoder, slot)
2638    }
2639
2640    pub fn regenerate_content_regions(
2641        &self,
2642        encoder: &mut wgpu::CommandEncoder,
2643        slot: usize,
2644        regions: &[maps::MipRegion],
2645    ) -> Result<(), String> {
2646        self.content_changed();
2647        self.content
2648            .borrow_mut()
2649            .regenerate_regions(&self.gpu.device, encoder, slot, regions)
2650    }
2651
2652    pub fn set_caustic(&self, caustic: Option<Caustic>) -> Result<(), String> {
2653        if let Some(caustic) = &caustic {
2654            caustic.check()?;
2655        }
2656        *self.caustic.borrow_mut() = caustic;
2657        Ok(())
2658    }
2659
2660    pub fn caustic(&self) -> Option<Caustic> {
2661        self.caustic.borrow().clone()
2662    }
2663
2664    pub fn release_content(&self, slot: usize) {
2665        self.content_changed();
2666        self.content.borrow_mut().release(slot);
2667    }
2668
2669    pub fn content(&self) -> std::cell::Ref<'_, ContentSlots> {
2670        self.content.borrow()
2671    }
2672
2673    pub fn set_surfaces(&self, surfaces: &[InstanceSurface]) -> Result<(), String> {
2674        if surfaces.iter().any(|surface| !surface.valid()) {
2675            return Err(
2676                "instance surface planes, offsets and crops must be finite and reflection occlusion between 0 and 1"
2677                    .into(),
2678            );
2679        }
2680        let mut current = self.surfaces.borrow_mut();
2681        current.clear();
2682        current.extend_from_slice(surfaces);
2683        Ok(())
2684    }
2685
2686    pub fn surfaces(&self) -> std::cell::Ref<'_, Vec<InstanceSurface>> {
2687        self.surfaces.borrow()
2688    }
2689
2690    pub fn lit_bindings(&self) -> Option<crate::text::LitBindings<'_>> {
2691        Some(crate::text::LitBindings {
2692            scene: &self.scene_group,
2693            shadow: &self.shadow_group,
2694            lighting: self.lighting_group.as_ref()?,
2695            deformers: self.deformers.buffer(),
2696            plan: self.deformers.plan_buffer(),
2697        })
2698    }
2699
2700    pub fn deformer_buffer(&self) -> &DeformerBuffer {
2701        &self.deformers
2702    }
2703
2704    pub fn has_tree(&self) -> bool {
2705        self.foliage.is_some()
2706    }
2707
2708    pub fn set_canopy_mode(&mut self, mode: CanopyMode) -> Result<(), String> {
2709        let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2710        foliage.canopy.set_mode(mode);
2711        Ok(())
2712    }
2713
2714    pub fn set_canopy_sharpness(&mut self, sharpness: f32) -> Result<(), String> {
2715        let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2716        foliage.canopy.set_sharpness(sharpness)
2717    }
2718
2719    pub fn set_canopy_strength(&mut self, strength: f32) -> Result<(), String> {
2720        let foliage = self.foliage.as_mut().ok_or("no tree is set")?;
2721        foliage.canopy.set_strength(strength)
2722    }
2723
2724    pub fn set_canopy_gobo(&mut self, gobo: Gobo) -> Result<(), String> {
2725        gobo.validate()?;
2726        if gobo == self.canopy_gobo {
2727            return Ok(());
2728        }
2729        let module = self
2730            .gpu
2731            .device
2732            .create_shader_module(wgpu::ShaderModuleDescriptor {
2733                label: Some("live frame shader"),
2734                source: wgpu::ShaderSource::Wgsl(shader_source_with(&gobo).into()),
2735            });
2736        self.pipelines.retarget(module);
2737        self.canopy_gobo = gobo;
2738        if let Some(foliage) = self.foliage.as_mut() {
2739            foliage.canopy.set_weight(gobo.weight);
2740        }
2741        Ok(())
2742    }
2743
2744    pub fn canopy_gobo(&self) -> Gobo {
2745        self.canopy_gobo
2746    }
2747
2748    pub fn visible_card_count(&self) -> u32 {
2749        self.foliage
2750            .as_ref()
2751            .map_or(0, |foliage| foliage.canopy.card_count)
2752    }
2753
2754    pub fn set_tree(
2755        &mut self,
2756        tree: &Tree,
2757        origin: [f32; 3],
2758        receiver_y: f32,
2759        id: u32,
2760    ) -> Result<MeshHandle, String> {
2761        let wood = &tree.wood;
2762        let handle = self.upload_swaying_mesh(
2763            MeshData {
2764                positions: &wood.positions,
2765                normals: &wood.normals,
2766                tangents: &wood.tangents,
2767                uvs: &wood.uvs,
2768                uvs1: None,
2769                alpha: None,
2770                indices: &wood.indices,
2771            },
2772            &tree.sway,
2773        )?;
2774        let device = &self.gpu.device;
2775        let mut canopy = Canopy::new(
2776            device,
2777            &self.gpu.queue,
2778            tree,
2779            crate::canopy::DEFAULT_RESOLUTION,
2780        );
2781        canopy.set_weight(self.canopy_gobo.weight);
2782        let pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
2783            label: Some("live cards layout"),
2784            bind_group_layouts: &[
2785                Some(&self.scene_layout),
2786                Some(&self.shadow_layout),
2787                Some(canopy.cookie_layout()),
2788                Some(&self.lighting_layout),
2789            ],
2790            immediate_size: 0,
2791        });
2792        let module = device.create_shader_module(wgpu::ShaderModuleDescriptor {
2793            label: Some("live cards"),
2794            source: wgpu::ShaderSource::Wgsl(card_source().into()),
2795        });
2796        let prepass = pipeline(
2797            device,
2798            PipelineSpec {
2799                label: "live card depth",
2800                layout: &pipeline_layout,
2801                module: &module,
2802                vertex: "card_vertex",
2803                fragment: Some("card_prepass"),
2804                buffers: &[],
2805                targets: &prepass_targets(),
2806                cull: None,
2807                write_depth: true,
2808                cache: None,
2809            },
2810        );
2811        let opaque = pipeline(
2812            device,
2813            PipelineSpec {
2814                label: "live card shading",
2815                layout: &pipeline_layout,
2816                module: &module,
2817                vertex: "card_vertex",
2818                fragment: Some("card_opaque"),
2819                buffers: &[],
2820                targets: &opaque_targets(),
2821                cull: None,
2822                write_depth: false,
2823                cache: None,
2824            },
2825        );
2826        self.foliage = Some(Foliage {
2827            canopy,
2828            origin,
2829            receiver_y,
2830            id,
2831            prepass,
2832            opaque,
2833        });
2834        Ok(handle)
2835    }
2836
2837    pub fn upload_mesh(&mut self, data: MeshData<'_>) -> Result<MeshHandle, String> {
2838        self.upload(data, None, None)
2839    }
2840
2841    pub fn upload_skinned_mesh(
2842        &mut self,
2843        data: MeshData<'_>,
2844        skin: SkinData<'_>,
2845    ) -> Result<MeshHandle, String> {
2846        if skin.joints.len() != data.positions.len() || skin.weights.len() != data.positions.len() {
2847            return Err("skin joints and weights need one entry per vertex".into());
2848        }
2849        if skin
2850            .weights
2851            .iter()
2852            .flatten()
2853            .any(|weight| !weight.is_finite())
2854        {
2855            return Err("a skin weight is not finite".into());
2856        }
2857        self.upload(data, None, Some(skin))
2858    }
2859
2860    pub fn upload_swaying_mesh(
2861        &mut self,
2862        data: MeshData<'_>,
2863        sway: &[SwayVertex],
2864    ) -> Result<MeshHandle, String> {
2865        if sway.len() != data.positions.len() {
2866            return Err("sway attributes need one entry per vertex".into());
2867        }
2868        self.upload(data, Some(sway), None)
2869    }
2870
2871    fn upload(
2872        &mut self,
2873        data: MeshData<'_>,
2874        sway: Option<&[SwayVertex]>,
2875        skin: Option<SkinData<'_>>,
2876    ) -> Result<MeshHandle, String> {
2877        check_mesh(&data)?;
2878        let slot = match self.free_meshes.last() {
2879            Some(&slot) => slot,
2880            None if self.meshes.len() <= MESH_SLOT_MASK as usize => self.meshes.len(),
2881            None => return Err("too many meshes".into()),
2882        };
2883        let generation = self.meshes.get(slot).map_or(0, |slot| slot.generation);
2884        let vertices = mesh_vertices(&data);
2885        let device = &self.gpu.device;
2886        let live = &self.mesh_buffers_live;
2887        let vertex_buffer = mesh_buffer(
2888            device,
2889            live,
2890            "live mesh vertices",
2891            bytemuck::cast_slice(&vertices),
2892            wgpu::BufferUsages::VERTEX,
2893        );
2894        let positions = mesh_buffer(
2895            device,
2896            live,
2897            "live mesh positions",
2898            bytemuck::cast_slice(data.positions),
2899            wgpu::BufferUsages::VERTEX,
2900        );
2901        let indices = mesh_buffer(
2902            device,
2903            live,
2904            "live mesh indices",
2905            bytemuck::cast_slice(data.indices),
2906            wgpu::BufferUsages::INDEX,
2907        );
2908        let (base, sway_len) = match sway {
2909            Some(sway) => (self.allocate_sway(sway)?, sway.len() as u32),
2910            None => (NO_SWAY, 0),
2911        };
2912        let (skin_base, skin_len, skin_joints) = match skin {
2913            Some(skin) => {
2914                let range = self.skins.allocate(&self.gpu.device, skin)?;
2915                self.rebuild_scene_group();
2916                (range.start, range.end - range.start, skin.palette() as u32)
2917            }
2918            None => (NO_SKIN, 0, 0),
2919        };
2920        let mesh = Mesh {
2921            vertices: vertex_buffer,
2922            positions,
2923            indices,
2924            vertex_capacity: data.positions.len(),
2925            index_capacity: data.indices.len(),
2926            count: data.indices.len() as u32,
2927            sway: base,
2928            sway_len,
2929            skin: skin_base,
2930            skin_len,
2931            skin_joints,
2932        };
2933        if slot == self.meshes.len() {
2934            self.meshes.push(MeshSlot {
2935                generation,
2936                mesh: Some(mesh),
2937            });
2938        } else {
2939            self.free_meshes.pop();
2940            self.meshes[slot].mesh = Some(mesh);
2941        }
2942        Ok(MeshHandle::pack(slot, generation))
2943    }
2944
2945    fn allocate_sway(&mut self, sway: &[SwayVertex]) -> Result<u32, String> {
2946        let base = self.sway_free.take(
2947            &mut self.sway_data,
2948            sway.len(),
2949            GpuSway {
2950                pivot_level: [0.0; 4],
2951                stiffness: [0.0; 4],
2952            },
2953            "sway vertices",
2954        )?;
2955        for (slot, vertex) in self.sway_data[base as usize..].iter_mut().zip(sway) {
2956            *slot = GpuSway {
2957                pivot_level: [
2958                    vertex.pivot[0],
2959                    vertex.pivot[1],
2960                    vertex.pivot[2],
2961                    vertex.level,
2962                ],
2963                stiffness: [vertex.stiffness, 0.0, 0.0, 0.0],
2964            };
2965        }
2966        self.sway = self
2967            .gpu
2968            .device
2969            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2970                label: Some("live sway"),
2971                contents: bytemuck::cast_slice(&self.sway_data),
2972                usage: wgpu::BufferUsages::STORAGE,
2973            });
2974        self.rebuild_scene_group();
2975        Ok(base)
2976    }
2977
2978    fn check_handle(&self, handle: MeshHandle) -> Result<usize, String> {
2979        match live_mesh(&self.meshes, handle) {
2980            Some(_) => Ok(handle.slot()),
2981            None => Err("mesh handle is unknown or already released".into()),
2982        }
2983    }
2984
2985    pub fn release_mesh(&mut self, handle: MeshHandle) -> Result<(), String> {
2986        let slot = self.check_handle(handle)?;
2987        let Some(mesh) = self.meshes[slot].mesh.take() else {
2988            return Err("mesh handle is unknown or already released".into());
2989        };
2990        if mesh.sway != NO_SWAY {
2991            self.sway_free.give(mesh.sway..mesh.sway + mesh.sway_len);
2992        }
2993        if mesh.skin != NO_SKIN {
2994            self.skins.release(mesh.skin..mesh.skin + mesh.skin_len);
2995        }
2996        let entry = &mut self.meshes[slot];
2997        entry.generation += 1;
2998        if entry.generation < MESH_GENERATION_LAST {
2999            self.free_meshes.push(slot);
3000        }
3001        self.mesh_epoch += 1;
3002        Ok(())
3003    }
3004
3005    pub fn replace_mesh(&mut self, handle: MeshHandle, data: MeshData<'_>) -> Result<(), String> {
3006        check_mesh(&data)?;
3007        let slot = self.check_handle(handle)?;
3008        let Some(mesh) = self.meshes[slot].mesh.as_mut() else {
3009            return Err("mesh handle is unknown or already released".into());
3010        };
3011        if mesh.sway != NO_SWAY {
3012            return Err("a swaying mesh cannot be replaced".into());
3013        }
3014        if mesh.skin != NO_SKIN {
3015            return Err("a skinned mesh cannot be replaced".into());
3016        }
3017        let device = &self.gpu.device;
3018        let queue = &self.gpu.queue;
3019        let live = &self.mesh_buffers_live;
3020        let vertices = mesh_vertices(&data);
3021        if data.positions.len() <= mesh.vertex_capacity {
3022            queue.write_buffer(&mesh.vertices, 0, bytemuck::cast_slice(&vertices));
3023            queue.write_buffer(&mesh.positions, 0, bytemuck::cast_slice(data.positions));
3024        } else {
3025            mesh.vertices = mesh_buffer(
3026                device,
3027                live,
3028                "live mesh vertices",
3029                bytemuck::cast_slice(&vertices),
3030                wgpu::BufferUsages::VERTEX,
3031            );
3032            mesh.positions = mesh_buffer(
3033                device,
3034                live,
3035                "live mesh positions",
3036                bytemuck::cast_slice(data.positions),
3037                wgpu::BufferUsages::VERTEX,
3038            );
3039            mesh.vertex_capacity = data.positions.len();
3040        }
3041        if data.indices.len() <= mesh.index_capacity {
3042            queue.write_buffer(&mesh.indices, 0, bytemuck::cast_slice(data.indices));
3043        } else {
3044            mesh.indices = mesh_buffer(
3045                device,
3046                live,
3047                "live mesh indices",
3048                bytemuck::cast_slice(data.indices),
3049                wgpu::BufferUsages::INDEX,
3050            );
3051            mesh.index_capacity = data.indices.len();
3052        }
3053        mesh.count = data.indices.len() as u32;
3054        self.mesh_epoch += 1;
3055        Ok(())
3056    }
3057
3058    pub fn mesh_stats(&self) -> MeshStats {
3059        let live = self
3060            .meshes
3061            .iter()
3062            .filter(|slot| slot.mesh.is_some())
3063            .count();
3064        MeshStats {
3065            live,
3066            slots: self.meshes.len(),
3067            buffers: self.mesh_buffers_live.load(Ordering::Relaxed),
3068            glass_live: 0,
3069            glass_slots: 0,
3070            glass_buffers: 0,
3071        }
3072    }
3073
3074    pub fn shadow_mesh(&self, handle: MeshHandle) -> Option<(&wgpu::Buffer, &wgpu::Buffer, u32)> {
3075        live_mesh(&self.meshes, handle)
3076            .map(|mesh| (&mesh.positions.buffer, &mesh.indices.buffer, mesh.count))
3077    }
3078
3079    pub fn mesh_buffers(&self, handle: MeshHandle) -> Option<MeshBuffers<'_>> {
3080        live_mesh(&self.meshes, handle).map(|mesh| MeshBuffers {
3081            vertices: &mesh.vertices,
3082            positions: &mesh.positions,
3083            indices: &mesh.indices,
3084            count: mesh.count,
3085        })
3086    }
3087
3088    pub fn swaying(&self, handle: MeshHandle) -> bool {
3089        live_mesh(&self.meshes, handle).is_some_and(|mesh| mesh.sway != NO_SWAY)
3090    }
3091
3092    pub fn skinned(&self, handle: MeshHandle) -> bool {
3093        live_mesh(&self.meshes, handle).is_some_and(|mesh| mesh.skin != NO_SKIN)
3094    }
3095
3096    fn deforms(&self, handle: MeshHandle) -> bool {
3097        live_mesh(&self.meshes, handle)
3098            .is_some_and(|mesh| mesh.sway != NO_SWAY || mesh.skin != NO_SKIN)
3099    }
3100
3101    pub fn set_poses(&self, poses: &[InstancePose<'_>]) -> Result<(), String> {
3102        self.skins.set_poses(poses)
3103    }
3104
3105    pub fn same_poses(&self, poses: &[InstancePose<'_>]) -> bool {
3106        self.skins.same(poses)
3107    }
3108
3109    pub fn render(&mut self, scene: &Scene<'_>) -> Result<&FrameTargets, String> {
3110        let mut encoder = self
3111            .gpu
3112            .device
3113            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
3114                label: Some("live frame"),
3115            });
3116        let mut profiler = std::mem::replace(
3117            &mut self.profiler,
3118            GpuProfiler::new(&self.gpu.device, &self.gpu.queue),
3119        );
3120        let result = self.encode(scene, &mut encoder, None, Some(&mut profiler));
3121        if let Err(error) = result {
3122            self.profiler = profiler;
3123            return Err(error);
3124        }
3125        let slot = profiler.finish(&mut encoder);
3126        self.gpu.queue.submit(Some(encoder.finish()));
3127        if let Some(slot) = slot {
3128            profiler.submitted(slot);
3129        }
3130        self.profiler = profiler;
3131        Ok(&self.targets)
3132    }
3133
3134    pub fn resize(&mut self, width: u32, height: u32) -> Result<(), String> {
3135        if width == 0 || height == 0 {
3136            return Err("viewport is empty".into());
3137        }
3138        if (width, height) == (self.targets.width, self.targets.height) {
3139            self.viewport = [width, height];
3140            return Ok(());
3141        }
3142        self.targets = new_targets(&self.gpu, width, height)?;
3143        self.ssr = None;
3144        self.viewport = [width, height];
3145        Ok(())
3146    }
3147
3148    pub fn viewport(&self) -> [u32; 2] {
3149        self.viewport
3150    }
3151
3152    pub fn set_viewport(&mut self, width: u32, height: u32) -> Result<(), String> {
3153        crate::viewport::check([width, height], [self.targets.width, self.targets.height])?;
3154        self.viewport = [width, height];
3155        Ok(())
3156    }
3157
3158    fn scene_surfaces(&self, count: usize) -> Vec<InstanceSurface> {
3159        let given = self.surfaces.borrow();
3160        (0..count)
3161            .map(|index| given.get(index).copied().unwrap_or_default())
3162            .collect()
3163    }
3164
3165    fn pack_instances(
3166        &self,
3167        scene: &Scene<'_>,
3168        surfaces: &[InstanceSurface],
3169        poses: &PoseLayout,
3170    ) -> Result<Vec<GpuInstance>, String> {
3171        let mut packed_instances = Vec::with_capacity(scene.instances.len());
3172        let caustic = self.caustic.borrow();
3173        for (index, (instance, surface)) in scene.instances.iter().zip(surfaces).enumerate() {
3174            let Some(mesh) = live_mesh(&self.meshes, instance.mesh) else {
3175                return Err("instance references a released or unknown mesh".into());
3176            };
3177            if instance.material as usize >= scene.materials.len() {
3178                return Err("instance references an unknown mesh or material".into());
3179            }
3180            if !(0.0..=1.0).contains(&instance.opacity)
3181                || !(0.0..=1.0).contains(&instance.alpha_cutoff)
3182                || !(0.0..=1.0).contains(&instance.coverage)
3183                || !instance.age.is_finite()
3184            {
3185                return Err("instance alpha, coverage and age must be finite fractions".into());
3186            }
3187            if instance.deformer != DeformerId::NONE
3188                && instance.deformer.0 as usize >= scene.deformers.len()
3189            {
3190                return Err("instance references an unknown deformer".into());
3191            }
3192            let skin = match poses.get(index).filter(|_| mesh.skin != NO_SKIN) {
3193                Some(placed) if placed.joints < mesh.skin_joints => {
3194                    return Err(format!(
3195                        "instance {index} is posed with {} joints where its mesh needs {}",
3196                        placed.joints, mesh.skin_joints
3197                    ));
3198                }
3199                Some(placed) => [mesh.skin, placed.current, placed.previous, placed.joints],
3200                None => [NO_SKIN, 0, 0, 0],
3201            };
3202            let still = u32::from(
3203                instance.deformer == DeformerId::NONE
3204                    && mesh.sway == NO_SWAY
3205                    && skin[0] == NO_SKIN
3206                    && same_bits(instance.model, instance.previous_model),
3207            );
3208            packed_instances.push(GpuInstance {
3209                model: instance.model,
3210                previous_model: instance.previous_model,
3211                normal: normal_columns(instance.model)?,
3212                meta: [
3213                    instance.material,
3214                    instance.id,
3215                    instance.opacity.to_bits(),
3216                    instance.alpha_cutoff.to_bits(),
3217                ],
3218                extra: [
3219                    instance.deformer.0,
3220                    mesh.sway,
3221                    instance.age.clamp(0.0, 1.0).to_bits(),
3222                    instance.coverage.to_bits(),
3223                ],
3224                clip: surface.clip,
3225                content_uv: [
3226                    surface.uv_offset[0],
3227                    surface.uv_offset[1],
3228                    surface.uv_scale[0],
3229                    surface.uv_scale[1],
3230                ],
3231                content_crop: surface.crop,
3232                content_face: [
3233                    surface.face.code(),
3234                    still,
3235                    u32::from(surface.contact)
3236                        | u32::from(caustic.as_ref().is_some_and(|c| c.receives(index))) << 1
3237                        | u32::from(surface.cutout) << 2
3238                        | surface.occlusion_release() << 8,
3239                    u32::from(surface.authored_ambient),
3240                ],
3241                skin,
3242            });
3243        }
3244        Ok(packed_instances)
3245    }
3246
3247    fn pack_map_materials(&self, scene: &Scene<'_>) -> Vec<GpuMapMaterial> {
3248        scene
3249            .materials
3250            .iter()
3251            .map(|material| {
3252                let indices = MapIndices::from_material(material)
3253                    .ok()
3254                    .filter(|indices| {
3255                        [
3256                            indices.base,
3257                            indices.normal,
3258                            indices.roughness,
3259                            indices.metal,
3260                        ]
3261                        .iter()
3262                        .zip(self.map_layers)
3263                        .all(|(index, count)| index.is_none_or(|index| (index as usize) < count))
3264                    })
3265                    .unwrap_or_default();
3266                self.maps.material(material, indices)
3267            })
3268            .collect()
3269    }
3270
3271    fn prepare(&mut self, scene: &Scene<'_>) -> Result<(Vec<Batch>, Vec<InstanceSurface>), String> {
3272        if scene.materials.is_empty() {
3273            return Err("scene needs at least one material".into());
3274        }
3275        let surfaces = self.scene_surfaces(scene.instances.len());
3276        self.content.borrow().write_caustic(
3277            &self.gpu.queue,
3278            Caustic::rows(self.caustic.borrow().as_ref(), scene.sun.direction),
3279        );
3280        let packed_instances = self.pack_instances(scene, &surfaces, &self.skins.layout())?;
3281        if self.skins.upload(&self.gpu.device, &self.gpu.queue) {
3282            self.rebuild_scene_group();
3283        }
3284        self.deformers
3285            .upload(&self.gpu.device, &self.gpu.queue, scene.deformers)?;
3286        let materials: Vec<PackedMaterial> =
3287            scene.materials.iter().map(PackedMaterial::pack).collect();
3288        self.maps.fit_coat(&self.gpu.queue, scene.materials);
3289        let map_materials = self.pack_map_materials(scene);
3290        self.instance_features =
3291            scene_instance_features(&packed_instances, &map_materials, scene.materials);
3292        let mut resized = false;
3293        if packed_instances.len() > self.instance_capacity {
3294            self.instance_capacity = packed_instances.len().next_power_of_two();
3295            self.instances = storage(
3296                &self.gpu.device,
3297                "live instances",
3298                self.instance_capacity * std::mem::size_of::<GpuInstance>(),
3299            );
3300            resized = true;
3301        }
3302        if materials.len() > self.material_capacity {
3303            self.material_capacity = materials.len().next_power_of_two();
3304            self.materials = storage(
3305                &self.gpu.device,
3306                "live materials",
3307                self.material_capacity * PackedMaterial::SIZE,
3308            );
3309            self.map_materials = storage(
3310                &self.gpu.device,
3311                "live map materials",
3312                self.material_capacity * std::mem::size_of::<GpuMapMaterial>(),
3313            );
3314            resized = true;
3315        }
3316        if resized {
3317            self.rebuild_scene_group();
3318        }
3319        if self
3320            .lights
3321            .prepare(&self.gpu.device, &self.gpu.queue, scene.materials.len())
3322        {
3323            self.cascade_group = Self::make_cascade_group(
3324                &self.gpu.device,
3325                &self.cascade_layout,
3326                &self.cascade_uniform,
3327                self.lights.table(),
3328            );
3329        }
3330        let [width, height] = self
3331            .ssr
3332            .as_ref()
3333            .map_or(self.viewport, |history| history.size);
3334        let motion_view_projection = multiply(scene.camera.projection, scene.camera.view);
3335        let wind = scene.wind;
3336        let uniforms = Uniforms {
3337            view_projection: multiply(jitter_matrix(self.jitter), motion_view_projection),
3338            motion_view_projection,
3339            previous_view_projection: scene.camera.previous_view_projection,
3340            view: scene.camera.view,
3341            camera_position: [
3342                scene.camera.position[0],
3343                scene.camera.position[1],
3344                scene.camera.position[2],
3345                if same_bits(
3346                    motion_view_projection,
3347                    scene.camera.previous_view_projection,
3348                ) {
3349                    1.0
3350                } else {
3351                    0.0
3352                },
3353            ],
3354            sun_direction: [
3355                scene.sun.direction[0],
3356                scene.sun.direction[1],
3357                scene.sun.direction[2],
3358                0.0,
3359            ],
3360            sun_colour_intensity: [
3361                scene.sun.colour[0],
3362                scene.sun.colour[1],
3363                scene.sun.colour[2],
3364                scene.sun.intensity,
3365            ],
3366            time_size: [
3367                scene.time,
3368                width as f32,
3369                height as f32,
3370                (self.sky.lighting.mip_count - 1) as f32,
3371            ],
3372            flags: [
3373                scene.seed,
3374                u32::from(self.ssr.is_some()),
3375                u32::from(self.foliage.is_some()),
3376                self.foliage.as_ref().map_or(0, |foliage| foliage.id),
3377            ],
3378            wind: [
3379                wind.current.time,
3380                wind.current.strength,
3381                wind.previous.time,
3382                wind.previous.strength,
3383            ],
3384            wind_direction: [
3385                wind.current.direction[0],
3386                wind.current.direction[1],
3387                wind.current.direction[2],
3388                0.0,
3389            ],
3390            previous_wind_direction: [
3391                wind.previous.direction[0],
3392                wind.previous.direction[1],
3393                wind.previous.direction[2],
3394                0.0,
3395            ],
3396        };
3397        let queue = &self.gpu.queue;
3398        queue.write_buffer(&self.uniform, 0, bytemuck::bytes_of(&uniforms));
3399        if !packed_instances.is_empty() {
3400            queue.write_buffer(&self.instances, 0, bytemuck::cast_slice(&packed_instances));
3401        }
3402        queue.write_buffer(&self.materials, 0, bytemuck::cast_slice(&materials));
3403        queue.write_buffer(&self.map_materials, 0, bytemuck::cast_slice(&map_materials));
3404        let mut light = SkyLight {
3405            sh: [[0.0; 4]; 9],
3406            info: [(self.sky.lighting.mip_count - 1) as f32, 0.0, 0.0, 0.0],
3407        };
3408        for (out, value) in light.sh.iter_mut().zip(self.sky.lighting.sh) {
3409            *out = [value[0], value[1], value[2], 0.0];
3410        }
3411        queue.write_buffer(&self.sky_light, 0, bytemuck::bytes_of(&light));
3412        let mut authored = light;
3413        for (out, value) in authored
3414            .sh
3415            .iter_mut()
3416            .zip(self.ambient.sh(self.sky.lighting.sh))
3417        {
3418            *out = [value[0], value[1], value[2], 0.0];
3419        }
3420        queue.write_buffer(&self.ambient_light, 0, bytemuck::bytes_of(&authored));
3421        let mut selection = ReflectionSelection::zeroed();
3422        selection.first.box_min = [-1.0; 3];
3423        selection.first.box_max = [1.0; 3];
3424        selection.second.box_min = [-1.0; 3];
3425        selection.second.box_max = [1.0; 3];
3426        let sky_mip = (self.sky.lighting.mip_count - 1) as f32;
3427        selection.weights_mip = [0.0, 0.0, sky_mip, sky_mip];
3428        if let Some(ref reflections) = *self.local_reflections.borrow() {
3429            let records = &reflections.array.records;
3430            let [first, second] = local_candidates(records, scene.camera.position);
3431            selection.first = records[first].into();
3432            selection.second = records[second].into();
3433            selection.weights_mip = [
3434                1.0,
3435                if second == first { 0.0 } else { 1.0 },
3436                reflections.array.max_mip,
3437                sky_mip,
3438            ];
3439        }
3440        queue.write_buffer(
3441            &self.reflection_selection,
3442            0,
3443            bytemuck::bytes_of(&selection),
3444        );
3445        self.lighting_group = Some(self.make_lighting_group());
3446        let batches = surface_batches(
3447            scene.instances,
3448            |mesh| self.deforms(mesh),
3449            |index| surfaces[index].clipped(),
3450            |index| surfaces[index].cutout,
3451        );
3452        Ok((batches, surfaces))
3453    }
3454
3455    fn make_lighting_group(&self) -> wgpu::BindGroup {
3456        let [lower, upper, grid] = self.probes.bindings();
3457        let cube = self.sky.cube_view();
3458        let (ssr, ssr_guide) = self
3459            .ssr
3460            .as_ref()
3461            .map_or((&self.fallbacks.black, &self.fallbacks.black), |ssr| {
3462                (&ssr.colour, &ssr.guide)
3463            });
3464        let reflections = self.local_reflections.borrow();
3465        let local = reflections
3466            .as_ref()
3467            .map_or(&self.fallbacks.local_probes, |current| &current.array.view);
3468        let (cookie, cookie_sampler, canopy_params) = match &self.foliage {
3469            Some(foliage) => (
3470                &foliage.canopy.view,
3471                &foliage.canopy.sampler,
3472                foliage.canopy.params(),
3473            ),
3474            None => (
3475                &self.fallbacks.black,
3476                &self.fallbacks.linear,
3477                &self.fallbacks.canopy_params,
3478            ),
3479        };
3480        let view = wgpu::BindingResource::TextureView;
3481        let content = self.content.borrow();
3482        let entries = [
3483            view(&self.maps.base.view),
3484            view(&self.maps.normal.view),
3485            view(&self.maps.roughness.view),
3486            view(&self.maps.metal.view),
3487            wgpu::BindingResource::Sampler(&self.maps.sampler),
3488            self.map_materials.as_entire_binding(),
3489            lower.as_entire_binding(),
3490            upper.as_entire_binding(),
3491            grid.as_entire_binding(),
3492            view(cookie),
3493            wgpu::BindingResource::Sampler(cookie_sampler),
3494            canopy_params.as_entire_binding(),
3495            self.sky_light.as_entire_binding(),
3496            view(&cube),
3497            view(local),
3498            wgpu::BindingResource::Sampler(&self.fallbacks.linear),
3499            view(ssr),
3500            self.reflection_selection.as_entire_binding(),
3501            view(content.view(0)),
3502            view(content.view(1)),
3503            view(content.view(2)),
3504            view(content.view(3)),
3505            wgpu::BindingResource::Sampler(&content.sampler),
3506            view(
3507                self.contact
3508                    .as_ref()
3509                    .map_or(&self.fallbacks.black, ContactField::view),
3510            ),
3511            self.contact_uniform.as_entire_binding(),
3512            self.ambient_light.as_entire_binding(),
3513            view(ssr_guide),
3514            content.caustic.as_entire_binding(),
3515        ];
3516        let entries: Vec<wgpu::BindGroupEntry<'_>> = entries
3517            .into_iter()
3518            .enumerate()
3519            .map(|(binding, resource)| wgpu::BindGroupEntry {
3520                binding: binding as u32,
3521                resource,
3522            })
3523            .collect();
3524        self.gpu
3525            .device
3526            .create_bind_group(&wgpu::BindGroupDescriptor {
3527                label: Some("live lighting"),
3528                layout: &self.lighting_layout,
3529                entries: &entries,
3530            })
3531    }
3532
3533    pub fn encode(
3534        &mut self,
3535        scene: &Scene<'_>,
3536        encoder: &mut wgpu::CommandEncoder,
3537        shadows: Option<(&mut Shadows, &Fit)>,
3538        profiler: Option<&mut GpuProfiler>,
3539    ) -> Result<(), String> {
3540        self.encode_with_shadow_hook(scene, encoder, shadows, profiler, |_, _, _, _| Ok(()))
3541    }
3542
3543    pub fn encode_with_shadow_hook(
3544        &mut self,
3545        scene: &Scene<'_>,
3546        encoder: &mut wgpu::CommandEncoder,
3547        shadows: Option<(&mut Shadows, &Fit)>,
3548        profiler: Option<&mut GpuProfiler>,
3549        shadow_hook: impl FnOnce(
3550            &Gpu,
3551            &mut wgpu::CommandEncoder,
3552            &mut Shadows,
3553            &Fit,
3554        ) -> Result<(), String>,
3555    ) -> Result<(), String> {
3556        self.encode_with_casts(scene, encoder, shadows, profiler, None, shadow_hook)
3557    }
3558
3559    pub fn encode_with_casts(
3560        &mut self,
3561        scene: &Scene<'_>,
3562        encoder: &mut wgpu::CommandEncoder,
3563        shadows: Option<(&mut Shadows, &Fit)>,
3564        mut profiler: Option<&mut GpuProfiler>,
3565        casts: Option<&mut crate::shadow::LayerDraw<'_>>,
3566        shadow_hook: impl FnOnce(
3567            &Gpu,
3568            &mut wgpu::CommandEncoder,
3569            &mut Shadows,
3570            &Fit,
3571        ) -> Result<(), String>,
3572    ) -> Result<(), String> {
3573        let (batches, surfaces) = self.prepare(scene)?;
3574        if batches.iter().any(|batch| batch.cut) {
3575            self.pipelines.ensure_cut(&self.gpu.device);
3576        }
3577        if let Some(foliage) = &mut self.foliage {
3578            foliage.canopy.select_lod(
3579                &self.gpu.queue,
3580                scene.camera,
3581                self.viewport[1],
3582                foliage.origin,
3583            );
3584            let timing = profiler
3585                .as_deref_mut()
3586                .and_then(|timer| timer.pass("canopy cookie"));
3587            let sun = scene.sun.direction;
3588            foliage.canopy.update(
3589                &self.gpu.queue,
3590                encoder,
3591                CanopyFrame {
3592                    origin: foliage.origin,
3593                    time: scene.wind.current.time,
3594                    strength: scene.wind.current.strength,
3595                    direction: scene.wind.current.direction,
3596                    sun,
3597                    receiver_y: foliage.receiver_y,
3598                    angular_radius: SUN_RADIUS,
3599                },
3600                profiler
3601                    .as_deref()
3602                    .and_then(|timer| timer.render_writes(timing)),
3603            );
3604        }
3605        let mut key = mix_key(
3606            static_key(scene.instances, &batches, &surfaces),
3607            self.mesh_epoch,
3608        );
3609        if batches.iter().any(|batch| batch.cut && batch.casts_shadow) {
3610            key = mix_key(key, self.cut_key(scene, &batches, &surfaces));
3611        }
3612        if let Some((shadows, fit)) = shadows {
3613            let cascades = mix_key(key, self.lights.table_key());
3614            self.encode_shadows(
3615                cascades,
3616                &batches,
3617                shadows,
3618                fit,
3619                encoder,
3620                profiler.as_deref_mut(),
3621            );
3622            let transmitting = self.encode_transmission(
3623                cascades,
3624                &batches,
3625                scene.instances,
3626                shadows,
3627                fit,
3628                encoder,
3629                casts,
3630                profiler.as_deref_mut(),
3631            );
3632            shadow_hook(&self.gpu, encoder, shadows, fit)?;
3633            self.encode_lights(key, &batches, encoder, profiler.as_deref_mut());
3634            self.lights
3635                .write_uniform(&self.gpu.queue, transmitting, shadows.face_tiles());
3636            self.set_shadows(shadows, fit);
3637        } else {
3638            self.encode_lights(key, &batches, encoder, profiler.as_deref_mut());
3639            self.lights
3640                .write_uniform(&self.gpu.queue, false, Default::default());
3641            self.set_unshadowed();
3642        }
3643        let features = self.opaque_features(&batches);
3644        let lit = self.lights.extended();
3645        for (batch, used) in batches.iter().zip(&features) {
3646            if batch.shadow_only {
3647                continue;
3648            }
3649            self.pipelines.ensure_opaque(
3650                &self.gpu.device,
3651                OpaqueKey {
3652                    features: *used,
3653                    lit,
3654                    two_sided: batch.two_sided,
3655                },
3656            );
3657        }
3658        self.encode_surfaces(&batches, &features, encoder, profiler)?;
3659        self.opaque_drawn = batches
3660            .iter()
3661            .zip(features)
3662            .filter(|(batch, _)| !batch.shadow_only)
3663            .map(|(_, used)| used)
3664            .collect();
3665        Ok(())
3666    }
3667
3668    fn cut_key(&self, scene: &Scene<'_>, batches: &[Batch], surfaces: &[InstanceSurface]) -> u64 {
3669        let mut hasher = std::collections::hash_map::DefaultHasher::new();
3670        self.content_epoch.get().hash(&mut hasher);
3671        for batch in batches
3672            .iter()
3673            .filter(|batch| batch.cut && batch.casts_shadow)
3674        {
3675            for index in batch.range.clone() {
3676                let index = index as usize;
3677                let instance = &scene.instances[index];
3678                let surface = &surfaces[index];
3679                instance.alpha_cutoff.to_bits().hash(&mut hasher);
3680                instance.opacity.to_bits().hash(&mut hasher);
3681                content_row_key(&scene.materials[instance.material as usize]).hash(&mut hasher);
3682                for value in surface
3683                    .uv_offset
3684                    .iter()
3685                    .chain(&surface.uv_scale)
3686                    .chain(&surface.crop)
3687                {
3688                    value.to_bits().hash(&mut hasher);
3689                }
3690            }
3691        }
3692        hasher.finish()
3693    }
3694
3695    #[allow(clippy::too_many_arguments)]
3696    fn encode_transmission(
3697        &self,
3698        key: u64,
3699        batches: &[Batch],
3700        instances: &[Instance],
3701        shadows: &mut Shadows,
3702        fit: &Fit,
3703        encoder: &mut wgpu::CommandEncoder,
3704        mut casts: Option<&mut crate::shadow::LayerDraw<'_>>,
3705        profiler: Option<&mut GpuProfiler>,
3706    ) -> bool {
3707        let chosen = if self.lights.any_transmissive() {
3708            transmission_batches(batches, instances, |material| {
3709                self.lights.transmissive(material)
3710            })
3711        } else {
3712            Vec::new()
3713        };
3714        if chosen.is_empty() && casts.is_none() {
3715            return false;
3716        }
3717        let mut hasher = std::collections::hash_map::DefaultHasher::new();
3718        key.hash(&mut hasher);
3719        for batch in &chosen {
3720            batch.range.hash(&mut hasher);
3721            for index in batch.range.clone() {
3722                instances[index as usize].material.hash(&mut hasher);
3723            }
3724        }
3725        let moving = casts.is_some() || chosen.iter().any(|batch| batch.moving);
3726        let faces = if casts.is_some() {
3727            self.lights.faces().len()
3728        } else {
3729            0
3730        };
3731        let mut draw = |pass: &mut wgpu::RenderPass<'_>, cascade: usize, statics: bool| {
3732            if !chosen.is_empty() && cascade < CASCADE_COUNT {
3733                pass.set_pipeline(&self.pipelines.transmit);
3734                pass.set_bind_group(0, &self.scene_group, &[]);
3735                pass.set_bind_group(
3736                    1,
3737                    &self.cascade_group,
3738                    &[self.cascade_align * cascade as u32],
3739                );
3740                pass.set_bind_group(2, &self.deformers.group, &[]);
3741                for batch in chosen.iter().filter(|batch| batch.moving != statics) {
3742                    let Some(mesh) = live_mesh(&self.meshes, batch.mesh) else {
3743                        continue;
3744                    };
3745                    pass.set_vertex_buffer(0, mesh.vertices.slice(..));
3746                    pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
3747                    pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
3748                }
3749            }
3750            if !statics && let Some(casts) = casts.as_mut() {
3751                casts(pass, cascade, statics);
3752            }
3753        };
3754        shadows.render_transmission(
3755            &self.gpu.device,
3756            encoder,
3757            fit,
3758            hasher.finish(),
3759            moving,
3760            (faces, self.lights.quality().resolution),
3761            &mut draw,
3762            profiler,
3763        );
3764        true
3765    }
3766
3767    fn encode_lights(
3768        &mut self,
3769        key: u64,
3770        batches: &[Batch],
3771        encoder: &mut wgpu::CommandEncoder,
3772        profiler: Option<&mut GpuProfiler>,
3773    ) {
3774        let faces = self.lights.faces();
3775        if faces.is_empty() {
3776            return;
3777        }
3778        for (layer, matrix) in &faces {
3779            let mut slot = [0u8; CASTER_BYTES as usize];
3780            slot[..64].copy_from_slice(bytemuck::cast_slice(matrix));
3781            self.gpu.queue.write_buffer(
3782                &self.cascade_uniform,
3783                u64::from(self.cascade_align) * (CASCADE_COUNT as u64 + u64::from(*layer)),
3784                &slot,
3785            );
3786        }
3787        let casters = shadow_batches(batches);
3788        let moving = casters.iter().any(|batch| batch.moving);
3789        let scene_group = &self.scene_group;
3790        let cascade_group = &self.cascade_group;
3791        let deformers = &self.deformers.group;
3792        let lighting = self.lighting_group.as_ref();
3793        let pipelines = &self.pipelines;
3794        let meshes = &self.meshes;
3795        let align = self.cascade_align;
3796        let mut draw = |pass: &mut wgpu::RenderPass<'_>, layer: usize, statics: bool| {
3797            pass.set_bind_group(0, scene_group, &[]);
3798            pass.set_bind_group(1, cascade_group, &[align * (CASCADE_COUNT + layer) as u32]);
3799            draw_casters(
3800                pass, &casters, statics, deformers, lighting, pipelines, meshes,
3801            );
3802        };
3803        self.lights
3804            .render(&self.gpu.device, encoder, key, moving, &mut draw, profiler);
3805    }
3806
3807    fn encode_shadows(
3808        &self,
3809        key: u64,
3810        batches: &[Batch],
3811        shadows: &mut Shadows,
3812        fit: &Fit,
3813        encoder: &mut wgpu::CommandEncoder,
3814        profiler: Option<&mut GpuProfiler>,
3815    ) {
3816        let mode = if self.lights.any_transmissive() {
3817            1.0f32
3818        } else {
3819            0.0
3820        };
3821        let near_map = shadows.quality().splits_near_map();
3822        for (index, cascade) in fit.cascades.iter().enumerate() {
3823            let pancake = if near_map && index == 1 { 0.0f32 } else { 1.0 };
3824            let mut slot = [0u8; CASTER_BYTES as usize];
3825            slot[..64].copy_from_slice(bytemuck::cast_slice(&cascade.view_proj.columns));
3826            slot[64..68].copy_from_slice(&mode.to_le_bytes());
3827            slot[68..72].copy_from_slice(&pancake.to_le_bytes());
3828            self.gpu.queue.write_buffer(
3829                &self.cascade_uniform,
3830                u64::from(self.cascade_align) * index as u64,
3831                &slot,
3832            );
3833        }
3834        let casters = shadow_batches(batches);
3835        let moving = casters.iter().any(|batch| batch.moving);
3836        let mut draw = |pass: &mut wgpu::RenderPass<'_>, cascade: usize, statics: bool| {
3837            pass.set_bind_group(0, &self.scene_group, &[]);
3838            pass.set_bind_group(
3839                1,
3840                &self.cascade_group,
3841                &[self.cascade_align * cascade as u32],
3842            );
3843            self.draw_casters(pass, &casters, statics);
3844        };
3845        shadows.render_with(encoder, fit, key, moving, &mut draw, profiler);
3846    }
3847
3848    fn draw_casters(&self, pass: &mut wgpu::RenderPass<'_>, casters: &[&Batch], statics: bool) {
3849        draw_casters(
3850            pass,
3851            casters,
3852            statics,
3853            &self.deformers.group,
3854            self.lighting_group.as_ref(),
3855            &self.pipelines,
3856            &self.meshes,
3857        );
3858    }
3859
3860    fn encode_surfaces(
3861        &self,
3862        batches: &[Batch],
3863        features: &[OpaqueFeatures],
3864        encoder: &mut wgpu::CommandEncoder,
3865        mut profiler: Option<&mut GpuProfiler>,
3866    ) -> Result<(), String> {
3867        let lighting = self
3868            .lighting_group
3869            .as_ref()
3870            .ok_or("frame lighting is not prepared")?;
3871        let depth_timing = profiler
3872            .as_deref_mut()
3873            .and_then(|timer| timer.pass("depth and velocity"));
3874        {
3875            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3876                multiview_mask: None,
3877                label: Some("depth and velocity"),
3878                color_attachments: &[
3879                    Some(wgpu::RenderPassColorAttachment {
3880                        view: &self.targets.velocity_view,
3881                        resolve_target: None,
3882                        depth_slice: None,
3883                        ops: wgpu::Operations {
3884                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3885                            store: wgpu::StoreOp::Store,
3886                        },
3887                    }),
3888                    Some(wgpu::RenderPassColorAttachment {
3889                        view: &self.targets.ids_view,
3890                        resolve_target: None,
3891                        depth_slice: None,
3892                        ops: wgpu::Operations {
3893                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3894                            store: wgpu::StoreOp::Store,
3895                        },
3896                    }),
3897                ],
3898                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3899                    view: &self.targets.depth_view,
3900                    depth_ops: Some(wgpu::Operations {
3901                        load: wgpu::LoadOp::Clear(1.0),
3902                        store: wgpu::StoreOp::Store,
3903                    }),
3904                    stencil_ops: None,
3905                }),
3906                occlusion_query_set: None,
3907                timestamp_writes: profiler
3908                    .as_deref()
3909                    .and_then(|timer| timer.render_writes(depth_timing)),
3910            });
3911            crate::viewport::apply(&mut pass, self.viewport);
3912            self.draw_with(&mut pass, batches, lighting, |index, two_sided| {
3913                let pair = match &self.pipelines.cut {
3914                    Some(cut) if batches[index].cut => &cut.prepass,
3915                    _ => &self.pipelines.prepass,
3916                };
3917                &pair[usize::from(two_sided)]
3918            });
3919            if let Some(foliage) = &self.foliage {
3920                self.draw_cards(&mut pass, foliage, &foliage.prepass, lighting);
3921            }
3922        }
3923        let opaque_timing = profiler
3924            .as_deref_mut()
3925            .and_then(|timer| timer.pass("opaque PBR"));
3926        {
3927            let mut pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
3928                multiview_mask: None,
3929                label: Some("opaque PBR"),
3930                color_attachments: &[
3931                    Some(wgpu::RenderPassColorAttachment {
3932                        view: &self.targets.hdr.view,
3933                        resolve_target: None,
3934                        depth_slice: None,
3935                        ops: wgpu::Operations {
3936                            load: wgpu::LoadOp::Clear(wgpu::Color::BLACK),
3937                            store: wgpu::StoreOp::Store,
3938                        },
3939                    }),
3940                    Some(wgpu::RenderPassColorAttachment {
3941                        view: &self.targets.normal_roughness_view,
3942                        resolve_target: None,
3943                        depth_slice: None,
3944                        ops: wgpu::Operations {
3945                            load: wgpu::LoadOp::Clear(wgpu::Color::TRANSPARENT),
3946                            store: wgpu::StoreOp::Store,
3947                        },
3948                    }),
3949                ],
3950                depth_stencil_attachment: Some(wgpu::RenderPassDepthStencilAttachment {
3951                    view: &self.targets.depth_view,
3952                    depth_ops: Some(wgpu::Operations {
3953                        load: wgpu::LoadOp::Load,
3954                        store: wgpu::StoreOp::Store,
3955                    }),
3956                    stencil_ops: None,
3957                }),
3958                occlusion_query_set: None,
3959                timestamp_writes: profiler
3960                    .as_deref()
3961                    .and_then(|timer| timer.render_writes(opaque_timing)),
3962            });
3963            crate::viewport::apply(&mut pass, self.viewport);
3964            let lit = self.lights.extended();
3965            self.draw_with(&mut pass, batches, lighting, |index, two_sided| {
3966                &self.pipelines.opaque[&OpaqueKey {
3967                    features: features[index],
3968                    lit,
3969                    two_sided,
3970                }]
3971            });
3972            if let Some(foliage) = &self.foliage {
3973                self.draw_cards(&mut pass, foliage, &foliage.opaque, lighting);
3974            }
3975        }
3976        Ok(())
3977    }
3978
3979    fn draw_with<'p>(
3980        &self,
3981        pass: &mut wgpu::RenderPass<'_>,
3982        batches: &[Batch],
3983        lighting: &wgpu::BindGroup,
3984        pipelines: impl Fn(usize, bool) -> &'p wgpu::RenderPipeline,
3985    ) {
3986        pass.set_bind_group(0, &self.scene_group, &[]);
3987        pass.set_bind_group(1, &self.shadow_group, &[]);
3988        pass.set_bind_group(2, &self.deformers.group, &[]);
3989        pass.set_bind_group(3, lighting, &[]);
3990        let mut current: Option<&wgpu::RenderPipeline> = None;
3991        for (index, batch) in batches
3992            .iter()
3993            .enumerate()
3994            .filter(|(_, batch)| !batch.shadow_only)
3995        {
3996            let chosen = pipelines(index, batch.two_sided);
3997            if !current.is_some_and(|last| std::ptr::eq(last, chosen)) {
3998                pass.set_pipeline(chosen);
3999                current = Some(chosen);
4000            }
4001            let Some(mesh) = live_mesh(&self.meshes, batch.mesh) else {
4002                continue;
4003            };
4004            pass.set_vertex_buffer(0, mesh.vertices.slice(..));
4005            pass.set_index_buffer(mesh.indices.slice(..), wgpu::IndexFormat::Uint32);
4006            pass.draw_indexed(0..mesh.count, 0, batch.range.clone());
4007        }
4008    }
4009
4010    fn draw_cards(
4011        &self,
4012        pass: &mut wgpu::RenderPass<'_>,
4013        foliage: &Foliage,
4014        pipeline: &wgpu::RenderPipeline,
4015        lighting: &wgpu::BindGroup,
4016    ) {
4017        if foliage.canopy.card_count == 0 {
4018            return;
4019        }
4020        pass.set_pipeline(pipeline);
4021        pass.set_bind_group(0, &self.scene_group, &[]);
4022        pass.set_bind_group(1, &self.shadow_group, &[]);
4023        pass.set_bind_group(2, foliage.canopy.cookie_bind(), &[]);
4024        pass.set_bind_group(3, lighting, &[]);
4025        pass.draw(0..6, 0..foliage.canopy.card_count);
4026    }
4027
4028    pub fn collect_timings(&mut self) -> Vec<Vec<PassTiming>> {
4029        self.profiler.collect(&self.gpu.device)
4030    }
4031
4032    pub fn readback_velocity(&self) -> Result<Vec<[u16; 2]>, String> {
4033        let [width, height] = self.viewport;
4034        let row = (width * 4).div_ceil(wgpu::COPY_BYTES_PER_ROW_ALIGNMENT)
4035            * wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
4036        let size = u64::from(row) * u64::from(height);
4037        let buffer = self.gpu.device.create_buffer(&wgpu::BufferDescriptor {
4038            label: Some("live velocity readback"),
4039            size,
4040            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
4041            mapped_at_creation: false,
4042        });
4043        let mut encoder = self
4044            .gpu
4045            .device
4046            .create_command_encoder(&wgpu::CommandEncoderDescriptor {
4047                label: Some("live velocity readback"),
4048            });
4049        encoder.copy_texture_to_buffer(
4050            self.targets.velocity.as_image_copy(),
4051            wgpu::TexelCopyBufferInfo {
4052                buffer: &buffer,
4053                layout: wgpu::TexelCopyBufferLayout {
4054                    offset: 0,
4055                    bytes_per_row: Some(row),
4056                    rows_per_image: Some(height),
4057                },
4058            },
4059            wgpu::Extent3d {
4060                width,
4061                height,
4062                depth_or_array_layers: 1,
4063            },
4064        );
4065        self.gpu.queue.submit(Some(encoder.finish()));
4066        let (send, receive) = std::sync::mpsc::channel();
4067        let slice = buffer.slice(..);
4068        slice.map_async(wgpu::MapMode::Read, move |result| {
4069            let _ = send.send(result);
4070        });
4071        self.gpu
4072            .device
4073            .poll(wgpu::PollType::wait_indefinitely())
4074            .map_err(|error| format!("{error:?}"))?;
4075        receive
4076            .recv()
4077            .map_err(|error| error.to_string())?
4078            .map_err(|error| error.to_string())?;
4079        let mapped = slice.get_mapped_range().expect("a mapped range");
4080        let mut values = Vec::with_capacity((width * height) as usize);
4081        for padded in mapped.chunks_exact(row as usize) {
4082            for pixel in padded[..width as usize * 4].chunks_exact(4) {
4083                values.push([
4084                    u16::from_le_bytes([pixel[0], pixel[1]]),
4085                    u16::from_le_bytes([pixel[2], pixel[3]]),
4086                ]);
4087            }
4088        }
4089        drop(mapped);
4090        buffer.unmap();
4091        Ok(values)
4092    }
4093}
4094
4095const SHADER: &str = r#"
4096override use_sun_shadow: bool = true;
4097override use_ssr: bool = true;
4098override use_canopy: bool = true;
4099override use_contact: bool = true;
4100override use_authored_ambient: bool = true;
4101override use_caustics: bool = true;
4102struct FrameUniforms {
4103    view_projection: mat4x4f,
4104    motion_view_projection: mat4x4f,
4105    previous_view_projection: mat4x4f,
4106    view: mat4x4f,
4107    camera_position: vec4f,
4108    sun_direction: vec4f,
4109    sun_colour_intensity: vec4f,
4110    time_size: vec4f,
4111    flags: vec4u,
4112    wind: vec4f,
4113    wind_direction: vec4f,
4114    previous_wind_direction: vec4f,
4115}
4116struct InstanceData {
4117    model: mat4x4f,
4118    previous_model: mat4x4f,
4119    normal0: vec4f,
4120    normal1: vec4f,
4121    normal2: vec4f,
4122    fields: vec4u,
4123    extra: vec4u,
4124    clip0: vec4f,
4125    clip1: vec4f,
4126    content_uv: vec4f,
4127    content_crop: vec4f,
4128    content_face: vec4u,
4129    skin: vec4u,
4130}
4131struct SwayData {
4132    pivot_level: vec4f,
4133    stiffness: vec4f,
4134}
4135struct SkyLight {
4136    sh: array<vec4f, 9>,
4137    info: vec4f,
4138}
4139struct MaterialData {
4140    base_roughness: vec4f,
4141    metal_spec_coat: vec4f,
4142    sheen_trans_ior_disp: vec4f,
4143    thick_sub_film: vec4f,
4144    tint_absorption: vec4f,
4145    emission_film: vec4f,
4146    normal_maps: vec4f,
4147    maps_content: vec4f,
4148    age_a: vec4f,
4149    age_b: vec4f,
4150    layer0: vec4f,
4151    layer1: vec4f,
4152    layer2: vec4f,
4153    layer3: vec4f,
4154    tail: vec4f,
4155    params: array<vec4f, 16>,
4156}
4157@group(0) @binding(0) var<uniform> frame: FrameUniforms;
4158@group(0) @binding(1) var<storage, read> instances: array<InstanceData>;
4159@group(0) @binding(2) var<storage, read> materials: array<MaterialData>;
4160@group(0) @binding(3) var<storage, read> sway_data: array<SwayData>;
4161@group(1) @binding(0) var<uniform> shadow_setting: ShadowSample;
4162@group(1) @binding(1) var shadow_map: texture_depth_2d_array;
4163@group(1) @binding(2) var shadow_sampler: sampler_comparison;
4164struct CasterView {
4165    view_proj: mat4x4f,
4166    mode: vec4f,
4167}
4168@group(1) @binding(3) var<uniform> caster: CasterView;
4169@group(3) @binding(12) var<uniform> sky_light: SkyLight;
4170@group(3) @binding(13) var sky_cube: texture_cube<f32>;
4171@group(3) @binding(14) var local_probes: texture_cube_array<f32>;
4172@group(3) @binding(15) var reflect_sampler: sampler;
4173@group(3) @binding(16) var ssr_history: texture_2d<f32>;
4174@group(3) @binding(26) var ssr_guide: texture_2d<f32>;
4175struct ReflectionSelection {
4176    first: ReflectionProbe,
4177    second: ReflectionProbe,
4178    weights_mip: vec4f,
4179}
4180@group(3) @binding(17) var<uniform> selected_reflections: ReflectionSelection;
4181struct ContactParams {
4182    rect: vec4f,
4183    tint: vec4f,
4184}
4185@group(3) @binding(23) var contact_field: texture_2d<f32>;
4186@group(3) @binding(24) var<uniform> contact_params: ContactParams;
4187@group(3) @binding(25) var<uniform> authored_light: SkyLight;
4188struct VertexInput {
4189    @location(0) position: vec4f,
4190    @location(1) normal: vec4f,
4191    @location(2) tangent: vec4f,
4192    @location(3) uv_alpha: vec4f,
4193    @builtin(instance_index) instance_index: u32,
4194    @builtin(vertex_index) vertex_index: u32,
4195}
4196struct VertexOutput {
4197    @builtin(position) position: vec4f,
4198    @location(0) world_position: vec3f,
4199    @location(1) normal: vec4f,
4200    @location(2) tangent: vec4f,
4201    @location(3) uv_alpha: vec4f,
4202    @location(4) previous_clip: vec4f,
4203    @location(5) current_clip: vec4f,
4204    @location(6) @interpolate(flat) material: u32,
4205    @location(7) @interpolate(flat) id: u32,
4206    @location(8) @interpolate(flat) settings: vec4f,
4207    @location(9) object_position: vec3f,
4208    @location(10) @interpolate(flat) instance: u32,
4209}
4210fn surface_shape(input: VertexInput, instance: InstanceData) -> DeformedVertex {
4211    if (instance.skin.x != 0xffffffffu) {
4212        return skin_shape(input.position.xyz, input.normal.xyz, input.tangent, input.vertex_index, instance.skin);
4213    }
4214    var shape = deform_vertex(input.position.xyz, input.normal.xyz, input.tangent, instance.extra.x);
4215    if (instance.extra.y != 0xffffffffu) {
4216        let sway = sway_data[instance.extra.y + input.vertex_index];
4217        shape.position = tree_sway(shape.position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz);
4218        shape.previous_position = tree_sway(shape.previous_position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.z, frame.wind.w, frame.previous_wind_direction.xyz);
4219    }
4220    return shape;
4221}
4222@vertex fn vertex_main(input: VertexInput) -> VertexOutput {
4223    let instance = instances[input.instance_index];
4224    let shape = surface_shape(input, instance);
4225    let world = instance.model * vec4f(shape.position, 1.0);
4226    let normal_matrix = mat3x3f(instance.normal0.xyz, instance.normal1.xyz, instance.normal2.xyz);
4227    var output: VertexOutput;
4228    output.position = frame.view_projection * world;
4229    output.current_clip = frame.motion_view_projection * world;
4230    let previous_world = select(instance.previous_model * vec4f(shape.previous_position, 1.0), world, instance.content_face.y != 0u);
4231    output.previous_clip = frame.previous_view_projection * previous_world;
4232    output.world_position = world.xyz;
4233    output.normal = vec4f(normalize(normal_matrix * shape.normal), input.normal.w);
4234    output.tangent = vec4f(normalize((instance.model * vec4f(shape.tangent.xyz, 0.0)).xyz), shape.tangent.w);
4235    output.uv_alpha = input.uv_alpha;
4236    output.material = instance.fields.x;
4237    output.id = instance.fields.y;
4238    output.settings = vec4f(bitcast<f32>(instance.fields.z), bitcast<f32>(instance.fields.w), bitcast<f32>(instance.extra.z), bitcast<f32>(instance.extra.w));
4239    output.object_position = input.position.xyz;
4240    output.instance = input.instance_index;
4241    return output;
4242}
4243fn caster_position(input: VertexInput, instance: InstanceData) -> vec3f {
4244    if (instance.skin.x != 0xffffffffu) {
4245        return skin_position(input.position.xyz, input.vertex_index, instance.skin);
4246    }
4247    var position = deform_position(input.position.xyz, instance.extra.x, false);
4248    if (instance.extra.y != 0xffffffffu) {
4249        let sway = sway_data[instance.extra.y + input.vertex_index];
4250        position = tree_sway(position, sway.pivot_level.xyz, sway.pivot_level.w, sway.stiffness.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz);
4251    }
4252    return position;
4253}
4254@vertex fn shadow_vertex(input: VertexInput) -> @builtin(position) vec4f {
4255    let instance = instances[input.instance_index];
4256    if (caster_skips(instance.fields.x)) {
4257        return vec4f(2.0, 2.0, 2.0, 1.0);
4258    }
4259    let position = caster_position(input, instance);
4260    return pancake(caster.view_proj * instance.model * vec4f(position, 1.0));
4261}
4262fn pancake(clip: vec4f) -> vec4f {
4263    if (caster.mode.y > 0.5) {
4264        return vec4f(clip.xy, max(clip.z, 0.0), clip.w);
4265    }
4266    return clip;
4267}
4268fn clip_plane(plane: vec4f, world: vec3f) -> bool {
4269    return any(plane.xyz != vec3f(0.0)) && dot(plane.xyz, world) > plane.w;
4270}
4271fn clipped(instance: u32, world: vec3f) -> bool {
4272    let data = instances[instance];
4273    return clip_plane(data.clip0, world) || clip_plane(data.clip1, world);
4274}
4275struct ShadowClipOutput {
4276    @builtin(position) position: vec4f,
4277    @location(0) world_position: vec3f,
4278    @location(1) @interpolate(flat) instance: u32,
4279}
4280@vertex fn shadow_clip_vertex(input: VertexInput) -> ShadowClipOutput {
4281    let instance = instances[input.instance_index];
4282    if (caster_skips(instance.fields.x)) {
4283        var skipped: ShadowClipOutput;
4284        skipped.position = vec4f(2.0, 2.0, 2.0, 1.0);
4285        return skipped;
4286    }
4287    let position = caster_position(input, instance);
4288    let world = instance.model * vec4f(position, 1.0);
4289    var output: ShadowClipOutput;
4290    output.position = pancake(caster.view_proj * world);
4291    output.world_position = world.xyz;
4292    output.instance = input.instance_index;
4293    return output;
4294}
4295@fragment fn shadow_clip_fragment(input: ShadowClipOutput) {
4296    if (clipped(input.instance, input.world_position)) {
4297        discard;
4298    }
4299}
4300struct ShadowCutOutput {
4301    @builtin(position) position: vec4f,
4302    @location(0) world_position: vec3f,
4303    @location(1) @interpolate(flat) instance: u32,
4304    @location(2) uv_alpha: vec4f,
4305}
4306@vertex fn shadow_cut_vertex(input: VertexInput) -> ShadowCutOutput {
4307    let instance = instances[input.instance_index];
4308    var output: ShadowCutOutput;
4309    if (caster_skips(instance.fields.x)) {
4310        output.position = vec4f(2.0, 2.0, 2.0, 1.0);
4311        return output;
4312    }
4313    let position = caster_position(input, instance);
4314    let world = instance.model * vec4f(position, 1.0);
4315    output.position = pancake(caster.view_proj * world);
4316    output.world_position = world.xyz;
4317    output.instance = input.instance_index;
4318    output.uv_alpha = input.uv_alpha;
4319    return output;
4320}
4321@fragment fn shadow_cut_fragment(input: ShadowCutOutput) {
4322    if (clipped(input.instance, input.world_position)) {
4323        discard;
4324    }
4325    let data = instances[input.instance];
4326    let cutoff = bitcast<f32>(data.fields.w);
4327    if (input.uv_alpha.z * bitcast<f32>(data.fields.z) < cutoff) {
4328        discard;
4329    }
4330    if (cut_by_content(input.instance, data.fields.x, input.uv_alpha.xy, cutoff)) {
4331        discard;
4332    }
4333}
4334fn cut_by_content(instance: u32, material: u32, uv: vec2f, cutoff: f32) -> bool {
4335    let data = instances[instance];
4336    return (data.content_face.z & 4u) != 0u && content_cut(material, uv, data.content_uv, data.content_crop, cutoff);
4337}
4338fn cutout(input: VertexOutput) {
4339    if (clipped(input.instance, input.world_position)) {
4340        discard;
4341    }
4342    if (input.uv_alpha.z * input.settings.x < input.settings.y) {
4343        discard;
4344    }
4345    if (use_content_cutout && cut_by_content(input.instance, input.material, input.uv_alpha.xy, input.settings.y)) {
4346        discard;
4347    }
4348    if (input.settings.w < 1.0 && !lod_keep(vec2u(input.position.xy), input.id, input.settings.w)) {
4349        discard;
4350    }
4351}
4352struct PrepassOutput {
4353    @location(0) velocity: vec2f,
4354    @location(1) id: u32,
4355}
4356fn motion(current_clip: vec4f, previous_clip: vec4f) -> vec2f {
4357    let current = current_clip.xy / current_clip.w;
4358    let previous = previous_clip.xy / previous_clip.w;
4359    let moved = (previous - current) * vec2f(0.5, -0.5);
4360    return select(moved, vec2f(0.0), abs(moved) < vec2f(1e-8));
4361}
4362@fragment fn prepass_main(input: VertexOutput) -> PrepassOutput {
4363    cutout(input);
4364    var output: PrepassOutput;
4365    if (instances[input.instance].content_face.y != 0u && frame.camera_position.w != 0.0) {
4366        output.velocity = vec2f(0.0);
4367    } else {
4368        output.velocity = motion(input.current_clip, input.previous_clip);
4369    }
4370    output.id = input.id;
4371    return output;
4372}
4373fn shadow_visibility(world_position: vec3f, normal: vec3f) -> f32 {
4374    if (!use_sun_shadow || shadow_setting.texels.w > 0.5) { return 1.0; }
4375    let distance = length(world_position - frame.camera_position.xyz);
4376    return shadow_sample(shadow_map, shadow_sampler, reflect_sampler, shadow_setting, world_position, normal, distance);
4377}
4378fn sky_irradiance(n: vec3f) -> vec3f {
4379    return sh_irradiance(sky_light.sh, n);
4380}
4381fn sh_irradiance(sh: array<vec4f, 9>, n: vec3f) -> vec3f {
4382    let d = normalize(n);
4383    let basis0 = vec3f(0.282095, 0.488603 * d.y, 0.488603 * d.z);
4384    let basis1 = vec3f(0.488603 * d.x, 1.092548 * d.x * d.y, 1.092548 * d.y * d.z);
4385    let basis2 = vec3f(0.315392 * (3.0 * d.z * d.z - 1.0), 1.092548 * d.x * d.z, 0.546274 * (d.x * d.x - d.y * d.y));
4386    let value = sh[0].xyz * basis0.x + sh[1].xyz * basis0.y + sh[2].xyz * basis0.z
4387        + sh[3].xyz * basis1.x + sh[4].xyz * basis1.y + sh[5].xyz * basis1.z
4388        + sh[6].xyz * basis2.x + sh[7].xyz * basis2.y + sh[8].xyz * basis2.z;
4389    return max(value, vec3f(0.0));
4390}
4391fn ambient_irradiance(position: vec3f, normal: vec3f) -> vec3f {
4392    let grid = sample_live_irradiance(position, normal);
4393    if (grid.x >= 0.0) {
4394        return grid;
4395    }
4396    return sky_irradiance(normal);
4397}
4398fn surface_irradiance(grid: vec3f, normal: vec3f, authored: bool) -> vec3f {
4399    if (grid.x >= 0.0) {
4400        return grid;
4401    }
4402    if (authored) {
4403        return sh_irradiance(authored_light.sh, normal);
4404    }
4405    return sky_irradiance(normal);
4406}
4407fn reflection_reflective(surface: Shaded) -> bool {
4408    return surface.specular > 0.0 || surface.metalness > 0.0 || surface.clearcoat > 0.0 || surface.transmission > 0.0 || surface.thin_film_amount > 0.0;
4409}
4410fn reflection_wants_probes(surface: Shaded, release: f32) -> bool {
4411    if (!use_probes || probe_grid.shade.y <= 0.0 || probe_grid.blend.y < 0.5 || release >= 1.0) {
4412        return false;
4413    }
4414    return reflection_reflective(surface);
4415}
4416fn reflection_sky_visibility(seen: f32, direction: vec3f, lookup: LocalLookup, grid: vec3f, release: f32) -> f32 {
4417    if (seen < 0.0 || grid.x < 0.0 || lookup.weights.x + lookup.weights.y >= 1.0) {
4418        return 1.0;
4419    }
4420    let reference = dot(sky_irradiance(direction), vec3f(0.2126, 0.7152, 0.0722));
4421    if (!(reference > 0.000001)) {
4422        return 1.0;
4423    }
4424    return mix(clamp(seen / reference, 0.0, 1.0), 1.0, release);
4425}
4426fn reflection_irradiance(n: vec3f, world: vec3f, sky_visible: f32) -> vec3f {
4427    var a = 0.0;
4428    var b = 0.0;
4429    if (use_local_reflections) {
4430        let weights = local_probe_weights(selected_reflections.first, selected_reflections.second, selected_reflections.weights_mip.xy, world);
4431        a = clamp(weights.x, 0.0, 1.0);
4432        b = clamp(weights.y, 0.0, 1.0 - a);
4433    }
4434    var reference = sky_irradiance(n) * ((1.0 - a - b) * sky_visible);
4435    if (a != 0.0) {
4436        reference += local_probe_reference(selected_reflections.first, world, n, local_probes, reflect_sampler, selected_reflections.weights_mip.z) * (PI * a);
4437    }
4438    if (b != 0.0) {
4439        reference += local_probe_reference(selected_reflections.second, world, n, local_probes, reflect_sampler, selected_reflections.weights_mip.z) * (PI * b);
4440    }
4441    return reference;
4442}
4443fn contact_factor(world_position: vec3f) -> vec3f {
4444    if (contact_params.tint.w < 0.5) { return vec3f(1.0); }
4445    let uv = (world_position.xz - contact_params.rect.xy) * contact_params.rect.zw;
4446    if (any(uv < vec2f(0.0)) || any(uv > vec2f(1.0))) { return vec3f(1.0); }
4447    let open = textureSampleLevel(contact_field, reflect_sampler, uv, 0.0).r;
4448    return mix(contact_params.tint.xyz, vec3f(1.0), open * open);
4449}
4450fn shaded(material: MaterialData, normal: vec3f) -> Shaded {
4451    return Shaded(
4452        material.base_roughness.xyz,
4453        material.base_roughness.w,
4454        material.metal_spec_coat.x,
4455        material.metal_spec_coat.y,
4456        material.metal_spec_coat.z,
4457        material.metal_spec_coat.w,
4458        material.sheen_trans_ior_disp.x,
4459        material.sheen_trans_ior_disp.y,
4460        material.sheen_trans_ior_disp.z,
4461        material.sheen_trans_ior_disp.w,
4462        material.thick_sub_film.x,
4463        material.thick_sub_film.y,
4464        material.tint_absorption.xyz,
4465        material.tint_absorption.w,
4466        material.thick_sub_film.z,
4467        material.thick_sub_film.w,
4468        material.emission_film.w,
4469        material.emission_film.xyz,
4470        material.age_b.w,
4471        normal,
4472    );
4473}
4474struct OpaqueOutput {
4475    @location(0) colour: vec4f,
4476    @location(1) normal_roughness: vec4f,
4477}
4478@fragment fn opaque_main(input: VertexOutput, @builtin(front_facing) front: bool) -> OpaqueOutput {
4479    return opaque_shade(input, front, false);
4480}
4481@fragment fn opaque_lit_main(input: VertexOutput, @builtin(front_facing) front: bool) -> OpaqueOutput {
4482    return opaque_shade(input, front, true);
4483}
4484fn opaque_shade(input: VertexOutput, front: bool, lit: bool) -> OpaqueOutput {
4485    let uv_dx = dpdx(input.uv_alpha.xy);
4486    let uv_dy = dpdy(input.uv_alpha.xy);
4487    let caustic_at = caustic_uv(input.world_position);
4488    let caustic_dx = dpdx(caustic_at);
4489    let caustic_dy = dpdy(caustic_at);
4490    map_footprint(input.object_position);
4491    cutout(input);
4492    let material = materials[input.material];
4493    var geometric = normalize(input.normal.xyz);
4494    if (!front) {
4495        geometric = -geometric;
4496    }
4497    let mapped = map_evaluate_with(material.base_roughness.xyz, material.base_roughness.w, material.metal_spec_coat.x, material.metal_spec_coat.w, material.thick_sub_film.z, geometric, input.tangent, input.uv_alpha.xy, vec2f(input.uv_alpha.w, input.normal.w), input.object_position, 0.0, input.settings.z, frame.time_size.x, input.material);
4498    var surface = shaded(material, mapped.normal);
4499    surface.base = mapped.base;
4500    surface.roughness = mapped.roughness;
4501    surface.metalness = mapped.metalness;
4502    surface.clearcoat_roughness = mapped.coat_roughness;
4503    surface.thin_film = mapped.thin_film;
4504    let placed = instances[input.instance];
4505    let face = placed.content_face.x;
4506    let shown = face == 0u || (face == 1u && front) || (face == 2u && !front);
4507    let content_crop = select(vec4f(2.0, 2.0, -1.0, -1.0), placed.content_crop, shown);
4508    let content_t = normalize(input.tangent.xyz - mapped.normal * dot(input.tangent.xyz, mapped.normal));
4509    let content_b = cross(mapped.normal, content_t) * input.tangent.w;
4510    surface = content_apply(surface, input.material, input.uv_alpha.xy, uv_dx, uv_dy, placed.content_uv, content_crop, content_t, content_b, geometric);
4511    let n = surface.normal;
4512    let t = normalize(input.tangent.xyz - n * dot(input.tangent.xyz, n));
4513    let b = cross(n, t) * input.tangent.w;
4514    let view = normalize(frame.camera_position.xyz - input.world_position);
4515    let light = normalize(frame.sun_direction.xyz);
4516    let wo = vec3f(dot(view, t), dot(view, b), dot(view, n));
4517    let wi = vec3f(dot(light, t), dot(light, b), dot(light, n));
4518    let facing = max(dot(n, light), 0.0);
4519    var sun = frame.sun_colour_intensity.xyz * frame.sun_colour_intensity.w * facing;
4520    if (facing > 0.0) {
4521        sun *= shadow_visibility(input.world_position, geometric);
4522        if (lit && any(sun > vec3f(0.0))) {
4523            sun *= shadow_tint(input.world_position, geometric, input.instance);
4524        }
4525        let shaded = all(sun == vec3f(0.0));
4526        if (use_canopy && frame.flags.z != 0u && !shaded) {
4527            sun *= canopy_light(input.world_position);
4528        }
4529        if (use_caustics && (placed.content_face.z & 2u) != 0u && !shaded) {
4530            sun *= caustic_gain(caustic_at, caustic_dx, caustic_dy);
4531        }
4532    }
4533    let contacted = use_contact && (placed.content_face.z & 1u) != 0u;
4534    var contact = vec3f(1.0);
4535    if (contacted) {
4536        contact = contact_factor(input.world_position);
4537        sun *= contact;
4538    }
4539    var film = vec3f(0.0);
4540    var film_normal = vec3f(0.0);
4541    if (filmed(surface)) {
4542        film = film_colour(surface, clamp(wo.z, 0.0, 1.0));
4543        film_normal = film_colour(surface, 1.0);
4544    }
4545    var direct = eval_filmed(surface, wo, wi, film) * sun;
4546    if (lit) {
4547        var local = local_light_radiance(surface, wo, t, b, n, input.world_position, geometric, film);
4548        if (contacted) {
4549            local *= contact;
4550        }
4551        direct += local;
4552    }
4553    let release = f32((placed.content_face.z >> 8u) & 255u) / 255.0;
4554    let sky_direction = reflect(-view, n);
4555    var seen = -1.0;
4556    let toward = select(vec3f(0.0), sky_direction, reflection_wants_probes(surface, release));
4557    let grid = sample_live_irradiance_toward(input.world_position, n, toward, &seen);
4558    var irradiance = surface_irradiance(grid, n, use_authored_ambient && placed.content_face.w != 0u);
4559    if (contacted) {
4560        irradiance *= contact;
4561    }
4562    let diffuse = surface.base * irradiance / PI * reflection_diffuse_weight(surface, view, n, film, film_normal);
4563    let previous_uv = input.previous_clip.xy / input.previous_clip.w * vec2f(0.5, -0.5) + vec2f(0.5);
4564    let view_normal = normalize((frame.view * vec4f(n, 0.0)).xyz);
4565    var ssr = vec4f(0.0);
4566    if (use_ssr) {
4567        ssr = ssr_resolve(ssr_history, ssr_guide, reflect_sampler, previous_uv, frame.time_size.yz, input.previous_clip.w, view_normal);
4568    }
4569    var lookup = LocalLookup(vec2f(0.0), sky_direction, sky_direction);
4570    if (reflection_reflective(surface)) {
4571        lookup = local_lookup(selected_reflections.first, selected_reflections.second, selected_reflections.weights_mip.xy, input.world_position, sky_direction, local_probes, reflect_sampler, selected_reflections.weights_mip.z);
4572    }
4573    let sky_visible = select(1.0, reflection_sky_visibility(seen, sky_direction, lookup, grid, release), any(toward != vec3f(0.0)));
4574    var specular = reflection_radiance(surface, view, n, ssr, sky_cube, local_probes, selected_reflections.first, selected_reflections.second, lookup, reflect_sampler, selected_reflections.weights_mip.z, selected_reflections.weights_mip.w, film, film_normal, sky_visible);
4575    if (probe_grid.shade.x > 0.0 && grid.x >= 0.0) {
4576        let occlusion = mix(probe_reflection_occlusion(grid, reflection_irradiance(n, input.world_position, sky_visible)), 1.0, release);
4577        if (occlusion < 1.0) {
4578            specular *= mix(occlusion, 1.0, clamp(ssr.a, 0.0, 1.0));
4579        }
4580    }
4581    var output: OpaqueOutput;
4582    output.colour = vec4f(max(direct + diffuse + specular + surface.emission, vec3f(0.0)), 1.0);
4583    output.normal_roughness = vec4f(view_normal, surface.roughness);
4584    return output;
4585}
4586"#;
4587
4588const CARD_SHADER: &str = r#"
4589struct FrameUniforms {
4590    view_projection: mat4x4f,
4591    motion_view_projection: mat4x4f,
4592    previous_view_projection: mat4x4f,
4593    view: mat4x4f,
4594    camera_position: vec4f,
4595    sun_direction: vec4f,
4596    sun_colour_intensity: vec4f,
4597    time_size: vec4f,
4598    flags: vec4u,
4599    wind: vec4f,
4600    wind_direction: vec4f,
4601    previous_wind_direction: vec4f,
4602}
4603struct SkyLight {
4604    sh: array<vec4f, 9>,
4605    info: vec4f,
4606}
4607struct CanopyParams {
4608    center_extent: vec4f,
4609    sun_receiver: vec4f,
4610    wind: vec4f,
4611    direction: vec4f,
4612    projected_extent: vec4f,
4613    mode_sharpness: vec4f,
4614}
4615struct Card {
4616    position_size: vec4f,
4617    right_size: vec4f,
4618    up: vec4f,
4619    pivot_level: vec4f,
4620    motion_color: vec4f,
4621    atlas: vec4f,
4622    lod: vec4f,
4623}
4624@group(0) @binding(0) var<uniform> frame: FrameUniforms;
4625@group(1) @binding(0) var<uniform> shadow_setting: ShadowSample;
4626@group(1) @binding(1) var shadow_map: texture_depth_2d_array;
4627@group(1) @binding(2) var shadow_sampler: sampler_comparison;
4628@group(2) @binding(0) var<uniform> params: CanopyParams;
4629@group(2) @binding(1) var<storage, read> cards: array<Card>;
4630@group(2) @binding(2) var alpha_atlas: texture_2d<f32>;
4631@group(2) @binding(3) var alpha_sampler: sampler;
4632@group(3) @binding(9) var canopy_cookie: texture_2d<f32>;
4633@group(3) @binding(10) var canopy_sampler: sampler;
4634@group(3) @binding(12) var<uniform> sky_light: SkyLight;
4635fn tree_sway(rest: vec3f, pivot: vec3f, level: f32, stiffness: f32, wind_time: f32, strength: f32, direction: vec3f) -> vec3f {
4636    let d = normalize(direction.xz + vec2f(0.00001, 0.0));
4637    let arm = length(rest - pivot);
4638    let phase = wind_time * (0.7 + level * 0.41) + pivot.x * 0.83 + pivot.z * 0.67;
4639    let bend = sin(phase) * strength * (1.0 - stiffness) * arm * (0.012 + 0.009 * level);
4640    return rest + vec3f(d.x * bend, -abs(bend) * 0.12, d.y * bend);
4641}
4642struct CardOut {
4643    @builtin(position) clip: vec4f,
4644    @location(0) uv: vec2f,
4645    @location(1) @interpolate(flat) atlas: vec4f,
4646    @location(2) @interpolate(flat) color: vec3f,
4647    @location(3) world: vec3f,
4648    @location(4) @interpolate(flat) normal: vec3f,
4649    @location(5) current_clip: vec4f,
4650    @location(6) previous_clip: vec4f,
4651    @location(7) @interpolate(flat) lod: vec4f,
4652}
4653@vertex fn card_vertex(@builtin(vertex_index) vertex: u32, @builtin(instance_index) instance: u32) -> CardOut {
4654    let card = cards[instance];
4655    let corners = array<vec2f, 6>(vec2f(-1.0, -1.0), vec2f(1.0, -1.0), vec2f(-1.0, 1.0), vec2f(-1.0, 1.0), vec2f(1.0, -1.0), vec2f(1.0, 1.0));
4656    let corner = corners[vertex];
4657    let rest = card.position_size.xyz + card.right_size.xyz * card.position_size.w * corner.x + card.up.xyz * card.right_size.w * corner.y;
4658    let origin = vec3f(params.center_extent.x, params.wind.z, params.center_extent.y);
4659    let moved = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, frame.wind.x, frame.wind.y, frame.wind_direction.xyz) + origin;
4660    let before = tree_sway(rest, card.pivot_level.xyz, card.pivot_level.w, card.motion_color.x, frame.wind.z, frame.wind.w, frame.previous_wind_direction.xyz) + origin;
4661    var out: CardOut;
4662    out.clip = frame.view_projection * vec4f(moved, 1.0);
4663    out.current_clip = frame.motion_view_projection * vec4f(moved, 1.0);
4664    out.previous_clip = frame.previous_view_projection * vec4f(before, 1.0);
4665    out.uv = (corner + vec2f(1.0)) * 0.5;
4666    out.atlas = card.atlas;
4667    out.color = card.motion_color.yzw;
4668    out.world = moved;
4669    out.normal = normalize(cross(card.right_size.xyz, card.up.xyz));
4670    out.lod = card.lod;
4671    return out;
4672}
4673fn card_texel(input: CardOut) -> vec4f {
4674    return textureSample(alpha_atlas, alpha_sampler, input.atlas.xy + input.uv * input.atlas.zw);
4675}
4676fn card_keep(input: CardOut) -> bool {
4677    if (input.lod.x >= 1.0) { return true; }
4678    let near = lod_keep(vec2u(input.clip.xy), u32(input.lod.y), input.lod.x);
4679    return near == (input.lod.z > 0.5);
4680}
4681struct PrepassOutput {
4682    @location(0) velocity: vec2f,
4683    @location(1) id: u32,
4684}
4685@fragment fn card_prepass(input: CardOut) -> PrepassOutput {
4686    if (card_texel(input).r < 0.5 || !card_keep(input)) {
4687        discard;
4688    }
4689    let current = input.current_clip.xy / input.current_clip.w;
4690    let previous = input.previous_clip.xy / input.previous_clip.w;
4691    var output: PrepassOutput;
4692    output.velocity = (previous - current) * vec2f(0.5, -0.5);
4693    output.id = frame.flags.w;
4694    return output;
4695}
4696fn card_sky(n: vec3f) -> vec3f {
4697    let d = normalize(n);
4698    let sh = sky_light.sh;
4699    let value = sh[0].xyz * 0.282095 + sh[1].xyz * (0.488603 * d.y) + sh[2].xyz * (0.488603 * d.z)
4700        + sh[3].xyz * (0.488603 * d.x) + sh[4].xyz * (1.092548 * d.x * d.y) + sh[5].xyz * (1.092548 * d.y * d.z)
4701        + sh[6].xyz * (0.315392 * (3.0 * d.z * d.z - 1.0)) + sh[7].xyz * (1.092548 * d.x * d.z) + sh[8].xyz * (0.546274 * (d.x * d.x - d.y * d.y));
4702    return max(value, vec3f(0.0));
4703}
4704struct OpaqueOutput {
4705    @location(0) colour: vec4f,
4706    @location(1) normal_roughness: vec4f,
4707}
4708@fragment fn card_opaque(input: CardOut, @builtin(front_facing) front: bool) -> OpaqueOutput {
4709    let texel = card_texel(input);
4710    if (texel.r < 0.5 || !card_keep(input)) {
4711        discard;
4712    }
4713    let n = select(-input.normal, input.normal, front);
4714    let light = normalize(frame.sun_direction.xyz);
4715    let facing = dot(n, light);
4716    let diffuse = max(facing, 0.0);
4717    let transmission = max(-facing, 0.0);
4718    var lit = 1.0;
4719    if (shadow_setting.texels.w < 0.5) {
4720        lit = shadow_sample(shadow_map, shadow_sampler, canopy_sampler, shadow_setting, input.world, n, length(input.world - frame.camera_position.xyz));
4721    }
4722    let projected = input.world.xz - light.xz / max(light.y, 0.05) * (input.world.y - params.sun_receiver.w);
4723    let cookie_uv = (projected - params.projected_extent.xy) / params.projected_extent.zw + vec2f(0.5);
4724    if (all(cookie_uv >= vec2f(0.0)) && all(cookie_uv <= vec2f(1.0))) {
4725        let occlusion = textureSample(canopy_cookie, canopy_sampler, cookie_uv).r;
4726        let depth = clamp((params.wind.w - input.world.y) / max(params.wind.w - params.wind.z, 0.01), 0.0, 1.0);
4727        lit *= 1.0 - occlusion * depth * 0.75;
4728    }
4729    let pigment = mix(input.color, vec3f(1.0, 0.44, 0.61) * length(input.color) * 0.58, texel.g * 0.7);
4730    let petal = mix(pigment, vec3f(1.0, 0.72, 0.38) * length(input.color) * 0.58, texel.b * 0.68);
4731    let sun = frame.sun_colour_intensity.xyz * frame.sun_colour_intensity.w * lit;
4732    let colour = petal * (card_sky(n) + sun * diffuse) * 0.31830988 + vec3f(1.0, 0.47, 0.64) * sun * transmission * 0.17;
4733    var output: OpaqueOutput;
4734    output.colour = vec4f(colour, 1.0);
4735    output.normal_roughness = vec4f(normalize((frame.view * vec4f(n, 0.0)).xyz), 0.8);
4736    return output;
4737}
4738"#;
4739
4740#[cfg(test)]
4741mod tests {
4742    use super::*;
4743    use crate::deform::{Deformer, RollerFrame};
4744    use crate::lod::{LodSet, MeshUploader};
4745    use pfx_geom::shapes::Shape;
4746    use std::f32::consts::PI;
4747
4748    fn identity() -> Matrix {
4749        [
4750            [1.0, 0.0, 0.0, 0.0],
4751            [0.0, 1.0, 0.0, 0.0],
4752            [0.0, 0.0, 1.0, 0.0],
4753            [0.0, 0.0, 0.0, 1.0],
4754        ]
4755    }
4756
4757    fn perspective(aspect: f32) -> Matrix {
4758        let near = 0.1;
4759        let far = 100.0;
4760        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
4761        [
4762            [f / aspect, 0.0, 0.0, 0.0],
4763            [0.0, f, 0.0, 0.0],
4764            [0.0, 0.0, far / (near - far), -1.0],
4765            [0.0, 0.0, far * near / (near - far), 0.0],
4766        ]
4767    }
4768
4769    fn look_down(height: f32) -> Matrix {
4770        [
4771            [1.0, 0.0, 0.0, 0.0],
4772            [0.0, 0.0, 1.0, 0.0],
4773            [0.0, -1.0, 0.0, 0.0],
4774            [0.0, 0.0, -height, 1.0],
4775        ]
4776    }
4777
4778    #[test]
4779    fn matrix_and_velocity() {
4780        let mut moved = identity();
4781        moved[3] = [2.0, -3.0, 4.0, 1.0];
4782        assert_eq!(
4783            transform(moved, [1.0, 2.0, 3.0, 1.0]),
4784            [3.0, -1.0, 7.0, 1.0]
4785        );
4786        assert_eq!(multiply(identity(), moved), moved);
4787        assert_eq!(multiply(moved, identity()), moved);
4788        let previous = [0.0, 0.0, 0.5, 1.0];
4789        let current = [0.5, -0.5, 0.5, 1.0];
4790        assert_eq!(velocity_uv(current, previous), [-0.25, -0.25]);
4791        assert_eq!(
4792            velocity_pixels(current, previous, [200.0, 100.0]),
4793            [-50.0, -25.0]
4794        );
4795        assert_eq!(
4796            velocity_pixels(previous, previous, [200.0, 100.0]),
4797            [0.0, 0.0]
4798        );
4799        let mut scaled = identity();
4800        scaled[0][0] = 2.0;
4801        scaled[1][1] = 4.0;
4802        let normals = normal_columns(scaled).unwrap();
4803        assert_eq!(normals[0][0], 0.5);
4804        assert_eq!(normals[1][1], 0.25);
4805        let shift = jitter_matrix([0.25, -0.5]);
4806        assert_eq!(
4807            transform(shift, [0.0, 0.0, 0.3, 1.0]),
4808            [0.25, -0.5, 0.3, 1.0]
4809        );
4810    }
4811
4812    #[test]
4813    fn gpu_layouts_and_shader_entry_points() {
4814        assert_eq!(std::mem::size_of::<Vertex>(), 64);
4815        assert_eq!(std::mem::size_of::<GpuInstance>(), 304);
4816        assert_eq!(std::mem::size_of::<Uniforms>(), 384);
4817        assert_eq!(std::mem::size_of::<SkyLight>(), 160);
4818        assert_eq!(std::mem::size_of::<GpuSway>(), 32);
4819        assert_eq!(std::mem::size_of::<PackedMaterial>(), PackedMaterial::SIZE);
4820        let material = Material {
4821            base: [0.2, 0.4, 0.6],
4822            roughness: 0.3,
4823            ..Default::default()
4824        };
4825        let packed = PackedMaterial::pack(&material);
4826        assert_eq!(packed.base_roughness, [0.2, 0.4, 0.6, 0.3]);
4827        assert_eq!(packed.metal_spec_coat[1], material.specular);
4828        let shader = shader_source();
4829        for entry in [
4830            "fn eval(",
4831            "@vertex fn vertex_main",
4832            "@vertex fn shadow_vertex",
4833            "@vertex fn shadow_clip_vertex",
4834            "@fragment fn shadow_clip_fragment",
4835            "@vertex fn shadow_cut_vertex",
4836            "@fragment fn shadow_cut_fragment",
4837            "fn content_cut(",
4838            "fn content_apply(",
4839            "fn content_composite(",
4840            "@fragment fn prepass_main",
4841            "@fragment fn opaque_main",
4842            "fn shadow_visibility",
4843            "fn map_evaluate(",
4844            "fn sample_live_irradiance(",
4845            "fn reflection_radiance(",
4846            "fn canopy_light(",
4847            "fn deform_vertex(",
4848            "fn tree_sway(",
4849            "fn lod_keep(",
4850            "fn local_light_radiance(",
4851            "fn shadow_tint(",
4852            "@vertex fn transmit_vertex",
4853            "@fragment fn transmit_fragment",
4854        ] {
4855            assert!(shader.contains(entry), "{entry}");
4856        }
4857    }
4858
4859    fn binding_kind(module: &naga::Module, variable: &naga::GlobalVariable) -> String {
4860        match (&variable.space, &module.types[variable.ty].inner) {
4861            (naga::AddressSpace::Uniform, _) => "uniform".into(),
4862            (naga::AddressSpace::Storage { access }, _) => {
4863                if access.contains(naga::StorageAccess::STORE) {
4864                    "storage".into()
4865                } else {
4866                    "read storage".into()
4867                }
4868            }
4869            (_, naga::TypeInner::Sampler { comparison }) => {
4870                if *comparison {
4871                    "comparison sampler".into()
4872                } else {
4873                    "sampler".into()
4874                }
4875            }
4876            (
4877                _,
4878                naga::TypeInner::Image {
4879                    dim,
4880                    arrayed,
4881                    class,
4882                },
4883            ) => {
4884                let class = match class {
4885                    naga::ImageClass::Depth { .. } => "depth",
4886                    naga::ImageClass::Sampled {
4887                        kind: naga::ScalarKind::Uint,
4888                        ..
4889                    } => "uint",
4890                    naga::ImageClass::Sampled { .. } => "float",
4891                    _ => "storage",
4892                };
4893                format!("{class} {dim:?} {arrayed}")
4894            }
4895            other => format!("{other:?}"),
4896        }
4897    }
4898
4899    fn entry_kind(entry: &wgpu::BindGroupLayoutEntry) -> String {
4900        match entry.ty {
4901            wgpu::BindingType::Buffer {
4902                ty: wgpu::BufferBindingType::Uniform,
4903                ..
4904            } => "uniform".into(),
4905            wgpu::BindingType::Buffer {
4906                ty: wgpu::BufferBindingType::Storage { read_only },
4907                ..
4908            } => if read_only { "read storage" } else { "storage" }.into(),
4909            wgpu::BindingType::Sampler(wgpu::SamplerBindingType::Comparison) => {
4910                "comparison sampler".into()
4911            }
4912            wgpu::BindingType::Sampler(_) => "sampler".into(),
4913            wgpu::BindingType::Texture {
4914                sample_type,
4915                view_dimension,
4916                ..
4917            } => {
4918                let class = match sample_type {
4919                    wgpu::TextureSampleType::Depth => "depth",
4920                    wgpu::TextureSampleType::Uint => "uint",
4921                    _ => "float",
4922                };
4923                let (dim, arrayed) = match view_dimension {
4924                    wgpu::TextureViewDimension::D2 => ("D2", false),
4925                    wgpu::TextureViewDimension::D2Array => ("D2", true),
4926                    wgpu::TextureViewDimension::Cube => ("Cube", false),
4927                    wgpu::TextureViewDimension::CubeArray => ("Cube", true),
4928                    wgpu::TextureViewDimension::D3 => ("D3", false),
4929                    wgpu::TextureViewDimension::D1 => ("D1", false),
4930                };
4931                format!("{class} {dim} {arrayed}")
4932            }
4933            _ => "other".into(),
4934        }
4935    }
4936
4937    pub fn check_pipeline(
4938        source: &str,
4939        entries: &[&str],
4940        layouts: &[Vec<wgpu::BindGroupLayoutEntry>],
4941    ) -> usize {
4942        let module = naga::front::wgsl::parse_str(source).unwrap();
4943        let info = naga::valid::Validator::new(
4944            naga::valid::ValidationFlags::all(),
4945            naga::valid::Capabilities::all(),
4946        )
4947        .validate(&module)
4948        .unwrap();
4949        let mut checked = 0;
4950        for name in entries {
4951            let index = module
4952                .entry_points
4953                .iter()
4954                .position(|entry| entry.name == *name)
4955                .unwrap_or_else(|| panic!("missing entry {name}"));
4956            let stage = match module.entry_points[index].stage {
4957                naga::ShaderStage::Vertex => wgpu::ShaderStages::VERTEX,
4958                naga::ShaderStage::Fragment => wgpu::ShaderStages::FRAGMENT,
4959                _ => wgpu::ShaderStages::COMPUTE,
4960            };
4961            let function = info.get_entry_point(index);
4962            for (handle, variable) in module.global_variables.iter() {
4963                let Some(binding) = &variable.binding else {
4964                    continue;
4965                };
4966                if function[handle].is_empty() {
4967                    continue;
4968                }
4969                let layout = layouts
4970                    .get(binding.group as usize)
4971                    .unwrap_or_else(|| panic!("{name} uses group {}", binding.group));
4972                let entry = layout
4973                    .iter()
4974                    .find(|entry| entry.binding == binding.binding)
4975                    .unwrap_or_else(|| {
4976                        panic!(
4977                            "{name} uses @group({}) @binding({}) missing from the layout",
4978                            binding.group, binding.binding
4979                        )
4980                    });
4981                assert_eq!(
4982                    binding_kind(&module, variable),
4983                    entry_kind(entry),
4984                    "{name} @group({}) @binding({})",
4985                    binding.group,
4986                    binding.binding
4987                );
4988                assert!(
4989                    entry.visibility.contains(stage),
4990                    "{name} @group({}) @binding({}) is not visible to its stage",
4991                    binding.group,
4992                    binding.binding
4993                );
4994                checked += 1;
4995            }
4996        }
4997        checked
4998    }
4999
5000    #[test]
5001    fn bind_layout_validates_against_every_pipeline() {
5002        let frame_layouts = [
5003            scene_entries(),
5004            shadow_entries(),
5005            deformer_entries(),
5006            lighting_entries(),
5007        ];
5008        let checked = check_pipeline(
5009            &shader_source(),
5010            &[
5011                "vertex_main",
5012                "prepass_main",
5013                "opaque_main",
5014                "opaque_lit_main",
5015            ],
5016            &frame_layouts,
5017        );
5018        assert!(checked >= 20, "{checked} frame bindings checked");
5019        check_pipeline(
5020            &shader_source(),
5021            &[
5022                "shadow_vertex",
5023                "shadow_clip_vertex",
5024                "shadow_clip_fragment",
5025                "transmit_vertex",
5026                "transmit_fragment",
5027            ],
5028            &[scene_entries(), cascade_entries(), deformer_entries()],
5029        );
5030        check_pipeline(
5031            &shader_source(),
5032            &["shadow_cut_vertex", "shadow_cut_fragment"],
5033            &[
5034                scene_entries(),
5035                cascade_entries(),
5036                deformer_entries(),
5037                lighting_entries(),
5038            ],
5039        );
5040        check_pipeline(
5041            &card_source(),
5042            &["card_vertex", "card_prepass", "card_opaque"],
5043            &[
5044                scene_entries(),
5045                shadow_entries(),
5046                card_entries(),
5047                lighting_entries(),
5048            ],
5049        );
5050        let maps = format!(
5051            "{}\n{}",
5052            maps::shader_source(),
5053            "@fragment fn probe() -> @location(0) vec4f { let r = map_evaluate(vec3f(1.0), 0.5, 0.0, 0.1, 0.0, vec3f(0.0, 1.0, 0.0), vec4f(1.0, 0.0, 0.0, 1.0), vec2f(0.5), vec3f(0.0), 0.0, 0.5, 0.0, 0u); return vec4f(r.base, 1.0); }"
5054        );
5055        check_pipeline(
5056            &maps,
5057            &["probe"],
5058            &[vec![], vec![], vec![], lighting_entries()],
5059        );
5060        let probes = format!(
5061            "{PROBE_WGSL}\n@fragment fn probe() -> @location(0) vec4f {{ return vec4f(sample_live_irradiance(vec3f(0.0), vec3f(0.0, 1.0, 0.0)), 1.0); }}"
5062        );
5063        check_pipeline(
5064            &probes,
5065            &["probe"],
5066            &[vec![], vec![], vec![], lighting_entries()],
5067        );
5068        let canopy = format!(
5069            "{}\n@fragment fn probe() -> @location(0) vec4f {{ return vec4f(canopy_light(vec3f(0.0))); }}",
5070            canopy_light_wgsl(&Gobo::default())
5071        );
5072        check_pipeline(
5073            &canopy,
5074            &["probe"],
5075            &[vec![], vec![], vec![], lighting_entries()],
5076        );
5077    }
5078
5079    struct Counter(u32);
5080
5081    impl MeshUploader for Counter {
5082        fn upload_mesh(&mut self, _data: MeshData<'_>) -> Result<MeshHandle, String> {
5083            self.0 += 1;
5084            Ok(MeshHandle(self.0 + 100))
5085        }
5086    }
5087
5088    fn lod_camera(z: f32) -> Camera {
5089        let mut projection = identity();
5090        projection[1][1] = 1.0 / (0.75_f32 * 0.5).tan();
5091        Camera {
5092            view: identity(),
5093            projection,
5094            previous_view_projection: identity(),
5095            position: [0.0, 0.0, z],
5096        }
5097    }
5098
5099    #[test]
5100    fn lod_output_feeds_the_draws() {
5101        let slab = Shape::Slab {
5102            half_length: 2.8,
5103            radius: 1.8,
5104            height: 0.16,
5105            fillet: 0.12,
5106        };
5107        let mut set = LodSet::new(2_000_000);
5108        let near = Instance::new(MeshHandle(0), identity(), 0, 1);
5109        let mut far_model = identity();
5110        far_model[3][2] = -30.0;
5111        let far = Instance::new(MeshHandle(0), far_model, 0, 2);
5112        set.add_shape(near, slab);
5113        set.add_shape(far, slab);
5114        let mut uploader = Counter(0);
5115        set.select(lod_camera(5.0), 2160);
5116        let close = set.upload_selected(&mut uploader).unwrap();
5117        let drawn = batches(&close, |_| false, |_| false);
5118        assert_eq!(drawn.len(), 2);
5119        assert_eq!(drawn[0].mesh, close[0].mesh);
5120        assert_eq!(drawn[1].mesh, close[1].mesh);
5121        assert_ne!(drawn[0].mesh, MeshHandle(0));
5122        assert_ne!(drawn[0].mesh, drawn[1].mesh);
5123        assert_eq!(drawn[0].range, 0..1);
5124        set.select(lod_camera(-25.0), 2160);
5125        let moved = set.upload_selected(&mut uploader).unwrap();
5126        let redrawn = batches(&moved, |_| false, |_| false);
5127        assert_ne!(redrawn[0].mesh, drawn[0].mesh);
5128        assert_ne!(redrawn[1].mesh, drawn[1].mesh);
5129        assert!(redrawn.iter().all(|batch| batch.mesh.0 > 100));
5130    }
5131
5132    #[test]
5133    fn batches_split_on_mesh_sidedness_and_motion() {
5134        let mut instances = vec![Instance::new(MeshHandle(1), identity(), 0, 1); 3];
5135        instances[1].deformer = DeformerId(0);
5136        instances.push(Instance::new(MeshHandle(2), identity(), 0, 4));
5137        let drawn = batches(&instances, |mesh| mesh == MeshHandle(2), |_| false);
5138        assert_eq!(drawn.len(), 4);
5139        assert!(!drawn[0].two_sided && !drawn[0].moving);
5140        assert!(drawn[1].two_sided && drawn[1].moving);
5141        assert_eq!(drawn[2].range, 2..3);
5142        assert!(drawn[3].moving && !drawn[3].two_sided);
5143        let mut surfaces = vec![InstanceSurface::default(); instances.len()];
5144        let key = static_key(&instances, &drawn, &surfaces);
5145        instances[3].model[3][0] = 4.0;
5146        assert_eq!(static_key(&instances, &drawn, &surfaces), key);
5147        instances[0].model[3][0] = 4.0;
5148        let moved = static_key(&instances, &drawn, &surfaces);
5149        assert_ne!(moved, key);
5150        surfaces[2].clip[0] = [0.0, 1.0, 0.0, 0.1];
5151        let cut = batches(
5152            &instances,
5153            |mesh| mesh == MeshHandle(2),
5154            |i| surfaces[i].clipped(),
5155        );
5156        assert_eq!(cut.len(), 4);
5157        assert!(cut[2].clipped && !cut[0].clipped);
5158        assert_ne!(static_key(&instances, &cut, &surfaces), moved);
5159        let unclipped = vec![Instance::new(MeshHandle(1), identity(), 0, 1); 3];
5160        let split = batches(&unclipped, |_| false, |i| i == 1);
5161        assert_eq!(split.len(), 3);
5162        assert_eq!(split[1].range, 1..2);
5163    }
5164
5165    #[test]
5166    fn instances_that_cast_no_shadow_stay_out_of_every_caster_list() {
5167        let mut instances = vec![
5168            Instance::new(MeshHandle(1), identity(), 0, 1),
5169            Instance::new(MeshHandle(1), identity(), 1, 2),
5170            Instance::new(MeshHandle(1), identity(), 1, 3),
5171        ];
5172        let surfaces = vec![InstanceSurface::default(); instances.len()];
5173        let all = batches(&instances, |_| false, |_| false);
5174        assert_eq!(all.len(), 1);
5175        assert!(all[0].casts_shadow);
5176        assert_eq!(shadow_batches(&all).len(), 1);
5177        let key = static_key(&instances, &all, &surfaces);
5178        instances[1].casts_shadow = false;
5179        let split = batches(&instances, |_| false, |_| false);
5180        assert_eq!(split.len(), 3);
5181        assert_eq!(
5182            split
5183                .iter()
5184                .map(|batch| batch.casts_shadow)
5185                .collect::<Vec<_>>(),
5186            [true, false, true]
5187        );
5188        let casters = shadow_batches(&split);
5189        assert_eq!(casters.len(), 2);
5190        assert!(casters.iter().all(|batch| batch.range != (1..2)));
5191        let transmitting = transmission_batches(&split, &instances, |material| material == 1);
5192        assert_eq!(transmitting.len(), 1);
5193        assert_eq!(transmitting[0].range, 2..3);
5194        assert_ne!(static_key(&instances, &split, &surfaces), key);
5195        let before = static_key(&instances, &split, &surfaces);
5196        instances[1].model[3][0] = 9.0;
5197        let moved = batches(&instances, |_| false, |_| false);
5198        assert_eq!(static_key(&instances, &moved, &surfaces), before);
5199        instances[1].previous_model = identity();
5200        let restless = batches(&instances, |_| false, |_| false);
5201        assert!(restless[1].moving);
5202        assert!(shadow_batches(&restless).iter().all(|batch| !batch.moving));
5203        assert_eq!(static_key(&instances, &restless, &surfaces), before);
5204        instances[1].previous_model = instances[1].model;
5205        instances[1].casts_shadow = true;
5206        let back = batches(&instances, |_| false, |_| false);
5207        assert_ne!(static_key(&instances, &back, &surfaces), before);
5208    }
5209
5210    #[test]
5211    fn instance_surfaces_clip_and_place_content() {
5212        let plain = InstanceSurface::default();
5213        assert!(!plain.clipped());
5214        assert!(!plain.cuts([5.0, 5.0, 5.0]));
5215        assert_eq!(plain.content_uv([0.3, 0.7]), Some([0.3, 0.7]));
5216        let strip = InstanceSurface {
5217            clip: [[0.0, 1.0, 0.0, 0.1449], [0.0, -1.0, 0.0, 0.0916]],
5218            ..Default::default()
5219        };
5220        assert!(strip.clipped());
5221        assert!(strip.cuts([0.0, 0.2, 0.0]));
5222        assert!(strip.cuts([0.0, -0.1, 0.0]));
5223        assert!(!strip.cuts([0.0, 0.0, 0.0]));
5224        let tape = InstanceSurface {
5225            uv_offset: [0.0, 0.25],
5226            crop: [0.0, 0.0, 1.0, 2000.0 / 2048.0],
5227            ..Default::default()
5228        };
5229        assert_eq!(tape.content_uv([0.5, 0.5]), Some([0.5, 0.75]));
5230        assert_eq!(tape.content_uv([0.5, 0.9]), None);
5231        let broken = InstanceSurface {
5232            crop: [0.0, f32::NAN, 1.0, 1.0],
5233            ..Default::default()
5234        };
5235        assert!(!broken.valid());
5236    }
5237
5238    #[test]
5239    fn an_instance_takes_all_of_the_reflection_occlusion_unless_it_says_otherwise() {
5240        let plain = InstanceSurface::default();
5241        assert_eq!(plain.reflection_occlusion, 1.0);
5242        assert!(plain.valid());
5243        assert_eq!(plain.occlusion_release(), 0);
5244        let release = |scale: f32| {
5245            InstanceSurface {
5246                reflection_occlusion: scale,
5247                ..Default::default()
5248            }
5249            .occlusion_release()
5250        };
5251        assert_eq!(release(0.0), 255);
5252        assert_eq!(release(0.5), 128);
5253        assert_eq!(release(0.25), 191);
5254        for scale in [-0.1, 1.1, f32::NAN, f32::INFINITY] {
5255            let surface = InstanceSurface {
5256                reflection_occlusion: scale,
5257                ..Default::default()
5258            };
5259            assert!(!surface.valid(), "{scale}");
5260        }
5261    }
5262
5263    #[test]
5264    fn ambient_prefers_the_grid_then_the_sky() {
5265        let mut sh = [[0.0; 3]; 9];
5266        sh[0] = [1.0, 2.0, 3.0];
5267        sh[1] = [0.5, 0.5, 0.5];
5268        let up = sky_irradiance(&sh, [0.0, 1.0, 0.0]);
5269        let down = sky_irradiance(&sh, [0.0, -1.0, 0.0]);
5270        assert!(up[0] > down[0]);
5271        assert_eq!(ambient_irradiance(None, &sh, [0.0, 1.0, 0.0]), up);
5272        assert_eq!(
5273            ambient_irradiance(Some([-1.0; 3]), &sh, [0.0, 1.0, 0.0]),
5274            up
5275        );
5276        assert_eq!(
5277            ambient_irradiance(Some([0.2, 0.3, 0.4]), &sh, [0.0, 1.0, 0.0]),
5278            [0.2, 0.3, 0.4]
5279        );
5280        let lighting = crate::sky::SkyLighting::new(default_sky(), 4);
5281        let mut settled = lighting.clone();
5282        while !settled.complete() {
5283            settled.step();
5284        }
5285        for normal in [[0.0, 1.0, 0.0], [0.6, 0.0, 0.8], [0.0, -1.0, 0.0]] {
5286            let reference = settled.irradiance(normal);
5287            let ours = sky_irradiance(&settled.sh, normal);
5288            for channel in 0..3 {
5289                assert!((reference[channel] - ours[channel]).abs() < 1e-5);
5290            }
5291        }
5292    }
5293
5294    fn sphere(frame: &mut Frame) -> MeshHandle {
5295        let mut positions = Vec::new();
5296        let mut normals = Vec::new();
5297        let mut tangents = Vec::new();
5298        let mut uvs = Vec::new();
5299        let mut indices = Vec::new();
5300        let rings = 20;
5301        let sides = 32;
5302        for ring in 0..=rings {
5303            let phi = std::f32::consts::PI * ring as f32 / rings as f32;
5304            for side in 0..=sides {
5305                let theta = std::f32::consts::TAU * side as f32 / sides as f32;
5306                let normal = [phi.sin() * theta.cos(), phi.cos(), phi.sin() * theta.sin()];
5307                positions.push([normal[0] * 0.75, normal[1] * 0.75, normal[2] * 0.75]);
5308                normals.push(normal);
5309                tangents.push([-theta.sin(), 0.0, theta.cos(), 1.0]);
5310                uvs.push([side as f32 / sides as f32, ring as f32 / rings as f32]);
5311            }
5312        }
5313        for ring in 0..rings {
5314            for side in 0..sides {
5315                let a = ring * (sides + 1) + side;
5316                let b = a + sides + 1;
5317                indices.extend_from_slice(&[a, a + 1, b, a + 1, b + 1, b]);
5318            }
5319        }
5320        frame
5321            .upload_mesh(MeshData {
5322                positions: &positions,
5323                normals: &normals,
5324                tangents: &tangents,
5325                uvs: &uvs,
5326                uvs1: None,
5327                alpha: None,
5328                indices: &indices,
5329            })
5330            .unwrap()
5331    }
5332
5333    fn quad(frame: &mut Frame, corners: [[f32; 3]; 4]) -> MeshHandle {
5334        frame
5335            .upload_mesh(MeshData {
5336                positions: &corners,
5337                normals: &[[0.0, 1.0, 0.0]; 4],
5338                tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5339                uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5340                uvs1: None,
5341                alpha: None,
5342                indices: &[0, 2, 1, 1, 2, 3],
5343            })
5344            .unwrap()
5345    }
5346
5347    fn plane(frame: &mut Frame) -> MeshHandle {
5348        quad(
5349            frame,
5350            [
5351                [-3.0, -1.0, -1.0],
5352                [3.0, -1.0, -1.0],
5353                [-3.0, -1.0, -9.0],
5354                [3.0, -1.0, -9.0],
5355            ],
5356        )
5357    }
5358
5359    fn half(value: u16) -> f32 {
5360        half::f16::from_bits(value).to_f32()
5361    }
5362
5363    fn calm_scene<'a>(
5364        camera: Camera,
5365        instances: &'a [Instance],
5366        materials: &'a [Material],
5367        sun: Sun,
5368    ) -> Scene<'a> {
5369        Scene {
5370            camera,
5371            time: 0.0,
5372            seed: 7,
5373            sun,
5374            instances,
5375            materials,
5376            deformers: &[],
5377            wind: SceneWind::default(),
5378        }
5379    }
5380
5381    #[test]
5382    #[ignore = "needs a GPU; run under pgpu"]
5383    fn warm_opaque_compiles_a_pipeline_on_the_frame_before_any_draw() {
5384        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5385        let mut renderer = crate::renderer::Renderer::new(gpu, 32, 32).unwrap();
5386        assert!(renderer.frame().pipelines.opaque.is_empty());
5387        renderer.warm_opaque(OpaqueFeatures::NONE, false);
5388        assert_eq!(renderer.frame().pipelines.opaque.len(), 2);
5389        for key in OpaqueKey::both_sides(OpaqueFeatures::NONE, false) {
5390            assert!(renderer.frame().pipelines.opaque.contains_key(&key));
5391        }
5392        renderer.warm_opaque(OpaqueFeatures::NONE, false);
5393        assert_eq!(renderer.frame().pipelines.opaque.len(), 2);
5394        renderer.warm_opaque(OpaqueFeatures::NONE, true);
5395        assert_eq!(renderer.frame().pipelines.opaque.len(), 4);
5396    }
5397
5398    #[test]
5399    #[ignore = "needs a GPU; run under pgpu"]
5400    fn a_shadow_only_scene_compiles_no_opaque_pipeline() {
5401        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5402        let mut frame = Frame::new(gpu, 32, 32).unwrap();
5403        let mesh = quad(
5404            &mut frame,
5405            [
5406                [-1.0, 0.0, -2.0],
5407                [1.0, 0.0, -2.0],
5408                [-1.0, 0.0, -4.0],
5409                [1.0, 0.0, -4.0],
5410            ],
5411        );
5412        let sun = Sun {
5413            direction: [0.2, 0.9, 0.1],
5414            colour: [1.0; 3],
5415            intensity: 1.0,
5416        };
5417        let materials = [Material::default()];
5418        let mut hidden = Instance::new(mesh, identity(), 0, 1);
5419        hidden.shadow_only = true;
5420        let mut shifted = identity();
5421        shifted[3][0] = 0.4;
5422        let mut other = Instance::new(mesh, shifted, 0, 2);
5423        other.shadow_only = true;
5424        let draw = |frame: &mut Frame, instances: &[Instance]| {
5425            let camera = Camera {
5426                view: look_down(4.0),
5427                projection: perspective(1.0),
5428                previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5429                position: [0.0, 4.0, 0.0],
5430            };
5431            let scene = calm_scene(camera, instances, &materials, sun);
5432            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5433            frame.encode(&scene, &mut encoder, None, None).unwrap();
5434            frame.gpu.queue.submit(Some(encoder.finish()));
5435        };
5436        assert!(frame.pipelines.opaque.is_empty());
5437        draw(&mut frame, &[hidden, other]);
5438        assert!(frame.pipelines.opaque.is_empty());
5439        assert!(frame.opaque_drawn().is_empty());
5440        draw(&mut frame, &[Instance::new(mesh, identity(), 0, 3)]);
5441        assert_eq!(frame.pipelines.opaque.len(), 1);
5442    }
5443
5444    #[test]
5445    #[ignore = "needs a GPU; run under pgpu"]
5446    fn shadow_only_cube_casts_without_colour_or_id() {
5447        use crate::shadow::{Quality, View};
5448        use pfx_geom::shapes::Shape;
5449
5450        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5451        let mut frame = Frame::new(gpu, 128, 128).unwrap();
5452        let floor = frame
5453            .upload_mesh(MeshData {
5454                positions: &[
5455                    [-3.0, -1.0, -1.0],
5456                    [3.0, -1.0, -1.0],
5457                    [-3.0, -1.0, -9.0],
5458                    [3.0, -1.0, -9.0],
5459                ],
5460                normals: &[[0.0, -1.0, 0.0]; 4],
5461                tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5462                uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5463                uvs1: None,
5464                alpha: None,
5465                indices: &[0, 2, 1, 1, 2, 3],
5466            })
5467            .unwrap();
5468        let cube_mesh = Shape::RoundBox {
5469            half: [0.35; 3],
5470            radius: 0.01,
5471        }
5472        .mesh(0.05);
5473        let cube = frame
5474            .upload_mesh(MeshData {
5475                positions: &cube_mesh.positions,
5476                normals: &cube_mesh.normals,
5477                tangents: &cube_mesh.tangents,
5478                uvs: &cube_mesh.uvs,
5479                uvs1: None,
5480                alpha: None,
5481                indices: &cube_mesh.indices,
5482            })
5483            .unwrap();
5484        let mut shadows = Shadows::new(
5485            &frame.gpu.device,
5486            Quality {
5487                resolution: 512,
5488                ..Default::default()
5489            },
5490        );
5491        let view = View {
5492            eye: [0.0, 4.0, 0.0],
5493            forward: [0.0, -1.0, 0.0],
5494            up: [0.0, 0.0, -1.0],
5495            fov_y: 0.9,
5496            aspect: 1.0,
5497            near: 0.1,
5498            far: 12.0,
5499        };
5500        let fit = shadows.fit(&view, [0.4, 0.9, 0.1]);
5501        let camera = Camera {
5502            view: look_down(4.0),
5503            projection: perspective(1.0),
5504            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5505            position: [0.0, 4.0, 0.0],
5506        };
5507        let sun = Sun {
5508            direction: [0.4, 0.9, 0.1],
5509            colour: [1.0; 3],
5510            intensity: 2.0,
5511        };
5512        let materials = [Material::default()];
5513        let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5514        floor_instance.two_sided = true;
5515        let mut cube_model = identity();
5516        cube_model[3][2] = -3.0;
5517        let mut cube_instance = Instance::new(cube, cube_model, 0, 2);
5518        cube_instance.shadow_only = true;
5519        let mut capture = |instances: &[Instance]| {
5520            let scene = calm_scene(camera, instances, &materials, sun);
5521            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5522            frame
5523                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5524                .unwrap();
5525            frame.gpu.queue.submit(Some(encoder.finish()));
5526            (
5527                read_target(&frame, &frame.targets.ids, 4),
5528                frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap(),
5529                read_atlas(&frame, &shadows),
5530            )
5531        };
5532        let empty = capture(&[]);
5533        let hidden = capture(&[cube_instance]);
5534        assert_eq!(empty.0, hidden.0);
5535        assert_eq!(empty.1, hidden.1);
5536        let baseline = capture(&[floor_instance]);
5537        let shadowed = capture(&[floor_instance, cube_instance]);
5538        let ids = shadowed
5539            .0
5540            .chunks_exact(4)
5541            .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()));
5542        assert!(ids.into_iter().all(|id| id != 2));
5543        assert!(baseline.2.iter().zip(&shadowed.2).any(|(a, b)| a != b));
5544    }
5545
5546    #[test]
5547    #[ignore = "needs a GPU; run under pgpu"]
5548    fn a_sphere_that_casts_no_shadow_leaves_the_floor_as_if_it_were_gone() {
5549        use crate::shadow::{Quality, View};
5550
5551        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5552        let mut frame = Frame::new(gpu, 128, 128).unwrap();
5553        let floor = quad(
5554            &mut frame,
5555            [
5556                [-3.0, -1.0, -3.0],
5557                [3.0, -1.0, -3.0],
5558                [-3.0, -1.0, 3.0],
5559                [3.0, -1.0, 3.0],
5560            ],
5561        );
5562        let ball = sphere(&mut frame);
5563        let mut shadows = Shadows::new(
5564            &frame.gpu.device,
5565            Quality {
5566                resolution: 512,
5567                ..Default::default()
5568            },
5569        );
5570        let view = View {
5571            eye: [0.0, 4.0, 0.0],
5572            forward: [0.0, -1.0, 0.0],
5573            up: [0.0, 0.0, -1.0],
5574            fov_y: 0.9,
5575            aspect: 1.0,
5576            near: 0.1,
5577            far: 12.0,
5578        };
5579        let fit = shadows.fit(&view, [0.4, 0.9, 0.1]);
5580        let camera = Camera {
5581            view: look_down(4.0),
5582            projection: perspective(1.0),
5583            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5584            position: [0.0, 4.0, 0.0],
5585        };
5586        let sun = Sun {
5587            direction: [0.4, 0.9, 0.1],
5588            colour: [1.0; 3],
5589            intensity: 2.0,
5590        };
5591        let materials = [Material::default()];
5592        let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5593        floor_instance.two_sided = true;
5594        let ball_model = identity();
5595        let casting = Instance::new(ball, ball_model, 0, 2);
5596        let mut lit_only = casting;
5597        lit_only.casts_shadow = false;
5598        let mut capture = |instances: &[Instance]| {
5599            let scene = calm_scene(camera, instances, &materials, sun);
5600            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5601            frame
5602                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5603                .unwrap();
5604            frame.gpu.queue.submit(Some(encoder.finish()));
5605            (
5606                read_target(&frame, &frame.targets.ids, 4),
5607                frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap(),
5608                read_atlas(&frame, &shadows),
5609            )
5610        };
5611        let removed = capture(&[floor_instance]);
5612        let unshadowed = capture(&[floor_instance, lit_only]);
5613        let shadowed = capture(&[floor_instance, casting]);
5614        let mut nothing = lit_only;
5615        nothing.shadow_only = true;
5616        let gone = capture(&[floor_instance, nothing]);
5617        assert!(gone.0 == removed.0 && gone.1 == removed.1 && gone.2 == removed.2);
5618        let sphere_pixels = |ids: &[u8]| -> Vec<bool> {
5619            ids.chunks_exact(4)
5620                .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()) == 2)
5621                .collect()
5622        };
5623        let covered = sphere_pixels(&unshadowed.0);
5624        assert!(covered.iter().filter(|hit| **hit).count() > 200);
5625        assert_eq!(covered, sphere_pixels(&shadowed.0));
5626        assert!(removed.2 == unshadowed.2);
5627        assert!(removed.2.iter().zip(&shadowed.2).any(|(a, b)| a != b));
5628        let mut worst = 0.0f32;
5629        let mut shadow_depth = 0.0f32;
5630        for (pixel, hit) in covered.iter().enumerate() {
5631            if *hit {
5632                continue;
5633            }
5634            for channel in 0..3 {
5635                let at = pixel * 4 + channel;
5636                worst = worst.max((half(removed.1[at]) - half(unshadowed.1[at])).abs());
5637                shadow_depth = shadow_depth.max((half(removed.1[at]) - half(shadowed.1[at])).abs());
5638            }
5639        }
5640        assert!(worst <= 1.0 / 255.0, "floor moved by {worst}");
5641        assert!(shadow_depth > 0.05, "control shadow only {shadow_depth}");
5642    }
5643
5644    #[test]
5645    #[ignore = "needs a GPU; run under pgpu"]
5646    fn a_caustic_lights_its_receiver_only_where_the_sun_reaches() {
5647        use crate::shadow::{Quality, View};
5648        use pfx_geom::shapes::Shape;
5649
5650        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5651        let mut frame = Frame::new(gpu, 128, 128).unwrap();
5652        let floor = frame
5653            .upload_mesh(MeshData {
5654                positions: &[
5655                    [-3.0, -1.0, -3.0],
5656                    [3.0, -1.0, -3.0],
5657                    [-3.0, -1.0, 3.0],
5658                    [3.0, -1.0, 3.0],
5659                ],
5660                normals: &[[0.0, 1.0, 0.0]; 4],
5661                tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5662                uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5663                uvs1: None,
5664                alpha: None,
5665                indices: &[0, 2, 1, 1, 2, 3],
5666            })
5667            .unwrap();
5668        let cube_mesh = Shape::RoundBox {
5669            half: [0.35; 3],
5670            radius: 0.01,
5671        }
5672        .mesh(0.05);
5673        let cube = frame
5674            .upload_mesh(MeshData {
5675                positions: &cube_mesh.positions,
5676                normals: &cube_mesh.normals,
5677                tangents: &cube_mesh.tangents,
5678                uvs: &cube_mesh.uvs,
5679                uvs1: None,
5680                alpha: None,
5681                indices: &cube_mesh.indices,
5682            })
5683            .unwrap();
5684        frame.set_content(1, 4, 4, ContentFormat::Linear16).unwrap();
5685        let texel: Vec<u8> = [1.0f32, 0.5, 0.0, 1.0]
5686            .iter()
5687            .flat_map(|v| half::f16::from_f32(*v).to_bits().to_le_bytes())
5688            .collect();
5689        frame
5690            .write_content(1, [0, 0, 4, 4], &texel.repeat(16))
5691            .unwrap();
5692        let mut shadows = Shadows::new(
5693            &frame.gpu.device,
5694            Quality {
5695                resolution: 512,
5696                ..Default::default()
5697            },
5698        );
5699        let toward = [0.1, 1.0, 0.05];
5700        let fit = shadows.fit(
5701            &View {
5702                eye: [0.0, 4.0, 0.0],
5703                forward: [0.0, -1.0, 0.0],
5704                up: [0.0, 0.0, -1.0],
5705                fov_y: 0.9,
5706                aspect: 1.0,
5707                near: 0.1,
5708                far: 12.0,
5709            },
5710            toward,
5711        );
5712        let camera = Camera {
5713            view: look_down(4.0),
5714            projection: perspective(1.0),
5715            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
5716            position: [0.0, 4.0, 0.0],
5717        };
5718        let materials = [Material::default()];
5719        let mut floor_instance = Instance::new(floor, identity(), 0, 1);
5720        floor_instance.two_sided = true;
5721        let mut caster = Instance::new(cube, identity(), 0, 2);
5722        caster.shadow_only = true;
5723        let instances = [floor_instance, caster];
5724        let caustic = Caustic {
5725            slot: 1,
5726            rect: [-3.0, -3.0, 6.0, 6.0],
5727            height: -1.0,
5728            strength: 1.0,
5729            receivers: vec![0],
5730        };
5731        let mut capture = |caustic: Option<Caustic>, intensity: f32| {
5732            frame.set_caustic(caustic).unwrap();
5733            let sun = Sun {
5734                direction: toward,
5735                colour: [1.0; 3],
5736                intensity,
5737            };
5738            let scene = calm_scene(camera, &instances, &materials, sun);
5739            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
5740            frame
5741                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
5742                .unwrap();
5743            frame.gpu.queue.submit(Some(encoder.finish()));
5744            frame
5745                .gpu
5746                .readback_rgba16(&frame.targets.hdr)
5747                .unwrap()
5748                .chunks_exact(4)
5749                .map(|p| std::array::from_fn::<f32, 3, _>(|c| half::f16::from_bits(p[c]).to_f32()))
5750                .collect::<Vec<_>>()
5751        };
5752        let plain = capture(None, 2.0);
5753        let lit = capture(Some(caustic.clone()), 2.0);
5754        let ambient = capture(Some(caustic.clone()), 0.0);
5755        let elsewhere = capture(
5756            Some(Caustic {
5757                receivers: vec![1],
5758                ..caustic.clone()
5759            }),
5760            2.0,
5761        );
5762        let off = capture(
5763            Some(Caustic {
5764                rect: [10.0, 10.0, 1.0, 1.0],
5765                ..caustic
5766            }),
5767            2.0,
5768        );
5769        assert_eq!(plain, elsewhere, "a caustic lights only its receivers");
5770        assert_eq!(plain, off, "a caustic lights only inside its rectangle");
5771        let at = |x: usize, y: usize| y * 128 + x;
5772        let (open, shade) = (at(104, 62), at(61, 62));
5773        let sun_open: [f32; 3] = std::array::from_fn(|c| plain[open][c] - ambient[open][c]);
5774        let sun_shade: [f32; 3] = std::array::from_fn(|c| plain[shade][c] - ambient[shade][c]);
5775        assert!(
5776            sun_open.iter().all(|v| *v > 0.1),
5777            "open floor is sunlit: {sun_open:?}"
5778        );
5779        assert!(
5780            (0..3).all(|c| sun_shade[c] < 0.05 * sun_open[c]),
5781            "the caster shades the floor: {sun_shade:?} against {sun_open:?}"
5782        );
5783        for (c, gain) in [2.0f32, 1.5, 1.0].into_iter().enumerate() {
5784            let got = (lit[open][c] - ambient[open][c]) / sun_open[c];
5785            assert!(
5786                (got - gain).abs() < 0.02,
5787                "channel {c}: gain {got}, want {gain}"
5788            );
5789            assert!(
5790                (lit[shade][c] - plain[shade][c]).abs() <= 0.01 * plain[shade][c].max(0.05),
5791                "channel {c}: the caustic shows in the shadow: {} against {}",
5792                lit[shade][c],
5793                plain[shade][c]
5794            );
5795        }
5796        assert!(
5797            frame
5798                .set_caustic(Some(Caustic {
5799                    slot: 9,
5800                    rect: [0.0, 0.0, 1.0, 1.0],
5801                    height: 0.0,
5802                    strength: 1.0,
5803                    receivers: vec![0],
5804                }))
5805                .is_err()
5806        );
5807    }
5808
5809    #[test]
5810    #[ignore = "needs a GPU; run under pgpu"]
5811    fn two_sided_back_face_flips_lit_normal() {
5812        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5813        let mut frame = Frame::new(gpu, 64, 64).unwrap();
5814        let positions = [
5815            [-0.8, -0.8, -3.0],
5816            [0.8, -0.8, -3.0],
5817            [-0.8, 0.8, -3.0],
5818            [0.8, 0.8, -3.0],
5819        ];
5820        let quad = frame
5821            .upload_mesh(MeshData {
5822                positions: &positions,
5823                normals: &[[0.0, 0.0, -1.0]; 4],
5824                tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
5825                uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
5826                uvs1: None,
5827                alpha: None,
5828                indices: &[0, 2, 1, 1, 2, 3],
5829            })
5830            .unwrap();
5831        let camera = Camera {
5832            view: identity(),
5833            projection: perspective(1.0),
5834            previous_view_projection: perspective(1.0),
5835            position: [0.0; 3],
5836        };
5837        let material = [Material::default()];
5838        let sun = Sun {
5839            direction: [0.0, 0.0, 1.0],
5840            colour: [1.0; 3],
5841            intensity: 4.0,
5842        };
5843        let mut instance = Instance::new(quad, identity(), 0, 7);
5844        let mut capture = |instance: Instance| {
5845            frame
5846                .render(&calm_scene(camera, &[instance], &material, sun))
5847                .unwrap();
5848            let center = 32 * 64 + 32;
5849            let ids = read_target(&frame, &frame.targets.ids, 4);
5850            let normals = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5851            (
5852                u32::from_le_bytes(ids[center * 4..center * 4 + 4].try_into().unwrap()),
5853                half(normals[center * 4]),
5854            )
5855        };
5856        let one_sided = capture(instance);
5857        instance.two_sided = true;
5858        let two_sided = capture(instance);
5859        assert_eq!(one_sided.0, 0);
5860        assert_eq!(two_sided.0, 7);
5861        assert!(two_sided.1 > one_sided.1 + 0.1);
5862    }
5863
5864    #[test]
5865    #[ignore = "needs a GPU; run under pgpu"]
5866    fn chrome_sphere_reflects_nearby_red_box() {
5867        use pfx_bake::reflection::{ReflectionManifest, ReflectionSpec, bake_reflection};
5868        use pfx_bake::{Anchor, BakeScene};
5869        use pfx_load::Sky;
5870        use pfx_trace::shapes::Shape as TraceShape;
5871
5872        let gpu = pollster::block_on(Gpu::headless()).unwrap();
5873        let bake_scene = BakeScene {
5874            triangles: Vec::new(),
5875            shapes: vec![TraceShape::RoundedBox {
5876                center: [1.6, 0.0, -3.0],
5877                half: [0.4; 3],
5878                radius: 0.02,
5879                material: 0,
5880            }],
5881            materials: vec![Material {
5882                base: [1.0, 0.0, 0.0],
5883                emission: [6.0, 0.0, 0.0],
5884                ..Default::default()
5885            }],
5886            anchors: vec![Anchor {
5887                hour: 12.0,
5888                sky: Sky {
5889                    width: 1,
5890                    height: 1,
5891                    texels: vec![[0.0, 0.0, 0.0, 1.0]],
5892                },
5893                sun: pfx_trace::Sun {
5894                    direction: [0.0, 1.0, 0.0],
5895                    color: [0.0; 3],
5896                    intensity: 0.0,
5897                },
5898            }],
5899        };
5900        let spec = ReflectionSpec {
5901            name: Some("red-box".into()),
5902            position: [0.0, 0.0, -3.0],
5903            min: [-2.0, -2.0, -5.0],
5904            max: [2.0, 2.0, 1.0],
5905            resolution: 16,
5906            anchors: None,
5907            fade: 0.25,
5908            priority: 0.0,
5909        };
5910        let cube = bake_reflection(&gpu, &bake_scene, &spec, 4, 7, 0).unwrap();
5911        let mut frame = Frame::new(gpu, 128, 128).unwrap();
5912        let sphere_handle = sphere(&mut frame);
5913        let box_mesh = Shape::RoundBox {
5914            half: [0.4; 3],
5915            radius: 0.02,
5916        }
5917        .mesh(0.02);
5918        let box_handle = frame
5919            .upload_mesh(MeshData {
5920                positions: &box_mesh.positions,
5921                normals: &box_mesh.normals,
5922                tangents: &box_mesh.tangents,
5923                uvs: &box_mesh.uvs,
5924                uvs1: None,
5925                alpha: None,
5926                indices: &box_mesh.indices,
5927            })
5928            .unwrap();
5929        let mut sphere_model = identity();
5930        sphere_model[3][2] = -3.0;
5931        let mut box_model = identity();
5932        box_model[3] = [1.6, 0.0, -3.0, 1.0];
5933        let instances = [
5934            Instance::new(sphere_handle, sphere_model, 0, 1),
5935            Instance::new(box_handle, box_model, 1, 2),
5936        ];
5937        let materials = [
5938            Material {
5939                base: [0.95; 3],
5940                metalness: 1.0,
5941                roughness: 0.04,
5942                ..Default::default()
5943            },
5944            bake_scene.materials[0],
5945        ];
5946        let camera = Camera {
5947            view: identity(),
5948            projection: perspective(1.0),
5949            previous_view_projection: perspective(1.0),
5950            position: [0.0; 3],
5951        };
5952        let scene = calm_scene(
5953            camera,
5954            &instances,
5955            &materials,
5956            Sun {
5957                direction: [0.0, 1.0, 0.0],
5958                colour: [0.0; 3],
5959                intensity: 0.0,
5960            },
5961        );
5962        frame.render(&scene).unwrap();
5963        let without = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5964        let artifact = ReflectionArtifact {
5965            manifest: ReflectionManifest {
5966                schema_version: 1,
5967                scene_hash: String::new(),
5968                spec,
5969                anchors: vec![12.0],
5970                sample_count: 4,
5971                seed: 7,
5972                format: String::new(),
5973                color_space: String::new(),
5974                coordinates: String::new(),
5975                files: Vec::new(),
5976            },
5977            cubes: vec![cube],
5978        };
5979        frame
5980            .set_local_reflections(Some(vec![artifact]), 12.0)
5981            .unwrap();
5982        frame.render(&scene).unwrap();
5983        let with = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
5984        let red = |pixels: &[u16]| {
5985            (45..80)
5986                .flat_map(|y| (85..95).map(move |x| (y * 128 + x) * 4))
5987                .map(|index| half(pixels[index]))
5988                .sum::<f32>()
5989        };
5990        assert!(
5991            red(&with) > red(&without) + 20.0,
5992            "without: {}, with: {}",
5993            red(&without),
5994            red(&with)
5995        );
5996    }
5997
5998    fn velocity_capture(
5999        model: Matrix,
6000        previous_model: Matrix,
6001        view: Matrix,
6002        previous_view: Matrix,
6003    ) -> (Vec<[u16; 2]>, Vec<bool>) {
6004        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6005        let mut frame = Frame::new(gpu, 128, 128).unwrap();
6006        let sphere_handle = sphere(&mut frame);
6007        let mut instance = Instance::new(sphere_handle, model, 0, 7);
6008        instance.previous_model = previous_model;
6009        let materials = [Material::default()];
6010        let camera = Camera {
6011            view,
6012            projection: perspective(1.0),
6013            previous_view_projection: multiply(perspective(1.0), previous_view),
6014            position: [0.0; 3],
6015        };
6016        let sun = Sun {
6017            direction: [0.4, 0.8, 0.7],
6018            colour: [1.0; 3],
6019            intensity: 2.0,
6020        };
6021        let instances = [instance];
6022        frame
6023            .render(&calm_scene(camera, &instances, &materials, sun))
6024            .unwrap();
6025        let ids = read_target(&frame, &frame.targets.ids, 4);
6026        let covered = ids
6027            .chunks_exact(4)
6028            .map(|pixel| u32::from_le_bytes(pixel.try_into().unwrap()) == 7)
6029            .collect();
6030        (frame.readback_velocity().unwrap(), covered)
6031    }
6032
6033    fn posed(x: f32, y: f32, z: f32) -> Matrix {
6034        let mut model = identity();
6035        model[3] = [x, y, z, 1.0];
6036        model
6037    }
6038
6039    #[test]
6040    #[ignore = "needs a GPU; run under pgpu"]
6041    fn unmoved_instance_under_static_camera_has_exact_zero_velocity() {
6042        let model = posed(0.37, -0.21, -3.3);
6043        let (velocity, covered) = velocity_capture(model, model, identity(), identity());
6044        assert!(covered.iter().filter(|&&covered| covered).count() > 500);
6045        let nonzero: Vec<_> = velocity
6046            .iter()
6047            .enumerate()
6048            .filter(|(_, pair)| **pair != [0, 0])
6049            .map(|(index, pair)| (index % 128, index / 128, covered[index], *pair))
6050            .collect();
6051        assert!(
6052            nonzero.is_empty(),
6053            "{} nonzero, covered {}, first: {:?}",
6054            nonzero.len(),
6055            nonzero.iter().filter(|entry| entry.2).count(),
6056            &nonzero[..nonzero.len().min(8)]
6057        );
6058    }
6059
6060    #[test]
6061    #[ignore = "needs a GPU; run under pgpu"]
6062    fn unmoved_instance_under_moving_camera_reads_camera_motion() {
6063        let model = posed(0.37, -0.21, -3.3);
6064        let mut previous_view = identity();
6065        previous_view[3][0] = 0.05;
6066        let (velocity, covered) = velocity_capture(model, model, identity(), previous_view);
6067        let focal = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
6068        let near = 0.05 * focal * 0.5 / (3.3 + 0.75);
6069        let far = 0.05 * focal * 0.5 / (3.3 - 0.75);
6070        let mut seen = 0;
6071        for (pair, _) in velocity
6072            .iter()
6073            .zip(&covered)
6074            .filter(|(_, covered)| **covered)
6075        {
6076            let x = half(pair[0]);
6077            assert!(
6078                x > near * 0.9 && x < far * 1.1,
6079                "x velocity {x} outside {near}..{far}"
6080            );
6081            assert!(half(pair[1]).abs() < far * 0.01);
6082            seen += 1;
6083        }
6084        assert!(seen > 500);
6085    }
6086
6087    #[test]
6088    #[ignore = "needs a GPU; run under pgpu"]
6089    fn slowly_moving_instance_keeps_its_velocity() {
6090        let (velocity, covered) = velocity_capture(
6091            posed(0.37, -0.21, -3.3),
6092            posed(0.37 - 1e-5, -0.21, -3.3),
6093            identity(),
6094            identity(),
6095        );
6096        assert!(covered.iter().filter(|&&covered| covered).count() > 500);
6097        assert!(
6098            velocity
6099                .iter()
6100                .zip(&covered)
6101                .any(|(pair, covered)| *covered && pair[0] != 0 && half(pair[0]) < 0.0)
6102        );
6103    }
6104
6105    #[test]
6106    #[ignore = "needs a GPU; run under pgpu"]
6107    fn headless_sphere_plane_and_4k_timing() {
6108        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6109        let mut frame = Frame::new(gpu, 128, 128).unwrap();
6110        let sphere_handle = sphere(&mut frame);
6111        let plane_handle = plane(&mut frame);
6112        let mut sphere_model = identity();
6113        sphere_model[3][2] = -3.0;
6114        let instances = [
6115            Instance::new(sphere_handle, sphere_model, 0, 17),
6116            Instance::new(plane_handle, identity(), 1, 23),
6117        ];
6118        let materials = [
6119            Material {
6120                base: [0.8, 0.2, 0.1],
6121                roughness: 0.6,
6122                ..Default::default()
6123            },
6124            Material {
6125                base: [0.4, 0.4, 0.4],
6126                ..Default::default()
6127            },
6128        ];
6129        let camera = Camera {
6130            view: identity(),
6131            projection: perspective(1.0),
6132            previous_view_projection: perspective(1.0),
6133            position: [0.0, 0.0, 0.0],
6134        };
6135        let sun = Sun {
6136            direction: [0.4, 0.8, 0.7],
6137            colour: [1.0; 3],
6138            intensity: 2.0,
6139        };
6140        let scene = calm_scene(camera, &instances, &materials, sun);
6141        frame.render(&scene).unwrap();
6142        let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6143        let center = (64 * 128 + 64) * 4;
6144        let luminance = 0.2126 * half(hdr[center])
6145            + 0.7152 * half(hdr[center + 1])
6146            + 0.0722 * half(hdr[center + 2]);
6147        assert!(
6148            (0.02..3.0).contains(&luminance),
6149            "sphere luminance: {luminance}"
6150        );
6151        assert_eq!(half(hdr[0]), 0.0);
6152        let velocity = frame.readback_velocity().unwrap();
6153        assert_eq!(velocity[64 * 128 + 64], [0, 0]);
6154        drop(frame);
6155        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6156        let mut frame = Frame::new(gpu, 3840, 2160).unwrap();
6157        let sphere = sphere(&mut frame);
6158        let plane = plane(&mut frame);
6159        let instances = [
6160            Instance {
6161                mesh: sphere,
6162                ..instances[0]
6163            },
6164            Instance {
6165                mesh: plane,
6166                ..instances[1]
6167            },
6168        ];
6169        let camera = Camera {
6170            projection: perspective(3840.0 / 2160.0),
6171            previous_view_projection: perspective(3840.0 / 2160.0),
6172            ..camera
6173        };
6174        let scene = calm_scene(camera, &instances, &materials, sun);
6175        frame.render(&scene).unwrap();
6176        frame
6177            .gpu
6178            .device
6179            .poll(wgpu::PollType::wait_indefinitely())
6180            .unwrap();
6181        let timings = frame.collect_timings();
6182        assert!(!timings.is_empty(), "GPU timestamps unavailable");
6183        for pass in &timings[0] {
6184            println!("3840x2160 {}: {:.3} ms", pass.label, pass.milliseconds);
6185        }
6186    }
6187
6188    fn write_grid(root: &std::path::Path, irradiance: [f32; 3]) {
6189        use pfx_bake::{FileEntry, Grid, GridSpec, Manifest, Probe};
6190        use sha2::{Digest, Sha256};
6191        let digest = |bytes: &[u8]| -> String {
6192            Sha256::digest(bytes)
6193                .iter()
6194                .map(|byte| format!("{byte:02x}"))
6195                .collect()
6196        };
6197        if root.exists() {
6198            std::fs::remove_dir_all(root).unwrap();
6199        }
6200        std::fs::create_dir_all(root).unwrap();
6201        let spec = GridSpec {
6202            min: [-1.0, -0.5, -1.0],
6203            max: [0.0, 0.5, 1.0],
6204            spacing: 0.5,
6205        };
6206        let dims = spec.dimensions().unwrap();
6207        let count = dims.iter().product::<u32>() as usize;
6208        let probe = Probe {
6209            lobes: [irradiance; 6],
6210            visibility: [100.0; 6],
6211        };
6212        let grid = Grid::new(spec, vec![probe; count]).unwrap();
6213        let bytes = grid.bytes();
6214        std::fs::write(root.join("probe-000.bin"), &bytes).unwrap();
6215        let manifest = Manifest {
6216            schema_version: 1,
6217            scene_hash: "ambient-test".into(),
6218            anchors: vec![12.0],
6219            sample_count: 1,
6220            seed: 0,
6221            grid: spec,
6222            dimensions: dims,
6223            format: "ambient_cube_rgb16f_visibility_f16_le_v1".into(),
6224            color_space: "linear_rec709_irradiance".into(),
6225            coordinates: "right_handed_y_up_xyz_x_fastest".into(),
6226            files: vec![FileEntry {
6227                path: "probe-000.bin".into(),
6228                bytes: bytes.len() as u64,
6229                sha256: digest(&bytes),
6230            }],
6231            emitters: None,
6232        };
6233        let json = serde_json::to_vec(&manifest).unwrap();
6234        std::fs::write(root.join("manifest.json"), &json).unwrap();
6235        std::fs::write(root.join("manifest.sha256"), digest(&json)).unwrap();
6236    }
6237
6238    #[test]
6239    #[ignore = "needs a GPU; run under pgpu"]
6240    fn ambient_uses_the_grid_where_bound_and_the_sky_elsewhere() {
6241        let size = 128;
6242        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6243        let mut frame = Frame::new(gpu, size, size).unwrap();
6244        let floor = quad(
6245            &mut frame,
6246            [
6247                [-2.0, 0.0, -2.0],
6248                [2.0, 0.0, -2.0],
6249                [-2.0, 0.0, 2.0],
6250                [2.0, 0.0, 2.0],
6251            ],
6252        );
6253        let instances = [Instance::new(floor, identity(), 0, 1)];
6254        let base = 0.8;
6255        let materials = [Material {
6256            base: [base; 3],
6257            roughness: 1.0,
6258            specular: 0.0,
6259            ..Default::default()
6260        }];
6261        let projection = perspective(1.0);
6262        let view = look_down(3.0);
6263        let camera = Camera {
6264            view,
6265            projection,
6266            previous_view_projection: multiply(projection, view),
6267            position: [0.0, 3.0, 0.0],
6268        };
6269        let dark = Sun {
6270            direction: [0.0, 1.0, 0.0],
6271            colour: [1.0; 3],
6272            intensity: 0.0,
6273        };
6274        let read = |frame: &mut Frame| {
6275            let scene = calm_scene(camera, &instances, &materials, dark);
6276            frame.render(&scene).unwrap();
6277            let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6278            let at = |x: u32, y: u32| {
6279                let i = ((y * size + x) * 4) as usize;
6280                [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6281            };
6282            (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6283        };
6284        let sky_only = read(&mut frame);
6285        let sh = frame.sky().lighting.sh;
6286        let expected_sky = sky_irradiance(&sh, [0.0, 1.0, 0.0]).map(|e| base * e / PI);
6287        for side in [sky_only.0, sky_only.1] {
6288            for channel in 0..3 {
6289                let ratio = side[channel] / expected_sky[channel];
6290                assert!(
6291                    (0.85..1.2).contains(&ratio),
6292                    "sky ambient {side:?} against {expected_sky:?}"
6293                );
6294            }
6295        }
6296        let grid = [0.6, 0.2, 0.1];
6297        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("../../tmp/ambient-grid");
6298        write_grid(&root, grid);
6299        let probes =
6300            ProbeLighting::load(&frame.gpu.device, &frame.gpu.queue, &root, 12.0, [-1.0; 3])
6301                .unwrap();
6302        frame.set_probes(probes);
6303        let (inside, outside) = read(&mut frame);
6304        let expected_grid = grid.map(|e| base * e / PI);
6305        for channel in 0..3 {
6306            let ratio = inside[channel] / expected_grid[channel];
6307            assert!(
6308                (0.85..1.2).contains(&ratio),
6309                "grid ambient {inside:?} against {expected_grid:?}"
6310            );
6311            assert!((outside[channel] - sky_only.1[channel]).abs() < 0.01);
6312        }
6313        assert!(inside[0] > inside[2] * 3.0);
6314        assert!(outside[2] > outside[0]);
6315        println!(
6316            "grid side {inside:?}, sky side {outside:?}, expected {expected_grid:?} and {expected_sky:?}"
6317        );
6318        std::fs::remove_dir_all(root).unwrap();
6319    }
6320
6321    #[test]
6322    #[ignore = "needs a GPU; run under pgpu"]
6323    fn reflection_occlusion_dims_a_dark_grid_and_an_instance_can_keep_its_reflection() {
6324        let size = 64;
6325        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6326        let mut frame = Frame::new(gpu, size, size).unwrap();
6327        let floor = quad(
6328            &mut frame,
6329            [
6330                [-2.0, 0.0, -2.0],
6331                [2.0, 0.0, -2.0],
6332                [-2.0, 0.0, 2.0],
6333                [2.0, 0.0, 2.0],
6334            ],
6335        );
6336        let instances = [Instance::new(floor, identity(), 0, 1)];
6337        let materials = [Material {
6338            base: [0.9; 3],
6339            metalness: 1.0,
6340            roughness: 0.3,
6341            ..Default::default()
6342        }];
6343        let projection = perspective(1.0);
6344        let view = look_down(3.0);
6345        let camera = Camera {
6346            view,
6347            projection,
6348            previous_view_projection: multiply(projection, view),
6349            position: [0.0, 3.0, 0.0],
6350        };
6351        let dark = Sun {
6352            direction: [0.0, 1.0, 0.0],
6353            colour: [1.0; 3],
6354            intensity: 0.0,
6355        };
6356        let root = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
6357            .join("../../tmp/reflection-occlusion-grid");
6358        write_grid(&root, [0.01; 3]);
6359        let probes =
6360            ProbeLighting::load(&frame.gpu.device, &frame.gpu.queue, &root, 12.0, [-1.0; 3])
6361                .unwrap();
6362        frame.set_probes(probes);
6363        frame.set_sky_visibility(false);
6364        let read = |frame: &mut Frame, scale: f32| {
6365            frame
6366                .set_surfaces(&[InstanceSurface {
6367                    reflection_occlusion: scale,
6368                    ..Default::default()
6369                }])
6370                .unwrap();
6371            let scene = calm_scene(camera, &instances, &materials, dark);
6372            frame.render(&scene).unwrap();
6373            let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6374            let at = |x: u32, y: u32| {
6375                let i = ((y * size + x) * 4) as usize;
6376                [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6377            };
6378            (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6379        };
6380        let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
6381        let open = read(&mut frame, 1.0);
6382        for scale in [0.0, 0.5] {
6383            assert_eq!(
6384                read(&mut frame, scale),
6385                open,
6386                "occlusion off, scale {scale}"
6387            );
6388        }
6389        frame.set_reflection_occlusion(1.0).unwrap();
6390        assert_eq!(frame.probes().reflection_occlusion(), 1.0);
6391        let full = read(&mut frame, 1.0);
6392        let half_kept = read(&mut frame, 0.5);
6393        let kept = read(&mut frame, 0.0);
6394        println!(
6395            "under the dark grid: open {:.4}, scale 1 {:.4}, scale 0.5 {:.4}, scale 0 {:.4}",
6396            luma(open.0),
6397            luma(full.0),
6398            luma(half_kept.0),
6399            luma(kept.0)
6400        );
6401        assert!(luma(full.0) < luma(open.0) * 0.5);
6402        assert!(luma(full.0) < luma(half_kept.0) && luma(half_kept.0) < luma(open.0));
6403        assert_eq!(kept, open);
6404        assert_eq!(full.1, open.1);
6405        assert!(frame.set_reflection_occlusion(1.5).is_err());
6406        assert!(
6407            frame
6408                .set_surfaces(&[InstanceSurface {
6409                    reflection_occlusion: -0.5,
6410                    ..Default::default()
6411                }])
6412                .is_err()
6413        );
6414        std::fs::remove_dir_all(root).unwrap();
6415    }
6416
6417    #[test]
6418    fn an_authored_ambient_is_flat_and_the_default_is_the_sky() {
6419        let mut sh = [[0.0; 3]; 9];
6420        sh[0] = [1.0, 2.0, 3.0];
6421        sh[1] = [0.5, 0.5, 0.5];
6422        assert_eq!(Ambient::default(), Ambient::Sky);
6423        assert_eq!(Ambient::Sky.sh(sh), sh);
6424        let constant = [0.31 * PI, 0.305 * PI, 0.43 * PI];
6425        let flat = Ambient::Constant(constant).sh(sh);
6426        for normal in [[0.0, 1.0, 0.0], [0.0, -1.0, 0.0], [0.6, 0.0, 0.8]] {
6427            let got = sky_irradiance(&flat, normal);
6428            for channel in 0..3 {
6429                assert!((got[channel] - constant[channel]).abs() < 1e-5);
6430            }
6431        }
6432        let own = [[0.25; 3]; 9];
6433        assert_eq!(Ambient::Sh(own).sh(sh), own);
6434        assert!(Ambient::Constant(constant).valid());
6435        assert!(!Ambient::Constant([-0.1, 0.0, 0.0]).valid());
6436        assert!(!Ambient::Constant([f32::NAN, 0.0, 0.0]).valid());
6437        assert!(!Ambient::Sh([[f32::INFINITY; 3]; 9]).valid());
6438    }
6439
6440    #[test]
6441    #[ignore = "needs a GPU; run under pgpu"]
6442    fn an_authored_ambient_survives_set_probes_and_the_default_is_unchanged() {
6443        let size = 128;
6444        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6445        let mut frame = Frame::new(gpu, size, size).unwrap();
6446        let floor = quad(
6447            &mut frame,
6448            [
6449                [-2.0, 0.0, -2.0],
6450                [2.0, 0.0, -2.0],
6451                [-2.0, 0.0, 2.0],
6452                [2.0, 0.0, 2.0],
6453            ],
6454        );
6455        let instances = [Instance::new(floor, identity(), 0, 1)];
6456        let base = 0.8;
6457        let materials = [Material {
6458            base: [base; 3],
6459            roughness: 1.0,
6460            specular: 0.0,
6461            ..Default::default()
6462        }];
6463        let projection = perspective(1.0);
6464        let view = look_down(3.0);
6465        let camera = Camera {
6466            view,
6467            projection,
6468            previous_view_projection: multiply(projection, view),
6469            position: [0.0, 3.0, 0.0],
6470        };
6471        let dark = Sun {
6472            direction: [0.0, 1.0, 0.0],
6473            colour: [1.0; 3],
6474            intensity: 0.0,
6475        };
6476        let read = |frame: &mut Frame| {
6477            let scene = calm_scene(camera, &instances, &materials, dark);
6478            frame.render(&scene).unwrap();
6479            let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6480            let i = (((size / 2) * size + size / 2) * 4) as usize;
6481            [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6482        };
6483        let sky_first = read(&mut frame);
6484        let fresh = ProbeLighting::fallback(&frame.gpu.device, &frame.gpu.queue, [-1.0; 3]);
6485        frame.set_probes(fresh);
6486        let sky_after = read(&mut frame);
6487        for channel in 0..3 {
6488            assert_eq!(sky_first[channel], sky_after[channel]);
6489        }
6490        let authored = [0.31 * PI, 0.305 * PI, 0.43 * PI];
6491        frame.set_ambient(Ambient::Constant(authored)).unwrap();
6492        let fresh = ProbeLighting::fallback(&frame.gpu.device, &frame.gpu.queue, [-1.0; 3]);
6493        frame.set_probes(fresh);
6494        assert_eq!(frame.ambient(), Ambient::Constant(authored));
6495        let unflagged = read(&mut frame);
6496        for channel in 0..3 {
6497            assert_eq!(sky_first[channel], unflagged[channel]);
6498        }
6499        frame
6500            .set_surfaces(&[InstanceSurface {
6501                authored_ambient: true,
6502                ..Default::default()
6503            }])
6504            .unwrap();
6505        let flat = read(&mut frame);
6506        for channel in 0..3 {
6507            let expected = base * authored[channel] / PI;
6508            assert!(
6509                (flat[channel] / expected - 1.0).abs() < 0.03,
6510                "authored ambient {flat:?} against {authored:?}"
6511            );
6512        }
6513        assert!(flat[2] > flat[0]);
6514        assert!(
6515            frame
6516                .set_ambient(Ambient::Constant([-1.0, 0.0, 0.0]))
6517                .is_err()
6518        );
6519        assert_eq!(frame.ambient(), Ambient::Constant(authored));
6520        frame.set_ambient(Ambient::Sky).unwrap();
6521        let back = read(&mut frame);
6522        for channel in 0..3 {
6523            assert_eq!(sky_first[channel], back[channel]);
6524        }
6525        println!("sky {sky_first:?}, authored {flat:?}");
6526    }
6527
6528    #[test]
6529    #[ignore = "needs a GPU; run under pgpu"]
6530    fn contact_darkens_flagged_receivers_by_mix_of_tint_and_one_over_the_field_squared() {
6531        let size = 128;
6532        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6533        let mut frame = Frame::new(gpu, size, size).unwrap();
6534        let floor = quad(
6535            &mut frame,
6536            [
6537                [-2.0, 0.0, -2.0],
6538                [2.0, 0.0, -2.0],
6539                [-2.0, 0.0, 2.0],
6540                [2.0, 0.0, 2.0],
6541            ],
6542        );
6543        let instances = [Instance::new(floor, identity(), 0, 1)];
6544        let materials = [Material {
6545            base: [0.8; 3],
6546            roughness: 1.0,
6547            specular: 0.0,
6548            ..Default::default()
6549        }];
6550        let projection = perspective(1.0);
6551        let view = look_down(3.0);
6552        let camera = Camera {
6553            view,
6554            projection,
6555            previous_view_projection: multiply(projection, view),
6556            position: [0.0, 3.0, 0.0],
6557        };
6558        let dark = Sun {
6559            direction: [0.0, 1.0, 0.0],
6560            colour: [1.0; 3],
6561            intensity: 0.0,
6562        };
6563        let read = |frame: &mut Frame| {
6564            let scene = calm_scene(camera, &instances, &materials, dark);
6565            frame.render(&scene).unwrap();
6566            let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
6567            let at = |x: u32, y: u32| {
6568                let i = ((y * size + x) * 4) as usize;
6569                [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
6570            };
6571            (at(size / 4, size / 2), at(size * 3 / 4, size / 2))
6572        };
6573        let open = read(&mut frame);
6574        let tint = [0.34, 0.34, 0.50];
6575        frame
6576            .set_contact(Some(ContactDesc {
6577                size: [64, 2],
6578                min: [-2.0, -2.0],
6579                max: [2.0, 2.0],
6580                tint,
6581            }))
6582            .unwrap();
6583        let field: Vec<f32> = (0..128)
6584            .map(|i| if i % 64 < 32 { 0.5 } else { 1.0 })
6585            .collect();
6586        frame.write_contact(&field).unwrap();
6587        let unflagged = read(&mut frame);
6588        for channel in 0..3 {
6589            assert_eq!(unflagged.0[channel], open.0[channel]);
6590            assert_eq!(unflagged.1[channel], open.1[channel]);
6591        }
6592        frame
6593            .set_surfaces(&[InstanceSurface {
6594                contact: true,
6595                ..Default::default()
6596            }])
6597            .unwrap();
6598        let (dim, clear) = read(&mut frame);
6599        for channel in 0..3 {
6600            let wanted = tint[channel] + (1.0 - tint[channel]) * 0.25;
6601            let got = dim[channel] / open.0[channel];
6602            assert!(
6603                (got / wanted - 1.0).abs() < 0.02,
6604                "contact {got} against {wanted} in channel {channel}"
6605            );
6606            assert!((clear[channel] / open.1[channel] - 1.0).abs() < 0.01);
6607        }
6608        println!("open {open:?}, flagged {dim:?} and {clear:?}");
6609        frame.set_contact(None).unwrap();
6610        let removed = read(&mut frame);
6611        for channel in 0..3 {
6612            assert_eq!(removed.0[channel], open.0[channel]);
6613        }
6614        assert!(frame.write_contact(&field).is_err());
6615    }
6616
6617    fn read_atlas(frame: &Frame, shadows: &Shadows) -> Vec<f32> {
6618        let resolution = shadows.resolution();
6619        let row = resolution * 4;
6620        let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
6621            label: Some("atlas readback"),
6622            size: u64::from(row * resolution) * CASCADE_COUNT as u64,
6623            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6624            mapped_at_creation: false,
6625        });
6626        let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6627        encoder.copy_texture_to_buffer(
6628            wgpu::TexelCopyTextureInfo {
6629                texture: shadows.atlas(),
6630                mip_level: 0,
6631                origin: wgpu::Origin3d::ZERO,
6632                aspect: wgpu::TextureAspect::DepthOnly,
6633            },
6634            wgpu::TexelCopyBufferInfo {
6635                buffer: &buffer,
6636                layout: wgpu::TexelCopyBufferLayout {
6637                    offset: 0,
6638                    bytes_per_row: Some(row),
6639                    rows_per_image: Some(resolution),
6640                },
6641            },
6642            wgpu::Extent3d {
6643                width: resolution,
6644                height: resolution,
6645                depth_or_array_layers: CASCADE_COUNT as u32,
6646            },
6647        );
6648        frame.gpu.queue.submit(Some(encoder.finish()));
6649        buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
6650        frame
6651            .gpu
6652            .device
6653            .poll(wgpu::PollType::wait_indefinitely())
6654            .unwrap();
6655        let values = bytemuck::cast_slice::<u8, f32>(
6656            &buffer.slice(..).get_mapped_range().expect("a mapped range"),
6657        )
6658        .to_vec();
6659        buffer.unmap();
6660        values
6661    }
6662
6663    #[test]
6664    #[ignore = "needs a GPU; run under pgpu"]
6665    fn deformation_reaches_the_shadow_pass() {
6666        use crate::shadow::{Quality, View};
6667        use pfx_geom::strip::{Roller, Strip};
6668        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6669        let mut frame = Frame::new(gpu, 256, 256).unwrap();
6670        let strip = Strip {
6671            length: 3.0,
6672            width: 1.2,
6673        };
6674        let mesh = strip.mesh(0.12, 0.002, None, 4);
6675        let handle = frame
6676            .upload_mesh(MeshData {
6677                positions: &mesh.positions,
6678                normals: &mesh.normals,
6679                tangents: &mesh.tangents,
6680                uvs: &mesh.uvs,
6681                uvs1: None,
6682                alpha: None,
6683                indices: &mesh.indices,
6684            })
6685            .unwrap();
6686        let mut shadows = Shadows::new(
6687            &frame.gpu.device,
6688            Quality {
6689                resolution: 512,
6690                ..Default::default()
6691            },
6692        );
6693        let view = View {
6694            eye: [1.5, 4.0, 4.0],
6695            forward: [0.0, -0.7, -0.7],
6696            up: [0.0, 1.0, 0.0],
6697            fov_y: 0.9,
6698            aspect: 1.0,
6699            near: 0.1,
6700            far: 12.0,
6701        };
6702        let fit = shadows.fit(&view, [0.3, 0.9, 0.2]);
6703        let mut instance = Instance::new(handle, identity(), 0, 1);
6704        instance.deformer = DeformerId(0);
6705        let instances = [instance];
6706        let materials = [Material::default()];
6707        let camera = Camera {
6708            view: look_down(4.0),
6709            projection: perspective(1.0),
6710            previous_view_projection: perspective(1.0),
6711            position: [0.0, 4.0, 0.0],
6712        };
6713        let mut render = |at: f32| {
6714            let roller = Roller {
6715                at,
6716                core: 0.12,
6717                thickness: 0.01,
6718            };
6719            let state = RollerFrame {
6720                at,
6721                outer: roller.outer(&strip),
6722                thickness: roller.thickness,
6723            };
6724            let deformers = [Deformer::Roller {
6725                current: state,
6726                previous: state,
6727            }];
6728            let scene = Scene {
6729                deformers: &deformers,
6730                ..calm_scene(
6731                    camera,
6732                    &instances,
6733                    &materials,
6734                    Sun {
6735                        direction: [0.3, 0.9, 0.2],
6736                        colour: [1.0; 3],
6737                        intensity: 1.0,
6738                    },
6739                )
6740            };
6741            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6742            frame
6743                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
6744                .unwrap();
6745            frame.gpu.queue.submit(Some(encoder.finish()));
6746            read_atlas(&frame, &shadows)
6747        };
6748        let flat = render(3.0);
6749        let rolled = render(1.2);
6750        let flat_again = render(3.0);
6751        assert_eq!(flat, flat_again);
6752        let covered = |depths: &[f32]| depths.iter().filter(|depth| **depth < 1.0).count();
6753        let changed = flat
6754            .iter()
6755            .zip(&rolled)
6756            .filter(|(a, b)| (**a - **b).abs() > 1e-4)
6757            .count();
6758        println!(
6759            "flat casters {} texels, rolled casters {} texels, {changed} texels changed",
6760            covered(&flat),
6761            covered(&rolled)
6762        );
6763        assert!(covered(&flat) > 1000);
6764        assert!(changed > 500);
6765    }
6766    fn read_target(frame: &Frame, texture: &wgpu::Texture, texel: u32) -> Vec<u8> {
6767        let size = texture.size();
6768        let row = (size.width * texel).div_ceil(256) * 256;
6769        let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
6770            label: Some("test readback"),
6771            size: u64::from(row * size.height),
6772            usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
6773            mapped_at_creation: false,
6774        });
6775        let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6776        encoder.copy_texture_to_buffer(
6777            wgpu::TexelCopyTextureInfo {
6778                texture,
6779                mip_level: 0,
6780                origin: wgpu::Origin3d::ZERO,
6781                aspect: wgpu::TextureAspect::All,
6782            },
6783            wgpu::TexelCopyBufferInfo {
6784                buffer: &buffer,
6785                layout: wgpu::TexelCopyBufferLayout {
6786                    offset: 0,
6787                    bytes_per_row: Some(row),
6788                    rows_per_image: Some(size.height),
6789                },
6790            },
6791            wgpu::Extent3d {
6792                width: size.width,
6793                height: size.height,
6794                depth_or_array_layers: 1,
6795            },
6796        );
6797        frame.gpu.queue.submit(Some(encoder.finish()));
6798        buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
6799        frame
6800            .gpu
6801            .device
6802            .poll(wgpu::PollType::wait_indefinitely())
6803            .unwrap();
6804        let mapped = buffer.slice(..).get_mapped_range().expect("a mapped range");
6805        let mut out = Vec::new();
6806        for y in 0..size.height {
6807            let start = (y * row) as usize;
6808            out.extend_from_slice(&mapped[start..start + (size.width * texel) as usize]);
6809        }
6810        drop(mapped);
6811        buffer.unmap();
6812        out
6813    }
6814
6815    struct Outputs {
6816        ids: Vec<u32>,
6817        depth: Vec<f32>,
6818        colour: Vec<[f32; 3]>,
6819        atlas: Vec<f32>,
6820    }
6821
6822    #[test]
6823    #[ignore = "needs a GPU; run under pgpu"]
6824    fn clip_planes_cut_prepass_opaque_shadow_and_ids_alike() {
6825        use crate::shadow::{Quality, View};
6826        let gpu = pollster::block_on(Gpu::headless()).unwrap();
6827        let size = 256;
6828        let mut frame = Frame::new(gpu, size, size).unwrap();
6829        let floor = quad(
6830            &mut frame,
6831            [
6832                [-4.0, 0.0, -4.0],
6833                [4.0, 0.0, -4.0],
6834                [-4.0, 0.0, 4.0],
6835                [4.0, 0.0, 4.0],
6836            ],
6837        );
6838        let card = |x0: f32, x1: f32| {
6839            [
6840                [x0, 1.0, -1.0],
6841                [x1, 1.0, -1.0],
6842                [x0, 1.0, 1.0],
6843                [x1, 1.0, 1.0],
6844            ]
6845        };
6846        let whole = quad(&mut frame, card(-1.0, 1.0));
6847        let half_card = quad(&mut frame, card(-1.0, 0.0));
6848        let mut shadows = Shadows::new(
6849            &frame.gpu.device,
6850            Quality {
6851                resolution: 1024,
6852                ..Default::default()
6853            },
6854        );
6855        let sun_toward = {
6856            let v: [f32; 3] = [0.6, 0.8, 0.0];
6857            let length = v.iter().map(|x| x * x).sum::<f32>().sqrt();
6858            v.map(|x| x / length)
6859        };
6860        let view = View {
6861            eye: [0.0, 4.0, 0.0],
6862            forward: [0.0, -1.0, 0.0],
6863            up: [0.0, 0.0, -1.0],
6864            fov_y: 50.0_f32.to_radians(),
6865            aspect: 1.0,
6866            near: 0.1,
6867            far: 100.0,
6868        };
6869        let fit = shadows.fit(&view, sun_toward);
6870        let materials = [
6871            Material {
6872                base: [0.6, 0.6, 0.6],
6873                roughness: 0.8,
6874                ..Default::default()
6875            },
6876            Material {
6877                base: [0.8, 0.1, 0.1],
6878                roughness: 0.8,
6879                ..Default::default()
6880            },
6881        ];
6882        let camera = Camera {
6883            view: look_down(4.0),
6884            projection: perspective(1.0),
6885            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
6886            position: [0.0, 4.0, 0.0],
6887        };
6888        let sun = Sun {
6889            direction: sun_toward,
6890            colour: [1.0; 3],
6891            intensity: 3.0,
6892        };
6893        let mut render = |card: MeshHandle, surfaces: &[InstanceSurface]| {
6894            let instances = [
6895                Instance::new(floor, identity(), 0, 1),
6896                Instance::new(card, identity(), 1, 2),
6897            ];
6898            frame.set_surfaces(surfaces).unwrap();
6899            let scene = calm_scene(camera, &instances, &materials, sun);
6900            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
6901            frame
6902                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
6903                .unwrap();
6904            frame.gpu.queue.submit(Some(encoder.finish()));
6905            let ids = read_target(&frame, &frame.targets.ids, 4)
6906                .chunks_exact(4)
6907                .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
6908                .collect();
6909            let depth = read_target(&frame, &frame.targets.depth, 4)
6910                .chunks_exact(4)
6911                .map(|b| f32::from_le_bytes([b[0], b[1], b[2], b[3]]))
6912                .collect();
6913            let colour = frame
6914                .gpu
6915                .readback_rgba16(&frame.targets.hdr)
6916                .unwrap()
6917                .chunks_exact(4)
6918                .map(|p| [half(p[0]), half(p[1]), half(p[2])])
6919                .collect();
6920            Outputs {
6921                ids,
6922                depth,
6923                colour,
6924                atlas: read_atlas(&frame, &shadows),
6925            }
6926        };
6927        let cut = InstanceSurface {
6928            clip: [[1.0, 0.0, 0.0, 0.0], [0.0; 4]],
6929            ..Default::default()
6930        };
6931        let clipped = render(whole, &[InstanceSurface::default(), cut]);
6932        let reference = render(half_card, &[]);
6933        let uncut = render(whole, &[]);
6934        let differ = |a: &[f32], b: &[f32], tolerance: f32| {
6935            a.iter()
6936                .zip(b)
6937                .filter(|(x, y)| (**x - **y).abs() > tolerance)
6938                .count()
6939        };
6940        let id_mismatch = clipped
6941            .ids
6942            .iter()
6943            .zip(&reference.ids)
6944            .filter(|(a, b)| a != b)
6945            .count();
6946        let depth_mismatch = differ(&clipped.depth, &reference.depth, 1e-6);
6947        let colour_mismatch = clipped
6948            .colour
6949            .iter()
6950            .zip(&reference.colour)
6951            .filter(|(a, b)| (0..3).any(|c| (a[c] - b[c]).abs() > 0.02))
6952            .count();
6953        let atlas_mismatch = differ(&clipped.atlas, &reference.atlas, 1e-4);
6954        let atlas_cut = differ(&uncut.atlas, &reference.atlas, 1e-4);
6955        let id_cut = uncut
6956            .ids
6957            .iter()
6958            .zip(&reference.ids)
6959            .filter(|(a, b)| a != b)
6960            .count();
6961        println!(
6962            "clip against a half card: ids {id_mismatch}, depth {depth_mismatch}, colour {colour_mismatch} of {} pixels, shadow {atlas_mismatch} of {} texels; the uncut card differs in {id_cut} ids and {atlas_cut} shadow texels",
6963            clipped.ids.len(),
6964            clipped.atlas.len()
6965        );
6966        assert!(id_cut > 2000 && atlas_cut > 1000);
6967        let seam = size as usize * 2;
6968        assert!(id_mismatch <= seam, "{id_mismatch} ids differ");
6969        assert!(depth_mismatch <= seam, "{depth_mismatch} depths differ");
6970        assert!(
6971            colour_mismatch <= seam * 2,
6972            "{colour_mismatch} colours differ"
6973        );
6974        assert!(
6975            atlas_mismatch * 20 <= atlas_cut,
6976            "{atlas_mismatch} shadow texels differ"
6977        );
6978        let at = |x: f32, z: f32| {
6979            let ndc = transform(multiply(perspective(1.0), look_down(4.0)), [x, 0.0, z, 1.0]);
6980            let px = ((ndc[0] / ndc[3] * 0.5 + 0.5) * size as f32) as usize;
6981            let py = ((0.5 - ndc[1] / ndc[3] * 0.5) * size as f32) as usize;
6982            py * size as usize + px
6983        };
6984        let shown = at(0.6, 0.0);
6985        assert_eq!(clipped.ids[shown], 1);
6986        assert_eq!(uncut.ids[shown], 2);
6987        assert_eq!(clipped.ids[at(-0.6, 0.0)], 2);
6988        let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
6989        let open_floor = luma(clipped.colour[at(1.6, 0.0)]);
6990        let under_cut = luma(clipped.colour[at(0.15, 0.0)]);
6991        let under_card = luma(clipped.colour[at(-1.5, 0.0)]);
6992        println!(
6993            "floor luma: open {open_floor}, under the cut half {under_cut}, in the card's shadow {under_card}"
6994        );
6995        assert!((under_cut - open_floor).abs() < 0.1 * open_floor);
6996        assert!(under_card < 0.6 * open_floor);
6997    }
6998
6999    const RING: u32 = 64;
7000
7001    fn ring_alpha(i: u32, j: u32) -> u8 {
7002        let u = (i as f32 + 0.5) / RING as f32 - 0.5;
7003        let v = (j as f32 + 0.5) / RING as f32 - 0.5;
7004        let r = (u * u + v * v).sqrt();
7005        if (0.25..=0.45).contains(&r) { 255 } else { 0 }
7006    }
7007
7008    fn traced_alpha(uv: [f32; 2]) -> f32 {
7009        let size = RING as f32;
7010        let p = [0, 1].map(|k| (uv[k] * size - 0.5).clamp(0.0, size - 1.0));
7011        let lo = p.map(|value| value.floor() as u32);
7012        let hi = lo.map(|value| (value + 1).min(RING - 1));
7013        let f = [p[0] - p[0].floor(), p[1] - p[1].floor()];
7014        let a = |i: u32, j: u32| f32::from(ring_alpha(i, j)) / 255.0;
7015        let top = a(lo[0], lo[1]) * (1.0 - f[0]) + a(hi[0], lo[1]) * f[0];
7016        let bottom = a(lo[0], hi[1]) * (1.0 - f[0]) + a(hi[0], hi[1]) * f[0];
7017        top * (1.0 - f[1]) + bottom * f[1]
7018    }
7019
7020    #[test]
7021    #[ignore = "needs a GPU; run under pgpu"]
7022    fn content_alpha_cuts_a_ring_out_of_one_quad_in_every_pass() {
7023        use crate::maps::ContentFormat;
7024        use crate::shadow::{Quality, View};
7025        use pfx_materials::ContentLayer;
7026        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7027        let size = 256;
7028        let height = 6.0;
7029        let half_card = 0.6;
7030        let mut frame = Frame::new(gpu, size, size).unwrap();
7031        let floor = quad(
7032            &mut frame,
7033            [
7034                [-4.0, 0.0, -4.0],
7035                [4.0, 0.0, -4.0],
7036                [-4.0, 0.0, 4.0],
7037                [4.0, 0.0, 4.0],
7038            ],
7039        );
7040        let card = quad(
7041            &mut frame,
7042            [
7043                [-half_card, 1.0, -half_card],
7044                [half_card, 1.0, -half_card],
7045                [-half_card, 1.0, half_card],
7046                [half_card, 1.0, half_card],
7047            ],
7048        );
7049        let texels: Vec<u8> = (0..RING * RING)
7050            .flat_map(|index| [230, 40, 30, ring_alpha(index % RING, index / RING)])
7051            .collect();
7052        frame
7053            .set_content(0, RING, RING, ContentFormat::Srgb8)
7054            .unwrap();
7055        frame.write_content(0, [0, 0, RING, RING], &texels).unwrap();
7056        let mut shadows = Shadows::new(
7057            &frame.gpu.device,
7058            Quality {
7059                resolution: 1024,
7060                ..Default::default()
7061            },
7062        );
7063        let sun_toward = [0.8, 0.6, 0.0];
7064        let view = View {
7065            eye: [0.0, height, 0.0],
7066            forward: [0.0, -1.0, 0.0],
7067            up: [0.0, 0.0, -1.0],
7068            fov_y: 50.0_f32.to_radians(),
7069            aspect: 1.0,
7070            near: 0.1,
7071            far: 100.0,
7072        };
7073        let fit = shadows.fit(&view, sun_toward);
7074        let materials = [
7075            Material {
7076                base: [0.6, 0.6, 0.6],
7077                roughness: 0.8,
7078                ..Default::default()
7079            },
7080            Material {
7081                base: [0.2, 0.2, 0.8],
7082                roughness: 0.8,
7083                content_layer: ContentLayer {
7084                    slot: 0,
7085                    ..Default::default()
7086                },
7087                ..Default::default()
7088            },
7089        ];
7090        let camera = Camera {
7091            view: look_down(height),
7092            projection: perspective(1.0),
7093            previous_view_projection: multiply(perspective(1.0), look_down(height)),
7094            position: [0.0, height, 0.0],
7095        };
7096        let sun = Sun {
7097            direction: sun_toward,
7098            colour: [1.0; 3],
7099            intensity: 3.0,
7100        };
7101        let mut render = |cutout: bool| {
7102            let instances = [
7103                Instance::new(floor, identity(), 0, 1),
7104                Instance::new(card, identity(), 1, 2),
7105            ];
7106            let surfaces = [
7107                InstanceSurface::default(),
7108                InstanceSurface {
7109                    cutout,
7110                    ..Default::default()
7111                },
7112            ];
7113            frame.set_surfaces(&surfaces).unwrap();
7114            let scene = calm_scene(camera, &instances, &materials, sun);
7115            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
7116            frame
7117                .encode(&scene, &mut encoder, Some((&mut shadows, &fit)), None)
7118                .unwrap();
7119            frame.gpu.queue.submit(Some(encoder.finish()));
7120            let ids: Vec<u32> = read_target(&frame, &frame.targets.ids, 4)
7121                .chunks_exact(4)
7122                .map(|b| u32::from_le_bytes([b[0], b[1], b[2], b[3]]))
7123                .collect();
7124            let colour: Vec<[f32; 3]> = frame
7125                .gpu
7126                .readback_rgba16(&frame.targets.hdr)
7127                .unwrap()
7128                .chunks_exact(4)
7129                .map(|p| [half(p[0]), half(p[1]), half(p[2])])
7130                .collect();
7131            (ids, colour, frame.opaque_drawn().to_vec())
7132        };
7133        let (ids, colour, drawn) = render(true);
7134        let (whole_ids, whole_colour, whole_drawn) = render(false);
7135        assert!(drawn.iter().any(|used| used.content_cutout));
7136        assert!(whole_drawn.iter().all(|used| !used.content_cutout));
7137        let at = |x: f32, y: f32, z: f32| {
7138            let ndc = transform(
7139                multiply(perspective(1.0), look_down(height)),
7140                [x, y, z, 1.0],
7141            );
7142            let px = ((ndc[0] / ndc[3] * 0.5 + 0.5) * size as f32) as usize;
7143            let py = ((0.5 - ndc[1] / ndc[3] * 0.5) * size as f32) as usize;
7144            py * size as usize + px
7145        };
7146        assert_eq!(ids[at(0.0, 1.0, 0.0)], 1, "the hole shows the floor");
7147        assert_eq!(ids[at(0.42, 1.0, 0.0)], 2, "the ring shows");
7148        assert_eq!(
7149            ids[at(0.55, 1.0, 0.55)],
7150            1,
7151            "the clear corner shows the floor"
7152        );
7153        assert_eq!(whole_ids[at(0.0, 1.0, 0.0)], 2);
7154        let f = 1.0 / (50.0_f32.to_radians() * 0.5).tan();
7155        let mut checked = 0;
7156        let mut wrong = 0;
7157        for py in 0..size {
7158            for px in 0..size {
7159                let ndc = [
7160                    (px as f32 + 0.5) / size as f32 * 2.0 - 1.0,
7161                    1.0 - (py as f32 + 0.5) / size as f32 * 2.0,
7162                ];
7163                let x = ndc[0] * (height - 1.0) / f;
7164                let z = -ndc[1] * (height - 1.0) / f;
7165                if x.abs() > half_card || z.abs() > half_card {
7166                    continue;
7167                }
7168                let uv = [
7169                    (x + half_card) / (2.0 * half_card),
7170                    (z + half_card) / (2.0 * half_card),
7171                ];
7172                let alpha = traced_alpha(uv);
7173                if (alpha - 0.5).abs() < 0.05 {
7174                    continue;
7175                }
7176                checked += 1;
7177                let shown = ids[(py * size + px) as usize] == 2;
7178                if shown != (alpha >= 0.5) {
7179                    wrong += 1;
7180                }
7181            }
7182        }
7183        println!("{wrong} of {checked} card pixels disagree with the tracer's cutout");
7184        assert!(checked > 3000, "{checked}");
7185        assert_eq!(wrong, 0);
7186        let luma = |c: [f32; 3]| 0.2126 * c[0] + 0.7152 * c[1] + 0.0722 * c[2];
7187        let shift = sun_toward[0] / sun_toward[1];
7188        let shadow_of = |x: f32, z: f32| at(x - shift, 0.0, z);
7189        let open = luma(colour[at(1.5, 0.0, -1.5)]);
7190        let through_hole = luma(colour[shadow_of(0.0, 0.0)]);
7191        let under_ring = luma(colour[shadow_of(-0.42, 0.0)]);
7192        let past_corner = luma(colour[shadow_of(-0.55, 0.55)]);
7193        let whole_hole = luma(whole_colour[shadow_of(0.0, 0.0)]);
7194        println!(
7195            "floor luma: open {open}, through the hole {through_hole}, under the ring {under_ring}, past the clear corner {past_corner}; the uncut card shades the hole's spot to {whole_hole}"
7196        );
7197        assert!((through_hole - open).abs() < 0.05 * open);
7198        assert!((past_corner - open).abs() < 0.05 * open);
7199        assert!(under_ring < 0.6 * open);
7200        assert!(whole_hole < 0.6 * open);
7201    }
7202
7203    #[test]
7204    #[ignore = "needs a GPU; run under pgpu"]
7205    fn content_updates_keep_the_arrays_and_regenerate_mips() {
7206        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7207        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7208        let paper = pfx_load::Image {
7209            width: 4,
7210            height: 4,
7211            space: pfx_load::ColorSpace::Srgb,
7212            pixels: pfx_load::Pixels::Eight([200, 190, 170, 255].repeat(16)),
7213        };
7214        frame
7215            .set_maps(&MapImages {
7216                base: std::slice::from_ref(&paper),
7217                ..Default::default()
7218            })
7219            .unwrap();
7220        let arrays = [
7221            frame.maps.base.texture.clone(),
7222            frame.maps.normal.texture.clone(),
7223            frame.maps.roughness.texture.clone(),
7224            frame.maps.metal.texture.clone(),
7225        ];
7226        frame
7227            .set_content(3, 1280, 720, ContentFormat::Srgb8)
7228            .unwrap();
7229        let video = frame.content().get(3).unwrap().texture.clone();
7230        assert_eq!(frame.content().get(3).unwrap().mips, 11);
7231        let started = std::time::Instant::now();
7232        for (index, colour) in [[255u8, 0, 0, 255], [0, 0, 255, 255], [0, 255, 0, 255]]
7233            .iter()
7234            .enumerate()
7235        {
7236            frame
7237                .write_content(3, [0, 0, 1280, 720], &colour.repeat(1280 * 720))
7238                .unwrap();
7239            frame
7240                .set_content(3, 1280, 720, ContentFormat::Srgb8)
7241                .unwrap();
7242            let content = frame.content();
7243            let slot = content.get(3).unwrap();
7244            assert_eq!(slot.texture, video);
7245            assert_eq!(slot.writes, index as u64 + 1);
7246            let last = slot.mips - 1;
7247            let buffer = frame.gpu.device.create_buffer(&wgpu::BufferDescriptor {
7248                label: Some("smallest mip"),
7249                size: 256,
7250                usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
7251                mapped_at_creation: false,
7252            });
7253            let mut encoder = frame.gpu.device.create_command_encoder(&Default::default());
7254            encoder.copy_texture_to_buffer(
7255                wgpu::TexelCopyTextureInfo {
7256                    texture: &slot.texture,
7257                    mip_level: last,
7258                    origin: wgpu::Origin3d::ZERO,
7259                    aspect: wgpu::TextureAspect::All,
7260                },
7261                wgpu::TexelCopyBufferInfo {
7262                    buffer: &buffer,
7263                    layout: wgpu::TexelCopyBufferLayout {
7264                        offset: 0,
7265                        bytes_per_row: Some(256),
7266                        rows_per_image: Some(1),
7267                    },
7268                },
7269                wgpu::Extent3d {
7270                    width: 1,
7271                    height: 1,
7272                    depth_or_array_layers: 1,
7273                },
7274            );
7275            frame.gpu.queue.submit(Some(encoder.finish()));
7276            buffer.slice(..).map_async(wgpu::MapMode::Read, |_| {});
7277            frame
7278                .gpu
7279                .device
7280                .poll(wgpu::PollType::wait_indefinitely())
7281                .unwrap();
7282            let texel = buffer.slice(..).get_mapped_range().expect("a mapped range")[..4].to_vec();
7283            assert_eq!(texel, colour.to_vec(), "frame {index}: the smallest mip");
7284        }
7285        println!(
7286            "three 1280x720 frames written with mips in {:.2} ms",
7287            started.elapsed().as_secs_f64() * 1000.0
7288        );
7289        assert_eq!(frame.maps.base.texture, arrays[0]);
7290        assert_eq!(frame.maps.normal.texture, arrays[1]);
7291        assert_eq!(frame.maps.roughness.texture, arrays[2]);
7292        assert_eq!(frame.maps.metal.texture, arrays[3]);
7293        assert!(frame.write_content(2, [0, 0, 1, 1], &[0; 4]).is_err());
7294        assert!(frame.write_content(3, [1279, 0, 2, 1], &[0; 8]).is_err());
7295        assert!(frame.set_content(4, 4, 4, ContentFormat::Linear16).is_err());
7296        frame
7297            .set_content(3, 640, 360, ContentFormat::Srgb8)
7298            .unwrap();
7299        assert_ne!(frame.content().get(3).unwrap().texture, video);
7300    }
7301
7302    #[test]
7303    #[ignore = "needs a GPU; run under pgpu"]
7304    fn screen_content_emits_through_the_frame() {
7305        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7306        let size = 64;
7307        let mut frame = Frame::new(gpu, size, size).unwrap();
7308        let screen = quad(
7309            &mut frame,
7310            [
7311                [-2.0, 0.0, -2.0],
7312                [2.0, 0.0, -2.0],
7313                [-2.0, 0.0, 2.0],
7314                [2.0, 0.0, 2.0],
7315            ],
7316        );
7317        frame.set_content(0, 4, 4, ContentFormat::Srgb8).unwrap();
7318        frame
7319            .write_content(0, [0, 0, 4, 4], &[0u8, 255, 0, 255].repeat(16))
7320            .unwrap();
7321        let dark = Material {
7322            base: [0.01; 3],
7323            roughness: 0.3,
7324            ..Default::default()
7325        };
7326        let lit = Material {
7327            content_layer: pfx_materials::ContentLayer::from_kind(
7328                pfx_materials::Content::Screen,
7329                1.0,
7330                0,
7331                &pfx_materials::ContentLook {
7332                    screen_gain: 1.5,
7333                    ..Default::default()
7334                },
7335            ),
7336            ..dark
7337        };
7338        let camera = Camera {
7339            view: look_down(4.0),
7340            projection: perspective(1.0),
7341            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
7342            position: [0.0, 4.0, 0.0],
7343        };
7344        let sun = Sun {
7345            direction: [0.0, 1.0, 0.0],
7346            colour: [1.0; 3],
7347            intensity: 0.0,
7348        };
7349        let mut centre = |materials: &[Material]| {
7350            let instances = [Instance::new(screen, identity(), 0, 1)];
7351            frame
7352                .render(&calm_scene(camera, &instances, materials, sun))
7353                .unwrap();
7354            let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
7355            let i = ((size / 2) * size + size / 2) as usize * 4;
7356            [half(hdr[i]), half(hdr[i + 1]), half(hdr[i + 2])]
7357        };
7358        let off = centre(&[dark]);
7359        let on = centre(&[lit]);
7360        println!("screen off {off:?}, on {on:?}");
7361        assert!((on[1] - off[1] - 1.5).abs() < 0.02);
7362        assert!((on[0] - off[0]).abs() < 0.01 && (on[2] - off[2]).abs() < 0.01);
7363    }
7364
7365    fn screen_materials() -> [Material; 2] {
7366        let dark = Material {
7367            base: [0.01; 3],
7368            roughness: 0.3,
7369            ..Default::default()
7370        };
7371        let lit = Material {
7372            content_layer: pfx_materials::ContentLayer::from_kind(
7373                pfx_materials::Content::Screen,
7374                1.0,
7375                0,
7376                &pfx_materials::ContentLook {
7377                    screen_gain: 1.5,
7378                    ..Default::default()
7379                },
7380            ),
7381            ..dark
7382        };
7383        [dark, lit]
7384    }
7385
7386    fn green_screen(frame: &mut Frame) {
7387        frame.set_content(0, 4, 4, ContentFormat::Srgb8).unwrap();
7388        frame
7389            .write_content(0, [0, 0, 4, 4], &[0u8, 255, 0, 255].repeat(16))
7390            .unwrap();
7391    }
7392
7393    fn hdr_green(frame: &Frame) -> Vec<f32> {
7394        frame
7395            .gpu
7396            .readback_rgba16(&frame.targets.hdr)
7397            .unwrap()
7398            .chunks_exact(4)
7399            .map(|pixel| half(pixel[1]))
7400            .collect()
7401    }
7402
7403    #[test]
7404    fn content_faces_choose_the_side_that_shows() {
7405        assert!(ContentFace::Both.shows(true) && ContentFace::Both.shows(false));
7406        assert!(ContentFace::Front.shows(true) && !ContentFace::Front.shows(false));
7407        assert!(!ContentFace::Back.shows(true) && ContentFace::Back.shows(false));
7408        assert_eq!(ContentFace::default(), ContentFace::Both);
7409        assert_eq!(InstanceSurface::default().face, ContentFace::Both);
7410        assert_eq!(ContentFace::Both.code(), 0);
7411        assert_eq!(ContentFace::Front.code(), 1);
7412        assert_eq!(ContentFace::Back.code(), 2);
7413    }
7414
7415    #[test]
7416    #[ignore = "needs a GPU; run under pgpu"]
7417    fn two_sided_content_shows_on_the_chosen_face_only() {
7418        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7419        let size = 64;
7420        let mut frame = Frame::new(gpu, size, size).unwrap();
7421        let screen = quad(
7422            &mut frame,
7423            [
7424                [-2.0, 0.0, -2.0],
7425                [2.0, 0.0, -2.0],
7426                [-2.0, 0.0, 2.0],
7427                [2.0, 0.0, 2.0],
7428            ],
7429        );
7430        green_screen(&mut frame);
7431        let materials = screen_materials();
7432        let camera = Camera {
7433            view: look_down(4.0),
7434            projection: perspective(1.0),
7435            previous_view_projection: multiply(perspective(1.0), look_down(4.0)),
7436            position: [0.0, 4.0, 0.0],
7437        };
7438        let sun = Sun {
7439            direction: [0.0, 1.0, 0.0],
7440            colour: [1.0; 3],
7441            intensity: 0.0,
7442        };
7443        let mut flipped = identity();
7444        flipped[1][1] = -1.0;
7445        flipped[2][2] = -1.0;
7446        let centre = (size / 2 * size + size / 2) as usize;
7447        let mut shown = |material: u32, model: Matrix, surface: Option<InstanceSurface>| {
7448            let mut instance = Instance::new(screen, model, material, 1);
7449            instance.two_sided = true;
7450            frame
7451                .set_surfaces(&surface.map_or_else(Vec::new, |surface| vec![surface]))
7452                .unwrap();
7453            frame
7454                .render(&calm_scene(camera, &[instance], &materials, sun))
7455                .unwrap();
7456            hdr_green(&frame)
7457        };
7458        let off = shown(0, identity(), None)[centre];
7459        let gain = 1.5;
7460        let faced = |face| {
7461            Some(InstanceSurface {
7462                face,
7463                ..Default::default()
7464            })
7465        };
7466        let default_front = shown(1, identity(), None);
7467        let default_back = shown(1, flipped, None);
7468        assert!((default_front[centre] - off - gain).abs() < 0.02);
7469        assert!((default_back[centre] - off - gain).abs() < 0.02);
7470        for (face, front_shows, back_shows) in [
7471            (ContentFace::Both, true, true),
7472            (ContentFace::Front, true, false),
7473            (ContentFace::Back, false, true),
7474        ] {
7475            let upright = shown(1, identity(), faced(face));
7476            let turned = shown(1, flipped, faced(face));
7477            for (image, expected) in [(&upright, front_shows), (&turned, back_shows)] {
7478                let lifted = image[centre] - off;
7479                if expected {
7480                    assert!((lifted - gain).abs() < 0.02, "{face:?}: {lifted}");
7481                } else {
7482                    assert!(lifted.abs() < 0.02, "{face:?}: {lifted}");
7483                }
7484            }
7485            if face == ContentFace::Both {
7486                assert_eq!(upright, default_front);
7487                assert_eq!(turned, default_back);
7488            }
7489        }
7490    }
7491
7492    #[test]
7493    #[ignore = "needs a GPU; run under pgpu"]
7494    fn front_content_keeps_a_rolled_strip_outside_blank() {
7495        use pfx_geom::strip::{Roller, Strip};
7496        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7497        let size = 128;
7498        let mut frame = Frame::new(gpu, size, size).unwrap();
7499        let strip = Strip {
7500            length: 3.0,
7501            width: 1.2,
7502        };
7503        let mesh = strip.mesh(0.12, 0.002, None, 4);
7504        let handle = frame
7505            .upload_mesh(MeshData {
7506                positions: &mesh.positions,
7507                normals: &mesh.normals,
7508                tangents: &mesh.tangents,
7509                uvs: &mesh.uvs,
7510                uvs1: None,
7511                alpha: None,
7512                indices: &mesh.indices,
7513            })
7514            .unwrap();
7515        green_screen(&mut frame);
7516        let materials = screen_materials();
7517        let mut instance = Instance::new(handle, identity(), 1, 1);
7518        instance.deformer = DeformerId(0);
7519        let camera = Camera {
7520            view: look_down(6.0),
7521            projection: perspective(1.0),
7522            previous_view_projection: multiply(perspective(1.0), look_down(6.0)),
7523            position: [0.0, 6.0, 0.0],
7524        };
7525        let sun = Sun {
7526            direction: [0.0, 1.0, 0.0],
7527            colour: [1.0; 3],
7528            intensity: 0.0,
7529        };
7530        let roller = Roller {
7531            at: 1.2,
7532            core: 0.12,
7533            thickness: 0.01,
7534        };
7535        let state = RollerFrame {
7536            at: roller.at,
7537            outer: roller.outer(&strip),
7538            thickness: roller.thickness,
7539        };
7540        let deformers = [Deformer::Roller {
7541            current: state,
7542            previous: state,
7543        }];
7544        let instances = [instance];
7545        let mut lit = |face: ContentFace| {
7546            frame
7547                .set_surfaces(&[InstanceSurface {
7548                    face,
7549                    ..Default::default()
7550                }])
7551                .unwrap();
7552            let scene = Scene {
7553                deformers: &deformers,
7554                ..calm_scene(camera, &instances, &materials, sun)
7555            };
7556            frame.render(&scene).unwrap();
7557            hdr_green(&frame)
7558                .into_iter()
7559                .map(|green| green > 0.8)
7560                .collect::<Vec<bool>>()
7561        };
7562        let both = lit(ContentFace::Both);
7563        let front = lit(ContentFace::Front);
7564        let back = lit(ContentFace::Back);
7565        let count = |image: &[bool]| image.iter().filter(|&&on| on).count();
7566        println!(
7567            "printed pixels: both {}, front {}, back {}",
7568            count(&both),
7569            count(&front),
7570            count(&back)
7571        );
7572        assert!(count(&front) > 0 && count(&back) > 0);
7573        assert!(count(&front) < count(&both));
7574        assert!(count(&back) < count(&both));
7575        for ((both, front), back) in both.iter().zip(&front).zip(&back) {
7576            assert_eq!(*both, *front || *back);
7577            assert!(!(*front && *back));
7578        }
7579    }
7580
7581    fn floor_quad(frame: &mut Frame, centre: [f32; 2], half: f32) -> MeshHandle {
7582        quad(frame, floor_corners(centre, half))
7583    }
7584
7585    fn floor_corners(centre: [f32; 2], half: f32) -> [[f32; 3]; 4] {
7586        [
7587            [centre[0] - half, 0.0, centre[1] - half],
7588            [centre[0] + half, 0.0, centre[1] - half],
7589            [centre[0] - half, 0.0, centre[1] + half],
7590            [centre[0] + half, 0.0, centre[1] + half],
7591        ]
7592    }
7593
7594    fn quad_data(corners: &[[f32; 3]; 4]) -> MeshData<'_> {
7595        MeshData {
7596            positions: corners,
7597            normals: &[[0.0, 1.0, 0.0]; 4],
7598            tangents: &[[1.0, 0.0, 0.0, 1.0]; 4],
7599            uvs: &[[0.0, 0.0], [1.0, 0.0], [0.0, 1.0], [1.0, 1.0]],
7600            uvs1: None,
7601            alpha: None,
7602            indices: &[0, 2, 1, 1, 2, 3],
7603        }
7604    }
7605
7606    fn top_camera() -> Camera {
7607        Camera {
7608            view: look_down(8.0),
7609            projection: perspective(1.0),
7610            previous_view_projection: multiply(perspective(1.0), look_down(8.0)),
7611            position: [0.0, 8.0, 0.0],
7612        }
7613    }
7614
7615    fn lit_pixels(frame: &mut Frame, instances: &[Instance]) -> Result<usize, String> {
7616        let materials = [Material {
7617            base: [0.8; 3],
7618            roughness: 0.5,
7619            ..Default::default()
7620        }];
7621        let sun = Sun {
7622            direction: [0.0, 1.0, 0.0],
7623            colour: [1.0; 3],
7624            intensity: 3.0,
7625        };
7626        frame.render(&calm_scene(top_camera(), instances, &materials, sun))?;
7627        let hdr = frame.gpu.readback_rgba16(&frame.targets.hdr).unwrap();
7628        Ok(hdr
7629            .chunks_exact(4)
7630            .filter(|pixel| half(pixel[1]) > 0.1)
7631            .count())
7632    }
7633
7634    #[test]
7635    fn mesh_handles_pack_a_slot_and_a_generation() {
7636        let first = MeshHandle(7);
7637        assert_eq!((first.slot(), first.generation()), (7, 0));
7638        let later = MeshHandle::pack(7, 3);
7639        assert_eq!((later.slot(), later.generation()), (7, 3));
7640        assert_ne!(first, later);
7641        assert_eq!(
7642            MeshHandle::pack(MESH_SLOT_MASK as usize, 1).slot(),
7643            MESH_SLOT_MASK as usize
7644        );
7645    }
7646
7647    #[test]
7648    #[ignore = "needs a GPU; run under pgpu"]
7649    fn released_mesh_stops_drawing_and_the_rest_still_render() {
7650        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7651        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7652        let big = floor_quad(&mut frame, [0.0, 0.0], 2.0);
7653        let small = floor_quad(&mut frame, [3.0, 3.0], 0.5);
7654        let both = [
7655            Instance::new(big, identity(), 0, 1),
7656            Instance::new(small, identity(), 0, 2),
7657        ];
7658        let before = lit_pixels(&mut frame, &both).unwrap();
7659        frame.release_mesh(big).unwrap();
7660        let after = lit_pixels(&mut frame, &both[1..]).unwrap();
7661        println!("lit pixels before {before}, after release {after}");
7662        assert!(before > after * 4);
7663        assert!(after > 0);
7664        assert_eq!(frame.mesh_stats().live, 1);
7665    }
7666
7667    #[test]
7668    #[ignore = "needs a GPU; run under pgpu"]
7669    fn stale_handles_are_refused_and_never_alias_a_newer_mesh() {
7670        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7671        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7672        let old = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7673        frame.release_mesh(old).unwrap();
7674        assert!(frame.release_mesh(old).is_err());
7675        let corners = floor_corners([0.0, 0.0], 1.0);
7676        assert!(frame.replace_mesh(old, quad_data(&corners)).is_err());
7677        assert!(frame.shadow_mesh(old).is_none());
7678        let fresh = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7679        assert_eq!(fresh.slot(), old.slot());
7680        assert_ne!(fresh, old);
7681        assert!(frame.shadow_mesh(fresh).is_some());
7682        assert!(frame.shadow_mesh(old).is_none());
7683        assert!(frame.release_mesh(old).is_err());
7684        assert!(frame.replace_mesh(old, quad_data(&corners)).is_err());
7685        assert!(
7686            frame
7687                .replace_mesh(MeshHandle(900), quad_data(&corners))
7688                .is_err()
7689        );
7690        assert_eq!(frame.mesh_stats().live, 1);
7691        assert!(frame.release_mesh(fresh).is_ok());
7692    }
7693
7694    #[test]
7695    #[ignore = "needs a GPU; run under pgpu"]
7696    fn instance_with_a_released_mesh_is_an_error_from_render() {
7697        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7698        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7699        let kept = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7700        let dropped = floor_quad(&mut frame, [2.0, 2.0], 1.0);
7701        let instances = [
7702            Instance::new(kept, identity(), 0, 1),
7703            Instance::new(dropped, identity(), 0, 2),
7704        ];
7705        assert!(lit_pixels(&mut frame, &instances).is_ok());
7706        frame.release_mesh(dropped).unwrap();
7707        let error = lit_pixels(&mut frame, &instances).unwrap_err();
7708        assert!(error.contains("released"), "{error}");
7709        let replaced = floor_quad(&mut frame, [-2.0, -2.0], 1.0);
7710        assert_eq!(replaced.slot(), dropped.slot());
7711        let error = lit_pixels(&mut frame, &instances).unwrap_err();
7712        assert!(error.contains("released"), "{error}");
7713        assert!(lit_pixels(&mut frame, &instances[..1]).is_ok());
7714    }
7715
7716    #[test]
7717    #[ignore = "needs a GPU; run under pgpu"]
7718    fn replacing_a_mesh_keeps_the_handle_and_reuses_buffers_that_fit() {
7719        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7720        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7721        let handle = floor_quad(&mut frame, [0.0, 0.0], 2.0);
7722        let instances = [Instance::new(handle, identity(), 0, 1)];
7723        let first = lit_pixels(&mut frame, &instances).unwrap();
7724        let buffer = frame.shadow_mesh(handle).unwrap().0.clone();
7725
7726        let smaller = floor_corners([0.0, 0.0], 1.0);
7727        frame.replace_mesh(handle, quad_data(&smaller)).unwrap();
7728        let same_size = lit_pixels(&mut frame, &instances).unwrap();
7729        println!("lit pixels {first} then {same_size} with the same buffers");
7730        assert!(same_size * 3 < first * 2 && same_size > 0);
7731        assert!(*frame.shadow_mesh(handle).unwrap().0 == buffer);
7732        assert_eq!(frame.mesh_stats().live, 1);
7733
7734        let large = sphere(&mut frame);
7735        let mut positions = Vec::new();
7736        let mut normals = Vec::new();
7737        let mut tangents = Vec::new();
7738        let mut uvs = Vec::new();
7739        let mut indices = Vec::new();
7740        for row in 0..10u32 {
7741            for column in 0..10u32 {
7742                let at = [
7743                    column as f32 * 4.0 / 9.0 - 2.0,
7744                    0.0,
7745                    row as f32 * 4.0 / 9.0 - 2.0,
7746                ];
7747                positions.push(at);
7748                normals.push([0.0, 1.0, 0.0]);
7749                tangents.push([1.0, 0.0, 0.0, 1.0]);
7750                uvs.push([column as f32 / 9.0, row as f32 / 9.0]);
7751                if row < 9 && column < 9 {
7752                    let a = row * 10 + column;
7753                    indices.extend_from_slice(&[a, a + 10, a + 1, a + 1, a + 10, a + 11]);
7754                }
7755            }
7756        }
7757        frame.release_mesh(large).unwrap();
7758        frame
7759            .replace_mesh(
7760                handle,
7761                MeshData {
7762                    positions: &positions,
7763                    normals: &normals,
7764                    tangents: &tangents,
7765                    uvs: &uvs,
7766                    uvs1: None,
7767                    alpha: None,
7768                    indices: &indices,
7769                },
7770            )
7771            .unwrap();
7772        let grown = lit_pixels(&mut frame, &instances).unwrap();
7773        println!(
7774            "lit pixels {grown} after growing to {} vertices",
7775            positions.len()
7776        );
7777        assert!(grown.abs_diff(first) * 20 < first);
7778        assert!(*frame.shadow_mesh(handle).unwrap().0 != buffer);
7779        assert_eq!(frame.shadow_mesh(handle).unwrap().2, indices.len() as u32);
7780        assert_eq!(frame.mesh_stats().live, 1);
7781
7782        let tiny = floor_corners([0.0, 0.0], 0.5);
7783        frame.replace_mesh(handle, quad_data(&tiny)).unwrap();
7784        let shrunk = lit_pixels(&mut frame, &instances).unwrap();
7785        assert!(shrunk < same_size && shrunk > 0);
7786        assert_eq!(frame.shadow_mesh(handle).unwrap().2, 6);
7787        let broken = [[0.0; 3]; 3];
7788        let error = frame.replace_mesh(
7789            handle,
7790            MeshData {
7791                positions: &broken,
7792                normals: &broken,
7793                tangents: &[[0.0; 4]; 3],
7794                uvs: &[[0.0; 2]; 3],
7795                uvs1: None,
7796                alpha: None,
7797                indices: &[0, 1, 7],
7798            },
7799        );
7800        assert!(error.is_err());
7801        assert_eq!(lit_pixels(&mut frame, &instances).unwrap(), shrunk);
7802    }
7803
7804    #[test]
7805    #[ignore = "needs a GPU; run under pgpu"]
7806    fn upload_and_release_cycles_keep_the_buffers_bounded() {
7807        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7808        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7809        let keep = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7810        let corners = floor_corners([0.0, 0.0], 1.0);
7811        let mut seen = std::collections::HashSet::new();
7812        for cycle in 0..1000 {
7813            let handle = frame.upload_mesh(quad_data(&corners)).unwrap();
7814            assert!(seen.insert(handle), "cycle {cycle} reused a handle");
7815            let stats = frame.mesh_stats();
7816            assert_eq!((stats.live, stats.slots, stats.buffers), (2, 2, 6));
7817            frame.release_mesh(handle).unwrap();
7818        }
7819        let stats = frame.mesh_stats();
7820        println!("after 1000 cycles: {stats:?}");
7821        assert_eq!((stats.live, stats.slots, stats.buffers), (1, 2, 3));
7822        println!(
7823            "allocator: {:?}",
7824            frame
7825                .gpu
7826                .device
7827                .generate_allocator_report()
7828                .map(|report| (report.total_allocated_bytes, report.total_reserved_bytes))
7829        );
7830        for _ in 0..5000 {
7831            let handle = frame.upload_mesh(quad_data(&corners)).unwrap();
7832            frame.release_mesh(handle).unwrap();
7833        }
7834        let stats = frame.mesh_stats();
7835        println!("after 6000 cycles: {stats:?}");
7836        assert_eq!(stats.live, 1);
7837        assert!(stats.slots <= 4);
7838        assert!(frame.shadow_mesh(keep).is_some());
7839    }
7840
7841    #[test]
7842    #[ignore = "needs a GPU; run under pgpu"]
7843    fn replace_cycles_count_buffers_by_their_own_life() {
7844        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7845        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7846        let start = frame.mesh_stats().buffers;
7847        let keep = floor_quad(&mut frame, [0.0, 0.0], 1.0);
7848        assert_eq!(frame.mesh_stats().buffers, start + 3);
7849        let grid = |cells: u32| {
7850            let side = cells + 1;
7851            let mut positions = Vec::new();
7852            let mut indices = Vec::new();
7853            for row in 0..side {
7854                for column in 0..side {
7855                    positions.push([column as f32, 0.0, row as f32]);
7856                    if row < cells && column < cells {
7857                        let a = row * side + column;
7858                        indices.extend_from_slice(&[
7859                            a,
7860                            a + side,
7861                            a + 1,
7862                            a + 1,
7863                            a + side,
7864                            a + side + 1,
7865                        ]);
7866                    }
7867                }
7868            }
7869            let count = positions.len();
7870            (
7871                positions,
7872                vec![[0.0, 1.0, 0.0]; count],
7873                vec![[1.0, 0.0, 0.0, 1.0]; count],
7874                vec![[0.0, 0.0]; count],
7875                indices,
7876            )
7877        };
7878        let handle = floor_quad(&mut frame, [2.0, 2.0], 1.0);
7879        let mut most = 0;
7880        for cycle in 0..1000u32 {
7881            let cells = 1 + (cycle * 7) % 23;
7882            let (positions, normals, tangents, uvs, indices) = grid(cells);
7883            frame
7884                .replace_mesh(
7885                    handle,
7886                    MeshData {
7887                        positions: &positions,
7888                        normals: &normals,
7889                        tangents: &tangents,
7890                        uvs: &uvs,
7891                        uvs1: None,
7892                        alpha: None,
7893                        indices: &indices,
7894                    },
7895                )
7896                .unwrap();
7897            let stats = frame.mesh_stats();
7898            most = most.max(stats.buffers);
7899            assert_eq!(stats.live, 2);
7900            assert_eq!(stats.buffers, start + 6, "cycle {cycle} with {cells} cells");
7901        }
7902        frame.release_mesh(handle).unwrap();
7903        let stats = frame.mesh_stats();
7904        println!(
7905            "buffers: start {start}, most {most}, after release {}",
7906            stats.buffers
7907        );
7908        assert_eq!(stats.buffers, start + 3);
7909        frame.release_mesh(keep).unwrap();
7910        assert_eq!(frame.mesh_stats().buffers, start);
7911    }
7912
7913    #[test]
7914    #[ignore = "needs a GPU; run under pgpu"]
7915    fn releasing_a_swaying_mesh_frees_its_sway_range_for_the_next() {
7916        let gpu = pollster::block_on(Gpu::headless()).unwrap();
7917        let mut frame = Frame::new(gpu, 64, 64).unwrap();
7918        let corners = floor_corners([0.0, 0.0], 1.0);
7919        let sway = [SwayVertex {
7920            pivot: [0.0; 3],
7921            level: 1.0,
7922            stiffness: 0.5,
7923        }; 4];
7924        let first = frame
7925            .upload_swaying_mesh(quad_data(&corners), &sway)
7926            .unwrap();
7927        let second = frame
7928            .upload_swaying_mesh(quad_data(&corners), &sway)
7929            .unwrap();
7930        assert_eq!(frame.sway_data.len(), 8);
7931        assert!(frame.swaying(first));
7932        assert!(frame.replace_mesh(first, quad_data(&corners)).is_err());
7933        frame.release_mesh(first).unwrap();
7934        assert!(!frame.swaying(first));
7935        let third = frame
7936            .upload_swaying_mesh(quad_data(&corners), &sway)
7937            .unwrap();
7938        assert_eq!(frame.sway_data.len(), 8);
7939        assert!(frame.swaying(third) && frame.swaying(second));
7940        frame.release_mesh(second).unwrap();
7941        frame.release_mesh(third).unwrap();
7942        assert_eq!(frame.sway_free.0, vec![0..8]);
7943    }
7944}