427 lines
14 KiB
Rust
427 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 += std::mem::size_of::<ScrollableTileLayer>();
|
|
total += layer.map.tiles.len() * std::mem::size_of::<crate::model::Tile>();
|
|
}
|
|
|
|
// 3. HUD Layer
|
|
total += std::mem::size_of::<HudTileLayer>();
|
|
total += self.hud.map.tiles.len() * std::mem::size_of::<crate::model::Tile>();
|
|
|
|
// 4. Tile Banks (Assets e Paletas)
|
|
for bank_opt in &self.banks {
|
|
if let Some(bank) = bank_opt {
|
|
total += std::mem::size_of::<TileBank>();
|
|
|
|
// Buffer de Índices (pixel_indices: Vec<u8>)
|
|
// Cada pixel ocupa exatamente 1 byte.
|
|
total += bank.pixel_indices.len();
|
|
|
|
// Tabela de Paletas (palettes: [[u16; 16]; 256])
|
|
// 256 paletas * 16 cores * 2 bytes cada.
|
|
total += 256 * 16 * 2;
|
|
}
|
|
}
|
|
|
|
// 5. Sprites (OAM)
|
|
// Array fixo de 512 Sprites.
|
|
total += self.sprites.len() * std::mem::size_of::<crate::model::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)
|
|
}
|
|
}
|
|
}
|