1004 lines
33 KiB
Rust

use crate::memory_banks::TileBankPoolAccess;
use prometeu_hal::GfxBridge;
use prometeu_hal::color::Color;
use prometeu_hal::sprite::Sprite;
use prometeu_hal::tile::Tile;
use prometeu_hal::tile_bank::{TileBank, TileSize};
use prometeu_hal::tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
use std::sync::Arc;
/// Blending modes inspired by classic 16-bit hardware.
/// Defines how source pixels are combined with existing pixels in the framebuffer.
///
/// ### Usage Example:
/// // Draw a semi-transparent blue rectangle
/// gfx.fill_rect_blend(10, 10, 50, 50, Color::BLUE, BlendMode::Half);
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlendMode {
/// No blending: a source overwrites the destination.
#[default]
None,
/// Average: `dst = (src + dst) / 2`. Creates a semi-transparent effect.
Half,
/// Additive: `dst = clamp(dst + (src / 2))`. Good for glows/light.
HalfPlus,
/// Subtractive: `dst = clamp(dst - (src / 2))`. Good for shadows.
HalfMinus,
/// Full Additive: `dst = clamp(dst + src)`. Saturated light effect.
Full,
}
/// PROMETEU Graphics Subsystem (GFX).
///
/// Models a specialized graphics chip with a fixed resolution, double buffering,
/// and a multi-layered tile/sprite architecture.
///
/// The GFX system works by composing several "layers" into a single 16-bit
/// RGB565 framebuffer. It supports hardware-accelerated primitives (lines, rects)
/// and specialized console features like background scrolling and sprite sorting.
///
/// ### Layer Composition Order (back to front):
/// 1. **Priority 0 Sprites**: Objects behind everything else.
/// 2. **Tile Layer 0 + Priority 1 Sprites**: Background 0.
/// 3. **Tile Layer 1 + Priority 2 Sprites**: Background 1.
/// 4. **Tile Layer 2 + Priority 3 Sprites**: Background 2.
/// 5. **Tile Layer 3 + Priority 4 Sprites**: Foreground.
/// 6. **Scene Fade**: Global brightness/color filter.
/// 7. **HUD Layer**: Fixed UI elements (always on top).
/// 8. **HUD Fade**: Independent fade for the UI.
pub struct Gfx {
/// Width of the internal framebuffer in pixels.
w: usize,
/// Height of the internal framebuffer in pixels.
h: usize,
/// Front buffer: the "VRAM" currently being displayed by the Host window.
front: Vec<u16>,
/// Back buffer: the "Work RAM" where new frames are composed.
back: Vec<u16>,
/// 4 scrollable background layers. Each can have its own scroll (X, Y) and TileBank.
pub layers: [ScrollableTileLayer; 4],
/// 1 fixed layer for User Interface (HUD). It doesn't scroll.
pub hud: HudTileLayer,
/// Shared access to graphical memory banks (tiles and palettes).
pub tile_banks: Arc<dyn TileBankPoolAccess>,
/// Hardware sprite list (512 slots). Equivalent to OAM (Object Attribute Memory).
pub sprites: [Sprite; 512],
/// Scene brightness/fade level (0 = black/invisible, 31 = fully visible).
pub scene_fade_level: u8,
/// Target color for the scene fade effect (usually Black).
pub scene_fade_color: Color,
/// HUD brightness/fade level (independent from the scene).
pub hud_fade_level: u8,
/// Target color for the HUD fade effect.
pub hud_fade_color: Color,
/// Internal cache used to sort sprites into priority groups to optimize rendering.
priority_buckets: [Vec<usize>; 5],
}
const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
#[inline]
fn glyph_for_char(c: char) -> &'static [u8; 5] {
match c.to_ascii_uppercase() {
'0' => &[0x7, 0x5, 0x5, 0x5, 0x7],
'1' => &[0x2, 0x6, 0x2, 0x2, 0x7],
'2' => &[0x7, 0x1, 0x7, 0x4, 0x7],
'3' => &[0x7, 0x1, 0x7, 0x1, 0x7],
'4' => &[0x5, 0x5, 0x7, 0x1, 0x1],
'5' => &[0x7, 0x4, 0x7, 0x1, 0x7],
'6' => &[0x7, 0x4, 0x7, 0x5, 0x7],
'7' => &[0x7, 0x1, 0x1, 0x1, 0x1],
'8' => &[0x7, 0x5, 0x7, 0x5, 0x7],
'9' => &[0x7, 0x5, 0x7, 0x1, 0x7],
'A' => &[0x7, 0x5, 0x7, 0x5, 0x5],
'B' => &[0x6, 0x5, 0x6, 0x5, 0x6],
'C' => &[0x7, 0x4, 0x4, 0x4, 0x7],
'D' => &[0x6, 0x5, 0x5, 0x5, 0x6],
'E' => &[0x7, 0x4, 0x6, 0x4, 0x7],
'F' => &[0x7, 0x4, 0x6, 0x4, 0x4],
'G' => &[0x7, 0x4, 0x5, 0x5, 0x7],
'H' => &[0x5, 0x5, 0x7, 0x5, 0x5],
'I' => &[0x7, 0x2, 0x2, 0x2, 0x7],
'J' => &[0x1, 0x1, 0x1, 0x5, 0x2],
'K' => &[0x5, 0x5, 0x6, 0x5, 0x5],
'L' => &[0x4, 0x4, 0x4, 0x4, 0x7],
'M' => &[0x5, 0x7, 0x5, 0x5, 0x5],
'N' => &[0x5, 0x5, 0x5, 0x5, 0x5],
'O' => &[0x7, 0x5, 0x5, 0x5, 0x7],
'P' => &[0x7, 0x5, 0x7, 0x4, 0x4],
'Q' => &[0x7, 0x5, 0x5, 0x7, 0x1],
'R' => &[0x7, 0x5, 0x6, 0x5, 0x5],
'S' => &[0x7, 0x4, 0x7, 0x1, 0x7],
'T' => &[0x7, 0x2, 0x2, 0x2, 0x2],
'U' => &[0x5, 0x5, 0x5, 0x5, 0x7],
'V' => &[0x5, 0x5, 0x5, 0x5, 0x2],
'W' => &[0x5, 0x5, 0x5, 0x7, 0x5],
'X' => &[0x5, 0x5, 0x2, 0x5, 0x5],
'Y' => &[0x5, 0x5, 0x2, 0x2, 0x2],
'Z' => &[0x7, 0x1, 0x2, 0x4, 0x7],
':' => &[0x0, 0x2, 0x0, 0x2, 0x0],
'.' => &[0x0, 0x0, 0x0, 0x0, 0x2],
',' => &[0x0, 0x0, 0x0, 0x2, 0x4],
'!' => &[0x2, 0x2, 0x2, 0x0, 0x2],
'?' => &[0x7, 0x1, 0x2, 0x0, 0x2],
' ' => &[0x0, 0x0, 0x0, 0x0, 0x0],
'|' => &[0x2, 0x2, 0x2, 0x2, 0x2],
'/' => &[0x1, 0x1, 0x2, 0x4, 0x4],
'(' => &[0x2, 0x4, 0x4, 0x4, 0x2],
')' => &[0x2, 0x1, 0x1, 0x1, 0x2],
'>' => &[0x4, 0x2, 0x1, 0x2, 0x4],
'<' => &[0x1, 0x2, 0x4, 0x2, 0x1],
_ => &GLYPH_UNKNOWN,
}
}
impl GfxBridge for Gfx {
fn size(&self) -> (usize, usize) {
self.size()
}
fn front_buffer(&self) -> &[u16] {
self.front_buffer()
}
fn clear(&mut self, color: Color) {
self.clear(color)
}
fn fill_rect_blend(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
color: Color,
mode: prometeu_hal::BlendMode,
) {
let m = match mode {
prometeu_hal::BlendMode::None => BlendMode::None,
prometeu_hal::BlendMode::Half => BlendMode::Half,
prometeu_hal::BlendMode::HalfPlus => BlendMode::HalfPlus,
prometeu_hal::BlendMode::HalfMinus => BlendMode::HalfMinus,
prometeu_hal::BlendMode::Full => BlendMode::Full,
};
self.fill_rect_blend(x, y, w, h, color, m)
}
fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.fill_rect(x, y, w, h, color)
}
fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
self.draw_pixel(x, y, color)
}
fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
self.draw_line(x0, y0, x1, y1, color)
}
fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.draw_circle(xc, yc, r, color)
}
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_points(xc, yc, x, y, color)
}
fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
self.fill_circle(xc, yc, r, color)
}
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_circle_lines(xc, yc, x, y, color)
}
fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
self.draw_disc(x, y, r, border_color, fill_color)
}
fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.draw_rect(x, y, w, h, color)
}
fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.draw_square(x, y, w, h, border_color, fill_color)
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
self.draw_horizontal_line(x0, x1, y, color)
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
self.draw_vertical_line(x, y0, y1, color)
}
fn present(&mut self) {
self.present()
}
fn render_all(&mut self) {
self.render_all()
}
fn render_layer(&mut self, layer_idx: usize) {
self.render_layer(layer_idx)
}
fn render_hud(&mut self) {
self.render_hud()
}
fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
self.draw_text(x, y, text, color)
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
self.draw_char(x, y, c, color)
}
fn layer(&self, index: usize) -> &ScrollableTileLayer {
&self.layers[index]
}
fn layer_mut(&mut self, index: usize) -> &mut ScrollableTileLayer {
&mut self.layers[index]
}
fn hud(&self) -> &HudTileLayer {
&self.hud
}
fn hud_mut(&mut self) -> &mut HudTileLayer {
&mut self.hud
}
fn sprite(&self, index: usize) -> &Sprite {
&self.sprites[index]
}
fn sprite_mut(&mut self, index: usize) -> &mut Sprite {
&mut self.sprites[index]
}
fn scene_fade_level(&self) -> u8 {
self.scene_fade_level
}
fn set_scene_fade_level(&mut self, level: u8) {
self.scene_fade_level = level;
}
fn scene_fade_color(&self) -> Color {
self.scene_fade_color
}
fn set_scene_fade_color(&mut self, color: Color) {
self.scene_fade_color = color;
}
fn hud_fade_level(&self) -> u8 {
self.hud_fade_level
}
fn set_hud_fade_level(&mut self, level: u8) {
self.hud_fade_level = level;
}
fn hud_fade_color(&self) -> Color {
self.hud_fade_color
}
fn set_hud_fade_color(&mut self, color: Color) {
self.hud_fade_color = color;
}
}
impl Gfx {
/// Initializes the graphics system with a specific resolution and shared memory banks.
pub fn new(w: usize, h: usize, tile_banks: Arc<dyn TileBankPoolAccess>) -> Self {
const EMPTY_SPRITE: Sprite = Sprite {
tile: Tile { id: 0, flip_x: false, flip_y: false, palette_id: 0 },
x: 0,
y: 0,
bank_id: 0,
active: false,
flip_x: false,
flip_y: false,
priority: 4,
};
let len = w * h;
let layers = [
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
ScrollableTileLayer::new(64, 64, TileSize::Size16),
];
Self {
w,
h,
front: vec![0; len],
back: vec![0; len],
layers,
hud: HudTileLayer::new(64, 32),
tile_banks,
sprites: [EMPTY_SPRITE; 512],
scene_fade_level: 31,
scene_fade_color: Color::BLACK,
hud_fade_level: 31,
hud_fade_color: Color::BLACK,
priority_buckets: [
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
],
}
}
pub fn size(&self) -> (usize, usize) {
(self.w, self.h)
}
/// The buffer that the host should display (RGB565).
pub fn front_buffer(&self) -> &[u16] {
&self.front
}
pub fn clear(&mut self, color: Color) {
self.back.fill(color.0);
}
/// Rectangle with blend mode.
pub fn fill_rect_blend(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
color: Color,
mode: BlendMode,
) {
if color == Color::COLOR_KEY {
return;
}
let fw = self.w as i32;
let fh = self.h as i32;
let x0 = x.clamp(0, fw);
let y0 = y.clamp(0, fh);
let x1 = (x + w).clamp(0, fw);
let y1 = (y + h).clamp(0, fh);
let src = color.0;
for yy in y0..y1 {
let row = (yy as usize) * self.w;
for xx in x0..x1 {
let idx = row + (xx as usize);
let dst = self.back[idx];
self.back[idx] = blend_rgb565(dst, src, mode);
}
}
}
/// Convenience: normal rectangle (no blend).
pub fn fill_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
self.fill_rect_blend(x, y, w, h, color, BlendMode::None);
}
/// Draws a single pixel.
pub fn draw_pixel(&mut self, x: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if x >= 0 && x < self.w as i32 && y >= 0 && y < self.h as i32 {
self.back[y as usize * self.w + x as usize] = color.0;
}
}
/// Draws a line between two points using Bresenham's algorithm.
pub fn draw_line(&mut self, x0: i32, y0: i32, x1: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
let dx = (x1 - x0).abs();
let sx = if x0 < x1 { 1 } else { -1 };
let dy = -(y1 - y0).abs();
let sy = if y0 < y1 { 1 } else { -1 };
let mut err = dx + dy;
let mut x = x0;
let mut y = y0;
loop {
self.draw_pixel(x, y, color);
if x == x1 && y == y1 {
break;
}
let e2 = 2 * err;
if e2 >= dy {
err += dy;
x += sx;
}
if e2 <= dx {
err += dx;
y += sy;
}
}
}
/// Draws a circle outline using Midpoint Circle Algorithm.
pub fn draw_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if r < 0 {
return;
}
let mut x = 0;
let mut y = r;
let mut d = 3 - 2 * r;
self.draw_circle_points(xc, yc, x, y, color);
while y >= x {
x += 1;
if d > 0 {
y -= 1;
d = d + 4 * (x - y) + 10;
} else {
d = d + 4 * x + 6;
}
self.draw_circle_points(xc, yc, x, y, color);
}
}
fn draw_circle_points(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_pixel(xc + x, yc + y, color);
self.draw_pixel(xc - x, yc + y, color);
self.draw_pixel(xc + x, yc - y, color);
self.draw_pixel(xc - x, yc - y, color);
self.draw_pixel(xc + y, yc + x, color);
self.draw_pixel(xc - y, yc + x, color);
self.draw_pixel(xc + y, yc - x, color);
self.draw_pixel(xc - y, yc - x, color);
}
/// Draws a filled circle.
pub fn fill_circle(&mut self, xc: i32, yc: i32, r: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if r < 0 {
return;
}
let mut x = 0;
let mut y = r;
let mut d = 3 - 2 * r;
self.draw_circle_lines(xc, yc, x, y, color);
while y >= x {
x += 1;
if d > 0 {
y -= 1;
d = d + 4 * (x - y) + 10;
} else {
d = d + 4 * x + 6;
}
self.draw_circle_lines(xc, yc, x, y, color);
}
}
fn draw_circle_lines(&mut self, xc: i32, yc: i32, x: i32, y: i32, color: Color) {
self.draw_horizontal_line(xc - x, xc + x, yc + y, color);
self.draw_horizontal_line(xc - x, xc + x, yc - y, color);
self.draw_horizontal_line(xc - y, xc + y, yc + x, color);
self.draw_horizontal_line(xc - y, xc + y, yc - x, color);
}
/// Draws a disc (filled circle with border).
pub fn draw_disc(&mut self, x: i32, y: i32, r: i32, border_color: Color, fill_color: Color) {
self.fill_circle(x, y, r, fill_color);
self.draw_circle(x, y, r, border_color);
}
/// Draws a rectangle outline.
pub fn draw_rect(&mut self, x: i32, y: i32, w: i32, h: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if w <= 0 || h <= 0 {
return;
}
self.draw_horizontal_line(x, x + w - 1, y, color);
self.draw_horizontal_line(x, x + w - 1, y + h - 1, color);
self.draw_vertical_line(x, y, y + h - 1, color);
self.draw_vertical_line(x + w - 1, y, y + h - 1, color);
}
/// Draws a square (filled rectangle with border).
pub fn draw_square(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
border_color: Color,
fill_color: Color,
) {
self.fill_rect(x, y, w, h, fill_color);
self.draw_rect(x, y, w, h, border_color);
}
fn draw_horizontal_line(&mut self, x0: i32, x1: i32, y: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if y < 0 || y >= self.h as i32 {
return;
}
let start = x0.max(0);
let end = x1.min(self.w as i32 - 1);
if start > end {
return;
}
for x in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
}
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
if color == Color::COLOR_KEY {
return;
}
if x < 0 || x >= self.w as i32 {
return;
}
let start = y0.max(0);
let end = y1.min(self.h as i32 - 1);
if start > end {
return;
}
for y in start..=end {
self.back[y as usize * self.w + x as usize] = color.0;
}
}
/// Double buffer swap (O(1), no pixel copying).
/// Typically called by the Host when it's time to display the finished frame.
pub fn present(&mut self) {
std::mem::swap(&mut self.front, &mut self.back);
}
/// The main rendering pipeline.
///
/// This method composes the final frame by rasterizing layers and sprites in the
/// correct priority order into the back buffer.
/// Follows the hardware model where layers and sprites are composed every frame.
pub fn render_all(&mut self) {
// 0. Preparation Phase: Filter and group sprites by their priority levels.
// This avoids iterating through all 512 sprites for every layer.
for bucket in self.priority_buckets.iter_mut() {
bucket.clear();
}
for (idx, sprite) in self.sprites.iter().enumerate() {
if sprite.active && sprite.priority < 5 {
self.priority_buckets[sprite.priority as usize].push(idx);
}
}
// 1. Priority 0 sprites: drawn at the very back, behind everything else.
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[0],
&self.sprites,
&*self.tile_banks,
);
// 2. Main layers and prioritized sprites.
// Order: Layer 0 -> Sprites 1 -> Layer 1 -> Sprites 2 ...
for i in 0..self.layers.len() {
let bank_id = self.layers[i].bank_id as usize;
if let Some(bank) = self.tile_banks.tile_bank_slot(bank_id) {
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[i].map,
&bank,
self.layers[i].scroll_x,
self.layers[i].scroll_y,
);
}
// Draw sprites that belong to this depth level
Self::draw_bucket_on_buffer(
&mut self.back,
self.w,
self.h,
&self.priority_buckets[i + 1],
&self.sprites,
&*self.tile_banks,
);
}
// 4. Scene Fade: Applies a color blend to the entire world (excluding HUD).
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
// 5. HUD: The fixed interface layer, always drawn on top of the world.
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
// 6. HUD Fade: Independent fade effect for the UI.
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
}
/// Renders a specific game layer.
pub fn render_layer(&mut self, layer_idx: usize) {
if layer_idx >= self.layers.len() {
return;
}
let bank_id = self.layers[layer_idx].bank_id as usize;
let scroll_x = self.layers[layer_idx].scroll_x;
let scroll_y = self.layers[layer_idx].scroll_y;
let bank = match self.tile_banks.tile_bank_slot(bank_id) {
Some(b) => b,
_ => return,
};
Self::draw_tile_map(
&mut self.back,
self.w,
self.h,
&self.layers[layer_idx].map,
&bank,
scroll_x,
scroll_y,
);
}
/// Renders the HUD (fixed position, no scroll).
pub fn render_hud(&mut self) {
Self::render_hud_with_pool(&mut self.back, self.w, self.h, &self.hud, &*self.tile_banks);
}
fn render_hud_with_pool(
back: &mut [u16],
w: usize,
h: usize,
hud: &HudTileLayer,
tile_banks: &dyn TileBankPoolAccess,
) {
let bank_id = hud.bank_id as usize;
let bank = match tile_banks.tile_bank_slot(bank_id) {
Some(b) => b,
_ => return,
};
Self::draw_tile_map(back, w, h, &hud.map, &bank, 0, 0);
}
/// Rasterizes a TileMap into the provided pixel buffer using scrolling.
fn draw_tile_map(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
map: &TileMap,
bank: &TileBank,
scroll_x: i32,
scroll_y: i32,
) {
let tile_size = bank.tile_size as usize;
// 1. Determine the range of visible tiles based on the scroll position.
// We add a margin of 1 tile to ensure smooth pixel-perfect scrolling at the borders.
let visible_tiles_x = (screen_w / tile_size) + 1;
let visible_tiles_y = (screen_h / tile_size) + 1;
// 2. Calculate offsets within the tilemap.
let start_tile_x = scroll_x / tile_size as i32;
let start_tile_y = scroll_y / tile_size as i32;
// 3. Fine scroll: how many pixels the tiles are shifted within the first visible cell.
let fine_scroll_x = scroll_x % tile_size as i32;
let fine_scroll_y = scroll_y % tile_size as i32;
// 4. Iterate only through the tiles that are actually visible on screen.
for ty in 0..visible_tiles_y {
for tx in 0..visible_tiles_x {
let map_x = (start_tile_x + tx as i32) as usize;
let map_y = (start_tile_y + ty as i32) as usize;
// Bounds check: don't draw if the camera is outside the map.
if map_x >= map.width || map_y >= map.height {
continue;
}
let tile = map.tiles[map_y * map.width + map_x];
// Optimized skip for empty (ID 0) tiles.
if tile.id == 0 {
continue;
}
// 5. Project the tile pixels to screen space.
let screen_tile_x = (tx as i32 * tile_size as i32) - fine_scroll_x;
let screen_tile_y = (ty as i32 * tile_size as i32) - fine_scroll_y;
Self::draw_tile_pixels(
back,
screen_w,
screen_h,
screen_tile_x,
screen_tile_y,
tile,
bank,
);
}
}
}
/// Internal helper to copy a single tile's pixels to the framebuffer.
/// Handles flipping and palette resolution.
fn draw_tile_pixels(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
x: i32,
y: i32,
tile: Tile,
bank: &TileBank,
) {
let size = bank.tile_size as usize;
for local_y in 0..size {
let world_y = y + local_y as i32;
if world_y < 0 || world_y >= screen_h as i32 {
continue;
}
for local_x in 0..size {
let world_x = x + local_x as i32;
if world_x < 0 || world_x >= screen_w as i32 {
continue;
}
// Handle flip flags by reversing the fetch coordinates.
let fetch_x = if tile.flip_x { size - 1 - local_x } else { local_x };
let fetch_y = if tile.flip_y { size - 1 - local_y } else { local_y };
// 1. Get the pixel color index (0-15) from the bank.
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
// 2. Hardware rule: Color index 0 is always fully transparent.
if px_index == 0 {
continue;
}
// 3. Resolve the virtual index to a real RGB565 color using the tile's assigned palette.
let color = bank.resolve_color(tile.palette_id, px_index);
back[world_y as usize * screen_w + world_x as usize] = color.raw();
}
}
}
fn draw_bucket_on_buffer(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
bucket: &[usize],
sprites: &[Sprite],
tile_banks: &dyn TileBankPoolAccess,
) {
for &idx in bucket {
let s = &sprites[idx];
let bank_id = s.bank_id as usize;
if let Some(bank) = tile_banks.tile_bank_slot(bank_id) {
Self::draw_sprite_pixel_by_pixel(back, screen_w, screen_h, s, &bank);
}
}
}
fn draw_sprite_pixel_by_pixel(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
sprite: &Sprite,
bank: &TileBank,
) {
// ... (same bounds/clipping calculation we already had) ...
let size = bank.tile_size as usize;
let start_x = sprite.x.max(0);
let start_y = sprite.y.max(0);
let end_x = (sprite.x + size as i32).min(screen_w as i32);
let end_y = (sprite.y + size as i32).min(screen_h as i32);
for world_y in start_y..end_y {
for world_x in start_x..end_x {
let local_x = (world_x - sprite.x) as usize;
let local_y = (world_y - sprite.y) as usize;
let fetch_x = if sprite.flip_x { size - 1 - local_x } else { local_x };
let fetch_y = if sprite.flip_y { size - 1 - local_y } else { local_y };
// 1. Get index
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
// 2. Transparency
if px_index == 0 {
continue;
}
// 3. Resolve color via palette (from the tile inside the sprite)
let color = bank.resolve_color(sprite.tile.palette_id, px_index);
back[world_y as usize * screen_w + world_x as usize] = color.raw();
}
}
}
/// Applies the fade effect to the entire back buffer.
/// level: 0 (full color) to 31 (visible)
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
if level >= 31 {
return;
} // Fully visible, skip processing
let weight = level as u16;
let inv_weight = 31 - weight;
let (fr, fg, fb) = Color::unpack_to_native(fade_color.0);
for px in back.iter_mut() {
let (sr, sg, sb) = Color::unpack_to_native(*px);
// Formula: (src * weight + fade * inv_weight) / 31
let r = ((sr as u16 * weight + fr as u16 * inv_weight) / 31) as u8;
let g = ((sg as u16 * weight + fg as u16 * inv_weight) / 31) as u8;
let b = ((sb as u16 * weight + fb as u16 * inv_weight) / 31) as u8;
*px = Color::pack_from_native(r, g, b);
}
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
let mut cx = x;
for c in text.chars() {
self.draw_char(cx, y, c, color);
cx += 4;
}
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
if color == Color::COLOR_KEY {
return;
}
let screen_w = self.w as i32;
let screen_h = self.h as i32;
if x >= screen_w || y >= screen_h || x + 3 <= 0 || y + 5 <= 0 {
return;
}
let glyph = glyph_for_char(c);
let raw = color.0;
let row_start = (-y).max(0).min(5) as usize;
let row_end = (screen_h - y).max(0).min(5) as usize;
let col_start = (-x).max(0).min(3) as usize;
let col_end = (screen_w - x).max(0).min(3) as usize;
for (row_idx, row) in glyph
.iter()
.enumerate()
.skip(row_start)
.take(row_end.saturating_sub(row_start))
{
let py = (y + row_idx as i32) as usize;
let base = py * self.w;
for col_idx in col_start..col_end {
if (row >> (2 - col_idx)) & 1 == 1 {
let px = (x + col_idx as i32) as usize;
self.back[base + px] = raw;
}
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::MemoryBanks;
#[test]
fn test_draw_pixel() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(10, 10, banks);
gfx.draw_pixel(5, 5, Color::WHITE);
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
// Out of bounds should not panic
gfx.draw_pixel(-1, -1, Color::WHITE);
gfx.draw_pixel(10, 10, Color::WHITE);
}
#[test]
fn test_draw_line() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(10, 10, banks);
gfx.draw_line(0, 0, 9, 9, Color::WHITE);
assert_eq!(gfx.back[0], Color::WHITE.0);
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
}
#[test]
fn test_draw_rect() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(10, 10, banks);
gfx.draw_rect(0, 0, 10, 10, Color::WHITE);
assert_eq!(gfx.back[0], Color::WHITE.0);
assert_eq!(gfx.back[9], Color::WHITE.0);
assert_eq!(gfx.back[90], Color::WHITE.0);
assert_eq!(gfx.back[99], Color::WHITE.0);
}
#[test]
fn test_fill_circle() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(10, 10, banks);
gfx.fill_circle(5, 5, 2, Color::WHITE);
assert_eq!(gfx.back[5 * 10 + 5], Color::WHITE.0);
}
#[test]
fn test_draw_square() {
let banks = Arc::new(MemoryBanks::new());
let mut gfx = Gfx::new(10, 10, banks);
gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
// Border
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
// Fill
assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0);
}
}
/// Blends in RGB565 per channel with saturation.
/// `dst` and `src` are RGB565 pixels (u16).
fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 {
match mode {
BlendMode::None => src,
BlendMode::Half => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let r = ((dr as u16 + sr as u16) >> 1) as u8;
let g = ((dg as u16 + sg as u16) >> 1) as u8;
let b = ((db as u16 + sb as u16) >> 1) as u8;
Color::pack_from_native(r, g, b)
}
BlendMode::HalfPlus => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let r = (dr as u16 + ((sr as u16) >> 1)).min(31) as u8;
let g = (dg as u16 + ((sg as u16) >> 1)).min(63) as u8;
let b = (db as u16 + ((sb as u16) >> 1)).min(31) as u8;
Color::pack_from_native(r, g, b)
}
BlendMode::HalfMinus => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let r = (dr as i16 - ((sr as i16) >> 1)).max(0) as u8;
let g = (dg as i16 - ((sg as i16) >> 1)).max(0) as u8;
let b = (db as i16 - ((sb as i16) >> 1)).max(0) as u8;
Color::pack_from_native(r, g, b)
}
BlendMode::Full => {
let (dr, dg, db) = Color::unpack_to_native(dst);
let (sr, sg, sb) = Color::unpack_to_native(src);
let r = (dr as u16 + sr as u16).min(31) as u8;
let g = (dg as u16 + sg as u16).min(63) as u8;
let b = (db as u16 + sb as u16).min(31) as u8;
Color::pack_from_native(r, g, b)
}
}
}