tile, layers and bank implementation

This commit is contained in:
bQUARKz 2026-01-13 06:30:32 +00:00
parent 072b6133d5
commit 13169698cf
Signed by: bquarkz
SSH Key Fingerprint: SHA256:Z7dgqoglWwoK6j6u4QC87OveEq74WOhFN+gitsxtkf8
13 changed files with 406 additions and 106 deletions

View File

@ -1,32 +0,0 @@
/// Cor simples em RGB565 (0bRRRRRGGGGGGBBBBB).
/// - 5 bits Red
/// - 6 bits Green
/// - 5 bits Blue
///
/// Não há canal alpha.
/// Transparência é tratada via Color Key ou Blend Mode.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);
impl Color {
/// Cria uma cor RGB565 a partir de componentes 8-bit.
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r5 = (r as u16 >> 3) & 0x1F;
let g6 = (g as u16 >> 2) & 0x3F;
let b5 = (b as u16 >> 3) & 0x1F;
Self((r5 << 11) | (g6 << 5) | b5)
}
pub const fn gray_scale(c: u8) -> Self {
Self::rgb(c, c, c)
}
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
pub const fn raw(self) -> u16 {
self.0
}
}

View File

@ -1,7 +1,5 @@
pub mod machine;
pub mod peripherals;
mod color;
mod c_button;
mod model;
pub use machine::Machine;
pub use color::Color;

View File

@ -1,5 +1,5 @@
use crate::peripherals::{BlendMode, Gfx, InputSignals, Pad, Touch};
use crate::Color;
use crate::model::Color;
use crate::peripherals::{Gfx, InputSignals, Pad, Touch};
/// PROMETEU "hardware lógico" (v0).
/// O Host alimenta INPUT SIGNALS e chama `step_frame()` em 60Hz.
@ -44,20 +44,50 @@ impl Machine {
/// Lógica do frame (demo hardcoded por enquanto).
pub fn tick(&mut self) {
self.gfx.clear(Color::rgb(0x10, 0x10, 0x10));
// self.gfx.clear(Color::rgb(0x10, 0x10, 0x10));
//
// let (x, y) = self.demo_pos();
//
// let color = if self.pad.a.down || self.touch.f.down {
// Color::rgb(0x00, 0xFF, 0x00)
// } else {
// Color::rgb(0xFF, 0x40, 0x40)
// };
//
// // game
// self.gfx.fill_rect(x, y, 20, 20, color);
// // smoke
// self.gfx.fill_rect_blend(140, 0, 40, 180, Color::gray_scale(0x88), BlendMode::Half)
let (x, y) = self.demo_pos();
let color = if self.pad.a.down || self.touch.f.down {
Color::rgb(0x00, 0xFF, 0x00)
} else {
Color::rgb(0xFF, 0x40, 0x40)
};
// Limpa a tela com um azul escuro "estilo console"
self.gfx.clear(Color::rgb(0x20, 0x20, 0x40));
// game
self.gfx.fill_rect(x, y, 20, 20, color);
// smoke
self.gfx.fill_rect_blend(140, 0, 40, 180, Color::gray_scale(0x88), BlendMode::Half)
// Simula o carregamento de um banco se estiver vazio (apenas para teste)
if self.gfx.banks[0].is_none() {
let mut test_bank = crate::model::TileBank::new(crate::model::TileSize::Size8, 128, 128);
// Pinta o primeiro tile (ID 1) de Branco para teste
// Ele começa no pixel (8, 0) pois o ID 0 é o primeiro (0,0)
let tile_size = 8;
let start_x = 8; // ID 1 está na segunda posição da primeira linha
let start_y = 0;
for y in 0..tile_size {
for x in 0..tile_size {
let px = start_x + x;
let py = start_y + y;
let idx = py * 128 + px;
test_bank.pixels[idx] = Color::WHITE;
}
}
self.gfx.banks[0] = Some(test_bank);
}
// Coloca o Tile ID 1 no mapa do HUD (canto superior esquerdo)
self.gfx.hud.map.set_tile(0, 0, crate::model::Tile { id: 1, ..Default::default() });
// Executa o pipeline gráfico
self.gfx.render_all();
}
/// Final do frame: troca buffers.
@ -65,9 +95,9 @@ impl Machine {
self.gfx.present();
}
fn demo_pos(&self) -> (i32, i32) {
let x = (self.frame_index % 300) as i32;
let y = (self.pad.a.hold_frames % 160) as i32;
(x, y)
}
// fn demo_pos(&self) -> (i32, i32) {
// let x = (self.frame_index % 300) as i32;
// let y = (self.pad.a.hold_frames % 160) as i32;
// (x, y)
// }
}

