1
//! POD types for the camera-capture surface
2
//! (SUPER_PLAN_2 §4 Priority 6 + research/01).
3
//!
4
//! Camera frames are GPU textures, not scalar samples, so the stateful side
5
//! is heavier than the sensors': `azul_layout::managers::camera` owns a
6
//! `CameraStream` per capture, each holding a shared `ImageRef` texture the
7
//! capture thread writes into (zero-copy - clones see new bytes via the
8
//! `ImageRef` `Arc`). A `CameraPreview` node renders that texture and, by
9
//! appearing in the DOM, declares "I need the camera" to the permission
10
//! layer (research/01 §"permission-as-DOM").
11
//!
12
//! Defined here in `azul-core` so the config / id / status types cross the
13
//! FFI without `azul-layout` (or AVFoundation / Camera2) as a dependency -
14
//! these are what an app passes to `start_camera` and reads back from a
15
//! stream. The `Nv12` zero-copy output format is a `RawImageFormat` addition
16
//! deferred to the backend tick; configs default to `BGRA8`.
17

            
18
use crate::resources::RawImageFormat;
19

            
20
/// Identifies one camera capture stream - assigned by `start_camera`, used
21
/// to read the stream back (`get_camera_frame`) and to stop / pause / flip it.
22
#[repr(C)]
23
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
24
pub struct CaptureStreamId {
25
    pub id: u64,
26
}
27

            
28
/// Which physical camera to open.
29
#[repr(C)]
30
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
31
pub enum CameraFacing {
32
    /// User-facing (selfie) camera.
33
    Front,
34
    /// World-facing (rear) camera.
35
    Back,
36
    /// An external / USB camera (desktop webcams report here).
37
    External,
38
}
39

            
40
/// Lifecycle of a capture stream.
41
#[repr(C)]
42
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
43
pub enum StreamState {
44
    /// Opening the device / negotiating the format.
45
    Starting,
46
    /// Delivering frames.
47
    Running,
48
    /// Temporarily suspended (app backgrounded, `pause_camera`).
49
    Paused,
50
    /// Stopped by the app (`stop_camera`) or torn down.
51
    Stopped,
52
    /// Failed - see the stream's [`CaptureErrorCode`].
53
    Error,
54
}
55

            
56
/// Rotation / mirroring the capture needs relative to the display (the
57
/// sensor's native orientation rarely matches the UI's).
58
#[repr(C)]
59
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
60
pub enum CaptureOrientation {
61
    /// Upright (0°).
62
    Up,
63
    /// Upside down (180°).
64
    Down,
65
    /// Rotated 90° counter-clockwise.
66
    Left,
67
    /// Rotated 90° clockwise.
68
    Right,
69
    /// Horizontally mirrored (typical for the front camera).
70
    Mirror,
71
}
72

            
73
/// Why a capture stream failed ([`StreamState::Error`]).
74
#[repr(C)]
75
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
76
pub enum CaptureErrorCode {
77
    /// The user denied (or hasn't granted) camera permission.
78
    PermissionDenied,
79
    /// No camera matched the requested [`CameraFacing`].
80
    DeviceUnavailable,
81
    /// The device disappeared mid-capture (unplugged / claimed).
82
    DeviceLost,
83
    /// The requested format / resolution isn't supported.
84
    Unsupported,
85
    /// A platform error not covered above.
86
    Internal,
87
}
88

            
89
/// Requested capture configuration - the input to `start_camera`. Zero
90
/// `width`/`height`/`fps` mean "let the backend pick its default".
91
#[repr(C)]
92
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
93
pub struct CameraConfig {
94
    /// Which camera to open.
95
    pub facing: CameraFacing,
96
    /// Preferred frame width in px (0 = backend default).
97
    pub width: u32,
98
    /// Preferred frame height in px (0 = backend default).
99
    pub height: u32,
100
    /// Preferred frame rate (0 = backend default).
101
    pub fps: u32,
102
    /// Texture format the backend should deliver. `BGRA8` is the portable
103
    /// default; `Nv12` (a later `RawImageFormat` addition) is the zero-copy
104
    /// path on platforms that produce it natively.
105
    pub output_format: RawImageFormat,
106
}
107

            
108
impl Default for CameraConfig {
109
38
    fn default() -> Self {
110
38
        Self {
111
38
            facing: CameraFacing::Back,
112
38
            width: 0,
113
38
            height: 0,
114
38
            fps: 0,
115
38
            output_format: RawImageFormat::BGRA8,
116
38
        }
117
38
    }
118
}
119

            
120
impl CameraConfig {
121
    /// A default config for the given `facing` (backend-chosen size/fps,
122
    /// `BGRA8`).
123
15
    #[must_use] pub fn new(facing: CameraFacing) -> Self {
124
15
        Self {
125
15
            facing,
126
15
            ..Self::default()
127
15
        }
128
15
    }
