Skip to main content

endbasic_std/gfx/lcd/buffered/
mod.rs

1// EndBASIC
2// Copyright 2024 Julio Merino
3//
4// Licensed under the Apache License, Version 2.0 (the "License"); you may not
5// use this file except in compliance with the License.  You may obtain a copy
6// of the License at:
7//
8//     http://www.apache.org/licenses/LICENSE-2.0
9//
10// Unless required by applicable law or agreed to in writing, software
11// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
12// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.  See the
13// License for the specific language governing permissions and limitations
14// under the License.
15
16//! Buffered implementation of the `RasterOps` for a hardware LCD.
17
18use crate::console::drawing;
19use crate::console::graphics::{RasterInfo, RasterOps};
20use crate::console::{CharsXY, PixelsXY, RGB, SizeInPixels};
21use crate::gfx::lcd::fonts::Font;
22use crate::gfx::lcd::{AsByteSlice, Lcd, LcdSize, LcdXY, to_xy_size};
23use std::convert::TryFrom;
24use std::io;
25
26#[cfg(test)]
27mod tests;
28#[cfg(test)]
29mod testutils;
30
31/// Implements buffering for a backing slow LCD `L` that renders text with the font `F`.
32///
33/// All drawing operations are saved to a memory-backed framebuffer.  If syncing is enabled, drawing
34/// primitives are flushed right away to the device; otherwise, they are applied to memory only
35/// until an explicit sync is requested.  The framebuffer is also used to implement all pixel data
36/// reading.
37pub struct BufferedLcd<L: Lcd> {
38    lcd: L,
39    font: &'static Font,
40
41    fb: Vec<u8>,
42    stride: usize,
43    sync: bool,
44    damage: Option<(LcdXY, LcdXY)>,
45
46    size_pixels: LcdSize,
47    size_chars: CharsXY,
48
49    draw_color: L::Pixel,
50    row_buffer: Vec<u8>,
51}
52
53impl<L> BufferedLcd<L>
54where
55    L: Lcd,
56{
57    /// Creates a new buffered LCD backed by `lcd`.
58    pub fn new(lcd: L, font: &'static Font) -> Self {
59        let (size, stride) = lcd.info();
60
61        let fb = {
62            let pixels = size.width * size.height;
63            vec![0; pixels * stride]
64        };
65
66        let size_chars = CharsXY::new(
67            u16::try_from(size.width / font.glyph_size.width).expect("Must fit"),
68            u16::try_from(size.height / font.glyph_size.height).expect("Must fit"),
69        );
70
71        let draw_color = lcd.encode((255, 255, 255));
72        let row_buffer = Vec::with_capacity(size.width * stride);
73
74        Self {
75            lcd,
76            font,
77            fb,
78            stride,
79            sync: true,
80            damage: None,
81            size_pixels: size,
82            size_chars,
83            draw_color,
84            row_buffer,
85        }
86    }
87
88    /// Executes mutations on the buffered LCD via `ops` while ensuring that syncing is disabled.
89    fn without_sync<O>(&mut self, ops: O) -> io::Result<()>
90    where
91        O: Fn(&mut BufferedLcd<L>) -> io::Result<()>,
92    {
93        if self.sync {
94            let old_sync = self.sync;
95            self.sync = false;
96
97            let result = ops(self);
98
99            self.sync = old_sync;
100            if self.sync {
101                self.force_present_canvas()?;
102            }
103
104            result
105        } else {
106            ops(self)
107        }
108    }
109
110    /// Clips the user-supplied `xy` coordinates to the LCD space.  Returns `None` if they are out
111    /// of range and the converted value otherwise.
112    fn clip_xy(&self, xy: PixelsXY) -> Option<LcdXY> {
113        fn clamp(value: i16, max: usize) -> Option<usize> {
114            if value < 0 {
115                None
116            } else {
117                let value = usize::try_from(value).expect("Positive value must fit");
118                if value > max { None } else { Some(value) }
119            }
120        }
121
122        let x = clamp(xy.x, self.size_pixels.width - 1);
123        let y = clamp(xy.y, self.size_pixels.height - 1);
124        match (x, y) {
125            (Some(x), Some(y)) => Some(LcdXY { x, y }),
126            _ => None,
127        }
128    }
129
130    /// Clamps the user-supplied `xy` coordinates to the LCD space.
131    fn clamp_xy(&self, xy: PixelsXY) -> LcdXY {
132        fn clamp(value: i16, max: usize) -> usize {
133            if value < 0 {
134                0
135            } else {
136                let value = usize::try_from(value).expect("Positive value must fit");
137                if value > max { max } else { value }
138            }
139        }
140
141        LcdXY {
142            x: clamp(xy.x, self.size_pixels.width - 1),
143            y: clamp(xy.y, self.size_pixels.height - 1),
144        }
145    }
146
147    /// Given a top-left `xy` coordinate, adds the user-supplied `size` to it and clamps the result
148    /// to the LCD space.
149    fn clip_x2y2(&self, xy: PixelsXY, size: SizeInPixels) -> Option<LcdXY> {
150        fn clamp(value: i16, delta: u16, max: usize) -> Option<usize> {
151            let value = i32::from(value);
152            let delta = i32::from(delta);
153
154            let value = value + delta;
155            if value < 0 {
156                None
157            } else {
158                let value = usize::try_from(value).expect("Positive value must fit");
159                if value > max { Some(max) } else { Some(value) }
160            }
161        }
162
163        let x = clamp(xy.x, size.width - 1, self.size_pixels.width - 1);
164        let y = clamp(xy.y, size.height - 1, self.size_pixels.height - 1);
165        match (x, y) {
166            (Some(x), Some(y)) => Some(LcdXY { x, y }),
167            _ => None,
168        }
169    }
170
171    /// Make sure that the coordinates are within the LCD space.
172    ///
173    /// This is only used to validate input parameters for those functions that are internal to the
174    /// console (such as `move_pixels`).  Functions subject to user input (such as `draw_rect`) must
175    /// not use this.
176    fn assert_xy_in_range(&mut self, xy: PixelsXY) {
177        if cfg!(test) {
178            let x = usize::try_from(xy.x).expect("x must be positive and must fit");
179            let y = usize::try_from(xy.y).expect("y must be positive and must fit");
180            debug_assert!(x < self.size_pixels.width, "x must be within the LCD width");
181            debug_assert!(y < self.size_pixels.height, "y must be within the LCD height");
182        }
183    }
184
185    /// Make sure that the coordinates and size are within the LCD space.
186    ///
187    /// This is only used to validate input parameters for those functions that are internal to the
188    /// console (such as `move_pixels`).  Functions subject to user input (such as `draw_rect`) must
189    /// not use this.
190    fn assert_xy_size_in_range(&mut self, xy: PixelsXY, size: SizeInPixels) {
191        if cfg!(test) {
192            self.assert_xy_in_range(xy);
193            let x = xy.x as usize;
194            let y = xy.y as usize;
195
196            let width = usize::from(size.width);
197            let height = usize::from(size.height);
198
199            debug_assert!(
200                x + width - 1 < self.size_pixels.width,
201                "x + width must be within the LCD width"
202            );
203            debug_assert!(
204                y + height - 1 < self.size_pixels.height,
205                "y + height must be within the LCD height"
206            );
207        }
208    }
209
210    /// Gets the start address of the pixel `x`/`y` in the framebuffer.
211    fn fb_addr(&self, x: usize, y: usize) -> usize {
212        debug_assert!(x < self.size_pixels.width);
213        debug_assert!(y < self.size_pixels.height);
214        ((y * self.size_pixels.width) + x) * self.stride
215    }
216
217    /// Extends the current damage area to include the area between between `x1y1` and `x2y2`
218    /// (inclusive) as damaged.
219    ///
220    /// This only makes sense when syncing is disabled, as the damage area represents the contents
221    /// that need to be flushed to the LCD once syncing is enabled again.
222    fn damage(&mut self, x1y1: LcdXY, x2y2: LcdXY) {
223        debug_assert!(!self.sync);
224        debug_assert!(x2y2.x >= x1y1.x);
225        debug_assert!(x2y2.y >= x1y1.y);
226
227        if self.damage.is_none() {
228            self.damage = Some((x1y1, x2y2));
229            return;
230        }
231        let mut damage = self.damage.unwrap();
232
233        if damage.0.x > x1y1.x {
234            damage.0.x = x1y1.x;
235        }
236        if damage.0.y > x1y1.y {
237            damage.0.y = x1y1.y;
238        }
239
240        if damage.1.x < x2y2.x {
241            damage.1.x = x2y2.x;
242        }
243        if damage.1.y < x2y2.y {
244            damage.1.y = x2y2.y;
245        }
246
247        self.damage = Some(damage);
248    }
249
250    /// Fills the area contained between `x1y1` and `x2y2` (inclusive) with the current drawing
251    /// color.
252    ///
253    /// If syncing is enabled, this writes directly to the LCD.  Otherwise, this writes to the
254    /// framebuffer and records the area as damaged.
255    fn fill(&mut self, x1y1: LcdXY, x2y2: LcdXY) -> io::Result<()> {
256        // Prepare self.row_buffer with the content of every row to be copied to the framebuffer.
257        // We do this for efficiency reasons because manipulating individual pixels is costly.
258        let rowlen = {
259            let xlen = x2y2.x - x1y1.x + 1;
260            let rowlen = xlen * self.stride;
261            self.row_buffer.clear();
262            let color = self.draw_color.as_slice();
263            for _ in 0..xlen {
264                self.row_buffer.extend_from_slice(color);
265            }
266            debug_assert_eq!(rowlen, self.row_buffer.len());
267            rowlen
268        };
269
270        if self.sync {
271            let mut data = LcdSize::between(x1y1, x2y2).new_buffer(self.stride);
272            for y in x1y1.y..(x2y2.y + 1) {
273                let offset = self.fb_addr(x1y1.x, y);
274                self.fb[offset..offset + rowlen].copy_from_slice(&self.row_buffer);
275                data.extend(&self.row_buffer);
276            }
277            self.lcd.set_data(x1y1, x2y2, &data)?;
278        } else {
279            for y in x1y1.y..(x2y2.y + 1) {
280                let offset = self.fb_addr(x1y1.x, y);
281                self.fb[offset..offset + rowlen].copy_from_slice(&self.row_buffer);
282            }
283            self.damage(x1y1, x2y2);
284        }
285
286        Ok(())
287    }
288
289    /// Flushes any pending damaged area to the LCD.
290    fn force_present_canvas(&mut self) -> io::Result<()> {
291        let (x1y1, x2y2) = match self.damage {
292            None => return Ok(()),
293            Some(damage) => damage,
294        };
295
296        let mut data = LcdSize::between(x1y1, x2y2).new_buffer(self.stride);
297        for y in x1y1.y..(x2y2.y + 1) {
298            for x in x1y1.x..(x2y2.x + 1) {
299                let offset = self.fb_addr(x, y);
300                data.extend_from_slice(&self.fb[offset..offset + self.stride]);
301            }
302        }
303        debug_assert_eq!(
304            {
305                let (_xy, size) = to_xy_size(x1y1, x2y2);
306                size.width * size.height * self.stride
307            },
308            data.len()
309        );
310
311        self.lcd.set_data(x1y1, x2y2, &data)?;
312
313        self.damage = None;
314
315        Ok(())
316    }
317
318    /// Writes a single character `ch` at `pos`.
319    fn write_char(&mut self, pos: LcdXY, ch: char) -> io::Result<()> {
320        let glyph = self.font.glyph(ch);
321        for j in 0..self.font.glyph_size.height {
322            for k in 0..self.font.stride {
323                let row = glyph[j * self.font.stride + k];
324                let mut mask = 0x80;
325                for i in 0..self.font.glyph_size.width {
326                    let bit = row & mask;
327                    if bit != 0 {
328                        let x = pos.x + i + k * 8;
329                        if x >= self.size_pixels.width {
330                            continue;
331                        }
332
333                        let y = pos.y + j;
334                        if y >= self.size_pixels.height {
335                            continue;
336                        }
337
338                        let xy = LcdXY { x, y };
339                        // TODO(jmmv): This is very inefficent on a pixel basis.
340                        self.fill(xy, xy)?;
341                    }
342                    mask >>= 1;
343                }
344            }
345        }
346        Ok(())
347    }
348}
349
350impl<L> Drop for BufferedLcd<L>
351where
352    L: Lcd,
353{
354    fn drop(&mut self) {
355        self.set_draw_color((0, 0, 0));
356        self.clear().unwrap();
357    }
358}
359
360impl<L> RasterOps for BufferedLcd<L>
361where
362    L: Lcd,
363{
364    type ID = (Vec<u8>, SizeInPixels);
365
366    fn get_info(&self) -> RasterInfo {
367        RasterInfo {
368            size_pixels: self.size_pixels.into(),
369            glyph_size: self.font.glyph_size.into(),
370            size_chars: self.size_chars,
371        }
372    }
373
374    fn set_draw_color(&mut self, color: RGB) {
375        self.draw_color = self.lcd.encode(color);
376    }
377
378    fn clear(&mut self) -> io::Result<()> {
379        self.fill(
380            LcdXY { x: 0, y: 0 },
381            LcdXY { x: self.size_pixels.width - 1, y: self.size_pixels.height - 1 },
382        )
383    }
384
385    fn set_sync(&mut self, enabled: bool) {
386        self.sync = enabled;
387    }
388
389    fn present_canvas(&mut self) -> io::Result<()> {
390        if self.sync { Ok(()) } else { self.force_present_canvas() }
391    }
392
393    fn read_pixels(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<Self::ID> {
394        self.assert_xy_size_in_range(xy, size);
395        let x1y1 = self.clip_xy(xy).expect("Internal ops must receive valid coordinates");
396        let x2y2 = self.clip_x2y2(xy, size).expect("Internal ops must receive valid coordinates");
397
398        let mut pixels = LcdSize::between(x1y1, x2y2).new_buffer(self.stride);
399
400        for y in x1y1.y..(x2y2.y + 1) {
401            for x in x1y1.x..(x2y2.x + 1) {
402                let offset = self.fb_addr(x, y);
403                pixels.extend_from_slice(&self.fb[offset..offset + self.stride]);
404            }
405        }
406
407        debug_assert_eq!(
408            usize::from(size.width) * usize::from(size.height) * self.stride,
409            pixels.len()
410        );
411        Ok((pixels, size))
412    }
413
414    fn put_pixels(&mut self, xy: PixelsXY, (pixels, size): &Self::ID) -> io::Result<()> {
415        debug_assert_eq!(
416            usize::from(size.width) * usize::from(size.height) * self.stride,
417            pixels.len()
418        );
419
420        self.assert_xy_in_range(xy);
421        let x1y1 = self.clip_xy(xy).expect("Internal ops must receive valid coordinates");
422        let x2y2 = self.clip_x2y2(xy, *size).expect("Internal ops must receive valid coordinates");
423
424        let mut p = 0;
425        for y in x1y1.y..(x2y2.y + 1) {
426            for x in x1y1.x..(x2y2.x + 1) {
427                let offset = self.fb_addr(x, y);
428                self.fb[offset..(offset + self.stride)]
429                    .copy_from_slice(&pixels[p..(p + self.stride)]);
430                p += self.stride;
431            }
432        }
433
434        if self.sync {
435            self.lcd.set_data(x1y1, x2y2, pixels)?;
436        } else {
437            self.damage(x1y1, x2y2);
438        }
439
440        Ok(())
441    }
442
443    fn move_pixels(
444        &mut self,
445        x1y1: PixelsXY,
446        x2y2: PixelsXY,
447        size: SizeInPixels,
448    ) -> io::Result<()> {
449        self.assert_xy_size_in_range(x1y1, size);
450        self.assert_xy_size_in_range(x2y2, size);
451
452        let data = self.read_pixels(x1y1, size)?;
453
454        self.without_sync(|self2| {
455            self2.draw_rect_filled(x1y1, size)?;
456            self2.put_pixels(x2y2, &data)
457        })?;
458
459        Ok(())
460    }
461
462    fn write_text(&mut self, xy: PixelsXY, text: &str) -> io::Result<()> {
463        self.assert_xy_in_range(xy);
464
465        let x1y1 = self.clip_xy(xy).expect("Internal ops must receive valid coordinates");
466
467        self.without_sync(|self2| {
468            let mut pos = x1y1;
469            for ch in text.chars() {
470                self2.write_char(pos, ch)?;
471                pos.x += self2.font.glyph_size.width;
472            }
473            Ok(())
474        })
475    }
476
477    fn draw_circle(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
478        self.without_sync(|self2| drawing::draw_circle(self2, center, radius))
479    }
480
481    fn draw_circle_filled(&mut self, center: PixelsXY, radius: u16) -> io::Result<()> {
482        self.without_sync(|self2| drawing::draw_circle_filled(self2, center, radius))
483    }
484
485    fn draw_line(&mut self, x1y1: PixelsXY, x2y2: PixelsXY) -> io::Result<()> {
486        self.without_sync(|self2| drawing::draw_line(self2, x1y1, x2y2))
487    }
488
489    fn draw_pixel(&mut self, xy: PixelsXY) -> io::Result<()> {
490        let xy = self.clip_xy(xy);
491        match xy {
492            Some(xy) => self.fill(xy, xy),
493            None => Ok(()),
494        }
495    }
496
497    fn draw_rect(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<()> {
498        self.without_sync(|self2| drawing::draw_rect(self2, xy, size))
499    }
500
501    fn draw_rect_filled(&mut self, xy: PixelsXY, size: SizeInPixels) -> io::Result<()> {
502        let x1y1 = self.clamp_xy(xy);
503        let x2y2 = self.clip_x2y2(xy, size);
504        match x2y2 {
505            Some(x2y2) => self.fill(x1y1, x2y2),
506            _ => Ok(()),
507        }
508    }
509}