View File

@ -1,21 +1,19 @@
#[derive(Default, Clone, Copy, Debug)]
pub struct CButton {
pub struct Button {
pub pressed: bool,
pub released: bool,
pub down: bool,
pub hold_frames: u32,
}
impl CButton {
impl Button {
pub fn begin_frame(&mut self, is_down_now: bool) {
let was_down = self.down;
self.down = is_down_now;
// Detecta transições
self.pressed = !was_down && self.down;
self.released = was_down && !self.down;
// Atualiza contador de frames
if self.down {
self.hold_frames = self.hold_frames.saturating_add(1);
} else {

View File

@ -0,0 +1,59 @@
/// Cor simples em RGB565 (0bRRRRRGGGGGGBBBBB).
/// - 5 bits Red
/// - 6 bits Green
/// - 5 bits Blue
///
/// Não há canal alpha.
/// Transparência é tratada via Color Key ou Blend Mode.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct Color(pub u16);
impl Color {
pub const BLACK: Self = Self::rgb(0, 0, 0); // 0x0000
pub const WHITE: Self = Self::rgb(255, 255, 255); // 0xFFFF
pub const RED: Self = Self::rgb(255, 0, 0); // 0xF800
pub const GREEN: Self = Self::rgb(0, 255, 0); // 0x07E0
pub const BLUE: Self = Self::rgb(0, 0, 255); // 0x001F
pub const YELLOW: Self = Self::rgb(255, 255, 0); // 0xFFE0
pub const CYAN: Self = Self::rgb(0, 255, 255); // 0x07FF
pub const MAGENTA: Self = Self::rgb(255, 0, 255); // 0xF81F
pub const COLOR_KEY: Self = Self::MAGENTA;
/// Extrai canais já na faixa nativa do RGB565:
/// R: 0..31, G: 0..63, B: 0..31
#[inline(always)]
pub const fn unpack_to_native(px: u16) -> (u8, u8, u8) {
let r = ((px >> 11) & 0x1F) as u8;
let g = ((px >> 5) & 0x3F) as u8;
let b = (px & 0x1F) as u8;
(r, g, b)
}
/// Incorpora canais já na faixa nativa do RGB565 em um pixel:
/// R: 0..31, G: 0..63, B: 0..31
#[inline(always)]
pub const fn pack_from_native(r: u8, g: u8, b: u8) -> u16 {
((r as u16 & 0x1F) << 11) | ((g as u16 & 0x3F) << 5) | (b as u16 & 0x1F)
}
/// Cria uma cor RGB565 a partir de componentes 8-bit (0..255).
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
let r5 = (r as u16 >> 3) & 0x1F;
let g6 = (g as u16 >> 2) & 0x3F;
let b5 = (b as u16 >> 3) & 0x1F;
Self((r5 << 11) | (g6 << 5) | b5)
}
pub const fn gray_scale(c: u8) -> Self {
Self::rgb(c, c, c)
}
pub const fn from_raw(raw: u16) -> Self {
Self(raw)
}
pub const fn raw(self) -> u16 {
self.0
}
}

View File

@ -0,0 +1,11 @@
mod color;
mod button;
mod tile;
mod tile_layer;
mod tile_bank;
pub use button::Button;
pub use color::Color;
pub use tile::Tile;
pub use tile_bank::{TileBank, TileSize};
pub use tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};

View File

@ -0,0 +1,7 @@
#[derive(Clone, Copy, Debug, Default)]
pub struct Tile {
pub id: u16,
pub flip_x: bool,
pub flip_y: bool,
pub palette_id: u8,
}

View File

@ -0,0 +1,45 @@
use crate::model::Color;
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TileSize {
Size8 = 8,
Size16 = 16,
Size32 = 32,
}
pub struct TileBank {
pub tile_size: TileSize,
pub width: usize, // em pixels
pub height: usize, // em pixels
pub pixels: Vec<Color>,
}
impl TileBank {
pub fn new(tile_size: TileSize, width: usize, height: usize) -> Self {
Self {
tile_size,
width,
height,
pixels: vec![Color::BLACK; width * height],
}
}
/// Retorna a cor de um pixel específico dentro de um tile.
/// tile_id: o índice do tile no banco
/// local_x, local_y: a posição do pixel dentro do tile (0 até tile_size-1)
pub fn get_pixel(&self, tile_id: u16, local_x: usize, local_y: usize) -> Color {
let tile_size = self.tile_size as usize;
let tiles_per_row = self.width / tile_size;
let tile_x = (tile_id as usize % tiles_per_row) * tile_size;
let tile_y = (tile_id as usize / tiles_per_row) * tile_size;
let pixel_x = tile_x + local_x;
let pixel_y = tile_y + local_y;
if pixel_x < self.width && pixel_y < self.height {
self.pixels[pixel_y * self.width + pixel_x]
} else {
Color::BLACK
}
}
}

View File

@ -0,0 +1,109 @@
use crate::model::Tile;
use crate::model::tile_bank::TileSize;
use crate::model::TileSize::Size8;
pub struct TileMap {
pub width: usize,
pub height: usize,
pub tiles: Vec<Tile>,
}
impl TileMap {
fn create(width: usize, height: usize) -> Self {
Self {
width,
height,
tiles: vec![Tile::default(); width * height],
}
}
pub fn set_tile(&mut self, x: usize, y: usize, tile: Tile) {
if x < self.width && y < self.height {
self.tiles[y * self.width + x] = tile;
}
}
}
pub struct TileLayer {
pub bank_id: u8,
pub tile_size: TileSize,
pub map: TileMap,
}
impl TileLayer {
fn create(width: usize, height: usize, tile_size: TileSize) -> Self {
Self {
bank_id: 0,
tile_size: tile_size,
map: TileMap::create(width, height),
}
}
}
impl std::ops::Deref for TileLayer {
type Target = TileMap;
fn deref(&self) -> &Self::Target {
&self.map
}
}
impl std::ops::DerefMut for TileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.map
}
}
pub struct ScrollableTileLayer {
pub layer: TileLayer,
pub scroll_x: i32,
pub scroll_y: i32,
}
impl ScrollableTileLayer {
pub fn new(width: usize, height: usize, tile_size: TileSize) -> Self {
Self {
layer: TileLayer::create(width, height, tile_size),
scroll_x: 0,
scroll_y: 0,
}
}
}
impl std::ops::Deref for ScrollableTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for ScrollableTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}
pub struct HudTileLayer {
pub layer: TileLayer,
}
impl HudTileLayer {
pub fn new(width: usize, height: usize) -> Self {
Self {
layer: TileLayer::create(width, height, Size8),
}
}
}
impl std::ops::Deref for HudTileLayer {
type Target = TileLayer;
fn deref(&self) -> &Self::Target {
&self.layer
}
}
impl std::ops::DerefMut for HudTileLayer {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.layer
}
}

