implemeted firmware and hardware bridge

This commit is contained in:
Nilton Constantino 2026-01-16 12:13:19 +00:00
parent 751619fb31
commit ba488a5fee
No known key found for this signature in database
19 changed files with 442 additions and 724 deletions

View File

@ -0,0 +1,77 @@
use crate::firmware::firmware_state::FirmwareState;
use crate::model::{AppMode, Cartridge, Color};
use crate::peripherals::InputSignals;
use crate::prometeu_hub::PrometeuHub;
use crate::prometeu_os::PrometeuOS;
pub struct Firmware {
pub os: PrometeuOS,
pub hub: PrometeuHub,
pub state: FirmwareState,
}
impl Firmware {
pub fn new() -> Self {
Self {
os: PrometeuOS::new(),
hub: PrometeuHub::new(),
state: FirmwareState::Reset,
}
}
pub fn step_frame(&mut self, signals: &InputSignals) {
match &mut self.state {
FirmwareState::Reset => {
self.os.reset();
self.state = FirmwareState::SplashScreen;
}
FirmwareState::SplashScreen => {
// Splash logic opcional
self.state = FirmwareState::LaunchHubHome;
}
FirmwareState::LaunchHubHome => {
self.hub.init();
self.state = FirmwareState::HubHome;
}
FirmwareState::HubHome => {
// UI do Hub
self.hub.update(&mut self.os);
}
FirmwareState::PosRunGame(_cart) => {
if let Some(error) = self.os.step_frame(signals) {
self.state = FirmwareState::PosCrashScreen(error);
}
}
FirmwareState::PosRunSystem(_cart) => {
// System Apps rodam "dentro" do Hub/Window System
if let Some(error) = self.os.step_frame(signals) {
self.state = FirmwareState::PosCrashScreen(error);
}
// TODO: Compor com a UI do Hub
}
FirmwareState::PosCrashScreen(_error) => {
// Atualiza periféricos para input na tela de crash
self.os.hardware_mut().pad_mut().begin_frame(signals);
// Tela de erro: fundo vermelho, texto branco
self.os.hardware_mut().gfx_mut().clear(Color::RED);
// Por enquanto apenas logamos ou mostramos algo simples
// No futuro, usar draw_text
self.os.hardware_mut().gfx_mut().present();
// Se apertar START, volta pro Hub
if self.os.hardware().pad().start.down {
self.state = FirmwareState::LaunchHubHome;
}
}
}
}
pub fn load_app(&mut self, cart: Cartridge, mode: AppMode) {
self.os.load_cartridge(&cart);
match mode {
AppMode::Game => self.state = FirmwareState::PosRunGame(cart),
AppMode::System => self.state = FirmwareState::PosRunSystem(cart),
}
}
}

View File

@ -0,0 +1,12 @@
use crate::model::Cartridge;
#[derive(Debug, Clone)]
pub enum FirmwareState {
Reset,
SplashScreen,
LaunchHubHome,
HubHome,
PosRunGame(Cartridge),
PosRunSystem(Cartridge),
PosCrashScreen(String),
}

View File

@ -0,0 +1,4 @@
mod firmware;
pub mod firmware_state;
pub use firmware::Firmware;

View File

@ -1,11 +0,0 @@
pub struct Firmware {
}
impl Firmware {
pub fn new() -> Self {
Self {
}
}
}

View File

@ -1 +0,0 @@
pub mod firmware;

View File

@ -0,0 +1,57 @@
use crate::peripherals::{Audio, Gfx, InputSignals, Pad, Touch, HardwareBridge};
use crate::firmware::{Firmware};
pub struct Hardware {
pub firmware: Firmware,
pub gfx: Gfx,
pub audio: Audio,
pub pad: Pad,
pub touch: Touch,
}
impl HardwareBridge for Hardware {
fn gfx(&self) -> &Gfx { &self.gfx }
fn gfx_mut(&mut self) -> &mut Gfx { &mut self.gfx }
fn audio(&self) -> &Audio { &self.audio }
fn audio_mut(&mut self) -> &mut Audio { &mut self.audio }
fn pad(&self) -> &Pad { &self.pad }
fn pad_mut(&mut self) -> &mut Pad { &mut self.pad }
fn touch(&self) -> &Touch { &self.touch }
fn touch_mut(&mut self) -> &mut Touch { &mut self.touch }
}
impl Hardware {
pub const W: usize = 320;
pub const H: usize = 180;
pub fn new() -> Box<Self> {
let mut hw = Box::new(Self {
firmware: Firmware::new(),
gfx: Gfx::new(Self::W, Self::H),
audio: Audio::new(),
pad: Pad::default(),
touch: Touch::default(),
});
// Configura a referência circular (através do trait HardwareBridge)
let hw_ptr = hw.as_mut() as *mut dyn HardwareBridge;
hw.firmware.os.hw_ptr = Some(hw_ptr);
hw
}
/// Atalhos para o Runner (desktop) não quebrar tanto,
/// delegando para o PrometeuOS onde os contadores agora vivem.
pub fn tick_index(&self) -> u64 { self.firmware.os.tick_index }
pub fn logical_frame_index(&self) -> u64 { self.firmware.os.logical_frame_index }
pub fn last_frame_cpu_time_us(&self) -> u64 { self.firmware.os.last_frame_cpu_time_us }
/// "Contrato de frame" do PROMETEU.
pub fn step_frame(&mut self, signals: &InputSignals) {
// O Host chama o hardware, que delega para o Firmware/OS.
self.firmware.step_frame(signals);
}
}

