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