Skip to main content

sim_lib_view_device/
rate.rs

1//! Timing envelopes for device surfaces.
2
3use sim_kernel::{Expr, NumberLiteral};
4use sim_value::{access, build};
5
6/// The declared content, adaptation, and staleness timing envelope.
7#[derive(Clone, Copy, Debug, PartialEq, Eq)]
8pub struct RateClass {
9    /// Content refresh cadence in hertz.
10    pub content_hz: u16,
11    /// Adapter loop cadence in hertz.
12    pub adapt_hz: u16,
13    /// Maximum stale data age accepted by the surface.
14    pub max_stale_ms: u16,
15}
16
17impl RateClass {
18    /// Safe fallback for missing rate metadata.
19    pub fn safe_default() -> Self {
20        Self {
21            content_hz: 1,
22            adapt_hz: 1,
23            max_stale_ms: 1000,
24        }
25    }
26
27    /// Low-rate watch-style envelope.
28    pub fn watch() -> Self {
29        Self {
30            content_hz: 1,
31            adapt_hz: 1,
32            max_stale_ms: 4000,
33        }
34    }
35
36    /// HUD-style envelope for low-bandwidth visual overlays.
37    pub fn hud() -> Self {
38        Self {
39            content_hz: 5,
40            adapt_hz: 30,
41            max_stale_ms: 500,
42        }
43    }
44
45    /// Stereo display envelope for rich pose-coupled surfaces.
46    pub fn stereo() -> Self {
47        Self {
48            content_hz: 60,
49            adapt_hz: 120,
50            max_stale_ms: 100,
51        }
52    }
53
54    /// Encodes this envelope as an open `rate` map.
55    pub fn to_expr(self) -> Expr {
56        build::map(vec![
57            ("content-hz", build::uint(u64::from(self.content_hz))),
58            ("adapt-hz", build::uint(u64::from(self.adapt_hz))),
59            ("max-stale-ms", build::uint(u64::from(self.max_stale_ms))),
60        ])
61    }
62
63    /// Parses a `rate` map.
64    pub fn from_expr(expr: &Expr) -> Result<Self, RateError> {
65        let Expr::Map(_) = expr else {
66            return Err(RateError::NotRateMap);
67        };
68        Ok(Self {
69            content_hz: rate_field(expr, "content-hz")?,
70            adapt_hz: rate_field(expr, "adapt-hz")?,
71            max_stale_ms: rate_field(expr, "max-stale-ms")?,
72        })
73    }
74
75    /// Parses an optional `rate` map, using [`RateClass::safe_default`] when it
76    /// is absent.
77    pub fn from_optional_expr(expr: Option<&Expr>) -> Result<Self, RateError> {
78        match expr {
79            Some(expr) => Self::from_expr(expr),
80            None => Ok(Self::safe_default()),
81        }
82    }
83}
84
85/// A reason a rate envelope could not be parsed.
86#[derive(Clone, Debug, PartialEq, Eq)]
87pub enum RateError {
88    /// The value was not a map.
89    NotRateMap,
90    /// A required rate field was missing.
91    MissingField(&'static str),
92    /// A rate field was not a non-negative integer literal.
93    BadField(&'static str),
94    /// A rate field was zero.
95    ZeroField(&'static str),
96    /// A rate field exceeded `u16`.
97    OutOfRange(&'static str),
98}
99
100impl core::fmt::Display for RateError {
101    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
102        match self {
103            RateError::NotRateMap => write!(f, "rate value is not a map"),
104            RateError::MissingField(name) => write!(f, "rate map missing field: {name}"),
105            RateError::BadField(name) => write!(f, "rate field has wrong shape: {name}"),
106            RateError::ZeroField(name) => write!(f, "rate field must be non-zero: {name}"),
107            RateError::OutOfRange(name) => write!(f, "rate field exceeds u16: {name}"),
108        }
109    }
110}
111
112impl std::error::Error for RateError {}
113
114fn rate_field(expr: &Expr, name: &'static str) -> Result<u16, RateError> {
115    let Some(value) = access::field(expr, name) else {
116        return Err(RateError::MissingField(name));
117    };
118    let Some(number) = integer_number(value) else {
119        return Err(RateError::BadField(name));
120    };
121    let parsed = number
122        .canonical
123        .parse::<u64>()
124        .map_err(|_| RateError::BadField(name))?;
125    if parsed == 0 {
126        return Err(RateError::ZeroField(name));
127    }
128    u16::try_from(parsed).map_err(|_| RateError::OutOfRange(name))
129}
130
131fn integer_number(expr: &Expr) -> Option<&NumberLiteral> {
132    match expr {
133        Expr::Number(number)
134            if number.domain.namespace.is_none()
135                && matches!(number.domain.name.as_ref(), "i64" | "u64") =>
136        {
137            Some(number)
138        }
139        _ => None,
140    }
141}