129
}
130

            
131
/// Runtime stats for a capture stream - surfaced for HUD / debugging.
132
#[repr(C)]
133
#[derive(Debug, Clone, Copy, PartialEq)]
134
pub struct CaptureStats {
135
    /// Measured delivery rate (frames/s), smoothed by the backend.
136
    pub measured_fps: f32,
137
    /// Frames delivered to the texture since the stream started.
138
    pub frames_delivered: u64,
139
    /// Frames the backend dropped (couldn't keep up / late).
140
    pub frames_dropped: u64,
141
}
142

            
143
impl Default for CaptureStats {
144
22
    fn default() -> Self {
145
22
        Self {
146
22
            measured_fps: 0.0,
147
22
            frames_delivered: 0,
148
22
            frames_dropped: 0,
149
22
        }
150
22
    }
151
}
152

            
153
#[cfg(test)]
154
mod autotest_generated {
155
    use super::*;
156

            
157
    const ALL_FACINGS: [CameraFacing; 3] =
158
        [CameraFacing::Front, CameraFacing::Back, CameraFacing::External];
159

            
160
    // ---- CameraConfig::new — constructor: no_panic + invariants_hold ----
161

            
162
    #[test]
163
    fn new_does_not_panic_for_any_facing() {
164
        // Constructor over every representative argument must not panic.
165
        for &facing in &ALL_FACINGS {
166
            let _ = CameraConfig::new(facing);
167
        }
168
    }
169

            
170
    #[test]
171
    fn new_sets_facing_and_leaves_everything_else_at_default() {
172
        // Documented contract: default config for the given `facing`
173
        // (backend-chosen size/fps, BGRA8). So new(f) must differ from
174
        // Default only in `facing`.
175
        let def = CameraConfig::default();
176
        for &facing in &ALL_FACINGS {
177
            let cfg = CameraConfig::new(facing);
178
            assert_eq!(cfg.facing, facing, "facing must equal the argument");
179
            assert_eq!(cfg.width, def.width);
180
            assert_eq!(cfg.height, def.height);
181
            assert_eq!(cfg.fps, def.fps);
182
            assert_eq!(cfg.output_format, def.output_format);
183
            // Absolute invariants (not just "== default"):
184
            assert_eq!(cfg.width, 0, "0 => backend default width");
185
            assert_eq!(cfg.height, 0, "0 => backend default height");
186
            assert_eq!(cfg.fps, 0, "0 => backend default fps");
187
            assert_eq!(cfg.output_format, RawImageFormat::BGRA8);
188
        }
189
    }
190

            
191
    #[test]
192
    fn new_equals_struct_update_syntax() {
193
        // new(f) must be observably identical to `{ facing: f, ..default() }`.
194
        for &facing in &ALL_FACINGS {
195
            let via_new = CameraConfig::new(facing);
196
            let via_update = CameraConfig {
197
                facing,
198
                ..CameraConfig::default()
199
            };
200
            assert_eq!(via_new, via_update);
201
        }
202
    }
203

            
204
    #[test]
205
    fn new_back_equals_full_default() {
206
        // Default facing is Back, so new(Back) must equal Default entirely.
207
        assert_eq!(
208
            CameraConfig::new(CameraFacing::Back),
209
            CameraConfig::default()
210
        );
211
    }
212

            
213
    #[test]
214
    fn new_result_is_copy_and_independent() {
215
        // CameraConfig is Copy; mutating a copy must not touch the original.
216
        let a = CameraConfig::new(CameraFacing::Front);
217
        let mut b = a;
218
        b.width = 1920;
219
        b.facing = CameraFacing::Back;
220
        assert_eq!(a.width, 0, "original must be untouched by copy mutation");
221
        assert_eq!(a.facing, CameraFacing::Front);
222
        assert_eq!(b.width, 1920);
223
        assert_eq!(b.facing, CameraFacing::Back);
224
    }
225

            
226
    #[test]
227
    fn new_differs_only_by_facing_between_variants() {
228
        // Two configs built from different facings must be unequal, and equal
229
        // once their facings are aligned (proves facing is the only variance).
230
        let front = CameraConfig::new(CameraFacing::Front);
231
        let mut back = CameraConfig::new(CameraFacing::Back);
232
        assert_ne!(front, back);
233
        back.facing = CameraFacing::Front;
234
        assert_eq!(front, back);
235
    }
236

            
237
    // ---- CameraConfig field storage: no clamping / no overflow on extremes ----
238

            
239
    #[test]
240
    fn camera_config_stores_extreme_dimensions_verbatim() {
241
        // These are POD "requested" values; the type must not clamp/saturate.
242
        let cfg = CameraConfig {
243
            width: u32::MAX,
244
            height: u32::MAX,
245
            fps: u32::MAX,
246
            ..CameraConfig::new(CameraFacing::External)
247
        };
248
        assert_eq!(cfg.width, u32::MAX);
249
        assert_eq!(cfg.height, u32::MAX);
250
        assert_eq!(cfg.fps, u32::MAX);
251
        assert_eq!(cfg.facing, CameraFacing::External);
252
    }
253

            
254
    #[test]
255
    fn camera_config_output_format_can_hold_non_default() {
256
        // new() forces BGRA8, but the struct must faithfully hold any format.
257
        let cfg = CameraConfig {
258
            output_format: RawImageFormat::RGBA8,
259
            ..CameraConfig::new(CameraFacing::Front)
260
        };
261
        assert_eq!(cfg.output_format, RawImageFormat::RGBA8);
262
        assert_ne!(cfg.output_format, RawImageFormat::BGRA8);
263
    }
264

            
265
    // ---- Default invariants ----
266

            
267
    #[test]
268
    fn camera_config_default_is_back_bgra8_zeroed() {
269
        let d = CameraConfig::default();
270
        assert_eq!(d.facing, CameraFacing::Back);
271
        assert_eq!(d.width, 0);
272
        assert_eq!(d.height, 0);
273
        assert_eq!(d.fps, 0);
274
        assert_eq!(d.output_format, RawImageFormat::BGRA8);
275
    }
276

            
277
    #[test]
278
    fn camera_config_default_is_idempotent() {
279
        assert_eq!(CameraConfig::default(), CameraConfig::default());
280
    }
281

            
282
    #[test]
283
    fn capture_stats_default_is_fully_zeroed() {
284
        let s = CaptureStats::default();
285
        assert_eq!(s.measured_fps, 0.0);
286
        assert_eq!(s.frames_delivered, 0);
287
        assert_eq!(s.frames_dropped, 0);
288
    }
289

            
290
    // ---- CaptureStats numeric / NaN adversarial (PartialEq over f32, NOT Eq) ----
291

            
292
    #[test]
293
    fn capture_stats_nan_fps_breaks_eq_reflexivity() {
294
        // measured_fps is f32; NaN != NaN, so a stats value carrying NaN must
295
        // not compare equal to itself. This confirms CaptureStats is (correctly)
296
        // PartialEq and not Eq.
297
        let s = CaptureStats {
298
            measured_fps: f32::NAN,
299
            ..CaptureStats::default()
300
        };
301
        assert_ne!(s, s, "NaN measured_fps must break PartialEq reflexivity");
302
        assert!(s.measured_fps.is_nan());
303
    }
304

            
305
    #[test]
306
    fn capture_stats_holds_infinities_and_saturated_counters() {
307
        let s = CaptureStats {
308
            measured_fps: f32::INFINITY,
309
            frames_delivered: u64::MAX,
310
            frames_dropped: u64::MAX,
311
        };
312
        assert!(s.measured_fps.is_infinite() && s.measured_fps > 0.0);
313
        assert_eq!(s.frames_delivered, u64::MAX);
314
        assert_eq!(s.frames_dropped, u64::MAX);
315

            
316
        let neg = CaptureStats {
317
            measured_fps: f32::NEG_INFINITY,
318
            ..CaptureStats::default()
319
        };
320
        assert!(neg.measured_fps.is_infinite() && neg.measured_fps < 0.0);
321
    }
322

            
323
    #[test]
324
    fn capture_stats_negative_zero_fps_equals_zero() {
325
        // IEEE: -0.0 == 0.0 for PartialEq; assert the type doesn't surprise us.
326
        let s = CaptureStats {
327
            measured_fps: -0.0,
328
            ..CaptureStats::default()
329
        };
330
        assert_eq!(s, CaptureStats::default());
331
    }
332

            
333
    // ---- CaptureStreamId round-trips its id over boundary values ----
334

            
335
    #[test]
336
    fn capture_stream_id_roundtrips_boundary_ids() {
337
        for id in [0u64, 1, 2, u64::MAX / 2, u64::MAX - 1, u64::MAX] {
338
            let s = CaptureStreamId { id };
339
            assert_eq!(s.id, id);
340
            assert_eq!(s, CaptureStreamId { id }, "Eq must hold for equal ids");
341
        }
342
    }
343

            
344
    #[test]
345
    fn capture_stream_id_distinct_ids_are_unequal_and_ordered() {
346
        let lo = CaptureStreamId { id: 0 };
347
        let hi = CaptureStreamId { id: u64::MAX };
348
        assert_ne!(lo, hi);
349
        assert!(lo < hi, "Ord must follow the inner u64");
350
    }
351

            
352
    // ---- Enum ordering / total-order / hash-eq contract (derived, but assert it) ----
353

            
354
    #[test]
355
    fn enum_ord_matches_declaration_order() {
356
        // CameraFacing: Front < Back < External.
357
        assert!(CameraFacing::Front < CameraFacing::Back);
358
        assert!(CameraFacing::Back < CameraFacing::External);
359
        // StreamState: Starting < Running < Paused < Stopped < Error.
360
        assert!(StreamState::Starting < StreamState::Running);
361
        assert!(StreamState::Running < StreamState::Paused);
362
        assert!(StreamState::Paused < StreamState::Stopped);
363
        assert!(StreamState::Stopped < StreamState::Error);
364
        // CaptureOrientation: Up < Down < Left < Right < Mirror.
365
        assert!(CaptureOrientation::Up < CaptureOrientation::Down);
366
        assert!(CaptureOrientation::Down < CaptureOrientation::Left);
367
        assert!(CaptureOrientation::Left < CaptureOrientation::Right);
368
        assert!(CaptureOrientation::Right < CaptureOrientation::Mirror);
369
        // CaptureErrorCode: PermissionDenied < DeviceUnavailable < DeviceLost
370
        //                   < Unsupported < Internal.
371
        assert!(CaptureErrorCode::PermissionDenied < CaptureErrorCode::DeviceUnavailable);
372
        assert!(CaptureErrorCode::DeviceUnavailable < CaptureErrorCode::DeviceLost);
373
        assert!(CaptureErrorCode::DeviceLost < CaptureErrorCode::Unsupported);
374
        assert!(CaptureErrorCode::Unsupported < CaptureErrorCode::Internal);
375
    }
376

            
377
    #[test]
378
    fn camera_facing_ord_is_total_and_antisymmetric() {
379
        use core::cmp::Ordering;
380
        for &a in &ALL_FACINGS {
381
            assert_eq!(a.cmp(&a), Ordering::Equal, "reflexive equality");
382
            for &b in &ALL_FACINGS {
383
                // Exactly one of <, ==, > holds (trichotomy / total order).
384
                let lt = (a < b) as u8;
385
                let eq = (a == b) as u8;
386
                let gt = (a > b) as u8;
387
                assert_eq!(lt + eq + gt, 1, "trichotomy must hold for ({a:?},{b:?})");
388
                // Antisymmetry: a < b implies !(b < a).
389
                if a < b {
390
                    assert!((b >= a), "antisymmetry violated for ({a:?},{b:?})");
391
                }
392
            }
393
        }
394
    }
395

            
396
    #[test]
397
    fn enum_hash_eq_consistency() {
398
        use std::collections::HashSet;
399
        let mut set = HashSet::new();
400
        assert!(set.insert(CameraFacing::Front));
401
        assert!(set.insert(CameraFacing::Back));
402
        assert!(set.insert(CameraFacing::External));
403
        // Re-inserting an equal value must be rejected (Hash agrees with Eq).
404
        assert!(!set.insert(CameraFacing::Front));
405
        assert_eq!(set.len(), 3);
406
        assert!(set.contains(&CameraFacing::Back));
407
    }
408

            
409
    // =====================================================================
410
    // Appended adversarial tests.
411
    //
412
    // NOTE: the module docs mention an `Nv12` output format, but it is an
413
    // explicitly deferred `RawImageFormat` addition and does not exist yet -
414
    // it is therefore un-constructible and deliberately NOT tested here.
415
    // =====================================================================
416

            
417
    const ALL_STATES: [StreamState; 5] = [
418
        StreamState::Starting,
419
        StreamState::Running,
420
        StreamState::Paused,
421
        StreamState::Stopped,
422
        StreamState::Error,
423
    ];
424

            
425
    const ALL_ORIENTATIONS: [CaptureOrientation; 5] = [
426
        CaptureOrientation::Up,
427
        CaptureOrientation::Down,
428
        CaptureOrientation::Left,
429
        CaptureOrientation::Right,
430
        CaptureOrientation::Mirror,
431
    ];
432

            
433
    const ALL_ERRORS: [CaptureErrorCode; 5] = [
434
        CaptureErrorCode::PermissionDenied,
435
        CaptureErrorCode::DeviceUnavailable,
436
        CaptureErrorCode::DeviceLost,
437
        CaptureErrorCode::Unsupported,
438
        CaptureErrorCode::Internal,
439
    ];
440

            
441
    /// Every `RawImageFormat` a `CameraConfig` could carry. `Nv12` is absent
442
    /// on purpose (see the note above).
443
    const ALL_FORMATS: [RawImageFormat; 12] = [
444
        RawImageFormat::R8,
445
        RawImageFormat::RG8,
446
        RawImageFormat::RGB8,
447
        RawImageFormat::RGBA8,
448
        RawImageFormat::R16,
449
        RawImageFormat::RG16,
450
        RawImageFormat::RGB16,
451
        RawImageFormat::RGBA16,
452
        RawImageFormat::BGR8,
453
        RawImageFormat::BGRA8,
454
        RawImageFormat::RGBF32,
455
        RawImageFormat::RGBAF32,
456
    ];
457

            
458
    // ---- Round-trip: enum -> FFI discriminant -> enum (encode == decode) ----
459
    //
460
    // These are `#[repr(C)]` fieldless enums that cross the FFI boundary, so
461
    // their discriminants ARE the wire format. A reordered / inserted variant
462
    // silently re-numbers the C ABI; these round-trips pin it down. The
463
    // `match` arms are exhaustive, so adding a variant fails to compile rather
464
    // than silently shipping an untested value.
465

            
466
    fn facing_from_discriminant(i: i32) -> Option<CameraFacing> {
467
        match i {
468
            0 => Some(CameraFacing::Front),
469
            1 => Some(CameraFacing::Back),
470
            2 => Some(CameraFacing::External),
471
            _ => None,
472
        }
473
    }
474

            
475
    fn state_from_discriminant(i: i32) -> Option<StreamState> {
476
        match i {
477
            0 => Some(StreamState::Starting),
478
            1 => Some(StreamState::Running),
479
            2 => Some(StreamState::Paused),
480
            3 => Some(StreamState::Stopped),
481
            4 => Some(StreamState::Error),
482
            _ => None,
483
        }
484
    }
485

            
486
    fn orientation_from_discriminant(i: i32) -> Option<CaptureOrientation> {
487
        match i {
488
            0 => Some(CaptureOrientation::Up),
489
            1 => Some(CaptureOrientation::Down),
490
            2 => Some(CaptureOrientation::Left),
491
            3 => Some(CaptureOrientation::Right),
492
            4 => Some(CaptureOrientation::Mirror),
493
            _ => None,
494
        }
495
    }
496

            
497
    fn error_from_discriminant(i: i32) -> Option<CaptureErrorCode> {
498
        match i {
499
            0 => Some(CaptureErrorCode::PermissionDenied),
500
            1 => Some(CaptureErrorCode::DeviceUnavailable),
501
            2 => Some(CaptureErrorCode::DeviceLost),
502
            3 => Some(CaptureErrorCode::Unsupported),
503
            4 => Some(CaptureErrorCode::Internal),
504
            _ => None,
505
        }
506
    }
507

            
508
    #[test]
509
    fn enum_discriminants_are_the_pinned_ffi_wire_values() {
510
        // If this fails, the C ABI changed: every C/Python/Kotlin caller that
511
        // passed an integer facing/state/orientation/error is now wrong.
512
        assert_eq!(CameraFacing::Front as i32, 0);
513
        assert_eq!(CameraFacing::Back as i32, 1);
514
        assert_eq!(CameraFacing::External as i32, 2);
515

            
516
        assert_eq!(StreamState::Starting as i32, 0);
517
        assert_eq!(StreamState::Running as i32, 1);
518
        assert_eq!(StreamState::Paused as i32, 2);
519
        assert_eq!(StreamState::Stopped as i32, 3);
520
        assert_eq!(StreamState::Error as i32, 4);
521

            
522
        assert_eq!(CaptureOrientation::Up as i32, 0);
523
        assert_eq!(CaptureOrientation::Down as i32, 1);
524
        assert_eq!(CaptureOrientation::Left as i32, 2);
525
        assert_eq!(CaptureOrientation::Right as i32, 3);
526
        assert_eq!(CaptureOrientation::Mirror as i32, 4);
527

            
528
        assert_eq!(CaptureErrorCode::PermissionDenied as i32, 0);
529
        assert_eq!(CaptureErrorCode::DeviceUnavailable as i32, 1);
530
        assert_eq!(CaptureErrorCode::DeviceLost as i32, 2);
531
        assert_eq!(CaptureErrorCode::Unsupported as i32, 3);
532
        assert_eq!(CaptureErrorCode::Internal as i32, 4);
533
    }
534

            
535
    #[test]
536
    fn enum_discriminant_roundtrip_is_identity() {
537
        for &f in &ALL_FACINGS {
538
            assert_eq!(facing_from_discriminant(f as i32), Some(f));
539
        }
540
        for &s in &ALL_STATES {
541
            assert_eq!(state_from_discriminant(s as i32), Some(s));
542
        }
543
        for &o in &ALL_ORIENTATIONS {
544
            assert_eq!(orientation_from_discriminant(o as i32), Some(o));
545
        }
546
        for &e in &ALL_ERRORS {
547
            assert_eq!(error_from_discriminant(e as i32), Some(e));
548
        }
549
    }
550

            
551
    #[test]
552
    fn enum_decode_rejects_out_of_range_discriminants() {
553
        // A hostile / buggy FFI caller can hand over ANY i32. Decoding must
554
        // yield None, never a bogus variant (and never wrap around).
555
        for bad in [-1i32, 3, 5, 99, i32::MIN, i32::MAX] {
556
            assert_eq!(facing_from_discriminant(bad), None, "facing {bad}");
557
        }
558
        for bad in [-1i32, 5, 6, 255, i32::MIN, i32::MAX] {
559
            assert_eq!(state_from_discriminant(bad), None, "state {bad}");
560
            assert_eq!(orientation_from_discriminant(bad), None, "orient {bad}");
561
            assert_eq!(error_from_discriminant(bad), None, "error {bad}");
562
        }
563
        // Boundary: the last valid value decodes, one past it does not.
564
        assert!(facing_from_discriminant(2).is_some());
565
        assert!(facing_from_discriminant(3).is_none());
566
        assert!(state_from_discriminant(4).is_some());
567
        assert!(state_from_discriminant(5).is_none());
568
    }
569

            
570
    #[test]
571
    fn enum_discriminant_order_agrees_with_ord() {
572
        // Ord is derived from declaration order, which is also the FFI
573
        // numbering; the two must not drift apart.
574
        for &a in &ALL_STATES {
575
            for &b in &ALL_STATES {
576
                assert_eq!(
577
                    a < b,
578
                    (a as i32) < (b as i32),
579
                    "Ord and discriminant disagree for ({a:?}, {b:?})"
580
                );
581
            }
582
        }
583
    }
584

            
585
    // ---- Round-trip: struct -> fields -> struct (decompose == recompose) ----
586

            
587
    #[test]
588
    fn camera_config_field_roundtrip_is_identity_over_full_cross_product() {
589
        // Every facing x every format, with adversarial extents: taking a
590
        // config apart and rebuilding it must reproduce it exactly.
591
        for &facing in &ALL_FACINGS {
592
            for &output_format in &ALL_FORMATS {
593
                for &(width, height, fps) in &[
594
                    (0u32, 0u32, 0u32),
595
                    (1, 1, 1),
596
                    (1920, 1080, 60),
597
                    (u32::MAX, u32::MAX, u32::MAX),
598
                    (u32::MAX, 0, 1),
599
                ] {
600
                    let cfg = CameraConfig {
601
                        facing,
602
                        width,
603
                        height,
604
                        fps,
605
                        output_format,
606
                    };
607
                    let rebuilt = CameraConfig {
608
                        facing: cfg.facing,
609
                        width: cfg.width,
610
                        height: cfg.height,
611
                        fps: cfg.fps,
612
                        output_format: cfg.output_format,
613
                    };
614
                    assert_eq!(cfg, rebuilt);
615
                    // ...and the fields survived verbatim - no clamping.
616
                    assert_eq!(rebuilt.width, width);
617
                    assert_eq!(rebuilt.height, height);
618
                    assert_eq!(rebuilt.fps, fps);
619
                    assert_eq!(rebuilt.facing, facing);
620
                    assert_eq!(rebuilt.output_format, output_format);
621
                }
622
            }
623
        }
624
    }
625

            
626
    #[test]
627
    fn camera_config_does_not_confuse_width_and_height() {
628
        // Guards against a transposed-field bug in any future manual PartialEq
629
        // or FFI struct layout: a config is NOT equal to its transpose.
630
        let a = CameraConfig {
631
            width: 1920,
632
            height: 1080,
633
            ..CameraConfig::default()
634
        };
635
        let transposed = CameraConfig {
636
            width: 1080,
637
            height: 1920,
638
            ..CameraConfig::default()
639
        };
640
        assert_ne!(a, transposed, "width/height must be distinguishable");
641
        // A square config IS its own transpose - sanity check on the above.
642
        let square = CameraConfig {
643
            width: 512,
644
            height: 512,
645
            ..CameraConfig::default()
646
        };
647
        let square2 = CameraConfig {
648
            width: 512,
649
            height: 512,
650
            ..CameraConfig::default()
651
        };
652
        assert_eq!(square, square2);
653
    }
654

            
655
    #[test]
656
    fn camera_config_eq_is_sensitive_to_every_field() {
657
        // Flipping any single field must break equality - i.e. no field is
658
        // accidentally excluded from the derived PartialEq.
659
        let base = CameraConfig::default();
660
        assert_ne!(
661
            base,
662
            CameraConfig {
663
                facing: CameraFacing::Front,
664
                ..base
665
            }
666
        );
667
        assert_ne!(base, CameraConfig { width: 1, ..base });
668
        assert_ne!(base, CameraConfig { height: 1, ..base });
669
        assert_ne!(base, CameraConfig { fps: 1, ..base });
670
        assert_ne!(
671
            base,
672
            CameraConfig {
673
                output_format: RawImageFormat::RGBA8,
674
                ..base
675
            }
676
        );
677
    }
678

            
679
    #[test]
680
    fn capture_stats_eq_is_sensitive_to_every_field() {
681
        let base = CaptureStats::default();
682
        assert_ne!(
683
            base,
684
            CaptureStats {
685
                measured_fps: 1.0,
686
                ..base
687
            }
688
        );
689
        assert_ne!(
690
            base,
691
            CaptureStats {
692
                frames_delivered: 1,
693
                ..base
694
            }
695
        );
696
        assert_ne!(
697
            base,
698
            CaptureStats {
699
                frames_dropped: 1,
700
                ..base
701
            }
702
        );
703
    }
704

            
705
    #[test]
706
    // These types are Copy; calling .clone() explicitly is the point of the
707
    // test (Clone must agree with Copy), so the lint is deliberately waived.
708
    #[allow(clippy::clone_on_copy)]
709
    fn clone_is_identity_for_all_pod_types_at_extremes() {
710
        let cfg = CameraConfig {
711
            facing: CameraFacing::External,
712
            width: u32::MAX,
713
            height: u32::MAX,
714
            fps: u32::MAX,
715
            output_format: RawImageFormat::RGBAF32,
716
        };
717
        assert_eq!(cfg.clone(), cfg);
718

            
719
        let id = CaptureStreamId { id: u64::MAX };
720
        assert_eq!(id.clone(), id);
721

            
722
        // NaN is excluded here on purpose: clone of NaN cannot compare equal
723
        // (see capture_stats_nan_fps_breaks_eq_reflexivity); assert bit
724
        // equality instead, which IS reflexive.
725
        let nan_stats = CaptureStats {
726
            measured_fps: f32::NAN,
727
            frames_delivered: u64::MAX,
728
            frames_dropped: u64::MAX,
729
        };
730
        let cloned = nan_stats.clone();
731
        assert_eq!(
732
            cloned.measured_fps.to_bits(),
733
            nan_stats.measured_fps.to_bits(),
734
            "clone must preserve the exact NaN bit pattern"
735
        );
736
        assert_eq!(cloned.frames_delivered, u64::MAX);
737
        assert_eq!(cloned.frames_dropped, u64::MAX);
738
    }
739

            
740
    // ---- Numeric: NaN ordering, saturation, overflow, precision limits ----
741

            
742
    #[test]
743
    fn capture_stats_nan_fps_makes_partial_cmp_none() {
744
        // CaptureStats is only PartialEq (no Ord) precisely because of the f32.
745
        // A NaN fps must yield an *incomparable* float, not a silent ordering.
746
        let nan = f32::NAN;
747
        assert!(nan.partial_cmp(&0.0).is_none());
748
        assert!(nan.partial_cmp(&nan).is_none());
749

            
750
        let s = CaptureStats {
751
            measured_fps: nan,
752
            ..CaptureStats::default()
753
        };
754
        // A NaN-carrying stats value can never be *found* by equality search,
755
        // so consumers must not rely on `contains` / dedup for it.
756
        let haystack = [s, CaptureStats::default()];
757
        assert!(!haystack.contains(&s), "NaN stats is unfindable by Eq");
758
        assert!(haystack.contains(&CaptureStats::default()));
759
    }
760

            
761
    #[test]
762
    fn measured_fps_to_integer_cast_saturates_instead_of_wrapping() {
763
        // HUD code casting measured_fps to an integer must not hit UB or wrap.
764
        // Rust `as` casts are saturating: NaN -> 0, +inf -> MAX, negative -> 0.
765
        let cases: [(f32, u32); 6] = [
766
            (f32::NAN, 0),
767
            (f32::INFINITY, u32::MAX),
768
            (f32::NEG_INFINITY, 0),
769
            (-1.0, 0),
770
            (-0.0, 0),
771
            (f32::MAX, u32::MAX),
772
        ];
773
        for (fps, expected) in cases {
774
            let s = CaptureStats {
775
                measured_fps: fps,
776
                ..CaptureStats::default()
777
            };
778
            assert_eq!(
779
                s.measured_fps as u32, expected,
780
                "cast of {fps} must saturate to {expected}"
781
            );
782
        }
783
        // Ordinary value is unchanged (truncates toward zero).
784
        let ok = CaptureStats {
785
            measured_fps: 59.94,
786
            ..CaptureStats::default()
787
        };
788
        assert_eq!(ok.measured_fps as u32, 59);
789
    }
790

            
791
    #[test]
792
    fn frame_counters_overflow_only_via_checked_arithmetic() {
793
        // `frames_delivered + frames_dropped` is the obvious "total frames"
794
        // consumers will compute - at the saturated boundary it overflows.
795
        let s = CaptureStats {
796
            measured_fps: 30.0,
797
            frames_delivered: u64::MAX,
798
            frames_dropped: 1,
799
        };
800
        assert_eq!(
801
            s.frames_delivered.checked_add(s.frames_dropped),
802
            None,
803
            "total-frames overflows u64; consumers must saturate/check"
804
        );
805
        assert_eq!(
806
            s.frames_delivered.saturating_add(s.frames_dropped),
807
            u64::MAX,
808
            "saturating_add is the safe path"
809
        );
810
        // One below the boundary is fine.
811
        let ok = CaptureStats {
812
            frames_delivered: u64::MAX - 1,
813
            frames_dropped: 1,
814
            ..CaptureStats::default()
815
        };
816
        assert_eq!(
817
            ok.frames_delivered.checked_add(ok.frames_dropped),
818
            Some(u64::MAX)
819
        );
820
    }
821

            
822
    #[test]
823
    fn drop_rate_over_zeroed_stats_must_be_guarded() {
824
        // Default stats have delivered == dropped == 0, so the natural drop
825
        // rate `dropped / (delivered + dropped)` divides by zero: integer
826
        // division would PANIC, float division yields NaN. Assert both, so the
827
        // hazard is pinned rather than discovered in a HUD at runtime.
828
        let s = CaptureStats::default();
829
        let total = s.frames_delivered + s.frames_dropped;
830
        assert_eq!(total, 0);
831
        assert_eq!(
832
            s.frames_dropped.checked_div(total),
833
            None,
834
            "integer drop-rate on zeroed stats must be checked, not raw `/`"
835
        );
836
        let rate = s.frames_dropped as f64 / total as f64;
837
        assert!(rate.is_nan(), "float drop-rate on zeroed stats is NaN");
838

            
839
        // With traffic, the rate is well-defined.
840
        let live = CaptureStats {
841
            measured_fps: 30.0,
842
            frames_delivered: 75,
843
            frames_dropped: 25,
844
        };
845
        let total = live.frames_delivered + live.frames_dropped;
846
        assert_eq!(live.frames_dropped.checked_div(total), Some(0));
847
        assert!(((live.frames_dropped as f64 / total as f64) - 0.25).abs() < 1e-12);
848
    }
849

            
850
    #[test]
851
    fn config_pixel_count_overflows_u32_at_extreme_dimensions() {
852
        // width * height is the natural buffer-size computation. At the
853
        // documented-legal extremes it overflows u32 and must be widened.
854
        let cfg = CameraConfig {
855
            width: u32::MAX,
856
            height: u32::MAX,
857
            ..CameraConfig::default()
858
        };
859
        assert_eq!(
860
            cfg.width.checked_mul(cfg.height),
861
            None,
862
            "u32 pixel count overflows; widen to u64 before multiplying"
863
        );
864
        assert_eq!(
865
            u64::from(cfg.width) * u64::from(cfg.height),
866
            (u64::from(u32::MAX)) * (u64::from(u32::MAX)),
867
            "u64 widening is the safe path"
868
        );
869
        // A realistic size does not overflow.
870
        let hd = CameraConfig {
871
            width: 1920,
872
            height: 1080,
873
            ..CameraConfig::default()
874
        };
875
        assert_eq!(hd.width.checked_mul(hd.height), Some(2_073_600));
876
    }
877

            
878
    #[test]
879
    fn fps_u32_to_f32_roundtrip_is_lossy_above_2_pow_24() {
880
        // The config's requested `fps` is u32 while the reported `measured_fps`
881
        // is f32. Comparing them via a cast is exact only below 2^24.
882
        let exact: u32 = 1 << 24; // 16_777_216
883
        assert_eq!(exact as f32 as u32, exact, "2^24 round-trips exactly");
884

            
885
        let lossy: u32 = (1 << 24) + 1; // 16_777_217
886
        assert_ne!(
887
            lossy as f32 as u32,
888
            lossy,
889
            "2^24+1 must NOT round-trip through f32 - precision is lost"
890
        );
891
        assert_eq!(lossy as f32 as u32, exact, "it rounds down to 2^24");
892

            
893
        // Realistic fps values are far below the boundary and are exact.
894
        for fps in [0u32, 1, 24, 30, 60, 120, 240] {
895
            let cfg = CameraConfig {
896
                fps,
897
                ..CameraConfig::default()
898
            };
899
            assert_eq!(cfg.fps as f32 as u32, fps, "fps {fps} must be exact");
900
        }
901
    }
902

            
903
    #[test]
904
    fn frames_delivered_u64_to_f32_roundtrip_is_lossy_at_the_boundary() {
905
        // Same hazard on the counter side: a HUD computing fps as
906
        // frames_delivered as f32 / secs loses counts past 2^24.
907
        let s = CaptureStats {
908
            frames_delivered: (1 << 24) + 1,
909
            ..CaptureStats::default()
910
        };
911
        assert_ne!(s.frames_delivered as f32 as u64, s.frames_delivered);
912
        // u64::MAX -> f32 rounds UP past u64::MAX, and the cast back saturates.
913
        let sat = CaptureStats {
914
            frames_delivered: u64::MAX,
915
            ..CaptureStats::default()
916
        };
917
        assert_eq!(
918
            sat.frames_delivered as f32 as u64,
919
            u64::MAX,
920
            "saturating cast clamps rather than wrapping to 0"
921
        );
922
    }
923

            
924
    // ---- Predicates / invariants over the whole variant space ----
925

            
926
    #[test]
927
    fn all_enum_variants_are_pairwise_distinct_and_hash_consistently() {
928
        use std::collections::HashSet;
929
        assert_eq!(
930
            ALL_STATES.iter().collect::<HashSet<_>>().len(),
931
            ALL_STATES.len(),
932
            "StreamState variants must be pairwise distinct"
933
        );
934
        assert_eq!(
935
            ALL_ORIENTATIONS.iter().collect::<HashSet<_>>().len(),
936
            ALL_ORIENTATIONS.len(),
937
            "CaptureOrientation variants must be pairwise distinct"
938
        );
939
        assert_eq!(
940
            ALL_ERRORS.iter().collect::<HashSet<_>>().len(),
941
            ALL_ERRORS.len(),
942
            "CaptureErrorCode variants must be pairwise distinct"
943
        );
944
        assert_eq!(
945
            ALL_FORMATS.iter().collect::<HashSet<_>>().len(),
946
            ALL_FORMATS.len(),
947
            "RawImageFormat variants must be pairwise distinct"
948
        );
949
    }
950

            
951
    #[test]
952
    fn ord_is_a_total_order_for_every_camera_enum() {
953
        use core::cmp::Ordering;
954
        // Trichotomy + antisymmetry + transitivity, exhaustively over the
955
        // (small) variant spaces. CameraFacing is already covered above.
956
        macro_rules! assert_total_order {
957
            ($set:expr) => {{
958
                for &a in $set {
959
                    assert_eq!(a.cmp(&a), Ordering::Equal);
960
                    for &b in $set {
961
                        let lt = (a < b) as u8;
962
                        let eq = (a == b) as u8;
963
                        let gt = (a > b) as u8;
964
                        assert_eq!(lt + eq + gt, 1, "trichotomy ({a:?}, {b:?})");
965
                        if a < b {
966
                            assert!(!(b < a), "antisymmetry ({a:?}, {b:?})");
967
                        }
968
                        for &c in $set {
969
                            if a < b && b < c {
970
                                assert!(a < c, "transitivity ({a:?}, {b:?}, {c:?})");
971
                            }
972
                        }
973
                    }
974
                }
975
            }};
976
        }
977
        assert_total_order!(&ALL_STATES);
978
        assert_total_order!(&ALL_ORIENTATIONS);
979
        assert_total_order!(&ALL_ERRORS);
980
    }
981

            
982
    #[test]
983
    fn capture_stream_id_hash_eq_and_copy_independence() {
984
        use std::collections::HashSet;
985
        let mut set = HashSet::new();
986
        for id in [0u64, 1, u64::MAX / 2, u64::MAX] {
987
            assert!(set.insert(CaptureStreamId { id }), "id {id} is new");
988
        }
989
        assert_eq!(set.len(), 4);
990
        // Re-inserting an equal id must be rejected (Hash agrees with Eq).
991
        assert!(!set.insert(CaptureStreamId { id: u64::MAX }));
992
        assert!(set.contains(&CaptureStreamId { id: 0 }));
993

            
994
        // Copy: mutating the copy must not disturb the original.
995
        let a = CaptureStreamId { id: 7 };
996
        let mut b = a;
997
        b.id = 9;
998
        assert_eq!(a.id, 7);
999
        assert_eq!(b.id, 9);
    }
    #[test]
    fn capture_stats_copy_is_independent() {
        let a = CaptureStats {
            measured_fps: 30.0,
            frames_delivered: 100,
            frames_dropped: 2,
        };
        let mut b = a;
        b.measured_fps = 0.0;
        b.frames_delivered = 0;
        b.frames_dropped = 0;
        assert_eq!(a.measured_fps, 30.0, "original must survive copy mutation");
        assert_eq!(a.frames_delivered, 100);
        assert_eq!(a.frames_dropped, 2);
        assert_eq!(b, CaptureStats::default());
    }
    #[test]
    fn debug_formatting_never_panics_on_extreme_or_nan_values() {
        // Debug is what a crash report / HUD log prints; it must survive the
        // adversarial values above.
        let stats = CaptureStats {
            measured_fps: f32::NAN,
            frames_delivered: u64::MAX,
            frames_dropped: u64::MAX,
        };
        assert!(!format!("{stats:?}").is_empty());
        assert!(!format!("{:?}", CaptureStats {
            measured_fps: f32::NEG_INFINITY,
            ..CaptureStats::default()
        })
        .is_empty());
        let cfg = CameraConfig {
            facing: CameraFacing::External,
            width: u32::MAX,
            height: u32::MAX,
            fps: u32::MAX,
            output_format: RawImageFormat::RGBAF32,
        };
        let s = format!("{cfg:?}");
        assert!(s.contains("External"), "Debug must name the facing: {s}");
        assert!(s.contains("4294967295"), "Debug must show raw extents: {s}");
        assert!(!format!("{:?}", CaptureStreamId { id: u64::MAX }).is_empty());
        for &st in &ALL_STATES {
            assert!(!format!("{st:?}").is_empty());
        }
        for &o in &ALL_ORIENTATIONS {
            assert!(!format!("{o:?}").is_empty());
        }
        for &e in &ALL_ERRORS {
            assert!(!format!("{e:?}").is_empty());
        }
    }
    #[test]
    fn repr_c_layout_invariants_hold_for_the_ffi_pod_types() {
        use core::mem::{align_of, size_of};
        // Single-field #[repr(C)] newtype: must be exactly its payload, so it
        // can be passed across the FFI as a bare u64.
        assert_eq!(size_of::<CaptureStreamId>(), size_of::<u64>());
        assert_eq!(align_of::<CaptureStreamId>(), align_of::<u64>());
        // No exact byte sizes asserted for the aggregates: u64 alignment (and
        // therefore trailing padding) differs on 32-bit targets. Assert the
        // portable invariants instead - big enough for the payload, and a
        // whole number of alignment units (a valid array element).
        assert!(size_of::<CaptureStats>() >= size_of::<f32>() + 2 * size_of::<u64>());
        assert_eq!(size_of::<CaptureStats>() % align_of::<CaptureStats>(), 0);
        assert!(size_of::<CameraConfig>() >= 3 * size_of::<u32>());
        assert_eq!(size_of::<CameraConfig>() % align_of::<CameraConfig>(), 0);
        // Fieldless repr(C) enums are C ints - never zero-sized, never huge.
        assert_eq!(size_of::<CameraFacing>(), size_of::<StreamState>());
        assert_eq!(size_of::<StreamState>(), size_of::<CaptureOrientation>());
        assert_eq!(size_of::<CaptureOrientation>(), size_of::<CaptureErrorCode>());
        assert!(size_of::<CameraFacing>() > 0);
    }
}