View File

@ -1,4 +1,4 @@
use crate::color::Color;
use crate::model::{Color, HudTileLayer, ScrollableTileLayer, TileBank, TileMap, TileSize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum BlendMode {
@ -20,16 +20,30 @@ pub struct Gfx {
h: usize,
front: Vec<u16>,
back: Vec<u16>,
pub layers: [ScrollableTileLayer; 4],
pub hud: HudTileLayer,
pub banks: [Option<TileBank>; 16],
}
impl Gfx {
impl Gfx {
pub fn new(w: usize, h: usize) -> Self {
const EMPTY_BANK: Option<TileBank> = None;
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],
}
}
@ -85,6 +99,82 @@ impl Gfx {
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) {
// 1. Layers de jogo (0 a 3)
for i in 0..self.layers.len() {
self.render_layer(i);
}
// 2. Sprites (Ainda vamos implementar)
// 3. HUD (Sempre por cima)
self.render_hud();
}
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 size = bank.tile_size as usize;
for screen_y in 0..screen_h {
for screen_x in 0..screen_w {
let world_x = screen_x as i32 + scroll_x;
let world_y = screen_y as i32 + scroll_y;
let tile_x = (world_x / size as i32) as usize;
let tile_y = (world_y / size as i32) as usize;
if tile_x >= map.width || tile_y >= map.height { continue; }
let tile = map.tiles[tile_y * map.width + tile_x];
if tile.id == 0 { continue; }
let local_x = (world_x % size as i32) as usize;
let local_y = (world_y % size as i32) as usize;
let color = bank.get_pixel(tile.id, local_x, local_y);
if color == Color::COLOR_KEY { continue; }
let idx = (screen_y * screen_w) + screen_x;
back[idx] = color.0;
}
}
}
}
/// Faz blend em RGB565 por canal com saturação.
@ -94,60 +184,45 @@ fn blend_rgb565(dst: u16, src: u16, mode: BlendMode) -> u16 {
BlendMode::None => src,
BlendMode::Half => {
let (dr, dg, db) = unpack_rgb565(dst);
let (sr, sg, sb) = unpack_rgb565(src);
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;
pack_rgb565(r, g, b)
Color::pack_from_native(r, g, b)
}
BlendMode::HalfPlus => {
let (dr, dg, db) = unpack_rgb565(dst);
let (sr, sg, sb) = unpack_rgb565(src);
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;
pack_rgb565(r, g, b)
Color::pack_from_native(r, g, b)
}
BlendMode::HalfMinus => {
let (dr, dg, db) = unpack_rgb565(dst);
let (sr, sg, sb) = unpack_rgb565(src);
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;
pack_rgb565(r, g, b)
Color::pack_from_native(r, g, b)
}
BlendMode::Full => {
let (dr, dg, db) = unpack_rgb565(dst);
let (sr, sg, sb) = unpack_rgb565(src);
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;
pack_rgb565(r, g, b)
Color::pack_from_native(r, g, b)
}
}
}
/// Extrai canais já na faixa nativa do RGB565:
/// R: 0..31, G: 0..63, B: 0..31
#[inline(always)]
fn unpack_rgb565(px: u16) -> (u8, u8, u8) {
let r = ((px >> 11) & 0x1F) as u8;
let g = ((px >> 5) & 0x3F) as u8;
let b = (px & 0x1F) as u8;
(r, g, b)
}
#[inline(always)]
fn pack_rgb565(r: u8, g: u8, b: u8) -> u16 {
((r as u16 & 0x1F) << 11) | ((g as u16 & 0x3F) << 5) | (b as u16 & 0x1F)
}

