Skip to main content

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}