View File

@ -1,8 +1,9 @@
pub mod logical_hardware;
pub mod hardware;
pub mod peripherals;
pub mod vm;
mod model;
mod native_interface;
mod utilz;
pub mod firmware;
mod prometeu_os;
mod prometeu_hub;
pub use logical_hardware::LogicalHardware;
pub use hardware::Hardware;

View File

@ -1,165 +0,0 @@
use crate::model::{Cartridge, Color, Sample};
use crate::peripherals::{Audio, Gfx, InputSignals, Pad, Touch};
use crate::vm::VirtualMachine;
use std::sync::Arc;
/// PROMETEU "hardware lógico" (v0).
/// O Host alimenta INPUT SIGNALS e chama `step_frame()` em 60Hz.
pub struct LogicalHardware {
pub gfx: Gfx,
pub audio: Audio,
pub pad: Pad,
pub touch: Touch,
pub tick_index: u64,
pub logical_frame_index: u64,
pub last_frame_cpu_time_us: u64,
pub vm: VirtualMachine,
pub cartridge: Option<Cartridge>,
// Estado Interno
logical_frame_open: bool,
// Assets de exemplo
pub sample_square: Option<Arc<Sample>>,
pub sample_kick: Option<Arc<Sample>>,
pub sample_snare: Option<Arc<Sample>>,
}
impl LogicalHardware {
pub const W: usize = 320;
pub const H: usize = 180;
pub const CYCLES_PER_FRAME: u64 = 100_000; // Exemplo de budget
pub fn new() -> Self {
let mut logical_hardware = Self {
gfx: Gfx::new(Self::W, Self::H),
audio: Audio::new(),
pad: Pad::default(),
touch: Touch::default(),
tick_index: 0,
logical_frame_index: 0,
last_frame_cpu_time_us: 0,
vm: VirtualMachine::default(),
cartridge: None,
logical_frame_open: false,
sample_square: None,
sample_kick: None,
sample_snare: None,
};
// Inicializa samples básicos
logical_hardware.sample_square = Some(Arc::new(Self::create_square_sample(440.0, 0.1)));
logical_hardware
}
pub fn get_color(&self, index: usize) -> Color {
// Implementação simplificada: se houver bancos, usa a paleta do banco 0
// Caso contrário, usa cores básicas fixas
if let Some(bank) = self.gfx.banks[0].as_ref() {
bank.palettes[0][index % 16]
} else {
match index % 16 {
0 => Color::BLACK,
1 => Color::WHITE,
2 => Color::RED,
3 => Color::GREEN,
4 => Color::BLUE,
5 => Color::YELLOW,
6 => Color::CYAN,
7 => Color::INDIGO,
8 => Color::GRAY,
_ => Color::BLACK,
}
}
}
pub fn is_button_down(&self, id: u32) -> bool {
match id {
0 => self.pad.up.down,
1 => self.pad.down.down,
2 => self.pad.left.down,
3 => self.pad.right.down,
4 => self.pad.a.down,
5 => self.pad.b.down,
6 => self.pad.x.down,
7 => self.pad.y.down,
8 => self.pad.l.down,
9 => self.pad.r.down,
10 => self.pad.start.down,
11 => self.pad.select.down,
_ => false,
}
}
pub fn load_cartridge(&mut self, cart: Cartridge) {
self.cartridge = Some(cart);
}
fn create_square_sample(freq: f64, duration: f64) -> Sample {
let sample_rate = crate::peripherals::OUTPUT_SAMPLE_RATE;
let num_samples = (duration * sample_rate as f64) as usize;
let mut data = Vec::with_capacity(num_samples);
let period = sample_rate as f64 / freq;
for i in 0..num_samples {
let val = if (i as f64 % period) < (period / 2.0) {
10000 // Volume médio
} else {
-10000
};
data.push(val);
}
Sample::new(sample_rate, data)
}
/// "Contrato de frame" do PROMETEU (Template Method sem loop).
/// O Host controla tempo/event loop; o Core define a sequência do frame.
pub fn step_frame(&mut self, signals: &InputSignals) {
let start = std::time::Instant::now();
self.tick_index += 1; // não importa o frame lógico, o tick sempre incrementa
// Se um frame logica estiver aberto evita limpar o input
if !self.logical_frame_open {
self.logical_frame_open = true;
self.begin_frame(signals);
}
// Executa uma fatia fixa de ciclos (budget do tick real do host)
let mut vm = std::mem::take(&mut self.vm);
let run = vm
.run_budget(Self::CYCLES_PER_FRAME, self)
.expect("vm error");
self.vm = vm;
// Só “materializa” e apresenta quando o frame lógico fecha
if run.reason == crate::vm::LogicalFrameEndingReason::FrameSync {
self.gfx.render_all();
self.end_frame();
self.logical_frame_index += 1; // conta frames lógicos apresentados
self.logical_frame_open = false;
}
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
}
/// Início do frame: zera flags transitórias.
pub fn begin_frame(&mut self, signals: &InputSignals) {
// Limpa comandos de áudio do frame anterior.
// Nota: O Host deve consumir esses comandos ANTES ou DURANTE o step_frame se quiser processá-los.
// Como o Host atual consome logo após o step_frame, limpar aqui no início do PRÓXIMO frame está correto.
self.audio.clear_commands();
// Flags transitórias do TOUCH devem durar 1 frame.
self.touch.begin_frame(signals);
// Se você quiser input com pressed/released depois:
self.pad.begin_frame(signals);
}
/// Final do frame: troca buffers.
pub fn end_frame(&mut self) {
self.gfx.present();
}
}

