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, back: Vec, pub layers: [ScrollableTileLayer; 4], pub hud: HudTileLayer, pub banks: [Option; 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 = 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; 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 }; let color = bank.get_pixel(tile.id, fetch_x, fetch_y); if color == Color::COLOR_KEY { continue; } back[world_y as usize * screen_w + world_x as usize] = color.0; } } } 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 ) { let size = bank.tile_size as usize; let sprite_w = size as i32; let sprite_h = size as i32; // 1. Clipping Rápido: Se o sprite estiver totalmente fora, nem processamos if sprite.x <= -sprite_w || sprite.x >= screen_w as i32 || sprite.y <= -sprite_h || sprite.y >= screen_h as i32 { return; } // 2. Calcular a área visível do sprite (Interseção com a tela) let start_x = sprite.x.max(0); let start_y = sprite.y.max(0); let end_x = (sprite.x + sprite_w).min(screen_w as i32); let end_y = (sprite.y + sprite_h).min(screen_h as i32); // 3. Loop apenas sobre os pixels visíveis na tela for world_y in start_y..end_y { for world_x in start_x..end_x { // Calcular a posição local dentro do tile considerando o scroll/posição do sprite let local_x = (world_x - sprite.x) as usize; let local_y = (world_y - sprite.y) as usize; // Lógica de Flip (Espelhamento) 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 }; let color = bank.get_pixel(sprite.tile.id, fetch_x, fetch_y); if color == Color::COLOR_KEY { continue; } let idx = (world_y as usize * screen_w) + world_x as usize; back[idx] = color.0; } } } /// 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 estimado de memória das estruturas gráficas em bytes. /// Isso ajuda a monitorar o "custo" do hardware simulado. pub fn memory_usage_bytes(&self) -> usize { let mut total = 0; // Buffers de tela (Front + Back) total += self.front.len() * 2; total += self.back.len() * 2; // Layers (Tiles + Metadados) for layer in &self.layers { total += layer.map.tiles.len() * std::mem::size_of::(); total += std::mem::size_of::(); } // HUD total += self.hud.map.tiles.len() * std::mem::size_of::(); total += std::mem::size_of::(); // Tile Banks (Apenas bancos carregados) for bank_opt in &self.banks { if let Some(bank) = bank_opt { total += bank.pixels.len() * 2; // RGB565 = 2 bytes total += std::mem::size_of::(); } } // Sprites (OAM) total += self.sprites.len() * std::mem::size_of::(); 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) } } }