#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Viewport {
pub ox: i32,
pub oy: i32,
pub w: i32,
pub h: i32,
}
impl Viewport {
pub fn full(fb_w: i32, fb_h: i32) -> Self {
Self { ox: 0, oy: 0, w: fb_w, h: fb_h }
}
}
#[inline]
pub fn set_pixel(buf: &mut [u8], fb_w: i32, vp: &Viewport, x: i32, y: i32, rgb: (u8, u8, u8)) {
if x < 0 || y < 0 || x >= vp.w || y >= vp.h {
return;
}
let gx = vp.ox + x;
let gy = vp.oy + y;
if gx < 0 || gy < 0 || gx >= fb_w {
return;
}
let idx = ((gy as usize) * (fb_w as usize) + gx as usize) * 4;
if idx + 3 >= buf.len() {
return;
}
buf[idx] = rgb.0;
buf[idx + 1] = rgb.1;
buf[idx + 2] = rgb.2;
buf[idx + 3] = 255;
}
#[allow(clippy::too_many_arguments)] pub fn fill_rect(
buf: &mut [u8],
fb_w: i32,
vp: &Viewport,
x: i32,
y: i32,
w: i32,
h: i32,
rgb: (u8, u8, u8),
) {
let x0 = x.max(0);
let y0 = y.max(0);
let x1 = x.saturating_add(w).min(vp.w);
let y1 = y.saturating_add(h).min(vp.h);
let mut yy = y0;
while yy < y1 {
let mut xx = x0;
while xx < x1 {
set_pixel(buf, fb_w, vp, xx, yy, rgb);
xx += 1;
}
yy += 1;
}
}
pub fn clear(buf: &mut [u8], fb_w: i32, vp: &Viewport, rgb: (u8, u8, u8)) {
fill_rect(buf, fb_w, vp, 0, 0, vp.w, vp.h, rgb);
}
#[allow(clippy::too_many_arguments)] pub fn blit_glyph(
buf: &mut [u8],
fb_w: i32,
vp: &Viewport,
x: i32,
y: i32,
code: u32,
color: (u8, u8, u8),
scale: i32,
) {
let glyph = glyph_5x7(code);
let scale = scale.max(1);
for (row, bits) in glyph.iter().enumerate() {
for col in 0..5i32 {
if (bits >> (4 - col)) & 1 == 0 {
continue;
}
for dy in 0..scale {
for dx in 0..scale {
set_pixel(buf, fb_w, vp, x + col * scale + dx, y + row as i32 * scale + dy, color);
}
}
}
}
}
#[allow(clippy::too_many_arguments)] pub fn draw_number(
buf: &mut [u8],
fb_w: i32,
vp: &Viewport,
x: i32,
y: i32,
value: i32,
color: (u8, u8, u8),
scale: i32,
) {
let s = scale.max(1);
let advance = 6 * s; let mut cx = x;
let mut n = (value as i64).unsigned_abs();
if value < 0 {
blit_glyph(buf, fb_w, vp, cx, y, '-' as u32, color, s);
cx += advance;
}
let mut digits = [0u8; 20];
let mut count = 0;
if n == 0 {
digits[0] = b'0';
count = 1;
} else {
while n > 0 {
digits[count] = b'0' + (n % 10) as u8;
n /= 10;
count += 1;
}
}
for i in (0..count).rev() {
blit_glyph(buf, fb_w, vp, cx, y, digits[i] as u32, color, s);
cx += advance;
}
}
pub fn glyph_5x7(c: u32) -> [u8; 7] {
match c {
0x20 => [0, 0, 0, 0, 0, 0, 0], 0x30 => [0x0E, 0x11, 0x13, 0x15, 0x19, 0x11, 0x0E], 0x31 => [0x04, 0x0C, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x32 => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x08, 0x1F], 0x33 => [0x1E, 0x01, 0x01, 0x0E, 0x01, 0x01, 0x1E], 0x34 => [0x02, 0x06, 0x0A, 0x12, 0x1F, 0x02, 0x02], 0x35 => [0x1F, 0x10, 0x1E, 0x01, 0x01, 0x11, 0x0E], 0x36 => [0x0E, 0x10, 0x10, 0x1E, 0x11, 0x11, 0x0E], 0x37 => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x08, 0x08], 0x38 => [0x0E, 0x11, 0x11, 0x0E, 0x11, 0x11, 0x0E], 0x39 => [0x0E, 0x11, 0x11, 0x0F, 0x01, 0x01, 0x0E], 0x21 => [0x04, 0x04, 0x04, 0x04, 0x04, 0x00, 0x04], 0x22 => [0x0A, 0x0A, 0x0A, 0x00, 0x00, 0x00, 0x00], 0x23 => [0x0A, 0x0A, 0x1F, 0x0A, 0x1F, 0x0A, 0x0A], 0x25 => [0x18, 0x19, 0x02, 0x04, 0x08, 0x13, 0x03], 0x26 => [0x0C, 0x12, 0x14, 0x08, 0x15, 0x12, 0x0D], 0x27 => [0x04, 0x04, 0x08, 0x00, 0x00, 0x00, 0x00], 0x28 => [0x04, 0x08, 0x10, 0x10, 0x10, 0x08, 0x04], 0x29 => [0x04, 0x02, 0x01, 0x01, 0x01, 0x02, 0x04], 0x2A => [0x00, 0x04, 0x15, 0x0E, 0x15, 0x04, 0x00], 0x2B => [0x00, 0x04, 0x04, 0x1F, 0x04, 0x04, 0x00], 0x2C => [0x00, 0x00, 0x00, 0x00, 0x06, 0x04, 0x08], 0x2D => [0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0x00], 0x2E => [0x00, 0x00, 0x00, 0x00, 0x00, 0x06, 0x06], 0x2F => [0x01, 0x01, 0x02, 0x04, 0x08, 0x10, 0x10], 0x3A => [0x00, 0x06, 0x06, 0x00, 0x06, 0x06, 0x00], 0x3B => [0x00, 0x06, 0x06, 0x00, 0x06, 0x04, 0x08], 0x3C => [0x02, 0x04, 0x08, 0x10, 0x08, 0x04, 0x02], 0x3D => [0x00, 0x00, 0x1F, 0x00, 0x1F, 0x00, 0x00], 0x3E => [0x08, 0x04, 0x02, 0x01, 0x02, 0x04, 0x08], 0x3F => [0x0E, 0x11, 0x01, 0x02, 0x04, 0x00, 0x04], 0x40 => [0x0E, 0x11, 0x17, 0x15, 0x17, 0x10, 0x0E], 0x5B => [0x0E, 0x08, 0x08, 0x08, 0x08, 0x08, 0x0E], 0x5D => [0x0E, 0x02, 0x02, 0x02, 0x02, 0x02, 0x0E], 0x5F => [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x1F], 0x41 => [0x0E, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x42 => [0x1E, 0x11, 0x11, 0x1E, 0x11, 0x11, 0x1E], 0x43 => [0x0E, 0x11, 0x10, 0x10, 0x10, 0x11, 0x0E], 0x44 => [0x1E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1E], 0x45 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x1F], 0x46 => [0x1F, 0x10, 0x10, 0x1E, 0x10, 0x10, 0x10], 0x47 => [0x0E, 0x11, 0x10, 0x17, 0x11, 0x11, 0x0E], 0x48 => [0x11, 0x11, 0x11, 0x1F, 0x11, 0x11, 0x11], 0x49 => [0x0E, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x4A => [0x07, 0x02, 0x02, 0x02, 0x12, 0x12, 0x0C], 0x4B => [0x11, 0x12, 0x14, 0x18, 0x14, 0x12, 0x11], 0x4C => [0x10, 0x10, 0x10, 0x10, 0x10, 0x10, 0x1F], 0x4D => [0x11, 0x1B, 0x15, 0x15, 0x11, 0x11, 0x11], 0x4E => [0x11, 0x11, 0x19, 0x15, 0x13, 0x11, 0x11], 0x4F => [0x0E, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x50 => [0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10, 0x10], 0x51 => [0x0E, 0x11, 0x11, 0x11, 0x15, 0x12, 0x0D], 0x52 => [0x1E, 0x11, 0x11, 0x1E, 0x14, 0x12, 0x11], 0x53 => [0x0F, 0x10, 0x10, 0x0E, 0x01, 0x01, 0x1E], 0x54 => [0x1F, 0x04, 0x04, 0x04, 0x04, 0x04, 0x04], 0x55 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x11, 0x0E], 0x56 => [0x11, 0x11, 0x11, 0x11, 0x11, 0x0A, 0x04], 0x57 => [0x11, 0x11, 0x11, 0x15, 0x15, 0x1B, 0x11], 0x58 => [0x11, 0x11, 0x0A, 0x04, 0x0A, 0x11, 0x11], 0x59 => [0x11, 0x11, 0x0A, 0x04, 0x04, 0x04, 0x04], 0x5A => [0x1F, 0x01, 0x02, 0x04, 0x08, 0x10, 0x1F], 0x61 => [0x00, 0x00, 0x0E, 0x01, 0x0F, 0x11, 0x0F], 0x62 => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x1E], 0x63 => [0x00, 0x00, 0x0E, 0x10, 0x10, 0x11, 0x0E], 0x64 => [0x01, 0x01, 0x0D, 0x13, 0x11, 0x11, 0x0F], 0x65 => [0x00, 0x00, 0x0E, 0x11, 0x1F, 0x10, 0x0E], 0x66 => [0x06, 0x09, 0x08, 0x1C, 0x08, 0x08, 0x08], 0x67 => [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x0E], 0x68 => [0x10, 0x10, 0x16, 0x19, 0x11, 0x11, 0x11], 0x69 => [0x04, 0x00, 0x0C, 0x04, 0x04, 0x04, 0x0E], 0x6A => [0x02, 0x00, 0x06, 0x02, 0x02, 0x12, 0x0C], 0x6B => [0x10, 0x10, 0x12, 0x14, 0x18, 0x14, 0x12], 0x6C => [0x0C, 0x04, 0x04, 0x04, 0x04, 0x04, 0x0E], 0x6D => [0x00, 0x00, 0x1A, 0x15, 0x15, 0x11, 0x11], 0x6E => [0x00, 0x00, 0x16, 0x19, 0x11, 0x11, 0x11], 0x6F => [0x00, 0x00, 0x0E, 0x11, 0x11, 0x11, 0x0E], 0x70 => [0x00, 0x1E, 0x11, 0x11, 0x1E, 0x10, 0x10], 0x71 => [0x00, 0x0F, 0x11, 0x11, 0x0F, 0x01, 0x01], 0x72 => [0x00, 0x00, 0x16, 0x19, 0x10, 0x10, 0x10], 0x73 => [0x00, 0x00, 0x0F, 0x10, 0x0E, 0x01, 0x1E], 0x74 => [0x08, 0x08, 0x1C, 0x08, 0x08, 0x09, 0x06], 0x75 => [0x00, 0x00, 0x11, 0x11, 0x11, 0x13, 0x0D], 0x76 => [0x00, 0x00, 0x11, 0x11, 0x11, 0x0A, 0x04], 0x77 => [0x00, 0x00, 0x11, 0x11, 0x15, 0x15, 0x0A], 0x78 => [0x00, 0x00, 0x11, 0x0A, 0x04, 0x0A, 0x11], 0x79 => [0x00, 0x11, 0x11, 0x11, 0x0F, 0x01, 0x0E], 0x7A => [0x00, 0x00, 0x1F, 0x02, 0x04, 0x08, 0x1F], _ => [0x1F, 0x11, 0x11, 0x11, 0x11, 0x11, 0x1F], }
}
#[cfg(test)]
mod tests {
use super::*;
fn fb(w: i32, h: i32) -> Vec<u8> {
vec![0u8; (w * h * 4) as usize]
}
#[test]
fn identity_viewport_set_pixel_matches_direct_index() {
let (w, h) = (256, 144);
let mut buf = fb(w, h);
let vp = Viewport::full(w, h);
set_pixel(&mut buf, w, &vp, 10, 20, (1, 2, 3));
let idx = ((20 * w + 10) * 4) as usize;
assert_eq!(&buf[idx..idx + 4], &[1, 2, 3, 255]);
}
#[test]
fn offset_viewport_translates_into_subrect() {
let (w, h) = (256, 144);
let mut buf = fb(w, h);
let vp = Viewport { ox: 100, oy: 50, w: 64, h: 32 };
set_pixel(&mut buf, w, &vp, 5, 5, (9, 9, 9));
let idx = (((50 + 5) * w + (100 + 5)) * 4) as usize;
assert_eq!(&buf[idx..idx + 4], &[9, 9, 9, 255]);
}
#[test]
fn viewport_clips_child_coords_outside_its_bounds() {
let (w, h) = (256, 144);
let mut buf = fb(w, h);
let vp = Viewport { ox: 100, oy: 50, w: 64, h: 32 };
set_pixel(&mut buf, w, &vp, 64, 0, (9, 9, 9)); set_pixel(&mut buf, w, &vp, -1, 0, (9, 9, 9)); set_pixel(&mut buf, w, &vp, 0, 32, (9, 9, 9)); assert!(buf.iter().all(|&b| b == 0), "no pixel should have been written");
}
#[test]
fn full_viewport_clear_fills_entire_framebuffer() {
let (w, h) = (8, 4);
let mut buf = fb(w, h);
clear(&mut buf, w, &Viewport::full(w, h), (5, 5, 5));
for px in buf.chunks(4) {
assert_eq!(px, &[5, 5, 5, 255]);
}
}
#[test]
fn blit_glyph_identity_renders_lit_pixels_in_place() {
let (w, h) = (32, 16);
let mut buf = fb(w, h);
blit_glyph(&mut buf, w, &Viewport::full(w, h), 0, 0, 0x2D, (9, 9, 9), 1);
for col in 0..5 {
let idx = (((3 * w) + col) * 4) as usize;
assert_eq!(&buf[idx..idx + 4], &[9, 9, 9, 255], "row 3 col {col} lit");
}
assert_eq!(&buf[0..4], &[0, 0, 0, 0], "row 0 is blank for '-'");
}
#[test]
fn blit_glyph_offset_viewport_translates_glyph() {
let (w, h) = (64, 64);
let mut buf = fb(w, h);
let vp = Viewport { ox: 10, oy: 20, w: 16, h: 16 };
blit_glyph(&mut buf, w, &vp, 0, 0, 0x2D, (1, 2, 3), 1);
let idx = (((23 * w) + 10) * 4) as usize; assert_eq!(&buf[idx..idx + 4], &[1, 2, 3, 255]);
}
#[test]
fn draw_number_negative_blits_minus_then_digit() {
let (w, h) = (64, 16);
let mut buf = fb(w, h);
draw_number(&mut buf, w, &Viewport::full(w, h), 0, 0, -5, (5, 5, 5), 1);
let minus = ((3 * w) * 4) as usize; assert_eq!(&buf[minus..minus + 4], &[5, 5, 5, 255]);
let five_top = (6 * 4) as usize; assert_eq!(&buf[five_top..five_top + 4], &[5, 5, 5, 255]);
}
#[test]
fn offset_clear_stays_inside_viewport() {
let (w, h) = (16, 16);
let mut buf = fb(w, h);
let vp = Viewport { ox: 4, oy: 4, w: 4, h: 4 };
clear(&mut buf, w, &vp, (7, 7, 7)); let inside = (((4 * w) + 4) * 4) as usize;
assert_eq!(&buf[inside..inside + 4], &[7, 7, 7, 255]);
assert_eq!(&buf[0..4], &[0, 0, 0, 0], "origin is outside the viewport");
let past = (((8 * w) + 8) * 4) as usize;
assert_eq!(&buf[past..past + 4], &[0, 0, 0, 0], "just past the viewport");
}
}