Skip to main content

capsules_extra/screen/screen_adapters/
mono_vlsb.rs

1// Licensed under the Apache License, Version 2.0 or the MIT License.
2// SPDX-License-Identifier: Apache-2.0 OR MIT
3// Copyright Tock Contributors 2026.
4
5use core::cell::Cell;
6
7use kernel::ErrorCode;
8use kernel::hil::screen::{Screen, ScreenClient, ScreenPixelFormat, ScreenRotation};
9use kernel::utilities::cells::OptionalCell;
10use kernel::utilities::leasable_buffer::SubSliceMut;
11
12use super::utils::Frame;
13
14/// Expose an underlying ARGB8888-formatted screen as a Mono VLSB
15/// (a.k.a. `Mono_8BitPage`) screen of the same resolution.
16///
17/// # Mono VLSB source layout
18///
19/// Mono VLSB packs 8 vertically-adjacent pixels into a single byte,
20/// from top to bottom, grouped into width-sized "pages", from left to
21/// right. A page is a row of width `N` and height 8 pixels, formed by
22/// `N` successive bytes. So, bit `k` of byte `col + (row / 8) *
23/// width` is the pixel at (`col`, row-within-frame `row`).
24///
25/// For a write frame of height `h`, the source needs `width * ceil(h
26/// / 8)` bytes; if the height is not a multiple of 8, the high bits
27/// of each column in the last page are unused.
28///
29/// # ARGB8888 output layout
30///
31/// One pixel per four bytes, in byte order `[A, R, G, B]` with A =
32/// 0xFF.
33///
34/// # Chunking and draw-buffer sizing
35///
36/// The `Screen` HIL allows large writes to be split across multiple
37/// `write(_, continue_write=true)` calls. This adapter accepts any
38/// chunk size (in Mono VLSB bytes); internally it processes one
39/// sub-rectangle at a time on the underlying screen, so the draw
40/// buffer only needs to hold one sub-op's ARGB output. A larger draw
41/// buffer lets one client `write` be satisfied in fewer underlying
42/// `set_write_frame + write` round-trips (at most one per full-width
43/// page held by the buffer) at the cost of more static RAM.
44///
45/// The buffer must be able to hold at least one row of the widest
46/// sub-op the adapter will ever emit --- i.e. one row of the client's
47/// write-frame width (`frame.width * 4` bytes). Boards that expect
48/// full-width writes should size it to at least `resource_width * 8 *
49/// 4` (one full-width page) so head/tail partial sub-ops always fit
50/// in a single underlying write.
51pub struct ScreenARGB8888ToMono8BitPage<'a, S: Screen<'a>> {
52    screen: &'a S,
53    /// ARGB pixel data buffer
54    draw_buffer: OptionalCell<SubSliceMut<'static, u8>>,
55    /// Underlying-array length of the draw buffer, cached so that
56    /// sub-op sizing can consult it without taking the buffer out.
57    draw_buffer_capacity: usize,
58    /// The client-provided source buffer, held for the duration of a
59    /// single `write` call's chunk.
60    client_buffer: OptionalCell<SubSliceMut<'static, u8>>,
61    client: OptionalCell<&'a dyn ScreenClient>,
62
63    /// Client's logical write frame. Set by `set_write_frame`.
64    display_frame: Cell<Frame>,
65
66    /// Byte cursor into the frame's Mono VLSB stream. Advanced by
67    /// each sub-op as it completes and reset by `set_write_frame` and
68    /// `write(_, false)`.
69    frame_cursor: Cell<usize>,
70    /// Value of `frame_cursor` at the start of the current chunk. The
71    /// next byte to consume in the client buffer is at offset
72    /// `frame_cursor - chunk_start`.
73    chunk_start: Cell<usize>,
74    /// Frame position at which the current chunk ends. When
75    /// `frame_cursor` reaches this, the client-visible write is done.
76    chunk_end: Cell<usize>,
77
78    /// The underlying-screen rectangle for the sub-op currently in
79    /// flight (between `start_next_sub_op` and
80    /// `write_complete`). Must only be [Option::Some] if there is an
81    /// outstanding write operation to the underlying device. That is,
82    /// on [ScreenClient::command_complete], if this is
83    /// [Option::None], the completion is for a client initiated
84    /// `set_frame`, if this is `Option::Some` it, the completion is
85    /// for an internal operation's `set_frame`.
86    current_op: Cell<Option<Frame>>,
87}
88
89impl<'a, S: Screen<'a>> ScreenARGB8888ToMono8BitPage<'a, S> {
90    pub fn new(screen: &'a S, draw_buffer: &'static mut [u8]) -> Self {
91        // Draw buffer must hold whole 4-byte pixels.
92        assert!(draw_buffer.len().is_multiple_of(4));
93        let capacity = draw_buffer.len();
94        ScreenARGB8888ToMono8BitPage {
95            screen,
96            draw_buffer: OptionalCell::new(SubSliceMut::new(draw_buffer)),
97            draw_buffer_capacity: capacity,
98            client_buffer: OptionalCell::empty(),
99            client: OptionalCell::empty(),
100            display_frame: Cell::new(Frame::default()),
101            frame_cursor: Cell::new(0),
102            chunk_start: Cell::new(0),
103            chunk_end: Cell::new(0),
104            current_op: Cell::new(None),
105        }
106    }
107}
108
109/// Convert one Mono VLSB sub-rectangle's worth of source bytes into
110/// row-major ARGB8888.
111///
112/// The sub-rectangle is `sub_cols` columns wide by `dst.len() /
113/// (sub_cols * 4)` rows tall. `src` provides Mono VLSB bytes relative
114/// to the sub-rectangle (page 0 first, then page 1, ...); it must be
115/// at least `sub_cols * ceil(rows / 8)` bytes. Unused high bits of
116/// the final page (when the row count is not a multiple of 8) are
117/// ignored.
118///
119/// A set bit becomes an opaque white ARGB pixel; a cleared bit an opaque
120/// black one. Output byte order is `[A, R, G, B]` with A = 0xFF.
121fn convert_mvlsb_sub_rect(src: &[u8], dst: &mut [u8], sub_cols: usize) {
122    // destination bytes per row
123    let row_bytes = sub_cols * 4;
124    // destination bytes per page
125    let page_bytes = 8 * row_bytes;
126    // Iterate over each source and destination "page" (columns * 8)
127    for (src_page, dst_page) in src.chunks(sub_cols).zip(dst.chunks_mut(page_bytes)) {
128        // Within a page, iterate on each row and corresponding bit
129        // position in src bytes.
130        for (bit, dst_row) in dst_page.chunks_exact_mut(row_bytes).enumerate() {
131            // Extract the row of source pixels
132            let src_row_pixels = src_page.iter().map(|b| (b >> bit) & 1 != 0);
133            // Within a row, each source byte fills one 4-byte pixel.
134            let dst_row_pixels = dst_row.chunks_exact_mut(4);
135
136            for (src_pixel, dst_pixel) in src_row_pixels.zip(dst_row_pixels) {
137                // Yellow foreground, black background.
138                let color = if src_pixel {
139                    [0, 0xFF, 0xFF, 0xFF]
140                } else {
141                    [0, 0, 0, 0xFF]
142                };
143                dst_pixel.copy_from_slice(&color);
144            }
145        }
146    }
147}
148
149/// How tall, in rows, is Mono VLSB page `page` given the total frame
150/// height (accounts for the final page being partial when
151/// `frame_h % 8 != 0`).
152fn page_rows(page: usize, frame_h: usize) -> usize {
153    core::cmp::min(8, frame_h.saturating_sub(page * 8))
154}
155
156/// Number of Mono VLSB source bytes a sub-op of shape `op` consumes.
157fn sub_op_src_bytes(op: Frame) -> usize {
158    op.width * op.height.div_ceil(8)
159}
160
161/// Number of ARGB destination bytes a sub-op of shape `op` writes.
162fn sub_op_dst_bytes(op: Frame) -> usize {
163    op.width * op.height * 4
164}
165
166/// Compute the next underlying-screen sub-rectangle given the current
167/// position in the client's Mono VLSB stream, the end of the current
168/// chunk, and the draw buffer's byte capacity.
169///
170/// Chooses a sub-op greedily: as many full pages as fit in both the
171/// remaining chunk and the draw buffer, or a single partial page (head
172/// or tail).
173///
174/// Returns `None` when (nothing left in this chunk).
175fn next_sub_op(
176    display_frame: Frame,
177    cursor: usize,
178    chunk_end: usize,
179    draw_buffer_capacity: usize,
180) -> Option<Frame> {
181    if cursor >= chunk_end {
182        return None;
183    }
184    let start_col = cursor % display_frame.width;
185    let start_page = cursor / display_frame.width;
186    let this_page_rows = page_rows(start_page, display_frame.height);
187    let remaining = chunk_end - cursor;
188
189    // Head partial page: chunk begins/continues mid-page (start_col > 0).
190    // One page's worth of rows, up to (W - start_col) columns wide.
191    if start_col > 0 {
192        let cols = core::cmp::min(display_frame.width - start_col, remaining);
193        return Some(Frame {
194            x: display_frame.x + start_col,
195            y: display_frame.y + start_page * 8,
196            width: cols,
197            height: this_page_rows,
198        });
199    }
200
201    // Tail partial page: at page boundary but fewer than W bytes
202    // remain, so we can't fill a full-width page.
203    if remaining < display_frame.width {
204        return Some(Frame {
205            x: display_frame.x,
206            y: display_frame.y + start_page * 8,
207            width: remaining,
208            height: this_page_rows,
209        });
210    }
211
212    // Middle band: at a page boundary with at least one full page
213    // remaining. Grab as many full pages as fit in both the chunk and
214    // the draw buffer.
215    let pages_available = remaining / display_frame.width;
216    let mut pages_taken = 0;
217    let mut rows_taken = 0;
218    for i in 0..pages_available {
219        let ph = page_rows(start_page + i, display_frame.height);
220        if display_frame.width * (rows_taken + ph) * 4 > draw_buffer_capacity {
221            break;
222        }
223        pages_taken += 1;
224        rows_taken += ph;
225    }
226    debug_assert!(
227        pages_taken >= 1,
228        "draw_buffer_capacity too small to hold one full-width page",
229    );
230    Some(Frame {
231        x: display_frame.x,
232        y: display_frame.y + start_page * 8,
233        width: display_frame.width,
234        height: rows_taken,
235    })
236}
237
238impl<'a, S: Screen<'a>> ScreenARGB8888ToMono8BitPage<'a, S> {
239    /// Compute the next sub-op, convert its source bytes into the draw
240    /// buffer, and issue `set_write_frame` on the underlying screen.
241    ///
242    /// Must only be called when `frame_cursor < chunk_end`.
243    fn start_next_sub_op(&self) -> Result<(), ErrorCode> {
244        let cursor = self.frame_cursor.get();
245        let chunk_end = self.chunk_end.get();
246        // Caller guarantees there's more work.
247        let op = next_sub_op(
248            self.display_frame.get(),
249            cursor,
250            chunk_end,
251            self.draw_buffer_capacity,
252        )
253        .ok_or(ErrorCode::FAIL)?;
254        let src_bytes = sub_op_src_bytes(op);
255        let dst_bytes = sub_op_dst_bytes(op);
256        let client_offset = cursor - self.chunk_start.get();
257
258        let mut draw_buffer = self.draw_buffer.take().ok_or(ErrorCode::BUSY)?;
259        let client_buffer = match self.client_buffer.take() {
260            Some(cb) => cb,
261            None => {
262                self.draw_buffer.replace(draw_buffer);
263                return Err(ErrorCode::FAIL);
264            }
265        };
266        draw_buffer.reset();
267        convert_mvlsb_sub_rect(
268            &client_buffer.as_slice()[client_offset..client_offset + src_bytes],
269            &mut draw_buffer.as_mut_slice()[..dst_bytes],
270            op.width,
271        );
272        draw_buffer.slice(..dst_bytes);
273        self.draw_buffer.replace(draw_buffer);
274        self.client_buffer.replace(client_buffer);
275
276        self.current_op.set(Some(op));
277        self.screen.set_write_frame(op.x, op.y, op.width, op.height)
278    }
279
280    /// Hand the pre-converted draw buffer to the underlying screen's
281    /// write. Called from `command_complete` after
282    /// `start_next_sub_op`'s `set_write_frame` finishes.
283    fn write_current_sub_op(&self) -> Result<(), ErrorCode> {
284        let draw_buffer = self.draw_buffer.take().ok_or(ErrorCode::FAIL)?;
285        self.screen.write(draw_buffer, false)
286    }
287
288    /// End of chunk: return the client's buffer and deliver
289    /// `write_complete` to them.
290    fn finish_chunk(&self, result: Result<(), ErrorCode>) {
291        self.current_op.set(None);
292        if let Some(cb) = self.client_buffer.take() {
293            self.client.map(|c| c.write_complete(cb, result));
294        }
295    }
296}
297
298impl<'a, S: Screen<'a>> Screen<'a> for ScreenARGB8888ToMono8BitPage<'a, S> {
299    fn set_client(&self, client: &'a dyn ScreenClient) {
300        self.client.replace(client);
301    }
302
303    fn get_resolution(&self) -> (usize, usize) {
304        self.screen.get_resolution()
305    }
306
307    fn get_pixel_format(&self) -> ScreenPixelFormat {
308        ScreenPixelFormat::Mono_8BitPage
309    }
310
311    fn get_rotation(&self) -> ScreenRotation {
312        self.screen.get_rotation()
313    }
314
315    fn set_write_frame(
316        &self,
317        x: usize,
318        y: usize,
319        width: usize,
320        height: usize,
321    ) -> Result<(), ErrorCode> {
322        if self.current_op.get().is_some() {
323            return Err(ErrorCode::BUSY);
324        }
325        self.display_frame.set(Frame {
326            x,
327            y,
328            width,
329            height,
330        });
331        self.frame_cursor.set(0);
332
333        self.screen.set_write_frame(x, y, width, height)
334    }
335
336    fn write(
337        &self,
338        buffer: SubSliceMut<'static, u8>,
339        continue_write: bool,
340    ) -> Result<(), ErrorCode> {
341        if self.current_op.get().is_some() {
342            return Err(ErrorCode::BUSY);
343        }
344        let frame = self.display_frame.get();
345        if frame.width == 0 || frame.height == 0 {
346            return Err(ErrorCode::INVAL);
347        }
348
349        if !continue_write {
350            self.frame_cursor.set(0);
351        }
352
353        let cursor = self.frame_cursor.get();
354        let total_frame_bytes = frame.width * frame.height.div_ceil(8);
355        if cursor >= total_frame_bytes {
356            return Err(ErrorCode::SIZE);
357        }
358        let chunk_len = core::cmp::min(buffer.len(), total_frame_bytes - cursor);
359        if chunk_len == 0 {
360            return Err(ErrorCode::SIZE);
361        }
362
363        // Truncate the client buffer's active window to the portion we
364        // will actually consume.
365        let mut buffer = buffer;
366        buffer.slice(..chunk_len);
367
368        self.chunk_start.set(cursor);
369        self.chunk_end.set(cursor + chunk_len);
370        assert!(self.client_buffer.replace(buffer).is_none());
371
372        if let Err(e) = self.start_next_sub_op() {
373            self.finish_chunk(Err(e));
374            return Err(e);
375        }
376        Ok(())
377    }
378
379    fn set_brightness(&self, brightness: u16) -> Result<(), ErrorCode> {
380        self.screen.set_brightness(brightness)
381    }
382
383    fn set_power(&self, enabled: bool) -> Result<(), ErrorCode> {
384        self.screen.set_power(enabled)
385    }
386
387    fn set_invert(&self, enabled: bool) -> Result<(), ErrorCode> {
388        self.screen.set_invert(enabled)
389    }
390}
391
392impl<'a, S: Screen<'a>> ScreenClient for ScreenARGB8888ToMono8BitPage<'a, S> {
393    fn screen_is_ready(&self) {
394        self.client.map(|c| c.screen_is_ready());
395    }
396
397    fn command_complete(&self, result: Result<(), ErrorCode>) {
398        if self.current_op.get().is_none() {
399            self.client.map(|c| c.command_complete(result));
400        } else {
401            if result.is_err() {
402                self.finish_chunk(result);
403                return;
404            }
405            if let Err(e) = self.write_current_sub_op() {
406                self.finish_chunk(Err(e));
407            }
408        }
409    }
410
411    fn write_complete(&self, buffer: SubSliceMut<'static, u8>, result: Result<(), ErrorCode>) {
412        self.draw_buffer.replace(buffer);
413
414        if result.is_err() {
415            self.finish_chunk(result);
416            return;
417        }
418
419        // Advance the frame cursor past the sub-op we just finished.
420        let Some(op) = self.current_op.get() else {
421            // Spurious callback; nothing to advance.
422            return;
423        };
424        self.frame_cursor
425            .set(self.frame_cursor.get() + sub_op_src_bytes(op));
426        self.current_op.set(None);
427
428        // More of this chunk to draw? Kick off the next sub-op.
429        // Otherwise finalize the client-visible write.
430        if self.frame_cursor.get() < self.chunk_end.get() {
431            if let Err(e) = self.start_next_sub_op() {
432                self.finish_chunk(Err(e));
433            }
434        } else {
435            self.finish_chunk(Ok(()));
436        }
437    }
438}