Skip to main content

pfx_trace/
gpu.rs

1use bytemuck::{Pod, Zeroable};
2use pfx_core::daylight::{Daylight, REFERENCE_HOUR};
3use pfx_gpu::pace::{Pacer, Slice, Stats, Turned, Turns, Work};
4use pfx_gpu::{Gpu, GpuRefusal, GpuShortfall, OffscreenTarget, trace_floor};
5use pfx_load::Sky;
6use pfx_load::scene::TraceSettings;
7use pfx_materials::{Content, ContentLayer, Material, PackedMaterial};
8use wgpu::util::DeviceExt;
9
10use crate::bvh::{Bvh, Triangle};
11use crate::detail::{Bounces, Detail, InstanceSurface, Lens, PlumeLook, TriangleDetail};
12use crate::lights::{Candidate, MAX_EMITTERS, MAX_LIGHTS, Source, build_emitters, emits};
13use crate::shapes::Shape;
14use crate::sky::{ENVIRONMENT_WGSL, Environment, SkyCdf};
15
16#[repr(C)]
17#[derive(Clone, Copy, Pod, Zeroable)]
18struct GpuTriangle {
19    a: [f32; 4],
20    b: [f32; 4],
21    c: [f32; 4],
22    na: [f32; 4],
23    nb: [f32; 4],
24    nc: [f32; 4],
25}
26
27#[repr(C)]
28#[derive(Clone, Copy, Pod, Zeroable)]
29struct GpuSurface {
30    clip0: [f32; 4],
31    clip1: [f32; 4],
32    transform: [f32; 4],
33    crop: [f32; 4],
34}
35
36impl From<InstanceSurface> for GpuSurface {
37    fn from(surface: InstanceSurface) -> Self {
38        Self {
39            clip0: surface.clip[0],
40            clip1: surface.clip[1],
41            transform: [
42                surface.uv_offset[0],
43                surface.uv_offset[1],
44                surface.uv_scale[0],
45                surface.uv_scale[1],
46            ],
47            crop: surface.crop,
48        }
49    }
50}
51
52#[repr(C)]
53#[derive(Clone, Copy, Pod, Zeroable)]
54struct GpuLayer {
55    layer: [f32; 4],
56    extra: [f32; 4],
57}
58
59impl From<ContentLayer> for GpuLayer {
60    fn from(layer: ContentLayer) -> Self {
61        Self {
62            layer: [
63                layer.slot as f32,
64                layer.blend.id() as f32,
65                layer.ink_roughness,
66                layer.emboss,
67            ],
68            extra: [layer.strength, 0.0, 0.0, 0.0],
69        }
70    }
71}
72
73pub const SHADOW_ONLY: u32 = 1;
74pub const CASTS_NO_SHADOW: u32 = 2;
75pub const CUTOUT: u32 = 4;
76pub const FACE_SHIFT: u32 = 3;
77pub const LAYER_SHIFT: u32 = 5;
78pub const ONE_SIDED: u32 = 1 << 23;
79pub const LAYER_MASK: u32 = (1 << 18) - 1;
80const LAYER_LIMIT: usize = 1 << 18;
81
82#[repr(C)]
83#[derive(Clone, Copy, Pod, Zeroable)]
84struct GpuContent {
85    offset: u32,
86    width: u32,
87    height: u32,
88    kind: u32,
89}
90
91#[repr(C)]
92#[derive(Clone, Copy, Pod, Zeroable)]
93struct GpuShape {
94    a: [f32; 4],
95    b: [f32; 4],
96    c: [f32; 4],
97    d: [f32; 4],
98}
99
100#[repr(C)]
101#[derive(Clone, Copy, Pod, Zeroable)]
102pub(crate) struct Frame {
103    pub(crate) size: [u32; 4],
104    counts: [u32; 4],
105    origin: [f32; 4],
106    pub(crate) forward: [f32; 4],
107    right: [f32; 4],
108    up: [f32; 4],
109    sun_dir: [f32; 4],
110    sun_color: [f32; 4],
111    lens: [f32; 4],
112    window: [u32; 4],
113    columns: [u32; 4],
114}
115
116#[derive(Clone, Copy, Debug)]
117pub struct Camera {
118    pub origin: [f32; 3],
119    pub forward: [f32; 3],
120    pub right: [f32; 3],
121    pub up: [f32; 3],
122}
123
124#[derive(Clone, Copy, Debug, Default, PartialEq)]
125pub enum Projection {
126    #[default]
127    Perspective,
128    Orthographic {
129        width: f32,
130        height: f32,
131    },
132    Equirectangular,
133}
134
135impl Projection {
136    pub fn valid(self) -> bool {
137        match self {
138            Self::Orthographic { width, height } => {
139                width.is_finite() && height.is_finite() && width > 0.0 && height > 0.0
140            }
141            Self::Perspective | Self::Equirectangular => true,
142        }
143    }
144
145    fn fits(self, camera: &Camera) -> bool {
146        self == Self::Perspective
147            || [camera.forward, camera.right, camera.up]
148                .into_iter()
149                .all(|axis| axis.into_iter().map(|v| v * v).sum::<f32>() > 1e-12)
150    }
151
152    fn place(self, frame: &mut Frame) {
153        let [kind, half_width, half_height] = match self {
154            Self::Perspective => [0.0; 3],
155            Self::Orthographic { width, height } => [1.0, width * 0.5, height * 0.5],
156            Self::Equirectangular => [2.0, 0.0, 0.0],
157        };
158        frame.origin[3] = kind;
159        frame.right[3] = half_width;
160        frame.up[3] = half_height;
161    }
162}
163
164pub fn equirect_direction(u: f32, v: f32) -> [f32; 3] {
165    let phi = (u - 0.5) * std::f32::consts::TAU;
166    let theta = v * std::f32::consts::PI;
167    [
168        theta.sin() * phi.sin(),
169        theta.cos(),
170        -theta.sin() * phi.cos(),
171    ]
172}
173
174#[derive(Clone, Copy, Debug)]
175pub struct Sun {
176    pub direction: [f32; 3],
177    pub color: [f32; 3],
178    pub intensity: f32,
179}
180
181impl Sun {
182    pub fn from_daylight(daylight: Daylight) -> Self {
183        let sun = daylight.sun();
184        let light = daylight.light(REFERENCE_HOUR);
185        Self {
186            direction: sun.y_up.map(|value| value as f32),
187            color: light.colour.map(|value| value as f32),
188            intensity: light.energy as f32,
189        }
190    }
191}
192
193pub struct Scene<E = Sky> {
194    pub triangles: Vec<Triangle>,
195    pub shapes: Vec<Shape>,
196    pub materials: Vec<Material>,
197    pub sky: E,
198    pub camera: Camera,
199    pub sun: Sun,
200}
201
202pub const SAMPLE_LABEL: &str = "trace samples";
203pub const BAND_STEP: u32 = 8;
204pub const SAMPLE_SPAN_MS: f64 = 12.0;
205pub const SAMPLE_TARGET_MS: f64 = 10.0;
206const LANES: u32 = 64;
207const PIECE_COLUMNS: u32 = 8;
208
209#[derive(Clone, Debug, PartialEq)]
210pub enum TraceError {
211    Refused(GpuRefusal),
212    Setup(String),
213}
214
215impl std::fmt::Display for TraceError {
216    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
217        match self {
218            Self::Refused(refusal) => write!(f, "{refusal}"),
219            Self::Setup(message) => write!(f, "{message}"),
220        }
221    }
222}
223
224impl std::error::Error for TraceError {}
225
226impl From<String> for TraceError {
227    fn from(message: String) -> Self {
228        Self::Setup(message)
229    }
230}
231
232impl From<&str> for TraceError {
233    fn from(message: &str) -> Self {
234        Self::Setup(message.into())
235    }
236}
237
238impl From<GpuRefusal> for TraceError {
239    fn from(refusal: GpuRefusal) -> Self {
240        Self::Refused(refusal)
241    }
242}
243
244impl From<TraceError> for String {
245    fn from(error: TraceError) -> Self {
246        error.to_string()
247    }
248}
249
250pub struct Trace {
251    pub(crate) width: u32,
252    pub(crate) height: u32,
253    pub(crate) samples: u32,
254    projection: Projection,
255    pub(crate) base: Frame,
256    pub(crate) frame: wgpu::Buffer,
257    binding: wgpu::BindGroup,
258    environment_binding: wgpu::BindGroup,
259    pipeline: wgpu::ComputePipeline,
260    pub(crate) materials: u32,
261    pub(crate) accum: wgpu::Buffer,
262    pub(crate) probe_parts: crate::probe::Parts,
263    pacer: Pacer,
264    turns: Turns,
265    lanes: Lanes,
266    pub color: OffscreenTarget,
267    pub albedo: OffscreenTarget,
268    pub normal: OffscreenTarget,
269    rt: Option<crate::rt::Rt>,
270}
271
272#[derive(Debug, PartialEq)]
273pub struct Output {
274    pub color: Vec<u8>,
275    pub albedo: Vec<u8>,
276    pub normal: Vec<u8>,
277}
278
279pub struct SceneBuffers {
280    pub triangles: Vec<u8>,
281    pub nodes: Vec<u8>,
282    pub shapes: Vec<u8>,
283    pub materials: Vec<u8>,
284    pub uv: Vec<u8>,
285    pub content: Vec<u8>,
286    pub surfaces: Vec<u8>,
287    pub layers: Vec<u8>,
288    pub content_info: Vec<u8>,
289    pub emitters: Vec<u8>,
290    pub emitter_cdf: Vec<u8>,
291    pub lights: Vec<u8>,
292    pub triangle_count: u32,
293    pub shape_count: u32,
294    pub emitter_count: u32,
295    pub light_count: u32,
296    pub rt: crate::rt::RtGeometry,
297}
298
299impl SceneBuffers {
300    pub fn build(
301        triangles: &[Triangle],
302        shapes: &[Shape],
303        materials: &[Material],
304        detail: &Detail,
305    ) -> Result<Self, String> {
306        Self::build_with(triangles, shapes, materials, detail, true)
307    }
308
309    fn build_with(
310        triangles: &[Triangle],
311        shapes: &[Shape],
312        materials: &[Material],
313        detail: &Detail,
314        emitter_sampling: bool,
315    ) -> Result<Self, String> {
316        if detail.lights.len() > MAX_LIGHTS || detail.lights.iter().any(|light| !light.valid()) {
317            return Err("invalid trace light".into());
318        }
319        if materials.is_empty()
320            || detail.content.len() > materials.len()
321            || detail.content.iter().flatten().any(|image| !image.valid())
322            || detail
323                .content_slots
324                .iter()
325                .flatten()
326                .any(|image| !image.valid())
327            || detail.text_slots.iter().flatten().any(|text| !text.valid())
328            || detail
329                .instances
330                .iter()
331                .any(|instance| !instance.surface.valid())
332        {
333            return Err("invalid trace scene detail".into());
334        }
335        let mut detailed: Vec<TriangleDetail> = triangles
336            .iter()
337            .copied()
338            .map(TriangleDetail::flat)
339            .collect();
340        detailed.extend_from_slice(&detail.triangles);
341        let mut surfaces = vec![GpuSurface::from(InstanceSurface::default()); detailed.len()];
342        let mut flags = vec![0_u32; detailed.len()];
343        let mut cutoffs = vec![0.0_f32; detailed.len()];
344        if materials.len() + detail.instances.len() >= LAYER_LIMIT {
345            return Err("trace scene has too many content layers".into());
346        }
347        let mut placed_layers = Vec::new();
348        for instance in &detail.instances {
349            let transformed = instance.transformed()?;
350            let surface = instance.surface;
351            let mut word = surface.face.code() << FACE_SHIFT;
352            if surface.layer.active() {
353                placed_layers.push(GpuLayer::from(surface.layer));
354                word |= ((materials.len() + placed_layers.len()) as u32) << LAYER_SHIFT;
355            }
356            if instance.shadow_only {
357                word |= SHADOW_ONLY;
358            }
359            if !surface.casts_shadow {
360                word |= CASTS_NO_SHADOW;
361            }
362            if surface.cutout {
363                word |= CUTOUT;
364            }
365            if !instance.two_sided {
366                word |= ONE_SIDED;
367            }
368            flags.extend(std::iter::repeat_n(word, transformed.len()));
369            surfaces.extend(std::iter::repeat_n(
370                GpuSurface::from(surface),
371                transformed.len(),
372            ));
373            cutoffs.extend(std::iter::repeat_n(surface.alpha_cutoff, transformed.len()));
374            detailed.extend(transformed);
375        }
376        if detailed.iter().any(|tri| {
377            tri.vertices.into_iter().any(|vertex| !finite3(vertex))
378                || tri.normals.into_iter().any(|normal| !finite3(normal))
379                || tri
380                    .uvs
381                    .into_iter()
382                    .flatten()
383                    .any(|value| !value.is_finite())
384                || tri.material as usize >= materials.len()
385        }) || shapes
386            .iter()
387            .copied()
388            .any(|shape| !valid_shape(shape) || shape.material() as usize >= materials.len())
389        {
390            return Err("invalid trace geometry".into());
391        }
392        for (index, shape) in shapes.iter().enumerate() {
393            if let Shape::SmoothUnion { left, right, .. } = shape
394                && (*left >= index
395                    || *right >= index
396                    || matches!(shapes[*left], Shape::SmoothUnion { .. })
397                    || matches!(shapes[*right], Shape::SmoothUnion { .. }))
398            {
399                return Err("smooth union needs earlier primitive shapes".into());
400            }
401        }
402        let all_triangles: Vec<Triangle> = detailed
403            .iter()
404            .copied()
405            .map(TriangleDetail::triangle)
406            .collect();
407        let bvh = Bvh::build(&all_triangles);
408        let mut triangles: Vec<_> = bvh
409            .order
410            .iter()
411            .map(|&index| {
412                let tri = detailed[index as usize];
413                GpuTriangle {
414                    a: vec4(tri.vertices[0], tri.material as f32),
415                    b: vec4(tri.vertices[1], flags[index as usize] as f32),
416                    c: vec4(tri.vertices[2], 0.0),
417                    na: [
418                        tri.normals[0][0],
419                        tri.normals[0][1],
420                        tri.normals[0][2],
421                        tri.uvs[0][0],
422                    ],
423                    nb: [
424                        tri.normals[1][0],
425                        tri.normals[1][1],
426                        tri.normals[1][2],
427                        tri.uvs[1][0],
428                    ],
429                    nc: [
430                        tri.normals[2][0],
431                        tri.normals[2][1],
432                        tri.normals[2][2],
433                        tri.uvs[2][0],
434                    ],
435                }
436            })
437            .collect();
438        let ordered_surfaces: Vec<GpuSurface> = bvh
439            .order
440            .iter()
441            .map(|&index| surfaces[index as usize])
442            .collect();
443        let mut effects = PackedMaterial::zeroed();
444        if let Some(leaf) = detail.leaf {
445            effects.base_roughness = [leaf.time, leaf.strength, 0.0, 0.0];
446            effects.metal_spec_coat = vec4(leaf.center, 0.0);
447        }
448        if let Some(steam) = detail.steam {
449            effects.base_roughness[3] = steam.time;
450            effects.metal_spec_coat[3] = steam.anisotropy;
451            effects.sheen_trans_ior_disp = vec4(steam.source, steam.radius);
452            effects.thick_sub_film = vec4(steam.box_lo, steam.density);
453            effects.tint_absorption = vec4(steam.box_hi, steam.ambient);
454        }
455        let uv_v: Vec<[f32; 4]> = bvh
456            .order
457            .iter()
458            .map(|&index| {
459                let tri = detailed[index as usize];
460                [
461                    tri.uvs[0][1],
462                    tri.uvs[1][1],
463                    tri.uvs[2][1],
464                    cutoffs[index as usize],
465                ]
466            })
467            .collect();
468        let slotted = !detail.content_slots.is_empty() || !detail.text_slots.is_empty();
469        let slot_images = if !slotted {
470            &detail.content
471        } else {
472            &detail.content_slots
473        };
474        let slot_count = slot_images.len().max(detail.text_slots.len());
475        let mut content_info = vec![GpuContent::zeroed(); slot_count.max(1)];
476        let mut content_texels: Vec<[f32; 4]> = Vec::new();
477        for (index, image) in slot_images.iter().enumerate() {
478            if let Some(image) = image {
479                content_info[index] = GpuContent {
480                    offset: content_texels.len() as u32,
481                    width: image.width,
482                    height: image.height,
483                    kind: 0,
484                };
485                content_texels.extend_from_slice(&image.texels);
486            }
487        }
488        for (index, text) in detail.text_slots.iter().enumerate() {
489            if let Some(text) = text {
490                if slot_images.get(index).is_some_and(Option::is_some) {
491                    return Err("a content slot holds both an image and text".into());
492                }
493                content_info[index] = GpuContent {
494                    offset: content_texels.len() as u32,
495                    width: text.width,
496                    height: text.height,
497                    kind: 1,
498                };
499                content_texels.extend_from_slice(&text.records);
500            }
501        }
502        if content_texels.len() > u32::MAX as usize {
503            return Err("trace content is too large".into());
504        }
505        if content_texels.is_empty() {
506            content_texels.push([0.0; 4]);
507        }
508        let mut content_layers: Vec<GpuLayer> = materials
509            .iter()
510            .enumerate()
511            .map(|(index, material)| {
512                let layer = if material.content_layer.active() {
513                    material.content_layer
514                } else if !slotted && detail.content.get(index).is_some_and(Option::is_some) {
515                    if material.content == Content::None {
516                        ContentLayer {
517                            slot: index as i32,
518                            ..ContentLayer::default()
519                        }
520                    } else {
521                        ContentLayer::from_kind(
522                            material.content,
523                            material.clearcoat,
524                            index as i32,
525                            &detail.content_look,
526                        )
527                    }
528                } else {
529                    ContentLayer::default()
530                };
531                GpuLayer::from(layer)
532            })
533            .collect();
534        content_layers.extend(placed_layers);
535        if content_layers
536            .iter()
537            .any(|layer| layer.layer[0] >= content_info.len() as f32)
538        {
539            return Err("content layer slot is missing".into());
540        }
541        let mut packed_shapes: Vec<_> = shapes.iter().copied().map(pack_shape).collect();
542        for shape in shapes {
543            if let Shape::SmoothUnion { left, right, .. } = shape {
544                packed_shapes[*left].d[0] = 1.0;
545                packed_shapes[*right].d[0] = 1.0;
546            }
547        }
548        let mut packed_materials: Vec<_> = materials.iter().map(PackedMaterial::pack).collect();
549        let emitting: Vec<bool> = packed_materials
550            .iter()
551            .zip(&content_layers)
552            .map(|(packed, layer)| {
553                let emission = [
554                    packed.emission_film[0],
555                    packed.emission_film[1],
556                    packed.emission_film[2],
557                ];
558                emitter_sampling && layer.layer[0] < 0.0 && emits(emission)
559            })
560            .collect();
561        let emission = |material: u32| {
562            let packed = &packed_materials[material as usize];
563            [
564                packed.emission_film[0],
565                packed.emission_film[1],
566                packed.emission_film[2],
567            ]
568        };
569        let mut candidates = Vec::new();
570        for (slot, &index) in bvh.order.iter().enumerate() {
571            let tri = detailed[index as usize];
572            if emitting[tri.material as usize]
573                && flags[index as usize] & (SHADOW_ONLY | CUTOUT) == 0
574                && (flags[index as usize] >> LAYER_SHIFT) & LAYER_MASK == 0
575                && crate::lights::triangle_area(tri.vertices) > 1e-12
576            {
577                candidates.push(Candidate {
578                    source: Source::Triangle {
579                        vertices: tri.vertices,
580                        slot: slot as u32,
581                    },
582                    emission: emission(tri.material),
583                });
584                triangles[slot].c[3] = candidates.len() as f32;
585            }
586        }
587        for (index, shape) in shapes.iter().enumerate() {
588            if packed_shapes[index].d[0] > 0.5 || !emitting[shape.material() as usize] {
589                continue;
590            }
591            let (center, radius) = crate::lights::bounding_sphere(*shape, shapes);
592            let area = crate::lights::shape_area(*shape, shapes);
593            candidates.push(Candidate {
594                source: Source::Shape {
595                    center,
596                    radius,
597                    index: index as u32,
598                    area,
599                },
600                emission: emission(shape.material()),
601            });
602            packed_shapes[index].d[1] = candidates.len() as f32;
603        }
604        if candidates.len() > MAX_EMITTERS {
605            return Err("trace scene has too many emitters".into());
606        }
607        let solid: Vec<bool> = bvh
608            .order
609            .iter()
610            .map(|&index| {
611                let index = index as usize;
612                let surface = surfaces[index];
613                flags[index] & (SHADOW_ONLY | CASTS_NO_SHADOW | CUTOUT | ONE_SIDED) == 0
614                    && surface.clip0[..3] == [0.0; 3]
615                    && surface.clip1[..3] == [0.0; 3]
616                    && materials[detailed[index].material as usize].transmission <= 0.0
617            })
618            .collect();
619        let placed: Vec<[[f32; 3]; 3]> = bvh
620            .order
621            .iter()
622            .map(|&index| detailed[index as usize].vertices)
623            .collect();
624        let boxes = shapes
625            .iter()
626            .enumerate()
627            .filter(|(index, _)| packed_shapes[*index].d[0] <= 0.5)
628            .map(|(index, shape)| {
629                let (center, radius) = crate::lights::bounding_sphere(*shape, shapes);
630                (
631                    index as u32,
632                    [
633                        center[0] - radius,
634                        center[1] - radius,
635                        center[2] - radius,
636                        center[0] + radius,
637                        center[1] + radius,
638                        center[2] + radius,
639                    ],
640                )
641            })
642            .collect();
643        let boxes = if crate::rt::boxes() {
644            boxes
645        } else {
646            Vec::new()
647        };
648        let rt = crate::rt::RtGeometry::new(&placed, &solid, boxes);
649        let list = build_emitters(&candidates, detail.lights.len());
650        let lights: Vec<_> = detail.lights.iter().map(|light| light.pack()).collect();
651        packed_materials.push(effects);
652        Ok(Self {
653            triangles: bytemuck::cast_slice(&triangles).to_vec(),
654            nodes: bytemuck::cast_slice(&bvh.nodes).to_vec(),
655            shapes: bytemuck::cast_slice(&packed_shapes).to_vec(),
656            materials: bytemuck::cast_slice(&packed_materials).to_vec(),
657            uv: bytemuck::cast_slice(&uv_v).to_vec(),
658            content: bytemuck::cast_slice(&content_texels).to_vec(),
659            surfaces: bytemuck::cast_slice(&ordered_surfaces).to_vec(),
660            layers: bytemuck::cast_slice(&content_layers).to_vec(),
661            content_info: bytemuck::cast_slice(&content_info).to_vec(),
662            emitters: bytemuck::cast_slice(&list.entries).to_vec(),
663            emitter_cdf: bytemuck::cast_slice(&list.cdf).to_vec(),
664            lights: bytemuck::cast_slice(&lights).to_vec(),
665            triangle_count: detailed.len() as u32,
666            shape_count: shapes.len() as u32,
667            emitter_count: candidates.len() as u32,
668            light_count: detail.lights.len() as u32,
669            rt,
670        })
671    }
672}
673
674fn vec4(v: [f32; 3], w: f32) -> [f32; 4] {
675    [v[0], v[1], v[2], w]
676}
677
678fn pack_shape(shape: Shape) -> GpuShape {
679    match shape {
680        Shape::RoundedBox {
681            center,
682            half,
683            radius,
684            material,
685        } => GpuShape {
686            a: vec4(center, 1.0),
687            b: vec4(half, radius),
688            c: [0.0, 0.0, 0.0, material as f32],
689            d: [0.0; 4],
690        },
691        Shape::RoundCone {
692            a,
693            b,
694            radius_a,
695            radius_b,
696            material,
697        } => GpuShape {
698            a: vec4(a, 2.0),
699            b: vec4(b, 0.0),
700            c: [radius_a, radius_b, 0.0, material as f32],
701            d: [0.0; 4],
702        },
703        Shape::Ellipsoid {
704            center,
705            radii,
706            material,
707        } => GpuShape {
708            a: vec4(center, 3.0),
709            b: vec4(radii, 0.0),
710            c: [0.0, 0.0, 0.0, material as f32],
711            d: [0.0; 4],
712        },
713        Shape::Capsule {
714            a,
715            b,
716            radius,
717            material,
718        } => GpuShape {
719            a: vec4(a, 4.0),
720            b: vec4(b, 0.0),
721            c: [radius, 0.0, 0.0, material as f32],
722            d: [0.0; 4],
723        },
724        Shape::SmoothUnion {
725            left,
726            right,
727            radius,
728            material,
729        } => GpuShape {
730            a: [0.0, 0.0, 0.0, 5.0],
731            b: [left as f32, right as f32, radius, 0.0],
732            c: [0.0, 0.0, 0.0, material as f32],
733            d: [0.0; 4],
734        },
735    }
736}
737
738fn finite3(v: [f32; 3]) -> bool {
739    v.into_iter().all(f32::is_finite)
740}
741
742fn bounce_limits(bounces: Bounces) -> [(&'static str, f64); 4] {
743    [
744        ("TOTAL_BOUNCES", bounces.total as f64),
745        ("DIFFUSE_BOUNCES", bounces.diffuse as f64),
746        ("GLOSSY_BOUNCES", bounces.glossy as f64),
747        ("TRANSMISSION_BOUNCES", bounces.transmission as f64),
748    ]
749}
750
751fn pipeline_constants(detail: &Detail, height: u32) -> Vec<(&'static str, f64)> {
752    let mut constants = bounce_limits(detail.bounces).to_vec();
753    constants.push((
754        "BLOCK_STRIDE",
755        f64::from(block_stride(height.div_ceil(BAND_STEP))),
756    ));
757    if detail.transmissive_shadows {
758        constants.push(("TRANSMISSIVE_SHADOWS", 1.0));
759    }
760    if detail.clamp_indirect > 0.0 {
761        constants.push(("CLAMP_INDIRECT", f64::from(detail.clamp_indirect)));
762    }
763    if detail.filter_glossy > 0.0 {
764        constants.push(("FILTER_GLOSSY", f64::from(detail.filter_glossy)));
765    }
766    if let Some(sequence) = detail.sampler.sequence() {
767        constants.push(("SOBOL", 1.0));
768        constants.push(("SOBOL_LENGTH", f64::from(sequence)));
769    }
770    if let Some(steam) = detail.steam {
771        constants.extend(
772            PlumeLook::NAMES
773                .into_iter()
774                .zip(steam.look.floats())
775                .map(|(name, value)| (name, f64::from(value))),
776        );
777    }
778    constants
779}
780
781fn block_stride(blocks: u32) -> u32 {
782    if blocks <= 2 {
783        return 1;
784    }
785    let golden = (f64::from(blocks) * 0.381_966_011_25).round() as u32;
786    (golden..blocks)
787        .chain(1..golden)
788        .find(|&stride| coprime(stride, blocks))
789        .unwrap_or(1)
790}
791
792fn coprime(mut a: u32, mut b: u32) -> bool {
793    while b != 0 {
794        (a, b) = (b, a % b);
795    }
796    a == 1
797}
798
799fn valid_shape(shape: Shape) -> bool {
800    match shape {
801        Shape::RoundedBox {
802            center,
803            half,
804            radius,
805            ..
806        } => {
807            finite3(center)
808                && finite3(half)
809                && radius.is_finite()
810                && radius >= 0.0
811                && half.into_iter().all(|v| v > 0.0 && radius <= v)
812        }
813        Shape::RoundCone {
814            a,
815            b,
816            radius_a,
817            radius_b,
818            ..
819        } => {
820            finite3(a)
821                && finite3(b)
822                && radius_a.is_finite()
823                && radius_b.is_finite()
824                && radius_a >= 0.0
825                && radius_b >= 0.0
826        }
827        Shape::Ellipsoid { center, radii, .. } => {
828            finite3(center) && finite3(radii) && radii.into_iter().all(|v| v > 0.0)
829        }
830        Shape::Capsule { a, b, radius, .. } => {
831            finite3(a) && finite3(b) && radius.is_finite() && radius > 0.0
832        }
833        Shape::SmoothUnion { radius, .. } => radius.is_finite() && radius > 0.0,
834    }
835}
836
837fn refused(gpu: &Gpu, shortfall: GpuShortfall) -> TraceError {
838    TraceError::Refused(GpuRefusal::new(shortfall, Some(gpu.info.clone())))
839}
840
841fn fits_buffer(gpu: &Gpu, bytes: u64) -> Result<(), TraceError> {
842    let limits = gpu.device.limits();
843    [
844        (
845            "max_storage_buffer_binding_size",
846            limits.max_storage_buffer_binding_size,
847        ),
848        ("max_buffer_size", limits.max_buffer_size),
849    ]
850    .into_iter()
851    .find(|(_, has)| bytes > *has)
852    .map_or(Ok(()), |(name, has)| {
853        Err(refused(
854            gpu,
855            GpuShortfall::Limit {
856                name,
857                needed: bytes,
858                has,
859            },
860        ))
861    })
862}
863
864fn fits_texture(gpu: &Gpu, width: u32, height: u32) -> Result<(), TraceError> {
865    let has = gpu.device.limits().max_texture_dimension_2d;
866    let side = width.max(height);
867    if side > has {
868        return Err(refused(
869            gpu,
870            GpuShortfall::Limit {
871                name: "max_texture_dimension_2d",
872                needed: u64::from(side),
873                has: u64::from(has),
874            },
875        ));
876    }
877    Ok(())
878}
879
880#[derive(Clone, Copy)]
881struct Pass {
882    lanes: (u32, u32),
883    per_block: u32,
884    columns: (u32, u32),
885}
886
887#[derive(Clone, Copy, Debug)]
888struct Spread {
889    longest: f64,
890    single: f64,
891}
892
893#[derive(Clone, Copy, Debug, PartialEq)]
894enum Step {
895    Held,
896    Blocks,
897    Lanes,
898    Pieces,
899}
900
901#[derive(Clone, Copy, Debug)]
902struct Lanes {
903    width: u32,
904    blocks: u32,
905    widest: u32,
906    most: u32,
907    pieces: u32,
908    fewest: u32,
909    last: Step,
910}
911
912impl Lanes {
913    fn new() -> Self {
914        Self {
915            width: LANES,
916            blocks: 1,
917            widest: LANES * 2,
918            most: u32::MAX,
919            pieces: 1,
920            fewest: 1,
921            last: Step::Held,
922        }
923    }
924
925    fn strips(&self, blocks: u32) -> u32 {
926        let blocks = blocks.max(1);
927        let slices = blocks.div_ceil(self.blocks.clamp(1, blocks));
928        blocks.div_ceil(slices)
929    }
930
931    fn learn(&mut self, longest: f64, blocks: u32) {
932        if !longest.is_finite() || longest <= 0.0 {
933            return;
934        }
935        let half = SAMPLE_SPAN_MS * 0.5;
936        let last = std::mem::replace(&mut self.last, Step::Held);
937        if longest > SAMPLE_SPAN_MS * 0.75 && last != Step::Held {
938            if last == Step::Blocks {
939                self.most = self.blocks;
940                self.blocks = (self.blocks / 2).max(1);
941            } else {
942                self.widest = self.width;
943                self.width = (self.width / 2).max(1);
944            }
945            return;
946        }
947        if longest > SAMPLE_SPAN_MS {
948            if self.blocks > 1 {
949                let fit = (f64::from(self.blocks) * half / longest).floor() as u32;
950                self.most = self.blocks;
951                self.blocks = fit.clamp(1, self.blocks / 2);
952            } else if self.width > 1 {
953                let fit = (f64::from(self.width) * half / longest).floor() as u32;
954                self.widest = self.width;
955                self.width = floor_power_of_two(fit.clamp(1, self.width / 2));
956            }
957            return;
958        }
959        if longest > half {
960            return;
961        }
962        let more = (self.blocks * 2).min(blocks).min(self.most - 1);
963        if self.blocks < blocks && more > self.blocks {
964            self.blocks = more;
965            self.last = Step::Blocks;
966        } else if self.width * 2 < self.widest {
967            self.width = (self.width * 2).min(LANES);
968            self.last = Step::Lanes;
969        }
970    }
971
972    fn learn_pieces(&mut self, longest: f64, blocks: u32, columns: u32) {
973        if !longest.is_finite() || longest <= 0.0 {
974            return;
975        }
976        let half = SAMPLE_SPAN_MS * 0.5;
977        let last = std::mem::replace(&mut self.last, Step::Held);
978        if longest > SAMPLE_SPAN_MS * 0.75 && last != Step::Held {
979            if last == Step::Blocks {
980                self.most = self.blocks;
981                self.blocks = (self.blocks / 2).max(1);
982            } else {
983                self.fewest = self.pieces;
984                self.pieces *= 2;
985            }
986            return;
987        }
988        if longest > SAMPLE_SPAN_MS {
989            let most = most_pieces(columns);
990            if self.blocks > 1 {
991                let fit = (f64::from(self.blocks) * half / longest).floor() as u32;
992                self.most = self.blocks;
993                self.blocks = fit.clamp(1, self.blocks / 2);
994            } else if self.pieces < most {
995                self.pieces *= 2;
996            } else {
997                *self = Self::lane_passes();
998            }
999            return;
1000        }
1001        if longest > half {
1002            return;
1003        }
1004        let more = (self.blocks * 2).min(blocks).min(self.most - 1);
1005        if self.blocks < blocks && more > self.blocks {
1006            self.blocks = more;
1007            self.last = Step::Blocks;
1008        } else if self.pieces / 2 > self.fewest {
1009            self.pieces /= 2;
1010            self.last = Step::Pieces;
1011        }
1012    }
1013
1014    fn learn_dense(&mut self, spread: Spread, columns: u32) {
1015        if spread.single.is_finite() && spread.single > SAMPLE_SPAN_MS {
1016            let most = most_pieces(columns);
1017            *self = if most > 1 {
1018                let fit = (spread.single / (SAMPLE_SPAN_MS * 0.5)).ceil() as u32;
1019                Self {
1020                    pieces: fit.next_power_of_two().clamp(2, most),
1021                    ..Self::new()
1022                }
1023            } else {
1024                Self::lane_passes()
1025            };
1026        }
1027    }
1028
1029    fn lane_passes() -> Self {
1030        Self {
1031            width: 1,
1032            widest: LANES,
1033            ..Self::new()
1034        }
1035    }
1036}
1037
1038fn most_pieces(columns: u32) -> u32 {
1039    floor_power_of_two(columns / PIECE_COLUMNS)
1040}
1041
1042fn floor_power_of_two(value: u32) -> u32 {
1043    if value == 0 {
1044        1
1045    } else {
1046        1 << (31 - value.leading_zeros())
1047    }
1048}
1049
1050#[derive(Debug, PartialEq)]
1051struct Piece {
1052    sample: u32,
1053    samples: u32,
1054    order: u32,
1055    blocks: u32,
1056    from: u32,
1057    to: u32,
1058}
1059
1060fn pieces(blocks: u32, per_block: u32, start: u32, count: u32) -> Vec<Piece> {
1061    let per_sample = blocks * per_block;
1062    let end = start + count;
1063    let mut pieces = Vec::new();
1064    let mut at = start;
1065    while at < end {
1066        let sample = at / per_sample;
1067        let within = at % per_sample;
1068        let order = within / per_block;
1069        let offset = within % per_block;
1070        let left = end - at;
1071        let piece = if within == 0 && left >= per_sample {
1072            Piece {
1073                sample,
1074                samples: left / per_sample,
1075                order: 0,
1076                blocks,
1077                from: 0,
1078                to: per_block,
1079            }
1080        } else if offset == 0 && left >= per_block {
1081            Piece {
1082                sample,
1083                samples: 1,
1084                order,
1085                blocks: (left / per_block).min(blocks - order),
1086                from: 0,
1087                to: per_block,
1088            }
1089        } else {
1090            Piece {
1091                sample,
1092                samples: 1,
1093                order,
1094                blocks: 1,
1095                from: offset,
1096                to: per_block.min(offset + left),
1097            }
1098        };
1099        at += piece.samples * (piece.blocks * per_block - piece.from) - (per_block - piece.to);
1100        pieces.push(piece);
1101    }
1102    pieces
1103}
1104
1105pub fn shader(lit: bool) -> String {
1106    shader_source(lit, false)
1107}
1108
1109pub fn shader_rt(lit: bool) -> String {
1110    shader_source(lit, true)
1111}
1112
1113fn shader_source(lit: bool, rt: bool) -> String {
1114    let (head, rest) = include_str!("trace.wgsl")
1115        .split_once("fn trace_sobol_start() {}\n")
1116        .unwrap();
1117    let (_, tail) = rest.split_once("fn trace_sobol_end() {}\n").unwrap();
1118    let source = format!("{head}{}{tail}", include_str!("sobol.wgsl"));
1119    let (start, rest) = source.split_once("fn trace_noise_start() {}\n").unwrap();
1120    let (_, end) = rest.split_once("fn trace_noise_end() {}\n").unwrap();
1121    let end = if lit {
1122        let (middle, rest) = end.split_once("fn trace_lights_start() {}\n").unwrap();
1123        let (_, tail) = rest.split_once("fn trace_lights_end() {}\n").unwrap();
1124        format!("{}\n{}\n{}", middle, include_str!("lights.wgsl"), tail)
1125    } else {
1126        end.to_string()
1127    };
1128    let source = format!(
1129        "{}\n{}\n{}\n{}\n{}\n{}",
1130        pfx_materials::NOISE,
1131        pfx_materials::BRDF,
1132        pfx_materials::CONTENT,
1133        ENVIRONMENT_WGSL.as_str(),
1134        start,
1135        end
1136    );
1137    if !rt {
1138        return source;
1139    }
1140    let source = [
1141        ("fn scene(o: vec3f", "fn soft_scene(o: vec3f"),
1142        ("fn occluded(o: vec3f", "fn soft_occluded(o: vec3f"),
1143        (
1144            "fn shadow_passed(o: vec3f",
1145            "fn soft_shadow_passed(o: vec3f",
1146        ),
1147    ]
1148    .into_iter()
1149    .fold(source, |source, (from, to)| source.replacen(from, to, 1));
1150    format!(
1151        "enable wgpu_ray_query;\nconst RT_BOXES: bool = {};\n{}\n{}",
1152        crate::rt::boxes(),
1153        source,
1154        crate::rt::RT_WGSL
1155    )
1156}
1157
1158pub(crate) fn texture(gpu: &Gpu, width: u32, height: u32, label: &'static str) -> OffscreenTarget {
1159    let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
1160        label: Some(label),
1161        size: wgpu::Extent3d {
1162            width,
1163            height,
1164            depth_or_array_layers: 1,
1165        },
1166        mip_level_count: 1,
1167        sample_count: 1,
1168        dimension: wgpu::TextureDimension::D2,
1169        format: wgpu::TextureFormat::Rgba32Float,
1170        usage: wgpu::TextureUsages::STORAGE_BINDING | wgpu::TextureUsages::COPY_SRC,
1171        view_formats: &[],
1172    });
1173    let view = texture.create_view(&Default::default());
1174    OffscreenTarget {
1175        texture,
1176        view,
1177        format: wgpu::TextureFormat::Rgba32Float,
1178        width,
1179        height,
1180    }
1181}
1182
1183fn storage(gpu: &Gpu, label: &'static str, data: &[u8], read_write: bool) -> wgpu::Buffer {
1184    if data.is_empty() {
1185        return gpu.device.create_buffer(&wgpu::BufferDescriptor {
1186            label: Some(label),
1187            size: 256,
1188            usage: wgpu::BufferUsages::STORAGE,
1189            mapped_at_creation: false,
1190        });
1191    }
1192    gpu.device
1193        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1194            label: Some(label),
1195            contents: data,
1196            usage: wgpu::BufferUsages::STORAGE
1197                | if read_write {
1198                    wgpu::BufferUsages::COPY_DST
1199                } else {
1200                    wgpu::BufferUsages::empty()
1201                },
1202        })
1203}
1204
1205fn sobol_table(gpu: &Gpu, sampler: crate::sobol::Sampler) -> wgpu::Texture {
1206    let texels = sampler
1207        .sequence()
1208        .map_or_else(|| vec![[0.0; 4]], crate::sobol::table);
1209    let width = crate::sobol::TABLE_WIDTH.min(texels.len() as u32);
1210    gpu.device.create_texture_with_data(
1211        &gpu.queue,
1212        &wgpu::TextureDescriptor {
1213            label: Some("trace sobol table"),
1214            size: wgpu::Extent3d {
1215                width,
1216                height: texels.len() as u32 / width,
1217                depth_or_array_layers: 1,
1218            },
1219            mip_level_count: 1,
1220            sample_count: 1,
1221            dimension: wgpu::TextureDimension::D2,
1222            format: wgpu::TextureFormat::Rgba32Float,
1223            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1224            view_formats: &[],
1225        },
1226        wgpu::util::TextureDataOrder::LayerMajor,
1227        bytemuck::cast_slice(&texels),
1228    )
1229}
1230
1231fn environment_binding(
1232    gpu: &Gpu,
1233    pipeline: &wgpu::ComputePipeline,
1234    environment: &Environment,
1235) -> Result<wgpu::BindGroup, String> {
1236    let (width, height, texels) = match environment {
1237        Environment::Hdr(hdr) => {
1238            if hdr.width == 0
1239                || hdr.height == 0
1240                || hdr.texels.len() != u64::from(hdr.width) as usize * hdr.height as usize
1241            {
1242                return Err("invalid sky".into());
1243            }
1244            (hdr.width, hdr.height, hdr.texels.as_slice())
1245        }
1246        Environment::Analytic(_) => (1, 1, &[[0.0; 4]][..]),
1247    };
1248    let cdf = SkyCdf::build(environment, 256, 128).without_sun();
1249    let texture = gpu.device.create_texture(&wgpu::TextureDescriptor {
1250        label: Some("trace environment"),
1251        size: wgpu::Extent3d {
1252            width,
1253            height,
1254            depth_or_array_layers: 1,
1255        },
1256        mip_level_count: 1,
1257        sample_count: 1,
1258        dimension: wgpu::TextureDimension::D2,
1259        format: wgpu::TextureFormat::Rgba32Float,
1260        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
1261        view_formats: &[],
1262    });
1263    gpu.queue.write_texture(
1264        texture.as_image_copy(),
1265        bytemuck::cast_slice(texels),
1266        wgpu::TexelCopyBufferLayout {
1267            offset: 0,
1268            bytes_per_row: Some(width * 16),
1269            rows_per_image: Some(height),
1270        },
1271        texture.size(),
1272    );
1273    let view = texture.create_view(&Default::default());
1274    let sampler = gpu.device.create_sampler(&wgpu::SamplerDescriptor {
1275        address_mode_u: wgpu::AddressMode::Repeat,
1276        address_mode_v: wgpu::AddressMode::ClampToEdge,
1277        mag_filter: wgpu::FilterMode::Linear,
1278        min_filter: wgpu::FilterMode::Linear,
1279        ..Default::default()
1280    });
1281    let uniform = gpu
1282        .device
1283        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1284            label: Some("trace environment uniform"),
1285            contents: bytemuck::bytes_of(&environment.uniform()),
1286            usage: wgpu::BufferUsages::UNIFORM,
1287        });
1288    let rows = storage(
1289        gpu,
1290        "trace sky rows",
1291        bytemuck::cast_slice(&cdf.rows),
1292        false,
1293    );
1294    let columns = storage(
1295        gpu,
1296        "trace sky columns",
1297        bytemuck::cast_slice(&cdf.columns),
1298        false,
1299    );
1300    let cdf_uniform = gpu
1301        .device
1302        .create_buffer_init(&wgpu::util::BufferInitDescriptor {
1303            label: Some("trace sky cdf uniform"),
1304            contents: bytemuck::bytes_of(&cdf.uniform()),
1305            usage: wgpu::BufferUsages::UNIFORM,
1306        });
1307    Ok(gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1308        label: Some("trace environment bindings"),
1309        layout: &pipeline.get_bind_group_layout(1),
1310        entries: &[
1311            wgpu::BindGroupEntry {
1312                binding: 0,
1313                resource: uniform.as_entire_binding(),
1314            },
1315            wgpu::BindGroupEntry {
1316                binding: 1,
1317                resource: wgpu::BindingResource::TextureView(&view),
1318            },
1319            wgpu::BindGroupEntry {
1320                binding: 2,
1321                resource: wgpu::BindingResource::Sampler(&sampler),
1322            },
1323            wgpu::BindGroupEntry {
1324                binding: 3,
1325                resource: rows.as_entire_binding(),
1326            },
1327            wgpu::BindGroupEntry {
1328                binding: 4,
1329                resource: columns.as_entire_binding(),
1330            },
1331            wgpu::BindGroupEntry {
1332                binding: 5,
1333                resource: cdf_uniform.as_entire_binding(),
1334            },
1335        ],
1336    }))
1337}
1338
1339impl Trace {
1340    pub fn new<E: Clone + Into<Environment>>(
1341        gpu: &Gpu,
1342        scene: &Scene<E>,
1343        width: u32,
1344        height: u32,
1345    ) -> Result<Self, TraceError> {
1346        Self::new_detailed(gpu, scene, &Detail::default(), width, height)
1347    }
1348
1349    pub fn new_detailed<E: Clone + Into<Environment>>(
1350        gpu: &Gpu,
1351        scene: &Scene<E>,
1352        detail: &Detail,
1353        width: u32,
1354        height: u32,
1355    ) -> Result<Self, TraceError> {
1356        Self::build(gpu, scene, detail, width, height, true)
1357    }
1358
1359    fn build<E: Clone + Into<Environment>>(
1360        gpu: &Gpu,
1361        scene: &Scene<E>,
1362        detail: &Detail,
1363        width: u32,
1364        height: u32,
1365        emitter_sampling: bool,
1366    ) -> Result<Self, TraceError> {
1367        gpu.check(&trace_floor())?;
1368        if width == 0 || height == 0 || scene.materials.is_empty() {
1369            return Err("trace needs dimensions and at least one material".into());
1370        }
1371        if !detail.lens.valid()
1372            || !detail.projection.valid()
1373            || !detail.bounces.valid()
1374            || !detail.sun_radius_deg.is_finite()
1375            || !(0.0..90.0).contains(&detail.sun_radius_deg)
1376            || detail.leaf.is_some_and(|leaf| !leaf.valid())
1377            || detail.steam.is_some_and(|steam| !steam.valid())
1378            || detail.content.iter().flatten().any(|image| !image.valid())
1379            || detail.content.len() > scene.materials.len()
1380            || detail
1381                .content_slots
1382                .iter()
1383                .flatten()
1384                .any(|image| !image.valid())
1385            || detail.text_slots.iter().flatten().any(|text| !text.valid())
1386            || detail
1387                .instances
1388                .iter()
1389                .any(|instance| !instance.surface.valid())
1390        {
1391            return Err("invalid trace detail".into());
1392        }
1393        TraceSettings {
1394            transmissive_shadows: detail.transmissive_shadows,
1395            clamp_indirect: detail.clamp_indirect,
1396            filter_glossy: detail.filter_glossy,
1397        }
1398        .check()
1399        .map_err(|message| TraceError::from(format!("invalid trace detail: {message}")))?;
1400        let pixels = u64::from(width) * u64::from(height);
1401        if pixels > u32::MAX as u64 {
1402            return Err("trace image is too large".into());
1403        }
1404        if height > u32::from(u16::MAX) {
1405            return Err("trace image is taller than 65535 rows".into());
1406        }
1407        fits_texture(gpu, width, height)?;
1408        fits_buffer(gpu, pixels * pfx_gpu::floor::TRACE_BYTES_PER_PIXEL)?;
1409        if !finite3(scene.camera.origin)
1410            || !finite3(scene.camera.forward)
1411            || !finite3(scene.camera.right)
1412            || !finite3(scene.camera.up)
1413            || !finite3(scene.sun.direction)
1414            || !finite3(scene.sun.color)
1415            || !scene.sun.intensity.is_finite()
1416        {
1417            return Err("camera or sun contains invalid parameters".into());
1418        }
1419        if !detail.projection.fits(&scene.camera) {
1420            return Err("an orthographic or equirect camera needs forward, right and up".into());
1421        }
1422        let buffers = SceneBuffers::build_with(
1423            &scene.triangles,
1424            &scene.shapes,
1425            &scene.materials,
1426            detail,
1427            emitter_sampling,
1428        )?;
1429        for bytes in [
1430            &buffers.triangles,
1431            &buffers.uv,
1432            &buffers.content,
1433            &buffers.surfaces,
1434            &buffers.layers,
1435            &buffers.content_info,
1436            &buffers.nodes,
1437            &buffers.shapes,
1438            &buffers.materials,
1439            &buffers.emitters,
1440            &buffers.emitter_cdf,
1441            &buffers.lights,
1442        ] {
1443            fits_buffer(gpu, bytes.len() as u64)?;
1444        }
1445        let frame = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1446            label: Some("trace frame"),
1447            size: std::mem::size_of::<Frame>() as u64,
1448            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
1449            mapped_at_creation: false,
1450        });
1451        let tri_buffer = storage(gpu, "trace triangles", &buffers.triangles, false);
1452        let uv_buffer = storage(gpu, "trace triangle uv v", &buffers.uv, false);
1453        let content_buffer = storage(gpu, "trace content", &buffers.content, false);
1454        let surface_buffer = storage(gpu, "trace instance surfaces", &buffers.surfaces, false);
1455        let layer_buffer = storage(gpu, "trace content layers", &buffers.layers, false);
1456        let content_info_buffer = storage(gpu, "trace content info", &buffers.content_info, false);
1457        let node_buffer = storage(gpu, "trace nodes", &buffers.nodes, false);
1458        let shape_buffer = storage(gpu, "trace shapes", &buffers.shapes, false);
1459        let material_buffer = storage(gpu, "trace materials", &buffers.materials, false);
1460        let emitter_buffer = storage(gpu, "trace emitters", &buffers.emitters, false);
1461        let emitter_cdf_buffer = storage(gpu, "trace emitter cdf", &buffers.emitter_cdf, false);
1462        let light_buffer = storage(gpu, "trace lights", &buffers.lights, false);
1463        let accum_buffer = gpu.device.create_buffer(&wgpu::BufferDescriptor {
1464            label: Some("trace accumulation"),
1465            size: pixels * 48,
1466            usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_SRC,
1467            mapped_at_creation: false,
1468        });
1469        let color = texture(gpu, width, height, "trace color");
1470        let albedo = texture(gpu, width, height, "trace albedo");
1471        let normal = texture(gpu, width, height, "trace normal");
1472        let lit = buffers.emitter_count > 0 || buffers.light_count > 0;
1473        let probe_parts = crate::probe::Parts {
1474            buffers: [
1475                (1, tri_buffer.clone()),
1476                (2, node_buffer.clone()),
1477                (3, shape_buffer.clone()),
1478                (4, material_buffer.clone()),
1479                (5, uv_buffer.clone()),
1480                (6, content_buffer.clone()),
1481                (11, content_info_buffer.clone()),
1482                (12, surface_buffer.clone()),
1483                (13, layer_buffer.clone()),
1484            ],
1485            lit,
1486        };
1487        let rt = (crate::rt::wanted(gpu) && !buffers.rt.is_empty())
1488            .then(|| crate::rt::Rt::build(gpu, &buffers.rt));
1489        let module = gpu
1490            .device
1491            .create_shader_module(wgpu::ShaderModuleDescriptor {
1492                label: Some("trace shader"),
1493                source: wgpu::ShaderSource::Wgsl(shader_source(lit, rt.is_some()).into()),
1494            });
1495        let pipeline = gpu
1496            .device
1497            .create_compute_pipeline(&wgpu::ComputePipelineDescriptor {
1498                label: Some("trace pipeline"),
1499                layout: None,
1500                module: &module,
1501                entry_point: Some("main"),
1502                compilation_options: wgpu::PipelineCompilationOptions {
1503                    constants: &pipeline_constants(detail, height),
1504                    ..Default::default()
1505                },
1506                cache: None,
1507            });
1508        let sobol_view =
1509            sobol_table(gpu, detail.sampler).create_view(&wgpu::TextureViewDescriptor::default());
1510        let mut entries = vec![
1511            wgpu::BindGroupEntry {
1512                binding: 0,
1513                resource: frame.as_entire_binding(),
1514            },
1515            wgpu::BindGroupEntry {
1516                binding: 1,
1517                resource: tri_buffer.as_entire_binding(),
1518            },
1519            wgpu::BindGroupEntry {
1520                binding: 2,
1521                resource: node_buffer.as_entire_binding(),
1522            },
1523            wgpu::BindGroupEntry {
1524                binding: 3,
1525                resource: shape_buffer.as_entire_binding(),
1526            },
1527            wgpu::BindGroupEntry {
1528                binding: 4,
1529                resource: material_buffer.as_entire_binding(),
1530            },
1531            wgpu::BindGroupEntry {
1532                binding: 5,
1533                resource: uv_buffer.as_entire_binding(),
1534            },
1535            wgpu::BindGroupEntry {
1536                binding: 6,
1537                resource: content_buffer.as_entire_binding(),
1538            },
1539            wgpu::BindGroupEntry {
1540                binding: 7,
1541                resource: accum_buffer.as_entire_binding(),
1542            },
1543            wgpu::BindGroupEntry {
1544                binding: 8,
1545                resource: wgpu::BindingResource::TextureView(&color.view),
1546            },
1547            wgpu::BindGroupEntry {
1548                binding: 9,
1549                resource: wgpu::BindingResource::TextureView(&albedo.view),
1550            },
1551            wgpu::BindGroupEntry {
1552                binding: 10,
1553                resource: wgpu::BindingResource::TextureView(&normal.view),
1554            },
1555            wgpu::BindGroupEntry {
1556                binding: 11,
1557                resource: content_info_buffer.as_entire_binding(),
1558            },
1559            wgpu::BindGroupEntry {
1560                binding: 12,
1561                resource: surface_buffer.as_entire_binding(),
1562            },
1563            wgpu::BindGroupEntry {
1564                binding: 13,
1565                resource: layer_buffer.as_entire_binding(),
1566            },
1567            wgpu::BindGroupEntry {
1568                binding: 19,
1569                resource: wgpu::BindingResource::TextureView(&sobol_view),
1570            },
1571        ];
1572        if lit {
1573            entries.extend([
1574                wgpu::BindGroupEntry {
1575                    binding: 14,
1576                    resource: emitter_buffer.as_entire_binding(),
1577                },
1578                wgpu::BindGroupEntry {
1579                    binding: 15,
1580                    resource: emitter_cdf_buffer.as_entire_binding(),
1581                },
1582                wgpu::BindGroupEntry {
1583                    binding: 16,
1584                    resource: light_buffer.as_entire_binding(),
1585                },
1586            ]);
1587        }
1588        if let Some(rt) = &rt {
1589            entries.retain(|entry| entry.binding != 2);
1590            entries.extend([
1591                wgpu::BindGroupEntry {
1592                    binding: 17,
1593                    resource: rt.tlas.as_binding(),
1594                },
1595                wgpu::BindGroupEntry {
1596                    binding: 18,
1597                    resource: rt.map.as_entire_binding(),
1598                },
1599            ]);
1600        }
1601        let binding = gpu.device.create_bind_group(&wgpu::BindGroupDescriptor {
1602            label: Some("trace bindings"),
1603            layout: &pipeline.get_bind_group_layout(0),
1604            entries: &entries,
1605        });
1606        let environment_binding = environment_binding(gpu, &pipeline, &scene.sky.clone().into())?;
1607        let base = Frame {
1608            size: [width, height, 0, 0],
1609            counts: [buffers.triangle_count, buffers.shape_count, 0, 0],
1610            origin: vec4(scene.camera.origin, 0.0),
1611            forward: vec4(scene.camera.forward, 0.0),
1612            right: vec4(scene.camera.right, 0.0),
1613            up: vec4(scene.camera.up, 0.0),
1614            sun_dir: vec4(scene.sun.direction, 0.0),
1615            sun_color: vec4(scene.sun.color, scene.sun.intensity),
1616            lens: [
1617                detail.lens.shift[0],
1618                detail.lens.shift[1],
1619                detail.lens.aperture * 0.5,
1620                detail.lens.focus_distance,
1621            ],
1622            window: [width, height, 0, 0],
1623            columns: [0; 4],
1624        };
1625        let mut base = base;
1626        detail.projection.place(&mut base);
1627        base.sun_dir[3] = detail.sun_radius_deg.to_radians().cos();
1628        Ok(Self {
1629            width,
1630            height,
1631            samples: 0,
1632            projection: detail.projection,
1633            base,
1634            frame,
1635            binding,
1636            environment_binding,
1637            pipeline,
1638            materials: scene.materials.len() as u32,
1639            accum: accum_buffer,
1640            probe_parts,
1641            pacer: Self::banded_pacer(),
1642            turns: Turns::default(),
1643            lanes: Lanes::new(),
1644            color,
1645            albedo,
1646            normal,
1647            rt,
1648        })
1649    }
1650
1651    pub fn hardware_rays(&self) -> bool {
1652        self.rt.is_some()
1653    }
1654
1655    fn banded_pacer() -> Pacer {
1656        let mut pacer = Pacer::default();
1657        pacer.set_step(BAND_STEP, BAND_STEP);
1658        pacer.set_target_ms(SAMPLE_TARGET_MS);
1659        pacer.set_ceiling_ms(SAMPLE_SPAN_MS);
1660        pacer
1661    }
1662
1663    pub fn set_environment(&mut self, gpu: &Gpu, environment: &Environment) -> Result<(), String> {
1664        self.environment_binding = environment_binding(gpu, &self.pipeline, environment)?;
1665        self.samples = 0;
1666        Ok(())
1667    }
1668
1669    pub fn set_camera(&mut self, camera: Camera) -> Result<(), String> {
1670        if !finite3(camera.origin)
1671            || !finite3(camera.forward)
1672            || !finite3(camera.right)
1673            || !finite3(camera.up)
1674        {
1675            return Err("camera contains invalid parameters".into());
1676        }
1677        if !self.projection.fits(&camera) {
1678            return Err("an orthographic or equirect camera needs forward, right and up".into());
1679        }
1680        self.base.origin = vec4(camera.origin, self.base.origin[3]);
1681        self.base.forward = vec4(camera.forward, self.base.forward[3]);
1682        self.base.right = vec4(camera.right, self.base.right[3]);
1683        self.base.up = vec4(camera.up, self.base.up[3]);
1684        self.samples = 0;
1685        Ok(())
1686    }
1687
1688    pub fn set_window(&mut self, full: [u32; 2], origin: [u32; 2]) -> Result<(), String> {
1689        let fits = |k: usize, side: u32| {
1690            origin[k]
1691                .checked_add(side)
1692                .is_some_and(|end| end <= full[k])
1693        };
1694        if !fits(0, self.width) || !fits(1, self.height) {
1695            return Err("the window must lie inside the full frame".into());
1696        }
1697        if u64::from(full[0]) * u64::from(full[1]) > u64::from(u32::MAX) {
1698            return Err("the full frame is too large".into());
1699        }
1700        self.base.window = [full[0], full[1], origin[0], origin[1]];
1701        self.samples = 0;
1702        Ok(())
1703    }
1704
1705    pub fn window(&self) -> ([u32; 2], [u32; 2]) {
1706        let [width, height, x, y] = self.base.window;
1707        ([width, height], [x, y])
1708    }
1709
1710    pub fn set_lens(&mut self, lens: Lens) -> Result<(), String> {
1711        if !lens.valid() {
1712            return Err("invalid lens".into());
1713        }
1714        self.base.lens = [
1715            lens.shift[0],
1716            lens.shift[1],
1717            lens.aperture * 0.5,
1718            lens.focus_distance,
1719        ];
1720        self.samples = 0;
1721        Ok(())
1722    }
1723
1724    pub fn set_projection(&mut self, projection: Projection) -> Result<(), String> {
1725        if !projection.valid() {
1726            return Err("invalid projection".into());
1727        }
1728        let camera = Camera {
1729            origin: std::array::from_fn(|k| self.base.origin[k]),
1730            forward: std::array::from_fn(|k| self.base.forward[k]),
1731            right: std::array::from_fn(|k| self.base.right[k]),
1732            up: std::array::from_fn(|k| self.base.up[k]),
1733        };
1734        if !projection.fits(&camera) {
1735            return Err("an orthographic or equirect camera needs forward, right and up".into());
1736        }
1737        projection.place(&mut self.base);
1738        self.projection = projection;
1739        self.samples = 0;
1740        Ok(())
1741    }
1742
1743    pub fn set_sun_radius(&mut self, radius_deg: f32) -> Result<(), String> {
1744        if !radius_deg.is_finite() || !(0.0..90.0).contains(&radius_deg) {
1745            return Err("invalid sun radius".into());
1746        }
1747        self.base.sun_dir[3] = radius_deg.to_radians().cos();
1748        self.samples = 0;
1749        Ok(())
1750    }
1751
1752    pub fn set_daylight(
1753        &mut self,
1754        gpu: &Gpu,
1755        daylight: Daylight,
1756        turbidity: f32,
1757        ground_albedo: [f32; 3],
1758    ) -> Result<(), String> {
1759        let environment = Environment::Analytic(crate::sky::AnalyticSky::new(
1760            daylight,
1761            REFERENCE_HOUR,
1762            turbidity,
1763            ground_albedo,
1764        ));
1765        self.set_environment(gpu, &environment)?;
1766        let sun = Sun::from_daylight(daylight);
1767        self.base.sun_dir = vec4(sun.direction, self.base.sun_dir[3]);
1768        self.base.sun_color = vec4(sun.color, sun.intensity);
1769        Ok(())
1770    }
1771
1772    pub fn sample(&mut self, gpu: &Gpu, count: u32, seed: u32) -> Result<(), String> {
1773        let mut pacer = std::mem::take(&mut self.pacer);
1774        let mut turns = std::mem::take(&mut self.turns);
1775        let result = self.sample_sliced(gpu, count, seed, &mut pacer, |ms| turns.add(ms));
1776        self.pacer = pacer;
1777        self.turns = turns;
1778        result.map(|_| ())
1779    }
1780
1781    pub fn sample_paced<T: Turned>(
1782        &mut self,
1783        gpu: &Gpu,
1784        count: u32,
1785        seed: u32,
1786        pacer: &mut Pacer,
1787        between: impl FnMut(f64) -> T,
1788    ) -> Result<Stats, String> {
1789        let (rows, columns) = pacer.step();
1790        let (target, ceiling) = (pacer.target_ms(), pacer.ceiling_ms());
1791        pacer.set_step(BAND_STEP, BAND_STEP);
1792        pacer.set_target_ms(SAMPLE_TARGET_MS);
1793        pacer.set_ceiling_ms(SAMPLE_SPAN_MS);
1794        let result = self.sample_sliced(gpu, count, seed, pacer, between);
1795        pacer.set_step(rows, columns);
1796        pacer.set_target_ms(target);
1797        pacer.set_ceiling_ms(ceiling);
1798        result
1799    }
1800
1801    pub fn lanes(&self) -> u32 {
1802        self.lanes.width
1803    }
1804
1805    fn sample_sliced<T: Turned>(
1806        &mut self,
1807        gpu: &Gpu,
1808        count: u32,
1809        seed: u32,
1810        pacer: &mut Pacer,
1811        between: impl FnMut(f64) -> T,
1812    ) -> Result<Stats, String> {
1813        self.samples
1814            .checked_add(count)
1815            .ok_or("sample count overflow")?;
1816        let step = pacer.step();
1817        let result = self.sample_passes(gpu, count, seed, pacer, between);
1818        pacer.set_step(step.0, step.1);
1819        result
1820    }
1821
1822    fn sample_passes<T: Turned>(
1823        &mut self,
1824        gpu: &Gpu,
1825        count: u32,
1826        seed: u32,
1827        pacer: &mut Pacer,
1828        mut between: impl FnMut(f64) -> T,
1829    ) -> Result<Stats, String> {
1830        let mut stats = Stats::default();
1831        let fixed = pacer.fixed_units();
1832        let blocks = self.height.div_ceil(BAND_STEP);
1833        let columns = self.width.div_ceil(8);
1834        let per_run = (u32::MAX / (blocks * BAND_STEP)).max(1);
1835        let mut done = 0;
1836        while done < count {
1837            if fixed.is_some() || (self.lanes.width >= LANES && self.lanes.pieces == 1) {
1838                if fixed.is_none() {
1839                    pacer.set_step(BAND_STEP, BAND_STEP);
1840                    if pacer.learned_units(SAMPLE_LABEL).is_none() {
1841                        pacer.teach(SAMPLE_LABEL, self.lanes.strips(blocks) * BAND_STEP);
1842                    }
1843                }
1844                let whole = fixed.is_some()
1845                    || pacer
1846                        .learned_units(SAMPLE_LABEL)
1847                        .is_some_and(|units| units >= blocks * BAND_STEP);
1848                let samples = if whole {
1849                    (count - done).min(per_run)
1850                } else {
1851                    1
1852                };
1853                let pass = Pass {
1854                    lanes: (0, LANES),
1855                    per_block: BAND_STEP,
1856                    columns: (0, columns),
1857                };
1858                let spread =
1859                    self.pass(gpu, seed, samples, pass, pacer, &mut between, &mut stats)?;
1860                if fixed.is_none() {
1861                    self.lanes.learn_dense(spread, columns);
1862                }
1863                self.samples += samples;
1864                done += samples;
1865                continue;
1866            }
1867            if self.lanes.width >= LANES {
1868                let mut at = 0;
1869                while at < columns {
1870                    let pieces = if self.lanes.width < LANES {
1871                        most_pieces(columns)
1872                    } else {
1873                        self.lanes.pieces
1874                    };
1875                    let span = columns.div_ceil(pieces);
1876                    let end = (at + span).min(columns);
1877                    pacer.set_step(1, 1);
1878                    pacer.set_fixed_units(Some(self.lanes.strips(blocks)));
1879                    let pass = Pass {
1880                        lanes: (0, LANES),
1881                        per_block: 1,
1882                        columns: (at, end),
1883                    };
1884                    let spread = self.pass(gpu, seed, 1, pass, pacer, &mut between, &mut stats);
1885                    pacer.set_fixed_units(fixed);
1886                    let spread = spread?;
1887                    if end - at == span && self.lanes.width >= LANES {
1888                        self.lanes.learn_pieces(spread.longest, blocks, columns);
1889                    }
1890                    at = end;
1891                }
1892                self.samples += 1;
1893                done += 1;
1894                continue;
1895            }
1896            let mut lane = 0;
1897            while lane < LANES {
1898                let configured = self.lanes.width;
1899                let width = floor_power_of_two(configured.min(LANES - lane));
1900                pacer.set_step(1, 1);
1901                pacer.set_fixed_units(Some(self.lanes.strips(blocks)));
1902                let pass = Pass {
1903                    lanes: (lane, lane + width),
1904                    per_block: 1,
1905                    columns: (0, columns),
1906                };
1907                let spread = self.pass(gpu, seed, 1, pass, pacer, &mut between, &mut stats);
1908                pacer.set_fixed_units(fixed);
1909                let spread = spread?;
1910                if width == configured {
1911                    self.lanes.learn(spread.longest, blocks);
1912                }
1913                lane += width;
1914            }
1915            self.samples += 1;
1916            done += 1;
1917        }
1918        Ok(stats)
1919    }
1920
1921    #[allow(clippy::too_many_arguments)]
1922    fn pass<T: Turned>(
1923        &self,
1924        gpu: &Gpu,
1925        seed: u32,
1926        samples: u32,
1927        pass: Pass,
1928        pacer: &mut Pacer,
1929        between: &mut impl FnMut(f64) -> T,
1930        stats: &mut Stats,
1931    ) -> Result<Spread, String> {
1932        let blocks = self.height.div_ceil(BAND_STEP);
1933        let accumulated = self.samples;
1934        let work = Work::Bands {
1935            width: self.width,
1936            height: blocks * pass.per_block * samples,
1937        };
1938        let mut sizes = Vec::new();
1939        let run = pacer.run(
1940            SAMPLE_LABEL,
1941            &gpu.device,
1942            &gpu.queue,
1943            work,
1944            |encoder, slice| {
1945                if let Slice::Pixels { height, .. } = slice {
1946                    sizes.push(height);
1947                }
1948                self.encode(gpu, encoder, slice, accumulated, seed, pass)
1949            },
1950            between,
1951        )?;
1952        let spread = Spread {
1953            longest: run.longest_ms(),
1954            single: sizes
1955                .iter()
1956                .zip(&run.milliseconds)
1957                .filter(|(units, _)| **units <= pass.per_block)
1958                .fold(0.0, |longest, (_, ms)| ms.max(longest)),
1959        };
1960        stats.milliseconds.extend(run.milliseconds);
1961        stats.timings.extend(run.timings);
1962        stats.wall_ms += run.wall_ms;
1963        Ok(spread)
1964    }
1965
1966    fn encode(
1967        &self,
1968        gpu: &Gpu,
1969        encoder: &mut wgpu::CommandEncoder,
1970        slice: Slice,
1971        accumulated: u32,
1972        seed: u32,
1973        pass: Pass,
1974    ) {
1975        let Slice::Pixels { y, height, .. } = slice else {
1976            return;
1977        };
1978        let blocks = self.height.div_ceil(BAND_STEP);
1979        let (first, end) = pass.lanes;
1980        let share = (end - first) / pass.per_block;
1981        for piece in pieces(blocks, pass.per_block, y, height) {
1982            self.dispatch(
1983                gpu,
1984                encoder,
1985                piece.order,
1986                piece.blocks,
1987                (first + piece.from * share, first + piece.to * share),
1988                pass.columns,
1989                accumulated + piece.sample,
1990                piece.samples,
1991                seed,
1992            );
1993        }
1994    }
1995
1996    #[allow(clippy::too_many_arguments)]
1997    fn dispatch(
1998        &self,
1999        gpu: &Gpu,
2000        encoder: &mut wgpu::CommandEncoder,
2001        order: u32,
2002        blocks: u32,
2003        lanes: (u32, u32),
2004        columns: (u32, u32),
2005        accumulated: u32,
2006        samples: u32,
2007        seed: u32,
2008    ) {
2009        let mut frame = self.base;
2010        frame.columns[0] = columns.0;
2011        frame.size[2] = accumulated;
2012        frame.size[3] = samples;
2013        frame.counts[2] = seed;
2014        frame.counts[3] = order | (lanes.0 << 16) | (lanes.1 << 23);
2015        let staging = gpu
2016            .device
2017            .create_buffer_init(&wgpu::util::BufferInitDescriptor {
2018                label: Some("trace frame slice"),
2019                contents: bytemuck::bytes_of(&frame),
2020                usage: wgpu::BufferUsages::COPY_SRC,
2021            });
2022        encoder.copy_buffer_to_buffer(
2023            &staging,
2024            0,
2025            &self.frame,
2026            0,
2027            std::mem::size_of::<Frame>() as u64,
2028        );
2029        let mut pass = encoder.begin_compute_pass(&wgpu::ComputePassDescriptor {
2030            label: Some("trace"),
2031            timestamp_writes: None,
2032        });
2033        pass.set_pipeline(&self.pipeline);
2034        pass.set_bind_group(0, &self.binding, &[]);
2035        pass.set_bind_group(1, &self.environment_binding, &[]);
2036        pass.dispatch_workgroups(columns.1 - columns.0, blocks, 1);
2037    }
2038
2039    pub fn samples(&self) -> u32 {
2040        self.samples
2041    }
2042    pub fn readback_color(&self, gpu: &Gpu) -> Result<Vec<u8>, String> {
2043        gpu.readback_bytes(&self.color)
2044    }
2045
2046    pub fn readback(&self, gpu: &Gpu) -> Result<Output, String> {
2047        Ok(Output {
2048            color: self.readback_color(gpu)?,
2049            albedo: gpu.readback_bytes(&self.albedo)?,
2050            normal: gpu.readback_bytes(&self.normal)?,
2051        })
2052    }
2053}
2054
2055#[cfg(test)]
2056mod tests {
2057    use super::*;
2058    use crate::detail::TriangleDetail;
2059    use crate::sobol::Sampler;
2060
2061    #[test]
2062    #[ignore = "needs a GPU; run under pgpu"]
2063    fn content_layer_matches_cpu_composite_and_clips() {
2064        use pfx_materials::{Blend, Shaded};
2065
2066        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2067        let triangle = TriangleDetail {
2068            vertices: [[-2.0, -2.0, -2.0], [2.0, -2.0, -2.0], [0.0, 2.0, -2.0]],
2069            normals: [[0.0, 0.0, 1.0]; 3],
2070            uvs: [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]],
2071            material: 0,
2072        };
2073        let texel = [0.12, 0.04, 0.02, 0.5];
2074        let mut material = Material {
2075            base: [0.8, 0.6, 0.4],
2076            roughness: 0.9,
2077            emission: [0.2, 0.1, 0.0],
2078            ..Material::default()
2079        };
2080        let scene = |material| Scene {
2081            triangles: Vec::new(),
2082            shapes: Vec::new(),
2083            materials: vec![material],
2084            sky: Sky {
2085                width: 1,
2086                height: 1,
2087                texels: vec![[0.0; 4]],
2088            },
2089            camera: Camera {
2090                origin: [0.0; 3],
2091                forward: [0.0, 0.0, -1.0],
2092                right: [0.7, 0.0, 0.0],
2093                up: [0.0, 0.7, 0.0],
2094            },
2095            sun: Sun {
2096                direction: [0.0, 1.0, 0.0],
2097                color: [0.0; 3],
2098                intensity: 0.0,
2099            },
2100        };
2101        let mut detail = Detail {
2102            instances: vec![crate::detail::Instance {
2103                triangles: vec![triangle],
2104                transform: [
2105                    [1.0, 0.0, 0.0, 0.0],
2106                    [0.0, 1.0, 0.0, 0.0],
2107                    [0.0, 0.0, 1.0, 0.0],
2108                    [0.0, 0.0, 0.0, 1.0],
2109                ],
2110                surface: InstanceSurface::default(),
2111                shadow_only: false,
2112                two_sided: true,
2113            }],
2114            content_slots: vec![Some(crate::detail::ContentImage {
2115                width: 1,
2116                height: 1,
2117                texels: vec![texel],
2118            })],
2119            ..Detail::default()
2120        };
2121        let channel = |bytes: &[u8], x: usize, y: usize, c: usize| {
2122            let at = (y * 32 + x) * 16 + c * 4;
2123            f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap())
2124        };
2125        for blend in [Blend::Over, Blend::Multiply, Blend::Emit] {
2126            let layer = ContentLayer {
2127                slot: 0,
2128                blend,
2129                ink_roughness: 0.55,
2130                emboss: 0.0,
2131                strength: 1.2,
2132            };
2133            material.content_layer = layer;
2134            let expected = layer.composite(&Shaded::from_material(&material), texel);
2135            let mut trace = Trace::new_detailed(&gpu, &scene(material), &detail, 32, 32).unwrap();
2136            trace.sample(&gpu, 1, 29).unwrap();
2137            let output = trace.readback(&gpu).unwrap();
2138            let mut repeated =
2139                Trace::new_detailed(&gpu, &scene(material), &detail, 32, 32).unwrap();
2140            repeated.sample(&gpu, 1, 29).unwrap();
2141            assert_eq!(output, repeated.readback(&gpu).unwrap());
2142            for c in 0..3 {
2143                assert!((channel(&output.albedo, 16, 16, c) - expected.base[c]).abs() < 1e-5);
2144                if blend == Blend::Emit {
2145                    assert!(
2146                        (channel(&output.color, 16, 16, c) - expected.emission[c]).abs() < 1e-3
2147                    );
2148                }
2149            }
2150        }
2151        detail.instances[0].surface.clip = [[1.0, 0.0, 0.0, 0.0], [0.0, 1.0, 0.0, 0.0]];
2152        let mut trace = Trace::new_detailed(&gpu, &scene(material), &detail, 32, 32).unwrap();
2153        trace.sample(&gpu, 1, 29).unwrap();
2154        let output = trace.readback(&gpu).unwrap();
2155        assert!(channel(&output.albedo, 8, 8, 0) < 1e-6);
2156        assert!(channel(&output.albedo, 24, 24, 0) < 1e-6);
2157        assert!(channel(&output.albedo, 8, 24, 0) > 0.1);
2158    }
2159
2160    #[test]
2161    #[ignore = "needs a GPU; run under pgpu"]
2162    fn detailed_lens_instance_and_determinism() {
2163        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2164        let scene = Scene {
2165            triangles: Vec::new(),
2166            shapes: Vec::new(),
2167            materials: vec![Material {
2168                emission: [4.0, 1.0, 0.5],
2169                ..Material::default()
2170            }],
2171            sky: Sky {
2172                width: 1,
2173                height: 1,
2174                texels: vec![[0.0; 4]],
2175            },
2176            camera: Camera {
2177                origin: [0.0; 3],
2178                forward: [0.0, 0.0, -1.0],
2179                right: [0.7, 0.0, 0.0],
2180                up: [0.0, 0.7, 0.0],
2181            },
2182            sun: Sun {
2183                direction: [0.0, 1.0, 0.0],
2184                color: [0.0; 3],
2185                intensity: 0.0,
2186            },
2187        };
2188        let triangle = TriangleDetail {
2189            vertices: [[-0.3, -0.3, -2.0], [0.3, -0.3, -2.0], [0.0, 0.3, -2.0]],
2190            normals: [[-0.3, 0.0, 0.95], [0.3, 0.0, 0.95], [0.0, 0.3, 0.95]],
2191            uvs: [[0.0, 0.0], [1.0, 0.0], [0.5, 1.0]],
2192            material: 0,
2193        };
2194        let baseline = Detail {
2195            triangles: vec![triangle],
2196            ..Detail::default()
2197        };
2198        let shifted = Detail {
2199            lens: Lens {
2200                shift: [0.25, 0.0],
2201                aperture: 0.12,
2202                focus_distance: 1.0,
2203            },
2204            ..baseline.clone()
2205        };
2206        let transformed = Detail {
2207            triangles: Vec::new(),
2208            instances: vec![crate::detail::Instance {
2209                triangles: vec![triangle],
2210                surface: InstanceSurface::default(),
2211                shadow_only: false,
2212                two_sided: true,
2213                transform: [
2214                    [1.0, 0.0, 0.0, 0.0],
2215                    [0.0, 1.0, 0.0, 0.0],
2216                    [0.0, 0.0, 1.0, 0.0],
2217                    [0.1, 0.0, 0.0, 1.0],
2218                ],
2219            }],
2220            ..Detail::default()
2221        };
2222        let textured = Detail {
2223            content: vec![Some(crate::detail::ContentImage {
2224                width: 2,
2225                height: 1,
2226                texels: vec![[0.0; 4], [1.0, 0.0, 0.0, 1.0]],
2227            })],
2228            ..baseline.clone()
2229        };
2230        let legacy_scene = Scene {
2231            triangles: vec![triangle.triangle()],
2232            shapes: Vec::new(),
2233            materials: scene.materials.clone(),
2234            sky: scene.sky.clone(),
2235            camera: scene.camera,
2236            sun: scene.sun,
2237        };
2238        let mut legacy = Trace::new(&gpu, &legacy_scene, 32, 32).unwrap();
2239        let mut pinhole = Trace::new_detailed(&gpu, &scene, &baseline, 32, 32).unwrap();
2240        let mut first = Trace::new_detailed(&gpu, &scene, &shifted, 32, 32).unwrap();
2241        let mut second = Trace::new_detailed(&gpu, &scene, &shifted, 32, 32).unwrap();
2242        let mut mover = Trace::new_detailed(&gpu, &scene, &transformed, 32, 32).unwrap();
2243        let mut content = Trace::new_detailed(&gpu, &scene, &textured, 32, 32).unwrap();
2244        for trace in [
2245            &mut legacy,
2246            &mut pinhole,
2247            &mut first,
2248            &mut second,
2249            &mut mover,
2250            &mut content,
2251        ] {
2252            trace.sample(&gpu, 8, 29).unwrap();
2253        }
2254        let legacy = legacy.readback(&gpu).unwrap();
2255        let pinhole = pinhole.readback(&gpu).unwrap();
2256        let first = first.readback(&gpu).unwrap();
2257        let second = second.readback(&gpu).unwrap();
2258        let mover = mover.readback(&gpu).unwrap();
2259        let content = content.readback(&gpu).unwrap();
2260        assert_eq!(legacy.color, pinhole.color);
2261        assert_eq!(first, second);
2262        assert_ne!(first.color, pinhole.color);
2263        assert_ne!(mover.color, pinhole.color);
2264        let red = |bytes: &[u8], x: usize| {
2265            f32::from_le_bytes(
2266                bytes[(16 * 32 + x) * 16..(16 * 32 + x) * 16 + 4]
2267                    .try_into()
2268                    .unwrap(),
2269            )
2270        };
2271        assert!(red(&content.albedo, 17) > red(&content.albedo, 15));
2272        assert!(red(&content.normal, 17) > red(&content.normal, 15));
2273    }
2274    #[test]
2275    #[ignore = "needs a GPU; run under pgpu"]
2276    fn retarget_matches_fresh_trace() {
2277        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2278        let mut scene = Scene {
2279            triangles: Vec::new(),
2280            shapes: vec![Shape::Ellipsoid {
2281                center: [0.0, 0.0, -3.0],
2282                radii: [1.0; 3],
2283                material: 0,
2284            }],
2285            materials: vec![Material::default()],
2286            sky: Sky {
2287                width: 1,
2288                height: 1,
2289                texels: vec![[0.4, 0.5, 0.6, 1.0]],
2290            },
2291            camera: Camera {
2292                origin: [0.0; 3],
2293                forward: [0.0, 0.0, -1.0],
2294                right: [1.0, 0.0, 0.0],
2295                up: [0.0, 1.0, 0.0],
2296            },
2297            sun: Sun {
2298                direction: [0.0, 1.0, 0.0],
2299                color: [0.0; 3],
2300                intensity: 0.0,
2301            },
2302        };
2303        let mut retargeted = Trace::new(&gpu, &scene, 16, 16).unwrap();
2304        retargeted.sample(&gpu, 2, 19).unwrap();
2305        scene.camera = Camera {
2306            origin: [0.4, 0.2, 0.0],
2307            forward: [-0.1, -0.05, -1.0],
2308            right: [1.0, 0.0, -0.1],
2309            up: [0.0, 1.0, -0.05],
2310        };
2311        retargeted.set_camera(scene.camera).unwrap();
2312        assert_eq!(retargeted.samples(), 0);
2313        let mut fresh = Trace::new(&gpu, &scene, 16, 16).unwrap();
2314        retargeted.sample(&gpu, 4, 29).unwrap();
2315        fresh.sample(&gpu, 4, 29).unwrap();
2316        assert_eq!(
2317            retargeted.readback(&gpu).unwrap(),
2318            fresh.readback(&gpu).unwrap()
2319        );
2320    }
2321    #[test]
2322    fn shader_assembles() {
2323        let lit = shader_source(true, false);
2324        let dark = shader_source(false, false);
2325        assert!(lit.contains(pfx_materials::NOISE));
2326        assert!(lit.contains("@binding(16)"));
2327        assert!(!dark.contains("@binding(14)"));
2328        let baked = format!(
2329            "{}\n{}\n{}\n{}",
2330            pfx_materials::BRDF,
2331            pfx_materials::CONTENT,
2332            ENVIRONMENT_WGSL.as_str(),
2333            include_str!("trace.wgsl")
2334        );
2335        for source in [lit, dark, baked] {
2336            let module = naga::front::wgsl::parse_str(&source).unwrap();
2337            naga::valid::Validator::new(
2338                naga::valid::ValidationFlags::all(),
2339                naga::valid::Capabilities::all(),
2340            )
2341            .validate(&module)
2342            .unwrap();
2343        }
2344    }
2345    #[test]
2346    fn the_shader_budget_defaults_to_the_reference_budget() {
2347        let source = include_str!("trace.wgsl");
2348        for (name, value) in bounce_limits(Bounces::REFERENCE) {
2349            let line = format!("override {name}: u32 = {}u;", value as u32);
2350            assert!(source.contains(&line), "{line}");
2351        }
2352    }
2353    #[test]
2354    fn the_firefly_settings_reach_the_shader_only_when_on() {
2355        let source = include_str!("trace.wgsl");
2356        assert!(source.contains("override CLAMP_INDIRECT: f32 = 0.0;"));
2357        assert!(source.contains("override FILTER_GLOSSY: f32 = 0.0;"));
2358        let named = |detail: &Detail| -> Vec<(&'static str, f64)> {
2359            pipeline_constants(detail, 64)
2360                .into_iter()
2361                .filter(|(name, _)| ["CLAMP_INDIRECT", "FILTER_GLOSSY"].contains(name))
2362                .collect()
2363        };
2364        assert!(named(&Detail::default()).is_empty());
2365        let on = Detail {
2366            clamp_indirect: 8.0,
2367            filter_glossy: 0.15,
2368            ..Detail::default()
2369        };
2370        assert_eq!(
2371            named(&on),
2372            [
2373                ("CLAMP_INDIRECT", 8.0),
2374                ("FILTER_GLOSSY", f64::from(0.15f32))
2375            ]
2376        );
2377    }
2378    #[test]
2379    #[ignore = "needs a GPU; run under pgpu"]
2380    fn shared_noise_matches_cpu_material() {
2381        use pfx_materials::{At, NoiseKind, NoiseLayer, resolve};
2382
2383        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2384        let mut material = Material {
2385            base: [0.5, 0.4, 0.3],
2386            ..Material::default()
2387        };
2388        material.layers[0] = NoiseLayer::new(NoiseKind::Value, 0.0, 0.7, 0);
2389        let expected = resolve(&material, 0.0, &At::default()).material.base;
2390        let scene = Scene {
2391            triangles: vec![Triangle {
2392                vertices: [[-4.0, -4.0, -2.0], [4.0, -4.0, -2.0], [0.0, 4.0, -2.0]],
2393                material: 0,
2394            }],
2395            shapes: Vec::new(),
2396            materials: vec![material],
2397            sky: Sky {
2398                width: 1,
2399                height: 1,
2400                texels: vec![[0.0; 4]],
2401            },
2402            camera: Camera {
2403                origin: [0.0; 3],
2404                forward: [0.0, 0.0, -1.0],
2405                right: [0.1, 0.0, 0.0],
2406                up: [0.0, 0.1, 0.0],
2407            },
2408            sun: Sun {
2409                direction: [0.0, 1.0, 0.0],
2410                color: [0.0; 3],
2411                intensity: 0.0,
2412            },
2413        };
2414        let mut trace = Trace::new(&gpu, &scene, 4, 4).unwrap();
2415        trace.sample(&gpu, 1, 17).unwrap();
2416        let bytes = trace.readback(&gpu).unwrap().albedo;
2417        for (channel, target) in expected.into_iter().enumerate() {
2418            let at = (2 * 4 + 2) * 16 + channel * 4;
2419            let actual = f32::from_le_bytes(bytes[at..at + 4].try_into().unwrap());
2420            assert!((actual - target).abs() < 1e-5, "{actual} != {target}");
2421        }
2422    }
2423    #[test]
2424    #[ignore = "needs a GPU; run under pgpu"]
2425    fn furnace_and_determinism() {
2426        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2427        let mut scene = Scene {
2428            triangles: Vec::new(),
2429            shapes: vec![Shape::Ellipsoid {
2430                center: [0.0, 0.0, -3.0],
2431                radii: [1.0; 3],
2432                material: 0,
2433            }],
2434            materials: vec![Material {
2435                base: [1.0; 3],
2436                specular: 0.0,
2437                roughness: 1.0,
2438                ..Material::default()
2439            }],
2440            sky: Sky {
2441                width: 4,
2442                height: 2,
2443                texels: vec![[1.0; 4]; 8],
2444            },
2445            camera: Camera {
2446                origin: [0.0; 3],
2447                forward: [0.0, 0.0, -1.0],
2448                right: [1.0, 0.0, 0.0],
2449                up: [0.0, 1.0, 0.0],
2450            },
2451            sun: Sun {
2452                direction: [0.0, 1.0, 0.0],
2453                color: [0.0; 3],
2454                intensity: 0.0,
2455            },
2456        };
2457        let mut first = Trace::new(&gpu, &scene, 16, 16).unwrap();
2458        let mut second = Trace::new(&gpu, &scene, 16, 16).unwrap();
2459        for pass in 0..32 {
2460            first.sample(&gpu, 4, 73 + pass).unwrap();
2461            second.sample(&gpu, 4, 73 + pass).unwrap();
2462        }
2463        let a = first.readback(&gpu).unwrap();
2464        let b = second.readback(&gpu).unwrap();
2465        assert_eq!(a, b);
2466        let mid = (8 * 16 + 8) * 16;
2467        let channels: Vec<f32> = a.color[mid..mid + 12]
2468            .chunks_exact(4)
2469            .map(|v| f32::from_le_bytes(v.try_into().unwrap()))
2470            .collect();
2471        for value in channels {
2472            assert!((value - 1.0).abs() < 0.12, "{value}");
2473        }
2474        let feature: Vec<f32> = a.albedo[mid..mid + 16]
2475            .chunks_exact(4)
2476            .map(|v| f32::from_le_bytes(v.try_into().unwrap()))
2477            .collect();
2478        assert!(feature[..3].iter().all(|value| *value > 0.95));
2479        let surface: Vec<f32> = a.normal[mid..mid + 16]
2480            .chunks_exact(4)
2481            .map(|v| f32::from_le_bytes(v.try_into().unwrap()))
2482            .collect();
2483        assert!(surface[2] > 0.8);
2484        let mut large = Trace::new(&gpu, &scene, 1920, 1080).unwrap();
2485        let start = std::time::Instant::now();
2486        large.sample(&gpu, 1, 4).unwrap();
2487        let _ = large.readback(&gpu).unwrap();
2488        eprintln!(
2489            "trace 1920x1080: {:.3} ms/sample",
2490            start.elapsed().as_secs_f64() * 1000.0
2491        );
2492        scene.camera.right = [0.0; 3];
2493        scene.camera.up = [0.0; 3];
2494        let cases = [
2495            vec![Shape::RoundedBox {
2496                center: [0.0, 0.0, -3.0],
2497                half: [1.0; 3],
2498                radius: 0.2,
2499                material: 0,
2500            }],
2501            vec![Shape::RoundCone {
2502                a: [0.0, -1.0, -3.0],
2503                b: [0.0, 1.0, -3.0],
2504                radius_a: 0.5,
2505                radius_b: 0.3,
2506                material: 0,
2507            }],
2508            vec![Shape::Capsule {
2509                a: [0.0, -1.0, -3.0],
2510                b: [0.0, 1.0, -3.0],
2511                radius: 0.5,
2512                material: 0,
2513            }],
2514            vec![Shape::Ellipsoid {
2515                center: [0.0, 0.0, -3.0],
2516                radii: [1.0; 3],
2517                material: 0,
2518            }],
2519            vec![
2520                Shape::Ellipsoid {
2521                    center: [-0.3, 0.0, -3.0],
2522                    radii: [0.7; 3],
2523                    material: 0,
2524                },
2525                Shape::Ellipsoid {
2526                    center: [0.3, 0.0, -3.0],
2527                    radii: [0.7; 3],
2528                    material: 0,
2529                },
2530                Shape::SmoothUnion {
2531                    left: 0,
2532                    right: 1,
2533                    radius: 0.2,
2534                    material: 0,
2535                },
2536            ],
2537        ];
2538        for shapes in cases {
2539            scene.shapes = shapes;
2540            let mut trace = Trace::new(&gpu, &scene, 1, 1).unwrap();
2541            trace.sample(&gpu, 1, 9).unwrap();
2542            let Output { albedo, normal, .. } = trace.readback(&gpu).unwrap();
2543            let opacity = f32::from_le_bytes(albedo[12..16].try_into().unwrap());
2544            let front = f32::from_le_bytes(normal[8..12].try_into().unwrap());
2545            assert!(opacity > 0.99 && front > 0.8);
2546        }
2547        scene.triangles = vec![Triangle {
2548            vertices: [[-1.0, -1.0, -2.0], [1.0, -1.0, -2.0], [0.0, 1.0, -2.0]],
2549            material: 1,
2550        }];
2551        scene.materials.push(Material {
2552            base: [1.0, 0.0, 0.0],
2553            ..Material::default()
2554        });
2555        let mut trace = Trace::new(&gpu, &scene, 1, 1).unwrap();
2556        trace.sample(&gpu, 1, 9).unwrap();
2557        let Output { albedo, .. } = trace.readback(&gpu).unwrap();
2558        let red = f32::from_le_bytes(albedo[0..4].try_into().unwrap());
2559        let green = f32::from_le_bytes(albedo[4..8].try_into().unwrap());
2560        assert!(red > 0.99 && green < 0.01);
2561    }
2562
2563    fn reference_hit(o: [f32; 3], d: [f32; 3]) -> Option<(f32, [f32; 3])> {
2564        let center = [0.0, 1.0, -3.0];
2565        let oc = crate::math::sub(o, center);
2566        let b = crate::math::dot(oc, d);
2567        let c = crate::math::dot(oc, oc) - 1.0;
2568        let discriminant = b * b - c;
2569        let sphere = if discriminant >= 0.0 {
2570            let t = -b - discriminant.sqrt();
2571            let t = if t > 0.0001 {
2572                t
2573            } else {
2574                -b + discriminant.sqrt()
2575            };
2576            if t > 0.0001 {
2577                let point = crate::math::add(o, crate::math::scale(d, t));
2578                Some((t, crate::math::normalize(crate::math::sub(point, center))))
2579            } else {
2580                None
2581            }
2582        } else {
2583            None
2584        };
2585        let plane = if d[1].abs() > 1e-6 {
2586            let t = -o[1] / d[1];
2587            let p = crate::math::add(o, crate::math::scale(d, t));
2588            if t > 0.0001 && p[0].abs() < 8.0 && (p[2] + 3.0).abs() < 8.0 {
2589                Some((t, [0.0, 1.0, 0.0]))
2590            } else {
2591                None
2592            }
2593        } else {
2594            None
2595        };
2596        match (sphere, plane) {
2597            (Some(a), Some(b)) => Some(if a.0 < b.0 { a } else { b }),
2598            (Some(a), None) => Some(a),
2599            (None, Some(b)) => Some(b),
2600            _ => None,
2601        }
2602    }
2603
2604    #[test]
2605    #[ignore = "needs a GPU; run under pgpu"]
2606    fn traced_and_live_sky_lighting_agree() {
2607        use crate::math::{add, cross, dot, normalize, scale, spawn_point};
2608        use pfx_materials::{Shaded, eval};
2609        use std::f32::consts::{PI, TAU};
2610
2611        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2612        let daylight = Daylight {
2613            hour: 16.0,
2614            day: 172.0,
2615            latitude: 45.0,
2616            heading: 180.0,
2617        };
2618        let model = crate::sky::AnalyticSky::new(daylight, REFERENCE_HOUR, 2.5, [0.2, 0.25, 0.2]);
2619        let material = Material {
2620            base: [0.1; 3],
2621            roughness: 1.0,
2622            specular: 0.0,
2623            ..Material::default()
2624        };
2625        let camera = Camera {
2626            origin: [0.0, 1.8, 1.0],
2627            forward: normalize([0.0, -0.14, -1.0]),
2628            right: [0.75, 0.0, 0.0],
2629            up: [0.0, 0.75, -0.105],
2630        };
2631        let scene = Scene {
2632            triangles: vec![
2633                Triangle {
2634                    vertices: [[-8.0, 0.0, -11.0], [8.0, 0.0, -11.0], [8.0, 0.0, 5.0]],
2635                    material: 0,
2636                },
2637                Triangle {
2638                    vertices: [[-8.0, 0.0, -11.0], [8.0, 0.0, 5.0], [-8.0, 0.0, 5.0]],
2639                    material: 0,
2640                },
2641            ],
2642            shapes: vec![Shape::Ellipsoid {
2643                center: [0.0, 1.0, -3.0],
2644                radii: [1.0; 3],
2645                material: 0,
2646            }],
2647            materials: vec![material],
2648            sky: Environment::Analytic(model),
2649            camera,
2650            sun: Sun::from_daylight(daylight),
2651        };
2652        let pcg = Detail {
2653            sampler: Sampler::Pcg,
2654            ..Detail::default()
2655        };
2656        let mut trace = Trace::new_detailed(&gpu, &scene, &pcg, 8, 8).unwrap();
2657        for pass in 0..64 {
2658            trace.sample(&gpu, 32, pass).unwrap();
2659        }
2660        let output = trace.readback(&gpu).unwrap();
2661        let shaded = Shaded::from_material(&material);
2662        let mut live_sum = [0.0_f64; 3];
2663        let mut trace_sum = [0.0_f64; 3];
2664        let mut pixels = 0;
2665        for y in 0..8 {
2666            for x in 0..8 {
2667                let ndc_x = (x as f32 + 0.5) / 8.0 * 2.0 - 1.0;
2668                let ndc_y = (y as f32 + 0.5) / 8.0 * 2.0 - 1.0;
2669                let ray = normalize(add(
2670                    add(camera.forward, scale(camera.right, ndc_x)),
2671                    scale(camera.up, -ndc_y),
2672                ));
2673                let Some((distance, n)) = reference_hit(camera.origin, ray) else {
2674                    continue;
2675                };
2676                let point = add(camera.origin, scale(ray, distance));
2677                let axis = if n[1].abs() > 0.9 {
2678                    [1.0, 0.0, 0.0]
2679                } else {
2680                    [0.0, 1.0, 0.0]
2681                };
2682                let tangent = normalize(cross(axis, n));
2683                let bitangent = cross(n, tangent);
2684                let local = |d: [f32; 3]| [dot(d, tangent), dot(d, bitangent), dot(d, n)];
2685                let wo = local(scale(ray, -1.0));
2686                let mut value = [0.0_f64; 3];
2687                for row in 0..64 {
2688                    let theta = PI * (row as f32 + 0.5) / 64.0;
2689                    for col in 0..128 {
2690                        let phi = TAU * (col as f32 + 0.5) / 128.0;
2691                        let direction = [
2692                            theta.sin() * phi.cos(),
2693                            theta.cos(),
2694                            theta.sin() * phi.sin(),
2695                        ];
2696                        let wi = local(direction);
2697                        if wi[2] <= 0.0 {
2698                            continue;
2699                        }
2700                        let origin = spawn_point(point, n, camera.origin, distance);
2701                        if reference_hit(origin, direction).is_some() {
2702                            continue;
2703                        }
2704                        let brdf = eval(&shaded, wo, wi);
2705                        let sky = model.diffuse(direction);
2706                        let weight = wi[2] * theta.sin() * PI * TAU / (64 * 128) as f32;
2707                        for channel in 0..3 {
2708                            value[channel] += (brdf[channel] * sky[channel] * weight) as f64;
2709                        }
2710                    }
2711                }
2712                let sun = scene.sun.direction;
2713                let wi = local(sun);
2714                if wi[2] > 0.0
2715                    && reference_hit(spawn_point(point, n, camera.origin, distance), sun).is_none()
2716                {
2717                    let brdf = eval(&shaded, wo, wi);
2718                    for channel in 0..3 {
2719                        value[channel] += (brdf[channel]
2720                            * wi[2]
2721                            * scene.sun.color[channel]
2722                            * scene.sun.intensity) as f64;
2723                    }
2724                }
2725                let offset = (y * 8 + x) * 16;
2726                let coverage =
2727                    f32::from_le_bytes(output.albedo[offset + 12..offset + 16].try_into().unwrap());
2728                if coverage < 0.98 {
2729                    continue;
2730                }
2731                for channel in 0..3 {
2732                    live_sum[channel] += value[channel];
2733                    trace_sum[channel] += f32::from_le_bytes(
2734                        output.color[offset + channel * 4..offset + channel * 4 + 4]
2735                            .try_into()
2736                            .unwrap(),
2737                    ) as f64;
2738                }
2739                pixels += 1;
2740            }
2741        }
2742        assert!(pixels >= 16, "{pixels}");
2743        let live_mean = live_sum.map(|v| v / pixels as f64);
2744        let trace_mean = trace_sum.map(|v| v / pixels as f64);
2745        eprintln!("16:00 live mean {live_mean:?}, traced mean {trace_mean:?}, pixels {pixels}");
2746        for channel in 0..3 {
2747            assert!((trace_mean[channel] / live_mean[channel] - 1.0).abs() < 0.05);
2748        }
2749    }
2750
2751    fn floats(bytes: &[u8]) -> Vec<[f32; 4]> {
2752        bytes
2753            .chunks_exact(16)
2754            .map(|pixel| {
2755                std::array::from_fn(|i| {
2756                    f32::from_le_bytes(pixel[i * 4..i * 4 + 4].try_into().unwrap())
2757                })
2758            })
2759            .collect()
2760    }
2761
2762    fn quad(corners: [[f32; 3]; 4], material: u32) -> [Triangle; 2] {
2763        [
2764            Triangle {
2765                vertices: [corners[0], corners[1], corners[2]],
2766                material,
2767            },
2768            Triangle {
2769                vertices: [corners[0], corners[2], corners[3]],
2770                material,
2771            },
2772        ]
2773    }
2774
2775    fn dark_sky() -> Sky {
2776        Sky {
2777            width: 1,
2778            height: 1,
2779            texels: vec![[0.0; 4]],
2780        }
2781    }
2782
2783    fn no_sun() -> Sun {
2784        Sun {
2785            direction: [0.0, 1.0, 0.0],
2786            color: [0.0; 3],
2787            intensity: 0.0,
2788        }
2789    }
2790
2791    fn softbox_room() -> Scene {
2792        let (x0, x1, y0, y1, z0, z1) = (-2.0, 2.0, 0.0, 3.0, -2.0, 2.0);
2793        let mut triangles = Vec::new();
2794        for face in [
2795            [[x0, y0, z0], [x1, y0, z0], [x1, y0, z1], [x0, y0, z1]],
2796            [[x0, y1, z0], [x0, y1, z1], [x1, y1, z1], [x1, y1, z0]],
2797            [[x0, y0, z0], [x0, y1, z0], [x1, y1, z0], [x1, y0, z0]],
2798            [[x0, y0, z1], [x1, y0, z1], [x1, y1, z1], [x0, y1, z1]],
2799            [[x0, y0, z0], [x0, y0, z1], [x0, y1, z1], [x0, y1, z0]],
2800            [[x1, y0, z0], [x1, y1, z0], [x1, y1, z1], [x1, y0, z1]],
2801        ] {
2802            triangles.extend(quad(face, 0));
2803        }
2804        for (cx, cy, cz, hx, hz) in [(-0.9, 2.6, -0.6, 0.3, 0.2), (1.0, 2.3, 0.5, 0.2, 0.25)] {
2805            triangles.extend(quad(
2806                [
2807                    [cx - hx, cy, cz - hz],
2808                    [cx + hx, cy, cz - hz],
2809                    [cx + hx, cy, cz + hz],
2810                    [cx - hx, cy, cz + hz],
2811                ],
2812                1,
2813            ));
2814        }
2815        Scene {
2816            triangles,
2817            shapes: vec![Shape::Ellipsoid {
2818                center: [-1.6, 2.6, 1.6],
2819                radii: [0.12; 3],
2820                material: 2,
2821            }],
2822            materials: vec![
2823                Material {
2824                    base: [0.7, 0.65, 0.6],
2825                    specular: 0.0,
2826                    roughness: 1.0,
2827                    ..Material::default()
2828                },
2829                Material {
2830                    base: [0.0; 3],
2831                    specular: 0.0,
2832                    emission: [8.0, 7.5, 7.0],
2833                    ..Material::default()
2834                },
2835                Material {
2836                    base: [0.0; 3],
2837                    specular: 0.0,
2838                    emission: [6.0, 3.0, 1.5],
2839                    ..Material::default()
2840                },
2841            ],
2842            sky: dark_sky(),
2843            camera: Camera {
2844                origin: [0.0, 1.4, 1.9],
2845                forward: [0.0, -0.35, -1.0],
2846                right: [0.8, 0.0, 0.0],
2847                up: [0.0, 0.47, -0.165],
2848            },
2849            sun: no_sun(),
2850        }
2851    }
2852
2853    fn render(
2854        gpu: &Gpu,
2855        scene: &Scene,
2856        detail: &Detail,
2857        size: (u32, u32),
2858        samples: u32,
2859        seed: u32,
2860        emitter_sampling: bool,
2861    ) -> Vec<[f32; 4]> {
2862        let mut trace = Trace::build(gpu, scene, detail, size.0, size.1, emitter_sampling).unwrap();
2863        let chunk = samples.min(64);
2864        for _ in 0..samples / chunk {
2865            trace.sample(gpu, chunk, seed).unwrap();
2866        }
2867        floats(&trace.readback(gpu).unwrap().color)
2868    }
2869
2870    fn brightness(pixel: [f32; 4]) -> f64 {
2871        (0.2126 * pixel[0] + 0.7152 * pixel[1] + 0.0722 * pixel[2]) as f64
2872    }
2873
2874    #[test]
2875    #[ignore = "needs a GPU; run under pgpu"]
2876    fn emitter_sampling_is_unbiased_and_quieter() {
2877        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2878        let scene = softbox_room();
2879        let detail = Detail::default();
2880        let size = (48, 36);
2881        let pixels = (size.0 * size.1) as usize;
2882        let runs = 8;
2883        let mut images = [Vec::new(), Vec::new()];
2884        for (index, sampling) in [true, false].into_iter().enumerate() {
2885            for run in 0..runs {
2886                images[index].push(render(&gpu, &scene, &detail, size, 64, 101 + run, sampling));
2887            }
2888        }
2889        let reference = render(&gpu, &scene, &detail, size, 8192, 7, false);
2890        let variance = |runs: &[Vec<[f32; 4]>]| {
2891            (0..pixels)
2892                .map(|pixel| {
2893                    let values: Vec<f64> =
2894                        runs.iter().map(|image| brightness(image[pixel])).collect();
2895                    let mean = values.iter().sum::<f64>() / values.len() as f64;
2896                    values.iter().map(|v| (v - mean).powi(2)).sum::<f64>()
2897                        / (values.len() - 1) as f64
2898                })
2899                .sum::<f64>()
2900                / pixels as f64
2901        };
2902        let image_mean =
2903            |image: &[[f32; 4]]| image.iter().map(|p| brightness(*p)).sum::<f64>() / pixels as f64;
2904        let spread = |means: &[f64]| {
2905            let mean = means.iter().sum::<f64>() / means.len() as f64;
2906            let var =
2907                means.iter().map(|v| (v - mean).powi(2)).sum::<f64>() / (means.len() - 1) as f64;
2908            (mean, var)
2909        };
2910        let nee_variance = variance(&images[0]);
2911        let bsdf_variance = variance(&images[1]);
2912        let (nee_mean, nee_var) =
2913            spread(&images[0].iter().map(|i| image_mean(i)).collect::<Vec<_>>());
2914        let (_, bsdf_var) = spread(&images[1].iter().map(|i| image_mean(i)).collect::<Vec<_>>());
2915        let reference_mean = image_mean(&reference);
2916        let sigma = (nee_var / runs as f64 + bsdf_var / (8192.0 / 64.0)).sqrt();
2917        eprintln!(
2918            "softbox room: mean with NEE {nee_mean:.5}, BSDF-only reference {reference_mean:.5}, sigma {sigma:.5}; \
2919             pixel variance at 64 spp {nee_variance:.6} with NEE, {bsdf_variance:.6} without, ratio {:.1}",
2920            bsdf_variance / nee_variance
2921        );
2922        assert!(
2923            (nee_mean - reference_mean).abs() < 4.0 * sigma,
2924            "{nee_mean} against {reference_mean}"
2925        );
2926        assert!((nee_mean / reference_mean - 1.0).abs() < 0.02);
2927        assert!(bsdf_variance >= 10.0 * nee_variance);
2928        let blocks = (4, 3);
2929        for by in 0..blocks.1 {
2930            for bx in 0..blocks.0 {
2931                let mut nee = 0.0;
2932                let mut long = 0.0;
2933                for y in by * 12..(by + 1) * 12 {
2934                    for x in bx * 12..(bx + 1) * 12 {
2935                        let at = (y * size.0 + x) as usize;
2936                        nee += images[0]
2937                            .iter()
2938                            .map(|image| brightness(image[at]))
2939                            .sum::<f64>()
2940                            / runs as f64;
2941                        long += brightness(reference[at]);
2942                    }
2943                }
2944                assert!(
2945                    (nee / long - 1.0).abs() < 0.05,
2946                    "block {bx},{by}: {nee} against {long}"
2947                );
2948            }
2949        }
2950        let again = render(&gpu, &scene, &detail, size, 64, 101, true);
2951        assert_eq!(again, images[0][0]);
2952    }
2953
2954    #[test]
2955    #[ignore = "needs a GPU; run under pgpu"]
2956    fn local_lights_match_the_live_direct_term() {
2957        use crate::lights::{LightShape, LocalLight};
2958        use crate::math::{add, dot, length, normalize, scale, sub};
2959        use pfx_materials::{Shaded, eval};
2960
2961        let gpu = pollster::block_on(Gpu::headless()).unwrap();
2962        let floor = Material {
2963            base: [0.5, 0.45, 0.4],
2964            roughness: 0.6,
2965            ..Material::default()
2966        };
2967        let sphere = ([-0.4, 0.4, 0.3], 0.25);
2968        let scene = Scene {
2969            triangles: quad(
2970                [
2971                    [-4.0, 0.0, -4.0],
2972                    [4.0, 0.0, -4.0],
2973                    [4.0, 0.0, 4.0],
2974                    [-4.0, 0.0, 4.0],
2975                ],
2976                0,
2977            )
2978            .to_vec(),
2979            shapes: vec![Shape::Ellipsoid {
2980                center: sphere.0,
2981                radii: [sphere.1; 3],
2982                material: 1,
2983            }],
2984            materials: vec![
2985                floor,
2986                Material {
2987                    base: [0.0; 3],
2988                    specular: 0.0,
2989                    ..Material::default()
2990                },
2991            ],
2992            sky: dark_sky(),
2993            camera: Camera {
2994                origin: [0.0, 4.0, 0.0],
2995                forward: [0.0, -1.0, 0.0],
2996                right: [0.6, 0.0, 0.0],
2997                up: [0.0, 0.0, -0.6],
2998            },
2999            sun: no_sun(),
3000        };
3001        let point = LocalLight {
3002            radius: 0.1,
3003            ..LocalLight::point([-0.8, 1.2, 0.3], [1.0, 0.9, 0.8], 3.0, 8.0)
3004        };
3005        let spot = LocalLight {
3006            shape: LightShape::Spot {
3007                direction: [0.0, -1.0, 0.0],
3008                inner_deg: 15.0,
3009                outer_deg: 25.0,
3010            },
3011            ..LocalLight::point([1.2, 1.5, 1.2], [0.9, 1.0, 0.8], 4.0, 10.0)
3012        };
3013        let rect = LocalLight::rect(
3014            [0.9, 1.0, -0.4],
3015            [0.3, 0.0, 0.0],
3016            [0.0, 0.0, 0.2],
3017            [0.8, 0.9, 1.0],
3018            2.0,
3019        );
3020        let detail = Detail {
3021            lights: vec![point, spot, rect],
3022            ..Detail::default()
3023        };
3024        let size = 48;
3025        let traced = render(&gpu, &scene, &detail, (size, size), 65536, 3, true);
3026        let blocked = |from: [f32; 3], to: [f32; 3], margin: f32| {
3027            let segment = sub(to, from);
3028            let length = length(segment);
3029            let along = (dot(sub(sphere.0, from), segment) / (length * length)).clamp(0.0, 1.0);
3030            crate::math::length(sub(add(from, scale(segment, along)), sphere.0)) < sphere.1 + margin
3031        };
3032        let rim: Vec<[f32; 3]> = (0..9)
3033            .map(|k| {
3034                if k == 0 {
3035                    point.position
3036                } else {
3037                    let angle = k as f32 * std::f32::consts::TAU / 8.0;
3038                    add(point.position, [0.1 * angle.cos(), 0.0, 0.1 * angle.sin()])
3039                }
3040            })
3041            .collect();
3042        let grid: Vec<[f32; 3]> = (0..81)
3043            .map(|k| {
3044                let u = (k % 9) as f32 / 4.0 - 1.0;
3045                let v = (k / 9) as f32 / 4.0 - 1.0;
3046                add(
3047                    add([0.9, 1.0, -0.4], [0.3 * u, 0.0, 0.0]),
3048                    [0.0, 0.0, 0.2 * v],
3049                )
3050            })
3051            .collect();
3052        let shaded = Shaded::from_material(&floor);
3053        let radiance = rect.colour.map(|c| c * rect.intensity / rect.area());
3054        let mut compared = [0, 0];
3055        let mut worst = [0.0_f32; 2];
3056        for y in 0..size {
3057            for x in 0..size {
3058                let mut expected = [[0.0_f64; 3]; 2];
3059                let mut lit = true;
3060                let mut umbra = true;
3061                let mut floor_hit = true;
3062                for sy in 0..4 {
3063                    for sx in 0..4 {
3064                        let ndc_x = (x as f32 + (sx as f32 + 0.5) / 4.0) / size as f32 * 2.0 - 1.0;
3065                        let ndc_y = (y as f32 + (sy as f32 + 0.5) / 4.0) / size as f32 * 2.0 - 1.0;
3066                        let ray = normalize(add(
3067                            add(scene.camera.forward, scale(scene.camera.right, ndc_x)),
3068                            scale(scene.camera.up, -ndc_y),
3069                        ));
3070                        let at = add(
3071                            scene.camera.origin,
3072                            scale(ray, -scene.camera.origin[1] / ray[1]),
3073                        );
3074                        if blocked(scene.camera.origin, at, 0.02) {
3075                            floor_hit = false;
3076                        }
3077                        let wo = [-ray[2], -ray[0], -ray[1]];
3078                        let local = |d: [f32; 3]| [d[2], d[0], d[1]];
3079                        if rim.iter().any(|&light| blocked(at, light, 0.03)) {
3080                            lit = false;
3081                        }
3082                        if !rim.iter().all(|&light| blocked(at, light, -0.03)) {
3083                            umbra = false;
3084                        }
3085                        if grid.iter().any(|&light| blocked(at, light, 0.05)) {
3086                            lit = false;
3087                            umbra = false;
3088                        }
3089                        let mut point_part = [0.0_f64; 3];
3090                        for light in [point, spot] {
3091                            let to = sub(light.position, at);
3092                            let distance = length(to);
3093                            let l = scale(to, 1.0 / distance);
3094                            let mut widened = shaded;
3095                            widened.roughness = shaded
3096                                .roughness
3097                                .max((light.radius / (2.0 * distance)).clamp(0.0, 1.0).sqrt());
3098                            let brdf = eval(&widened, wo, local(l));
3099                            let strength =
3100                                light.falloff(distance) * light.cone(scale(l, -1.0)) * l[1];
3101                            for c in 0..3 {
3102                                point_part[c] += (brdf[c] * light.colour[c] * strength) as f64;
3103                            }
3104                        }
3105                        let mut rect_part = [0.0_f64; 3];
3106                        let cells = 48;
3107                        let cell_area = rect.area() / (cells * cells) as f32;
3108                        for cv in 0..cells {
3109                            for cu in 0..cells {
3110                                let u = (cu as f32 + 0.5) / cells as f32 * 2.0 - 1.0;
3111                                let v = (cv as f32 + 0.5) / cells as f32 * 2.0 - 1.0;
3112                                let sample = add(
3113                                    add(rect.position, [0.3 * u, 0.0, 0.0]),
3114                                    [0.0, 0.0, 0.2 * v],
3115                                );
3116                                let to = sub(sample, at);
3117                                let d2 = dot(to, to);
3118                                let l = scale(to, 1.0 / d2.sqrt());
3119                                let brdf = eval(&shaded, wo, local(l));
3120                                let weight = l[1] * l[1] * cell_area / d2;
3121                                for c in 0..3 {
3122                                    rect_part[c] += (brdf[c] * radiance[c] * weight) as f64;
3123                                }
3124                            }
3125                        }
3126                        for c in 0..3 {
3127                            expected[0][c] += (point_part[c] + rect_part[c]) / 16.0;
3128                            expected[1][c] += rect_part[c] / 16.0;
3129                        }
3130                    }
3131                }
3132                if !floor_hit {
3133                    continue;
3134                }
3135                let pick = if lit {
3136                    0
3137                } else if umbra {
3138                    1
3139                } else {
3140                    continue;
3141                };
3142                let got = traced[(y * size + x) as usize];
3143                for c in 0..3 {
3144                    let error = (got[c] as f64 / expected[pick][c] - 1.0).abs() as f32;
3145                    worst[pick] = worst[pick].max(error);
3146                    assert!(
3147                        error < 0.02,
3148                        "{x},{y} channel {c}: {} against {}",
3149                        got[c],
3150                        expected[pick][c]
3151                    );
3152                }
3153                compared[pick] += 1;
3154            }
3155        }
3156        eprintln!(
3157            "local lights: {} lit pixels within {:.4}, {} point-umbra pixels within {:.4}",
3158            compared[0], worst[0], compared[1], worst[1]
3159        );
3160        assert!(compared[0] > size as usize * size as usize / 2);
3161        assert!(compared[1] >= 4);
3162    }
3163
3164    #[test]
3165    fn instance_flags_and_placed_layers_reach_the_buffers() {
3166        let triangle = TriangleDetail {
3167            vertices: [[0.0; 3], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
3168            normals: [[0.0, 0.0, 1.0]; 3],
3169            uvs: [[0.0, 0.0], [1.0, 0.0], [0.0, 1.0]],
3170            material: 0,
3171        };
3172        let instance = |surface: InstanceSurface, shadow_only: bool, two_sided: bool, x: f32| {
3173            crate::detail::Instance {
3174                triangles: vec![triangle],
3175                transform: [
3176                    [1.0, 0.0, 0.0, 0.0],
3177                    [0.0, 1.0, 0.0, 0.0],
3178                    [0.0, 0.0, 1.0, 0.0],
3179                    [x, 0.0, 0.0, 1.0],
3180                ],
3181                surface,
3182                shadow_only,
3183                two_sided,
3184            }
3185        };
3186        let placed = ContentLayer {
3187            slot: 1,
3188            ..ContentLayer::from_kind(
3189                Content::Screen,
3190                0.0,
3191                0,
3192                &pfx_materials::ContentLook {
3193                    screen_gain: 1.5,
3194                    ..Default::default()
3195                },
3196            )
3197        };
3198        let detail = Detail {
3199            instances: vec![
3200                instance(InstanceSurface::default(), false, true, 0.0),
3201                instance(
3202                    InstanceSurface {
3203                        casts_shadow: false,
3204                        alpha_cutoff: 0.25,
3205                        ..InstanceSurface::default()
3206                    },
3207                    true,
3208                    true,
3209                    2.0,
3210                ),
3211                instance(
3212                    InstanceSurface {
3213                        layer: placed,
3214                        cutout: true,
3215                        face: crate::detail::ContentFace::Back,
3216                        alpha_cutoff: 0.9,
3217                        ..InstanceSurface::default()
3218                    },
3219                    false,
3220                    true,
3221                    4.0,
3222                ),
3223                instance(InstanceSurface::default(), false, false, 6.0),
3224            ],
3225            content_slots: vec![
3226                None,
3227                Some(crate::detail::ContentImage {
3228                    width: 1,
3229                    height: 1,
3230                    texels: vec![[1.0; 4]],
3231                }),
3232            ],
3233            ..Detail::default()
3234        };
3235        let materials = [Material::default(), Material::default()];
3236        let buffers = SceneBuffers::build(&[], &[], &materials, &detail).unwrap();
3237        let triangles: &[GpuTriangle] = bytemuck::cast_slice(&buffers.triangles);
3238        let layers: &[GpuLayer] = bytemuck::cast_slice(&buffers.layers);
3239        assert_eq!(buffers.surfaces.len(), 4 * 64);
3240        let mut seen: Vec<(f32, u32)> = triangles
3241            .iter()
3242            .map(|triangle| (triangle.a[0], triangle.b[3] as u32))
3243            .collect();
3244        seen.sort_by(|a, b| a.0.total_cmp(&b.0));
3245        assert_eq!(
3246            seen,
3247            [
3248                (0.0, 0),
3249                (2.0, SHADOW_ONLY | CASTS_NO_SHADOW),
3250                (4.0, CUTOUT | 2 << FACE_SHIFT | 3 << LAYER_SHIFT),
3251                (6.0, ONE_SIDED),
3252            ]
3253        );
3254        assert_eq!(layers.len(), 3);
3255        assert_eq!(layers[2].layer, [1.0, 2.0, -1.0, 0.0]);
3256        assert_eq!(layers[2].extra[0], placed.strength);
3257        let uvs: &[[f32; 4]] = bytemuck::cast_slice(&buffers.uv);
3258        let mut lanes: Vec<(f32, f32)> = triangles
3259            .iter()
3260            .zip(uvs)
3261            .map(|(triangle, uv)| (triangle.a[0], uv[3]))
3262            .collect();
3263        lanes.sort_by(|a, b| a.0.total_cmp(&b.0));
3264        assert_eq!(lanes, [(0.0, 0.5), (2.0, 0.25), (4.0, 0.9), (6.0, 0.5)]);
3265        let flat = Triangle {
3266            vertices: [[-2.0, 0.0, 0.0], [-1.0, 0.0, 0.0], [-2.0, 1.0, 0.0]],
3267            material: 0,
3268        };
3269        let with_scene = SceneBuffers::build(&[flat], &[], &materials, &detail).unwrap();
3270        let scene_tris: &[GpuTriangle] = bytemuck::cast_slice(&with_scene.triangles);
3271        let scene_uvs: &[[f32; 4]] = bytemuck::cast_slice(&with_scene.uv);
3272        let scene_lane = scene_tris
3273            .iter()
3274            .zip(scene_uvs)
3275            .find(|(triangle, _)| triangle.a[0] == -2.0)
3276            .map(|(_, uv)| uv[3]);
3277        assert_eq!(scene_lane, Some(0.0));
3278        let glow = Material {
3279            emission: [1.0, 0.2, 0.0],
3280            ..Material::default()
3281        };
3282        let sided = |two_sided| Detail {
3283            instances: vec![instance(InstanceSurface::default(), false, two_sided, 0.0)],
3284            ..Detail::default()
3285        };
3286        assert_eq!(
3287            SceneBuffers::build(&[], &[], &[glow], &sided(false))
3288                .unwrap()
3289                .emitter_count,
3290            1
3291        );
3292        assert_eq!(
3293            SceneBuffers::build(&[], &[], &[glow], &sided(true))
3294                .unwrap()
3295                .emitter_count,
3296            1
3297        );
3298        let missing = Detail {
3299            content_slots: Vec::new(),
3300            ..detail
3301        };
3302        assert!(SceneBuffers::build(&[], &[], &materials, &missing).is_err());
3303    }
3304
3305    #[test]
3306    fn a_band_splits_into_whole_samples_whole_blocks_and_partial_blocks() {
3307        let piece = |sample, samples, order, blocks, from, to| Piece {
3308            sample,
3309            samples,
3310            order,
3311            blocks,
3312            from,
3313            to,
3314        };
3315        assert_eq!(pieces(4, 8, 0, 32), vec![piece(0, 1, 0, 4, 0, 8)]);
3316        assert_eq!(
3317            pieces(4, 8, 7, 3),
3318            vec![piece(0, 1, 0, 1, 7, 8), piece(0, 1, 1, 1, 0, 2)]
3319        );
3320        assert_eq!(
3321            pieces(4, 8, 28, 40),
3322            vec![
3323                piece(0, 1, 3, 1, 4, 8),
3324                piece(1, 1, 0, 4, 0, 8),
3325                piece(2, 1, 0, 1, 0, 4)
3326            ]
3327        );
3328        assert_eq!(
3329            pieces(2, 1, 1, 6),
3330            vec![
3331                piece(0, 1, 1, 1, 0, 1),
3332                piece(1, 2, 0, 2, 0, 1),
3333                piece(3, 1, 0, 1, 0, 1)
3334            ]
3335        );
3336        for blocks in [1, 2, 4, 5] {
3337            for per_block in [1, 8] {
3338                for band in [1, 3, 7, 8, 9, 64, 100] {
3339                    let samples = 5;
3340                    let total = blocks * per_block * samples;
3341                    let mut seen = Vec::new();
3342                    let mut at = 0;
3343                    while at < total {
3344                        let count = band.min(total - at);
3345                        let mut covered = 0;
3346                        for piece in pieces(blocks, per_block, at, count) {
3347                            assert!(piece.order + piece.blocks <= blocks);
3348                            assert!(piece.from < piece.to && piece.to <= per_block);
3349                            assert!(piece.blocks == 1 || (piece.from, piece.to) == (0, per_block));
3350                            for sample in piece.sample..piece.sample + piece.samples {
3351                                for order in piece.order..piece.order + piece.blocks {
3352                                    for unit in piece.from..piece.to {
3353                                        seen.push((sample, order, unit));
3354                                        covered += 1;
3355                                    }
3356                                }
3357                            }
3358                        }
3359                        assert_eq!(covered, count);
3360                        at += count;
3361                    }
3362                    let expected: Vec<_> = (0..samples)
3363                        .flat_map(|sample| {
3364                            (0..blocks).flat_map(move |order| {
3365                                (0..per_block).map(move |unit| (sample, order, unit))
3366                            })
3367                        })
3368                        .collect();
3369                    assert_eq!(
3370                        seen, expected,
3371                        "{blocks} blocks, {per_block} per block, band {band}"
3372                    );
3373                }
3374            }
3375        }
3376    }
3377
3378    #[test]
3379    fn the_block_stride_visits_every_block_once() {
3380        for blocks in 1..200u32 {
3381            let stride = block_stride(blocks);
3382            let mut seen: Vec<u32> = (0..blocks).map(|k| k * stride % blocks).collect();
3383            seen.sort_unstable();
3384            assert_eq!(seen, (0..blocks).collect::<Vec<_>>(), "{blocks} blocks");
3385        }
3386        assert_eq!(block_stride(135), 52);
3387        let order: Vec<u32> = (0..8).map(|k| k * block_stride(16) % 16).collect();
3388        assert_eq!(order, [0, 7, 14, 5, 12, 3, 10, 1]);
3389    }
3390
3391    #[test]
3392    fn the_lane_controller_widens_under_half_the_span_and_undoes_a_step_over_it() {
3393        assert_eq!(floor_power_of_two(1), 1);
3394        assert_eq!(floor_power_of_two(6), 4);
3395        assert_eq!(floor_power_of_two(64), 64);
3396        assert_eq!(Lanes::new().width, LANES);
3397        let mut lanes = Lanes::new();
3398        lanes.width = 1;
3399        assert_eq!(lanes.strips(6), 1);
3400        for blocks in [2, 4, 6, 6] {
3401            lanes.learn(4.3, 6);
3402            assert_eq!(lanes.blocks, blocks);
3403        }
3404        assert_eq!((lanes.width, lanes.last), (2, Step::Lanes));
3405        lanes.learn(4.5, 6);
3406        assert_eq!(lanes.width, 4);
3407        lanes.learn(11.5, 6);
3408        assert_eq!((lanes.width, lanes.widest), (2, 4));
3409        lanes.learn(4.5, 6);
3410        assert_eq!((lanes.width, lanes.last), (2, Step::Held));
3411        lanes.learn(7.0, 6);
3412        assert_eq!((lanes.width, lanes.blocks), (2, 6));
3413        lanes.learn(20.0, 6);
3414        assert_eq!((lanes.blocks, lanes.most), (1, 6));
3415        lanes.learn(5.0, 6);
3416        assert_eq!(lanes.blocks, 2);
3417        lanes.learn(f64::NAN, 6);
3418        assert_eq!(lanes.blocks, 2);
3419        lanes.blocks = 4;
3420        assert_eq!(lanes.strips(6), 3);
3421        assert_eq!(lanes.strips(135), 4);
3422        lanes.blocks = 200;
3423        assert_eq!(lanes.strips(135), 135);
3424        let mut alone = Lanes::new();
3425        alone.width = 8;
3426        alone.learn(30.0, 1);
3427        assert_eq!((alone.width, alone.widest), (1, 8));
3428        alone.learn(30.0, 1);
3429        assert_eq!(alone.width, 1);
3430        let mut dense = Lanes::new();
3431        dense.learn_dense(
3432            Spread {
3433                longest: 40.7,
3434                single: 0.0,
3435            },
3436            12,
3437        );
3438        assert_eq!(dense.width, LANES);
3439        dense.learn_dense(
3440            Spread {
3441                longest: 40.7,
3442                single: 40.7,
3443            },
3444            12,
3445        );
3446        assert_eq!((dense.width, dense.widest, dense.blocks), (1, LANES, 1));
3447    }
3448
3449    #[test]
3450    fn a_dense_wide_strip_splits_into_column_pieces_before_it_drops_lanes() {
3451        assert_eq!(
3452            (most_pieces(480), most_pieces(60), most_pieces(12)),
3453            (32, 4, 1)
3454        );
3455        let mut wide = Lanes::new();
3456        wide.learn_dense(
3457            Spread {
3458                longest: 14.0,
3459                single: 14.0,
3460            },
3461            480,
3462        );
3463        assert_eq!((wide.width, wide.pieces, wide.blocks), (LANES, 4, 1));
3464        for blocks in [2, 4, 8] {
3465            wide.learn_pieces(3.0, 270, 480);
3466            assert_eq!(wide.blocks, blocks);
3467        }
3468        wide.learn_pieces(10.0, 270, 480);
3469        assert_eq!((wide.blocks, wide.most, wide.pieces), (4, 8, 4));
3470        wide.learn_pieces(5.0, 270, 480);
3471        assert_eq!((wide.blocks, wide.pieces), (7, 4));
3472        wide.learn_pieces(5.0, 270, 480);
3473        assert_eq!((wide.pieces, wide.last), (2, Step::Pieces));
3474        wide.learn_pieces(10.0, 270, 480);
3475        assert_eq!((wide.pieces, wide.fewest), (4, 2));
3476        wide.learn_pieces(4.0, 270, 480);
3477        assert_eq!((wide.pieces, wide.blocks), (4, 7));
3478        wide.blocks = 1;
3479        wide.learn_pieces(50.0, 270, 480);
3480        assert_eq!((wide.width, wide.pieces), (LANES, 8));
3481        wide.pieces = 32;
3482        wide.learn_pieces(50.0, 270, 480);
3483        assert_eq!((wide.width, wide.widest, wide.pieces), (1, LANES, 1));
3484    }
3485
3486    #[test]
3487    #[ignore = "needs a GPU; run under pgpu"]
3488    fn sliced_samples_trace_the_same_bytes() {
3489        let gpu = pollster::block_on(Gpu::headless()).unwrap();
3490        let scene = softbox_room();
3491        let detail = Detail::default();
3492        let (width, height) = (37, 29);
3493        let calls = [(3, 5), (2, 6)];
3494        let mut turns = Turns::default();
3495        for lit in [true, false] {
3496            let fresh = || Trace::build(&gpu, &scene, &detail, width, height, lit).unwrap();
3497            let mut whole = fresh();
3498            for (count, seed) in calls {
3499                let mut encoder = gpu.device.create_command_encoder(&Default::default());
3500                whole.dispatch(
3501                    &gpu,
3502                    &mut encoder,
3503                    0,
3504                    height.div_ceil(BAND_STEP),
3505                    (0, LANES),
3506                    (0, width.div_ceil(8)),
3507                    whole.samples,
3508                    count,
3509                    seed,
3510                );
3511                gpu.queue.submit(Some(encoder.finish()));
3512                gpu.device
3513                    .poll(wgpu::PollType::wait_indefinitely())
3514                    .unwrap();
3515                whole.samples += count;
3516            }
3517            let unsliced = whole.readback(&gpu).unwrap();
3518            for (step, fixed) in [
3519                (1, Some(1)),
3520                (1, Some(7)),
3521                (1, Some(64)),
3522                (1, None),
3523                (8, Some(8)),
3524                (8, Some(16)),
3525                (8, None),
3526            ] {
3527                let mut trace = fresh();
3528                let mut pacer = Pacer::default();
3529                pacer.set_step(step, step);
3530                pacer.set_fixed_units(fixed);
3531                let mut slices = 0;
3532                for (count, seed) in calls {
3533                    let stats = trace
3534                        .sample_sliced(&gpu, count, seed, &mut pacer, |ms| turns.add(ms))
3535                        .unwrap();
3536                    slices += stats.count();
3537                }
3538                if let Some(rows) = fixed {
3539                    let expected: u32 = calls
3540                        .iter()
3541                        .map(|(count, _)| {
3542                            (height.next_multiple_of(BAND_STEP) * count).div_ceil(rows)
3543                        })
3544                        .sum();
3545                    assert_eq!(slices, expected as usize, "step {step}, {rows} rows");
3546                }
3547                assert_eq!(trace.samples(), 5);
3548                assert!(
3549                    trace.readback(&gpu).unwrap() == unsliced,
3550                    "lit {lit}, step {step}, fixed {fixed:?}: sliced bytes differ"
3551                );
3552            }
3553            let mut paced = fresh();
3554            let mut pacer = Pacer::default();
3555            let mut own = fresh();
3556            for (count, seed) in calls {
3557                paced
3558                    .sample_paced(&gpu, count, seed, &mut pacer, |ms| turns.add(ms))
3559                    .unwrap();
3560                own.sample(&gpu, count, seed).unwrap();
3561            }
3562            assert_eq!(pacer.step(), (1, 1));
3563            assert!(
3564                paced.readback(&gpu).unwrap() == unsliced,
3565                "lit {lit}: paced"
3566            );
3567            assert!(own.readback(&gpu).unwrap() == unsliced, "lit {lit}: sample");
3568            for pieces in [2, 4] {
3569                let mut split = fresh();
3570                split.lanes = Lanes {
3571                    pieces,
3572                    ..Lanes::new()
3573                };
3574                let mut pacer = Pacer::default();
3575                for (count, seed) in calls {
3576                    split
3577                        .sample_paced(&gpu, count, seed, &mut pacer, |ms| turns.add(ms))
3578                        .unwrap();
3579                }
3580                assert!(
3581                    split.readback(&gpu).unwrap() == unsliced,
3582                    "lit {lit}: {pieces} column pieces"
3583                );
3584            }
3585        }
3586    }
3587
3588    fn device_with(label: &'static str, limits: wgpu::Limits) -> Gpu {
3589        let instance = wgpu::Instance::new(wgpu::InstanceDescriptor {
3590            backends: wgpu::Backends::VULKAN | wgpu::Backends::METAL,
3591            ..wgpu::InstanceDescriptor::new_without_display_handle()
3592        });
3593        let adapter = pollster::block_on(instance.request_adapter(&wgpu::RequestAdapterOptions {
3594            power_preference: wgpu::PowerPreference::HighPerformance,
3595            ..Default::default()
3596        }))
3597        .unwrap();
3598        let (device, queue) = pollster::block_on(adapter.request_device(&wgpu::DeviceDescriptor {
3599            label: Some(label),
3600            required_features: adapter.features() & wgpu::Features::FLOAT32_FILTERABLE,
3601            required_limits: limits,
3602            ..Default::default()
3603        }))
3604        .unwrap();
3605        Gpu {
3606            info: adapter.get_info(),
3607            adapter,
3608            device,
3609            queue,
3610        }
3611    }
3612
3613    #[test]
3614    #[ignore = "needs a GPU; run under pgpu"]
3615    fn a_device_below_the_trace_floor_is_refused() {
3616        let gpu = device_with("below the trace floor", wgpu::Limits::default());
3617        let Err(error) = Trace::new(&gpu, &softbox_room(), 8, 8) else {
3618            panic!("a device with 8 storage buffers per stage traced");
3619        };
3620        let TraceError::Refused(refusal) = &error else {
3621            panic!("{error}");
3622        };
3623        assert_eq!(
3624            refusal.shortfall,
3625            GpuShortfall::Limit {
3626                name: "max_storage_buffers_per_shader_stage",
3627                needed: u64::from(pfx_gpu::floor::TRACE_STORAGE_BUFFERS_PER_STAGE),
3628                has: 8,
3629            }
3630        );
3631        let message = String::from(error);
3632        assert!(
3633            message.contains("max_storage_buffers_per_shader_stage"),
3634            "{message}"
3635        );
3636        let fit = pollster::block_on(Gpu::headless()).unwrap();
3637        assert!(Trace::new(&fit, &softbox_room(), 8, 8).is_ok());
3638    }
3639
3640    #[test]
3641    #[ignore = "needs a GPU; run under pgpu"]
3642    fn default_buffer_limits_trace_a_small_image_and_refuse_a_4k_one() {
3643        let gpu = device_with(
3644            "default buffer limits",
3645            wgpu::Limits {
3646                max_storage_buffers_per_shader_stage:
3647                    pfx_gpu::floor::TRACE_STORAGE_BUFFERS_PER_STAGE,
3648                ..wgpu::Limits::default()
3649            },
3650        );
3651        let defaults = wgpu::Limits::default();
3652        assert_eq!(
3653            gpu.device.limits().max_storage_buffer_binding_size,
3654            defaults.max_storage_buffer_binding_size
3655        );
3656        gpu.check(&trace_floor()).unwrap();
3657        let mut small = Trace::new(&gpu, &softbox_room(), 64, 64).unwrap();
3658        small.sample(&gpu, 2, 3).unwrap();
3659        assert_eq!(small.samples(), 2);
3660        let (width, height) = (3840, 2160);
3661        let Err(error) = Trace::new(&gpu, &softbox_room(), width, height) else {
3662            panic!("a 4K trace was built on a 128 MiB storage binding range");
3663        };
3664        let TraceError::Refused(refusal) = &error else {
3665            panic!("{error}");
3666        };
3667        assert_eq!(
3668            refusal.shortfall,
3669            GpuShortfall::Limit {
3670                name: "max_storage_buffer_binding_size",
3671                needed: u64::from(width) * u64::from(height) * 48,
3672                has: defaults.max_storage_buffer_binding_size,
3673            }
3674        );
3675    }
3676
3677    fn glass_material(base: [f32; 3], roughness: f32, ior: f32) -> Material {
3678        Material {
3679            base,
3680            roughness,
3681            transmission: 1.0,
3682            ior,
3683            specular: 0.04,
3684            ..Material::default()
3685        }
3686    }
3687
3688    fn sheet(y: f32, x0: f32, x1: f32, z0: f32, z1: f32, material: u32) -> [Triangle; 2] {
3689        quad(
3690            [[x0, y, z0], [x1, y, z0], [x1, y, z1], [x0, y, z1]],
3691            material,
3692        )
3693    }
3694
3695    fn liquid_surface(material: u32, nx: u32, nz: u32) -> Vec<Triangle> {
3696        let (x0, x1, z0, z1) = (-1.6, 1.6, -2.4, -0.2);
3697        let mut points = Vec::with_capacity(((nx + 1) * (nz + 1)) as usize);
3698        for j in 0..=nz {
3699            for i in 0..=nx {
3700                let u = i as f32 / nx as f32;
3701                let v = j as f32 / nz as f32;
3702                let x = x0 + (x1 - x0) * u;
3703                let z = z0 + (z1 - z0) * v;
3704                let y = 0.12 + 0.07 * (u * 14.0).sin() * (v * 10.0).cos();
3705                points.push([x, y, z]);
3706            }
3707        }
3708        let at = |i: u32, j: u32| points[(j * (nx + 1) + i) as usize];
3709        let mut triangles = Vec::new();
3710        for j in 0..nz {
3711            for i in 0..nx {
3712                triangles.extend(quad(
3713                    [at(i, j), at(i + 1, j), at(i + 1, j + 1), at(i, j + 1)],
3714                    material,
3715                ));
3716            }
3717        }
3718        triangles.extend(sheet(0.02, x0, x1, z0, z1, material));
3719        triangles
3720    }
3721
3722    fn liquid_volume() -> Vec<Shape> {
3723        let mut shapes = Vec::new();
3724        for (x, z) in [(-0.7, -1.7), (0.5, -1.3), (-0.2, -0.8), (0.9, -2.0)] {
3725            let left = shapes.len();
3726            shapes.push(Shape::Ellipsoid {
3727                center: [x, 0.35, z],
3728                radii: [0.45, 0.28, 0.38],
3729                material: 1,
3730            });
3731            shapes.push(Shape::Ellipsoid {
3732                center: [x + 0.28, 0.42, z + 0.16],
3733                radii: [0.32, 0.22, 0.3],
3734                material: 1,
3735            });
3736            shapes.push(Shape::SmoothUnion {
3737                left,
3738                right: left + 1,
3739                radius: 0.18,
3740                material: 1,
3741            });
3742        }
3743        shapes
3744    }
3745
3746    fn stacked_glass(origin_z: f32) -> Scene {
3747        let mut triangles = Vec::new();
3748        triangles.extend(sheet(0.0, -4.0, 4.0, -3.2, 3.2, 2));
3749        for (pane, y) in [0.55, 0.95, 1.35, 1.75].into_iter().enumerate() {
3750            let shift = if pane.is_multiple_of(2) { -0.05 } else { 0.05 };
3751            triangles.extend(sheet(y, -1.7, 1.7, -2.45 + shift, -0.15, 0));
3752        }
3753        triangles.extend(liquid_surface(1, 24, 16));
3754        Scene {
3755            triangles,
3756            shapes: liquid_volume(),
3757            materials: vec![
3758                glass_material([0.92, 0.96, 1.0], 0.02, 1.5),
3759                glass_material([0.75, 0.88, 0.95], 0.08, 1.33),
3760                Material {
3761                    base: [0.55, 0.52, 0.48],
3762                    roughness: 0.8,
3763                    specular: 0.03,
3764                    ..Material::default()
3765                },
3766            ],
3767            sky: Sky {
3768                width: 2,
3769                height: 1,
3770                texels: vec![[1.0, 0.95, 0.9, 1.0], [0.75, 0.82, 0.95, 1.0]],
3771            },
3772            camera: Camera {
3773                origin: [0.0, 6.0, origin_z],
3774                forward: [0.0, -1.0, 0.0],
3775                right: [1.0, 0.0, 0.0],
3776                up: [0.0, 0.0, 1.0],
3777            },
3778            sun: Sun {
3779                direction: [0.2, 0.9, 0.1],
3780                color: [1.0, 0.98, 0.94],
3781                intensity: 2.0,
3782            },
3783        }
3784    }
3785
3786    struct Pinned;
3787
3788    impl Pinned {
3789        fn software_pcg() -> Self {
3790            unsafe {
3791                std::env::set_var("PFX_TRACE_RT", "0");
3792                std::env::set_var("PFX_TRACE_SAMPLER", "pcg");
3793            }
3794            Self
3795        }
3796    }
3797
3798    impl Drop for Pinned {
3799        fn drop(&mut self) {
3800            unsafe {
3801                std::env::remove_var("PFX_TRACE_RT");
3802                std::env::remove_var("PFX_TRACE_SAMPLER");
3803            }
3804        }
3805    }
3806
3807    fn ortho(width: f32, height: f32) -> Detail {
3808        Detail {
3809            projection: Projection::Orthographic { width, height },
3810            ..Detail::default()
3811        }
3812    }
3813
3814    fn dense_glass(nx: u32, nz: u32) -> Scene {
3815        let mut scene = stacked_glass(-1.3);
3816        scene.triangles.retain(|triangle| triangle.material != 1);
3817        scene.triangles.extend(liquid_surface(1, nx, nz));
3818        scene
3819    }
3820
3821    const EIGHT_ROW_BANDS: [[u64; 3]; 3] = [
3822        [
3823            0x10cc_b82e_6e49_544b,
3824            0x4764_c25b_7669_ef25,
3825            0x11a9_7794_10d3_2325,
3826        ],
3827        [
3828            0xbaf6_c654_3b05_bee4,
3829            0x2b63_4909_53bb_71e3,
3830            0x2716_720e_ead3_6d2b,
3831        ],
3832        [
3833            0x0dd7_acd3_33e6_3335,
3834            0x2b63_4909_53bb_71e3,
3835            0x2716_720e_ead3_6d2b,
3836        ],
3837    ];
3838
3839    fn fixed_pass(
3840        gpu: &Gpu,
3841        turns: &mut Turns,
3842        trace: &mut Trace,
3843        lanes: (u32, u32),
3844        per_block: u32,
3845        units: Option<u32>,
3846    ) -> Stats {
3847        let mut pacer = Pacer::default();
3848        pacer.set_step(1, 1);
3849        pacer.set_fixed_units(units);
3850        let mut stats = Stats::default();
3851        let pass = Pass {
3852            lanes,
3853            per_block,
3854            columns: (0, trace.width.div_ceil(8)),
3855        };
3856        trace
3857            .pass(
3858                gpu,
3859                3,
3860                1,
3861                pass,
3862                &mut pacer,
3863                &mut |ms| turns.add(ms),
3864                &mut stats,
3865            )
3866            .unwrap();
3867        stats
3868    }
3869
3870    fn summary(stats: &Stats) -> String {
3871        format!(
3872            "{} slices, median {:.2} ms, longest {:.2} ms, sum {:.1} ms",
3873            stats.count(),
3874            stats.median_ms(),
3875            stats.longest_ms(),
3876            stats.milliseconds.iter().sum::<f64>()
3877        )
3878    }
3879
3880    #[test]
3881    #[ignore = "needs a GPU; run under pgpu"]
3882    fn deep_glass_cost_by_lanes_and_blocks() {
3883        let gpu = pollster::block_on(Gpu::headless()).unwrap();
3884        let scene = dense_glass(48, 32);
3885        let frame = ortho(3.2, 2.0);
3886        let (width, height): (u32, u32) = (96, 48);
3887        let blocks = height.div_ceil(BAND_STEP);
3888        let mut turns = Turns::default();
3889        let fresh = || Trace::new_detailed(&gpu, &scene, &frame, width, height).unwrap();
3890        let mut trace = fresh();
3891        let rows = fixed_pass(&gpu, &mut turns, &mut trace, (0, LANES), BAND_STEP, Some(1));
3892        println!("one row per submission: {}", summary(&rows));
3893        let mut trace = fresh();
3894        let bands = fixed_pass(
3895            &gpu,
3896            &mut turns,
3897            &mut trace,
3898            (0, LANES),
3899            BAND_STEP,
3900            Some(BAND_STEP),
3901        );
3902        println!("one 8-row band per submission: {}", summary(&bands));
3903        let mut trace = fresh();
3904        let whole = fixed_pass(
3905            &gpu,
3906            &mut turns,
3907            &mut trace,
3908            (0, LANES),
3909            BAND_STEP,
3910            Some(blocks * BAND_STEP),
3911        );
3912        println!("whole frame in one submission: {}", summary(&whole));
3913        for lanes in [2, 4] {
3914            for strips in 1..=blocks {
3915                let mut trace = fresh();
3916                let mut runs = Stats::default();
3917                for first in (0..LANES).step_by(lanes as usize) {
3918                    let run = fixed_pass(
3919                        &gpu,
3920                        &mut turns,
3921                        &mut trace,
3922                        (first, first + lanes),
3923                        1,
3924                        Some(strips),
3925                    );
3926                    runs.milliseconds.extend(run.milliseconds);
3927                }
3928                println!(
3929                    "{lanes} lanes, {strips} blocks per submission: {}",
3930                    summary(&runs)
3931                );
3932            }
3933        }
3934        for lanes in [1, 2, 4, 8, 16, 32] {
3935            let mut trace = fresh();
3936            let mut each = Stats::default();
3937            let mut all = Stats::default();
3938            for first in (0..LANES).step_by(lanes as usize) {
3939                let block = fixed_pass(
3940                    &gpu,
3941                    &mut turns,
3942                    &mut trace,
3943                    (first, first + lanes),
3944                    1,
3945                    Some(1),
3946                );
3947                each.milliseconds.extend(block.milliseconds);
3948                let whole = fixed_pass(
3949                    &gpu,
3950                    &mut turns,
3951                    &mut trace,
3952                    (first, first + lanes),
3953                    1,
3954                    Some(blocks),
3955                );
3956                all.milliseconds.extend(whole.milliseconds);
3957            }
3958            println!(
3959                "{lanes} lanes, one block per submission: {}",
3960                summary(&each)
3961            );
3962            println!(
3963                "{lanes} lanes, every block in one submission: {}",
3964                summary(&all)
3965            );
3966        }
3967    }
3968
3969    #[test]
3970    #[ignore = "needs a GPU; run under pgpu"]
3971    fn a_wide_dense_frame_keeps_every_lane() {
3972        let gpu = pollster::block_on(Gpu::headless()).unwrap();
3973        let mut turns = Turns::default();
3974        let scene = dense_glass(4, 4);
3975        let frame = ortho(3.2, 0.1);
3976        let (width, height, samples) = (5120, 64, 8);
3977        let mut run = |lanes: Lanes| {
3978            let mut trace = Trace::new_detailed(&gpu, &scene, &frame, width, height).unwrap();
3979            trace.lanes = lanes;
3980            let mut pacer = Pacer::default();
3981            let stats = trace
3982                .sample_paced(&gpu, samples, 3, &mut pacer, |ms| turns.add(ms))
3983                .unwrap();
3984            let gpu_ms: f64 = stats.milliseconds.iter().sum();
3985            println!(
3986                "{}, lanes {}, pieces {}, {:.2} M paths/s",
3987                summary(&stats),
3988                trace.lanes.width,
3989                trace.lanes.pieces,
3990                f64::from(width * height * samples) / gpu_ms / 1e3
3991            );
3992            (trace.lanes.width, gpu_ms)
3993        };
3994        let (width_now, now) = run(Lanes::new());
3995        let (_, one_lane) = run(Lanes::lane_passes());
3996        assert_eq!(width_now, LANES);
3997        assert!(
3998            now * 2.0 < one_lane,
3999            "{now:.0} ms against {one_lane:.0} ms at one lane"
4000        );
4001    }
4002
4003    #[test]
4004    #[ignore = "needs a GPU; run under pgpu"]
4005    fn stacked_glass_and_liquid_stay_within_the_span() {
4006        let _pinned = Pinned::software_pcg();
4007        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4008        let mut turns = Turns::default();
4009        let scene = dense_glass(48, 32);
4010        let frame = ortho(3.2, 2.0);
4011        let (width, height) = (96, 48);
4012        let samples = 2;
4013        let mut banded = Trace::new_detailed(&gpu, &scene, &frame, width, height).unwrap();
4014        let mut bands = Pacer::default();
4015        bands.set_step(BAND_STEP, BAND_STEP);
4016        bands.set_fixed_units(Some(BAND_STEP));
4017        let before = banded
4018            .sample_sliced(&gpu, samples, 3, &mut bands, |ms| turns.add(ms))
4019            .unwrap();
4020        println!("8-row bands: {}", summary(&before));
4021        assert!(
4022            before.longest_ms() > SAMPLE_SPAN_MS,
4023            "an 8-row band no longer overshoots: {}",
4024            before.longest_ms()
4025        );
4026        let mut paced = Trace::new_detailed(&gpu, &scene, &frame, width, height).unwrap();
4027        let mut pacer = Pacer::default();
4028        let after = paced
4029            .sample_paced(&gpu, samples, 3, &mut pacer, |ms| turns.add(ms))
4030            .unwrap();
4031        println!("paced: {}, {} lanes", summary(&after), paced.lanes());
4032        println!(
4033            "paced ms: {}",
4034            after
4035                .milliseconds
4036                .iter()
4037                .map(|ms| format!("{ms:.2}"))
4038                .collect::<Vec<_>>()
4039                .join(" ")
4040        );
4041        let strips = height.div_ceil(BAND_STEP) as usize;
4042        let (probe, rest) = after.milliseconds.split_at(strips);
4043        assert!(
4044            probe.iter().all(|ms| *ms <= SAMPLE_SPAN_MS * 8.0),
4045            "probe {probe:?}"
4046        );
4047        let mut sorted = rest.to_vec();
4048        sorted.sort_by(f64::total_cmp);
4049        let p95 = sorted[(sorted.len() * 95).div_ceil(100).saturating_sub(1)];
4050        assert!(p95 <= SAMPLE_SPAN_MS, "p95 {p95}");
4051        let longest = sorted.last().copied().unwrap_or(0.0);
4052        assert!(longest <= SAMPLE_SPAN_MS * 1.5, "{longest}");
4053        let sum = |stats: &Stats| stats.milliseconds.iter().sum::<f64>();
4054        assert!(
4055            sum(&after) <= sum(&before) * 1.5,
4056            "paced {} against bands {}",
4057            sum(&after),
4058            sum(&before)
4059        );
4060        assert!(paced.readback(&gpu).unwrap() == banded.readback(&gpu).unwrap());
4061    }
4062
4063    #[test]
4064    #[ignore = "needs a GPU; run under pgpu"]
4065    fn transmissive_shadows_cost_on_deep_glass() {
4066        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4067        let mut turns = Turns::default();
4068        let scene = dense_glass(48, 32);
4069        let samples = 4;
4070        for round in 0..2 {
4071            for transmissive in [false, true] {
4072                let detail = Detail {
4073                    transmissive_shadows: transmissive,
4074                    ..ortho(3.2, 2.0)
4075                };
4076                let mut trace = Trace::new_detailed(&gpu, &scene, &detail, 96, 48).unwrap();
4077                let mut pacer = Pacer::default();
4078                trace
4079                    .sample_paced(&gpu, 1, 3, &mut pacer, |ms| turns.add(ms))
4080                    .unwrap();
4081                let stats = trace
4082                    .sample_paced(&gpu, samples, 4, &mut pacer, |ms| turns.add(ms))
4083                    .unwrap();
4084                println!(
4085                    "deep glass, round {round}, option {}: {:.2} ms per sample; {}, {} lanes",
4086                    if transmissive { "on" } else { "off" },
4087                    stats.milliseconds.iter().sum::<f64>() / f64::from(samples),
4088                    summary(&stats),
4089                    trace.lanes()
4090                );
4091                assert!(
4092                    stats.longest_ms() <= SAMPLE_SPAN_MS * 2.0,
4093                    "{}",
4094                    summary(&stats)
4095                );
4096            }
4097        }
4098    }
4099
4100    fn fnv(bytes: &[u8]) -> u64 {
4101        bytes.iter().fold(0xcbf2_9ce4_8422_2325, |hash, byte| {
4102            (hash ^ u64::from(*byte)).wrapping_mul(0x0000_0100_0000_01b3)
4103        })
4104    }
4105
4106    fn hashes(output: &Output) -> [u64; 3] {
4107        [fnv(&output.color), fnv(&output.albedo), fnv(&output.normal)]
4108    }
4109
4110    #[test]
4111    #[ignore = "needs a GPU; run under pgpu"]
4112    fn paced_traces_keep_the_bytes_of_eight_row_bands() {
4113        let _pinned = Pinned::software_pcg();
4114        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4115        let mut traced = Vec::new();
4116        let mut glass =
4117            Trace::new_detailed(&gpu, &dense_glass(48, 32), &ortho(3.2, 2.0), 96, 48).unwrap();
4118        let mut pacer = Pacer::default();
4119        let mut turns = Turns::default();
4120        glass
4121            .sample_paced(&gpu, 2, 3, &mut pacer, |ms| turns.add(ms))
4122            .unwrap();
4123        traced.push(hashes(&glass.readback(&gpu).unwrap()));
4124        for lit in [true, false] {
4125            let mut room =
4126                Trace::build(&gpu, &softbox_room(), &Detail::default(), 37, 29, lit).unwrap();
4127            room.sample(&gpu, 3, 5).unwrap();
4128            room.sample(&gpu, 2, 6).unwrap();
4129            traced.push(hashes(&room.readback(&gpu).unwrap()));
4130        }
4131        println!("hashes: {traced:x?}");
4132        assert_eq!(traced, EIGHT_ROW_BANDS);
4133    }
4134
4135    #[test]
4136    #[ignore = "needs a GPU; run under pgpu"]
4137    fn the_sobol_sampler_repeats_its_bytes_and_keeps_the_pcg_mean() {
4138        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4139        let mean = |sampler| {
4140            let detail = Detail {
4141                sampler,
4142                ..Detail::default()
4143            };
4144            let mut room = Trace::build(&gpu, &softbox_room(), &detail, 64, 48, true).unwrap();
4145            room.sample(&gpu, 256, 9).unwrap();
4146            let output = room.readback(&gpu).unwrap();
4147            let pixels = floats(&output.color);
4148            let sum: f64 = pixels.iter().map(|p| f64::from(p[0] + p[1] + p[2])).sum();
4149            (fnv(&output.color), sum / pixels.len() as f64)
4150        };
4151        let sobol = Sampler::Sobol { samples: 256 };
4152        let (first, sobol_mean) = mean(sobol);
4153        let (again, _) = mean(sobol);
4154        let (_, pcg_mean) = mean(Sampler::Pcg);
4155        assert_eq!(first, again);
4156        assert!(
4157            (sobol_mean / pcg_mean - 1.0).abs() < 0.02,
4158            "{sobol_mean} against {pcg_mean}"
4159        );
4160    }
4161
4162    #[test]
4163    #[ignore = "needs a GPU; run under pgpu"]
4164    fn a_cheap_trace_returns_to_full_lanes() {
4165        let gpu = pollster::block_on(Gpu::headless()).unwrap();
4166        let mut room =
4167            Trace::build(&gpu, &softbox_room(), &Detail::default(), 160, 96, false).unwrap();
4168        let mut pacer = Pacer::default();
4169        let mut turns = Turns::default();
4170        let first = room
4171            .sample_paced(&gpu, 1, 1, &mut pacer, |ms| turns.add(ms))
4172            .unwrap();
4173        println!("first sample: {}, {} lanes", summary(&first), room.lanes());
4174        let later = room
4175            .sample_paced(&gpu, 8, 2, &mut pacer, |ms| turns.add(ms))
4176            .unwrap();
4177        println!("eight more: {}, {} lanes", summary(&later), room.lanes());
4178        assert_eq!(room.lanes(), LANES);
4179        assert!(
4180            later.longest_ms() <= SAMPLE_SPAN_MS,
4181            "{}",
4182            later.longest_ms()
4183        );
4184        assert!(later.count() <= 16, "{}", later.count());
4185    }
4186}