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