166 lines
5.4 KiB
Rust
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();
|
|
}
|
|
}
|