prometeu-runtime/crates/core/src/logical_hardware.rs
2026-03-24 13:40:12 +00:00

166 lines
5.4 KiB
Rust

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();
}
}