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}