Skip to main content

xpanse_api/interfaces/
video.rs

1//! Direct RGB565 video resources available to apps.
2//!
3//! Apps can acquire a `Rgb565FrameBuffer` capability (via the
4//! [`crate::registry::Registry`]) and use it to render pixels directly to a
5//! caller-provided buffer. The display task picks up presented frames through
6//! the paired `Rgb565FrameDisplay`.
7
8use alloc::sync::Arc;
9use core::{
10    cell::RefCell,
11    marker::PhantomData,
12    sync::atomic::{AtomicU32, Ordering},
13};
14
15use embassy_sync::blocking_mutex::{CriticalSectionMutex, ThreadModeMutex};
16
17pub use slint::platform::software_renderer::Rgb565Pixel;
18
19static NEXT_FRAME_ID: AtomicU32 = AtomicU32::new(1);
20
21/// Error returned when creating or using a frame buffer.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, defmt::Format)]
23pub enum FrameBufferError {
24    /// The supplied dimensions are zero or do not match the storage slice length.
25    InvalidDimensions,
26}
27
28struct FrameStorage {
29    pixels: ThreadModeMutex<RefCell<&'static mut [Rgb565Pixel]>>,
30    len: usize,
31    width: u16,
32    height: u16,
33}
34
35struct ActiveFrame {
36    id: u32,
37    width: u16,
38    height: u16,
39    revision: AtomicU32,
40}
41
42struct FrameLink {
43    storage: FrameStorage,
44    active: CriticalSectionMutex<RefCell<Option<Arc<ActiveFrame>>>>,
45}
46
47/// The app-facing RGB565 framebuffer capability stored in the registry.
48///
49/// Write pixels via [`Rgb565FrameSession`] (obtained through
50/// [`Rgb565FrameBuffer::start`]), then call `present` on the session to make
51/// them visible to the display task.
52pub struct Rgb565FrameBuffer {
53    link: Arc<FrameLink>,
54}
55
56/// The display-task endpoint paired with `Rgb565FrameBuffer`.
57///
58/// Created alongside a `Rgb565FrameBuffer` by `rgb565_frame_buffer`. The
59/// display task uses it to read the currently active frame via
60/// `active_frame()`.
61pub struct Rgb565FrameDisplay {
62    link: Arc<FrameLink>,
63}
64
65/// Creates a platform framebuffer resource backed by caller-provided storage.
66///
67/// Returns a `(Rgb565FrameBuffer, Rgb565FrameDisplay)` pair — the app-facing
68/// buffer handle and the display-task handle. Both share the same underlying
69/// pixel storage.
70///
71/// # Errors
72///
73/// Returns [`FrameBufferError::InvalidDimensions`] if `width * height` is zero
74/// or does not match `pixels.len()`.
75///
76/// # Example
77///
78/// ```ignore
79/// use xpanse_api::interfaces::video::{rgb565_frame_buffer, Rgb565Pixel};
80///
81/// static mut FRAMEBUF: [Rgb565Pixel; 320 * 240] = [Rgb565Pixel(0); 320 * 240];
82///
83/// # fn example() {
84/// let pixels = unsafe {
85///     core::slice::from_raw_parts_mut(&raw mut FRAMEBUF, 320 * 240)
86/// };
87/// let (buffer, display) = rgb565_frame_buffer(pixels, 320, 240).unwrap();
88/// # }
89/// ```
90pub fn rgb565_frame_buffer(
91    pixels: &'static mut [Rgb565Pixel],
92    width: u16,
93    height: u16,
94) -> Result<(Rgb565FrameBuffer, Rgb565FrameDisplay), FrameBufferError> {
95    let expected_len = usize::from(width)
96        .checked_mul(usize::from(height))
97        .filter(|len| *len > 0)
98        .ok_or(FrameBufferError::InvalidDimensions)?;
99    if pixels.len() != expected_len {
100        return Err(FrameBufferError::InvalidDimensions);
101    }
102
103    let len = pixels.len();
104    let link = Arc::new(FrameLink {
105        storage: FrameStorage {
106            pixels: ThreadModeMutex::new(RefCell::new(pixels)),
107            len,
108            width,
109            height,
110        },
111        active: CriticalSectionMutex::new(RefCell::new(None)),
112    });
113    Ok((
114        Rgb565FrameBuffer {
115            link: Arc::clone(&link),
116        },
117        Rgb565FrameDisplay { link },
118    ))
119}
120
121impl Rgb565FrameBuffer {
122    /// Starts a direct-rendering session which remains active until dropped.
123    ///
124    /// The session dimensions must fit within the buffer allocated by
125    /// [`rgb565_frame_buffer`].
126    ///
127    /// # Errors
128    ///
129    /// Returns [`FrameBufferError::InvalidDimensions`] if the requested
130    /// dimensions exceed the backing storage or are zero.
131    pub fn start(
132        &mut self,
133        width: u16,
134        height: u16,
135    ) -> Result<Rgb565FrameSession<'_>, FrameBufferError> {
136        let len = usize::from(width)
137            .checked_mul(usize::from(height))
138            .filter(|len| *len > 0)
139            .ok_or(FrameBufferError::InvalidDimensions)?;
140        if width > self.link.storage.width
141            || height > self.link.storage.height
142            || len > self.link.storage.len
143        {
144            return Err(FrameBufferError::InvalidDimensions);
145        }
146
147        let frame = Arc::new(ActiveFrame {
148            id: NEXT_FRAME_ID.fetch_add(1, Ordering::Relaxed),
149            width,
150            height,
151            revision: AtomicU32::new(0),
152        });
153        self.link
154            .storage
155            .pixels
156            .lock(|pixels| pixels.borrow_mut()[..len].fill(Rgb565Pixel(0)));
157        self.link
158            .active
159            .lock(|active| *active.borrow_mut() = Some(Arc::clone(&frame)));
160
161        Ok(Rgb565FrameSession {
162            link: Arc::clone(&self.link),
163            frame,
164            resource: PhantomData,
165        })
166    }
167}
168
169/// A scoped direct-rendering session borrowed from the registry resource.
170///
171/// Obtain one via [`Rgb565FrameBuffer::start`]. Dropping the session releases
172/// the active-frame lock so another task can begin a new session.
173pub struct Rgb565FrameSession<'a> {
174    link: Arc<FrameLink>,
175    frame: Arc<ActiveFrame>,
176    resource: PhantomData<&'a mut Rgb565FrameBuffer>,
177}
178
179impl Rgb565FrameSession<'_> {
180    /// Set a single pixel within the active session dimensions.
181    ///
182    /// Pixels outside the bounds are silently ignored.
183    pub fn set_pixel(&self, x: u16, y: u16, color: Rgb565Pixel) {
184        if x >= self.frame.width || y >= self.frame.height {
185            return;
186        }
187        let index = usize::from(y) * usize::from(self.frame.width) + usize::from(x);
188        self.link
189            .storage
190            .pixels
191            .lock(|pixels| pixels.borrow_mut()[index] = color);
192    }
193
194    /// Makes all pixel writes since the previous call available to the display task.
195    ///
196    /// The display task reads the frame's token (see [`PresentedFrame::token`])
197    /// to detect when new content is available.
198    pub fn present(&self) {
199        self.frame.revision.fetch_add(1, Ordering::Release);
200    }
201}
202
203impl Drop for Rgb565FrameSession<'_> {
204    fn drop(&mut self) {
205        self.link.active.lock(|active| {
206            let mut active = active.borrow_mut();
207            if active
208                .as_ref()
209                .is_some_and(|frame| Arc::ptr_eq(frame, &self.frame))
210            {
211                *active = None;
212            }
213        });
214    }
215}
216
217/// A snapshot of a frame that has been presented by an app.
218///
219/// Returned by [`Rgb565FrameDisplay::active_frame`]. The display task should
220/// compare [`token`](Self::token) values to detect when a new frame is available.
221pub struct PresentedFrame {
222    link: Arc<FrameLink>,
223    frame: Arc<ActiveFrame>,
224}
225
226impl PresentedFrame {
227    /// Width of the presented frame in pixels.
228    pub fn width(&self) -> u16 {
229        self.frame.width
230    }
231
232    /// Height of the presented frame in pixels.
233    pub fn height(&self) -> u16 {
234        self.frame.height
235    }
236
237    /// Total number of pixels (`width * height`).
238    pub fn len(&self) -> usize {
239        usize::from(self.frame.width) * usize::from(self.frame.height)
240    }
241
242    /// Returns `true` if the frame has zero pixels.
243    pub fn is_empty(&self) -> bool {
244        self.len() == 0
245    }
246
247    /// Access the pixel slice of the frame for rendering.
248    pub fn with_pixels<R>(&self, f: impl FnOnce(&[Rgb565Pixel]) -> R) -> R {
249        let len = self.len();
250        self.link
251            .storage
252            .pixels
253            .lock(|pixels| f(&pixels.borrow()[..len]))
254    }
255
256    /// Monotonically changing token that changes when the app presents a new
257    /// revision.
258    ///
259    /// The upper 32 bits encode the frame ID; the lower 32 bits encode the
260    /// revision counter.
261    pub fn token(&self) -> u64 {
262        let revision = self.frame.revision.load(Ordering::Acquire);
263        (u64::from(self.frame.id) << 32) | u64::from(revision)
264    }
265}
266
267impl Rgb565FrameDisplay {
268    /// Returns the frame currently presented by the app holding the resource.
269    ///
270    /// If no app currently has an active rendering session, returns `None`.
271    pub fn active_frame(&self) -> Option<PresentedFrame> {
272        self.link.active.lock(|active| {
273            active
274                .borrow()
275                .as_ref()
276                .cloned()
277                .map(|frame| PresentedFrame {
278                    link: Arc::clone(&self.link),
279                    frame,
280                })
281        })
282    }
283
284    /// Gives the platform mutable access to the shared storage while direct video is inactive.
285    ///
286    /// Returns `None` if an app currently has an active rendering session.
287    pub fn with_buffer_mut<R>(&self, f: impl FnOnce(&mut [Rgb565Pixel]) -> R) -> Option<R> {
288        if self.link.active.lock(|active| active.borrow().is_some()) {
289            return None;
290        }
291        self.link.storage.pixels.lock(|pixels| {
292            let mut pixels = pixels.borrow_mut();
293            Some(f(&mut pixels))
294        })
295    }
296}