use drmkit_core::Device;
use drmkit_dumb::{Buffer, DumbError};
use drmkit_fmt::fourcc;
use drmkit_planes::PlaneCapabilities;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[allow(
clippy::exhaustive_structs,
reason = "a value defined by its fields; there is nothing to add to it"
)]
pub struct CompositeRect {
pub x: i32,
pub y: i32,
pub w: u32,
pub h: u32,
}
#[derive(Debug, Clone, Copy)]
#[allow(
clippy::exhaustive_structs,
reason = "built by callers, with no default to fill a new field from"
)]
pub struct CompositeSrc<'a> {
pub pixels: &'a [u8],
pub src_stride_bytes: u32,
pub src_width: u32,
pub src_height: u32,
pub drm_fourcc: u32,
pub plane_alpha: u16,
}
#[must_use]
pub const fn format_supported(drm_fourcc: u32) -> bool {
matches!(drm_fourcc, fourcc::ARGB8888 | fourcc::XRGB8888)
}
const fn source_is_opaque(drm_fourcc: u32) -> bool {
drm_fourcc == fourcc::XRGB8888
}
fn blend_pixel_over(src: u32, dst: u32) -> u32 {
let sa = (src >> 24) & 0xFF;
if sa == 0 {
return dst;
}
if sa == 0xFF {
return src;
}
let inv = 255 - sa;
let scale = |c: u32| (c * inv + 127) / 255;
let out_r = ((src >> 16) & 0xFF) + scale((dst >> 16) & 0xFF);
let out_g = ((src >> 8) & 0xFF) + scale((dst >> 8) & 0xFF);
let out_b = (src & 0xFF) + scale(dst & 0xFF);
let out_a = sa + scale((dst >> 24) & 0xFF);
let clamp = |c: u32| if c > 0xFF { 0xFF } else { c };
(clamp(out_a) << 24) | (clamp(out_r) << 16) | (clamp(out_g) << 8) | clamp(out_b)
}
fn modulate_pixel(pixel: u32, alpha8: u32) -> u32 {
let scale = |c: u32| (c * alpha8 + 127) / 255;
(scale((pixel >> 24) & 0xFF) << 24)
| (scale((pixel >> 16) & 0xFF) << 16)
| (scale((pixel >> 8) & 0xFF) << 8)
| scale(pixel & 0xFF)
}
const fn plane_alpha8(plane_alpha: u16) -> u32 {
(plane_alpha as u32 * 255 + 32767) / 65535
}
#[derive(Debug, Clone, Copy)]
struct AxisRange {
dst_start: u32,
dst_end: u32,
src_start: u32,
src_span: u32,
}
fn clip_axis(
dst_pos: i32,
dst_size: u32,
dst_extent: u32,
src_pos: i32,
src_size: u32,
src_extent: u32,
) -> Option<AxisRange> {
if dst_size == 0 || src_size == 0 {
return None;
}
let clip = |pos: i32, size: u32, extent: u32| -> Option<(u32, u32)> {
let clipped_off = if pos < 0 { pos.unsigned_abs() } else { 0 };
let start = pos.saturating_add(clipped_off.cast_signed());
if start >= extent.cast_signed() {
return None;
}
let start = start.cast_unsigned();
let remaining = size.checked_sub(clipped_off)?;
if remaining == 0 {
return None;
}
let visible = remaining.min(extent - start);
if visible == 0 {
None
} else {
Some((start, visible))
}
};
let (src_start, src_span) = clip(src_pos, src_size, src_extent)?;
let (dst_start, dst_visible) = clip(dst_pos, dst_size, dst_extent)?;
Some(AxisRange {
dst_start,
dst_end: dst_start + dst_visible,
src_start,
src_span,
})
}
const fn map_src_index(dst_idx: u32, dst_visible: u32, src_span: u32) -> u32 {
if dst_visible <= 1 {
return 0;
}
(dst_idx * src_span) / dst_visible
}
fn fits(len: usize, height: u32, stride: u32) -> bool {
let required = u64::from(height) * u64::from(stride);
required <= len as u64
}
pub fn clear_into(
dst: &mut [u8],
dst_stride_bytes: u32,
dst_width: u32,
dst_height: u32,
rect: CompositeRect,
) {
if dst_width == 0 || dst_height == 0 {
return;
}
if u64::from(dst_stride_bytes) < u64::from(dst_width) * 4
|| !fits(dst.len(), dst_height, dst_stride_bytes)
{
return;
}
let Some(xr) = clip_axis(rect.x, rect.w, dst_width, 0, rect.w, dst_width) else {
return;
};
let Some(yr) = clip_axis(rect.y, rect.h, dst_height, 0, rect.h, dst_height) else {
return;
};
for y in yr.dst_start..yr.dst_end {
let row = (y * dst_stride_bytes) as usize;
let from = row + (xr.dst_start * 4) as usize;
let to = row + (xr.dst_end * 4) as usize;
dst[from..to].fill(0);
}
}
pub fn blend_into(
dst: &mut [u8],
dst_stride_bytes: u32,
dst_width: u32,
dst_height: u32,
src: &CompositeSrc<'_>,
src_rect: CompositeRect,
dst_rect: CompositeRect,
) {
if dst_width == 0 || dst_height == 0 || !format_supported(src.drm_fourcc) {
return;
}
if u64::from(dst_stride_bytes) < u64::from(dst_width) * 4
|| !fits(dst.len(), dst_height, dst_stride_bytes)
{
return;
}
if u64::from(src.src_stride_bytes) < u64::from(src.src_width) * 4
|| !fits(src.pixels.len(), src.src_height, src.src_stride_bytes)
{
return;
}
let Some(xr) = clip_axis(
dst_rect.x,
dst_rect.w,
dst_width,
src_rect.x,
src_rect.w,
src.src_width,
) else {
return;
};
let Some(yr) = clip_axis(
dst_rect.y,
dst_rect.h,
dst_height,
src_rect.y,
src_rect.h,
src.src_height,
) else {
return;
};
let opaque = source_is_opaque(src.drm_fourcc);
let alpha8 = plane_alpha8(src.plane_alpha);
let modulate = alpha8 < 0xFF;
let visible_x = xr.dst_end - xr.dst_start;
let visible_y = yr.dst_end - yr.dst_start;
for dy in 0..visible_y {
let sy = yr.src_start + map_src_index(dy, visible_y, yr.src_span);
let src_row = (sy * src.src_stride_bytes) as usize;
let dst_row = ((yr.dst_start + dy) * dst_stride_bytes) as usize;
for dx in 0..visible_x {
let sx = xr.src_start + map_src_index(dx, visible_x, xr.src_span);
let s_at = src_row + (sx * 4) as usize;
let d_at = dst_row + ((xr.dst_start + dx) * 4) as usize;
let mut pixel = u32::from_ne_bytes([
src.pixels[s_at],
src.pixels[s_at + 1],
src.pixels[s_at + 2],
src.pixels[s_at + 3],
]);
if opaque {
pixel |= 0xFF00_0000;
}
if modulate {
pixel = modulate_pixel(pixel, alpha8);
}
let under =
u32::from_ne_bytes([dst[d_at], dst[d_at + 1], dst[d_at + 2], dst[d_at + 3]]);
let out = blend_pixel_over(pixel, under).to_ne_bytes();
dst[d_at..d_at + 4].copy_from_slice(&out);
}
}
}
fn copy_rows(dst: &mut [u8], dst_stride: usize, shadow: &[u8], row_bytes: usize, height: usize) {
if row_bytes > dst_stride {
return;
}
if dst.len() < dst_stride * height || shadow.len() < row_bytes * height {
return;
}
for y in 0..height {
let to = y * dst_stride;
let from = y * row_bytes;
dst[to..to + row_bytes].copy_from_slice(&shadow[from..from + row_bytes]);
}
}
#[must_use]
pub const fn canvas_output_bpp(fourcc: u32) -> u32 {
match fourcc {
fourcc::ARGB8888 | fourcc::XRGB8888 | fourcc::XBGR8888 | fourcc::ABGR8888 => 4,
fourcc::RGB565 | fourcc::BGR565 => 2,
_ => 0,
}
}
const CANVAS_FORMATS: [u32; 6] = [
fourcc::ARGB8888,
fourcc::XRGB8888,
fourcc::XBGR8888,
fourcc::ABGR8888,
fourcc::RGB565,
fourcc::BGR565,
];
#[must_use]
pub fn canvas_format_for_plane(plane: &PlaneCapabilities) -> Option<u32> {
CANVAS_FORMATS
.into_iter()
.find(|fourcc| canvas_output_bpp(*fourcc) != 0 && plane.scans_out_linear(*fourcc))
}
pub fn convert_row(dst: &mut [u8], src: &[u8], pixels: usize, out_fourcc: u32) {
match out_fourcc {
fourcc::ARGB8888 | fourcc::XRGB8888 => {
dst[..pixels * 4].copy_from_slice(&src[..pixels * 4]);
}
fourcc::XBGR8888 | fourcc::ABGR8888 => {
for i in 0..pixels {
let s = &src[i * 4..i * 4 + 4];
dst[i * 4] = s[2];
dst[i * 4 + 1] = s[1];
dst[i * 4 + 2] = s[0];
dst[i * 4 + 3] = s[3];
}
}
fourcc::RGB565 | fourcc::BGR565 => {
let swap = out_fourcc == fourcc::BGR565;
for i in 0..pixels {
let s = &src[i * 4..i * 4 + 4];
let (b, g, r) = (s[0], s[1], s[2]);
let (high, low) = if swap { (b, r) } else { (r, b) };
let packed =
(u16::from(high >> 3) << 11) | (u16::from(g >> 2) << 5) | u16::from(low >> 3);
dst[i * 2..i * 2 + 2].copy_from_slice(&packed.to_le_bytes());
}
}
_ => {}
}
}
pub struct CompositeCanvas {
buffers: [Buffer; 2],
back: usize,
shadow: Vec<u8>,
width: u32,
height: u32,
fourcc: u32,
}
impl CompositeCanvas {
pub fn create(device: &Device, width: u32, height: u32) -> Result<Self, DumbError> {
Self::create_in(device, width, height, fourcc::ARGB8888)
}
pub fn create_in(
device: &Device,
width: u32,
height: u32,
fourcc: u32,
) -> Result<Self, DumbError> {
let bpp = canvas_output_bpp(fourcc);
if bpp == 0 {
return Err(DumbError::InvalidConfig {
reason: "the canvas cannot write this format",
});
}
let config = drmkit_dumb::Config {
width,
height,
fourcc,
bpp: bpp * 8,
add_fb: true,
};
let buffers = [
Buffer::create(device, &config)?,
Buffer::create(device, &config)?,
];
Ok(Self {
buffers,
back: 0,
shadow: vec![0; (width as usize) * (height as usize) * 4],
width,
height,
fourcc,
})
}
#[must_use]
pub const fn fourcc(&self) -> u32 {
self.fourcc
}
pub const fn begin_frame(&mut self) {
self.back = 1 - self.back;
}
pub fn clear(&mut self) {
self.shadow.fill(0);
}
pub fn clear_rect(&mut self, rect: CompositeRect) {
let (w, h) = (self.width, self.height);
clear_into(&mut self.shadow, w * 4, w, h, rect);
}
pub fn blend(
&mut self,
src: &CompositeSrc<'_>,
src_rect: CompositeRect,
dst_rect: CompositeRect,
) {
let (w, h) = (self.width, self.height);
blend_into(&mut self.shadow, w * 4, w, h, src, src_rect, dst_rect);
}
pub fn flush(&mut self) {
let stride = self.buffers[self.back].stride() as usize;
let pixels = self.width as usize;
let height = self.height as usize;
let fourcc = self.fourcc;
let shadow_row = pixels * 4;
let dst = self.buffers[self.back].data_mut();
if fourcc == fourcc::ARGB8888 || fourcc == fourcc::XRGB8888 {
copy_rows(dst, stride, &self.shadow, shadow_row, height);
return;
}
for row in 0..height {
let Some(out) = dst.get_mut(row * stride..) else {
break;
};
let src = &self.shadow[row * shadow_row..][..shadow_row];
convert_row(out, src, pixels, fourcc);
}
}
#[must_use]
pub fn fb_id(&self) -> Option<u32> {
self.buffers[self.back].fb_id()
}
#[must_use]
pub const fn width(&self) -> u32 {
self.width
}
#[must_use]
pub const fn height(&self) -> u32 {
self.height
}
#[must_use]
pub fn back_pixel(&self, x: u32, y: u32) -> u32 {
if x >= self.width || y >= self.height {
return 0;
}
let stride = self.buffers[self.back].stride() as usize;
let data = self.buffers[self.back].data();
let at = y as usize * stride + x as usize * 4;
data.get(at..at + 4).map_or(0, |bytes| {
u32::from_ne_bytes([bytes[0], bytes[1], bytes[2], bytes[3]])
})
}
#[must_use]
pub fn armable(&self) -> bool {
!self.buffers[0].is_empty() && !self.buffers[1].is_empty() && self.fb_id().is_some()
}
pub fn forget(&mut self) {
for buffer in &mut self.buffers {
buffer.forget();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn canvas(w: u32, h: u32) -> Vec<u8> {
vec![0; (w * h * 4) as usize]
}
fn px(buf: &[u8], stride: u32, x: u32, y: u32) -> u32 {
let at = (y * stride + x * 4) as usize;
u32::from_ne_bytes([buf[at], buf[at + 1], buf[at + 2], buf[at + 3]])
}
fn solid(w: u32, h: u32, argb: u32) -> Vec<u8> {
argb.to_ne_bytes().repeat((w * h) as usize)
}
fn src(pixels: &[u8], w: u32, h: u32, fourcc: u32) -> CompositeSrc<'_> {
CompositeSrc {
pixels,
src_stride_bytes: w * 4,
src_width: w,
src_height: h,
drm_fourcc: fourcc,
plane_alpha: 0xFFFF,
}
}
fn whole(w: u32, h: u32) -> CompositeRect {
CompositeRect { x: 0, y: 0, w, h }
}
#[test]
fn an_opaque_source_replaces_what_was_under_it() {
let mut dst = solid(4, 4, 0xFF00_00FF);
let pixels = solid(4, 4, 0xFFFF_0000);
blend_into(
&mut dst,
16,
4,
4,
&src(&pixels, 4, 4, fourcc::ARGB8888),
whole(4, 4),
whole(4, 4),
);
assert_eq!(px(&dst, 16, 2, 2), 0xFFFF_0000);
}
#[test]
fn a_fully_transparent_source_leaves_the_destination_alone() {
let mut dst = solid(4, 4, 0xFF00_00FF);
let pixels = solid(4, 4, 0x0000_0000);
blend_into(
&mut dst,
16,
4,
4,
&src(&pixels, 4, 4, fourcc::ARGB8888),
whole(4, 4),
whole(4, 4),
);
assert_eq!(px(&dst, 16, 1, 1), 0xFF00_00FF);
}
#[test]
fn a_half_alpha_source_composites_premultiplied() {
let mut dst = solid(2, 2, 0xFF00_00FF);
let pixels = solid(2, 2, 0x8080_0000);
blend_into(
&mut dst,
8,
2,
2,
&src(&pixels, 2, 2, fourcc::ARGB8888),
whole(2, 2),
whole(2, 2),
);
assert_eq!(px(&dst, 8, 0, 0), 0xFF80_007F);
}
#[test]
fn an_xrgb_source_ignores_its_padding_byte() {
let mut dst = solid(2, 2, 0xFF00_00FF);
let pixels = solid(2, 2, 0x00FF_0000);
blend_into(
&mut dst,
8,
2,
2,
&src(&pixels, 2, 2, fourcc::XRGB8888),
whole(2, 2),
whole(2, 2),
);
assert_eq!(px(&dst, 8, 0, 0), 0xFFFF_0000);
}
#[test]
fn plane_alpha_keeps_the_pixel_premultiplied() {
let mut dst = canvas(2, 2);
let pixels = solid(2, 2, 0xFFFF_FFFF);
let mut source = src(&pixels, 2, 2, fourcc::ARGB8888);
source.plane_alpha = 0x8000;
blend_into(&mut dst, 8, 2, 2, &source, whole(2, 2), whole(2, 2));
let out = px(&dst, 8, 0, 0);
let a = (out >> 24) & 0xFF;
for shift in [16, 8, 0] {
assert!(
(out >> shift) & 0xFF <= a,
"channel at {shift} exceeds alpha {a:#x} in {out:#010x}"
);
}
}
#[test]
fn opaque_plane_alpha_is_a_no_op() {
let mut dst = canvas(2, 2);
let pixels = solid(2, 2, 0xFFAB_CDEF);
blend_into(
&mut dst,
8,
2,
2,
&src(&pixels, 2, 2, fourcc::ARGB8888),
whole(2, 2),
whole(2, 2),
);
assert_eq!(px(&dst, 8, 1, 1), 0xFFAB_CDEF);
}
#[test]
fn a_negative_destination_clips() {
let mut dst = canvas(4, 1);
let pixels = solid(4, 1, 0xFFFF_0000);
blend_into(
&mut dst,
16,
4,
1,
&src(&pixels, 4, 1, fourcc::ARGB8888),
whole(4, 1),
CompositeRect {
x: -2,
y: 0,
w: 4,
h: 1,
},
);
assert_eq!(px(&dst, 16, 0, 0), 0xFFFF_0000);
assert_eq!(px(&dst, 16, 1, 0), 0xFFFF_0000);
assert_eq!(px(&dst, 16, 2, 0), 0);
assert_eq!(px(&dst, 16, 3, 0), 0);
}
#[test]
fn a_fully_clipped_destination_writes_nothing() {
let mut dst = solid(4, 4, 0xFF00_00FF);
let before = dst.clone();
let pixels = solid(4, 4, 0xFFFF_0000);
blend_into(
&mut dst,
16,
4,
4,
&src(&pixels, 4, 4, fourcc::ARGB8888),
whole(4, 4),
CompositeRect {
x: 100,
y: 0,
w: 4,
h: 4,
},
);
assert_eq!(dst, before);
}
#[test]
fn an_unsupported_format_writes_nothing() {
let mut dst = solid(2, 2, 0xFF00_00FF);
let before = dst.clone();
let pixels = solid(2, 2, 0xFFFF_0000);
blend_into(
&mut dst,
8,
2,
2,
&src(&pixels, 2, 2, fourcc::NV12),
whole(2, 2),
whole(2, 2),
);
assert_eq!(dst, before, "an NV12 source must not be blended as ARGB");
}
#[test]
fn a_short_source_buffer_writes_nothing() {
let mut dst = solid(4, 4, 0xFF00_00FF);
let before = dst.clone();
let pixels = vec![0xFFu8; 8]; blend_into(
&mut dst,
16,
4,
4,
&src(&pixels, 4, 4, fourcc::ARGB8888),
whole(4, 4),
whole(4, 4),
);
assert_eq!(dst, before);
}
#[test]
fn a_smaller_source_scales_up() {
let mut dst = canvas(4, 1);
let mut pixels = Vec::new();
pixels.extend_from_slice(&0xFFFF_0000u32.to_ne_bytes());
pixels.extend_from_slice(&0xFF00_FF00u32.to_ne_bytes());
blend_into(
&mut dst,
16,
4,
1,
&src(&pixels, 2, 1, fourcc::ARGB8888),
whole(2, 1),
whole(4, 1),
);
assert_eq!(px(&dst, 16, 0, 0), 0xFFFF_0000);
assert_eq!(px(&dst, 16, 1, 0), 0xFFFF_0000);
assert_eq!(px(&dst, 16, 2, 0), 0xFF00_FF00);
assert_eq!(px(&dst, 16, 3, 0), 0xFF00_FF00);
}
#[test]
fn clearing_a_rect_leaves_the_rest_intact() {
let mut dst = solid(4, 2, 0xFFFF_FFFF);
clear_into(
&mut dst,
16,
4,
2,
CompositeRect {
x: 1,
y: 0,
w: 2,
h: 1,
},
);
assert_eq!(px(&dst, 16, 0, 0), 0xFFFF_FFFF);
assert_eq!(px(&dst, 16, 1, 0), 0);
assert_eq!(px(&dst, 16, 2, 0), 0);
assert_eq!(px(&dst, 16, 3, 0), 0xFFFF_FFFF);
assert_eq!(px(&dst, 16, 1, 1), 0xFFFF_FFFF, "row 1 is untouched");
}
#[test]
fn later_blends_land_on_top() {
let mut dst = canvas(2, 2);
let under = solid(2, 2, 0xFFFF_0000);
let over = solid(2, 2, 0xFF00_FF00);
for pixels in [&under, &over] {
blend_into(
&mut dst,
8,
2,
2,
&src(pixels, 2, 2, fourcc::ARGB8888),
whole(2, 2),
whole(2, 2),
);
}
assert_eq!(px(&dst, 8, 0, 0), 0xFF00_FF00);
}
}
#[cfg(test)]
mod stride_tests {
use super::copy_rows;
#[test]
fn a_padded_stride_does_not_shear_the_image() {
let row_bytes = 8; let stride = 12; let height = 3;
let shadow: Vec<u8> = (0..u8::try_from(row_bytes * height).expect("fits")).collect();
let mut dst = vec![0xEE; stride * height];
copy_rows(&mut dst, stride, &shadow, row_bytes, height);
for y in 0..height {
let at = y * stride;
assert_eq!(
&dst[at..at + row_bytes],
&shadow[y * row_bytes..(y + 1) * row_bytes],
"row {y} landed at the wrong offset"
);
assert_eq!(
&dst[at + row_bytes..at + stride],
&[0xEE; 4],
"row {y}'s padding must be left alone, not written through"
);
}
}
#[test]
fn a_short_destination_copies_nothing() {
let mut dst = vec![0u8; 8];
let before = dst.clone();
copy_rows(&mut dst, 8, &[1, 2, 3, 4, 5, 6, 7, 8], 8, 4);
assert_eq!(dst, before);
}
#[test]
fn a_stride_narrower_than_a_row_copies_nothing() {
let mut dst = vec![0u8; 16];
let before = dst.clone();
copy_rows(&mut dst, 4, &[9u8; 16], 8, 2);
assert_eq!(dst, before);
}
use super::{canvas_format_for_plane, canvas_output_bpp, convert_row};
use drmkit_fmt::fourcc;
use drmkit_planes::PlaneCapabilities;
fn plane_with(formats: &[u32]) -> PlaneCapabilities {
PlaneCapabilities {
formats: formats.to_vec(),
..PlaneCapabilities::default()
}
}
#[test]
fn tilcdc_picks_xbgr_rather_than_being_stuck() {
let plane = plane_with(&[fourcc::RGB565, fourcc::XBGR8888, fourcc::BGR888]);
assert_eq!(canvas_format_for_plane(&plane), Some(fourcc::XBGR8888));
}
#[test]
fn argb_wins_where_it_is_offered() {
let plane = plane_with(&[
fourcc::NV12,
fourcc::XRGB8888,
fourcc::ARGB8888,
fourcc::RGB565,
]);
assert_eq!(canvas_format_for_plane(&plane), Some(fourcc::ARGB8888));
}
#[test]
fn the_plane_s_advertisement_order_does_not_decide() {
let plane = plane_with(&[fourcc::XBGR8888, fourcc::ARGB8888]);
assert_eq!(canvas_format_for_plane(&plane), Some(fourcc::ARGB8888));
}
#[test]
fn a_565_only_panel_still_hosts_a_canvas() {
let plane = plane_with(&[fourcc::RGB565]);
assert_eq!(canvas_format_for_plane(&plane), Some(fourcc::RGB565));
}
#[test]
fn every_format_the_negotiation_offers_is_one_flush_can_write() {
for fourcc in [
fourcc::ARGB8888,
fourcc::XRGB8888,
fourcc::XBGR8888,
fourcc::ABGR8888,
fourcc::RGB565,
fourcc::BGR565,
] {
assert_eq!(
canvas_format_for_plane(&plane_with(&[fourcc])),
Some(fourcc),
"{fourcc:#010x} is not selectable"
);
assert_ne!(
canvas_output_bpp(fourcc),
0,
"{fourcc:#010x} cannot be written"
);
}
}
#[test]
fn a_plane_that_takes_the_format_only_compressed_hosts_none() {
let afbc = drmkit_fmt::Modifier(0x0800_0000_0000_0001);
let formats = [fourcc::ARGB8888, fourcc::XRGB8888, fourcc::RGB565];
let plane = PlaneCapabilities {
format_table: drmkit_fmt::FormatTable::from_pairs(formats.map(|f| (f, afbc))),
..plane_with(&formats)
};
assert_eq!(canvas_format_for_plane(&plane), None);
}
#[test]
fn a_format_offered_linear_wins_over_one_offered_compressed() {
let afbc = drmkit_fmt::Modifier(0x0800_0000_0000_0001);
let plane = PlaneCapabilities {
format_table: drmkit_fmt::FormatTable::from_pairs([
(fourcc::ARGB8888, afbc),
(fourcc::XRGB8888, drmkit_fmt::Modifier::LINEAR),
]),
..plane_with(&[fourcc::ARGB8888, fourcc::XRGB8888])
};
assert_eq!(canvas_format_for_plane(&plane), Some(fourcc::XRGB8888));
}
#[test]
fn a_plane_with_nothing_the_canvas_can_write_hosts_none() {
let plane = plane_with(&[fourcc::NV12, fourcc::YUYV]);
assert_eq!(canvas_format_for_plane(&plane), None);
assert_eq!(canvas_format_for_plane(&plane_with(&[])), None);
}
#[test]
fn the_conversion_leaves_argb_alone() {
let src = [0x11_u8, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88];
for fourcc in [fourcc::ARGB8888, fourcc::XRGB8888] {
let mut dst = [0_u8; 8];
convert_row(&mut dst, &src, 2, fourcc);
assert_eq!(dst, src, "{fourcc:#010x}");
}
}
#[test]
fn the_conversion_exchanges_red_and_blue() {
let src = [0x11_u8, 0x22, 0x33, 0x44];
for fourcc in [fourcc::XBGR8888, fourcc::ABGR8888] {
let mut dst = [0_u8; 4];
convert_row(&mut dst, &src, 1, fourcc);
assert_eq!(dst, [0x33, 0x22, 0x11, 0x44], "{fourcc:#010x}");
}
}
#[test]
fn the_conversion_packs_565_and_truncates() {
let cases = [
([0xFF_u8, 0xFF, 0xFF, 0xFF], 0xFFFF_u16),
([0x00, 0x00, 0x00, 0xFF], 0x0000),
([0x00, 0x00, 0xFF, 0xFF], 0xF800), ([0x00, 0xFF, 0x00, 0xFF], 0x07E0), ([0xFF, 0x00, 0x00, 0xFF], 0x001F), ];
for (src, expected) in cases {
let mut dst = [0_u8; 2];
convert_row(&mut dst, &src, 1, fourcc::RGB565);
assert_eq!(u16::from_le_bytes(dst), expected, "{src:02x?} packed wrong");
}
}
#[test]
fn the_565_swap_moves_red_and_blue_between_the_fields() {
let src = [0x00_u8, 0x00, 0xFF, 0xFF]; let mut dst = [0_u8; 2];
convert_row(&mut dst, &src, 1, fourcc::BGR565);
assert_eq!(
u16::from_le_bytes(dst),
0x001F,
"red belongs in the low field"
);
}
#[test]
fn a_format_the_canvas_cannot_write_has_no_depth() {
assert_eq!(canvas_output_bpp(fourcc::NV12), 0);
assert_eq!(canvas_output_bpp(fourcc::YUYV), 0);
assert_eq!(canvas_output_bpp(fourcc::ARGB8888), 4);
assert_eq!(canvas_output_bpp(fourcc::RGB565), 2);
}
#[test]
fn the_canvas_refuses_a_format_it_cannot_write() {
assert_eq!(canvas_output_bpp(fourcc::NV12), 0);
}
#[test]
fn a_565_canvas_is_half_the_bytes_per_pixel() {
assert_eq!(canvas_output_bpp(fourcc::RGB565) * 8, 16);
assert_eq!(canvas_output_bpp(fourcc::ARGB8888) * 8, 32);
}
}