breakwater_parser/framebuffer/
simple.rs

1use core::slice;
2
3use tracing::debug;
4
5use super::{FB_BYTES_PER_PIXEL, FrameBuffer};
6
7pub struct SimpleFrameBuffer {
8    width: usize,
9    height: usize,
10    buffer: Vec<u32>,
11}
12
13impl SimpleFrameBuffer {
14    pub fn new(width: usize, height: usize) -> Self {
15        let mut buffer = Vec::with_capacity(width * height);
16        buffer.resize_with(width * height, || 0);
17        Self {
18            width,
19            height,
20            buffer,
21        }
22    }
23}
24
25impl FrameBuffer for SimpleFrameBuffer {
26    #[inline(always)]
27    fn get_width(&self) -> usize {
28        self.width
29    }
30
31    #[inline(always)]
32    fn get_height(&self) -> usize {
33        self.height
34    }
35
36    #[inline(always)]
37    unsafe fn get_unchecked(&self, x: usize, y: usize) -> u32 {
38        unsafe { *self.buffer.get_unchecked(x + y * self.width) }
39    }
40
41    #[inline(always)]
42    fn set(&self, x: usize, y: usize, rgba: u32) {
43        // https://github.com/sbernauer/breakwater/pull/11
44        // If we make the FrameBuffer large enough (e.g. 10_000 x 10_000) we don't need to check the bounds here
45        // (x and y are max 4 digit numbers). Flamegraph has shown 5.21% of runtime in this bound check. On the other
46        // hand this can increase the framebuffer size dramatically and lowers the cash locality.
47        // In the end we did *not* go with this change.
48        if x < self.width && y < self.height {
49            unsafe {
50                let ptr = self.buffer.as_ptr().add(x + y * self.width) as *mut u32;
51                *ptr = rgba;
52            }
53        }
54    }
55
56    #[inline(always)]
57    fn set_multi_from_start_index(&self, starting_index: usize, pixels: &[u8]) -> usize {
58        let num_pixels = pixels.len() / FB_BYTES_PER_PIXEL;
59
60        if starting_index + num_pixels > self.buffer.len() {
61            debug!(
62                starting_index,
63                num_pixels,
64                buffer_len = self.buffer.len(),
65                "Ignoring invalid set_multi call, which would exceed the screen",
66            );
67            // We did not move
68            return 0;
69        }
70
71        let starting_ptr = unsafe { self.buffer.as_ptr().add(starting_index) };
72        let target_slice =
73            unsafe { slice::from_raw_parts_mut(starting_ptr as *mut u8, pixels.len()) };
74        target_slice.copy_from_slice(pixels);
75
76        num_pixels
77    }
78
79    #[inline(always)]
80    fn as_bytes(&self) -> &[u8] {
81        let len = self.buffer.len() * FB_BYTES_PER_PIXEL;
82        let ptr = self.buffer.as_ptr() as *const u8;
83        unsafe { std::slice::from_raw_parts(ptr, len) }
84    }
85}
86
87#[cfg(test)]
88mod tests {
89    use rstest::{fixture, rstest};
90
91    use super::*;
92
93    #[fixture]
94    fn fb() -> SimpleFrameBuffer {
95        // We keep the framebuffer so small, so that we can easily test all pixels in a test run
96        SimpleFrameBuffer::new(640, 480)
97    }
98
99    #[rstest]
100    #[case(0, 0, 0)]
101    #[case(0, 0, 0xff0000)]
102    #[case(0, 0, 0x0000ff)]
103    #[case(0, 0, 0x12345678)]
104    pub fn test_roundtrip(
105        fb: SimpleFrameBuffer,
106        #[case] x: usize,
107        #[case] y: usize,
108        #[case] rgba: u32,
109    ) {
110        fb.set(x, y, rgba);
111        assert_eq!(fb.get(x, y), Some(rgba));
112    }
113
114    #[rstest]
115    pub fn test_out_of_bounds(fb: SimpleFrameBuffer) {
116        assert_eq!(fb.get(usize::MAX, usize::MAX), None);
117        assert_eq!(fb.get(usize::MAX, usize::MAX), None);
118    }
119
120    #[rstest]
121    pub fn test_set_multi_from_beginning(fb: SimpleFrameBuffer) {
122        let pixels = (0..10_u32).collect::<Vec<_>>();
123        let pixel_bytes: Vec<u8> = pixels.iter().flat_map(|p| p.to_le_bytes()).collect();
124
125        let (current_x, current_y) = fb.set_multi(0, 0, &pixel_bytes);
126
127        assert_eq!(current_x, 10);
128        assert_eq!(current_y, 0);
129
130        for x in 0..10 {
131            assert_eq!(fb.get(x as usize, 0), Some(x), "Checking pixel {x}");
132        }
133
134        // The next pixel must not have been colored
135        assert_eq!(fb.get(11, 0), Some(0));
136    }
137
138    #[rstest]
139    pub fn test_set_multi_in_the_middle(fb: SimpleFrameBuffer) {
140        let mut x = 10;
141        let mut y = 100;
142
143        // Let's color exactly 3 lines and 42 pixels
144        let pixels = (0..3 * fb.width as u32 + 42).collect::<Vec<_>>();
145        let pixel_bytes: Vec<u8> = pixels.iter().flat_map(|p| p.to_le_bytes()).collect();
146        let (current_x, current_y) = fb.set_multi(x, y, &pixel_bytes);
147
148        assert_eq!(current_x, 52);
149        assert_eq!(current_y, 103);
150
151        // Let's check everything has been colored
152        for rgba in 0..3 * fb.width as u32 + 42 {
153            assert_eq!(fb.get(x, y), Some(rgba));
154
155            x += 1;
156            if x >= fb.width {
157                x = 0;
158                y += 1;
159            }
160        }
161
162        // Everything afterwards must have not been touched (let's check the next 10 lines)
163        for _ in 0..10 * fb.width as u32 {
164            assert_eq!(fb.get(x, y), Some(0));
165
166            x += 1;
167            if x >= fb.width {
168                x = 0;
169                y += 1;
170            }
171        }
172    }
173
174    #[rstest]
175    pub fn test_set_multi_does_nothing_when_too_long(fb: SimpleFrameBuffer) {
176        let mut too_long = Vec::with_capacity(fb.width * fb.height * FB_BYTES_PER_PIXEL);
177        too_long.fill_with(|| 42_u8);
178        let (current_x, current_y) = fb.set_multi(1, 0, &too_long);
179
180        // Should be unchanged
181        assert_eq!(current_x, 1);
182        assert_eq!(current_y, 0);
183
184        for x in 0..fb.width {
185            for y in 0..fb.height {
186                assert_eq!(fb.get(x, y), Some(0));
187            }
188        }
189    }
190}