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