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}