#![no_std]
use core::array::from_fn;
pub mod prelude {
pub use crate::color::*;
pub use crate::data::*;
}
pub mod color;
pub mod data;
use color::*;
mod error;
mod bg;
pub use bg::*;
mod iter;
pub use iter::*;
mod cluster;
pub use cluster::*;
mod tile;
pub use tile::*;
pub const LINE_COUNT: usize = 196;
pub const TILE_SIZE: u8 = 8;
pub const COLORS_PER_TILE: u8 = 4;
pub const COLORS_PER_PALETTE: u8 = 16;
pub const LOCAL_PALETTE_COUNT: u8 = 16;
pub const SUBPIXELS_TILE: u8 = Cluster::<2>::PIXELS_PER_BYTE as u8;
pub const SUBPIXELS_FRAMEBUFFER: u8 = Cluster::<4>::PIXELS_PER_BYTE as u8;
pub const BG_COLUMNS: u8 = 64;
pub const BG_ROWS: u8 = 64;
pub const BG_WIDTH: u16 = BG_COLUMNS as u16 * TILE_SIZE as u16;
pub const BG_HEIGHT: u16 = BG_ROWS as u16 * TILE_SIZE as u16;
const TILE_MEM_LEN: usize = 8182;
#[derive(Debug, Clone, Copy)]
pub struct DrawBundle {
pub x: i16,
pub y: i16,
pub id: TileID,
pub flags: TileFlags,
}
#[derive(Debug, Clone)]
pub struct VideoChip {
pub bg_map: BGMap,
pub bg_color: ColorID,
pub fg_palette: [Color9Bit; COLORS_PER_PALETTE as usize],
pub bg_palette: [Color9Bit; COLORS_PER_PALETTE as usize],
pub local_palettes: [[ColorID; COLORS_PER_TILE as usize]; LOCAL_PALETTE_COUNT as usize],
pub wrap_sprites: bool,
pub wrap_bg: bool,
pub scroll_x: i16,
pub scroll_y: i16,
scanlines: [[Cluster<4>; 256 / PIXELS_PER_CLUSTER as usize]; LINE_COUNT],
crop_x: u8,
crop_y: u8,
max_x: u8,
max_y: u8,
tile_pixels: [Cluster<2>; TILE_MEM_LEN], tiles: [TileEntry; 256],
tile_id_head: u8,
tile_pixel_head: u16,
palette_head: u8,
view_left: u8,
view_top: u8,
view_right: u8,
view_bottom: u8,
}
impl VideoChip {
pub fn new(w: u32, h: u32) -> Self {
assert!(w > 7 && w < 257, err!("Screen width range is 8 to 256"));
assert!(h > 7 && h < 257, err!("Screen height range is 8 to 256"));
assert!(LINE_COUNT < 256, err!("LINE_COUNT must be less than 256"));
let mut result = Self {
bg_map: BGMap::new(),
tile_pixels: [Cluster::default(); TILE_MEM_LEN],
bg_color: GRAY,
wrap_sprites: true,
wrap_bg: true,
tiles: [TileEntry::default(); 256],
fg_palette: [Color9Bit::default(); COLORS_PER_PALETTE as usize],
bg_palette: [Color9Bit::default(); COLORS_PER_PALETTE as usize],
local_palettes: [[ColorID(0); COLORS_PER_TILE as usize]; LOCAL_PALETTE_COUNT as usize],
scanlines: from_fn(|_| from_fn(|_| Cluster::default())),
max_x: (w - 1) as u8,
max_y: (h - 1) as u8,
tile_id_head: 0,
tile_pixel_head: 0,
palette_head: 0,
view_left: 0,
view_top: 0,
view_right: (w - 1) as u8,
view_bottom: (h - 1) as u8,
crop_x: 0,
crop_y: 0,
scroll_x: 0,
scroll_y: 0,
};
result.reset_all();
result
}
pub fn max_x(&self) -> u8 {
self.max_x
}
pub fn max_y(&self) -> u8 {
self.max_y
}
pub fn width(&self) -> u32 {
self.max_x as u32 + 1
}
pub fn height(&self) -> u32 {
self.max_y as u32 + 1
}
pub fn tile_entry(&self, tile_id: TileID) -> TileEntry {
self.tiles[tile_id.0 as usize]
}
pub fn crop_x(&self) -> u8 {
self.crop_x
}
pub fn set_crop_x(&mut self, value: u8) {
assert!(
value < 255 - self.max_x + 1,
err!("crop_x must be lower than 255 - width")
);
self.crop_x = value;
}
pub fn crop_y(&self) -> u8 {
self.crop_y
}
pub fn set_crop_y(&mut self, value: u8) {
assert!(
value < 255 - self.max_y + 1,
err!("crop_y must be lower than 255 - height")
);
self.crop_y = value;
}
pub fn set_viewport(&mut self, left: u8, top: u8, w: u8, h: u8) {
self.view_left = left;
self.view_top = top;
self.view_right = left.saturating_add(w);
self.view_bottom = top.saturating_add(h);
}
pub fn reset_all(&mut self) {
self.bg_color = GRAY;
self.wrap_sprites = true;
self.reset_scroll();
self.reset_tiles();
self.reset_palettes();
self.reset_bgmap();
self.reset_crop();
self.reset_viewport();
}
pub fn reset_tiles(&mut self) {
self.tile_id_head = 0;
}
pub fn reset_palettes(&mut self) {
self.fg_palette = from_fn(|i| {
if i < PALETTE_DEFAULT.len() {
PALETTE_DEFAULT[i]
} else {
Color9Bit::default()
}
});
self.bg_palette = from_fn(|i| {
if i < PALETTE_DEFAULT.len() {
PALETTE_DEFAULT[i]
} else {
Color9Bit::default()
}
});
self.local_palettes = from_fn(|_| from_fn(|i| ColorID(i as u8)));
self.palette_head = 0;
}
pub fn reset_scroll(&mut self) {
self.scroll_x = 0;
self.scroll_y = 0;
}
pub fn reset_crop(&mut self) {
self.crop_x = 0;
self.crop_y = 0;
}
pub fn reset_bgmap(&mut self) {
self.bg_map = BGMap::new();
}
pub fn reset_viewport(&mut self) {
self.view_left = 0;
self.view_top = 0;
self.view_right = self.max_x;
self.view_bottom = self.max_y;
}
pub fn set_palette(&mut self, index: PaletteID, colors: [ColorID; COLORS_PER_TILE as usize]) {
debug_assert!(
index.0 < LOCAL_PALETTE_COUNT,
err!("Invalid local palette index, must be less than PALETTE_COUNT")
);
self.local_palettes[index.0 as usize] = colors;
}
pub fn push_palette(&mut self, colors: [ColorID; COLORS_PER_TILE as usize]) -> PaletteID {
assert!(self.palette_head < 16, err!("PALETTE_COUNT exceeded"));
let result = self.palette_head;
self.local_palettes[self.palette_head as usize] = colors;
self.palette_head += 1;
PaletteID(result)
}
pub fn new_tile(&mut self, w: u8, h: u8, data: &[u8]) -> TileID {
let tile_id = self.tile_id_head;
let pixel_start = self.tile_pixel_head as usize;
let len = (w as usize * h as usize + 7) / 8;
if self.tile_id_head == 255 || pixel_start + len > TILE_MEM_LEN {
panic!(err!("Not enough space for new tile"))
}
assert!(
w % TILE_SIZE == 0 && h % TILE_SIZE == 0,
err!("Tile dimensions are not multiple of TILE_SIZE")
);
assert!(
w >= TILE_SIZE && h >= TILE_SIZE,
err!("Tile dimensions must be TILE_SIZE or larger")
);
assert!(
data.len() == w as usize * h as usize,
err!("Tile data length does not match w * h")
);
for i in 0..data.len() {
let value = data[i].clamp(0, COLORS_PER_TILE as u8);
let cluster_index = i / 8; let subpixel_index = (i % 8) as u8;
let cluster = &mut self.tile_pixels[pixel_start + cluster_index];
cluster.set_subpixel(value, subpixel_index);
}
let cluster_index = self.tile_pixel_head;
self.tiles[tile_id as usize] = TileEntry {
w,
h,
cluster_index,
};
self.tile_id_head += 1;
self.tile_pixel_head += len as u16;
TileID(tile_id)
}
pub fn draw_sprite(&mut self, data: DrawBundle) {
if data.id.0 >= self.tile_id_head {
return;
}
let wrapped_x: u8;
let wrapped_y: u8;
if self.wrap_sprites {
wrapped_x = (data.x - self.scroll_x).rem_euclid(256) as u8;
wrapped_y = (data.y - self.scroll_y).rem_euclid(LINE_COUNT as i16) as u8;
} else {
let max_x = self.scroll_x + self.max_x as i16 + self.crop_x as i16;
if data.x < self.scroll_x || data.x > max_x {
return;
} else {
wrapped_x = (data.x - self.scroll_x) as u8;
}
let max_y = self.scroll_y + self.max_y as i16 + self.crop_y as i16;
if data.y < self.scroll_y || data.y > max_y {
return;
} else {
wrapped_y = (data.y - self.scroll_y).clamp(0, LINE_COUNT as i16 - 1) as u8;
}
}
let tile = self.tiles[data.id.0 as usize];
let (width, height) = if data.flags.is_rotated() {
(tile.h, tile.w) } else {
(tile.w, tile.h)
};
let right_bound = if (wrapped_x as u16 + width as u16) > 255 {
255
} else {
wrapped_x + width
};
let bottom_bound = if (wrapped_y as u16 + height as u16) > LINE_COUNT as u16 {
LINE_COUNT as u8
} else {
wrapped_y + height
};
for screen_y in wrapped_y..bottom_bound {
let local_y = screen_y - wrapped_y;
for screen_x in wrapped_x..right_bound {
let local_x = screen_x - wrapped_x;
let (tx, ty) =
Self::transform_tile_coords(local_x, local_y, width, height, data.flags);
let pixel_index = ty as usize * tile.w as usize + tx as usize;
let cluster_index = pixel_index / PIXELS_PER_CLUSTER as usize;
let subpixel_index = (pixel_index % PIXELS_PER_CLUSTER as usize) as u8;
let cluster = self.tile_pixels[(tile.cluster_index as usize) + cluster_index];
let color_index = cluster.get_subpixel(subpixel_index);
if color_index > 0 {
let palette_index = data.flags.palette().0 as usize;
let color_id = self.local_palettes[palette_index][color_index as usize];
let scanline_cluster = screen_x as usize / PIXELS_PER_CLUSTER as usize;
let scanline_subpixel = (screen_x % PIXELS_PER_CLUSTER) as u8;
self.scanlines[screen_y as usize][scanline_cluster]
.set_subpixel(color_id.0, scanline_subpixel);
}
}
}
}
#[inline(always)]
pub(crate) fn transform_tile_coords(x: u8, y: u8, w: u8, h: u8, flags: TileFlags) -> (u8, u8) {
if flags.is_rotated() {
let rotated_x = h - 1 - y;
let rotated_y = x;
if flags.is_flipped_x() {
(rotated_x, w - 1 - rotated_y)
} else if flags.is_flipped_y() {
(h - 1 - rotated_x, rotated_y)
} else {
(rotated_x, rotated_y)
}
} else {
let flipped_x = if flags.is_flipped_x() { w - 1 - x } else { x };
let flipped_y = if flags.is_flipped_y() { h - 1 - y } else { y };
(flipped_x, flipped_y)
}
}
pub fn color_cycle(&mut self, palette: PaletteID, color: u8, min: u8, max: u8) {
let color_cycle = &mut self.local_palettes[palette.id()][color as usize].0;
if max > min {
*color_cycle += 1;
if *color_cycle > max {
*color_cycle = min
}
} else {
*color_cycle -= 1;
if *color_cycle < min {
*color_cycle = max
}
}
}
pub fn start_frame(&mut self) {
for line in &mut self.scanlines {
*line = from_fn(|_| Cluster::default());
}
}
pub fn iter_pixels(&self) -> PixelIter {
PixelIter::new(self)
}
}