1405 lines
46 KiB
Rust

use crate::memory_banks::GlyphBankPoolAccess;
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 prometeu_hal::{Game2DFramePacket, Gfx2dCommand, GfxBridge};
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.
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<u32>,
/// Back buffer: the working buffer where canonical game frames are composed
back: Vec<u32>,
/// Shared access to graphical memory banks (tiles and palettes).
pub glyph_banks: Arc<dyn GlyphBankPoolAccess>,
/// 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<usize>; 4],
}
// const GLYPH_UNKNOWN: [u8; 5] = [0x7, 0x7, 0x7, 0x7, 0x7];
struct RenderTarget<'a> {
back: &'a mut [u32],
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,
}
pub const HW_FONT_WIDTH: usize = 7;
pub const HW_FONT_HEIGHT: usize = 6;
pub const HW_FONT_SPACING_X: usize = 1;
pub const HW_FONT_SPACING_Y: usize = 2;
pub const HW_FONT_ADVANCE_X: usize = HW_FONT_WIDTH + HW_FONT_SPACING_X; // 8
pub const HW_FONT_ADVANCE_Y: usize = HW_FONT_HEIGHT + HW_FONT_SPACING_Y; // 8
pub const GLYPH_UNKNOWN: [u8; HW_FONT_HEIGHT] = [
0x7F, // #######
0x41, // #.....#
0x5D, // #.###.#
0x55, // #.#.#.#
0x41, // #.....#
0x7F, // #######
];
pub const GLYPH_SPACE: [u8; HW_FONT_HEIGHT] = [
0x00, // .......
0x00, // .......
0x00, // .......
0x00, // .......
0x00, // .......
0x00, // .......
];
#[inline]
pub fn glyph_for_char(c: char) -> &'static [u8; HW_FONT_HEIGHT] {
match c {
'0' => &[0x3E, 0x41, 0x45, 0x49, 0x41, 0x3E],
'1' => &[0x08, 0x18, 0x08, 0x08, 0x08, 0x1C],
'2' => &[0x3E, 0x41, 0x01, 0x0E, 0x30, 0x7F],
'3' => &[0x7E, 0x01, 0x1E, 0x01, 0x41, 0x3E],
'4' => &[0x42, 0x42, 0x42, 0x7F, 0x02, 0x02],
'5' => &[0x7F, 0x40, 0x7E, 0x01, 0x41, 0x3E],
'6' => &[0x3E, 0x40, 0x7E, 0x41, 0x41, 0x3E],
'7' => &[0x7F, 0x01, 0x02, 0x04, 0x08, 0x08],
'8' => &[0x3E, 0x41, 0x3E, 0x41, 0x41, 0x3E],
'9' => &[0x3E, 0x41, 0x41, 0x3F, 0x01, 0x3E],
'a' => &[0x00, 0x3E, 0x01, 0x3F, 0x41, 0x3F],
'b' => &[0x40, 0x40, 0x7E, 0x41, 0x41, 0x7E],
'c' => &[0x00, 0x3E, 0x41, 0x40, 0x41, 0x3E],
'd' => &[0x01, 0x01, 0x3F, 0x41, 0x41, 0x3F],
'e' => &[0x00, 0x3E, 0x41, 0x7F, 0x40, 0x3E],
'f' => &[0x0E, 0x10, 0x3E, 0x10, 0x10, 0x10],
'g' => &[0x00, 0x3F, 0x41, 0x3F, 0x01, 0x3E],
'h' => &[0x40, 0x40, 0x7E, 0x41, 0x41, 0x41],
'i' => &[0x08, 0x00, 0x18, 0x08, 0x08, 0x1C],
'j' => &[0x04, 0x00, 0x0C, 0x04, 0x44, 0x38],
'k' => &[0x40, 0x42, 0x44, 0x78, 0x44, 0x42],
'l' => &[0x18, 0x08, 0x08, 0x08, 0x08, 0x1C],
'm' => &[0x00, 0x76, 0x49, 0x49, 0x49, 0x49],
'n' => &[0x00, 0x7E, 0x41, 0x41, 0x41, 0x41],
'o' => &[0x00, 0x3E, 0x41, 0x41, 0x41, 0x3E],
'p' => &[0x00, 0x7E, 0x41, 0x7E, 0x40, 0x40],
'q' => &[0x00, 0x3F, 0x41, 0x3F, 0x01, 0x01],
'r' => &[0x00, 0x5E, 0x61, 0x40, 0x40, 0x40],
's' => &[0x00, 0x3F, 0x40, 0x3E, 0x01, 0x7E],
't' => &[0x10, 0x10, 0x7C, 0x10, 0x10, 0x0E],
'u' => &[0x00, 0x41, 0x41, 0x41, 0x43, 0x3D],
'v' => &[0x00, 0x41, 0x41, 0x22, 0x22, 0x1C],
'w' => &[0x00, 0x41, 0x49, 0x49, 0x55, 0x22],
'x' => &[0x00, 0x41, 0x22, 0x1C, 0x22, 0x41],
'y' => &[0x00, 0x41, 0x41, 0x3F, 0x01, 0x3E],
'z' => &[0x00, 0x7F, 0x02, 0x0C, 0x10, 0x7F],
'A' => &[0x3E, 0x41, 0x41, 0x7F, 0x41, 0x41],
'B' => &[0x7E, 0x41, 0x7E, 0x41, 0x41, 0x7E],
'C' => &[0x3E, 0x41, 0x40, 0x40, 0x41, 0x3E],
'D' => &[0x7E, 0x41, 0x41, 0x41, 0x41, 0x7E],
'E' => &[0x7F, 0x40, 0x7C, 0x40, 0x40, 0x7F],
'F' => &[0x7F, 0x40, 0x7C, 0x40, 0x40, 0x40],
'G' => &[0x3E, 0x41, 0x40, 0x4F, 0x41, 0x3E],
'H' => &[0x41, 0x41, 0x7F, 0x41, 0x41, 0x41],
'I' => &[0x7F, 0x08, 0x08, 0x08, 0x08, 0x7F],
'J' => &[0x01, 0x01, 0x01, 0x41, 0x41, 0x3E],
'K' => &[0x41, 0x42, 0x7C, 0x42, 0x41, 0x41],
'L' => &[0x40, 0x40, 0x40, 0x40, 0x40, 0x7F],
'M' => &[0x41, 0x63, 0x55, 0x49, 0x41, 0x41],
'N' => &[0x41, 0x61, 0x51, 0x49, 0x45, 0x43],
'O' => &[0x3E, 0x41, 0x41, 0x41, 0x41, 0x3E],
'P' => &[0x7E, 0x41, 0x41, 0x7E, 0x40, 0x40],
'Q' => &[0x3E, 0x41, 0x41, 0x49, 0x45, 0x3F],
'R' => &[0x7E, 0x41, 0x41, 0x7E, 0x42, 0x41],
'S' => &[0x3F, 0x40, 0x3E, 0x01, 0x41, 0x3E],
'T' => &[0x7F, 0x08, 0x08, 0x08, 0x08, 0x08],
'U' => &[0x41, 0x41, 0x41, 0x41, 0x41, 0x3E],
'V' => &[0x41, 0x41, 0x41, 0x22, 0x22, 0x1C],
'W' => &[0x41, 0x41, 0x49, 0x55, 0x63, 0x41],
'X' => &[0x41, 0x22, 0x1C, 0x1C, 0x22, 0x41],
'Y' => &[0x41, 0x22, 0x1C, 0x08, 0x08, 0x08],
'Z' => &[0x7F, 0x02, 0x04, 0x08, 0x10, 0x7F],
':' => &[0x00, 0x08, 0x08, 0x00, 0x08, 0x08],
'.' => &[0x00, 0x00, 0x00, 0x00, 0x18, 0x18],
',' => &[0x00, 0x00, 0x00, 0x00, 0x18, 0x10],
'!' => &[0x08, 0x08, 0x08, 0x08, 0x00, 0x08],
'?' => &[0x3E, 0x41, 0x01, 0x0E, 0x00, 0x08],
' ' => &GLYPH_SPACE,
'|' => &[0x08, 0x08, 0x08, 0x08, 0x08, 0x08],
'/' => &[0x01, 0x02, 0x04, 0x08, 0x10, 0x40],
'\\' => &[0x40, 0x10, 0x08, 0x04, 0x02, 0x01],
'(' => &[0x04, 0x08, 0x10, 0x10, 0x08, 0x04],
')' => &[0x10, 0x08, 0x04, 0x04, 0x08, 0x10],
'>' => &[0x20, 0x10, 0x08, 0x10, 0x20, 0x00],
'<' => &[0x02, 0x04, 0x08, 0x04, 0x02, 0x00],
'-' => &[0x00, 0x00, 0x7F, 0x00, 0x00, 0x00],
'_' => &[0x00, 0x00, 0x00, 0x00, 0x00, 0x7F],
'+' => &[0x00, 0x08, 0x08, 0x3E, 0x08, 0x08],
'=' => &[0x00, 0x00, 0x3E, 0x00, 0x3E, 0x00],
'*' => &[0x00, 0x22, 0x14, 0x7F, 0x14, 0x22],
'#' => &[0x14, 0x14, 0x7F, 0x14, 0x7F, 0x14],
'%' => &[0x61, 0x62, 0x04, 0x08, 0x13, 0x23],
'@' => &[0x3E, 0x41, 0x5D, 0x55, 0x5E, 0x40],
_ => &GLYPH_UNKNOWN,
}
}
impl GfxBridge for Gfx {
fn size(&self) -> (usize, usize) {
self.size()
}
fn front_buffer(&self) -> &[u32] {
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,
resolved_glyph_slots: &[usize; 4],
) {
self.render_scene_from_cache(cache, update, resolved_glyph_slots)
}
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<dyn GlyphBankPoolAccess>) -> 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,
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)
}
/// The buffer that the host should display (RGBA8888 in RGBA raw order).
pub fn front_buffer(&self) -> &[u32] {
&self.front
}
pub fn clear(&mut self, color: Color) {
self.back.fill(color.raw());
}
/// Rectangle with blend mode.
pub fn fill_rect_blend(
&mut self,
x: i32,
y: i32,
w: i32,
h: i32,
color: Color,
mode: BlendMode,
) {
if color.alpha() == 0 {
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.raw();
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_rgba8888(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.alpha() == 0 {
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.raw();
}
}
/// 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 color.alpha() == 0 {
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 color.alpha() == 0 {
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.alpha() == 0 {
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) {
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.alpha() == 0 {
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,
) {
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.alpha() == 0 {
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.raw();
}
}
fn draw_vertical_line(&mut self, x: i32, y0: i32, y1: i32, color: Color) {
if color.alpha() == 0 {
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.raw();
}
}
/// 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,
resolved_glyph_slots: &[usize; 4],
) {
self.back.fill(Color::BLACK.raw());
self.populate_layer_buckets();
for (layer_index, glyph_slot) in
resolved_glyph_slots.iter().enumerate().take(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],
*glyph_slot,
&*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);
}
pub fn render_game2d_frame_packet(&mut self, packet: &Game2DFramePacket) {
let sprites: Vec<Sprite> = packet
.composer
.sprites
.iter()
.map(|sprite| Sprite {
glyph: Glyph {
glyph_id: sprite.glyph_id as u16,
palette_id: sprite.palette_id as u8,
},
x: sprite.x,
y: sprite.y,
layer: sprite.layer as u8,
bank_id: sprite.bank_id as u8,
active: true,
flip_x: sprite.flip_x,
flip_y: sprite.flip_y,
priority: sprite.priority as u8,
})
.collect();
self.load_frame_sprites(&sprites);
self.render_no_scene_frame();
for command in &packet.gfx2d {
self.apply_gfx2d_command(command);
}
}
fn apply_gfx2d_command(&mut self, command: &Gfx2dCommand) {
match command {
Gfx2dCommand::Clear { color } => self.clear(*color),
Gfx2dCommand::FillRect { rect, color } => {
self.fill_rect(rect.x, rect.y, rect.w, rect.h, *color);
}
Gfx2dCommand::DrawLine { x0, y0, x1, y1, color } => {
self.draw_line(*x0, *y0, *x1, *y1, *color);
}
Gfx2dCommand::DrawCircle { x, y, radius, color } => {
self.draw_circle(*x, *y, *radius, *color);
}
Gfx2dCommand::DrawDisc { x, y, radius, border_color, fill_color } => {
self.draw_disc(*x, *y, *radius, *border_color, *fill_color);
}
Gfx2dCommand::DrawSquare { rect, border_color, fill_color } => {
self.draw_square(rect.x, rect.y, rect.w, rect.h, *border_color, *fill_color);
}
Gfx2dCommand::DrawText { x, y, text, color } => {
self.draw_text(*x, *y, text, *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 [u32],
screen_w: usize,
screen_h: usize,
cache: &SceneViewportCache,
request: &LayerCopyRequest,
glyph_slot: usize,
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(glyph_slot) else {
panic!(
"SCENE composition fatal: resolved glyph slot {} for layer {} is not resident",
glyph_slot, request.layer_index
);
};
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);
let color = tile.bank.resolve_color(tile.entry.palette_id, px_index);
if color.alpha() == 0 {
continue;
}
target.back[world_y as usize * target.screen_w + world_x as usize] = color.raw();
}
}
}
fn draw_bucket_on_buffer(
back: &mut [u32],
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 [u32],
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 };
let px_index = bank.get_pixel_index(sprite.glyph.glyph_id, fetch_x, fetch_y);
let color = bank.resolve_color(sprite.glyph.palette_id, px_index);
if color.alpha() == 0 {
continue;
}
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 [u32], 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, fa) = unpack_rgba8888(fade_color.raw());
for px in back.iter_mut() {
let (sr, sg, sb, sa) = unpack_rgba8888(*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;
let a = ((sa as u16 * weight + fa as u16 * inv_weight) / 31) as u8;
*px = pack_rgba8888(r, g, b, a);
}
}
pub fn draw_text(&mut self, x: i32, y: i32, text: &str, color: Color) {
let mut cx = x;
let mut cy = y;
for c in text.chars() {
match c {
'\n' => {
cx = x;
cy += HW_FONT_ADVANCE_Y as i32;
}
'\r' => {}
'\t' => {
cx += (HW_FONT_ADVANCE_X * 4) as i32;
}
_ => {
self.draw_char(cx, cy, c, color);
cx += HW_FONT_ADVANCE_X as i32;
}
}
}
}
fn draw_char(&mut self, x: i32, y: i32, c: char, color: Color) {
if color.alpha() == 0 {
return;
}
let screen_w = self.w as i32;
let screen_h = self.h as i32;
let glyph_w = HW_FONT_WIDTH as i32;
let glyph_h = HW_FONT_HEIGHT as i32;
if x >= screen_w || y >= screen_h || x + glyph_w <= 0 || y + glyph_h <= 0 {
return;
}
let glyph = glyph_for_char(c);
let raw = color.raw();
let row_start = (-y).clamp(0, glyph_h) as usize;
let row_end = (screen_h - y).clamp(0, glyph_h) as usize;
let col_start = (-x).clamp(0, glyph_w) as usize;
let col_end = (screen_w - x).clamp(0, glyph_w) as usize;
for (row_idx, _) in glyph.iter().enumerate().take(row_end).skip(row_start) {
let row = glyph[row_idx];
let py = (y + row_idx as i32) as usize;
let base = py * self.w;
for col_idx in col_start..col_end {
let bit = HW_FONT_WIDTH - 1 - col_idx;
if ((row >> bit) & 1) != 0 {
let px = (x + col_idx as i32) as usize;
self.back[base + px] = raw;
}
}
}
}
}
fn blend_rgba8888(dst: u32, src: u32, mode: BlendMode) -> u32 {
match mode {
BlendMode::None => src,
BlendMode::Half => {
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(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;
let a = ((da as u16 + sa as u16) >> 1) as u8;
pack_rgba8888(r, g, b, a)
}
BlendMode::HalfPlus => {
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = (dr as u16 + ((sr as u16) >> 1)).min(255) as u8;
let g = (dg as u16 + ((sg as u16) >> 1)).min(255) as u8;
let b = (db as u16 + ((sb as u16) >> 1)).min(255) as u8;
let a = (da as u16 + ((sa as u16) >> 1)).min(255) as u8;
pack_rgba8888(r, g, b, a)
}
BlendMode::HalfMinus => {
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(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;
let a = (da as i16 - ((sa as i16) >> 1)).max(0) as u8;
pack_rgba8888(r, g, b, a)
}
BlendMode::Full => {
let (dr, dg, db, da) = unpack_rgba8888(dst);
let (sr, sg, sb, sa) = unpack_rgba8888(src);
let r = (dr as u16 + sr as u16).min(255) as u8;
let g = (dg as u16 + sg as u16).min(255) as u8;
let b = (db as u16 + sb as u16).min(255) as u8;
let a = (da as u16 + sa as u16).min(255) as u8;
pack_rgba8888(r, g, b, a)
}
}
}
fn unpack_rgba8888(px: u32) -> (u8, u8, u8, u8) {
Color::unpack_to_native(px)
}
fn pack_rgba8888(r: u8, g: u8, b: u8, a: u8) -> u32 {
Color::pack_from_native(r, g, b, a)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::memory_banks::{GlyphBankPoolInstaller, MemoryBanks};
use prometeu_hal::asset::AssetId;
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, u8, Color)]) -> GlyphBank {
let size = tile_size as usize;
let mut bank = GlyphBank::new(tile_size, size, size);
for (palette_id, color_index, color) in palette_colors {
bank.palettes[*palette_id as usize][*color_index as usize] = *color;
}
bank
}
fn make_filled_glyph_bank(
tile_size: TileSize,
pixel_index: u8,
palette_colors: &[(u8, u8, Color)],
) -> GlyphBank {
let mut bank = make_glyph_bank(tile_size, palette_colors);
for index in 0..bank.pixel_indices.len() {
bank.pixel_indices[index] = pixel_index;
}
bank
}
fn make_layer(
glyph_asset_id: AssetId,
glyph_id: u16,
palette_id: u8,
width: usize,
height: usize,
) -> SceneLayer {
SceneLayer {
active: true,
glyph_asset_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_asset_id: AssetId, width: usize, height: usize) -> SceneLayer {
SceneLayer {
active: false,
glyph_asset_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.draw_line(0, 0, 9, 9, Color::WHITE);
assert_eq!(gfx.back[0], Color::WHITE.0);
assert_eq!(gfx.back[9 * 10 + 9], Color::WHITE.0);
}
#[test]
fn test_sprite_draws_opaque_color_index_zero() {
let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]);
let mut back = vec![Color::BLACK.raw(); 8 * 8];
let sprite = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
flip_x: false,
flip_y: false,
bank_id: 0,
active: true,
priority: 0,
};
Gfx::draw_sprite_pixel_by_pixel(&mut back, 8, 8, &sprite, &bank);
assert_eq!(back[0], Color::GREEN.raw());
}
#[test]
fn test_sprite_skips_alpha_zero_color_index_zero() {
let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::TRANSPARENT)]);
let mut back = vec![Color::RED.raw(); 8 * 8];
let sprite = Sprite {
glyph: Glyph { glyph_id: 0, palette_id: 0 },
x: 0,
y: 0,
layer: 0,
flip_x: false,
flip_y: false,
bank_id: 0,
active: true,
priority: 0,
};
Gfx::draw_sprite_pixel_by_pixel(&mut back, 8, 8, &sprite, &bank);
assert_eq!(back[0], Color::RED.raw());
}
#[test]
fn test_cached_tile_draws_opaque_color_index_zero() {
let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::GREEN)]);
let mut back = vec![Color::BLACK.raw(); 8 * 8];
let mut target = RenderTarget { back: &mut back, screen_w: 8, screen_h: 8 };
let entry = CachedTileEntry {
active: true,
glyph_id: 0,
palette_id: 0,
flags: 0,
glyph_asset_id: 0,
};
Gfx::draw_cached_tile_pixels(
&mut target,
CachedTileDraw { x: 0, y: 0, entry, bank: &bank, tile_size: TileSize::Size8 },
);
assert_eq!(target.back[0], Color::GREEN.raw());
}
#[test]
fn test_cached_tile_skips_alpha_zero_color_index_zero() {
let bank = make_filled_glyph_bank(TileSize::Size8, 0, &[(0, 0, Color::TRANSPARENT)]);
let mut back = vec![Color::RED.raw(); 8 * 8];
let mut target = RenderTarget { back: &mut back, screen_w: 8, screen_h: 8 };
let entry = CachedTileEntry {
active: true,
glyph_id: 0,
palette_id: 0,
flags: 0,
glyph_asset_id: 0,
};
Gfx::draw_cached_tile_pixels(
&mut target,
CachedTileDraw { x: 0, y: 0, entry, bank: &bank, tile_size: TileSize::Size8 },
);
assert_eq!(target.back[0], Color::RED.raw());
}
#[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.draw_square(2, 2, 6, 6, Color::WHITE, Color::BLACK);
assert_eq!(gfx.back[2 * 10 + 2], Color::WHITE.0);
assert_eq!(gfx.back[3 * 10 + 3], Color::BLACK.0);
}
#[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_filled_glyph_bank(
TileSize::Size8,
1,
&[(0, 1, Color::RED), (1, 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, &[0, 0, 0, 0]);
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_filled_glyph_bank(
TileSize::Size8,
1,
&[
(0, 1, Color::RED),
(1, 1, Color::GREEN),
(2, 1, Color::BLUE),
(4, 1, 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<dyn GlyphBankPoolAccess>);
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, &[0, 0, 0, 0]);
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<dyn GlyphBankPoolAccess>);
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);
}
#[test]
fn render_game2d_frame_packet_applies_primitive_commands() {
let mut gfx = Gfx::new(8, 8, Arc::new(MemoryBanks::default()));
let packet = Game2DFramePacket::new(
Default::default(),
vec![Gfx2dCommand::FillRect {
rect: prometeu_hal::primitives::Rect { x: 1, y: 1, w: 2, h: 2 },
color: Color::RED,
}],
);
gfx.render_game2d_frame_packet(&packet);
gfx.present();
assert_eq!(gfx.front_buffer()[1 + 8], Color::RED.raw());
assert_eq!(gfx.front_buffer()[2 + 2 * 8], Color::RED.raw());
}
}