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 }; // 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, 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) total += size_of::(); total += layer.map.tiles.len() * size_of::(); } // 3. HUD Layer total += size_of::(); total += self.hud.map.tiles.len() * size_of::(); // 4. Tile Banks (Assets e Paletas) for bank_opt in &self.banks { if let Some(bank) = bank_opt { total += size_of::(); total += bank.pixel_indices.len(); // Tabela de Paletas: 256 paletas * 16 cores * tamanho da struct Color total += 256 * 16 * size_of::(); } } // 5. Sprites (OAM) // Array fixo de 512 Sprites. total += self.sprites.len() * 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) } } }