2026-03-24 13:40:10 +00:00

423 lines
14 KiB
Rust

use crate::model::{Color, HudTileLayer, ScrollableTileLayer, Sprite, TileBank, TileMap, TileSize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlendMode {
/// dst = src
#[default]
None,
/// dst = (src + dst) / 2
Half,
/// dst = dst + (src / 2)
HalfPlus,
/// dst = dst - (src / 2)
HalfMinus,
/// dst = dst + src
Full,
}
pub struct Gfx {
w: usize,
h: usize,
front: Vec<u16>,
back: Vec<u16>,
pub layers: [ScrollableTileLayer; 4],
pub hud: HudTileLayer,
pub banks: [Option<TileBank>; 16],
pub sprites: [Sprite; 512],
pub scene_fade_level: u8, // 0..31
pub scene_fade_color: Color,
pub hud_fade_level: u8, // 0..31
pub hud_fade_color: Color,
}
impl Gfx {
pub fn new(w: usize, h: usize) -> Self {
const EMPTY_BANK: Option<TileBank> = None;
const EMPTY_SPRITE: Sprite = Sprite {
tile: crate::model::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;
Self {
w,
h,
front: vec![0; len],
back: vec![0; len],
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),
],
hud: HudTileLayer::new(64, 32),
banks: [EMPTY_BANK; 16],
sprites: [EMPTY_SPRITE; 512],
scene_fade_level: 31,
scene_fade_color: Color::BLACK,
hud_fade_level: 31,
hud_fade_color: Color::BLACK,
}
}
pub fn size(&self) -> (usize, usize) {
(self.w, self.h)
}
/// O buffer que o host deve exibir (RGB565).
pub fn front_buffer(&self) -> &[u16] {
&self.front
}
pub fn clear(&mut self, color: Color) {
self.back.fill(color.0);
}
/// Retângulo com modo de blend.
pub fn fill_rect_blend(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
color: Color,
mode: BlendMode,
) {
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);
}
}
}
/// Conveniência: retângulo normal (sem 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);
}
/// Double buffer swap (O(1), sem cópia de pixels).
pub fn present(&mut self) {
std::mem::swap(&mut self.front, &mut self.back);
}
/// Pipeline principal de renderização do frame.
/// Segue a ordem de prioridade do manual (Capítulo 4.11).
pub fn render_all(&mut self) {
// 0. Fase de Preparação: Organiza quem deve ser desenhado em cada camada
// Criamos listas de índices para evitar percorrer os 512 sprites 5 vezes
let mut priority_buckets: [Vec<usize>; 5] = [
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
Vec::with_capacity(128),
];
for (idx, sprite) in self.sprites.iter().enumerate() {
if sprite.active && sprite.priority < 5 {
priority_buckets[sprite.priority as usize].push(idx);
}
}
// 1. Sprites de prioridade 0 (Atrás da Layer 0 - o fundo do fundo)
self.draw_bucket(&priority_buckets[0]);
for i in 0..self.layers.len() {
// 2. Layers de jogo (0 a 3)
self.render_layer(i);
// 3. Sprites de acordo com prioridade
// i=0 desenha sprites priority 1 (sobre layer 0)
// i=1 desenha sprites priority 2 (sobre layer 1)
// i=2 desenha sprites priority 3 (sobre layer 2)
// i=3 desenha sprites priority 4 (sobre layer 3 - à frente de tudo)
self.draw_bucket(&priority_buckets[i + 1]);
}
// 4. Aplica Scene Fade (Afeta tudo desenhado até agora)
Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color);
// 5. HUD (Sempre por cima)
self.render_hud();
// 6. Aplica HUD Fade (Opcional, apenas para o HUD)
Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color);
}
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.banks.get(bank_id) {
Some(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);
}
/// Renderiza o HUD (sem scroll).
pub fn render_hud(&mut self) {
let bank_id = self.hud.bank_id as usize;
let bank = match self.banks.get(bank_id) {
Some(Some(b)) => b,
_ => return,
};
Self::draw_tile_map(&mut self.back, self.w, self.h, &self.hud.map, bank, 0, 0);
}
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. Calcular quantos tiles cabem na tela (com margem de 1 para o scroll)
let visible_tiles_x = (screen_w / tile_size) + 1;
let visible_tiles_y = (screen_h / tile_size) + 1;
// 2. Calcular o offset inicial (onde o primeiro tile começa a ser desenhado)
let start_tile_x = scroll_x / tile_size as i32;
let start_tile_y = scroll_y / tile_size as i32;
let fine_scroll_x = scroll_x % tile_size as i32;
let fine_scroll_y = scroll_y % tile_size as i32;
// 3. Loop por Tile (Muito mais eficiente)
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;
// Skip se estiver fora dos limites do mapa
if map_x >= map.width || map_y >= map.height { continue; }
let tile = map.tiles[map_y * map.width + map_x];
if tile.id == 0 { continue; }
// 4. Desenha o bloco do tile na tela
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);
}
}
}
// Função auxiliar para desenhar um bloco de 8x8, 16x16 ou 32x32 pixels
fn draw_tile_pixels(back: &mut [u16], screen_w: usize, screen_h: usize, x: i32, y: i32, tile: crate::model::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; }
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. Pega o índice do pixel no banco
let px_index = bank.get_pixel_index(tile.id, fetch_x, fetch_y);
// 2. Regra: Índice 0 é transparente
if px_index == 0 { continue; }
// 3. Resolve a cor usando a paleta do tile
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(&mut self, bucket: &[usize]) {
for &idx in bucket {
let sprite = &self.sprites[idx];
let bank = match self.banks.get(sprite.bank_id as usize) {
Some(Some(b)) => b,
_ => continue,
};
Self::draw_sprite_pixel_by_pixel(&mut self.back, self.w, self.h, sprite, bank);
}
}
fn draw_sprite_pixel_by_pixel(
back: &mut [u16],
screen_w: usize,
screen_h: usize,
sprite: &Sprite,
bank: &TileBank
) {
// ... (mesmo cálculo de bounds/clipping que já tínhamos) ...
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. Pega o índice
let px_index = bank.get_pixel_index(sprite.tile.id, fetch_x, fetch_y);
// 2. Transparência
if px_index == 0 { continue; }
// 3. Resolve cor via paleta (do tile dentro do 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();
}
}
}
/// Aplica o efeito de fade em todo o back buffer.
/// level: 0 (cor total) até 31 (visível)
fn apply_fade_to_buffer(back: &mut [u16], level: u8, fade_color: Color) {
if level >= 31 { return; } // Totalmente visível, pula processamento
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);
// Fórmula: (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);
}
}
/// Retorna o uso real de memória das estruturas gráficas em bytes.
/// Reflete os buffers de índices, paletas e OAM conforme Capítulo 10.8.
pub fn memory_usage_bytes(&self) -> usize {
let mut total = 0;
// 1. Framebuffers (Front + Back)
// Cada um é Vec<u16>, ocupando 2 bytes por pixel.
total += self.front.len() * 2;
total += self.back.len() * 2;
// 2. Tile Layers (4 Game Layers)
for layer in &self.layers {
// Tamanho da struct + os dados do mapa (Vec<Tile>)
total += size_of::<ScrollableTileLayer>();
total += layer.map.tiles.len() * size_of::<crate::model::Tile>();
}
// 3. HUD Layer
total += size_of::<HudTileLayer>();
total += self.hud.map.tiles.len() * size_of::<crate::model::Tile>();
// 4. Tile Banks (Assets e Paletas)
for bank_opt in &self.banks {
if let Some(bank) = bank_opt {
total += size_of::<TileBank>();
total += bank.pixel_indices.len();
// Tabela de Paletas: 256 paletas * 16 cores * tamanho da struct Color
total += 256 * 16 * size_of::<Color>();
}
}
// 5. Sprites (OAM)
// Array fixo de 512 Sprites.
total += self.sprites.len() * size_of::<Sprite>();
total
}
}
/// Faz blend em RGB565 por canal com saturação.
/// `dst` e `src` são pixels RGB565 (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)
}
}
}