sim_lib_web_bridge/
glasses.rs1use std::rc::Rc;
4
5use sim_kernel::{Error, Expr, Result};
6use sim_lib_scene::GlanceCard;
7use sim_lib_view::SurfaceCaps;
8use sim_lib_view_device::{
9 DeviceProfile, DeviceSurfaceCapsExt, EncodedScene, GlanceState, GlassesClass, LocalAdapter,
10 glasses_class,
11};
12use sim_lib_view_spatial::{ClampedReprojector, PoseView, halo_glance_config};
13use sim_value::{access, build};
14
15#[derive(Clone, Copy, Debug, PartialEq, Eq)]
17pub struct GlassesViewport {
18 per_eye_px: [u32; 2],
19}
20
21impl GlassesViewport {
22 pub fn from_caps(caps: &SurfaceCaps) -> Result<Self> {
28 let values = match access::required(&caps.display, "per-eye-px", "glasses display caps")? {
29 Expr::List(values) if values.len() == 2 => values,
30 _ => {
31 return Err(Error::HostError(
32 "glasses display per-eye-px must contain width and height".to_owned(),
33 ));
34 }
35 };
36 Ok(Self {
37 per_eye_px: [read_px(&values[0])?, read_px(&values[1])?],
38 })
39 }
40
41 pub fn per_eye_px(self) -> [u32; 2] {
43 self.per_eye_px
44 }
45
46 pub fn frame_px(self) -> [u32; 2] {
48 [self.per_eye_px[0].saturating_mul(2), self.per_eye_px[1]]
49 }
50}
51
52#[derive(Debug)]
57pub struct VitureSceneClient {
58 profile: DeviceProfile,
59 viewport: GlassesViewport,
60 reprojector: ClampedReprojector,
61 scene: Option<EncodedScene>,
62 content_receipts: u64,
63}
64
65impl VitureSceneClient {
66 pub fn new(caps: &SurfaceCaps, max_predict_ms: u64) -> Result<Self> {
73 let profile = caps.device_profile();
74 match glasses_class(&profile) {
75 Some(GlassesClass::Stereo6Dof | GlassesClass::DisplayOnly) => {}
76 _ => {
77 return Err(Error::HostError(
78 "Viture client requires stereo or display-only glasses caps".to_owned(),
79 ));
80 }
81 }
82 Ok(Self {
83 profile,
84 viewport: GlassesViewport::from_caps(caps)?,
85 reprojector: ClampedReprojector::new(max_predict_ms),
86 scene: None,
87 content_receipts: 0,
88 })
89 }
90
91 pub fn receive(&mut self, scene: Expr) -> Result<()> {
97 expect_scene_kind(&scene, "spatial")?;
98 self.scene = Some(EncodedScene::new(scene));
99 self.content_receipts = self.content_receipts.saturating_add(1);
100 Ok(())
101 }
102
103 pub fn frame(&self, pose: &PoseView) -> Result<Rc<Expr>> {
110 let scene = self
111 .scene
112 .as_ref()
113 .ok_or_else(|| Error::HostError("Viture client has no content Scene".to_owned()))?;
114 let frame = self.reprojector.adapt(scene, pose, &self.profile)?;
115 if glasses_class(&self.profile) == Some(GlassesClass::DisplayOnly) {
116 return Ok(frame);
117 }
118 Ok(Rc::new(with_stereo_viewport(frame.as_ref(), self.viewport)))
119 }
120
121 pub fn content_receipts(&self) -> u64 {
123 self.content_receipts
124 }
125
126 pub fn viewport(&self) -> GlassesViewport {
128 self.viewport
129 }
130
131 pub fn is_mirror(&self) -> bool {
133 glasses_class(&self.profile) == Some(GlassesClass::DisplayOnly)
134 }
135}
136
137#[derive(Debug)]
139pub struct HaloPreviewClient {
140 profile: DeviceProfile,
141 scene: Option<EncodedScene>,
142 content_receipts: u64,
143}
144
145impl HaloPreviewClient {
146 pub fn new(caps: &SurfaceCaps) -> Result<Self> {
152 let profile = caps.device_profile();
153 if glasses_class(&profile) != Some(GlassesClass::MonoHud) {
154 return Err(Error::HostError(
155 "Halo preview requires mono-HUD glasses caps".to_owned(),
156 ));
157 }
158 Ok(Self {
159 profile,
160 scene: None,
161 content_receipts: 0,
162 })
163 }
164
165 pub fn receive(&mut self, scene: Expr) -> Result<()> {
171 GlanceCard::from_scene(&scene)?;
172 self.scene = Some(EncodedScene::new(scene));
173 self.content_receipts = self.content_receipts.saturating_add(1);
174 Ok(())
175 }
176
177 pub fn frame(&self, state: &GlanceState) -> Result<Rc<Expr>> {
184 let scene = self
185 .scene
186 .as_ref()
187 .ok_or_else(|| Error::HostError("Halo preview has no glance Scene".to_owned()))?;
188 halo_glance_config().adapt(scene, state, &self.profile)
189 }
190
191 pub fn content_receipts(&self) -> u64 {
193 self.content_receipts
194 }
195}
196
197fn read_px(expr: &Expr) -> Result<u32> {
198 let Expr::Number(number) = expr else {
199 return Err(Error::HostError(
200 "glasses viewport dimensions must be numbers".to_owned(),
201 ));
202 };
203 number
204 .canonical
205 .parse::<u32>()
206 .ok()
207 .filter(|value| *value > 0)
208 .ok_or_else(|| Error::HostError("glasses viewport dimensions must be positive".to_owned()))
209}
210
211fn with_stereo_viewport(scene: &Expr, viewport: GlassesViewport) -> Expr {
212 let per_eye = viewport.per_eye_px();
213 let frame = viewport.frame_px();
214 let scene = access::set(scene, "layout", build::sym("side-by-side"));
215 let scene = access::set(
216 &scene,
217 "eye-px",
218 build::list(vec![
219 build::uint(per_eye[0].into()),
220 build::uint(per_eye[1].into()),
221 ]),
222 );
223 access::set(
224 &scene,
225 "frame-px",
226 build::list(vec![
227 build::uint(frame[0].into()),
228 build::uint(frame[1].into()),
229 ]),
230 )
231}
232
233fn expect_scene_kind(scene: &Expr, expected: &str) -> Result<()> {
234 let matches = sim_lib_scene::node_kind(scene).is_some_and(|kind| {
235 kind.namespace.as_deref() == Some(sim_lib_scene::SCENE_NAMESPACE)
236 && kind.name.as_ref() == expected
237 });
238 if matches {
239 Ok(())
240 } else {
241 Err(Error::HostError(format!("expected scene/{expected}")))
242 }
243}