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