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