euv_engine/raytracing/pipeline/data/fn.rs
1use super::*;
2
3/// Converts one `f64` component into a packed `f32`, rounding to nearest.
4///
5/// Rejects non-finite input and values whose magnitude exceeds the finite
6/// `f32` range instead of silently truncating. Round-to-nearest is correct
7/// for positions, colours and material scalars, where a half-ulp
8/// difference is imperceptible.
9///
10/// # Arguments
11///
12/// - `f64` - The value to narrow.
13/// - `&str` - The error message for a non-finite value.
14/// - `&str` - The error message for a value outside the `f32` range.
15///
16/// # Returns
17///
18/// - `Result<f32, String>` - The narrowed value, or the applicable error.
19pub fn pack_finite(value: f64, non_finite: &str, out_of_range: &str) -> Result<f32, String> {
20 if !value.is_finite() {
21 return Err(non_finite.to_string());
22 }
23 let narrowed: f32 = value as f32;
24 if !narrowed.is_finite() {
25 return Err(out_of_range.to_string());
26 }
27 Ok(narrowed)
28}
29
30/// Returns the next representable `f32` below `value`.
31///
32/// The step is one unit in the last place in the direction of decreasing
33/// magnitude, which is what a directed rounding needs. Signed zero is
34/// handled so the function is total: `-0.0` steps to the smallest negative
35/// subnormal and `+0.0` to the smallest positive one.
36///
37/// # Arguments
38///
39/// - `f32` - The value to step down from.
40///
41/// # Returns
42///
43/// - `f32` - The next lower `f32`, or `value` itself at the negative
44/// infinity boundary where no lower value exists.
45pub(crate) fn next_down(value: f32) -> f32 {
46 if value.is_nan() || value == f32::NEG_INFINITY {
47 return value;
48 }
49 if value > 0.0 {
50 return f32::from_bits(value.to_bits() - 1);
51 }
52 if value == 0.0 {
53 return -f32::from_bits(1);
54 }
55 f32::from_bits(value.to_bits() + 1)
56}
57
58/// Returns the next representable `f32` above `value`, the mirror of
59/// [`next_down`].
60///
61/// # Arguments
62///
63/// - `f32` - The value to step up from.
64///
65/// # Returns
66///
67/// - `f32` - The next higher `f32`, or `value` itself at the positive
68/// infinity boundary where no higher value exists.
69pub(crate) fn next_up(value: f32) -> f32 {
70 if value.is_nan() || value == f32::INFINITY {
71 return value;
72 }
73 if value >= 0.0 {
74 return f32::from_bits(value.to_bits() + 1);
75 }
76 if value == 0.0 {
77 return f32::from_bits(1);
78 }
79 f32::from_bits(value.to_bits() - 1)
80}
81
82/// Converts one BVH **lower** bound to `f32`, rounding toward negative
83/// infinity.
84///
85/// A plain `as f32` rounds to nearest, so a lower bound can be nudged
86/// *inward*; the packed node then no longer contains geometry the CPU
87/// hierarchy considers inside it, and the shader would miss a hit the CPU
88/// finds. Snapping outward by one ulp whenever the conversion was inexact
89/// makes the packed node a conservative superset of the source node, which
90/// is the only safe direction for a bounding volume.
91///
92/// # Arguments
93///
94/// - `f64` - The source bound.
95///
96/// # Returns
97///
98/// - `Result<f32, String>` - A representable `f32` no greater than the
99/// source.
100pub(crate) fn pack_lower_bound(value: f64) -> Result<f32, String> {
101 let narrowed: f32 = pack_finite(value, RAYTRACE_ERROR_NON_FINITE, RAYTRACE_ERROR_F32_RANGE)?;
102 if (narrowed as f64) > value {
103 // Round-to-nearest landed above the source, so step one ulp down.
104 return Ok(next_down(narrowed));
105 }
106 Ok(narrowed)
107}
108
109/// Converts one BVH **upper** bound to `f32`, rounding toward positive
110/// infinity. The mirror of [`pack_lower_bound`].
111///
112/// # Arguments
113///
114/// - `f64` - The source bound.
115///
116/// # Returns
117///
118/// - `Result<f32, String>` - A representable `f32` no less than the source.
119pub(crate) fn pack_upper_bound(value: f64) -> Result<f32, String> {
120 let narrowed: f32 = pack_finite(value, RAYTRACE_ERROR_NON_FINITE, RAYTRACE_ERROR_F32_RANGE)?;
121 if (narrowed as f64) < value {
122 // Round-to-nearest landed below the source, so step one ulp up.
123 return Ok(next_up(narrowed));
124 }
125 Ok(narrowed)
126}
127
128/// Converts a vector to three `f32` components, rounding to nearest.
129///
130/// # Arguments
131///
132/// - `Vector3D` - The vector to pack.
133/// - `&str` - The error message used for a non-finite component.
134/// - `&str` - The error message used for an `f32`-overflowing component.
135///
136/// # Returns
137///
138/// - `Result<[f32; 3], String>` - The packed components, or the first
139/// rejection reason.
140pub fn pack_vector(
141 vector: Vector3D,
142 non_finite: &str,
143 out_of_range: &str,
144) -> Result<[f32; 3], String> {
145 let x: f32 = pack_finite(vector.get_x(), non_finite, out_of_range)?;
146 let y: f32 = pack_finite(vector.get_y(), non_finite, out_of_range)?;
147 let z: f32 = pack_finite(vector.get_z(), non_finite, out_of_range)?;
148 Ok([x, y, z])
149}
150
151/// Converts a vector to three `f32` components, rounding every component
152/// toward negative infinity.
153///
154/// # Arguments
155///
156/// - `Vector3D` - The vector to narrow.
157/// - `&str` - The error message for a non-finite component.
158/// - `&str` - The error message for an out-of-range component.
159///
160/// # Returns
161///
162/// - `Result<[f32; 3], String>` - The narrowed components, or the first
163/// applicable error.
164pub fn pack_vector_lower(
165 vector: Vector3D,
166 non_finite: &str,
167 out_of_range: &str,
168) -> Result<[f32; 3], String> {
169 if !vector.get_x().is_finite() || !vector.get_y().is_finite() || !vector.get_z().is_finite() {
170 return Err(non_finite.to_string());
171 }
172 let magnitude: f64 = vector
173 .get_x()
174 .abs()
175 .max(vector.get_y().abs())
176 .max(vector.get_z().abs());
177 if magnitude > RAYTRACE_MAX_F32_MAGNITUDE {
178 return Err(out_of_range.to_string());
179 }
180 Ok([
181 pack_lower_bound(vector.get_x())?,
182 pack_lower_bound(vector.get_y())?,
183 pack_lower_bound(vector.get_z())?,
184 ])
185}
186
187/// Converts a vector to three `f32` components, rounding every component
188/// toward positive infinity.
189///
190/// # Arguments
191///
192/// - `Vector3D` - The vector to narrow.
193/// - `&str` - The error message for a non-finite component.
194/// - `&str` - The error message for an out-of-range component.
195///
196/// # Returns
197///
198/// - `Result<[f32; 3], String>` - The narrowed components, or the first
199/// applicable error.
200pub fn pack_vector_upper(
201 vector: Vector3D,
202 non_finite: &str,
203 out_of_range: &str,
204) -> Result<[f32; 3], String> {
205 if !vector.get_x().is_finite() || !vector.get_y().is_finite() || !vector.get_z().is_finite() {
206 return Err(non_finite.to_string());
207 }
208 let magnitude: f64 = vector
209 .get_x()
210 .abs()
211 .max(vector.get_y().abs())
212 .max(vector.get_z().abs());
213 if magnitude > RAYTRACE_MAX_F32_MAGNITUDE {
214 return Err(out_of_range.to_string());
215 }
216 Ok([
217 pack_upper_bound(vector.get_x())?,
218 pack_upper_bound(vector.get_y())?,
219 pack_upper_bound(vector.get_z())?,
220 ])
221}
222
223/// Appends four `f32` values to `out`, reusing its capacity when it
224/// already covers the write.
225///
226/// # Arguments
227///
228/// - `&mut Vec<f32>` - The destination, which keeps its capacity.
229/// - `f32` - The x component.
230/// - `f32` - The y component.
231/// - `f32` - The z component.
232/// - `f32` - The w component.
233pub fn push_vec4(out: &mut Vec<f32>, x: f32, y: f32, z: f32, w: f32) {
234 out.push(x);
235 out.push(y);
236 out.push(z);
237 out.push(w);
238}
239
240/// Converts a `usize` node or primitive index into an exactly
241/// representable `f32`.
242///
243/// `f32` represents integers exactly only up to `2^24`; above that a
244/// plain `as f32` truncates, which would make a shader read the wrong node
245/// or primitive rather than failing loudly.
246///
247/// # Arguments
248///
249/// - `usize` - The index to convert.
250///
251/// # Returns
252///
253/// - `Result<f32, String>` - The exactly representable index, or an error
254/// once the index passes the `2^24` exact range.
255pub fn pack_index(index: usize) -> Result<f32, String> {
256 if index > RAYTRACE_MAX_EXACT_F32_INDEX {
257 return Err(RAYTRACE_ERROR_INDEX_RANGE.to_string());
258 }
259 Ok(index as f32)
260}
261
262/// Returns the `f32` discriminator both shaders switch on for an
263/// [`OccluderKind`].
264///
265/// # Arguments
266///
267/// - `OccluderKind` - The source shape.
268///
269/// # Returns
270///
271/// - `f32` - The shader discriminator: sphere `0.0`, AABB `1.0`, triangle
272/// `2.0`.
273pub fn packed_shape_code(kind: OccluderKind) -> f32 {
274 match kind {
275 OccluderKind::Sphere => 0.0,
276 OccluderKind::Aabb => 1.0,
277 OccluderKind::Triangle => 2.0,
278 }
279}
280
281/// Returns the `f32` discriminator both shaders switch on for a
282/// [`MaterialKind`].
283///
284/// # Arguments
285///
286/// - `MaterialKind` - The source material.
287///
288/// # Returns
289///
290/// - `f32` - The shader discriminator: Lambert `0.0`, Phong `1.0`, PBR
291/// `2.0`.
292pub fn packed_material_code(kind: MaterialKind) -> f32 {
293 match kind {
294 MaterialKind::Lambert => 0.0,
295 MaterialKind::Phong => 1.0,
296 MaterialKind::Pbr => 2.0,
297 }
298}
299
300/// Returns the `f32` discriminator both shaders switch on for a
301/// [`LightType`].
302///
303/// # Arguments
304///
305/// - `LightType` - The source light.
306///
307/// # Returns
308///
309/// - `f32` - The shader discriminator: directional `0.0`, point `1.0`, spot
310/// `2.0`.
311pub fn packed_light_code(kind: LightType) -> f32 {
312 match kind {
313 LightType::Directional => 0.0,
314 LightType::Point => 1.0,
315 LightType::Spot => 2.0,
316 }
317}
318
319/// Returns half the `f32` unit in the last place at `probe`.
320///
321/// This is the worst-case magnitude by which a round-to-nearest `f64` to
322/// `f32` conversion can move a value, and is what a packed sphere radius is
323/// widened by.
324///
325/// # Arguments
326///
327/// - `f32` - A probe with the magnitude of interest.
328///
329/// # Returns
330///
331/// - `f64` - Half an ulp at `probe`, never zero.
332pub(crate) fn half_ulp(probe: f32) -> f64 {
333 let up: f64 = next_up(probe) as f64;
334 let down: f64 = next_down(probe) as f64;
335 let span: f64 = (up - down).abs() * 0.5;
336 if span.is_finite() && span > 0.0 {
337 return span;
338 }
339 f32::from_bits(1) as f64
340}
341
342/// Returns the packed radius of a sphere whose centre is packed toward
343/// negative infinity.
344///
345/// The packed centre is never above the source, so it can sit up to half an
346/// ulp below it on each axis. The radius absorbs the worst-case corner
347/// displacement of that drift, `sqrt(3)` half-ulp, and is then rounded up so
348/// the packed sphere still contains every point of the source sphere.
349///
350/// # Arguments
351///
352/// - `Vector3D` - The sphere centre, sampled for its ulp scale.
353/// - `f64` - The source radius.
354///
355/// # Returns
356///
357/// - `Result<f32, String>` - The widened radius, rounded up.
358pub fn pack_sphere_radius(center: Vector3D, radius: f64) -> Result<f32, String> {
359 let probe: f32 = pack_finite(
360 center.get_x(),
361 RAYTRACE_ERROR_NON_FINITE,
362 RAYTRACE_ERROR_F32_RANGE,
363 )?;
364 let widened: f64 = radius + half_ulp(probe) * 3.0_f64.sqrt();
365 pack_upper_bound(widened)
366}
367
368/// Packs one occluder into its eight `vec4` record.
369///
370/// The record layout is frozen by the shader contract:
371///
372/// ```text
373/// 0: center.xyz, shape
374/// 1: extent.xyz, material_kind
375/// 2: vertex0.xyz, original_primitive_index
376/// 3: vertex1.xyz, 0
377/// 4: vertex2.xyz, 0
378/// 5: albedo.xyz, specular
379/// 6: emissive.xyz, shininess
380/// 7: 0, 0, 0, 0
381/// ```
382///
383/// Spheres carry their radius in `extent.x` and are packed conservatively:
384/// the centre rounds toward `-inf` on every axis and the radius is enlarged
385/// by the same absolute error, so the packed sphere encloses the source
386/// sphere after narrowing. AABBs are normalized so `center` is the minimum
387/// corner and both bounds round outward. Triangles carry their three
388/// vertices explicitly.
389///
390/// # Arguments
391///
392/// - `&mut Vec<f32>` - The destination packed buffer.
393/// - `&Occluder` - The occluder to pack.
394/// - `usize` - The original primitive index, stored as the tie breaker.
395///
396/// # Returns
397///
398/// - `Result<(), String>` - `Ok` once the record is appended, or the
399/// rejection reason.
400pub(crate) fn pack_primitive(
401 out: &mut Vec<f32>,
402 occluder: &Occluder,
403 original_index: usize,
404) -> Result<(), String> {
405 let bound_index: f32 = pack_index(original_index)?;
406 let kind: OccluderKind = occluder.get_kind();
407 let (center, extent): (Vector3D, Vector3D) = match kind {
408 OccluderKind::Aabb => {
409 let lo: Vector3D = occluder.get_center();
410 let hi: Vector3D = occluder.get_extent();
411 (
412 Vector3D::new(
413 lo.get_x().min(hi.get_x()),
414 lo.get_y().min(hi.get_y()),
415 lo.get_z().min(hi.get_z()),
416 ),
417 Vector3D::new(
418 lo.get_x().max(hi.get_x()),
419 lo.get_y().max(hi.get_y()),
420 lo.get_z().max(hi.get_z()),
421 ),
422 )
423 }
424 _ => (occluder.get_center(), occluder.get_extent()),
425 };
426 let material: &Material = occluder.get_material();
427 let center_packed: [f32; 3] = match kind {
428 OccluderKind::Sphere => {
429 pack_vector_lower(center, RAYTRACE_ERROR_NON_FINITE, RAYTRACE_ERROR_F32_RANGE)?
430 }
431 _ => pack_vector(center, RAYTRACE_ERROR_NON_FINITE, RAYTRACE_ERROR_F32_RANGE)?,
432 };
433 let extent_packed: [f32; 3] = match kind {
434 OccluderKind::Sphere => {
435 let radius: f32 = pack_sphere_radius(center, extent.get_x())?;
436 [radius, 0.0, 0.0]
437 }
438 OccluderKind::Aabb => {
439 pack_vector_upper(extent, RAYTRACE_ERROR_NON_FINITE, RAYTRACE_ERROR_F32_RANGE)?
440 }
441 OccluderKind::Triangle => [0.0, 0.0, 0.0],
442 };
443 let albedo: [f32; 3] = pack_vector(
444 material.get_albedo(),
445 RAYTRACE_ERROR_NON_FINITE,
446 RAYTRACE_ERROR_F32_RANGE,
447 )?;
448 let emissive: [f32; 3] = pack_vector(
449 material.get_emissive(),
450 RAYTRACE_ERROR_NON_FINITE,
451 RAYTRACE_ERROR_F32_RANGE,
452 )?;
453 let specular: f32 = pack_finite(
454 material.get_specular(),
455 RAYTRACE_ERROR_NON_FINITE,
456 RAYTRACE_ERROR_F32_RANGE,
457 )?;
458 let shininess: f32 = pack_finite(
459 material.get_shininess(),
460 RAYTRACE_ERROR_NON_FINITE,
461 RAYTRACE_ERROR_F32_RANGE,
462 )?;
463 let vertices: [Vector3D; 3] = occluder.get_vertices();
464 push_vec4(
465 out,
466 center_packed[0],
467 center_packed[1],
468 center_packed[2],
469 packed_shape_code(kind),
470 );
471 push_vec4(
472 out,
473 extent_packed[0],
474 extent_packed[1],
475 extent_packed[2],
476 packed_material_code(material.get_kind()),
477 );
478 let vertex0: [f32; 3] = pack_vector(
479 vertices[0],
480 RAYTRACE_ERROR_NON_FINITE,
481 RAYTRACE_ERROR_F32_RANGE,
482 )?;
483 push_vec4(out, vertex0[0], vertex0[1], vertex0[2], bound_index);
484 let vertex1: [f32; 3] = pack_vector(
485 vertices[1],
486 RAYTRACE_ERROR_NON_FINITE,
487 RAYTRACE_ERROR_F32_RANGE,
488 )?;
489 push_vec4(out, vertex1[0], vertex1[1], vertex1[2], 0.0);
490 let vertex2: [f32; 3] = pack_vector(
491 vertices[2],
492 RAYTRACE_ERROR_NON_FINITE,
493 RAYTRACE_ERROR_F32_RANGE,
494 )?;
495 push_vec4(out, vertex2[0], vertex2[1], vertex2[2], 0.0);
496 push_vec4(out, albedo[0], albedo[1], albedo[2], specular);
497 push_vec4(out, emissive[0], emissive[1], emissive[2], shininess);
498 push_vec4(out, 0.0, 0.0, 0.0, 0.0);
499 Ok(())
500}
501
502/// Packs one BVH node into its three `vec4` record.
503///
504/// The record layout is frozen by the shader contract:
505///
506/// ```text
507/// 0: lower.xyz, start
508/// 1: upper.xyz, count
509/// 2: escape, 0, 0, 0
510/// ```
511///
512/// `lower` rounds toward negative infinity and `upper` toward positive
513/// infinity, so the packed node is always a conservative superset of the
514/// CPU node and traversal cannot prune a subtree the CPU would have
515/// visited. `start` indexes the reordered packed primitive records, and
516/// `count` is zero for internal nodes, which both shaders detect to choose
517/// between `start + 1` descent and an `escape` skip.
518///
519/// # Arguments
520///
521/// - `&mut Vec<f32>` - The destination, which keeps its capacity.
522/// - `&RayTraceNode` - The node to pack.
523///
524/// # Returns
525///
526/// - `Result<(), String>` - `Ok` once three slots are appended, or the
527/// first component that could not be represented.
528pub(crate) fn pack_node(out: &mut Vec<f32>, node: &RayTraceNode) -> Result<(), String> {
529 let lower: [f32; 3] = pack_vector_lower(
530 node.get_lower(),
531 RAYTRACE_ERROR_NON_FINITE,
532 RAYTRACE_ERROR_F32_RANGE,
533 )?;
534 let upper: [f32; 3] = pack_vector_upper(
535 node.get_upper(),
536 RAYTRACE_ERROR_NON_FINITE,
537 RAYTRACE_ERROR_F32_RANGE,
538 )?;
539 let start: f32 = pack_index(node.get_start())?;
540 let count: f32 = pack_index(node.get_count())?;
541 let escape: f32 = pack_index(node.get_escape())?;
542 push_vec4(out, lower[0], lower[1], lower[2], start);
543 push_vec4(out, upper[0], upper[1], upper[2], count);
544 push_vec4(out, escape, 0.0, 0.0, 0.0);
545 Ok(())
546}
547
548/// Validates one frame's camera and returns its basis.
549///
550/// This is the single place a frame is accepted or rejected: the packed
551/// header writer and the CPU pixel loop both go through it, so they can
552/// never disagree about which cameras render and which do not. The checks
553/// are the dimensions being non-zero and not overflowing the packed-buffer
554/// limit, a field of view strictly inside `(0, PI)`, a depth range
555/// satisfying `0 <= near < far` that survives the `f32` narrowing without
556/// collapsing, a finite eye, and a non-degenerate basis.
557///
558/// Nothing here allocates: the result is a stack value, so a caller
559/// rendering every frame pays no heap cost for validation, and the CPU
560/// renderer needs no packed buffer at all.
561///
562/// # Arguments
563///
564/// - `&Camera3D` - The camera supplying the eye, target, up, fov, and
565/// depth range.
566/// - `u32` - The frame width in pixels.
567/// - `u32` - The frame height in pixels.
568///
569/// # Returns
570///
571/// - `Result<FrameBasis, String>` - The validated basis, or the reason the
572/// frame cannot be rendered.
573pub(crate) fn frame_basis(
574 camera: &Camera3D,
575 width: u32,
576 height: u32,
577) -> Result<FrameBasis, String> {
578 if width == 0 || height == 0 {
579 return Err(RAYTRACE_ERROR_ZERO_DIMENSION.to_string());
580 }
581 let pixel_count: usize = match (width as usize).checked_mul(height as usize) {
582 Some(product) => product,
583 None => return Err(RAYTRACE_ERROR_DIMENSION_OVERFLOW.to_string()),
584 };
585 if pixel_count > RAYTRACE_MAX_PACKED_FLOATS {
586 return Err(RAYTRACE_ERROR_DIMENSION_OVERFLOW.to_string());
587 }
588 let eye: Vector3D = camera.get_position();
589 if !eye.get_x().is_finite() || !eye.get_y().is_finite() || !eye.get_z().is_finite() {
590 return Err(RAYTRACE_ERROR_CAMERA_NON_FINITE.to_string());
591 }
592 let fov: f64 = camera.get_fov();
593 let fov_usable: bool = fov.is_finite() && fov > 0.0 && fov < PI;
594 if !fov_usable {
595 return Err(RAYTRACE_ERROR_CAMERA_FOV.to_string());
596 }
597 let tan_half_fov: f64 = (fov * 0.5).tan();
598 let tan_usable: bool = tan_half_fov.is_finite() && tan_half_fov > 0.0;
599 if !tan_usable {
600 return Err(RAYTRACE_ERROR_CAMERA_FOV.to_string());
601 }
602 let forward: Vector3D = camera.forward();
603 if forward == Vector3D::zero() {
604 return Err(RAYTRACE_ERROR_CAMERA_BASIS.to_string());
605 }
606 let right: Vector3D = forward.cross(camera.get_up()).normalized();
607 let up: Vector3D = right.cross(forward);
608 if right == Vector3D::zero() || up == Vector3D::zero() {
609 return Err(RAYTRACE_ERROR_CAMERA_BASIS.to_string());
610 }
611 let near_t: f64 = camera.get_near();
612 let far_t: f64 = camera.get_far();
613 let range_ordered: bool =
614 near_t.is_finite() && far_t.is_finite() && near_t >= 0.0 && far_t > near_t;
615 if !range_ordered {
616 return Err(RAYTRACE_ERROR_CAMERA_RANGE.to_string());
617 }
618 let near_packed: f32 = pack_finite(
619 near_t,
620 RAYTRACE_ERROR_CAMERA_NON_FINITE,
621 RAYTRACE_ERROR_CAMERA_F32_RANGE,
622 )?;
623 let far_packed: f32 = pack_finite(
624 far_t,
625 RAYTRACE_ERROR_CAMERA_NON_FINITE,
626 RAYTRACE_ERROR_CAMERA_F32_RANGE,
627 )?;
628 let packed_range_ordered: bool = far_packed > near_packed;
629 if !packed_range_ordered {
630 return Err(RAYTRACE_ERROR_CAMERA_RANGE.to_string());
631 }
632 Ok(FrameBasis {
633 eye,
634 forward,
635 right,
636 up,
637 tan_half_fov,
638 aspect: width as f64 / height as f64,
639 width,
640 height,
641 near_t,
642 far_t,
643 })
644}
645
646/// Packs the per-frame camera, lighting, and settings block consumed by
647/// both shader backends.
648///
649/// The buffer layout is frozen by the shader contract:
650///
651/// ```text
652/// 0: eye.xyz, tan(fov / 2)
653/// 1: forward.xyz, aspect
654/// 2: right.xyz, width
655/// 3: up.xyz, height
656/// 4: ambient.xyz, light_count
657/// 5: lighting_eye.xyz, max_bounces
658/// 6: near_t, far_t, samples_per_axis, 0
659/// 7: 0, 0, 0, 0
660/// ```
661///
662/// followed by four `vec4` per light:
663///
664/// ```text
665/// 0: position.xyz, kind
666/// 1: direction.xyz, intensity
667/// 2: color.xyz, falloff
668/// 3: spot_cos, 0, 0, 0
669/// ```
670///
671/// The camera basis is recomputed from `position`, `target`, and `up`, so a
672/// caller never supplies a basis that disagrees with `target`. A degenerate
673/// basis — a zero forward, a zero right, or an up parallel to forward — is
674/// rejected rather than packed as a zero direction that would collapse every
675/// ray onto one axis. The field of view must lie in `(0, PI)` and the depth
676/// range must satisfy `0 <= near < far`; both are packed through
677/// [`pack_finite`], so a `f64` far plane beyond the `f32` range is reported
678/// rather than written as infinity.
679///
680/// The light count is taken from the input and never capped: a frame with
681/// more lights than the packed-buffer limit allows is reported through the
682/// overflow check, not silently truncated.
683///
684/// The supplied `out` is cleared and then reused, so its capacity is kept
685/// and a caller packing every frame performs no allocation.
686///
687/// # Arguments
688///
689/// - `&mut Vec<f32>` - The destination packed buffer, cleared then reused.
690/// - `&Camera3D` - The camera supplying the eye, target, up, and fov.
691/// - `&LightingUniforms` - The lights, ambient, and shading eye.
692/// - `&RayTraceSettings` - The sampling and bounce settings.
693/// - `u32` - The frame width in pixels.
694/// - `u32` - The frame height in pixels.
695///
696/// # Returns
697///
698/// - `Result<(), String>` - `Ok` once the block is packed, or the reason
699/// the frame cannot be represented.
700pub fn pack_raytrace_frame(
701 out: &mut Vec<f32>,
702 camera: &Camera3D,
703 lights: &LightingUniforms,
704 settings: &RayTraceSettings,
705 width: u32,
706 height: u32,
707) -> Result<(), String> {
708 settings.validate()?;
709 let basis: FrameBasis = frame_basis(camera, width, height)?;
710 let light_count: usize = lights.get_lights().len();
711 let light_slots: usize = light_count
712 .checked_mul(RAYTRACE_PACKED_LIGHT_VEC4S)
713 .ok_or_else(|| RAYTRACE_ERROR_FRAME_OVERFLOW.to_string())?;
714 let total_slots: usize = RAYTRACE_PACKED_FRAME_HEADER_VEC4S
715 .checked_add(light_slots)
716 .ok_or_else(|| RAYTRACE_ERROR_FRAME_OVERFLOW.to_string())?;
717 let total_floats: usize = total_slots
718 .checked_mul(4)
719 .ok_or_else(|| RAYTRACE_ERROR_FRAME_OVERFLOW.to_string())?;
720 if total_floats > RAYTRACE_MAX_PACKED_FLOATS {
721 return Err(RAYTRACE_ERROR_FRAME_OVERFLOW.to_string());
722 }
723 let eye: [f32; 3] = pack_vector(
724 camera.get_position(),
725 RAYTRACE_ERROR_CAMERA_NON_FINITE,
726 RAYTRACE_ERROR_CAMERA_NON_FINITE,
727 )?;
728 let forward_packed: [f32; 3] = pack_vector(
729 basis.get_forward(),
730 RAYTRACE_ERROR_CAMERA_NON_FINITE,
731 RAYTRACE_ERROR_CAMERA_NON_FINITE,
732 )?;
733 let right_packed: [f32; 3] = pack_vector(
734 basis.get_right(),
735 RAYTRACE_ERROR_CAMERA_NON_FINITE,
736 RAYTRACE_ERROR_CAMERA_NON_FINITE,
737 )?;
738 let up_packed: [f32; 3] = pack_vector(
739 basis.get_up(),
740 RAYTRACE_ERROR_CAMERA_NON_FINITE,
741 RAYTRACE_ERROR_CAMERA_NON_FINITE,
742 )?;
743 let tan_half_fov: f32 = pack_finite(
744 basis.get_tan_half_fov(),
745 RAYTRACE_ERROR_CAMERA_NON_FINITE,
746 RAYTRACE_ERROR_CAMERA_NON_FINITE,
747 )?;
748 let ambient: [f32; 3] = pack_vector(
749 lights.get_ambient(),
750 RAYTRACE_ERROR_LIGHT_NON_FINITE,
751 RAYTRACE_ERROR_LIGHT_F32_RANGE,
752 )?;
753 let lighting_eye: [f32; 3] = pack_vector(
754 lights.get_eye(),
755 RAYTRACE_ERROR_LIGHT_NON_FINITE,
756 RAYTRACE_ERROR_LIGHT_F32_RANGE,
757 )?;
758 out.clear();
759 push_vec4(out, eye[0], eye[1], eye[2], tan_half_fov);
760 push_vec4(
761 out,
762 forward_packed[0],
763 forward_packed[1],
764 forward_packed[2],
765 basis.get_aspect() as f32,
766 );
767 push_vec4(
768 out,
769 right_packed[0],
770 right_packed[1],
771 right_packed[2],
772 basis.get_width() as f32,
773 );
774 push_vec4(
775 out,
776 up_packed[0],
777 up_packed[1],
778 up_packed[2],
779 basis.get_height() as f32,
780 );
781 push_vec4(out, ambient[0], ambient[1], ambient[2], light_count as f32);
782 push_vec4(
783 out,
784 lighting_eye[0],
785 lighting_eye[1],
786 lighting_eye[2],
787 settings.get_max_bounces() as f32,
788 );
789 push_vec4(
790 out,
791 basis.get_near_t() as f32,
792 basis.get_far_t() as f32,
793 settings.get_samples_per_axis() as f32,
794 0.0,
795 );
796 push_vec4(out, 0.0, 0.0, 0.0, 0.0);
797 for light in lights.get_lights().iter() {
798 let position: [f32; 3] = pack_vector(
799 light.get_position(),
800 RAYTRACE_ERROR_LIGHT_NON_FINITE,
801 RAYTRACE_ERROR_LIGHT_F32_RANGE,
802 )?;
803 let direction: [f32; 3] = pack_vector(
804 light.get_direction(),
805 RAYTRACE_ERROR_LIGHT_NON_FINITE,
806 RAYTRACE_ERROR_LIGHT_F32_RANGE,
807 )?;
808 let color: [f32; 3] = pack_vector(
809 light.get_color(),
810 RAYTRACE_ERROR_LIGHT_NON_FINITE,
811 RAYTRACE_ERROR_LIGHT_F32_RANGE,
812 )?;
813 let intensity: f32 = pack_finite(
814 light.get_intensity(),
815 RAYTRACE_ERROR_LIGHT_NON_FINITE,
816 RAYTRACE_ERROR_LIGHT_F32_RANGE,
817 )?;
818 let falloff: f32 = pack_finite(
819 light.get_falloff(),
820 RAYTRACE_ERROR_LIGHT_NON_FINITE,
821 RAYTRACE_ERROR_LIGHT_F32_RANGE,
822 )?;
823 let spot_cos: f32 = pack_finite(
824 light.get_spot_cos(),
825 RAYTRACE_ERROR_LIGHT_NON_FINITE,
826 RAYTRACE_ERROR_LIGHT_F32_RANGE,
827 )?;
828 push_vec4(
829 out,
830 position[0],
831 position[1],
832 position[2],
833 packed_light_code(light.get_kind()),
834 );
835 push_vec4(out, direction[0], direction[1], direction[2], intensity);
836 push_vec4(out, color[0], color[1], color[2], falloff);
837 push_vec4(out, spot_cos, 0.0, 0.0, 0.0);
838 }
839 Ok(())
840}