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