View File

@ -1,12 +1,27 @@
use crate::vm::Program;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum AppMode {
Game,
System,
}
#[derive(Debug, Clone)]
pub struct AppHeader {
pub app_id: String,
pub title: String,
pub mode: AppMode,
pub entrypoint: u32,
}
#[derive(Clone, Debug)]
pub struct Cartridge {
pub header: AppHeader,
pub program: Program,
}
impl Cartridge {
pub fn new(program: Program) -> Self {
Self { program }
pub fn new(header: AppHeader, program: Program) -> Self {
Self { header, program }
}
}

View File

@ -14,4 +14,4 @@ pub use tile_bank::{TileBank, TileSize};
pub use tile_layer::{HudTileLayer, ScrollableTileLayer, TileMap};
pub use sprite::Sprite;
pub use sample::Sample;
pub use cartridge::Cartridge;
pub use cartridge::{Cartridge, AppMode};

View File

@ -10,3 +10,19 @@ pub use input_signal::InputSignals;
pub use pad::Pad;
pub use touch::Touch;
pub use audio::{Audio, Channel, AudioCommand, LoopMode, MAX_CHANNELS, OUTPUT_SAMPLE_RATE};
/// Interface de acesso ao hardware para o Firmware/OS.
/// Centraliza as chamadas para periféricos sem expor a estrutura interna do HardwareBridge.
pub trait HardwareBridge {
fn gfx(&self) -> &Gfx;
fn gfx_mut(&mut self) -> &mut Gfx;
fn audio(&self) -> &Audio;
fn audio_mut(&mut self) -> &mut Audio;
fn pad(&self) -> &Pad;
fn pad_mut(&mut self) -> &mut Pad;
fn touch(&self) -> &Touch;
fn touch_mut(&mut self) -> &mut Touch;
}

View File

@ -0,0 +1,3 @@
mod prometeu_hub;
pub use prometeu_hub::PrometeuHub;

View File

@ -0,0 +1,40 @@
use crate::model::Color;
use crate::prometeu_os::PrometeuOS;
// TODO: Mover para arquivo próprio se crescer.
/// Sistema de janelas do PROMETEU.
pub struct WindowSystem {
pub theme_color: Color,
}
impl WindowSystem {
pub fn new() -> Self {
Self {
theme_color: Color::INDIGO,
}
}
}
/// PrometeuHub: Launcher e ambiente de UI do sistema.
pub struct PrometeuHub {
pub window_system: WindowSystem,
}
impl PrometeuHub {
pub fn new() -> Self {
Self {
window_system: WindowSystem::new(),
}
}
pub fn init(&mut self) {
// Inicializa o Window System e lista apps
}
pub fn update(&mut self, os: &mut PrometeuOS) {
// Renderiza a UI básica do Hub
os.hardware_mut().gfx_mut().clear(self.window_system.theme_color);
// TODO: Desenhar lista de apps
os.hardware_mut().gfx_mut().present();
}
}

View File

@ -0,0 +1,5 @@
mod prometeu_os;
mod native_interface;
pub use prometeu_os::PrometeuOS;
pub use native_interface::NativeInterface;

View File

@ -1,28 +1,22 @@
use crate::prometeu_os::prometeu_os::PrometeuOS;
use crate::vm::{Value, VirtualMachine};
use crate::LogicalHardware;
pub trait NativeInterface {
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine) -> Result<u64, String>;
}
impl NativeInterface for LogicalHardware {
impl NativeInterface for PrometeuOS {
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine) -> Result<u64, String> {
match id {
// system.has_cart() -> bool
0x0001 => {
vm.push(Value::Boolean(self.cartridge.is_some()));
vm.push(Value::Boolean(self.current_cartridge.is_some()));
Ok(10)
}
// system.run_cart()
0x0002 => {
if let Some(cart) = self.cartridge.as_ref() {
vm.program = cart.program.clone();
vm.pc = 0;
vm.operand_stack.clear();
vm.call_stack.clear();
vm.globals.clear();
vm.heap.clear();
vm.halted = false;
if let Some(cart) = self.current_cartridge.as_ref().cloned() {
self.load_cartridge(&cart);
} else {
return Err("No cartridge inserted".into());
}
@ -32,7 +26,7 @@ impl NativeInterface for LogicalHardware {
0x1001 => {
let color_idx = vm.pop_integer()? as usize;
let color = self.get_color(color_idx);
self.gfx.clear(color);
self.hardware_mut().gfx_mut().clear(color);
Ok(100)
}
// gfx.draw_rect(x, y, w, h, color_index)
@ -43,7 +37,7 @@ impl NativeInterface for LogicalHardware {
let y = vm.pop_integer()? as i32;
let x = vm.pop_integer()? as i32;
let color = self.get_color(color_idx);
self.gfx.fill_rect(x, y, w, h, color);
self.hardware_mut().gfx_mut().fill_rect(x, y, w, h, color);
Ok(200)
}
// input.get_pad(button_id) -> bool
@ -54,7 +48,6 @@ impl NativeInterface for LogicalHardware {
Ok(50)
}
// audio.play_sample(sample_id, voice_id, volume, pan, pitch)
// sample_id: 0=square, 1=kick, 2=snare
0x3001 => {
let pitch = vm.pop_number()?;
let pan = vm.pop_integer()? as u8;
@ -70,7 +63,7 @@ impl NativeInterface for LogicalHardware {
};
if let Some(s) = sample {
self.audio.play(s, voice_id, volume, pan, pitch, 0, crate::peripherals::LoopMode::Off);
self.hardware_mut().audio_mut().play(s, voice_id, volume, pan, pitch, 0, crate::peripherals::LoopMode::Off);
}
Ok(300)
}

View File

@ -0,0 +1,182 @@
use crate::model::{Cartridge, Color, Sample};
use crate::peripherals::{HardwareBridge, InputSignals};
use crate::vm::VirtualMachine;
use std::sync::Arc;
/// PrometeuOS (POS): O firmware/base do sistema.
/// Autoridade máxima do boot, periféricos, execução da PVM e tratamento de falhas.
pub struct PrometeuOS {
pub vm: VirtualMachine,
pub tick_index: u64,
pub logical_frame_index: u64,
pub logical_frame_open: bool,
pub last_frame_cpu_time_us: u64,
pub hw_ptr: Option<*mut dyn HardwareBridge>,
pub current_cartridge: Option<Cartridge>,
// Assets de exemplo (mantidos para compatibilidade com syscalls de áudio v0)
pub sample_square: Option<Arc<Sample>>,
pub sample_kick: Option<Arc<Sample>>,
pub sample_snare: Option<Arc<Sample>>,
}
impl PrometeuOS {
pub const CYCLES_PER_FRAME: u64 = 100_000;
pub fn new() -> Self {
let mut os = Self {
vm: VirtualMachine::default(),
hw_ptr: None,
tick_index: 0,
logical_frame_index: 0,
logical_frame_open: false,
last_frame_cpu_time_us: 0,
current_cartridge: None,
sample_square: None,
sample_kick: None,
sample_snare: None,
};
// Inicializa samples básicos (mesma lógica do LogicalHardware)
os.sample_square = Some(Arc::new(Self::create_square_sample(440.0, 0.1)));
os
}
/// Helper para acessar o hardware de forma segura (dentro do contexto do firmware).
#[inline]
pub fn hardware(&self) -> &dyn HardwareBridge {
unsafe { &*self.hw_ptr.expect("Hardware pointer not initialized") }
}
/// Helper para acessar o hardware de forma mutável.
#[inline]
pub fn hardware_mut(&mut self) -> &mut dyn HardwareBridge {
unsafe { &mut *self.hw_ptr.expect("Hardware pointer not initialized") }
}
pub fn reset(&mut self) {
self.vm = VirtualMachine::default();
self.tick_index = 0;
self.logical_frame_index = 0;
self.logical_frame_open = false;
self.last_frame_cpu_time_us = 0;
self.current_cartridge = None;
}
/// Carrega um cartucho na PVM e reseta o estado de execução.
pub fn load_cartridge(&mut self, cart: &Cartridge) {
// Na spec: "resetar PC/stack/heap ao iniciar app"
self.current_cartridge = Some(cart.clone());
self.vm.program = cart.program.clone();
self.vm.pc = 0;
self.vm.operand_stack.clear();
self.vm.call_stack.clear();
self.vm.globals.clear();
self.vm.heap.clear();
self.vm.halted = false;
}
/// Executa um tick do host (60Hz).
pub fn step_frame(&mut self, signals: &InputSignals) -> Option<String> {
let start = std::time::Instant::now();
self.tick_index += 1;
if !self.logical_frame_open {
self.logical_frame_open = true;
self.begin_logical_frame(signals);
}
// Executa budget
let mut vm = std::mem::take(&mut self.vm);
let run_result = vm.run_budget(Self::CYCLES_PER_FRAME, self);
self.vm = vm;
match run_result {
Ok(run) => {
if run.reason == crate::vm::LogicalFrameEndingReason::FrameSync {
self.hardware_mut().gfx_mut().render_all();
self.end_logical_frame();
self.logical_frame_index += 1;
self.logical_frame_open = false;
}
}
Err(e) => {
return Some(format!("PVM Fault: {:?}", e));
}
}
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64;
None
}
fn begin_logical_frame(&mut self, signals: &InputSignals) {
let hw = self.hardware_mut();
hw.audio_mut().clear_commands();
hw.touch_mut().begin_frame(signals);
hw.pad_mut().begin_frame(signals);
}
fn end_logical_frame(&mut self) {
self.hardware_mut().gfx_mut().present();
}
// Helper para syscalls
pub fn get_color(&self, index: usize) -> Color {
let hw = self.hardware();
if let Some(bank) = hw.gfx().banks[0].as_ref() {
bank.palettes[0][index % 16]
} else {
match index % 16 {
0 => Color::BLACK,
1 => Color::WHITE,
2 => Color::RED,
3 => Color::GREEN,
4 => Color::BLUE,
5 => Color::YELLOW,
6 => Color::CYAN,
7 => Color::INDIGO,
8 => Color::GRAY,
_ => Color::BLACK,
}
}
}
// Helper para syscalls
pub fn is_button_down(&self, id: u32) -> bool {
let hw = self.hardware();
match id {
0 => hw.pad().up.down,
1 => hw.pad().down.down,
2 => hw.pad().left.down,
3 => hw.pad().right.down,
4 => hw.pad().a.down,
5 => hw.pad().b.down,
6 => hw.pad().x.down,
7 => hw.pad().y.down,
8 => hw.pad().l.down,
9 => hw.pad().r.down,
10 => hw.pad().start.down,
11 => hw.pad().select.down,
_ => false,
}
}
fn create_square_sample(freq: f64, duration: f64) -> Sample {
let sample_rate = crate::peripherals::OUTPUT_SAMPLE_RATE;
let num_samples = (duration * sample_rate as f64) as usize;
let mut data = Vec::with_capacity(num_samples);
let period = sample_rate as f64 / freq;
for i in 0..num_samples {
let val = if (i as f64 % period) < (period / 2.0) {
10000
} else {
-10000
};
data.push(val);
}
Sample::new(sample_rate, data)
}
}

View File

@ -1,18 +0,0 @@
use crate::vm::OpCode;
pub fn emit_u16(rom: &mut Vec<u8>, val: u16) {
rom.extend_from_slice(&val.to_le_bytes());
}
pub fn emit_u32(rom: &mut Vec<u8>, val: u32) {
rom.extend_from_slice(&val.to_le_bytes());
}
pub fn emit_op(rom: &mut Vec<u8>, op: OpCode) {
emit_u16(rom, op as u16);
}
pub fn patch_u32(rom: &mut [u8], offset: usize, val: u32) {
let bytes = val.to_le_bytes();
rom[offset..offset + 4].copy_from_slice(&bytes);
}

View File

@ -1,9 +1,7 @@
use crate::native_interface::NativeInterface;
use crate::utilz;
use crate::prometeu_os::NativeInterface;
use crate::vm::call_frame::CallFrame;
use crate::vm::opcode::OpCode;
use crate::vm::value::Value;
use crate::vm::Program;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@ -44,246 +42,11 @@ impl VirtualMachine {
halted: false,
}
}
/// Cria uma VM com um "Boot ROM" básico que exibe uma animação de padrão de teste.
/// Isso garante que a máquina sempre mostre algo ao ser ligada, mesmo sem cartucho.
pub fn new_boot_rom() -> Self {
let mut rom = Vec::new();
let mut constant_pool = Vec::new();
// Constant Pool
constant_pool.push(Value::Integer(160)); // 0: Center X
constant_pool.push(Value::Integer(90)); // 1: Center Y
constant_pool.push(Value::Integer(40)); // 2: Logo Size
constant_pool.push(Value::Integer(7)); // 3: Indigo (Logo)
constant_pool.push(Value::Integer(0)); // 4: Black (Background)
constant_pool.push(Value::Integer(180)); // 5: Splash Timeout (3s)
constant_pool.push(Value::Integer(2)); // 6: Divisor
constant_pool.push(Value::Integer(1)); // 7: 1
constant_pool.push(Value::Integer(10)); // 8: Start Button
constant_pool.push(Value::Integer(0)); // 9: Square Sample
constant_pool.push(Value::Integer(255)); // 10: Volume
constant_pool.push(Value::Integer(127)); // 11: Pan
constant_pool.push(Value::Float(1.0)); // 12: Pitch
constant_pool.push(Value::Integer(0)); // 13: Voice ID
constant_pool.push(Value::Integer(0)); // 14: Zero
constant_pool.push(Value::Integer(6)); // 15: Cyan (SO Logo)
// -- INICIALIZAÇÃO --
// G0 = 0 (Contador de frames)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 14);
utilz::emit_op(&mut rom, OpCode::SetGlobal);
utilz::emit_u32(&mut rom, 0);
// G1 = 0 (Estado BIOS: 0=Splash, 1=SO)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 14);
utilz::emit_op(&mut rom, OpCode::SetGlobal);
utilz::emit_u32(&mut rom, 1);
// Toca o som de boot "plim"
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 9); // sample
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 13); // voice
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 10); // vol
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 11); // pan
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 12); // pitch
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x3001);
// Escopo local para cálculos temporários [0:f, 1:s, 2:x, 3:y]
utilz::emit_op(&mut rom, OpCode::PushScope);
utilz::emit_u32(&mut rom, 4);
let loop_start = rom.len() as u32;
// Limpa tela
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 4); // black
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x1001);
// --- MÁQUINA DE ESTADOS (G1) ---
utilz::emit_op(&mut rom, OpCode::GetGlobal);
utilz::emit_u32(&mut rom, 1); // G1
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 14); // 0
utilz::emit_op(&mut rom, OpCode::Eq);
let jmp_to_so = rom.len();
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
utilz::emit_u32(&mut rom, 0);
// === ESTADO 0: SPLASH ===
// 1. Cálculos da Animação
utilz::emit_op(&mut rom, OpCode::GetGlobal);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::SetLocal);
utilz::emit_u32(&mut rom, 0); // L0 = f
// S = if f < 40 { f } else { 40 }
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 0); // f
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 2); // 40
utilz::emit_op(&mut rom, OpCode::Lt);
let jmp_s_full = rom.len();
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 0); // f
let jmp_s_set = rom.len();
utilz::emit_op(&mut rom, OpCode::Jmp);
utilz::emit_u32(&mut rom, 0);
let s_full_addr = rom.len() as u32;
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 2); // 40
let s_set_addr = rom.len() as u32;
utilz::patch_u32(&mut rom, jmp_s_full + 2, s_full_addr);
utilz::patch_u32(&mut rom, jmp_s_set + 2, s_set_addr);
utilz::emit_op(&mut rom, OpCode::SetLocal);
utilz::emit_u32(&mut rom, 1); // L1 = S
// X = 160 - S/2, Y = 90 - S/2
for (local_idx, const_idx) in [(2, 0), (3, 1)] {
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, const_idx);
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 1); // S
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 6); // 2
utilz::emit_op(&mut rom, OpCode::Div);
utilz::emit_op(&mut rom, OpCode::Sub);
utilz::emit_op(&mut rom, OpCode::SetLocal);
utilz::emit_u32(&mut rom, local_idx);
}
// Desenha Quadrado (Logo)
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 2); // X
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 3); // Y
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 1); // W
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 1); // H
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 3); // Indigo
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x1002);
// Checagem de Saída do Splash (f >= 180 OR START)
utilz::emit_op(&mut rom, OpCode::GetGlobal);
utilz::emit_u32(&mut rom, 0); // f
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 5); // 180
utilz::emit_op(&mut rom, OpCode::Gt); // f > 180 (ou eq, mas gt é mais seguro)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 8); // Start
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x2001); // get_pad
utilz::emit_op(&mut rom, OpCode::Or);
let jmp_splash_continue = rom.len();
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
utilz::emit_u32(&mut rom, 0);
// Validação de Cartucho
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x0001); // has_cart?
let jmp_no_cart_init = rom.len();
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x0002); // run_cart
// Senão tem cartucho: vai para o SO
let no_cart_init_addr = rom.len() as u32;
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 7); // 1
utilz::emit_op(&mut rom, OpCode::SetGlobal);
utilz::emit_u32(&mut rom, 1); // G1 = 1 (SO)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 14); // 0
utilz::emit_op(&mut rom, OpCode::SetGlobal);
utilz::emit_u32(&mut rom, 0); // f = 0 (reset counter for SO)
let splash_continue_addr = rom.len() as u32;
utilz::patch_u32(&mut rom, jmp_splash_continue + 2, splash_continue_addr);
utilz::patch_u32(&mut rom, jmp_no_cart_init + 2, no_cart_init_addr);
let jmp_end_state = rom.len();
utilz::emit_op(&mut rom, OpCode::Jmp);
utilz::emit_u32(&mut rom, 0);
// === ESTADO 1: SO ===
let so_state_addr = rom.len() as u32;
utilz::patch_u32(&mut rom, jmp_to_so + 2, so_state_addr);
// Desenha "SO" (Quadrado Ciano)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0); // 160
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 1); // 90
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 2); // 40
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 2); // 40
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 15); // Cyan
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x1002);
// Monitora Inserção de Cartucho (Hot-plug) + START
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x0001); // has_cart?
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 8); // Start
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x2001); // get_pad
utilz::emit_op(&mut rom, OpCode::And); // has_cart AND START
let jmp_so_continue = rom.len();
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x0002); // run_cart
let so_continue_addr = rom.len() as u32;
utilz::patch_u32(&mut rom, jmp_so_continue + 2, so_continue_addr);
// Incrementa G0 (f)
let end_state_addr = rom.len() as u32;
utilz::patch_u32(&mut rom, jmp_end_state + 2, end_state_addr);
utilz::emit_op(&mut rom, OpCode::GetGlobal);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 7); // 1
utilz::emit_op(&mut rom, OpCode::Add);
utilz::emit_op(&mut rom, OpCode::SetGlobal);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::FrameSync);
utilz::emit_op(&mut rom, OpCode::Jmp);
utilz::emit_u32(&mut rom, loop_start);
Self::new(rom, constant_pool)
}
}
impl Default for VirtualMachine {
fn default() -> Self {
Self::new_boot_rom()
Self::new(vec![], vec![])
}
}
@ -633,255 +396,3 @@ impl VirtualMachine {
Ok(())
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_add_instructions() {
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
}
let mut native = NoopNative;
let mut rom = Vec::new();
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0); // Const index 0
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 1); // Const index 1
utilz::emit_op(&mut rom, OpCode::Add);
utilz::emit_op(&mut rom, OpCode::Halt);
let constant_pool = vec![Value::Integer(10), Value::Integer(20)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(100, &mut native).unwrap();
assert_eq!(vm.operand_stack.len(), 1);
assert_eq!(vm.operand_stack[0], Value::Integer(30));
assert!(vm.halted);
}
#[test]
fn test_jump_and_loop() {
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
}
let mut native = NoopNative;
let mut rom = Vec::new();
// Index 0: PUSH 0 (counter)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0);
// Index 6: DUP
let loop_start = rom.len();
utilz::emit_op(&mut rom, OpCode::Dup);
// PUSH 10
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 1);
// LT (counter < 10)
utilz::emit_op(&mut rom, OpCode::Lt);
// JMP_IF_FALSE to end
utilz::emit_op(&mut rom, OpCode::JmpIfFalse);
let jmp_placeholder = rom.len();
utilz::emit_u32(&mut rom, 0);
// PUSH 1
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 2);
// ADD (increment counter)
utilz::emit_op(&mut rom, OpCode::Add);
// JMP to start
utilz::emit_op(&mut rom, OpCode::Jmp);
utilz::emit_u32(&mut rom, loop_start as u32);
// End
let loop_end = rom.len();
utilz::emit_op(&mut rom, OpCode::Halt);
// Patch JMP_IF_FALSE addr
let end_addr_bytes = (loop_end as u32).to_le_bytes();
rom[jmp_placeholder..jmp_placeholder+4].copy_from_slice(&end_addr_bytes);
let constant_pool = vec![Value::Integer(0), Value::Integer(10), Value::Integer(1)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(1000, &mut native).unwrap();
assert!(vm.halted);
// O valor final na pilha deve ser 10
assert_eq!(vm.operand_stack.last(), Some(&Value::Integer(10)));
}
#[test]
fn test_native_call_gfx_clear() {
struct MockGfx {
cleared_color: Option<usize>,
}
impl NativeInterface for MockGfx {
fn syscall(&mut self, id: u32, vm: &mut VirtualMachine) -> Result<u64, String> {
if id == 0x1001 {
let color = vm.pop_integer()? as usize;
self.cleared_color = Some(color);
return Ok(100);
}
Ok(0)
}
}
let mut native = MockGfx { cleared_color: None };
let mut rom = Vec::new();
// PUSH 5 (color index)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0);
// CALL_NATIVE 0x1001
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x1001);
utilz::emit_op(&mut rom, OpCode::Halt);
let constant_pool = vec![Value::Integer(5)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(1000, &mut native).unwrap();
assert!(vm.halted);
assert_eq!(native.cleared_color, Some(5));
}
#[test]
fn test_system_run_cart() {
use crate::LogicalHardware;
use crate::model::Cartridge;
let mut logical_hardware = LogicalHardware::new();
// 1. Verifica que não tem cartucho inicialmente
let mut rom = Vec::new();
// CALL_NATIVE 0x0001 (has_cart)
utilz::emit_op(&mut rom, OpCode::Syscall);
utilz::emit_u32(&mut rom, 0x0001);
utilz::emit_op(&mut rom, OpCode::Halt);
logical_hardware.vm = VirtualMachine::new(rom, vec![]);
let mut vm = std::mem::take(&mut logical_hardware.vm);
vm.run_budget(100, &mut logical_hardware).unwrap();
logical_hardware.vm = vm;
assert_eq!(logical_hardware.vm.pop().unwrap(), Value::Boolean(false));
// 2. Adiciona um cartucho e roda
let mut cart_rom = Vec::new();
// PUSH_CONST 0
utilz::emit_op(&mut cart_rom, OpCode::PushConst);
utilz::emit_u32(&mut cart_rom, 0);
utilz::emit_op(&mut cart_rom, OpCode::Halt);
let cart_pool = vec![Value::Integer(42)];
let cart = Cartridge::new(Program::new(cart_rom, cart_pool));
logical_hardware.load_cartridge(cart);
// Código para rodar o cartucho
let mut boot_rom = Vec::new();
// CALL_NATIVE 0x0002 (run_cart)
utilz::emit_op(&mut boot_rom, OpCode::Syscall);
utilz::emit_u32(&mut boot_rom, 0x0002);
logical_hardware.vm = VirtualMachine::new(boot_rom, vec![]);
let mut vm = std::mem::take(&mut logical_hardware.vm);
vm.run_budget(1000, &mut logical_hardware).unwrap();
logical_hardware.vm = vm;
// Após o run_budget, a VM deve ter executado o cartucho
assert_eq!(logical_hardware.vm.operand_stack.last(), Some(&Value::Integer(42)));
assert!(logical_hardware.vm.halted);
}
#[test]
fn test_stack_operations() {
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
}
let mut native = NoopNative;
let mut rom = Vec::new();
// PUSH 10, PUSH 20, SWAP, POP
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 1);
utilz::emit_op(&mut rom, OpCode::Swap);
utilz::emit_op(&mut rom, OpCode::Pop);
utilz::emit_op(&mut rom, OpCode::Halt);
let constant_pool = vec![Value::Integer(10), Value::Integer(20)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(100, &mut native).unwrap();
assert_eq!(vm.operand_stack.len(), 1);
assert_eq!(vm.operand_stack[0], Value::Integer(20));
}
#[test]
fn test_logical_operations() {
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
}
let mut native = NoopNative;
let mut rom = Vec::new();
// PUSH true, NOT (-> false), PUSH true, OR (-> true)
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0); // true
utilz::emit_op(&mut rom, OpCode::Not);
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0); // true
utilz::emit_op(&mut rom, OpCode::Or);
utilz::emit_op(&mut rom, OpCode::Halt);
let constant_pool = vec![Value::Boolean(true)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(100, &mut native).unwrap();
assert_eq!(vm.operand_stack.last(), Some(&Value::Boolean(true)));
}
#[test]
fn test_scopes() {
struct NoopNative;
impl NativeInterface for NoopNative {
fn syscall(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
}
let mut native = NoopNative;
let mut rom = Vec::new();
// PUSH_SCOPE 2 (reserves 2 nulls), PUSH 42, SET_LOCAL 0, GET_LOCAL 0, POP_SCOPE
utilz::emit_op(&mut rom, OpCode::PushScope);
utilz::emit_u32(&mut rom, 2);
utilz::emit_op(&mut rom, OpCode::PushConst);
utilz::emit_u32(&mut rom, 0); // 42
utilz::emit_op(&mut rom, OpCode::SetLocal);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::GetLocal);
utilz::emit_u32(&mut rom, 0);
utilz::emit_op(&mut rom, OpCode::PopScope);
utilz::emit_op(&mut rom, OpCode::Halt);
let constant_pool = vec![Value::Integer(42)];
let mut vm = VirtualMachine::new(rom, constant_pool);
vm.run_budget(100, &mut native).unwrap();
// Stack should be empty because POP_SCOPE truncates to stack_base
assert_eq!(vm.operand_stack.len(), 0);
}
}

