sim-lib-view-device 0.1.0

Device profiles, frame timing, and tier derivation for SIM Web surfaces.
Documentation
use std::{
    rc::Rc,
    sync::{
        Arc,
        atomic::{AtomicUsize, Ordering},
    },
};

use sim_kernel::{Expr, Result};
use sim_value::build;

use crate::{
    AdapterInput, AdapterLoop, DeviceTier, EncodedScene, FrameClock, LocalAdapter, RateClass,
    StalePolicy, blank_frame, tier_preset,
};

#[test]
fn loop_counts_drops_and_honors_staleness() {
    let encoded = EncodedScene::new(encoded_scene());
    let profile = tier_preset(DeviceTier::Actuator);
    let mut clock = FrameClock::at_zero(RateClass::watch());
    let mut adapter_loop = AdapterLoop::new(StateTagAdapter, StalePolicy::Predict);
    let encode_count = Arc::new(AtomicUsize::new(0));
    let mut dropped = 0;

    for batch in [3_u64, 3, 4] {
        let mut newest = 0;
        for _ in 0..batch {
            newest += 1;
            adapter_loop.offer(&newest);
        }
        let input = AdapterInput::new(encoded.clone(), 11, newest, clock.tick);
        let frame = adapter_loop
            .step(&clock, &input, &profile)
            .expect("adapter step");
        dropped += frame.dropped;
        assert_eq!(frame.encoded_seq, 11);
        clock.advance();
    }

    assert!(
        dropped >= 7,
        "expected at least 7 dropped inputs, got {dropped}"
    );
    assert_eq!(encode_count.load(Ordering::SeqCst), 0);
}

#[test]
fn hold_last_repeats_previous_frame_for_stale_state() {
    let encoded = EncodedScene::new(encoded_scene());
    let profile = tier_preset(DeviceTier::Actuator);
    let rate = RateClass {
        content_hz: 1,
        adapt_hz: 1,
        max_stale_ms: 1,
    };
    let mut clock = FrameClock::at_zero(rate);
    let mut adapter_loop = AdapterLoop::new(StateTagAdapter, StalePolicy::HoldLast);

    adapter_loop.offer(&7);
    let first = adapter_loop
        .step(
            &clock,
            &AdapterInput::new(encoded.clone(), 3, 7, clock.tick),
            &profile,
        )
        .expect("fresh frame");
    assert!(!first.stale);

    clock.advance();
    clock.advance();
    adapter_loop.offer(&8);
    let stale = adapter_loop
        .step(&clock, &AdapterInput::new(encoded, 3, 8, 0), &profile)
        .expect("stale frame");

    assert!(stale.stale);
    assert!(Rc::ptr_eq(&stale.out, &first.out));
}

#[test]
fn blank_policy_emits_profile_tagged_blank_frame() {
    let encoded = EncodedScene::new(encoded_scene());
    let profile = tier_preset(DeviceTier::Display);
    let rate = RateClass {
        content_hz: 1,
        adapt_hz: 1,
        max_stale_ms: 1,
    };
    let clock = FrameClock::new(2, rate);
    let mut adapter_loop = AdapterLoop::new(StateTagAdapter, StalePolicy::Blank);

    adapter_loop.offer(&1);
    let frame = adapter_loop
        .step(&clock, &AdapterInput::new(encoded, 9, 1, 0), &profile)
        .expect("blank frame");

    assert!(frame.stale);
    assert_eq!(frame.out.as_ref(), &blank_frame(&profile));
}

#[test]
fn frame_clock_uses_modeled_ticks_for_staleness() {
    let rate = RateClass {
        content_hz: 1,
        adapt_hz: 10,
        max_stale_ms: 150,
    };
    let mut clock = FrameClock::at_zero(rate);
    clock.advance();
    assert_eq!(clock.elapsed_ms_since(0), 100);
    assert!(!clock.stale(0));
    clock.advance();
    assert_eq!(clock.elapsed_ms_since(0), 200);
    assert!(clock.stale(0));
}

#[derive(Clone, Copy, Debug)]
struct StateTagAdapter;

impl LocalAdapter for StateTagAdapter {
    type State = u64;

    fn adapt(
        &self,
        scene: &EncodedScene,
        state: &Self::State,
        _profile: &crate::DeviceProfile,
    ) -> Result<Rc<Expr>> {
        Ok(Rc::new(build::map(vec![
            ("kind", build::qsym("device", "adapted")),
            ("scene", scene.expr().clone()),
            ("state", build::uint(*state)),
        ])))
    }
}

fn encoded_scene() -> Expr {
    build::map(vec![
        ("kind", build::qsym("scene", "text")),
        ("text", build::text("encoded")),
    ])
}