1use core::convert::Infallible;
32
33use embedded_graphics::pixelcolor::RgbColor;
34use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
35
36use super::{make_data_template, map_index, Address, Entry, ADDR_TABLE, INTER_ROW_BLANK, OE_BLANK};
37use crate::Color;
38use crate::{map_row_index, slot_addresses};
39use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
40use crate::{FrameBufferOperations, MutableFrameBuffer};
41
42#[derive(Clone, Copy, PartialEq, Debug)]
43#[repr(C)]
44pub struct Row<const COLS: usize> {
49 pub(crate) data: [Entry; COLS],
50 pub(crate) address: [Address; 4],
51 pub(crate) gap: [Entry; INTER_ROW_BLANK],
52}
53
54impl<const COLS: usize> Row<COLS> {
55 #[must_use]
60 pub const fn new() -> Self {
61 Self {
62 data: [Entry::new(); COLS],
63 address: [Address::new(); 4],
64 gap: [Entry::new(); INTER_ROW_BLANK],
65 }
66 }
67
68 #[inline]
77 pub const fn format(&mut self, addr: u8) {
78 assert!((addr as usize) < ADDR_TABLE.len());
79 let src_addr = ADDR_TABLE[addr as usize];
80 self.address[0] = src_addr[0];
81 self.address[1] = src_addr[1];
82 self.address[2] = src_addr[2];
83 self.address[3] = src_addr[3];
84
85 let data_template = make_data_template::<COLS>();
86 let mut i = 0;
87 while i < COLS {
88 self.data[i] = data_template[i];
89 i += 1;
90 }
91
92 let mut i = 0;
93 #[allow(clippy::absurd_extreme_comparisons)]
95 while i < INTER_ROW_BLANK {
96 self.gap[i].0 = OE_BLANK;
97 i += 1;
98 }
99 }
100}
101
102impl<const COLS: usize> Default for Row<COLS> {
103 fn default() -> Self {
104 Self::new()
105 }
106}
107
108const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
113 let covered = planes - plane_idx;
114 let reps = if plane_idx == 0 {
115 1
116 } else {
117 1 << (plane_idx - 1)
118 };
119 (covered, reps)
120}
121
122const fn bcm_sequence<const NROWS: usize, const COLS: usize, const PLANES: usize>(
126) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
127 assert!(PLANES <= BCM_SEQUENCE_CAPACITY);
128 let plane_bytes = NROWS * core::mem::size_of::<Row<COLS>>();
129 let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
130 let mut plane = 0usize;
131 while plane < PLANES {
132 let (covered, reps) = plane_suffix(plane, PLANES);
133 seq[plane] = BcmLenReps {
134 len: plane_bytes * covered,
135 reps,
136 };
137 plane += 1;
138 }
139 seq
140}
141
142#[derive(Copy, Clone)]
144#[repr(C, align(4))]
145pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
146 pub(crate) planes: [[Row<COLS>; NROWS]; PLANES],
147}
148
149impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
150 DmaFrameBuffer<NROWS, COLS, PLANES>
151{
152 #[must_use]
172 pub const fn new() -> Self {
173 assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
174 assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
175 #[cfg(feature = "esp32-ordering")]
176 assert!(
177 COLS % 4 == 0,
178 "esp32-ordering feature requires COLS to be divisible by 4"
179 );
180 let mut instance = Self {
181 planes: [[Row::new(); NROWS]; PLANES],
182 };
183 instance.format();
184 instance
185 }
186
187 #[must_use]
189 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
190 pub const fn bcm_chunk_count() -> usize {
191 PLANES
192 }
193
194 #[must_use]
196 #[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
197 pub const fn bcm_chunk_bytes() -> usize {
198 NROWS * core::mem::size_of::<Row<COLS>>()
199 }
200
201 #[must_use]
203 pub const fn bcm_row_count() -> usize {
204 NROWS
205 }
206
207 #[inline]
209 pub const fn format(&mut self) {
210 let mut p = 0;
211 while p < PLANES {
212 let mut slot = 0;
213 while slot < NROWS {
214 let (_, addr) = slot_addresses::<NROWS>(slot);
215 self.planes[p][slot].format(addr);
216 slot += 1;
217 }
218 p += 1;
219 }
220 }
221
222 #[inline]
224 pub fn erase(&mut self) {
225 const MASK: u8 = !crate::pinmap::LATCHED_COLOR_MASK;
226 for plane in &mut self.planes {
227 for row in plane {
228 for entry in &mut row.data {
229 entry.0 &= MASK;
230 }
231 }
232 }
233 }
234
235 #[inline]
237 pub fn set_pixel(&mut self, p: Point, color: Color) {
238 if p.x < 0 || p.y < 0 {
239 return;
240 }
241 self.set_pixel_internal(p.x as usize, p.y as usize, color);
242 }
243
244 #[inline]
245 fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
246 if x >= COLS || y >= NROWS * 2 {
247 return;
248 }
249
250 #[cfg(feature = "skip-black-pixels")]
253 if color == Color::BLACK {
254 return;
255 }
256
257 let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
258 let is_top = y < NROWS;
259 let red = color.r();
260 let green = color.g();
261 let blue = color.b();
262
263 for plane_idx in 0..PLANES {
264 let bit = (8 - PLANES + plane_idx) as u32;
265 let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
266 | ((u8::from(((green >> bit) & 1) != 0)) << 1)
267 | u8::from(((red >> bit) & 1) != 0);
268 let col_idx = map_index(x);
269 let entry = &mut self.planes[plane_idx][row_idx].data[col_idx];
270 if is_top {
271 entry.set_color0_bits(bits);
272 } else {
273 entry.set_color1_bits(bits);
274 }
275 }
276 }
277}
278
279impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
280 for DmaFrameBuffer<NROWS, COLS, PLANES>
281{
282 fn default() -> Self {
283 Self::new()
284 }
285}
286
287impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
288 for DmaFrameBuffer<NROWS, COLS, PLANES>
289{
290 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
291 f.debug_struct("DmaFrameBuffer")
292 .field("size", &core::mem::size_of_val(&self.planes))
293 .field("plane_count", &self.planes.len())
294 .field("plane_size", &core::mem::size_of_val(&self.planes[0]))
295 .finish()
296 }
297}
298
299#[cfg(feature = "defmt")]
300impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
301 for DmaFrameBuffer<NROWS, COLS, PLANES>
302{
303 fn format(&self, f: defmt::Formatter) {
304 defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
305 defmt::write!(f, " size: {}", core::mem::size_of_val(&self.planes));
306 defmt::write!(
307 f,
308 " plane_size: {}",
309 core::mem::size_of_val(&self.planes[0])
310 );
311 }
312}
313
314impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
315 for DmaFrameBuffer<NROWS, COLS, PLANES>
316{
317 type Word = u8;
318
319 const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<NROWS, COLS, PLANES>();
320
321 const BCM_SEQUENCE_LEN: usize = PLANES;
322
323 const BCM_SEQUENCE_COUNT: usize = 1;
324
325 fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
326 assert!(
327 index < PLANES,
328 "segment index {index} out of range for {PLANES} planes"
329 );
330 self.planes[index].as_ptr().cast::<u8>()
331 }
332}
333
334impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
335 for DmaFrameBuffer<NROWS, COLS, PLANES>
336{
337 #[inline]
338 fn erase(&mut self) {
339 DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
340 }
341
342 #[inline]
343 fn set_pixel(&mut self, p: Point, color: Color) {
344 DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
345 }
346}
347
348impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
349 for DmaFrameBuffer<NROWS, COLS, PLANES>
350{
351}
352
353impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
354 for DmaFrameBuffer<NROWS, COLS, PLANES>
355{
356 fn size(&self) -> Size {
357 Size::new(COLS as u32, (NROWS * 2) as u32)
358 }
359}
360
361impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
362 for DmaFrameBuffer<NROWS, COLS, PLANES>
363{
364 type Color = Color;
365 type Error = Infallible;
366
367 fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
368 where
369 I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
370 {
371 for pixel in pixels {
372 if pixel.0.x < 0 || pixel.0.y < 0 {
373 continue;
374 }
375 self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
376 }
377 Ok(())
378 }
379}
380
381#[cfg(test)]
382mod tests {
383 extern crate std;
384
385 use super::super::{make_addr_table, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
386 use super::*;
387 use embedded_graphics::prelude::*;
388 use std::format;
389
390 const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
391 128
392 } else {
393 64
394 };
395 type TestBuffer = DmaFrameBuffer<16, TEST_COLS, 8>;
396
397 #[test]
398 fn row_format_sets_address_and_control_bits() {
399 const TEST_N: usize = TEST_COLS;
400 let mut row = Row::<TEST_N>::new();
401 row.format(5);
402 let latch_count = row.address.iter().filter(|a| a.latch()).count();
403 assert_eq!(latch_count, 2);
404 assert_eq!(row.address[map_index(0)].addr(), 5);
405 assert_eq!(row.address[map_index(1)].addr(), 5);
406 assert_eq!(row.address[map_index(2)].addr(), 5);
407 assert_eq!(row.address[map_index(3)].0, OE_BLANK);
408 let oe_active = !cfg!(feature = "invert-oe");
409 let active_count = TEST_N.saturating_sub(LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 1);
410 let blank_count = TEST_N - active_count;
411 let oe_blank_count = row
412 .data
413 .iter()
414 .filter(|entry| entry.output_enable() != oe_active)
415 .count();
416 assert_eq!(oe_blank_count, blank_count);
417 }
418
419 #[test]
420 fn format_sets_expected_row_addresses_for_all_rows() {
421 let mut fb = TestBuffer::new();
422 fb.format();
423
424 for plane_idx in 0..8 {
425 for slot in 0..16 {
426 let (_, addr) = slot_addresses::<16>(slot);
427 let row = &fb.planes[plane_idx][slot];
428 assert_eq!(row.address[map_index(0)].addr(), addr);
429 assert_eq!(row.address[map_index(1)].addr(), addr);
430 assert_eq!(row.address[map_index(2)].addr(), addr);
431 assert_eq!(row.address[map_index(3)].0, OE_BLANK);
432 }
433 }
434 }
435
436 #[test]
437 fn set_pixel_maps_top_half_bits_per_plane() {
438 let mut fb = TestBuffer::new();
439 let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
440 fb.set_pixel(Point::new(2, 3), color);
441
442 for plane_idx in 0..8 {
443 let bit = plane_idx;
444 let entry = fb.planes[plane_idx][map_row_index::<16>(3)].data[map_index(2)];
445 assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
446 assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
447 assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
448 }
449 }
450
451 #[test]
452 fn set_pixel_maps_bottom_half_bits_per_plane() {
453 let mut fb = TestBuffer::new();
454 let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
455 fb.set_pixel(Point::new(4, 20), color);
456
457 for plane_idx in 0..8 {
458 let bit = plane_idx;
459 let entry = fb.planes[plane_idx][map_row_index::<16>(4)].data[map_index(4)];
460 assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
461 assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
462 assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
463 }
464 }
465
466 #[test]
467 #[cfg(feature = "reverse-row-order")]
468 fn reverse_row_order_stores_rows_back_to_front() {
469 let mut fb = TestBuffer::new();
470
471 assert_eq!(fb.planes[0][0].address[map_index(0)].addr(), 15);
475 assert_eq!(fb.planes[0][15].address[map_index(0)].addr(), 0);
476
477 fb.set_pixel(Point::new(2, 0), Color::RED);
479 let col2 = map_index(2);
480 assert!(fb.planes[7][15].data[col2].red1());
481 assert!(!fb.planes[7][0].data[col2].red1());
482 }
483
484 #[test]
485 fn erase_clears_only_color_bits() {
486 let mut fb = TestBuffer::new();
487 let oe_before = fb.planes[0][map_row_index::<16>(0)].data[0].output_enable();
488 fb.set_pixel(Point::new(0, 0), Color::WHITE);
489 fb.erase();
490
491 for plane in &fb.planes {
492 for row in plane {
493 for entry in &row.data {
494 assert!(!entry.red1());
495 assert!(!entry.grn1());
496 assert!(!entry.blu1());
497 assert!(!entry.red2());
498 assert!(!entry.grn2());
499 assert!(!entry.blu2());
500 }
501 }
502 }
503
504 assert_eq!(
505 fb.planes[0][map_row_index::<16>(0)].data[0].output_enable(),
506 oe_before
507 );
508 }
509
510 #[test]
511 fn draw_target_iter_sets_pixels() {
512 let mut fb = TestBuffer::new();
513 let pixels = [Pixel(Point::new(1, 1), Color::RED)];
514 let result = fb.draw_iter(pixels);
515 assert!(result.is_ok());
516
517 for plane_idx in 0..8 {
518 let bit = plane_idx;
519 let entry = fb.planes[plane_idx][map_row_index::<16>(1)].data[map_index(1)];
520 assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
521 assert!(!entry.grn1());
522 assert!(!entry.blu1());
523 }
524 }
525
526 #[test]
527 fn set_pixel_ignores_out_of_bounds_and_negative() {
528 let mut fb = TestBuffer::new();
529 let before = fb.planes;
530 fb.set_pixel(Point::new(-1, 0), Color::WHITE);
531 fb.set_pixel(Point::new(0, -1), Color::WHITE);
532 fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
533 fb.set_pixel(Point::new(0, 32), Color::WHITE);
534 assert_eq!(fb.planes, before);
535 }
536
537 #[test]
538 #[cfg(feature = "skip-black-pixels")]
539 fn test_skip_black_pixels_enabled() {
540 let mut fb = TestBuffer::new();
541
542 fb.set_pixel_internal(10, 5, Color::RED);
544
545 let mapped_col_10 = map_index(10);
547 assert!(fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].red1());
548 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].grn1());
549 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].blu1());
550
551 fb.set_pixel_internal(10, 5, Color::BLACK);
553
554 assert!(fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].red1());
556 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].grn1());
557 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].blu1());
558 }
559
560 #[test]
561 #[cfg(not(feature = "skip-black-pixels"))]
562 fn test_skip_black_pixels_disabled() {
563 let mut fb = TestBuffer::new();
564
565 fb.set_pixel_internal(10, 5, Color::RED);
567
568 let mapped_col_10 = map_index(10);
570 assert!(fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].red1());
571 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].grn1());
572 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].blu1());
573
574 fb.set_pixel_internal(10, 5, Color::BLACK);
576
577 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].red1());
579 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].grn1());
580 assert!(!fb.planes[0][map_row_index::<16>(5)].data[mapped_col_10].blu1());
581 }
582
583 #[test]
584 #[should_panic(expected = "NROWS must be within 1..=32")]
585 fn new_panics_for_too_many_rows() {
586 let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
587 }
588
589 #[test]
590 #[should_panic(expected = "PLANES must be within 1..=8")]
591 fn new_panics_for_too_many_planes() {
592 let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
593 }
594
595 #[test]
596 #[allow(deprecated)]
597 fn bcm_chunk_info_for_common_panel() {
598 assert_eq!(TestBuffer::bcm_chunk_count(), 8);
599 assert_eq!(
600 TestBuffer::bcm_chunk_bytes(),
601 16 * core::mem::size_of::<Row<TEST_COLS>>()
602 );
603 }
604
605 #[test]
606 fn origin_dimensions_match_panel_geometry() {
607 let fb = TestBuffer::new();
608 assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
609 }
610
611 #[test]
612 fn debug_impl_includes_shape_information() {
613 let fb = TestBuffer::new();
614 let s = format!("{fb:?}");
615 assert!(s.contains("DmaFrameBuffer"));
616 assert!(s.contains("plane_count"));
617 assert!(s.contains("plane_size"));
618 }
619
620 #[test]
621 fn row_format_sets_exactly_one_data_word_with_oe_low() {
622 const TEST_N: usize = TEST_COLS;
623 let mut row = Row::<TEST_N>::new();
624 row.format(9);
625
626 let oe_active = !cfg!(feature = "invert-oe");
627 let active_count = TEST_N.saturating_sub(LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 1);
628 let blank_count = TEST_N - active_count;
629 let oe_blank_indices: std::vec::Vec<_> = row
630 .data
631 .iter()
632 .enumerate()
633 .filter_map(|(i, entry)| (entry.output_enable() != oe_active).then_some(i))
634 .collect();
635 assert_eq!(oe_blank_indices.len(), blank_count);
636 assert!(oe_blank_indices.contains(&map_index(TEST_N - 1)));
637 }
638
639 #[test]
640 fn default_constructors_match_new() {
641 let row_default = Row::<TEST_COLS>::default();
642 let row_new = Row::<TEST_COLS>::new();
643 assert_eq!(row_default, row_new);
644
645 let fb_default = TestBuffer::default();
646 let fb_new = TestBuffer::new();
647 assert_eq!(fb_default.planes, fb_new.planes);
648 }
649
650 #[test]
651 fn framebuffer_operations_trait_delegates_correctly() {
652 let mut fb = TestBuffer::new();
653 FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
654
655 assert!(fb.planes[0][map_row_index::<16>(5)].data[map_index(3)].grn1());
656
657 FrameBufferOperations::erase(&mut fb);
658 for plane in &fb.planes {
659 for row in plane {
660 for entry in &row.data {
661 assert!(!entry.red1());
662 assert!(!entry.grn1());
663 assert!(!entry.blu1());
664 assert!(!entry.red2());
665 assert!(!entry.grn2());
666 assert!(!entry.blu2());
667 }
668 }
669 }
670 }
671
672 #[test]
673 fn addr_table_entries_are_consistent() {
674 let table = make_addr_table();
675 for addr in 0..32u8 {
676 let row = &table[addr as usize];
677 assert!(row[map_index(0)].latch());
679 assert_eq!(row[map_index(0)].addr(), addr);
680 assert!(row[map_index(1)].latch());
681 assert_eq!(row[map_index(1)].addr(), addr);
682 assert!(!row[map_index(2)].latch());
684 assert_eq!(row[map_index(2)].addr(), addr);
685 assert!(!row[map_index(3)].latch());
687 assert_eq!(row[map_index(3)].0, OE_BLANK);
688 }
689 assert_eq!(table, ADDR_TABLE);
690 }
691
692 #[test]
693 fn test_blanking_delay() {
694 let mut row = Row::<TEST_COLS>::new();
695 row.format(5);
696
697 let oe_active = !cfg!(feature = "invert-oe");
698
699 if TRAIL_BLANK_DELAY > 0 {
701 let trail_blank_idx = map_index(TRAIL_BLANK_DELAY - 1);
702 assert_eq!(row.data[trail_blank_idx].output_enable(), !oe_active);
703 }
704
705 let first_active_idx = map_index(TRAIL_BLANK_DELAY);
706 assert_eq!(row.data[first_active_idx].output_enable(), oe_active);
707
708 let last_active_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 2);
710 assert_eq!(row.data[last_active_idx].output_enable(), oe_active);
711
712 let lead_blank_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
713 assert_eq!(row.data[lead_blank_idx].output_enable(), !oe_active);
714
715 let last_pixel_idx = map_index(TEST_COLS - 1);
716 assert_eq!(row.data[last_pixel_idx].output_enable(), !oe_active);
717 }
718
719 static STATIC_FB: TestBuffer = TestBuffer::new();
720
721 #[test]
722 fn test_static_construction_is_formatted() {
723 let runtime_fb = TestBuffer::new();
724
725 for (pi, plane) in STATIC_FB.planes.iter().enumerate() {
726 for (ri, row) in plane.iter().enumerate() {
727 assert_eq!(
728 row.data, runtime_fb.planes[pi][ri].data,
729 "static vs runtime data mismatch at plane {pi}, row {ri}"
730 );
731 assert_eq!(
732 row.address, runtime_fb.planes[pi][ri].address,
733 "static vs runtime address mismatch at plane {pi}, row {ri}"
734 );
735 assert_eq!(
736 row.gap, runtime_fb.planes[pi][ri].gap,
737 "static vs runtime gap mismatch at plane {pi}, row {ri}"
738 );
739 }
740 }
741 }
742
743 #[test]
744 fn test_format_reinitializes_at_runtime() {
745 let mut fb = TestBuffer::new();
746 fb.erase();
747 fb.format();
748
749 for (pi, plane) in fb.planes.iter().enumerate() {
750 for (ri, row) in plane.iter().enumerate() {
751 assert_eq!(
752 row.data, STATIC_FB.planes[pi][ri].data,
753 "re-formatted vs static mismatch at plane {pi}, row {ri}"
754 );
755 }
756 }
757 }
758
759 #[test]
760 fn bcm_segment_count_equals_planes() {
761 use crate::FrameBuffer;
762 let fb = TestBuffer::new();
763 assert_eq!(fb.bcm_segment_count(), 8);
764 }
765
766 #[test]
767 fn bcm_segments_have_correct_reps() {
768 use crate::FrameBuffer;
769 let fb = TestBuffer::new();
770 for i in 0..8 {
771 let seg = fb.bcm_segment(i);
772 let expected = if i == 0 { 1 } else { 1 << (i - 1) };
773 assert_eq!(seg.reps, expected, "wrong reps for segment {i}");
774 }
775 }
776
777 #[test]
778 fn bcm_segments_point_to_plane_data() {
779 use crate::FrameBuffer;
780 let fb = TestBuffer::new();
781 let plane_len = 16 * core::mem::size_of::<Row<TEST_COLS>>();
782 for i in 0..8 {
783 let seg = fb.bcm_segment(i);
784 let expected_ptr = fb.planes[i].as_ptr().cast::<u8>();
785 assert_eq!(seg.ptr, expected_ptr, "wrong ptr for segment {i}");
786 assert_eq!(seg.len, plane_len * (8 - i), "wrong len for segment {i}");
787 }
788 }
789
790 #[test]
791 fn bcm_segment_total_reps_is_halved_by_coalescing() {
792 use crate::FrameBuffer;
793 let fb = TestBuffer::new();
794 let total: usize = (0..fb.bcm_segment_count())
795 .map(|i| fb.bcm_segment(i).reps)
796 .sum();
797 assert_eq!(total, 1 << (8 - 1));
798 }
799
800 #[test]
801 fn bcm_segment_plane_coverage_matches_bcm_weights() {
802 use crate::FrameBuffer;
803 let fb = TestBuffer::new();
804 let plane_bytes = 16 * core::mem::size_of::<Row<TEST_COLS>>();
805
806 for plane in 0..8usize {
807 let mut coverage = 0;
811 for first in 0..=plane {
812 let seg = fb.bcm_segment(first);
813 if first + seg.len / plane_bytes > plane {
814 coverage += seg.reps;
815 }
816 }
817 assert_eq!(coverage, 1 << plane, "plane {plane} coverage wrong");
818 }
819 }
820
821 #[test]
822 #[should_panic(expected = "out of range")]
823 fn bcm_segment_panics_for_invalid_index() {
824 use crate::FrameBuffer;
825 let fb = TestBuffer::new();
826 let _ = fb.bcm_segment(8);
827 }
828
829 #[test]
830 fn bcm_sequence_matches_runtime_segments() {
831 use crate::FrameBuffer;
832 let fb = TestBuffer::new();
833 assert_eq!(
834 TestBuffer::BCM_SEGMENT_COUNT,
835 TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
836 );
837 assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
838 assert_eq!(
839 fb.bcm_segments_per_group(),
840 TestBuffer::BCM_SEGMENTS_PER_GROUP
841 );
842 assert_eq!(
843 TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
844 0
845 );
846 for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
847 let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
848 let seg = fb.bcm_segment(i);
849 assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
850 assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
851 }
852 for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
853 assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
854 }
855 }
856}