Skip to main content

nv_view/
view_state.rs

1//! Core view state types: [`ViewState`], [`ViewSnapshot`], [`ViewEpoch`], [`ViewVersion`].
2
3use std::fmt;
4use std::sync::Arc;
5
6use crate::camera_motion::{CameraMotionState, MotionSource};
7use crate::ptz::PtzTelemetry;
8use crate::transform::GlobalTransformEstimate;
9use crate::transition::TransitionPhase;
10use crate::validity::ContextValidity;
11
12/// Opaque monotonic epoch counter.
13///
14/// Incremented when the view system (via its configured [`EpochPolicy`](crate::EpochPolicy))
15/// determines a discontinuity occurred that warrants segmenting temporal state.
16///
17/// Within a single uninterrupted feed session with `CameraMode::Fixed`,
18/// the epoch never changes. Even for fixed feeds, epoch increments on feed restart.
19#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
20pub struct ViewEpoch(u64);
21
22impl ViewEpoch {
23    /// The initial epoch (zero).
24    pub const INITIAL: Self = Self(0);
25
26    /// Create from a raw value.
27    #[must_use]
28    pub fn new(val: u64) -> Self {
29        Self(val)
30    }
31
32    /// Return the next epoch.
33    #[must_use]
34    pub fn next(self) -> Self {
35        Self(self.0 + 1)
36    }
37
38    /// Returns the raw value.
39    #[must_use]
40    pub fn as_u64(self) -> u64 {
41        self.0
42    }
43}
44
45impl fmt::Display for ViewEpoch {
46    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
47        write!(f, "epoch-{}", self.0)
48    }
49}
50
51impl fmt::Debug for ViewEpoch {
52    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53        write!(f, "ViewEpoch({})", self.0)
54    }
55}
56
57/// Monotonic version counter for the view-state itself.
58///
59/// Incremented on every [`ViewState`] update — even within the same epoch.
60/// Allows consumers to cheaply test staleness: `my_version == current.version()`
61/// without deep-comparing the full `ViewState`.
62#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default)]
63pub struct ViewVersion(u64);
64
65impl ViewVersion {
66    /// The initial version (zero).
67    pub const INITIAL: Self = Self(0);
68
69    /// Create from a raw value.
70    #[must_use]
71    pub fn new(val: u64) -> Self {
72        Self(val)
73    }
74
75    /// Return the next version.
76    #[must_use]
77    pub fn next(self) -> Self {
78        Self(self.0 + 1)
79    }
80
81    /// Returns the raw value.
82    #[must_use]
83    pub fn as_u64(self) -> u64 {
84        self.0
85    }
86
87    /// Number of versions between `self` and `other`.
88    /// Returns 0 if `other >= self`.
89    #[must_use]
90    pub fn versions_since(self, other: Self) -> u64 {
91        self.0.saturating_sub(other.0)
92    }
93}
94
95impl fmt::Display for ViewVersion {
96    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
97        write!(f, "v{}", self.0)
98    }
99}
100
101impl fmt::Debug for ViewVersion {
102    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
103        write!(f, "ViewVersion({})", self.0)
104    }
105}
106
107/// The current best estimate of the camera's view.
108///
109/// Updated every frame by the view system. Stages and output consumers
110/// receive a [`ViewSnapshot`] (read-only, `Arc`-wrapped) rather than
111/// `ViewState` directly.
112#[derive(Clone, Debug)]
113pub struct ViewState {
114    /// Current epoch — incremented on significant view discontinuities.
115    pub epoch: ViewEpoch,
116    /// Current version — incremented on every view-state update.
117    pub version: ViewVersion,
118    /// Whether the camera is stable, moving, or unknown.
119    pub motion: CameraMotionState,
120    /// How the current motion state was determined.
121    pub motion_source: MotionSource,
122    /// Position in the motion transition state machine.
123    pub transition: TransitionPhase,
124    /// PTZ telemetry, if available.
125    pub ptz: Option<PtzTelemetry>,
126    /// Global transform estimate (frame-to-reference), if available.
127    pub global_transform: Option<GlobalTransformEstimate>,
128    /// Whether temporal state is valid under the current view.
129    pub validity: ContextValidity,
130    /// Stability score in `[0.0, 1.0]` — 1.0 = fully stable.
131    pub stability_score: f32,
132}
133
134impl ViewState {
135    /// Create the default initial view state for a fixed camera.
136    #[must_use]
137    pub fn fixed_initial() -> Self {
138        Self {
139            epoch: ViewEpoch::INITIAL,
140            version: ViewVersion::INITIAL,
141            motion: CameraMotionState::Stable,
142            motion_source: MotionSource::None,
143            transition: TransitionPhase::Settled,
144            ptz: None,
145            global_transform: None,
146            validity: ContextValidity::Valid,
147            stability_score: clamp_unit(1.0),
148        }
149    }
150
151    /// Create the default initial view state for an observed camera.
152    ///
153    /// Starts with `CameraMotionState::Unknown` and `ContextValidity::Degraded`
154    /// because no motion data has been received yet. The view system will
155    /// upgrade to `Valid` once a provider delivers stable motion data.
156    #[must_use]
157    pub fn observed_initial() -> Self {
158        Self {
159            epoch: ViewEpoch::INITIAL,
160            version: ViewVersion::INITIAL,
161            motion: CameraMotionState::Unknown,
162            motion_source: MotionSource::None,
163            transition: TransitionPhase::Settled,
164            ptz: None,
165            global_transform: None,
166            validity: ContextValidity::Degraded {
167                reason: crate::validity::DegradationReason::Unknown,
168            },
169            stability_score: clamp_unit(0.0),
170        }
171    }
172}
173
174/// Read-only, cheaply-cloneable snapshot of [`ViewState`].
175///
176/// Created once per frame by the view system and shared with all stages
177/// via `StageContext`. `Clone` is an `Arc` bump.
178///
179/// Stages and output consumers receive `ViewSnapshot`, not `ViewState`.
180#[derive(Clone, Debug)]
181pub struct ViewSnapshot {
182    inner: Arc<ViewState>,
183}
184
185impl ViewSnapshot {
186    /// Create a snapshot from a `ViewState`.
187    #[must_use]
188    pub fn new(state: ViewState) -> Self {
189        Self {
190            inner: Arc::new(state),
191        }
192    }
193
194    /// Current epoch.
195    #[must_use]
196    pub fn epoch(&self) -> ViewEpoch {
197        self.inner.epoch
198    }
199
200    /// Current version.
201    #[must_use]
202    pub fn version(&self) -> ViewVersion {
203        self.inner.version
204    }
205
206    /// Camera motion state.
207    #[must_use]
208    pub fn motion(&self) -> &CameraMotionState {
209        &self.inner.motion
210    }
211
212    /// How the motion state was determined.
213    #[must_use]
214    pub fn motion_source(&self) -> &MotionSource {
215        &self.inner.motion_source
216    }
217
218    /// Transition phase.
219    #[must_use]
220    pub fn transition(&self) -> TransitionPhase {
221        self.inner.transition
222    }
223
224    /// PTZ telemetry, if available.
225    #[must_use]
226    pub fn ptz(&self) -> Option<&PtzTelemetry> {
227        self.inner.ptz.as_ref()
228    }
229
230    /// Global transform estimate, if available.
231    #[must_use]
232    pub fn global_transform(&self) -> Option<&GlobalTransformEstimate> {
233        self.inner.global_transform.as_ref()
234    }
235
236    /// Context validity under the current view.
237    #[must_use]
238    pub fn validity(&self) -> &ContextValidity {
239        &self.inner.validity
240    }
241
242    /// Stability score in `[0.0, 1.0]`.
243    #[must_use]
244    pub fn stability_score(&self) -> f32 {
245        self.inner.stability_score
246    }
247
248    /// Borrow the underlying `ViewState`.
249    #[must_use]
250    pub fn as_view_state(&self) -> &ViewState {
251        &self.inner
252    }
253}
254
255fn clamp_unit(v: f32) -> f32 {
256    if v.is_finite() {
257        v.clamp(0.0, 1.0)
258    } else {
259        0.0
260    }
261}
262
263#[cfg(test)]
264mod tests {
265    use super::*;
266
267    // -- ViewEpoch --
268
269    #[test]
270    fn view_epoch_initial_is_zero() {
271        assert_eq!(ViewEpoch::INITIAL.as_u64(), 0);
272    }
273
274    #[test]
275    fn view_epoch_next_increments() {
276        let e = ViewEpoch::INITIAL;
277        assert_eq!(e.next().as_u64(), 1);
278        assert_eq!(e.next().next().as_u64(), 2);
279    }
280
281    #[test]
282    fn view_epoch_ordering() {
283        let a = ViewEpoch::new(3);
284        let b = ViewEpoch::new(5);
285        assert!(a < b);
286        assert_eq!(a, ViewEpoch::new(3));
287    }
288
289    #[test]
290    fn view_epoch_display() {
291        assert_eq!(format!("{}", ViewEpoch::new(7)), "epoch-7");
292    }
293
294    // -- ViewVersion --
295
296    #[test]
297    fn view_version_initial_is_zero() {
298        assert_eq!(ViewVersion::INITIAL.as_u64(), 0);
299    }
300
301    #[test]
302    fn view_version_next_increments() {
303        assert_eq!(ViewVersion::INITIAL.next().as_u64(), 1);
304    }
305
306    #[test]
307    fn view_version_versions_since() {
308        let v5 = ViewVersion::new(5);
309        let v2 = ViewVersion::new(2);
310        assert_eq!(v5.versions_since(v2), 3);
311        // Returns 0 when other >= self.
312        assert_eq!(v2.versions_since(v5), 0);
313    }
314
315    // -- ViewState constructors --
316
317    #[test]
318    fn fixed_initial_is_stable_valid() {
319        let vs = ViewState::fixed_initial();
320        assert_eq!(vs.epoch, ViewEpoch::INITIAL);
321        assert_eq!(vs.motion, CameraMotionState::Stable);
322        assert_eq!(vs.validity, ContextValidity::Valid);
323        assert_eq!(vs.stability_score, 1.0);
324        assert_eq!(vs.transition, TransitionPhase::Settled);
325    }
326
327    #[test]
328    fn observed_initial_is_unknown_degraded() {
329        let vs = ViewState::observed_initial();
330        assert_eq!(vs.motion, CameraMotionState::Unknown);
331        assert!(matches!(vs.validity, ContextValidity::Degraded { .. }));
332        assert_eq!(vs.stability_score, 0.0);
333    }
334
335    // -- ViewSnapshot --
336
337    #[test]
338    fn snapshot_reflects_state() {
339        let vs = ViewState::fixed_initial();
340        let snap = ViewSnapshot::new(vs);
341        assert_eq!(snap.epoch(), ViewEpoch::INITIAL);
342        assert_eq!(snap.stability_score(), 1.0);
343        assert_eq!(snap.transition(), TransitionPhase::Settled);
344    }
345
346    #[test]
347    fn snapshot_clone_is_cheap() {
348        let snap = ViewSnapshot::new(ViewState::fixed_initial());
349        let snap2 = snap.clone();
350        // Both should point to the same Arc allocation.
351        assert!(std::ptr::eq(snap.as_view_state(), snap2.as_view_state()));
352    }
353}