Skip to main content

sim_lib_view_device/
loop.rs

1//! Deterministic adapter loop with staleness and drop accounting.
2
3use std::rc::Rc;
4
5use sim_kernel::{Expr, Result};
6use sim_value::build;
7
8use crate::{DeviceProfile, EncodedScene, FrameClock, LocalAdapter};
9
10/// Input consumed by one adapter-loop step.
11#[derive(Clone, Debug, PartialEq)]
12pub struct AdapterInput<S> {
13    /// Latest content-rate encoded Scene.
14    pub encoded: EncodedScene,
15    /// Sequence of the encoded Scene.
16    pub encoded_seq: u64,
17    /// Freshest device-local state available for this step.
18    pub state: S,
19    /// Modeled tick sequence for `state`.
20    pub state_seq: u64,
21}
22
23impl<S> AdapterInput<S> {
24    /// Builds an input from an encoded Scene and state.
25    pub fn new(encoded: EncodedScene, encoded_seq: u64, state: S, state_seq: u64) -> Self {
26        Self {
27            encoded,
28            encoded_seq,
29            state,
30            state_seq,
31        }
32    }
33
34    /// Builds an input from a shared encoded Scene pointer.
35    pub fn from_shared_scene(scene: Rc<Expr>, encoded_seq: u64, state: S, state_seq: u64) -> Self {
36        Self::new(
37            EncodedScene::from_shared(scene),
38            encoded_seq,
39            state,
40            state_seq,
41        )
42    }
43}
44
45/// Staleness behavior for adapter-loop steps.
46#[derive(Clone, Copy, Debug, PartialEq, Eq)]
47pub enum StalePolicy {
48    /// Reuse the last emitted frame, falling back to the encoded Scene.
49    HoldLast,
50    /// Run the adapter against the stale state and mark the frame stale.
51    Predict,
52    /// Emit a deterministic blank frame.
53    Blank,
54}
55
56/// Output of one adapter-loop step.
57#[derive(Clone, Debug, PartialEq)]
58pub struct Frame {
59    /// Local adapter output.
60    pub out: Rc<Expr>,
61    /// Number of offered state updates coalesced before this step.
62    pub dropped: u32,
63    /// Whether the newest state exceeded the stale window.
64    pub stale: bool,
65    /// Modeled frame sequence.
66    pub seq: u64,
67    /// Sequence of the encoded Scene used for this step.
68    pub encoded_seq: u64,
69}
70
71/// Device-rate loop over an already encoded Scene and a local adapter.
72///
73/// The loop is pure and deterministic. It has no runtime context, no
74/// [`sim_lib_view::SurfaceCodec`], and no route back to content encoding.
75#[derive(Clone, Debug)]
76pub struct AdapterLoop<A> {
77    adapter: A,
78    policy: StalePolicy,
79    last: Option<Rc<Expr>>,
80    pending: u32,
81}
82
83impl<A> AdapterLoop<A> {
84    /// Builds a loop for one adapter and staleness policy.
85    pub fn new(adapter: A, policy: StalePolicy) -> Self {
86        Self {
87            adapter,
88            policy,
89            last: None,
90            pending: 0,
91        }
92    }
93
94    /// Returns the adapter.
95    pub fn adapter(&self) -> &A {
96        &self.adapter
97    }
98
99    /// Returns the configured staleness policy.
100    pub fn policy(&self) -> StalePolicy {
101        self.policy
102    }
103
104    /// Returns the number of offered updates waiting for the next step.
105    pub fn pending(&self) -> u32 {
106        self.pending
107    }
108}
109
110impl<A: LocalAdapter> AdapterLoop<A> {
111    /// Records one available device-local state update.
112    pub fn offer(&mut self, _state: &A::State) {
113        self.pending = self.pending.saturating_add(1);
114    }
115
116    /// Advances the loop by one modeled frame.
117    pub fn step(
118        &mut self,
119        clock: &FrameClock,
120        input: &AdapterInput<A::State>,
121        profile: &DeviceProfile,
122    ) -> Result<Frame> {
123        let dropped = self.pending.saturating_sub(1);
124        self.pending = 0;
125        let stale = clock.stale(input.state_seq);
126        let out = if stale {
127            match self.policy {
128                StalePolicy::HoldLast => {
129                    self.last.clone().unwrap_or_else(|| input.encoded.shared())
130                }
131                StalePolicy::Predict => {
132                    self.adapter.adapt(&input.encoded, &input.state, profile)?
133                }
134                StalePolicy::Blank => Rc::new(blank_frame(profile)),
135            }
136        } else {
137            self.adapter.adapt(&input.encoded, &input.state, profile)?
138        };
139        self.last = Some(Rc::clone(&out));
140        Ok(Frame {
141            out,
142            dropped,
143            stale,
144            seq: clock.tick,
145            encoded_seq: input.encoded_seq,
146        })
147    }
148}
149
150/// Builds the deterministic blank frame used by [`StalePolicy::Blank`].
151pub fn blank_frame(profile: &DeviceProfile) -> Expr {
152    build::map(vec![
153        ("kind", build::qsym("device", "blank-frame")),
154        ("tier", Expr::Symbol(profile.tier.to_symbol())),
155    ])
156}