View File

@ -1,22 +1,22 @@
use crate::c_button::CButton;
use crate::model::Button;
use crate::peripherals::input_signal::InputSignals;
#[derive(Default, Clone, Copy, Debug)]
pub struct Pad {
pub up: CButton,
pub down: CButton,
pub left: CButton,
pub right: CButton,
pub up: Button,
pub down: Button,
pub left: Button,
pub right: Button,
pub a: CButton, // ps: square
pub d: CButton, // ps: circle
pub w: CButton, // ps: triangle
pub s: CButton, // ps: cross
pub q: CButton, // ps: R
pub e: CButton, // ps: L
pub a: Button, // ps: square
pub d: Button, // ps: circle
pub w: Button, // ps: triangle
pub s: Button, // ps: cross
pub q: Button, // ps: R
pub e: Button, // ps: L
pub start: CButton,
pub select: CButton,
pub start: Button,
pub select: Button,
}
impl Pad {

View File

@ -1,9 +1,9 @@
use crate::c_button::CButton;
use crate::model::Button;
use crate::peripherals::input_signal::InputSignals;
#[derive(Default, Clone, Copy, Debug)]
pub struct Touch {
pub f: CButton,
pub f: Button,
pub x: i32,
pub y: i32,
}

View File

@ -72,7 +72,7 @@ Isso garante:
O mundo gráfico é composto por:
- Até **16 Tile Banks**
- **4 Game Layers** (scrolláveis)
- **4 Tile Layers** (scrolláveis)
- **1 HUD Layer** (fixa, sempre por cima)
- Sprites com prioridade entre layers
@ -90,7 +90,7 @@ O mundo gráfico é composto por:
### 4.2 Layers
- Existem:
- 4 Game Layers
- 4 Tile Layers
- 1 HUD Layer
- Cada layer aponta para **um único bank**
- Sprites podem usar **qualquer bank**
@ -296,7 +296,7 @@ Por design:
O PROMETEU suporta **fade gradual** como um PostFX especial, com dois controles
independentes:
- **Scene Fade**: afeta toda a cena (Game Layers 03 + Sprites)
- **Scene Fade**: afeta toda a cena (Tile Layers 03 + Sprites)
- **HUD Fade**: afeta apenas o HUD Layer (sempre composto por último)
O fade é implementado sem alpha contínuo por pixel e sem floats.
@ -371,7 +371,7 @@ Observações:
A composição do frame segue esta ordem:
1. Rasterizar **Game Layers 03** → Back Buffer
1. Rasterizar **Tile Layers 03** → Back Buffer
2. Rasterizar **Sprites** conforme prioridade
3. (Opcional) Pipeline extra (Emission/Light/Glow etc.)
4. Aplicar **Scene Fade** usando:
@ -415,7 +415,7 @@ O GFX do PROMETEU é simples **por escolha**, não por limitação.
- Color key para transparência
- Blending discreto estilo SNES
- Até 16 tile banks
- 4 game layers + 1 HUD
- 4 Tile Layers + 1 HUD
- Layer = tilemap + cache + scroll
- Projeção rasterizada por frame
- Profundidade definida por ordem de desenho