Skip to main content

sim/runtime/glasses/proofs/
two_rate.rs

1use std::rc::Rc;
2
3use sim_kernel::{Error, Expr, Result};
4use sim_lib_scene::{Anchor, AnchorSpace, Transform3};
5use sim_lib_stream_device::ModeledSource;
6use sim_lib_stream_xr::{ModeledViturePoseSource, XrPoseSample};
7use sim_lib_view::{SurfaceCaps, SurfaceCodec};
8use sim_lib_view_device::{AdapterInput, DeviceSurfaceCapsExt, EncodedScene, FrameClock};
9use sim_lib_view_spatial::{PoseView, SpatialSurfaceCodec, viture_loop};
10use sim_value::build;
11
12/// Result of the modeled Viture two-rate reprojection proof.
13#[derive(Clone, Debug, PartialEq, Eq)]
14pub struct TwoRateProof {
15    /// Number of content-rate encodes performed.
16    pub content_encodes: u64,
17    /// Number of coalesced pose updates reported as drops.
18    pub dropped: u32,
19    /// Whether the stale pose frame is marked stale.
20    pub stale: bool,
21    /// Prediction lead after the reprojector clamp.
22    pub clamped_predict_ms: u64,
23    /// Whether a pose beyond the clamp holds the prior frame.
24    pub held_after_clamp: bool,
25}
26
27impl TwoRateProof {
28    /// Encodes the proof as expression data for cookbook recipes.
29    pub fn to_expr(&self) -> Expr {
30        build::map(vec![
31            ("kind", build::qsym("glasses/sdk", "two-rate-proof")),
32            ("content-encodes", build::uint(self.content_encodes)),
33            ("dropped", build::uint(u64::from(self.dropped))),
34            ("stale", Expr::Bool(self.stale)),
35            ("clamped-predict-ms", build::uint(self.clamped_predict_ms)),
36            ("held-after-clamp", Expr::Bool(self.held_after_clamp)),
37        ])
38    }
39}
40
41/// Encodes once and reprojects a modeled Viture pose track at device rate.
42pub fn prove_two_rate() -> Result<TwoRateProof> {
43    let caps = SurfaceCaps::from_preset("glasses-luma-ultra", "sdk.viture")
44        .ok_or_else(|| Error::HostError("Viture surface preset missing".to_owned()))?;
45    let profile = caps.device_profile();
46    let mut cx = sim_kernel::testing::bare_cx();
47    let scene = SpatialSurfaceCodec::new().encode(&mut cx, &workspace_scene(), &caps)?;
48    let encoded = EncodedScene::new(scene);
49    let content_encodes = 1;
50    let source = ModeledViturePoseSource;
51    let (mut adapter, clock) = viture_loop(&profile, 12);
52
53    let mut newest = pose_view(&source.at(0), 1, 4_000_000);
54    for index in 0..4 {
55        newest = pose_view(&source.at(index), 1, 4_000_000);
56        adapter.offer(&newest);
57    }
58    let fresh = adapter.step(
59        &clock,
60        &AdapterInput::new(encoded.clone(), 41, newest, clock.tick),
61        &profile,
62    )?;
63
64    let stale_clock = FrameClock::new(4, profile.rate);
65    let stale_pose = pose_view(&source.at(5), 12, 40_000_000);
66    adapter.offer(&stale_pose);
67    let stale = adapter.step(
68        &stale_clock,
69        &AdapterInput::new(encoded.clone(), 41, stale_pose, 0),
70        &profile,
71    )?;
72
73    let beyond = pose_view(&source.at(6), 13, 80_000_000);
74    adapter.offer(&beyond);
75    let held = adapter.step(
76        &stale_clock,
77        &AdapterInput::new(encoded, 41, beyond, 0),
78        &profile,
79    )?;
80
81    Ok(TwoRateProof {
82        content_encodes,
83        dropped: fresh.dropped,
84        stale: stale.stale,
85        clamped_predict_ms: field_u64(stale.out.as_ref(), "predict-ms").unwrap_or(0),
86        held_after_clamp: Rc::ptr_eq(&stale.out, &held.out),
87    })
88}
89
90fn pose_view(sample: &XrPoseSample, age_ms: u64, predict_ns: u64) -> PoseView {
91    let mut pose = PoseView::identity(sample.seq()).with_timing(age_ms, predict_ns);
92    if let Some(position) = sample.position_m() {
93        pose = pose.with_translation(position);
94    }
95    pose
96}
97
98fn workspace_scene() -> Expr {
99    sim_lib_scene::spatial(vec![sim_lib_scene::panel(
100        "review",
101        sim_lib_scene::text_node("Modeled review"),
102        Anchor::new(AnchorSpace::World, "desk"),
103        Transform3::new([0.0, 0.0, -1.6], [0.0, 0.0, 0.0, 1.0], [1.0, 1.0, 1.0]),
104    )])
105}
106
107fn field_u64(expr: &Expr, name: &str) -> Option<u64> {
108    let Expr::Number(number) = sim_value::access::field(expr, name)? else {
109        return None;
110    };
111    number.canonical.parse().ok()
112}