use core::convert::Infallible;
use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::prelude::{DrawTarget, OriginDimensions, Point, Size};
use super::{
map_index, Address, Entry, ADDR_TABLE, INTER_ROW_BLANK, LEAD_BLANK_DELAY, OE_ACTIVE, OE_BLANK,
TRAIL_BLANK_DELAY,
};
use crate::Color;
use crate::{map_row_index, slot_addresses};
use crate::{BcmLenReps, FrameBuffer, BCM_SEQUENCE_CAPACITY};
use crate::{FrameBufferOperations, MutableFrameBuffer};
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
pub struct PlaneRow<const COLS: usize> {
pub(crate) data: [Entry; COLS],
pub(crate) address: [Address; 4],
}
impl<const COLS: usize> PlaneRow<COLS> {
const fn new() -> Self {
Self {
data: [Entry::new(); COLS],
address: [Address::new(); 4],
}
}
}
const GAP_BYTES: usize = INTER_ROW_BLANK * core::mem::size_of::<Entry>();
#[allow(clippy::absurd_extreme_comparisons)]
const HAS_GAP: bool = GAP_BYTES > 0;
const fn plane_suffix(plane_idx: usize, planes: usize) -> (usize, usize) {
let covered = if HAS_GAP && plane_idx == 0 {
1
} else {
planes - plane_idx
};
let reps = if plane_idx == 0 {
1
} else if HAS_GAP && plane_idx == 1 {
2
} else {
1 << (plane_idx - 1)
};
(covered, reps)
}
#[allow(clippy::absurd_extreme_comparisons)]
const fn bcm_sequence<const COLS: usize, const PLANES: usize>(
) -> [BcmLenReps; BCM_SEQUENCE_CAPACITY] {
let gap = HAS_GAP as usize;
let seq_len = PLANES + gap;
assert!(seq_len <= BCM_SEQUENCE_CAPACITY);
let plane_bytes = core::mem::size_of::<PlaneRow<COLS>>();
let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
let mut within = 0usize;
while within < seq_len {
let entry = if within == 0 {
let (covered, reps) = plane_suffix(0, PLANES);
BcmLenReps {
len: plane_bytes * covered,
reps,
}
} else if HAS_GAP && within == 1 {
BcmLenReps {
len: GAP_BYTES,
reps: 1,
}
} else {
let (covered, reps) = plane_suffix(within - gap, PLANES);
BcmLenReps {
len: plane_bytes * covered,
reps,
}
};
seq[within] = entry;
within += 1;
}
seq
}
#[inline]
#[allow(clippy::absurd_extreme_comparisons)]
const fn make_row_plane_template<const COLS: usize>(
trail_blank: bool,
lead_blank: bool,
) -> [Entry; COLS] {
let trailing = if lead_blank { LEAD_BLANK_DELAY } else { 0 };
let lead_start = COLS.saturating_sub(trailing);
let mut data = [Entry::new(); COLS];
let mut i = 0;
while i < COLS {
let blanked = (trail_blank && i < TRAIL_BLANK_DELAY) || i >= lead_start;
data[map_index(i)].0 = if blanked { OE_BLANK } else { OE_ACTIVE };
i += 1;
}
data
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
pub struct RowData<const COLS: usize, const PLANES: usize> {
pub(crate) planes: [PlaneRow<COLS>; PLANES],
pub(crate) gap: [Entry; INTER_ROW_BLANK],
}
impl<const COLS: usize, const PLANES: usize> RowData<COLS, PLANES> {
const fn new() -> Self {
Self {
planes: [PlaneRow::new(); PLANES],
gap: [Entry::new(); INTER_ROW_BLANK],
}
}
const fn format(&mut self, addr: u8) {
let src_addr = ADDR_TABLE[addr as usize];
let mut p = 0;
while p < PLANES {
let trail_blank = p == 1 || (p == 0 && PLANES == 1);
let lead_blank = p == 0;
let template = make_row_plane_template::<COLS>(trail_blank, lead_blank);
let mut c = 0;
while c < COLS {
self.planes[p].data[c] = template[c];
c += 1;
}
self.planes[p].address[0] = src_addr[0];
self.planes[p].address[1] = src_addr[1];
self.planes[p].address[2] = src_addr[2];
self.planes[p].address[3] = src_addr[3];
p += 1;
}
let mut i = 0;
#[allow(clippy::absurd_extreme_comparisons)]
while i < INTER_ROW_BLANK {
self.gap[i].0 = OE_BLANK;
i += 1;
}
}
}
#[derive(Copy, Clone)]
#[repr(C, align(4))]
pub struct DmaFrameBuffer<const NROWS: usize, const COLS: usize, const PLANES: usize> {
pub(crate) rows: [RowData<COLS, PLANES>; NROWS],
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize>
DmaFrameBuffer<NROWS, COLS, PLANES>
{
#[must_use]
pub const fn new() -> Self {
assert!(NROWS >= 1 && NROWS <= 32, "NROWS must be within 1..=32");
assert!(PLANES >= 1 && PLANES <= 8, "PLANES must be within 1..=8");
#[cfg(feature = "esp32-ordering")]
assert!(
COLS % 4 == 0,
"esp32-ordering feature requires COLS to be divisible by 4"
);
let mut instance = Self {
rows: [RowData::new(); NROWS],
};
instance.format();
instance
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_count() -> usize {
PLANES
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_bytes() -> usize {
core::mem::size_of::<PlaneRow<COLS>>()
}
#[must_use]
pub const fn bcm_row_count() -> usize {
NROWS
}
#[must_use]
pub const fn bcm_row_bytes() -> usize {
GAP_BYTES
}
#[inline]
pub const fn format(&mut self) {
let mut slot = 0;
while slot < NROWS {
let (_, addr) = slot_addresses::<NROWS>(slot);
self.rows[slot].format(addr);
slot += 1;
}
}
#[inline]
pub fn erase(&mut self) {
const MASK: u8 = !crate::pinmap::LATCHED_COLOR_MASK;
for row in &mut self.rows {
for plane in &mut row.planes {
for entry in &mut plane.data {
entry.0 &= MASK;
}
}
}
}
#[inline]
pub fn set_pixel(&mut self, p: Point, color: Color) {
if p.x < 0 || p.y < 0 {
return;
}
self.set_pixel_internal(p.x as usize, p.y as usize, color);
}
#[inline]
fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
if x >= COLS || y >= NROWS * 2 {
return;
}
#[cfg(feature = "skip-black-pixels")]
if color == Color::BLACK {
return;
}
let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
let is_top = y < NROWS;
let red = color.r();
let green = color.g();
let blue = color.b();
for plane_idx in 0..PLANES {
let bit = (8 - PLANES + plane_idx) as u32;
let bits = ((u8::from(((blue >> bit) & 1) != 0)) << 2)
| ((u8::from(((green >> bit) & 1) != 0)) << 1)
| u8::from(((red >> bit) & 1) != 0);
let col_idx = map_index(x);
let entry = &mut self.rows[row_idx].planes[plane_idx].data[col_idx];
if is_top {
entry.set_color0_bits(bits);
} else {
entry.set_color1_bits(bits);
}
}
}
#[must_use]
pub fn pixel_row_ptr_len(&self, row_idx: usize, plane_idx: usize) -> (*const u8, usize) {
assert!(
row_idx < NROWS,
"row_idx {row_idx} out of range for {NROWS} rows"
);
assert!(
plane_idx < PLANES,
"plane_idx {plane_idx} out of range for {PLANES} planes"
);
let ptr = (&raw const self.rows[row_idx].planes[plane_idx]).cast::<u8>();
let len = core::mem::size_of::<PlaneRow<COLS>>();
(ptr, len)
}
#[must_use]
pub fn gap_ptr_len(&self, row_idx: usize) -> (*const u8, usize) {
assert!(
row_idx < NROWS,
"row_idx {row_idx} out of range for {NROWS} rows"
);
let ptr = (&raw const self.rows[row_idx].gap).cast::<u8>();
(ptr, GAP_BYTES)
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> Default
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn default() -> Self {
Self::new()
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> core::fmt::Debug
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_struct("DmaFrameBuffer")
.field("size", &core::mem::size_of_val(self))
.field("row_count", &NROWS)
.field("planes", &PLANES)
.field("cols", &COLS)
.finish()
}
}
#[cfg(feature = "defmt")]
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> defmt::Format
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "DmaFrameBuffer<{}, {}, {}>", NROWS, COLS, PLANES);
defmt::write!(f, " size: {}", core::mem::size_of_val(self));
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBuffer
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
type Word = u8;
const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = bcm_sequence::<COLS, PLANES>();
const BCM_SEQUENCE_LEN: usize = PLANES + HAS_GAP as usize;
const BCM_SEQUENCE_COUNT: usize = NROWS;
const BCM_SEGMENTS_PER_GROUP: usize = PLANES + HAS_GAP as usize;
fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
let gap = usize::from(HAS_GAP);
let segments_per_row = PLANES + gap;
assert!(
index < NROWS * segments_per_row,
"segment index {index} out of range"
);
let row_idx = index / segments_per_row;
let within_row = index % segments_per_row;
if within_row == 0 {
let (ptr, _) = self.pixel_row_ptr_len(row_idx, 0);
ptr
} else if HAS_GAP && within_row == 1 {
let (ptr, _) = self.gap_ptr_len(row_idx);
ptr
} else {
let plane_idx = within_row - gap;
let (ptr, _) = self.pixel_row_ptr_len(row_idx, plane_idx);
ptr
}
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> FrameBufferOperations
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
#[inline]
fn erase(&mut self) {
DmaFrameBuffer::<NROWS, COLS, PLANES>::erase(self);
}
#[inline]
fn set_pixel(&mut self, p: Point, color: Color) {
DmaFrameBuffer::<NROWS, COLS, PLANES>::set_pixel(self, p, color);
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> MutableFrameBuffer
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> OriginDimensions
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
fn size(&self) -> Size {
Size::new(COLS as u32, (NROWS * 2) as u32)
}
}
impl<const NROWS: usize, const COLS: usize, const PLANES: usize> DrawTarget
for DmaFrameBuffer<NROWS, COLS, PLANES>
{
type Color = Color;
type Error = Infallible;
fn draw_iter<I>(&mut self, pixels: I) -> Result<(), Self::Error>
where
I: IntoIterator<Item = embedded_graphics::Pixel<Self::Color>>,
{
for pixel in pixels {
if pixel.0.x < 0 || pixel.0.y < 0 {
continue;
}
self.set_pixel_internal(pixel.0.x as usize, pixel.0.y as usize, pixel.1);
}
Ok(())
}
}
#[cfg(test)]
mod tests {
extern crate std;
use super::super::{LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
use super::*;
use embedded_graphics::prelude::*;
use std::format;
const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
128
} else {
64
};
const TEST_PLANES: usize = 8;
type TestBuffer = DmaFrameBuffer<16, TEST_COLS, TEST_PLANES>;
#[test]
fn format_sets_current_row_address_for_all_planes() {
let fb = TestBuffer::new();
for slot in 0..16 {
let (_, addr) = slot_addresses::<16>(slot);
for plane_idx in 0..TEST_PLANES {
let plane = &fb.rows[slot].planes[plane_idx];
assert_eq!(
plane.address[map_index(0)].addr(),
addr,
"slot {slot} plane {plane_idx} addr byte 0 should be the current row"
);
assert_eq!(
plane.address[map_index(1)].addr(),
addr,
"slot {slot} plane {plane_idx} addr byte 1 should be the current row"
);
assert_eq!(
plane.address[map_index(2)].addr(),
addr,
"slot {slot} plane {plane_idx} addr byte 2 should be the current row"
);
assert_eq!(
plane.address[map_index(3)].0,
OE_BLANK,
"slot {slot} plane {plane_idx} addr byte 3 should be blanked with addr 0"
);
}
}
}
#[test]
fn address_bytes_have_two_latch_pulses() {
let fb = TestBuffer::new();
for row in &fb.rows {
for plane in &row.planes {
let latch_count = plane.address.iter().filter(|a| a.latch()).count();
assert_eq!(latch_count, 2, "expected exactly two latch pulses");
assert!(plane.address[map_index(0)].latch());
assert!(plane.address[map_index(1)].latch());
}
}
}
#[test]
fn planes_match_row_plane_template() {
let fb = TestBuffer::new();
for row_idx in 0..16 {
for p in 0..TEST_PLANES {
let expected = make_row_plane_template::<TEST_COLS>(p == 1, p == 0);
assert_eq!(
fb.rows[row_idx].planes[p].data, expected,
"row {row_idx} plane {p} doesn't match template"
);
}
}
}
#[test]
fn first_plane_has_lead_blank_only() {
let fb = TestBuffer::new();
let oe_active = !cfg!(feature = "invert-oe");
for row in &fb.rows {
let plane = &row.planes[0];
let first_idx = map_index(0);
assert_eq!(
plane.data[first_idx].output_enable(),
oe_active,
"plane 0: first pixel should have OE active (no trail blank)"
);
for i in (TEST_COLS - LEAD_BLANK_DELAY)..TEST_COLS {
assert_eq!(
plane.data[map_index(i)].output_enable(),
!oe_active,
"plane 0: lead blank entry {i} should have OE blank"
);
}
let active_idx = map_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
assert_eq!(
plane.data[active_idx].output_enable(),
oe_active,
"plane 0: entry before the lead blank should have OE active"
);
}
}
#[test]
fn second_plane_has_trail_blank_only() {
let fb = TestBuffer::new();
let oe_active = !cfg!(feature = "invert-oe");
for row in &fb.rows {
let plane = &row.planes[1];
for i in 0..TRAIL_BLANK_DELAY {
assert_eq!(
plane.data[map_index(i)].output_enable(),
!oe_active,
"plane 1: trail blank entry {i} should have OE blank"
);
}
let active_idx = map_index(TRAIL_BLANK_DELAY);
assert_eq!(
plane.data[active_idx].output_enable(),
oe_active,
"plane 1: entry after the trail blank should have OE active"
);
let last_idx = map_index(TEST_COLS - 1);
assert_eq!(
plane.data[last_idx].output_enable(),
oe_active,
"plane 1: last pixel should have OE active (no lead blank)"
);
}
}
#[test]
fn middle_planes_have_full_width_window() {
let fb = TestBuffer::new();
let oe_active = !cfg!(feature = "invert-oe");
for row in &fb.rows {
for plane in row.planes.iter().take(TEST_PLANES - 1).skip(2) {
let first_idx = map_index(0);
assert_eq!(
plane.data[first_idx].output_enable(),
oe_active,
"middle plane: first pixel should have OE active"
);
let last_idx = map_index(TEST_COLS - 1);
assert_eq!(
plane.data[last_idx].output_enable(),
oe_active,
"middle plane: last pixel should have OE active"
);
}
}
}
#[test]
fn last_plane_has_full_width_window() {
let fb = TestBuffer::new();
let oe_active = !cfg!(feature = "invert-oe");
let last = TEST_PLANES - 1;
for row in &fb.rows {
let plane = &row.planes[last];
let last_idx = map_index(TEST_COLS - 1);
assert_eq!(
plane.data[last_idx].output_enable(),
oe_active,
"last plane: last pixel should have OE active (no lead blank)"
);
let first_idx = map_index(0);
assert_eq!(
plane.data[first_idx].output_enable(),
oe_active,
"last plane: first pixel should have OE active (no trail blank)"
);
}
}
#[test]
fn oe_window_sizes_match_template() {
let fb = TestBuffer::new();
let oe_active = !cfg!(feature = "invert-oe");
for row in &fb.rows {
for (p, plane) in row.planes.iter().enumerate() {
let trail = if p == 1 { TRAIL_BLANK_DELAY } else { 0 };
let lead = if p == 0 { LEAD_BLANK_DELAY } else { 0 };
let expected_active = TEST_COLS.saturating_sub(trail + lead);
let active = plane
.data
.iter()
.filter(|e| e.output_enable() == oe_active)
.count();
assert_eq!(active, expected_active, "plane {p} active window size");
}
}
}
#[test]
fn single_plane_has_trail_and_lead_blank() {
type OnePlane = DmaFrameBuffer<16, TEST_COLS, 1>;
let fb = OnePlane::new();
for slot in 0..16 {
let (_, addr) = slot_addresses::<16>(slot);
let plane = &fb.rows[slot].planes[0];
assert_eq!(
plane.address[map_index(0)].addr(),
addr,
"single plane slot {slot}: should use the current row address"
);
let expected = make_row_plane_template::<TEST_COLS>(true, true);
assert_eq!(
plane.data, expected,
"single plane slot {slot}: template mismatch"
);
}
}
#[test]
fn set_pixel_maps_top_half_bits_per_plane() {
let mut fb = TestBuffer::new();
let color = Color::new(0b1010_0101, 0b0101_1010, 0b1111_0000);
fb.set_pixel(Point::new(2, 3), color);
for plane_idx in 0..TEST_PLANES {
let bit = 8 - TEST_PLANES + plane_idx;
let entry = fb.rows[map_row_index::<16>(3)].planes[plane_idx].data[map_index(2)];
assert_eq!(entry.red1(), ((color.r() >> bit) & 1) != 0);
assert_eq!(entry.grn1(), ((color.g() >> bit) & 1) != 0);
assert_eq!(entry.blu1(), ((color.b() >> bit) & 1) != 0);
}
}
#[test]
fn set_pixel_maps_bottom_half_bits_per_plane() {
let mut fb = TestBuffer::new();
let color = Color::new(0b1100_0011, 0b0011_1100, 0b1001_0110);
fb.set_pixel(Point::new(4, 20), color);
for plane_idx in 0..TEST_PLANES {
let bit = 8 - TEST_PLANES + plane_idx;
let entry = fb.rows[map_row_index::<16>(4)].planes[plane_idx].data[map_index(4)];
assert_eq!(entry.red2(), ((color.r() >> bit) & 1) != 0);
assert_eq!(entry.grn2(), ((color.g() >> bit) & 1) != 0);
assert_eq!(entry.blu2(), ((color.b() >> bit) & 1) != 0);
}
}
#[test]
#[cfg(feature = "reverse-row-order")]
fn reverse_row_order_stores_rows_back_to_front() {
let mut fb = TestBuffer::new();
assert_eq!(fb.rows[0].planes[0].address[map_index(0)].addr(), 15);
assert_eq!(fb.rows[15].planes[0].address[map_index(0)].addr(), 0);
fb.set_pixel(Point::new(2, 0), Color::RED);
let col2 = map_index(2);
assert!(fb.rows[15].planes[TEST_PLANES - 1].data[col2].red1());
assert!(!fb.rows[0].planes[TEST_PLANES - 1].data[col2].red1());
}
#[test]
fn erase_clears_only_color_bits() {
let mut fb = TestBuffer::new();
let oe_before =
fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable();
let addr_before = fb.rows[map_row_index::<16>(3)].planes[2].address;
fb.set_pixel(Point::new(0, 0), Color::WHITE);
fb.erase();
for row in &fb.rows {
for plane in &row.planes {
for entry in &plane.data {
assert!(!entry.red1());
assert!(!entry.grn1());
assert!(!entry.blu1());
assert!(!entry.red2());
assert!(!entry.grn2());
assert!(!entry.blu2());
}
}
}
assert_eq!(
fb.rows[map_row_index::<16>(0)].planes[0].data[map_index(1)].output_enable(),
oe_before
);
assert_eq!(
fb.rows[map_row_index::<16>(3)].planes[2].address,
addr_before
);
}
#[test]
fn draw_target_iter_sets_pixels() {
let mut fb = TestBuffer::new();
let pixels = [Pixel(Point::new(1, 1), Color::RED)];
let result = fb.draw_iter(pixels);
assert!(result.is_ok());
for plane_idx in 0..TEST_PLANES {
let bit = 8 - TEST_PLANES + plane_idx;
let entry = fb.rows[map_row_index::<16>(1)].planes[plane_idx].data[map_index(1)];
assert_eq!(entry.red1(), ((Color::RED.r() >> bit) & 1) != 0);
assert!(!entry.grn1());
assert!(!entry.blu1());
}
}
#[test]
fn set_pixel_ignores_out_of_bounds_and_negative() {
let mut fb = TestBuffer::new();
let before = fb.rows;
fb.set_pixel(Point::new(-1, 0), Color::WHITE);
fb.set_pixel(Point::new(0, -1), Color::WHITE);
fb.set_pixel(Point::new(TEST_COLS as i32, 0), Color::WHITE);
fb.set_pixel(Point::new(0, 32), Color::WHITE);
assert_eq!(fb.rows, before);
}
#[test]
#[cfg(feature = "skip-black-pixels")]
fn test_skip_black_pixels_enabled() {
let mut fb = TestBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = map_index(10);
assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
fb.set_pixel_internal(10, 5, Color::BLACK);
assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
}
#[test]
#[cfg(not(feature = "skip-black-pixels"))]
fn test_skip_black_pixels_disabled() {
let mut fb = TestBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = map_index(10);
assert!(fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
fb.set_pixel_internal(10, 5, Color::BLACK);
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].red1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].grn1());
assert!(!fb.rows[map_row_index::<16>(5)].planes[0].data[mapped_col_10].blu1());
}
#[test]
#[should_panic(expected = "NROWS must be within 1..=32")]
fn new_panics_for_too_many_rows() {
let _ = DmaFrameBuffer::<33, TEST_COLS, 8>::new();
}
#[test]
#[should_panic(expected = "PLANES must be within 1..=8")]
fn new_panics_for_too_many_planes() {
let _ = DmaFrameBuffer::<16, TEST_COLS, 9>::new();
}
#[test]
fn pixel_row_ptr_len_returns_correct_pointers() {
let fb = TestBuffer::new();
let (ptr, len) = fb.pixel_row_ptr_len(0, 0);
assert_eq!(len, core::mem::size_of::<PlaneRow<TEST_COLS>>());
assert_eq!(ptr, (&raw const fb.rows[0].planes[0]).cast::<u8>());
}
#[test]
fn gap_ptr_len_returns_correct_pointers() {
let fb = TestBuffer::new();
let (ptr, len) = fb.gap_ptr_len(0);
assert_eq!(len, GAP_BYTES);
assert_eq!(ptr, (&raw const fb.rows[0].gap).cast::<u8>());
}
#[test]
#[should_panic(expected = "out of range")]
fn pixel_row_ptr_len_panics_for_invalid_row() {
let fb = TestBuffer::new();
let _ = fb.pixel_row_ptr_len(16, 0);
}
#[test]
#[should_panic(expected = "out of range")]
fn pixel_row_ptr_len_panics_for_invalid_plane() {
let fb = TestBuffer::new();
let _ = fb.pixel_row_ptr_len(0, TEST_PLANES);
}
#[test]
#[should_panic(expected = "out of range")]
fn gap_ptr_len_panics_for_invalid_row() {
let fb = TestBuffer::new();
let _ = fb.gap_ptr_len(16);
}
#[test]
fn origin_dimensions_match_panel_geometry() {
let fb = TestBuffer::new();
assert_eq!(fb.size(), Size::new(TEST_COLS as u32, 32));
}
#[test]
fn default_matches_new() {
let fb_default = TestBuffer::default();
let fb_new = TestBuffer::new();
assert_eq!(fb_default.rows, fb_new.rows);
}
#[test]
fn debug_impl_includes_shape_information() {
let fb = TestBuffer::new();
let s = format!("{fb:?}");
assert!(s.contains("DmaFrameBuffer"));
assert!(s.contains("row_count"));
assert!(s.contains("planes"));
}
static STATIC_FB: TestBuffer = TestBuffer::new();
#[test]
fn static_construction_is_formatted() {
let runtime_fb = TestBuffer::new();
for (ri, row) in STATIC_FB.rows.iter().enumerate() {
assert_eq!(
row.planes, runtime_fb.rows[ri].planes,
"static vs runtime plane mismatch at row {ri}"
);
assert_eq!(
row.gap, runtime_fb.rows[ri].gap,
"static vs runtime gap mismatch at row {ri}"
);
}
}
#[test]
fn format_reinitializes_at_runtime() {
let mut fb = TestBuffer::new();
fb.erase();
fb.format();
for (ri, row) in fb.rows.iter().enumerate() {
assert_eq!(
row.planes, STATIC_FB.rows[ri].planes,
"re-formatted vs static plane mismatch at row {ri}"
);
assert_eq!(
row.gap, STATIC_FB.rows[ri].gap,
"re-formatted vs static gap mismatch at row {ri}"
);
}
}
#[test]
fn framebuffer_operations_trait_delegates_correctly() {
let mut fb = TestBuffer::new();
FrameBufferOperations::set_pixel(&mut fb, Point::new(3, 5), Color::GREEN);
assert!(fb.rows[map_row_index::<16>(5)].planes[TEST_PLANES - 1].data[map_index(3)].grn1());
FrameBufferOperations::erase(&mut fb);
for row in &fb.rows {
for plane in &row.planes {
for entry in &plane.data {
assert!(!entry.red1());
assert!(!entry.grn1());
assert!(!entry.blu1());
assert!(!entry.red2());
assert!(!entry.grn2());
assert!(!entry.blu2());
}
}
}
}
#[test]
fn row_data_size_matches_expected_layout() {
let expected_planes = core::mem::size_of::<PlaneRow<TEST_COLS>>() * TEST_PLANES;
assert_eq!(
core::mem::size_of::<RowData<TEST_COLS, TEST_PLANES>>(),
expected_planes + GAP_BYTES
);
}
#[test]
fn bcm_segment_count_equals_rows_times_planes_plus_gap() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
assert_eq!(fb.bcm_segment_count(), 16 * (TEST_PLANES + gap));
}
#[test]
fn bcm_segments_per_group_equals_planes_plus_gap() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
assert_eq!(fb.bcm_segments_per_group(), TEST_PLANES + gap);
assert_eq!(
fb.bcm_segment_count() % fb.bcm_segments_per_group(),
0,
"segment count must be divisible by segments_per_group"
);
}
#[test]
fn bcm_segments_interleave_pixel_and_gap() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
let segments_per_row = TEST_PLANES + gap;
for row in 0..16usize {
let seg = fb.bcm_segment(row * segments_per_row);
let (ptr, plane_len) = fb.pixel_row_ptr_len(row, 0);
let covered0 = if HAS_GAP { 1 } else { TEST_PLANES };
assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane 0");
assert_eq!(
seg.len,
plane_len * covered0,
"wrong len at row {row} plane 0"
);
assert_eq!(seg.reps, 1, "wrong reps at row {row} plane 0");
if HAS_GAP {
let seg = fb.bcm_segment(row * segments_per_row + 1);
let (ptr, len) = fb.gap_ptr_len(row);
assert_eq!(seg.ptr, ptr, "wrong gap ptr at row {row}");
assert_eq!(seg.len, len, "wrong gap len at row {row}");
assert_eq!(seg.reps, 1, "gap reps must be 1 at row {row}");
}
for plane in 1..TEST_PLANES {
let idx = row * segments_per_row + plane + gap;
let seg = fb.bcm_segment(idx);
let (ptr, plane_len) = fb.pixel_row_ptr_len(row, plane);
let expected_reps = if HAS_GAP && plane == 1 {
2
} else {
1 << (plane - 1)
};
assert_eq!(seg.ptr, ptr, "wrong ptr at row {row} plane {plane}");
assert_eq!(
seg.len,
plane_len * (TEST_PLANES - plane),
"wrong len at row {row} plane {plane}"
);
assert_eq!(
seg.reps, expected_reps,
"wrong reps at row {row} plane {plane}"
);
}
}
}
#[test]
fn bcm_segment_total_reps_per_row() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
let segments_per_row = TEST_PLANES + gap;
for row in 0..16usize {
let mut pixel_reps = 0;
for plane in 0..TEST_PLANES {
let offset = if plane == 0 { 0 } else { plane + gap };
pixel_reps += fb.bcm_segment(row * segments_per_row + offset).reps;
}
let expected = if HAS_GAP {
(1 << (TEST_PLANES - 1)) + 1
} else {
1 << (TEST_PLANES - 1)
};
assert_eq!(pixel_reps, expected, "total pixel reps wrong for row {row}");
}
}
#[test]
fn bcm_segment_plane_coverage_matches_bcm_weights() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
let segments_per_row = TEST_PLANES + gap;
let plane_bytes = core::mem::size_of::<PlaneRow<TEST_COLS>>();
for row in 0..16usize {
for plane in 0..TEST_PLANES {
let mut coverage = 0;
for first in 0..=plane {
let offset = if first == 0 { 0 } else { first + gap };
let seg = fb.bcm_segment(row * segments_per_row + offset);
if first + seg.len / plane_bytes > plane {
coverage += seg.reps;
}
}
assert_eq!(
coverage,
1 << plane,
"plane {plane} coverage wrong for row {row}"
);
}
}
}
#[test]
#[should_panic(expected = "out of range")]
fn bcm_segment_panics_for_invalid_index() {
let fb = TestBuffer::new();
let gap = if HAS_GAP { 1 } else { 0 };
let segments_per_row = TEST_PLANES + gap;
let _ = fb.bcm_segment(16 * segments_per_row);
}
#[test]
fn bcm_sequence_matches_runtime_segments() {
use crate::FrameBuffer;
let fb = TestBuffer::new();
assert_eq!(
TestBuffer::BCM_SEGMENT_COUNT,
TestBuffer::BCM_SEQUENCE_LEN * TestBuffer::BCM_SEQUENCE_COUNT
);
assert_eq!(fb.bcm_segment_count(), TestBuffer::BCM_SEGMENT_COUNT);
assert_eq!(
fb.bcm_segments_per_group(),
TestBuffer::BCM_SEGMENTS_PER_GROUP
);
assert_eq!(
TestBuffer::BCM_SEQUENCE_LEN % TestBuffer::BCM_SEGMENTS_PER_GROUP,
0
);
for i in 0..TestBuffer::BCM_SEGMENT_COUNT {
let entry = TestBuffer::BCM_SEQUENCE[i % TestBuffer::BCM_SEQUENCE_LEN];
let seg = fb.bcm_segment(i);
assert_eq!((seg.len, seg.reps), (entry.len, entry.reps), "segment {i}");
assert!(!seg.ptr.is_null(), "segment {i} has null pointer");
}
for entry in &TestBuffer::BCM_SEQUENCE[TestBuffer::BCM_SEQUENCE_LEN..] {
assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
}
}
#[test]
fn bcm_segment_data_contains_written_pixels() {
const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
128
} else {
64
};
type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
let mut fb = TrinityBuf::new();
fb.set_pixel(Point::new(10, 0), Color::WHITE);
let col_idx = map_index(10);
let gap = usize::from(HAS_GAP);
let row0_base = map_row_index::<32>(0) * (5 + gap);
for plane_idx in 0..5usize {
let seg = fb.bcm_segment(if plane_idx == 0 {
row0_base
} else {
row0_base + plane_idx + gap
});
let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
let raw = data[col_idx];
let entry = Entry(raw);
let bit = 8 - 5 + plane_idx; let expect_r = (0xFF >> bit) & 1 != 0;
let expect_g = (0xFF >> bit) & 1 != 0;
let expect_b = (0xFF >> bit) & 1 != 0;
assert_eq!(
entry.red1(),
expect_r,
"plane {plane_idx} (bit {bit}) R1 mismatch: raw={raw:#04x}"
);
assert_eq!(
entry.grn1(),
expect_g,
"plane {plane_idx} (bit {bit}) G1 mismatch: raw={raw:#04x}"
);
assert_eq!(
entry.blu1(),
expect_b,
"plane {plane_idx} (bit {bit}) B1 mismatch: raw={raw:#04x}"
);
}
}
#[test]
fn bcm_segment_data_contains_gradient_pixels() {
const TRINITY_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
128
} else {
64
};
type TrinityBuf = DmaFrameBuffer<32, TRINITY_COLS, 5>;
let mut fb = TrinityBuf::new();
let step: u8 = (256 / TRINITY_COLS) as u8;
for x in 0..TRINITY_COLS {
let brightness = (x as u8).wrapping_mul(step);
fb.set_pixel(Point::new(x as i32, 0), Color::new(brightness, 0, 0));
}
let mut any_nonzero = false;
let gap = usize::from(HAS_GAP);
let row0_base = map_row_index::<32>(0) * (5 + gap);
for plane_idx in 0..5usize {
let seg = fb.bcm_segment(if plane_idx == 0 {
row0_base
} else {
row0_base + plane_idx + gap
});
let data = unsafe { core::slice::from_raw_parts(seg.ptr, TRINITY_COLS) };
for x in 0..TRINITY_COLS {
let col_idx = map_index(x);
let entry = Entry(data[col_idx]);
let brightness = (x as u8).wrapping_mul(step);
let bit = 8 - 5 + plane_idx; let expect_r = (brightness >> bit) & 1 != 0;
assert_eq!(
entry.red1(),
expect_r,
"x={x} plane={plane_idx} bit={bit} brightness={brightness} \
raw={:#04x}",
data[col_idx]
);
if expect_r {
any_nonzero = true;
}
}
}
assert!(
any_nonzero,
"gradient should produce at least some non-zero color bits"
);
}
}