View File

@ -2,7 +2,7 @@ use crate::audio_mixer::AudioMixer;
use cpal::traits::{DeviceTrait, HostTrait, StreamTrait};
use pixels::{Pixels, SurfaceTexture};
use prometeu_core::peripherals::{AudioCommand, InputSignals, OUTPUT_SAMPLE_RATE};
use prometeu_core::LogicalHardware;
use prometeu_core::Hardware;
use ringbuf::traits::{Consumer, Producer, Split};
use ringbuf::HeapRb;
use std::sync::Arc;
@ -18,7 +18,7 @@ pub struct PrometeuRunner {
window: Option<&'static Window>,
pixels: Option<Pixels<'static>>,
logical_hardware: LogicalHardware,
hardware_bridge: Box<Hardware>,
input_signals: InputSignals,
@ -43,11 +43,8 @@ impl PrometeuRunner {
Self {
window: None,
pixels: None,
logical_hardware: LogicalHardware::new(),
hardware_bridge: Hardware::new(),
input_signals: InputSignals::default(),
frame_target_dt: Duration::from_nanos(1_000_000_000 / target_fps),
last_frame_time: Instant::now(),
accumulator: Duration::ZERO,
@ -148,7 +145,7 @@ impl ApplicationHandler for PrometeuRunner {
let size = window.inner_size();
let surface_texture = SurfaceTexture::new(size.width, size.height, window);
let mut pixels = Pixels::new(LogicalHardware::W as u32, LogicalHardware::H as u32, surface_texture)
let mut pixels = Pixels::new(Hardware::W as u32, Hardware::H as u32, surface_texture)
.expect("failed to create Pixels");
pixels.frame_mut().fill(0);
@ -183,7 +180,7 @@ impl ApplicationHandler for PrometeuRunner {
let frame = pixels.frame_mut();
// Borrow imutável do core (campo diferente, ok)
let src = self.logical_hardware.gfx.front_buffer();
let src = self.hardware_bridge.gfx.front_buffer();
draw_rgb565_to_rgba8(src, frame);
} // <- frame borrow termina aqui
@ -256,11 +253,11 @@ impl ApplicationHandler for PrometeuRunner {
// 🔥 O coração do determinismo: consome o tempo em fatias exatas de 60Hz
while self.accumulator >= self.frame_target_dt {
self.logical_hardware.step_frame(&self.input_signals);
self.hardware_bridge.step_frame(&self.input_signals);
// Envia comandos de áudio gerados neste frame para a thread de áudio
if let Some(producer) = &mut self.audio_producer {
for cmd in self.logical_hardware.audio.commands.drain(..) {
for cmd in self.hardware_bridge.audio.commands.drain(..) {
let _ = producer.try_push(cmd);
}
}
@ -282,11 +279,11 @@ impl ApplicationHandler for PrometeuRunner {
if stats_elapsed >= Duration::from_secs(1) {
if let Some(window) = self.window {
let fps = self.frames_since_last_update as f64 / stats_elapsed.as_secs_f64();
let kb = self.logical_hardware.gfx.memory_usage_bytes() as f64 / 1024.0;
let kb = self.hardware_bridge.gfx.memory_usage_bytes() as f64 / 1024.0;
// comparação fixa sempre contra 60Hz, manter mesmo quando fazer teste de stress na CPU
let frame_budget_us = 16666.0;
let cpu_load_core = (self.logical_hardware.last_frame_cpu_time_us as f64 / frame_budget_us) * 100.0;
let cpu_load_core = (self.hardware_bridge.last_frame_cpu_time_us() as f64 / frame_budget_us) * 100.0;
let cpu_load_audio = if self.audio_load_samples > 0 {
// O load real é (tempo total processando) / (tempo total de parede).
@ -297,7 +294,7 @@ impl ApplicationHandler for PrometeuRunner {
let title = format!(
"PROMETEU | GFX: {:.1} KB | FPS: {:.1} | Load: {:.1}% (C) + {:.1}% (A) | Frame: tick {} logical {}",
kb, fps, cpu_load_core, cpu_load_audio, self.logical_hardware.tick_index, self.logical_hardware.logical_frame_index
kb, fps, cpu_load_core, cpu_load_audio, self.hardware_bridge.tick_index(), self.hardware_bridge.logical_frame_index()
);
window.set_title(&title);
}
@ -316,13 +313,13 @@ impl ApplicationHandler for PrometeuRunner {
/// Depois podemos fazer letterbox/aspect-ratio correto.
fn window_to_fb(wx: f32, wy: f32, window: &Window) -> (i32, i32) {
let size = window.inner_size();
let fb_w = LogicalHardware::W as f32;
let fb_h = LogicalHardware::H as f32;
let fb_w = Hardware::W as f32;
let fb_h = Hardware::H as f32;
let x = (wx * fb_w / size.width as f32).floor() as i32;
let y = (wy * fb_h / size.height as f32).floor() as i32;
(x.clamp(0, LogicalHardware::W as i32 - 1), y.clamp(0, LogicalHardware::H as i32 - 1))
(x.clamp(0, Hardware::W as i32 - 1), y.clamp(0, Hardware::H as i32 - 1))
}
/// Copia RGB565 (u16) -> RGBA8888 (u8[4]) para o frame do pixels.