use crate::gfx_overlay::{DeferredGfxOverlay, OverlayCommand}; use crate::memory_banks::GlyphBankPoolAccess; use prometeu_hal::GfxBridge; use prometeu_hal::color::Color; use prometeu_hal::glyph::Glyph; use prometeu_hal::glyph_bank::GlyphBank; use prometeu_hal::scene_viewport_cache::{CachedTileEntry, SceneViewportCache}; use prometeu_hal::scene_viewport_resolver::{LayerCopyRequest, ResolverUpdate}; use prometeu_hal::sprite::Sprite; 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). /// /// `Gfx` owns the framebuffer backend and the canonical game-frame raster path /// consumed by `FrameComposer`. That canonical path covers scene composition, /// sprite composition, and fades. Public `gfx.*` primitives remain valid, but /// they do not define the canonical game composition contract; they belong to a /// separate final overlay/debug stage. 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, /// Back buffer: the working buffer where canonical game frames are composed /// before any final overlay/debug drain. back: Vec, /// Shared access to graphical memory banks (tiles and palettes). pub glyph_banks: Arc, /// Deferred overlay/debug capture kept separate from canonical game composition. overlay: DeferredGfxOverlay, /// Internal guard to replay deferred overlay commands without re-enqueueing them. is_draining_overlay: bool, /// 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 sprite count for the current frame state. sprite_count: usize, /// Internal cache used to sort sprites by layer while keeping stable priority order. layer_buckets: [Vec; 4], } const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7]; struct RenderTarget<'a> { back: &'a mut [u16], screen_w: usize, screen_h: usize, } #[derive(Clone, Copy)] struct CachedTileDraw<'a> { x: i32, y: i32, entry: CachedTileEntry, bank: &'a GlyphBank, tile_size: prometeu_hal::glyph_bank::TileSize, } #[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_no_scene_frame(&mut self) { self.render_no_scene_frame() } fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.render_scene_from_cache(cache, update) } fn load_frame_sprites(&mut self, sprites: &[Sprite]) { self.load_frame_sprites(sprites) } 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 sprite(&self, index: usize) -> &Sprite { &self.sprites[index] } fn sprite_mut(&mut self, index: usize) -> &mut Sprite { self.sprite_count = self.sprite_count.max(index.saturating_add(1)).min(self.sprites.len()); &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, glyph_banks: Arc) -> Self { const EMPTY_GLYPH: Glyph = Glyph { glyph_id: 0, palette_id: 0 }; const EMPTY_SPRITE: Sprite = Sprite { glyph: EMPTY_GLYPH, x: 0, y: 0, layer: 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], glyph_banks, overlay: DeferredGfxOverlay::default(), is_draining_overlay: false, sprites: [EMPTY_SPRITE; 512], sprite_count: 0, scene_fade_level: 31, scene_fade_color: Color::BLACK, hud_fade_level: 31, hud_fade_color: Color::BLACK, layer_buckets: [ 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) } pub fn begin_overlay_frame(&mut self) { self.overlay.begin_frame(); } pub fn overlay(&self) -> &DeferredGfxOverlay { &self.overlay } pub fn drain_overlay_debug(&mut self) { let commands = self.overlay.take_commands(); self.is_draining_overlay = true; for command in commands { match command { OverlayCommand::FillRectBlend { x, y, w, h, color, mode } => { self.fill_rect_blend(x, y, w, h, color, mode) } OverlayCommand::DrawLine { x0, y0, x1, y1, color } => { self.draw_line(x0, y0, x1, y1, color) } OverlayCommand::DrawCircle { x, y, r, color } => self.draw_circle(x, y, r, color), OverlayCommand::DrawDisc { x, y, r, border_color, fill_color } => { self.draw_disc(x, y, r, border_color, fill_color) } OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color } => { self.draw_square(x, y, w, h, border_color, fill_color) } OverlayCommand::DrawText { x, y, text, color } => { self.draw_text(x, y, &text, color) } } } self.is_draining_overlay = false; } /// 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 !self.is_draining_overlay { self.overlay.push(OverlayCommand::FillRectBlend { x, y, w, h, color, mode }); return; } 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 !self.is_draining_overlay { self.overlay.push(OverlayCommand::DrawLine { x0, y0, x1, y1, color }); return; } 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 !self.is_draining_overlay { self.overlay.push(OverlayCommand::DrawCircle { x: xc, y: yc, r, color }); return; } 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) { if !self.is_draining_overlay { self.overlay.push(OverlayCommand::DrawDisc { x, y, r, border_color, fill_color }); return; } 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, ) { if !self.is_draining_overlay { self.overlay.push(OverlayCommand::DrawSquare { x, y, w, h, border_color, fill_color }); return; } 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); } pub fn load_frame_sprites(&mut self, sprites: &[Sprite]) { self.sprite_count = sprites.len().min(self.sprites.len()); for (index, sprite) in sprites.iter().copied().take(self.sprites.len()).enumerate() { self.sprites[index] = Sprite { active: true, ..sprite }; } for sprite in self.sprites.iter_mut().skip(self.sprite_count) { sprite.active = false; } } /// 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_no_scene_frame(&mut self) { self.populate_layer_buckets(); for bucket in &self.layer_buckets { Self::draw_bucket_on_buffer( &mut self.back, self.w, self.h, bucket, &self.sprites, &*self.glyph_banks, ); } // 2. Scene-only fallback path: sprites and fades still work even before a // cache-backed world composition request is issued for the frame. Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); // 3. HUD Fade: independent from scene fade; HUD composition itself remains external. Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); } /// Composes the world from the viewport cache using resolver copy requests. /// /// This is the cache-backed world path accepted by DEC-0013. The canonical scene /// is not consulted here; the renderer only consumes prepared cache materialization /// plus sprite state and fade controls. pub fn render_scene_from_cache(&mut self, cache: &SceneViewportCache, update: &ResolverUpdate) { self.back.fill(Color::BLACK.raw()); self.populate_layer_buckets(); for layer_index in 0..cache.layers.len() { Self::draw_bucket_on_buffer( &mut self.back, self.w, self.h, &self.layer_buckets[layer_index], &self.sprites, &*self.glyph_banks, ); Self::draw_cache_layer_to_buffer( &mut self.back, self.w, self.h, cache, &update.copy_requests[layer_index], &*self.glyph_banks, ); } Self::apply_fade_to_buffer(&mut self.back, self.scene_fade_level, self.scene_fade_color); Self::apply_fade_to_buffer(&mut self.back, self.hud_fade_level, self.hud_fade_color); } fn populate_layer_buckets(&mut self) { for bucket in self.layer_buckets.iter_mut() { bucket.clear(); } for (idx, sprite) in self.sprites.iter().take(self.sprite_count).enumerate() { if sprite.active && (sprite.layer as usize) < self.layer_buckets.len() { self.layer_buckets[sprite.layer as usize].push(idx); } } for bucket in self.layer_buckets.iter_mut() { bucket.sort_by_key(|&idx| self.sprites[idx].priority); } } fn draw_cache_layer_to_buffer( back: &mut [u16], screen_w: usize, screen_h: usize, cache: &SceneViewportCache, request: &LayerCopyRequest, glyph_banks: &dyn GlyphBankPoolAccess, ) { let mut target = RenderTarget { back, screen_w, screen_h }; let layer_cache = &cache.layers[request.layer_index]; if !layer_cache.valid { return; } let Some(bank) = glyph_banks.glyph_bank_slot(layer_cache.glyph_bank_id as usize) else { return; }; let tile_size_px = request.tile_size as i32; for cache_y in 0..layer_cache.height() { let screen_tile_y = cache_y as i32 * tile_size_px - request.source_offset_y_px; if screen_tile_y >= screen_h as i32 || screen_tile_y + tile_size_px <= 0 { continue; } for cache_x in 0..layer_cache.width() { let screen_tile_x = cache_x as i32 * tile_size_px - request.source_offset_x_px; if screen_tile_x >= screen_w as i32 || screen_tile_x + tile_size_px <= 0 { continue; } let entry = layer_cache.entry(cache_x, cache_y); if !entry.active { continue; } Self::draw_cached_tile_pixels( &mut target, CachedTileDraw { x: screen_tile_x, y: screen_tile_y, entry, bank: &bank, tile_size: request.tile_size, }, ); } } } fn draw_cached_tile_pixels(target: &mut RenderTarget<'_>, tile: CachedTileDraw<'_>) { let size = tile.tile_size as usize; for local_y in 0..size { let world_y = tile.y + local_y as i32; if world_y < 0 || world_y >= target.screen_h as i32 { continue; } for local_x in 0..size { let world_x = tile.x + local_x as i32; if world_x < 0 || world_x >= target.screen_w as i32 { continue; } let fetch_x = if tile.entry.flip_x() { size - 1 - local_x } else { local_x }; let fetch_y = if tile.entry.flip_y() { size - 1 - local_y } else { local_y }; let px_index = tile.bank.get_pixel_index(tile.entry.glyph_id, fetch_x, fetch_y); if px_index == 0 { continue; } let color = tile.bank.resolve_color(tile.entry.palette_id, px_index); target.back[world_y as usize * target.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], glyph_banks: &dyn GlyphBankPoolAccess, ) { for &idx in bucket { let s = &sprites[idx]; let bank_id = s.bank_id as usize; if let Some(bank) = glyph_banks.glyph_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: &GlyphBank, ) { // ... (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.glyph.glyph_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.glyph.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) { if !self.is_draining_overlay { self.overlay.push(OverlayCommand::DrawText { x, y, text: text.to_string(), color }); return; } 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).clamp(0, 5) as usize; let row_end = (screen_h - y).clamp(0, 5) as usize; let col_start = (-x).clamp(0, 3) as usize; let col_end = (screen_w - x).clamp(0, 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::FrameComposer; use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks, SceneBankPoolAccess}; use prometeu_hal::glyph_bank::TileSize; use prometeu_hal::scene_bank::SceneBank; use prometeu_hal::scene_layer::{ParallaxFactor, SceneLayer}; use prometeu_hal::scene_viewport_cache::SceneViewportCache; use prometeu_hal::scene_viewport_resolver::SceneViewportResolver; use prometeu_hal::tile::Tile; use prometeu_hal::tilemap::TileMap; fn make_glyph_bank(tile_size: TileSize, palette_colors: &[(u8, Color)]) -> GlyphBank { let size = tile_size as usize; let mut bank = GlyphBank::new(tile_size, size, size); for (palette_id, color) in palette_colors { bank.palettes[*palette_id as usize][1] = *color; } for y in 0..size { for x in 0..size { bank.pixel_indices[y * bank.width + x] = 1; } } bank } fn make_layer( glyph_bank_id: u8, glyph_id: u16, palette_id: u8, width: usize, height: usize, ) -> SceneLayer { SceneLayer { active: true, glyph_bank_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width, height, tiles: vec![ Tile { active: true, glyph: Glyph { glyph_id, palette_id }, flip_x: false, flip_y: false, }; width * height ], }, } } fn make_inactive_layer(glyph_bank_id: u8, width: usize, height: usize) -> SceneLayer { SceneLayer { active: false, glyph_bank_id, tile_size: TileSize::Size8, parallax_factor: ParallaxFactor { x: 1.0, y: 1.0 }, tilemap: TileMap { width, height, tiles: vec![Tile::default(); width * height] }, } } fn make_scene(palette_ids: [u8; 4]) -> SceneBank { SceneBank { layers: [ make_layer(0, 0, palette_ids[0], 8, 8), make_layer(0, 0, palette_ids[1], 8, 8), make_layer(0, 0, palette_ids[2], 8, 8), make_layer(0, 0, palette_ids[3], 8, 8), ], } } fn make_scene_with_inactive_top_layers() -> SceneBank { SceneBank { layers: [ make_layer(0, 0, 0, 8, 8), make_layer(0, 0, 1, 8, 8), make_layer(0, 0, 2, 8, 8), make_inactive_layer(0, 8, 8), ], } } #[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.begin_overlay_frame(); gfx.draw_line(0, 0, 9, 9, Color::WHITE); gfx.drain_overlay_debug(); assert_eq!(gfx.back[0], Color::WHITE.0); assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0); assert_eq!(gfx.overlay().command_count(), 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.begin_overlay_frame(); gfx.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK); gfx.drain_overlay_debug(); // Border assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0); // Fill assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0); assert_eq!(gfx.overlay().command_count(), 0); } #[test] fn draw_text_captures_overlay_command() { let banks = Arc::new(MemoryBanks::new()); let mut gfx = Gfx::new(32, 18, banks); gfx.begin_overlay_frame(); gfx.draw_text(4, 5, "HUD", Color::WHITE); assert_eq!( gfx.overlay().commands(), &[OverlayCommand::DrawText { x: 4, y: 5, text: "HUD".into(), color: Color::WHITE }] ); } #[test] fn overlay_state_is_separate_from_frame_composer_sprite_state() { let banks = Arc::new(MemoryBanks::new()); let mut gfx = Gfx::new(32, 18, Arc::clone(&banks) as Arc); let mut frame_composer = FrameComposer::new(32, 18, Arc::clone(&banks) as Arc); gfx.begin_overlay_frame(); frame_composer.begin_frame(); frame_composer.emit_sprite(Sprite { glyph: Glyph { glyph_id: 0, palette_id: 0 }, x: 1, y: 2, layer: 0, bank_id: 0, active: true, flip_x: false, flip_y: false, priority: 0, }); gfx.draw_text(1, 1, "X", Color::WHITE); assert_eq!(frame_composer.sprite_controller().sprite_count(), 1); assert_eq!(gfx.overlay().command_count(), 1); } #[test] fn render_scene_from_cache_uses_materialized_cache_not_canonical_scene() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank( 0, Arc::new(make_glyph_bank(TileSize::Size8, &[(0, Color::RED), (1, Color::GREEN)])), ); let mut scene = make_scene([0, 0, 0, 0]); let mut cache = SceneViewportCache::new(&scene, 4, 4); cache.materialize_all_layers(&scene); scene.layers[0].tilemap.tiles[0].glyph.palette_id = 1; let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20); let update = resolver.update(&scene, 0, 0); let mut gfx = Gfx::new(16, 16, banks); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; gfx.render_scene_from_cache(&cache, &update); assert_eq!(gfx.back[0], Color::RED.raw()); } #[test] fn render_scene_from_cache_preserves_layer_and_sprite_order() { let banks = Arc::new(MemoryBanks::new()); banks.install_glyph_bank( 0, Arc::new(make_glyph_bank( TileSize::Size8, &[(0, Color::RED), (1, Color::GREEN), (2, Color::BLUE), (4, Color::WHITE)], )), ); let scene = make_scene_with_inactive_top_layers(); let mut cache = SceneViewportCache::new(&scene, 4, 4); cache.materialize_all_layers(&scene); let mut resolver = SceneViewportResolver::new(16, 16, 4, 4, 12, 20); let update = resolver.update(&scene, 0, 0); let mut gfx = Gfx::new(16, 16, Arc::clone(&banks) as Arc); gfx.scene_fade_level = 31; gfx.hud_fade_level = 31; gfx.sprites[0] = Sprite { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, layer: 0, bank_id: 0, active: true, flip_x: false, flip_y: false, priority: 0, }; gfx.sprites[1] = Sprite { glyph: Glyph { glyph_id: 0, palette_id: 4 }, x: 0, y: 0, layer: 2, bank_id: 0, active: true, flip_x: false, flip_y: false, priority: 2, }; gfx.sprite_count = 2; gfx.render_scene_from_cache(&cache, &update); assert_eq!(gfx.back[0], Color::BLUE.raw()); } #[test] fn load_frame_sprites_replaces_slot_first_submission_for_render_state() { let banks = Arc::new(MemoryBanks::new()); let mut gfx = Gfx::new(16, 16, banks as Arc); gfx.load_frame_sprites(&[ Sprite { glyph: Glyph { glyph_id: 1, palette_id: 2 }, x: 2, y: 3, layer: 1, bank_id: 4, active: false, flip_x: true, flip_y: false, priority: 7, }, Sprite { glyph: Glyph { glyph_id: 5, palette_id: 6 }, x: 7, y: 8, layer: 3, bank_id: 9, active: false, flip_x: false, flip_y: true, priority: 1, }, ]); assert_eq!(gfx.sprite_count, 2); assert!(gfx.sprites[0].active); assert!(gfx.sprites[1].active); assert!(!gfx.sprites[2].active); assert_eq!(gfx.sprites[0].layer, 1); assert_eq!(gfx.sprites[1].glyph.glyph_id, 5); } } /// 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) } } }