use core::convert::Infallible;
use crate::{
map_row_index, slot_addresses, BcmLenReps, FrameBuffer, FrameBufferOperations,
MutableFrameBuffer, BCM_SEQUENCE_CAPACITY,
};
use bitfield::bitfield;
use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::prelude::Point;
use super::Color;
#[cfg(feature = "tail-closes-latch")]
use crate::pinmap::ADDR_MASK;
use crate::pinmap::PINMAP;
use crate::pinmap::{
ADDR_SHIFT, COLOR0_MASK, COLOR0_SHIFT, COLOR1_MASK, COLOR1_SHIFT, COLOR_MASK, LATCH_BIT, OE_BIT,
};
use crate::{INTER_ROW_BLANK, LEAD_BLANK_DELAY, TRAIL_BLANK_DELAY};
#[cfg(not(feature = "invert-oe"))]
const OE_ACTIVE: u16 = OE_BIT;
#[cfg(not(feature = "invert-oe"))]
const OE_BLANK: u16 = 0;
#[cfg(feature = "invert-oe")]
const OE_ACTIVE: u16 = 0;
#[cfg(feature = "invert-oe")]
const OE_BLANK: u16 = OE_BIT;
#[allow(clippy::absurd_extreme_comparisons)]
#[inline]
const fn make_data_template<const COLS: usize>(addr: u8) -> [Entry; COLS] {
let mut data = [Entry::new(); COLS];
let mut i = 0;
while i < COLS {
let mut entry = Entry::new();
entry.0 = ((addr as u16) << ADDR_SHIFT) | OE_BLANK;
if i == COLS - 1 {
entry.0 |= LATCH_BIT;
} else if i >= TRAIL_BLANK_DELAY && i < COLS.saturating_sub(LEAD_BLANK_DELAY + 1) {
entry.0 = (entry.0 & !OE_BIT) | OE_ACTIVE;
}
data[map_index(i)] = entry;
i += 1;
}
data
}
bitfield! {
#[derive(Clone, Copy, Default, PartialEq)]
#[repr(transparent)]
struct Entry(u16);
dummy2, set_dummy2: PINMAP.spare[2];
blu2, set_blu2: PINMAP.blu1;
grn2, set_grn2: PINMAP.grn1;
red2, set_red2: PINMAP.red1;
blu1, set_blu1: PINMAP.blu0;
grn1, set_grn1: PINMAP.grn0;
red1, set_red1: PINMAP.red0;
output_enable, set_output_enable: PINMAP.oe;
dummy1, set_dummy1: PINMAP.spare[1];
dummy0, set_dummy0: PINMAP.spare[0];
latch, set_latch: PINMAP.latch;
addr, set_addr: PINMAP.addr_msb(), PINMAP.addr_lsb;
}
impl core::fmt::Debug for Entry {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
f.debug_tuple("Entry")
.field(&format_args!("{:#x}", self.0))
.finish()
}
}
#[cfg(feature = "defmt")]
impl defmt::Format for Entry {
fn format(&self, f: defmt::Formatter) {
defmt::write!(f, "Entry({=u16:#x})", self.0);
}
}
impl Entry {
const fn new() -> Self {
Self(OE_BLANK)
}
#[inline]
fn set_color0_bits(&mut self, bits: u8) {
let bits16 = u16::from(bits) << COLOR0_SHIFT;
self.0 = (self.0 & !COLOR0_MASK) | (bits16 & COLOR0_MASK);
}
#[inline]
fn set_color1_bits(&mut self, bits: u8) {
let bits16 = u16::from(bits) << COLOR1_SHIFT;
self.0 = (self.0 & !COLOR1_MASK) | (bits16 & COLOR1_MASK);
}
}
#[derive(Clone, Copy, PartialEq, Debug)]
#[repr(C)]
struct Row<const COLS: usize> {
data: [Entry; COLS],
gap: [Entry; INTER_ROW_BLANK],
}
const fn map_index(i: usize) -> usize {
#[cfg(feature = "esp32-ordering")]
{
i ^ 1
}
#[cfg(not(feature = "esp32-ordering"))]
{
i
}
}
impl<const COLS: usize> Default for Row<COLS> {
fn default() -> Self {
Self::new()
}
}
impl<const COLS: usize> Row<COLS> {
pub const fn new() -> Self {
Self {
data: [Entry::new(); COLS],
gap: [Entry::new(); INTER_ROW_BLANK],
}
}
pub const fn format(&mut self, prev_addr: u8) {
let template = make_data_template::<COLS>(prev_addr);
let mut i = 0;
while i < COLS {
self.data[i] = template[i];
i += 1;
}
let gap_val = ((prev_addr as u16) << ADDR_SHIFT) | OE_BLANK;
let mut i = 0;
#[allow(clippy::absurd_extreme_comparisons)]
while i < INTER_ROW_BLANK {
self.gap[i].0 = gap_val;
i += 1;
}
}
#[inline]
pub fn clear_colors(&mut self) {
const COLOR_CLEAR_MASK: u16 = !COLOR_MASK;
for entry in &mut self.data {
entry.0 &= COLOR_CLEAR_MASK;
}
}
#[inline]
pub fn set_color0(&mut self, col: usize, r: bool, g: bool, b: bool) {
let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
let col = map_index(col);
self.data[col].set_color0_bits(bits);
}
#[inline]
pub fn set_color1(&mut self, col: usize, r: bool, g: bool, b: bool) {
let bits = (u8::from(b) << 2) | (u8::from(g) << 1) | u8::from(r);
let col = map_index(col);
self.data[col].set_color1_bits(bits);
}
}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct Frame<const ROWS: usize, const COLS: usize, const NROWS: usize> {
rows: [Row<COLS>; NROWS],
}
impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Frame<ROWS, COLS, NROWS> {
pub const fn new() -> Self {
Self {
rows: [Row::new(); NROWS],
}
}
pub const fn format(&mut self) {
let mut slot = 0;
while slot < NROWS {
let (prev_addr, _) = slot_addresses::<NROWS>(slot);
self.rows[slot].format(prev_addr);
slot += 1;
}
}
#[inline]
pub fn clear_colors(&mut self) {
for row in &mut self.rows {
row.clear_colors();
}
}
#[inline]
pub fn set_pixel(&mut self, y: usize, x: usize, red: bool, green: bool, blue: bool) {
let row_idx = map_row_index::<NROWS>(if y < NROWS { y } else { y - NROWS });
let row = &mut self.rows[row_idx];
if y < NROWS {
row.set_color0(x, red, green, blue);
} else {
row.set_color1(x, red, green, blue);
}
}
}
impl<const ROWS: usize, const COLS: usize, const NROWS: usize> Default
for Frame<ROWS, COLS, NROWS>
{
fn default() -> Self {
Self::new()
}
}
#[derive(Copy, Clone, Debug)]
#[repr(C)]
struct FrameData<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
{
frames: [Frame<ROWS, COLS, NROWS>; FRAME_COUNT],
#[cfg(all(feature = "tail-closes-latch", feature = "esp32-ordering"))]
padding: Entry,
#[cfg(feature = "tail-closes-latch")]
tail: Entry,
}
impl<const ROWS: usize, const COLS: usize, const NROWS: usize, const FRAME_COUNT: usize>
FrameData<ROWS, COLS, NROWS, FRAME_COUNT>
{
const fn new() -> Self {
Self {
frames: [Frame::new(); FRAME_COUNT],
#[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
padding: Entry::new(),
#[cfg(feature = "tail-closes-latch")]
tail: Entry::new(),
}
}
}
#[derive(Copy, Clone)]
#[repr(C)]
pub struct DmaFrameBuffer<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> {
_align: u64,
data: FrameData<ROWS, COLS, NROWS, FRAME_COUNT>,
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> Default for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
fn default() -> Self {
Self::new()
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
#[must_use]
pub const fn new() -> Self {
assert!(BITS <= 8);
#[cfg(feature = "esp32-ordering")]
assert!(
COLS % 2 == 0,
"esp32-ordering feature requires COLS to be even"
);
let mut instance = Self {
_align: 0,
data: FrameData::new(),
};
instance.format();
instance
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_count() -> usize {
1
}
#[must_use]
#[deprecated(since = "0.11.0", note = "use BCM_SEQUENCE instead")]
pub const fn bcm_chunk_bytes() -> usize {
core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
}
#[inline]
pub const fn format(&mut self) {
let mut i = 0;
while i < FRAME_COUNT {
self.data.frames[i].format();
i += 1;
}
#[cfg(feature = "tail-closes-latch")]
{
self.data.tail.0 = ADDR_MASK | OE_BLANK;
}
#[cfg(all(feature = "esp32-ordering", feature = "tail-closes-latch"))]
{
self.data.padding.0 = ADDR_MASK | OE_BLANK;
}
}
#[inline]
pub fn erase(&mut self) {
for frame in &mut self.data.frames {
frame.clear_colors();
}
}
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 frames_on(v: u8) -> usize {
(v as usize) >> (8 - BITS)
}
#[inline]
fn set_pixel_internal(&mut self, x: usize, y: usize, color: Color) {
if x >= COLS || y >= ROWS {
return;
}
#[cfg(feature = "skip-black-pixels")]
if color == Color::BLACK {
return;
}
let red_frames = Self::frames_on(color.r());
let green_frames = Self::frames_on(color.g());
let blue_frames = Self::frames_on(color.b());
for (frame_idx, frame) in self.data.frames.iter_mut().enumerate() {
frame.set_pixel(
y,
x,
frame_idx < red_frames,
frame_idx < green_frames,
frame_idx < blue_frames,
);
}
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> FrameBufferOperations for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
#[inline]
fn erase(&mut self) {
DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::erase(self);
}
#[inline]
fn set_pixel(&mut self, p: Point, color: Color) {
DmaFrameBuffer::<ROWS, COLS, NROWS, BITS, FRAME_COUNT>::set_pixel(self, p, color);
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> embedded_graphics::prelude::OriginDimensions
for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
fn size(&self) -> embedded_graphics::prelude::Size {
embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> embedded_graphics::prelude::OriginDimensions
for &mut DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
fn size(&self) -> embedded_graphics::prelude::Size {
embedded_graphics::prelude::Size::new(COLS as u32, ROWS as u32)
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> embedded_graphics::draw_target::DrawTarget
for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
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(())
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> core::fmt::Debug for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
let brightness_step = 1 << (8 - BITS);
let size = core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>();
f.debug_struct("DmaFrameBuffer")
.field("size", &size)
.field("frame_count", &self.data.frames.len())
.field("frame_size", &core::mem::size_of_val(&self.data.frames[0]))
.field("brightness_step", &&brightness_step)
.finish_non_exhaustive()
}
}
#[cfg(feature = "defmt")]
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> defmt::Format for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
fn format(&self, f: defmt::Formatter) {
let brightness_step = 1 << (8 - BITS);
defmt::write!(
f,
"DmaFrameBuffer<{}, {}, {}, {}, {}>",
ROWS,
COLS,
NROWS,
BITS,
FRAME_COUNT
);
defmt::write!(
f,
" size: {}",
core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>()
);
defmt::write!(
f,
" frame_size: {}",
core::mem::size_of_val(&self.data.frames[0])
);
defmt::write!(f, " brightness_step: {}", brightness_step);
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> FrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
type Word = u16;
const BCM_SEQUENCE: [BcmLenReps; BCM_SEQUENCE_CAPACITY] = {
let mut seq = [BcmLenReps::ZERO; BCM_SEQUENCE_CAPACITY];
seq[0] = BcmLenReps {
len: core::mem::size_of::<FrameData<ROWS, COLS, NROWS, FRAME_COUNT>>(),
reps: 1,
};
seq
};
const BCM_SEQUENCE_LEN: usize = 1;
const BCM_SEQUENCE_COUNT: usize = 1;
fn bcm_segment_ptr(&self, index: usize) -> *const u8 {
assert!(index == 0, "threshold DmaFrameBuffer has only 1 segment");
(&raw const self.data).cast::<u8>()
}
}
impl<
const ROWS: usize,
const COLS: usize,
const NROWS: usize,
const BITS: u8,
const FRAME_COUNT: usize,
> MutableFrameBuffer for DmaFrameBuffer<ROWS, COLS, NROWS, BITS, FRAME_COUNT>
{
}
#[cfg(test)]
mod tests {
extern crate std;
use std::format;
use std::vec;
use super::*;
use embedded_graphics::pixelcolor::RgbColor;
use embedded_graphics::prelude::*;
use embedded_graphics::primitives::{Circle, PrimitiveStyle, Rectangle};
const TEST_ROWS: usize = 32;
const TEST_COLS: usize = if LEAD_BLANK_DELAY + TRAIL_BLANK_DELAY + 2 > 64 {
128
} else {
64
};
const TEST_NROWS: usize = TEST_ROWS / 2;
const TEST_BITS: u8 = 3;
const TEST_FRAME_COUNT: usize = (1 << TEST_BITS) - 1;
type TestFrameBuffer =
DmaFrameBuffer<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_BITS, TEST_FRAME_COUNT>;
fn get_mapped_index(index: usize) -> usize {
map_index(index)
}
fn get_mapped_row(row_idx: usize) -> usize {
map_row_index::<TEST_NROWS>(row_idx)
}
#[test]
fn test_entry_construction() {
let entry = Entry::new();
let expect_oe = cfg!(feature = "invert-oe");
assert_eq!(entry.dummy2(), false);
assert_eq!(entry.blu2(), false);
assert_eq!(entry.grn2(), false);
assert_eq!(entry.red2(), false);
assert_eq!(entry.blu1(), false);
assert_eq!(entry.grn1(), false);
assert_eq!(entry.red1(), false);
assert_eq!(entry.output_enable(), expect_oe);
assert_eq!(entry.dummy1(), false);
assert_eq!(entry.dummy0(), false);
assert_eq!(entry.latch(), false);
assert_eq!(entry.addr(), 0);
}
#[test]
fn test_entry_setters() {
let mut entry = Entry::new();
entry.set_dummy2(true);
assert_eq!(entry.dummy2(), true);
entry.set_blu2(true);
assert_eq!(entry.blu2(), true);
entry.set_grn2(true);
assert_eq!(entry.grn2(), true);
entry.set_red2(true);
assert_eq!(entry.red2(), true);
entry.set_blu1(true);
assert_eq!(entry.blu1(), true);
entry.set_grn1(true);
assert_eq!(entry.grn1(), true);
entry.set_red1(true);
assert_eq!(entry.red1(), true);
assert_eq!(entry.0 & COLOR0_MASK, COLOR0_MASK);
entry.set_output_enable(true);
assert_eq!(entry.output_enable(), true);
assert_eq!(entry.0 & OE_BIT, OE_BIT);
entry.set_dummy1(true);
assert_eq!(entry.dummy1(), true);
entry.set_dummy0(true);
assert_eq!(entry.dummy0(), true);
entry.set_latch(true);
assert_eq!(entry.latch(), true);
assert_eq!(entry.0 & LATCH_BIT, LATCH_BIT);
entry.set_addr(0b11111);
assert_eq!(entry.addr(), 0b11111);
assert_eq!(entry.0 & (0x1f_u16 << ADDR_SHIFT), 0x1f_u16 << ADDR_SHIFT);
}
#[test]
fn test_entry_bit_isolation() {
let mut entry = Entry::new();
let expect_oe = cfg!(feature = "invert-oe");
entry.set_addr(0b11111);
entry.set_latch(true);
assert_eq!(entry.addr(), 0b11111);
assert_eq!(entry.latch(), true);
assert_eq!(entry.output_enable(), expect_oe);
assert_eq!(entry.red1(), false);
entry.set_red1(true);
entry.set_grn2(true);
assert_eq!(entry.addr(), 0b11111);
assert_eq!(entry.latch(), true);
assert_eq!(entry.red1(), true);
assert_eq!(entry.grn2(), true);
assert_eq!(entry.blu1(), false);
assert_eq!(entry.red2(), false);
}
#[test]
fn test_entry_set_color0() {
let mut entry = Entry::new();
let bits = (u8::from(true) << 2) | (u8::from(false) << 1) | u8::from(true); entry.set_color0_bits(bits);
assert_eq!(entry.red1(), true);
assert_eq!(entry.grn1(), false);
assert_eq!(entry.blu1(), true);
assert_eq!(entry.0 & COLOR0_MASK, u16::from(bits) << COLOR0_SHIFT);
}
#[test]
fn test_entry_set_color1() {
let mut entry = Entry::new();
let bits = (u8::from(true) << 2) | (u8::from(true) << 1) | u8::from(false); entry.set_color1_bits(bits);
assert_eq!(entry.red2(), false);
assert_eq!(entry.grn2(), true);
assert_eq!(entry.blu2(), true);
assert_eq!(entry.0 & COLOR1_MASK, u16::from(bits) << COLOR1_SHIFT);
}
#[cfg(feature = "interstate75")]
#[test]
fn interstate75_word_layout_matches_pinout() {
let blanked = Entry::new();
assert_eq!(blanked.0, OE_BLANK);
let mut e = Entry::new();
e.set_red1(true); e.set_blu1(true); e.set_grn2(true); e.set_addr(0b1_0101); e.set_latch(true);
let expected = 0b1u16 | (0b1 << 2) | (0b1 << 4) | (0b1_0101 << 6) | (1 << 12) | OE_BLANK;
assert_eq!(e.0, expected);
assert_eq!(e.0 & (1 << 11), 0, "unused bit 11 must stay 0");
assert_eq!(e.0 & (0b11 << 14), 0, "bits 14/15 must stay 0");
}
#[test]
fn test_entry_debug_formatting() {
let entry = Entry(0x1234);
let debug_str = format!("{:?}", entry);
assert_eq!(debug_str, "Entry(0x1234)");
let entry = Entry(0xabcd);
let debug_str = format!("{:?}", entry);
assert_eq!(debug_str, "Entry(0xabcd)");
}
#[test]
fn test_row_construction() {
let row: Row<TEST_COLS> = Row::new();
assert_eq!(row.data.len(), TEST_COLS);
let expected = Entry::new().0;
for entry in &row.data {
assert_eq!(entry.0, expected);
}
}
#[test]
fn test_row_format() {
let mut row: Row<TEST_COLS> = Row::new();
let prev_addr = 4;
row.format(prev_addr);
let oe_active = !cfg!(feature = "invert-oe");
for (physical_i, entry) in row.data.iter().enumerate() {
let logical_i = get_mapped_index(physical_i);
assert_eq!(entry.addr(), u16::from(prev_addr));
if logical_i == TEST_COLS - 1 {
assert_eq!(entry.latch(), true);
assert_eq!(entry.output_enable(), !oe_active);
} else {
assert_eq!(entry.latch(), false);
if logical_i >= TRAIL_BLANK_DELAY && logical_i < TEST_COLS - LEAD_BLANK_DELAY - 1 {
assert_eq!(entry.output_enable(), oe_active);
} else {
assert_eq!(entry.output_enable(), !oe_active);
}
}
}
}
#[test]
fn test_row_set_color0() {
let mut row: Row<TEST_COLS> = Row::new();
row.set_color0(0, true, false, true);
let mapped_col_0 = get_mapped_index(0);
assert_eq!(row.data[mapped_col_0].red1(), true);
assert_eq!(row.data[mapped_col_0].grn1(), false);
assert_eq!(row.data[mapped_col_0].blu1(), true);
row.set_color0(1, false, true, false);
let mapped_col_1 = get_mapped_index(1);
assert_eq!(row.data[mapped_col_1].red1(), false);
assert_eq!(row.data[mapped_col_1].grn1(), true);
assert_eq!(row.data[mapped_col_1].blu1(), false);
}
#[test]
fn test_row_set_color1() {
let mut row: Row<TEST_COLS> = Row::new();
row.set_color1(0, true, true, false);
let mapped_col_0 = get_mapped_index(0);
assert_eq!(row.data[mapped_col_0].red2(), true);
assert_eq!(row.data[mapped_col_0].grn2(), true);
assert_eq!(row.data[mapped_col_0].blu2(), false);
}
#[test]
fn test_row_default() {
let row1: Row<TEST_COLS> = Row::new();
let row2: Row<TEST_COLS> = Row::default();
assert_eq!(row1, row2);
assert_eq!(row1.data.len(), row2.data.len());
let expected = Entry::new().0;
for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
assert_eq!(entry1.0, entry2.0);
assert_eq!(entry1.0, expected);
}
}
#[test]
fn test_frame_construction() {
let frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
assert_eq!(frame.rows.len(), TEST_NROWS);
}
#[test]
fn test_frame_format() {
let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
frame.format();
for slot in 0..TEST_NROWS {
let (prev_addr, _) = slot_addresses::<TEST_NROWS>(slot);
let row = &frame.rows[slot];
let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
assert_eq!(row.data[last_pixel_idx].addr(), u16::from(prev_addr));
assert_eq!(row.data[last_pixel_idx].latch(), true);
let first_pixel_idx = get_mapped_index(0);
assert_eq!(row.data[first_pixel_idx].addr(), u16::from(prev_addr));
assert_eq!(row.data[first_pixel_idx].latch(), false);
}
}
#[test]
fn test_frame_set_pixel() {
let mut frame: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
frame.set_pixel(5, 10, true, false, true);
let mapped_col_10 = get_mapped_index(10);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
true
);
frame.set_pixel(TEST_NROWS + 5, 15, false, true, false);
let mapped_col_15 = get_mapped_index(15);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_15].red2(),
false
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_15].grn2(),
true
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_15].blu2(),
false
);
}
#[test]
#[cfg(feature = "reverse-row-order")]
fn test_reverse_row_order() {
let mut fb = TestFrameBuffer::new();
let first = &fb.data.frames[0].rows[0];
assert_eq!(first.data[get_mapped_index(0)].addr(), 0);
let last = &fb.data.frames[0].rows[TEST_NROWS - 1];
assert_eq!(last.data[get_mapped_index(0)].addr(), 1);
fb.set_pixel(Point::new(3, 0), Color::RED);
let col3 = get_mapped_index(3);
assert!(fb.data.frames[0].rows[TEST_NROWS - 1].data[col3].red1());
assert!(!fb.data.frames[0].rows[0].data[col3].red1());
}
#[test]
fn test_frame_default() {
let frame1: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::new();
let frame2: Frame<TEST_ROWS, TEST_COLS, TEST_NROWS> = Frame::default();
assert_eq!(frame1.rows.len(), frame2.rows.len());
let expected = Entry::new().0;
for (row1, row2) in frame1.rows.iter().zip(frame2.rows.iter()) {
assert_eq!(row1, row2);
for (entry1, entry2) in row1.data.iter().zip(row2.data.iter()) {
assert_eq!(entry1.0, entry2.0);
assert_eq!(entry1.0, expected);
}
}
}
#[test]
fn test_dma_framebuffer_construction() {
let fb = TestFrameBuffer::new();
assert_eq!(fb.data.frames.len(), TEST_FRAME_COUNT);
assert_eq!(fb._align, 0);
}
#[test]
#[allow(deprecated)]
fn test_bcm_chunk_info() {
let expected_size =
core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
assert_eq!(TestFrameBuffer::bcm_chunk_bytes(), expected_size);
assert_eq!(TestFrameBuffer::bcm_chunk_count(), 1);
}
#[test]
fn test_dma_framebuffer_erase() {
let fb = TestFrameBuffer::new();
for frame in &fb.data.frames {
for slot in 0..TEST_NROWS {
let (prev_addr, _) = slot_addresses::<TEST_NROWS>(slot);
let row = &frame.rows[slot];
let last_pixel_idx = get_mapped_index(TEST_COLS - 1);
assert_eq!(row.data[last_pixel_idx].addr(), u16::from(prev_addr));
assert_eq!(row.data[last_pixel_idx].latch(), true);
let first_pixel_idx = get_mapped_index(0);
assert_eq!(row.data[first_pixel_idx].addr(), u16::from(prev_addr));
assert_eq!(row.data[first_pixel_idx].latch(), false);
}
}
}
#[test]
fn test_dma_framebuffer_set_pixel_bounds() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel(Point::new(-1, 5), Color::RED);
fb.set_pixel(Point::new(5, -1), Color::RED);
fb.set_pixel(Point::new(TEST_COLS as i32, 5), Color::RED);
fb.set_pixel(Point::new(5, TEST_ROWS as i32), Color::RED);
}
#[test]
fn test_dma_framebuffer_set_pixel_internal() {
let mut fb = TestFrameBuffer::new();
let red_color = Color::RED;
fb.set_pixel_internal(10, 5, red_color);
for frame in &fb.data.frames {
let mapped_col_10 = get_mapped_index(10);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
}
}
#[test]
fn test_dma_framebuffer_brightness_modulation() {
let mut fb = TestFrameBuffer::new();
let brightness_step = 1 << (8 - TEST_BITS); let test_brightness = brightness_step * 3; let color = Color::from(embedded_graphics::pixelcolor::Rgb888::new(
test_brightness,
0,
0,
));
fb.set_pixel_internal(0, 0, color);
for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
let should_be_active = test_brightness >= frame_threshold;
let mapped_col_0 = get_mapped_index(0);
assert_eq!(
frame.rows[get_mapped_row(0)].data[mapped_col_0].red1(),
should_be_active
);
}
}
#[test]
fn test_origin_dimensions() {
let fb = TestFrameBuffer::new();
let size = fb.size();
assert_eq!(size.width, TEST_COLS as u32);
assert_eq!(size.height, TEST_ROWS as u32);
let size = (&fb).size();
assert_eq!(size.width, TEST_COLS as u32);
assert_eq!(size.height, TEST_ROWS as u32);
let mut fb = TestFrameBuffer::new();
let fb_ref = &mut fb;
let size = fb_ref.size();
assert_eq!(size.width, TEST_COLS as u32);
assert_eq!(size.height, TEST_ROWS as u32);
}
#[test]
fn test_draw_target() {
let mut fb = TestFrameBuffer::new();
let pixels = vec![
embedded_graphics::Pixel(Point::new(0, 0), Color::RED),
embedded_graphics::Pixel(Point::new(1, 1), Color::GREEN),
embedded_graphics::Pixel(Point::new(2, 2), Color::BLUE),
];
let result = fb.draw_iter(pixels);
assert!(result.is_ok());
let result = (&mut fb).draw_iter(vec![embedded_graphics::Pixel(
Point::new(3, 3),
Color::WHITE,
)]);
assert!(result.is_ok());
}
#[test]
fn test_draw_iter_pixel_verification() {
let mut fb = TestFrameBuffer::new();
let pixels = vec![
embedded_graphics::Pixel(Point::new(5, 2), Color::RED), embedded_graphics::Pixel(Point::new(10, 5), Color::GREEN), embedded_graphics::Pixel(Point::new(15, 8), Color::BLUE), embedded_graphics::Pixel(Point::new(20, 10), Color::WHITE), embedded_graphics::Pixel(Point::new(25, (TEST_NROWS + 3) as i32), Color::RED), embedded_graphics::Pixel(Point::new(30, (TEST_NROWS + 7) as i32), Color::GREEN), embedded_graphics::Pixel(Point::new(35, (TEST_NROWS + 12) as i32), Color::BLUE), embedded_graphics::Pixel(Point::new(40, 1), Color::BLACK), ];
let result = fb.draw_iter(pixels);
assert!(result.is_ok());
let first_frame = &fb.data.frames[0];
let brightness_step = 1 << (8 - TEST_BITS); let first_frame_threshold = brightness_step;
let col_idx = get_mapped_index(5);
assert_eq!(
first_frame.rows[get_mapped_row(2)].data[col_idx].red1(),
Color::RED.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(2)].data[col_idx].grn1(),
Color::RED.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(2)].data[col_idx].blu1(),
Color::RED.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(10);
assert_eq!(
first_frame.rows[get_mapped_row(5)].data[col_idx].red1(),
Color::GREEN.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(5)].data[col_idx].grn1(),
Color::GREEN.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(5)].data[col_idx].blu1(),
Color::GREEN.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(15);
assert_eq!(
first_frame.rows[get_mapped_row(8)].data[col_idx].red1(),
Color::BLUE.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(8)].data[col_idx].grn1(),
Color::BLUE.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(8)].data[col_idx].blu1(),
Color::BLUE.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(20);
assert_eq!(
first_frame.rows[get_mapped_row(10)].data[col_idx].red1(),
Color::WHITE.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(10)].data[col_idx].grn1(),
Color::WHITE.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(10)].data[col_idx].blu1(),
Color::WHITE.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(25);
assert_eq!(
first_frame.rows[get_mapped_row(3)].data[col_idx].red2(),
Color::RED.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(3)].data[col_idx].grn2(),
Color::RED.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(3)].data[col_idx].blu2(),
Color::RED.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(30);
assert_eq!(
first_frame.rows[get_mapped_row(7)].data[col_idx].red2(),
Color::GREEN.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(7)].data[col_idx].grn2(),
Color::GREEN.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(7)].data[col_idx].blu2(),
Color::GREEN.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(35);
assert_eq!(
first_frame.rows[get_mapped_row(12)].data[col_idx].red2(),
Color::BLUE.r() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(12)].data[col_idx].grn2(),
Color::BLUE.g() >= first_frame_threshold
);
assert_eq!(
first_frame.rows[get_mapped_row(12)].data[col_idx].blu2(),
Color::BLUE.b() >= first_frame_threshold
);
let col_idx = get_mapped_index(40);
assert_eq!(
first_frame.rows[get_mapped_row(1)].data[col_idx].red1(),
false
);
assert_eq!(
first_frame.rows[get_mapped_row(1)].data[col_idx].grn1(),
false
);
assert_eq!(
first_frame.rows[get_mapped_row(1)].data[col_idx].blu1(),
false
);
}
#[test]
fn test_embedded_graphics_integration() {
let mut fb = TestFrameBuffer::new();
let result = Rectangle::new(Point::new(5, 5), Size::new(10, 8))
.into_styled(PrimitiveStyle::with_fill(Color::RED))
.draw(&mut fb);
assert!(result.is_ok());
let result = Circle::new(Point::new(30, 15), 8)
.into_styled(PrimitiveStyle::with_fill(Color::BLUE))
.draw(&mut fb);
assert!(result.is_ok());
}
#[test]
#[cfg(feature = "skip-black-pixels")]
fn test_skip_black_pixels_enabled() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = get_mapped_index(10);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
fb.set_pixel_internal(10, 5, Color::BLACK);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
}
#[test]
#[cfg(not(feature = "skip-black-pixels"))]
fn test_skip_black_pixels_disabled() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
let mapped_col_10 = get_mapped_index(10);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
fb.set_pixel_internal(10, 5, Color::BLACK);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
}
#[test]
fn test_bcm_frame_overwrite() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel_internal(10, 5, Color::WHITE);
let mapped_col_10 = get_mapped_index(10);
for frame in fb.data.frames.iter() {
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
true
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
true
);
}
let half_white = Color::from(embedded_graphics::pixelcolor::Rgb888::new(128, 128, 128));
fb.set_pixel_internal(10, 5, half_white);
let brightness_step = 1 << (8 - TEST_BITS); for (frame_idx, frame) in fb.data.frames.iter().enumerate() {
let frame_threshold = (frame_idx as u8 + 1) * brightness_step;
let should_be_active = 128 >= frame_threshold;
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].red1(),
should_be_active
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
should_be_active
);
assert_eq!(
frame.rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
should_be_active
);
}
for frame_idx in 0..4 {
assert_eq!(
fb.data.frames[frame_idx].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
}
for frame_idx in 4..TEST_FRAME_COUNT {
assert_eq!(
fb.data.frames[frame_idx].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
false
);
}
}
#[test]
fn test_debug_formatting() {
let fb = TestFrameBuffer::new();
let debug_string = format!("{:?}", fb);
assert!(debug_string.contains("DmaFrameBuffer"));
assert!(debug_string.contains("frame_count"));
assert!(debug_string.contains("frame_size"));
assert!(debug_string.contains("brightness_step"));
}
#[test]
fn test_default_implementation() {
let fb1 = TestFrameBuffer::new();
let fb2 = TestFrameBuffer::default();
assert_eq!(fb1.data.frames.len(), fb2.data.frames.len());
assert_eq!(fb1._align, fb2._align);
}
#[test]
fn test_memory_alignment() {
let fb = TestFrameBuffer::new();
let ptr = &fb as *const _ as usize;
assert_eq!(ptr % 8, 0);
}
#[test]
fn test_color_values() {
let mut fb = TestFrameBuffer::new();
let colors = [
(Color::RED, (255, 0, 0)),
(Color::GREEN, (0, 255, 0)),
(Color::BLUE, (0, 0, 255)),
(Color::WHITE, (255, 255, 255)),
(Color::BLACK, (0, 0, 0)),
];
for (i, (color, (r, g, b))) in colors.iter().enumerate() {
fb.set_pixel(Point::new(i as i32, 0), *color);
assert_eq!(color.r(), *r);
assert_eq!(color.g(), *g);
assert_eq!(color.b(), *b);
}
}
#[test]
fn test_blanking_delay() {
let mut row: Row<TEST_COLS> = Row::new();
let prev_addr = 4;
row.format(prev_addr);
let oe_active = !cfg!(feature = "invert-oe");
if TRAIL_BLANK_DELAY > 0 {
let trail_blank_idx = get_mapped_index(TRAIL_BLANK_DELAY - 1);
assert_eq!(row.data[trail_blank_idx].output_enable(), !oe_active);
}
let after_trail_idx = get_mapped_index(TRAIL_BLANK_DELAY);
assert_eq!(row.data[after_trail_idx].output_enable(), oe_active);
let before_lead_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 2);
assert_eq!(row.data[before_lead_idx].output_enable(), oe_active);
let lead_blank_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
assert_eq!(row.data[lead_blank_idx].output_enable(), !oe_active);
}
#[test]
fn test_esp32_mapping() {
#[cfg(feature = "esp32-ordering")]
{
assert_eq!(map_index(0), 1);
assert_eq!(map_index(1), 0);
assert_eq!(map_index(2), 3);
assert_eq!(map_index(3), 2);
assert_eq!(map_index(4), 5);
assert_eq!(map_index(5), 4);
}
#[cfg(not(feature = "esp32-ordering"))]
{
assert_eq!(map_index(0), 0);
assert_eq!(map_index(1), 1);
assert_eq!(map_index(2), 2);
assert_eq!(map_index(3), 3);
}
}
#[test]
fn test_bits_assertion() {
assert!(TEST_BITS <= 8);
}
#[test]
fn test_fast_clear_method() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
fb.set_pixel_internal(20, 10, Color::GREEN);
let mapped_col_10 = get_mapped_index(10);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
true
);
fb.erase();
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].red1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].grn1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mapped_col_10].blu1(),
false
);
let last_col = get_mapped_index(TEST_COLS - 1);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[last_col].latch(),
true
);
}
const CHAR_W: i32 = 6;
const CHAR_H: i32 = 10;
fn verify_glyph_at(fb: &mut TestFrameBuffer, origin: Point) {
use embedded_graphics::mock_display::MockDisplay;
use embedded_graphics::mono_font::ascii::FONT_6X10;
use embedded_graphics::mono_font::MonoTextStyle;
use embedded_graphics::text::{Baseline, Text};
let style = MonoTextStyle::new(&FONT_6X10, Color::WHITE);
Text::with_baseline("A", origin, style, Baseline::Top)
.draw(fb)
.unwrap();
let mut reference: MockDisplay<Color> = MockDisplay::new();
Text::with_baseline("A", Point::zero(), style, Baseline::Top)
.draw(&mut reference)
.unwrap();
for dy in 0..CHAR_H {
for dx in 0..CHAR_W {
let expected_on = reference
.get_pixel(Point::new(dx, dy))
.unwrap_or(Color::BLACK)
!= Color::BLACK;
let gx = (origin.x + dx) as usize;
let gy = (origin.y + dy) as usize;
let frame0 = &fb.data.frames[0];
let e = if gy < TEST_NROWS {
&frame0.rows[get_mapped_row(gy)].data[get_mapped_index(gx)]
} else {
&frame0.rows[get_mapped_row(gy - TEST_NROWS)].data[get_mapped_index(gx)]
};
let (r, g, b) = if gy >= TEST_NROWS {
(e.red2(), e.grn2(), e.blu2())
} else {
(e.red1(), e.grn1(), e.blu1())
};
if expected_on {
assert!(r && g && b,);
} else {
assert!(!r && !g && !b);
}
}
}
}
#[test]
fn test_draw_char_corners() {
let upper_left = Point::new(0, 0);
let lower_right = Point::new(TEST_COLS as i32 - CHAR_W, TEST_ROWS as i32 - CHAR_H);
let mut fb = TestFrameBuffer::new();
verify_glyph_at(&mut fb, upper_left);
verify_glyph_at(&mut fb, lower_right);
}
#[test]
fn test_framebuffer_operations_trait_erase() {
let mut fb = TestFrameBuffer::new();
fb.set_pixel_internal(10, 5, Color::RED);
fb.set_pixel_internal(20, 10, Color::GREEN);
<TestFrameBuffer as FrameBufferOperations>::erase(&mut fb);
let mc10 = get_mapped_index(10);
let mc20 = get_mapped_index(20);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(5)].data[mc10].red1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(10)].data[mc20].grn1(),
false
);
let last_col = get_mapped_index(TEST_COLS - 1);
assert!(fb.data.frames[0].rows[get_mapped_row(0)].data[last_col].latch());
}
#[test]
fn test_framebuffer_operations_trait_set_pixel() {
let mut fb = TestFrameBuffer::new();
<TestFrameBuffer as FrameBufferOperations>::set_pixel(
&mut fb,
Point::new(8, 3),
Color::BLUE,
);
let idx = get_mapped_index(8);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(3)].data[idx].blu1(),
true
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(3)].data[idx].red1(),
false
);
assert_eq!(
fb.data.frames[0].rows[get_mapped_row(3)].data[idx].grn1(),
false
);
}
#[test]
fn test_entry_new_oe_matches_feature() {
let entry = Entry::new();
if cfg!(feature = "invert-oe") {
assert!(entry.output_enable());
} else {
assert!(!entry.output_enable());
}
}
#[test]
fn test_make_data_template_oe_polarity() {
let mut row = Row::<TEST_COLS>::new();
row.format(4);
let active_idx = get_mapped_index(TRAIL_BLANK_DELAY);
let blank_idx = get_mapped_index(TEST_COLS - LEAD_BLANK_DELAY - 1);
let latch_idx = get_mapped_index(TEST_COLS - 1);
if cfg!(feature = "invert-oe") {
assert!(!row.data[active_idx].output_enable());
assert!(row.data[blank_idx].output_enable());
assert!(row.data[latch_idx].output_enable());
} else {
assert!(row.data[active_idx].output_enable());
assert!(!row.data[blank_idx].output_enable());
assert!(!row.data[latch_idx].output_enable());
}
}
static STATIC_FB: TestFrameBuffer = TestFrameBuffer::new();
#[test]
fn test_static_construction_is_formatted() {
let runtime_fb = TestFrameBuffer::new();
for (fi, frame) in STATIC_FB.data.frames.iter().enumerate() {
for (ri, row) in frame.rows.iter().enumerate() {
assert_eq!(
row.data, runtime_fb.data.frames[fi].rows[ri].data,
"static vs runtime mismatch at frame {fi}, row {ri}"
);
assert_eq!(
row.gap, runtime_fb.data.frames[fi].rows[ri].gap,
"static vs runtime gap mismatch at frame {fi}, row {ri}"
);
}
}
}
#[test]
fn test_format_reinitializes_at_runtime() {
let mut fb = TestFrameBuffer::new();
fb.erase();
fb.format();
for (fi, frame) in fb.data.frames.iter().enumerate() {
for (ri, row) in frame.rows.iter().enumerate() {
assert_eq!(
row.data, STATIC_FB.data.frames[fi].rows[ri].data,
"re-formatted vs static mismatch at frame {fi}, row {ri}"
);
}
}
}
#[test]
fn bcm_segment_count_is_one() {
use crate::FrameBuffer;
let fb = TestFrameBuffer::new();
assert_eq!(fb.bcm_segment_count(), 1);
}
#[test]
fn bcm_segment_covers_entire_buffer() {
use crate::FrameBuffer;
let fb = TestFrameBuffer::new();
let seg = fb.bcm_segment(0);
let expected_ptr = (&raw const fb.data).cast::<u8>();
let expected_len =
core::mem::size_of::<FrameData<TEST_ROWS, TEST_COLS, TEST_NROWS, TEST_FRAME_COUNT>>();
assert_eq!(seg.ptr, expected_ptr);
assert_eq!(seg.len, expected_len);
assert_eq!(seg.reps, 1);
}
#[test]
#[should_panic(expected = "out of range")]
fn bcm_segment_panics_for_invalid_index() {
use crate::FrameBuffer;
let fb = TestFrameBuffer::new();
let _ = fb.bcm_segment(1);
}
#[test]
fn bcm_sequence_matches_runtime_segments() {
use crate::FrameBuffer;
let fb = TestFrameBuffer::new();
assert_eq!(
TestFrameBuffer::BCM_SEGMENT_COUNT,
TestFrameBuffer::BCM_SEQUENCE_LEN * TestFrameBuffer::BCM_SEQUENCE_COUNT
);
assert_eq!(fb.bcm_segment_count(), TestFrameBuffer::BCM_SEGMENT_COUNT);
for i in 0..TestFrameBuffer::BCM_SEGMENT_COUNT {
let entry = TestFrameBuffer::BCM_SEQUENCE[i % TestFrameBuffer::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 &TestFrameBuffer::BCM_SEQUENCE[TestFrameBuffer::BCM_SEQUENCE_LEN..] {
assert_eq!(*entry, crate::BcmLenReps::ZERO, "padding must be zero");
}
}
}