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