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::safe_default()
30    }
31
32    /// HUD-style envelope for low-bandwidth visual overlays.
33    pub fn hud() -> Self {
34        Self {
35            content_hz: 5,
36            adapt_hz: 30,
37            max_stale_ms: 500,
38        }
39    }
40
41    /// Stereo display envelope for rich pose-coupled surfaces.
42    pub fn stereo() -> Self {
43        Self {
44            content_hz: 60,
45            adapt_hz: 120,
46            max_stale_ms: 100,
47        }
48    }
49
50    /// Encodes this envelope as an open `rate` map.
51    pub fn to_expr(self) -> Expr {
52        build::map(vec![
53            ("content-hz", build::uint(u64::from(self.content_hz))),
54            ("adapt-hz", build::uint(u64::from(self.adapt_hz))),
55            ("max-stale-ms", build::uint(u64::from(self.max_stale_ms))),
56        ])
57    }
58
59    /// Parses a `rate` map.
60    pub fn from_expr(expr: &Expr) -> Result<Self, RateError> {
61        let Expr::Map(_) = expr else {
62            return Err(RateError::NotRateMap);
63        };
64        Ok(Self {
65            content_hz: rate_field(expr, "content-hz")?,
66            adapt_hz: rate_field(expr, "adapt-hz")?,
67            max_stale_ms: rate_field(expr, "max-stale-ms")?,
68        })
69    }
70
71    /// Parses an optional `rate` map, using [`RateClass::safe_default`] when it
72    /// is absent.
73    pub fn from_optional_expr(expr: Option<&Expr>) -> Result<Self, RateError> {
74        match expr {
75            Some(expr) => Self::from_expr(expr),
76            None => Ok(Self::safe_default()),
77        }
78    }
79}
80
81/// A reason a rate envelope could not be parsed.
82#[derive(Clone, Debug, PartialEq, Eq)]
83pub enum RateError {
84    /// The value was not a map.
85    NotRateMap,
86    /// A required rate field was missing.
87    MissingField(&'static str),
88    /// A rate field was not a non-negative integer literal.
89    BadField(&'static str),
90    /// A rate field was zero.
91    ZeroField(&'static str),
92    /// A rate field exceeded `u16`.
93    OutOfRange(&'static str),
94}
95
96impl core::fmt::Display for RateError {
97    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
98        match self {
99            RateError::NotRateMap => write!(f, "rate value is not a map"),
100            RateError::MissingField(name) => write!(f, "rate map missing field: {name}"),
101            RateError::BadField(name) => write!(f, "rate field has wrong shape: {name}"),
102            RateError::ZeroField(name) => write!(f, "rate field must be non-zero: {name}"),
103            RateError::OutOfRange(name) => write!(f, "rate field exceeds u16: {name}"),
104        }
105    }
106}
107
108impl std::error::Error for RateError {}
109
110fn rate_field(expr: &Expr, name: &'static str) -> Result<u16, RateError> {
111    let Some(value) = access::field(expr, name) else {
112        return Err(RateError::MissingField(name));
113    };
114    let Some(number) = integer_number(value) else {
115        return Err(RateError::BadField(name));
116    };
117    let parsed = number
118        .canonical
119        .parse::<u64>()
120        .map_err(|_| RateError::BadField(name))?;
121    if parsed == 0 {
122        return Err(RateError::ZeroField(name));
123    }
124    u16::try_from(parsed).map_err(|_| RateError::OutOfRange(name))
125}
126
127fn integer_number(expr: &Expr) -> Option<&NumberLiteral> {
128    match expr {
129        Expr::Number(number)
130            if number.domain.namespace.is_none()
131                && matches!(number.domain.name.as_ref(), "i64" | "u64") =>
132        {
133            Some(number)
134        }
135        _ => None,
136    }
137}