added vm
This commit is contained in:
parent
000ebdebd3
commit
760189929a
@ -1,5 +1,6 @@
|
||||
pub mod machine;
|
||||
pub mod peripherals;
|
||||
pub mod vm;
|
||||
mod model;
|
||||
|
||||
pub use machine::Machine;
|
||||
|
||||
@ -1,7 +1,7 @@
|
||||
use crate::model::Color;
|
||||
use crate::peripherals::{Gfx, InputSignals, Pad, Touch, Audio, LoopMode};
|
||||
use crate::model::{Color, Sample, Cartridge};
|
||||
use crate::peripherals::{Gfx, InputSignals, Pad, Touch, Audio};
|
||||
use crate::vm::{VirtualMachine, NativeInterface, Value};
|
||||
use std::sync::Arc;
|
||||
use crate::model::Sample;
|
||||
|
||||
/// PROMETEU "hardware lógico" (v0).
|
||||
/// O Host alimenta INPUT SIGNALS e chama `step_frame()` em 60Hz.
|
||||
@ -12,6 +12,8 @@ pub struct Machine {
|
||||
pub touch: Touch,
|
||||
pub frame_index: u64,
|
||||
pub last_frame_cpu_time_us: u64,
|
||||
pub vm: VirtualMachine,
|
||||
pub cartridge: Option<Cartridge>,
|
||||
|
||||
// Assets de exemplo
|
||||
pub sample_square: Option<Arc<Sample>>,
|
||||
@ -19,22 +21,164 @@ pub struct Machine {
|
||||
pub sample_snare: Option<Arc<Sample>>,
|
||||
}
|
||||
|
||||
impl NativeInterface for Machine {
|
||||
fn call(&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()));
|
||||
Ok(10)
|
||||
}
|
||||
// system.run_cart()
|
||||
0x0002 => {
|
||||
if let Some(cart) = self.cartridge.as_ref() {
|
||||
vm.rom = cart.rom.clone();
|
||||
vm.constant_pool = cart.constant_pool.clone();
|
||||
vm.pc = 0;
|
||||
vm.operand_stack.clear();
|
||||
vm.call_stack.clear();
|
||||
vm.halted = false;
|
||||
} else {
|
||||
return Err("No cartridge inserted".into());
|
||||
}
|
||||
Ok(100)
|
||||
}
|
||||
// gfx.clear(color_index)
|
||||
0x1001 => {
|
||||
let color_idx = vm.pop_integer()? as usize;
|
||||
let color = self.get_color(color_idx);
|
||||
self.gfx.clear(color);
|
||||
Ok(100)
|
||||
}
|
||||
// gfx.draw_rect(x, y, w, h, color_index)
|
||||
0x1002 => {
|
||||
let color_idx = vm.pop_integer()? as usize;
|
||||
let h = vm.pop_integer()? as i32;
|
||||
let w = vm.pop_integer()? as i32;
|
||||
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);
|
||||
Ok(200)
|
||||
}
|
||||
// input.get_pad(button_id) -> bool
|
||||
0x2001 => {
|
||||
let button_id = vm.pop_integer()? as u32;
|
||||
let is_down = self.is_button_down(button_id);
|
||||
vm.push(Value::Boolean(is_down));
|
||||
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;
|
||||
let volume = vm.pop_integer()? as u8;
|
||||
let voice_id = vm.pop_integer()? as usize;
|
||||
let sample_id = vm.pop_integer()? as u32;
|
||||
|
||||
let sample = match sample_id {
|
||||
0 => self.sample_square.clone(),
|
||||
1 => self.sample_kick.clone(),
|
||||
2 => self.sample_snare.clone(),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
if let Some(s) = sample {
|
||||
self.audio.play(s, voice_id, volume, pan, pitch, 0, crate::peripherals::LoopMode::Off);
|
||||
}
|
||||
Ok(300)
|
||||
}
|
||||
_ => Err(format!("Unknown native call: 0x{:08X}", id)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Machine {
|
||||
pub const W: usize = 320;
|
||||
pub const H: usize = 180;
|
||||
pub const CYCLES_PER_FRAME: u64 = 100_000; // Exemplo de budget
|
||||
|
||||
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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 new() -> Self {
|
||||
Self {
|
||||
let mut machine = Self {
|
||||
gfx: Gfx::new(Self::W, Self::H),
|
||||
audio: Audio::new(),
|
||||
pad: Pad::default(),
|
||||
touch: Touch::default(),
|
||||
frame_index: 0,
|
||||
last_frame_cpu_time_us: 0,
|
||||
vm: VirtualMachine::default(),
|
||||
cartridge: None,
|
||||
sample_square: None,
|
||||
sample_kick: None,
|
||||
sample_snare: None,
|
||||
};
|
||||
|
||||
// Inicializa samples básicos
|
||||
machine.sample_square = Some(Arc::new(Self::create_square_sample(440.0, 0.1)));
|
||||
|
||||
machine
|
||||
}
|
||||
|
||||
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).
|
||||
@ -42,7 +186,13 @@ impl Machine {
|
||||
pub fn step_frame(&mut self, signals: &InputSignals) {
|
||||
let start = std::time::Instant::now();
|
||||
self.begin_frame(signals);
|
||||
self.tick(); // futuro: executa cartucho/VM
|
||||
|
||||
// Retira a VM temporariamente para executar run_budget passando self (Machine) como NativeInterface.
|
||||
// Como VirtualMachine implementa Default, usamos std::mem::take.
|
||||
let mut vm = std::mem::take(&mut self.vm);
|
||||
let _result = vm.run_budget(Self::CYCLES_PER_FRAME, self);
|
||||
self.vm = vm;
|
||||
|
||||
self.gfx.render_all(); // A máquina executa o pipeline gráfico (Ação física)
|
||||
self.end_frame(); // present/housekeeping
|
||||
self.last_frame_cpu_time_us = start.elapsed().as_micros() as u64; // Calcula quanto tempo a "CPU simulada" trabalhou de verdade
|
||||
@ -64,243 +214,8 @@ impl Machine {
|
||||
self.pad.begin_frame(signals);
|
||||
}
|
||||
|
||||
/// Lógica do frame (demo hardcoded por enquanto).
|
||||
pub fn tick(&mut self) {
|
||||
// Limpa a tela com Preto
|
||||
self.gfx.clear(Color::GRAY);
|
||||
|
||||
// SETUP BANCO 0 (Tiles 8x8)
|
||||
if self.gfx.banks[0].is_none() {
|
||||
let mut bank = crate::model::TileBank::new(crate::model::TileSize::Size8, 128, 128);
|
||||
// Define Cor na Paleta 0, Índice 1 = VERDE
|
||||
bank.palettes[0][1] = Color::GREEN;
|
||||
|
||||
let tile_size = 8;
|
||||
let start_x = 8; // Tile ID 1
|
||||
for y in 0..tile_size {
|
||||
for x in 0..tile_size {
|
||||
let idx = y * 128 + (start_x + x);
|
||||
bank.pixel_indices[idx] = 1; // Atribui o índice da paleta
|
||||
}
|
||||
}
|
||||
self.gfx.banks[0] = Some(bank);
|
||||
}
|
||||
|
||||
// SETUP BANCO 1 (Tiles 16x16)
|
||||
if self.gfx.banks[1].is_none() {
|
||||
let mut bank = crate::model::TileBank::new(crate::model::TileSize::Size16, 128, 128);
|
||||
// Define Cor na Paleta 0, Índice 1 = VERMELHO
|
||||
bank.palettes[0][1] = Color::RED;
|
||||
|
||||
let tile_size = 16;
|
||||
let start_x = 16; // Tile ID 1
|
||||
for y in 0..tile_size {
|
||||
for x in 0..tile_size {
|
||||
let idx = y * 128 + (start_x + x);
|
||||
bank.pixel_indices[idx] = 1;
|
||||
}
|
||||
}
|
||||
self.gfx.banks[1] = Some(bank);
|
||||
}
|
||||
|
||||
// SETUP BANCO 2 (Tiles 16x16 para Sprites)
|
||||
if self.gfx.banks[2].is_none() {
|
||||
let mut bank = crate::model::TileBank::new(crate::model::TileSize::Size16, 128, 128);
|
||||
bank.palettes[0][1] = Color::INDIGO;
|
||||
bank.palettes[1][1] = Color::YELLOW;
|
||||
bank.palettes[2][1] = Color::RED;
|
||||
bank.palettes[3][1] = Color::GREEN;
|
||||
bank.palettes[4][1] = Color::ORANGE;
|
||||
|
||||
let tile_size = 16;
|
||||
let start_x = 16;
|
||||
for y in 0..tile_size {
|
||||
for x in 0..tile_size {
|
||||
let idx = y * 128 + (start_x + x);
|
||||
bank.pixel_indices[idx] = 1;
|
||||
}
|
||||
}
|
||||
self.gfx.banks[2] = Some(bank);
|
||||
}
|
||||
|
||||
// --- LÓGICA DE ESTADO ---
|
||||
|
||||
// HUD
|
||||
self.gfx.hud.map.set_tile(18, 6, crate::model::Tile { id: 1, palette_id: 0, ..Default::default() });
|
||||
|
||||
// Camadas de Jogo (usando Banco 1 - Vermelho)
|
||||
for i in 0..4 {
|
||||
self.gfx.layers[i].bank_id = 1;
|
||||
self.gfx.layers[i].map.set_tile(8 + i, 1 + i, crate::model::Tile { id: 1, palette_id: 0, ..Default::default() });
|
||||
}
|
||||
|
||||
// 512 Sprites (usando Banco 2 - Indigo e Amarelo)
|
||||
for i in 0..510 {
|
||||
let s = &mut self.gfx.sprites[i];
|
||||
s.active = true;
|
||||
s.bank_id = 2;
|
||||
s.tile.id = 1;
|
||||
|
||||
// Palette Swap: pares Indigo (0), ímpares Amarelo (1)
|
||||
s.tile.palette_id = (i % 2) as u8;
|
||||
|
||||
let speed = (1 + (i % 3)) as u64;
|
||||
s.x = (((self.frame_index * speed) + (i as u64 * 10)) % 400) as i32 - 40;
|
||||
let y_base = (((self.frame_index * speed) + (i as u64 * 4)) % 180) as i32;
|
||||
s.y = if i % 2 == 0 { y_base } else { 179 - y_base };
|
||||
|
||||
s.priority = (i % 5) as u8;
|
||||
s.flip_x = i % 2 == 0;
|
||||
s.flip_y = i % 3 == 0;
|
||||
}
|
||||
|
||||
// --- INTERATIVIDADE COM TOUCH ---
|
||||
let cursor = &mut self.gfx.sprites[511];
|
||||
cursor.active = true;
|
||||
cursor.bank_id = 2; // Banco Verde
|
||||
cursor.tile.id = 1;
|
||||
cursor.priority = 4;
|
||||
|
||||
// Se o dedo/mouse estiver pressionado agora (down)
|
||||
if self.touch.f.down {
|
||||
cursor.x = self.touch.x - 4; // Centraliza tile 8x8
|
||||
cursor.y = self.touch.y - 4;
|
||||
cursor.tile.palette_id = 2; // RED
|
||||
} else {
|
||||
cursor.tile.palette_id = 3; // GREEN
|
||||
}
|
||||
|
||||
// --- INTERATIVIDADE COM INPUT (Gamepad) ---
|
||||
let player = &mut self.gfx.sprites[510]; // Usaremos outro sprite para o Pad
|
||||
player.active = true;
|
||||
player.bank_id = 2;
|
||||
player.tile.id = 1;
|
||||
player.priority = 4;
|
||||
player.tile.palette_id = 4; // ORANGE
|
||||
|
||||
// Movimento contínuo (enquanto segura)
|
||||
let move_speed = 2;
|
||||
if self.pad.up.down { player.y -= move_speed; }
|
||||
if self.pad.down.down { player.y += move_speed; }
|
||||
if self.pad.left.down { player.x -= move_speed; }
|
||||
if self.pad.right.down { player.x += move_speed; }
|
||||
|
||||
// Trigger (apenas no frame que apertou)
|
||||
if self.pad.a.down {
|
||||
player.tile.palette_id = 2;
|
||||
}
|
||||
|
||||
// Exemplo de uso do released
|
||||
if self.pad.start.released {
|
||||
// Se soltar o Start, podemos pausar algo futuramente
|
||||
}
|
||||
|
||||
// --- DEMO DE ÁUDIO ---
|
||||
// Inicializa assets de áudio se não existirem
|
||||
if self.sample_square.is_none() {
|
||||
// 1. Onda Quadrada com Loop (Instrumento)
|
||||
let freq = 440.0;
|
||||
let sample_rate = 44100;
|
||||
let period = (sample_rate as f32 / freq) as usize;
|
||||
let mut data = Vec::with_capacity(period);
|
||||
for i in 0..period {
|
||||
data.push(if i < period / 2 { 4000 } else { -4000 });
|
||||
}
|
||||
self.sample_square = Some(Arc::new(Sample::new(sample_rate, data).with_loop(0, period as u32)));
|
||||
|
||||
// 2. "Kick" simples (Pitch descent)
|
||||
let mut kick_data = Vec::new();
|
||||
for i in 0..4000 {
|
||||
let t = i as f32 / 4000.0;
|
||||
let f = 100.0 * (1.0 - t * 0.9);
|
||||
let val = (i as f32 * f * 2.0 * std::f32::consts::PI / 44100.0).sin();
|
||||
kick_data.push((val * 12000.0 * (1.0 - t)) as i16);
|
||||
}
|
||||
self.sample_kick = Some(Arc::new(Sample::new(44100, kick_data)));
|
||||
|
||||
// 3. "Snare" simples (White noise)
|
||||
let mut snare_data = Vec::new();
|
||||
for i in 0..3000 {
|
||||
let t = i as f32 / 3000.0;
|
||||
let noise = Self::rand_f32(i as u32) * 2.0 - 1.0;
|
||||
snare_data.push((noise * 8000.0 * (1.0 - t)) as i16);
|
||||
}
|
||||
self.sample_snare = Some(Arc::new(Sample::new(44100, snare_data)));
|
||||
}
|
||||
|
||||
// --- SEQUENCER SIMPLES (60Hz) ---
|
||||
let bpm = 125;
|
||||
let frames_per_beat = (60 * 60) / bpm;
|
||||
let frames_per_step = frames_per_beat / 4; // 16th notes
|
||||
|
||||
let step = (self.frame_index / frames_per_step) % 16;
|
||||
let is_new_step = self.frame_index % frames_per_step == 0;
|
||||
|
||||
if is_new_step {
|
||||
// Bassline (Square)
|
||||
// Notas: C-2, C-2, Eb2, F-2, C-2, C-2, G-2, Bb2...
|
||||
let melody = [
|
||||
36, 36, 39, 41,
|
||||
36, 36, 43, 46,
|
||||
36, 36, 39, 41,
|
||||
34, 35, 36, 0,
|
||||
];
|
||||
|
||||
let note = melody[step as usize];
|
||||
if note > 0 {
|
||||
// Cálculo de pitch otimizado (tabela simples ou aproximação rápida se necessário,
|
||||
// mas powf uma vez por step está ok. O problema é se fosse todo frame)
|
||||
let freq = 440.0 * 2.0f64.powf((note as f64 - 69.0) / 12.0);
|
||||
let pitch = freq / 440.0;
|
||||
if let Some(s) = &self.sample_square {
|
||||
self.audio.play(s.clone(), 0, 160, 100, pitch, 0, LoopMode::On);
|
||||
}
|
||||
}
|
||||
|
||||
// Drums
|
||||
let kick_pattern = 0b0101100100011001u16; // Invertido para facilitar bitshift
|
||||
let snare_pattern = 0b0001000000001000u16;
|
||||
|
||||
if (kick_pattern >> step) & 1 == 1 {
|
||||
if let Some(s) = &self.sample_kick {
|
||||
self.audio.play(s.clone(), 1, 255, 127, 1.0, 1, LoopMode::Off);
|
||||
}
|
||||
}
|
||||
if (snare_pattern >> step) & 1 == 1 {
|
||||
if let Some(s) = &self.sample_snare {
|
||||
self.audio.play(s.clone(), 2, 140, 150, 1.0, 1, LoopMode::Off);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Toca um "beep" extra se apertar A (mantendo interatividade)
|
||||
if self.pad.a.pressed {
|
||||
if let Some(s) = &self.sample_square {
|
||||
self.audio.play(s.clone(), 3, 255, 60, 2.0, 10, LoopMode::Off);
|
||||
}
|
||||
}
|
||||
|
||||
// Post-FX Fade Pulsante
|
||||
let pulse = (self.frame_index / 4) % 64;
|
||||
let level = if pulse > 31 { 63 - pulse } else { pulse };
|
||||
self.gfx.scene_fade_level = level as u8;
|
||||
self.gfx.scene_fade_color = Color::BLACK;
|
||||
self.gfx.hud_fade_level = 31;
|
||||
}
|
||||
|
||||
/// Final do frame: troca buffers.
|
||||
pub fn end_frame(&mut self) {
|
||||
self.gfx.present();
|
||||
}
|
||||
|
||||
/// Helper para noise determinístico
|
||||
fn rand_f32(seed: u32) -> f32 {
|
||||
let mut x = seed.wrapping_add(0x9E3779B9);
|
||||
x = x ^ (x >> 16);
|
||||
x = x.wrapping_mul(0x85EBCA6B);
|
||||
x = x ^ (x >> 13);
|
||||
x = x.wrapping_mul(0xC2B2AE35);
|
||||
x = x ^ (x >> 16);
|
||||
(x as f32) / (u32::MAX as f32)
|
||||
}
|
||||
}
|
||||
|
||||
13
crates/core/src/model/cartridge.rs
Normal file
13
crates/core/src/model/cartridge.rs
Normal file
@ -0,0 +1,13 @@
|
||||
use crate::vm::Value;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Cartridge {
|
||||
pub rom: Vec<u8>,
|
||||
pub constant_pool: Vec<Value>,
|
||||
}
|
||||
|
||||
impl Cartridge {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
||||
Self { rom, constant_pool }
|
||||
}
|
||||
}
|
||||
@ -5,6 +5,7 @@ mod tile_layer;
|
||||
mod tile_bank;
|
||||
mod sprite;
|
||||
mod sample;
|
||||
mod cartridge;
|
||||
|
||||
pub use button::Button;
|
||||
pub use color::Color;
|
||||
@ -13,3 +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;
|
||||
|
||||
820
crates/core/src/vm.rs
Normal file
820
crates/core/src/vm.rs
Normal file
@ -0,0 +1,820 @@
|
||||
use std::convert::TryFrom;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
#[repr(u16)]
|
||||
pub enum OpCode {
|
||||
// 6.1 Controle de Execução
|
||||
Nop = 0x00,
|
||||
Halt = 0x01,
|
||||
Jmp = 0x02,
|
||||
JmpIfFalse = 0x03,
|
||||
|
||||
// 6.2 Pilha
|
||||
PushConst = 0x10,
|
||||
Pop = 0x11,
|
||||
Dup = 0x12,
|
||||
Swap = 0x13,
|
||||
|
||||
// 6.3 Aritmética
|
||||
Add = 0x20,
|
||||
Sub = 0x21,
|
||||
Mul = 0x22,
|
||||
Div = 0x23,
|
||||
|
||||
// 6.4 Comparação e Lógica
|
||||
Eq = 0x30,
|
||||
Neq = 0x31,
|
||||
Lt = 0x32,
|
||||
Gt = 0x33,
|
||||
And = 0x34,
|
||||
Or = 0x35,
|
||||
Not = 0x36,
|
||||
|
||||
// 6.5 Variáveis
|
||||
GetGlobal = 0x40,
|
||||
SetGlobal = 0x41,
|
||||
GetLocal = 0x42,
|
||||
SetLocal = 0x43,
|
||||
|
||||
// 6.6 Funções
|
||||
Call = 0x50,
|
||||
Ret = 0x51,
|
||||
PushScope = 0x52,
|
||||
PopScope = 0x53,
|
||||
|
||||
// 6.7 Heap
|
||||
Alloc = 0x60,
|
||||
LoadRef = 0x61,
|
||||
StoreRef = 0x62,
|
||||
|
||||
// 6.8 Periféricos e Sistema
|
||||
Syscall = 0x70,
|
||||
FrameSync = 0x80,
|
||||
}
|
||||
|
||||
impl TryFrom<u16> for OpCode {
|
||||
type Error = String;
|
||||
|
||||
fn try_from(value: u16) -> Result<Self, Self::Error> {
|
||||
match value {
|
||||
0x00 => Ok(OpCode::Nop),
|
||||
0x01 => Ok(OpCode::Halt),
|
||||
0x02 => Ok(OpCode::Jmp),
|
||||
0x03 => Ok(OpCode::JmpIfFalse),
|
||||
0x10 => Ok(OpCode::PushConst),
|
||||
0x11 => Ok(OpCode::Pop),
|
||||
0x12 => Ok(OpCode::Dup),
|
||||
0x13 => Ok(OpCode::Swap),
|
||||
0x20 => Ok(OpCode::Add),
|
||||
0x21 => Ok(OpCode::Sub),
|
||||
0x22 => Ok(OpCode::Mul),
|
||||
0x23 => Ok(OpCode::Div),
|
||||
0x30 => Ok(OpCode::Eq),
|
||||
0x31 => Ok(OpCode::Neq),
|
||||
0x32 => Ok(OpCode::Lt),
|
||||
0x33 => Ok(OpCode::Gt),
|
||||
0x34 => Ok(OpCode::And),
|
||||
0x35 => Ok(OpCode::Or),
|
||||
0x36 => Ok(OpCode::Not),
|
||||
0x40 => Ok(OpCode::GetGlobal),
|
||||
0x41 => Ok(OpCode::SetGlobal),
|
||||
0x42 => Ok(OpCode::GetLocal),
|
||||
0x43 => Ok(OpCode::SetLocal),
|
||||
0x50 => Ok(OpCode::Call),
|
||||
0x51 => Ok(OpCode::Ret),
|
||||
0x52 => Ok(OpCode::PushScope),
|
||||
0x53 => Ok(OpCode::PopScope),
|
||||
0x60 => Ok(OpCode::Alloc),
|
||||
0x61 => Ok(OpCode::LoadRef),
|
||||
0x62 => Ok(OpCode::StoreRef),
|
||||
0x70 => Ok(OpCode::Syscall),
|
||||
0x80 => Ok(OpCode::FrameSync),
|
||||
_ => Err(format!("Invalid OpCode: 0x{:04X}", value)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpCode {
|
||||
pub fn cycles(&self) -> u64 {
|
||||
match self {
|
||||
OpCode::Nop => 1,
|
||||
OpCode::Halt => 1,
|
||||
OpCode::Jmp => 2,
|
||||
OpCode::JmpIfFalse => 3,
|
||||
OpCode::PushConst => 2,
|
||||
OpCode::Pop => 1,
|
||||
OpCode::Dup => 1,
|
||||
OpCode::Swap => 1,
|
||||
OpCode::Add => 2,
|
||||
OpCode::Sub => 2,
|
||||
OpCode::Mul => 4,
|
||||
OpCode::Div => 6,
|
||||
OpCode::Eq => 2,
|
||||
OpCode::Neq => 2,
|
||||
OpCode::Lt => 2,
|
||||
OpCode::Gt => 2,
|
||||
OpCode::And => 2,
|
||||
OpCode::Or => 2,
|
||||
OpCode::Not => 1,
|
||||
OpCode::GetGlobal => 3,
|
||||
OpCode::SetGlobal => 3,
|
||||
OpCode::GetLocal => 2,
|
||||
OpCode::SetLocal => 2,
|
||||
OpCode::Call => 5,
|
||||
OpCode::Ret => 4,
|
||||
OpCode::PushScope => 3,
|
||||
OpCode::PopScope => 3,
|
||||
OpCode::Alloc => 10,
|
||||
OpCode::LoadRef => 3,
|
||||
OpCode::StoreRef => 3,
|
||||
OpCode::Syscall => 1, // Variável, mas vamos usar 1 como base ou definir via ID
|
||||
OpCode::FrameSync => 1,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum Value {
|
||||
Integer(i64),
|
||||
Float(f64),
|
||||
Boolean(bool),
|
||||
String(String),
|
||||
Ref(usize), // Referência ao heap
|
||||
Null,
|
||||
}
|
||||
|
||||
impl PartialEq for Value {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
match (self, other) {
|
||||
(Value::Integer(a), Value::Integer(b)) => a == b,
|
||||
(Value::Float(a), Value::Float(b)) => a == b,
|
||||
(Value::Integer(a), Value::Float(b)) => *a as f64 == *b,
|
||||
(Value::Float(a), Value::Integer(b)) => *a == *b as f64,
|
||||
(Value::Boolean(a), Value::Boolean(b)) => a == b,
|
||||
(Value::String(a), Value::String(b)) => a == b,
|
||||
(Value::Ref(a), Value::Ref(b)) => a == b,
|
||||
(Value::Null, Value::Null) => true,
|
||||
_ => false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Value {
|
||||
pub fn as_float(&self) -> Option<f64> {
|
||||
match self {
|
||||
Value::Integer(i) => Some(*i as f64),
|
||||
Value::Float(f) => Some(*f),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn as_integer(&self) -> Option<i64> {
|
||||
match self {
|
||||
Value::Integer(i) => Some(*i),
|
||||
Value::Float(f) => Some(*f as i64),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct CallFrame {
|
||||
pub return_address: usize,
|
||||
pub stack_base: usize,
|
||||
pub locals_count: usize,
|
||||
}
|
||||
|
||||
pub trait NativeInterface {
|
||||
fn call(&mut self, id: u32, vm: &mut VirtualMachine) -> Result<u64, String>;
|
||||
}
|
||||
|
||||
pub struct VirtualMachine {
|
||||
pub pc: usize,
|
||||
pub operand_stack: Vec<Value>,
|
||||
pub call_stack: Vec<CallFrame>,
|
||||
pub globals: Vec<Value>,
|
||||
pub constant_pool: Vec<Value>,
|
||||
pub rom: Vec<u8>,
|
||||
pub heap: Vec<Value>, // Simplificado para demo, futuramente RAM/Heap real
|
||||
pub cycles: u64,
|
||||
pub halted: bool,
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
pub fn new(rom: Vec<u8>, constant_pool: Vec<Value>) -> Self {
|
||||
Self {
|
||||
pc: 0,
|
||||
operand_stack: Vec::new(),
|
||||
call_stack: Vec::new(),
|
||||
globals: Vec::new(),
|
||||
constant_pool,
|
||||
rom,
|
||||
heap: Vec::new(),
|
||||
cycles: 0,
|
||||
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();
|
||||
|
||||
// Constantes do Boot ROM
|
||||
constant_pool.push(Value::Integer(140)); // 0: x
|
||||
constant_pool.push(Value::Integer(70)); // 1: y
|
||||
constant_pool.push(Value::Integer(40)); // 2: largura
|
||||
constant_pool.push(Value::Integer(40)); // 3: altura
|
||||
constant_pool.push(Value::Integer(7)); // 4: cor rect (indigo)
|
||||
constant_pool.push(Value::Integer(0)); // 5: sample_id (square)
|
||||
constant_pool.push(Value::Integer(0)); // 6: voice_id
|
||||
constant_pool.push(Value::Integer(255)); // 7: volume
|
||||
constant_pool.push(Value::Integer(127)); // 8: pan (center)
|
||||
constant_pool.push(Value::Float(1.0)); // 9: pitch
|
||||
constant_pool.push(Value::Integer(0)); // 10: bg color (black)
|
||||
constant_pool.push(Value::Integer(10)); // 11: button START
|
||||
|
||||
// -- PROGRAMA --
|
||||
|
||||
// 1. Toca o som de boot "plim" (uma vez)
|
||||
// Push arguments for audio.play_sample(sample_id, voice_id, volume, pan, pitch)
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&5u32.to_le_bytes()); // sample_id
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&6u32.to_le_bytes()); // voice_id
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&7u32.to_le_bytes()); // volume
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&8u32.to_le_bytes()); // pan
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&9u32.to_le_bytes()); // pitch
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x3001u32.to_le_bytes());
|
||||
|
||||
let loop_start = rom.len() as u32;
|
||||
|
||||
// 2. Verifica Cartucho e Input para Boot
|
||||
// system.has_cart?
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x0001u32.to_le_bytes());
|
||||
|
||||
let jmp_no_cart_idx = rom.len();
|
||||
rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes()); // placeholder
|
||||
|
||||
// Se tem cartucho, checa START
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&11u32.to_le_bytes()); // START button ID
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x2001u32.to_le_bytes()); // input.get_pad
|
||||
|
||||
let jmp_no_start_idx = rom.len();
|
||||
rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes()); // placeholder
|
||||
|
||||
// Se tem cartucho E START, run_cart!
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x0002u32.to_le_bytes()); // system.run_cart
|
||||
|
||||
// Destino para quando não tem cartucho ou não apertou START
|
||||
let skip_cart_addr = rom.len() as u32;
|
||||
// Patch placeholders
|
||||
let skip_bytes = skip_cart_addr.to_le_bytes();
|
||||
rom[jmp_no_cart_idx+2..jmp_no_cart_idx+6].copy_from_slice(&skip_bytes);
|
||||
rom[jmp_no_start_idx+2..jmp_no_start_idx+6].copy_from_slice(&skip_bytes);
|
||||
|
||||
// 3. Limpa a tela
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&10u32.to_le_bytes()); // bg color
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x1001u32.to_le_bytes());
|
||||
|
||||
// 4. Desenha o quadrado no centro
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes()); // x
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&1u32.to_le_bytes()); // y
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&2u32.to_le_bytes()); // w
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&3u32.to_le_bytes()); // h
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&4u32.to_le_bytes()); // color
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x1002u32.to_le_bytes());
|
||||
|
||||
// 4. Sincroniza frame
|
||||
rom.extend_from_slice(&(OpCode::FrameSync as u16).to_le_bytes());
|
||||
|
||||
// 5. Loop infinito
|
||||
rom.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&loop_start.to_le_bytes());
|
||||
|
||||
Self::new(rom, constant_pool)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for VirtualMachine {
|
||||
fn default() -> Self {
|
||||
Self::new_boot_rom()
|
||||
}
|
||||
}
|
||||
|
||||
impl VirtualMachine {
|
||||
pub fn run_budget(&mut self, budget: u64, native: &mut dyn NativeInterface) -> Result<u64, String> {
|
||||
let start_cycles = self.cycles;
|
||||
let mut budget_used = 0;
|
||||
|
||||
while budget_used < budget && !self.halted && self.pc < self.rom.len() {
|
||||
let pc_before = self.pc;
|
||||
let cycles_before = self.cycles;
|
||||
let opcode_val = self.peek_u16()?;
|
||||
let opcode = OpCode::try_from(opcode_val)?;
|
||||
|
||||
if opcode == OpCode::FrameSync {
|
||||
self.pc += 2;
|
||||
self.cycles += OpCode::FrameSync.cycles();
|
||||
break;
|
||||
}
|
||||
|
||||
self.step(native)?;
|
||||
|
||||
// Garantir progresso para evitar loop infinito real (onde nem PC nem ciclos avançam)
|
||||
if self.pc == pc_before && self.cycles == cycles_before && !self.halted {
|
||||
return Err(format!("VM stuck at PC 0x{:08X}", self.pc));
|
||||
}
|
||||
|
||||
budget_used = self.cycles - start_cycles;
|
||||
}
|
||||
|
||||
Ok(budget_used)
|
||||
}
|
||||
|
||||
fn peek_u16(&self) -> Result<u16, String> {
|
||||
if self.pc + 2 > self.rom.len() {
|
||||
return Err("Unexpected end of ROM".into());
|
||||
}
|
||||
let bytes = [
|
||||
self.rom[self.pc],
|
||||
self.rom[self.pc + 1],
|
||||
];
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub fn step(&mut self, native: &mut dyn NativeInterface) -> Result<(), String> {
|
||||
if self.halted || self.pc >= self.rom.len() {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let opcode_val = self.read_u16()?;
|
||||
let opcode = OpCode::try_from(opcode_val)?;
|
||||
|
||||
match opcode {
|
||||
OpCode::Nop => {}
|
||||
OpCode::Halt => {
|
||||
self.halted = true;
|
||||
}
|
||||
OpCode::Jmp => {
|
||||
let addr = self.read_u32()? as usize;
|
||||
self.pc = addr;
|
||||
}
|
||||
OpCode::JmpIfFalse => {
|
||||
let addr = self.read_u32()? as usize;
|
||||
let val = self.pop()?;
|
||||
if let Value::Boolean(false) = val {
|
||||
self.pc = addr;
|
||||
}
|
||||
}
|
||||
OpCode::PushConst => {
|
||||
let idx = self.read_u32()? as usize;
|
||||
let val = self.constant_pool.get(idx).cloned().ok_or("Invalid constant index")?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::Pop => {
|
||||
self.pop()?;
|
||||
}
|
||||
OpCode::Dup => {
|
||||
let val = self.peek()?.clone();
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::Swap => {
|
||||
let a = self.pop()?;
|
||||
let b = self.pop()?;
|
||||
self.push(a);
|
||||
self.push(b);
|
||||
}
|
||||
OpCode::Add => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_add(b))),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a + b)),
|
||||
(Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 + b)),
|
||||
(Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a + b as f64)),
|
||||
_ => Err("Invalid types for ADD".into()),
|
||||
})?,
|
||||
OpCode::Sub => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_sub(b))),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a - b)),
|
||||
(Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 - b)),
|
||||
(Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a - b as f64)),
|
||||
_ => Err("Invalid types for SUB".into()),
|
||||
})?,
|
||||
OpCode::Mul => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => Ok(Value::Integer(a.wrapping_mul(b))),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Float(a * b)),
|
||||
(Value::Integer(a), Value::Float(b)) => Ok(Value::Float(a as f64 * b)),
|
||||
(Value::Float(a), Value::Integer(b)) => Ok(Value::Float(a * b as f64)),
|
||||
_ => Err("Invalid types for MUL".into()),
|
||||
})?,
|
||||
OpCode::Div => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => {
|
||||
if b == 0 { return Err("Division by zero".into()); }
|
||||
Ok(Value::Integer(a / b))
|
||||
}
|
||||
(Value::Float(a), Value::Float(b)) => {
|
||||
if b == 0.0 { return Err("Division by zero".into()); }
|
||||
Ok(Value::Float(a / b))
|
||||
}
|
||||
(Value::Integer(a), Value::Float(b)) => {
|
||||
if b == 0.0 { return Err("Division by zero".into()); }
|
||||
Ok(Value::Float(a as f64 / b))
|
||||
}
|
||||
(Value::Float(a), Value::Integer(b)) => {
|
||||
if b == 0 { return Err("Division by zero".into()); }
|
||||
Ok(Value::Float(a / b as f64))
|
||||
}
|
||||
_ => Err("Invalid types for DIV".into()),
|
||||
})?,
|
||||
OpCode::Eq => self.binary_op(|a, b| Ok(Value::Boolean(a == b)))?,
|
||||
OpCode::Neq => self.binary_op(|a, b| Ok(Value::Boolean(a != b)))?,
|
||||
OpCode::Lt => self.binary_op(|a, b| {
|
||||
match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => Ok(Value::Boolean(a < b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Boolean(a < b)),
|
||||
(Value::Integer(a), Value::Float(b)) => Ok(Value::Boolean((a as f64) < b)),
|
||||
(Value::Float(a), Value::Integer(b)) => Ok(Value::Boolean(a < (b as f64))),
|
||||
_ => Err("Invalid types for LT".into()),
|
||||
}
|
||||
})?,
|
||||
OpCode::Gt => self.binary_op(|a, b| {
|
||||
match (a, b) {
|
||||
(Value::Integer(a), Value::Integer(b)) => Ok(Value::Boolean(a > b)),
|
||||
(Value::Float(a), Value::Float(b)) => Ok(Value::Boolean(a > b)),
|
||||
(Value::Integer(a), Value::Float(b)) => Ok(Value::Boolean((a as f64) > b)),
|
||||
(Value::Float(a), Value::Integer(b)) => Ok(Value::Boolean(a > (b as f64))),
|
||||
_ => Err("Invalid types for GT".into()),
|
||||
}
|
||||
})?,
|
||||
OpCode::And => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a && b)),
|
||||
_ => Err("Invalid types for AND".into()),
|
||||
})?,
|
||||
OpCode::Or => self.binary_op(|a, b| match (a, b) {
|
||||
(Value::Boolean(a), Value::Boolean(b)) => Ok(Value::Boolean(a || b)),
|
||||
_ => Err("Invalid types for OR".into()),
|
||||
})?,
|
||||
OpCode::Not => {
|
||||
let val = self.pop()?;
|
||||
if let Value::Boolean(b) = val {
|
||||
self.push(Value::Boolean(!b));
|
||||
} else {
|
||||
return Err("Invalid type for NOT".into());
|
||||
}
|
||||
}
|
||||
OpCode::GetGlobal => {
|
||||
let idx = self.read_u32()? as usize;
|
||||
let val = self.globals.get(idx).cloned().ok_or("Invalid global index")?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::SetGlobal => {
|
||||
let idx = self.read_u32()? as usize;
|
||||
let val = self.pop()?;
|
||||
if idx >= self.globals.len() {
|
||||
self.globals.resize(idx + 1, Value::Null);
|
||||
}
|
||||
self.globals[idx] = val;
|
||||
}
|
||||
OpCode::GetLocal => {
|
||||
let idx = self.read_u32()? as usize;
|
||||
let frame = self.call_stack.last().ok_or("No active call frame")?;
|
||||
let val = self.operand_stack.get(frame.stack_base + idx).cloned().ok_or("Invalid local index")?;
|
||||
self.push(val);
|
||||
}
|
||||
OpCode::SetLocal => {
|
||||
let idx = self.read_u32()? as usize;
|
||||
let val = self.pop()?;
|
||||
let frame = self.call_stack.last().ok_or("No active call frame")?;
|
||||
let stack_idx = frame.stack_base + idx;
|
||||
if stack_idx >= self.operand_stack.len() {
|
||||
return Err("Local index out of bounds".into());
|
||||
}
|
||||
self.operand_stack[stack_idx] = val;
|
||||
}
|
||||
OpCode::Call => {
|
||||
let addr = self.read_u32()? as usize;
|
||||
let args_count = self.read_u32()? as usize;
|
||||
let stack_base = self.operand_stack.len() - args_count;
|
||||
self.call_stack.push(CallFrame {
|
||||
return_address: self.pc,
|
||||
stack_base,
|
||||
locals_count: args_count,
|
||||
});
|
||||
self.pc = addr;
|
||||
}
|
||||
OpCode::Ret => {
|
||||
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
||||
let return_val = self.pop()?;
|
||||
self.operand_stack.truncate(frame.stack_base);
|
||||
self.push(return_val);
|
||||
self.pc = frame.return_address;
|
||||
}
|
||||
OpCode::PushScope => {
|
||||
let locals_count = self.read_u32()? as usize;
|
||||
let stack_base = self.operand_stack.len();
|
||||
for _ in 0..locals_count {
|
||||
self.push(Value::Null);
|
||||
}
|
||||
self.call_stack.push(CallFrame {
|
||||
return_address: 0,
|
||||
stack_base,
|
||||
locals_count,
|
||||
});
|
||||
}
|
||||
OpCode::PopScope => {
|
||||
let frame = self.call_stack.pop().ok_or("Call stack underflow")?;
|
||||
self.operand_stack.truncate(frame.stack_base);
|
||||
}
|
||||
OpCode::Alloc => {
|
||||
let size = self.read_u32()? as usize;
|
||||
let ref_idx = self.heap.len();
|
||||
for _ in 0..size {
|
||||
self.heap.push(Value::Null);
|
||||
}
|
||||
self.push(Value::Ref(ref_idx));
|
||||
}
|
||||
OpCode::LoadRef => {
|
||||
let offset = self.read_u32()? as usize;
|
||||
let ref_val = self.pop()?;
|
||||
if let Value::Ref(base) = ref_val {
|
||||
let val = self.heap.get(base + offset).cloned().ok_or("Invalid heap access")?;
|
||||
self.push(val);
|
||||
} else {
|
||||
return Err("Expected reference for LOAD_REF".into());
|
||||
}
|
||||
}
|
||||
OpCode::StoreRef => {
|
||||
let offset = self.read_u32()? as usize;
|
||||
let val = self.pop()?;
|
||||
let ref_val = self.pop()?;
|
||||
if let Value::Ref(base) = ref_val {
|
||||
if base + offset >= self.heap.len() {
|
||||
return Err("Invalid heap access".into());
|
||||
}
|
||||
self.heap[base + offset] = val;
|
||||
} else {
|
||||
return Err("Expected reference for STORE_REF".into());
|
||||
}
|
||||
}
|
||||
OpCode::Syscall => {
|
||||
let id = self.read_u32()?;
|
||||
let native_cycles = native.call(id, self).map_err(|e| format!("Native call 0x{:08X} failed: {}", id, e))?;
|
||||
self.cycles += native_cycles;
|
||||
}
|
||||
OpCode::FrameSync => {
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
self.cycles += opcode.cycles();
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_u32(&mut self) -> Result<u32, String> {
|
||||
if self.pc + 4 > self.rom.len() {
|
||||
return Err("Unexpected end of ROM".into());
|
||||
}
|
||||
let bytes = [
|
||||
self.rom[self.pc],
|
||||
self.rom[self.pc + 1],
|
||||
self.rom[self.pc + 2],
|
||||
self.rom[self.pc + 3],
|
||||
];
|
||||
self.pc += 4;
|
||||
Ok(u32::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
fn read_u16(&mut self) -> Result<u16, String> {
|
||||
if self.pc + 2 > self.rom.len() {
|
||||
return Err("Unexpected end of ROM".into());
|
||||
}
|
||||
let bytes = [
|
||||
self.rom[self.pc],
|
||||
self.rom[self.pc + 1],
|
||||
];
|
||||
self.pc += 2;
|
||||
Ok(u16::from_le_bytes(bytes))
|
||||
}
|
||||
|
||||
pub fn push(&mut self, val: Value) {
|
||||
self.operand_stack.push(val);
|
||||
}
|
||||
|
||||
pub fn pop(&mut self) -> Result<Value, String> {
|
||||
self.operand_stack.pop().ok_or("Stack underflow".into())
|
||||
}
|
||||
|
||||
pub fn pop_number(&mut self) -> Result<f64, String> {
|
||||
let val = self.pop()?;
|
||||
val.as_float().ok_or_else(|| "Expected number".into())
|
||||
}
|
||||
|
||||
pub fn pop_integer(&mut self) -> Result<i64, String> {
|
||||
let val = self.pop()?;
|
||||
val.as_integer().ok_or_else(|| "Expected integer".into())
|
||||
}
|
||||
|
||||
pub fn peek(&self) -> Result<&Value, String> {
|
||||
self.operand_stack.last().ok_or("Stack underflow".into())
|
||||
}
|
||||
|
||||
fn binary_op<F>(&mut self, f: F) -> Result<(), String>
|
||||
where
|
||||
F: FnOnce(Value, Value) -> Result<Value, String>,
|
||||
{
|
||||
let b = self.pop()?;
|
||||
let a = self.pop()?;
|
||||
let res = f(a, b)?;
|
||||
self.push(res);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_add_instructions() {
|
||||
struct NoopNative;
|
||||
impl NativeInterface for NoopNative {
|
||||
fn call(&mut self, _id: u32, _vm: &mut VirtualMachine) -> Result<u64, String> { Ok(0) }
|
||||
}
|
||||
let mut native = NoopNative;
|
||||
|
||||
let mut rom = Vec::new();
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes()); // Const index 0
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&1u32.to_le_bytes()); // Const index 1
|
||||
rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
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 call(&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)
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// Index 6: DUP
|
||||
let loop_start = rom.len();
|
||||
rom.extend_from_slice(&(OpCode::Dup as u16).to_le_bytes());
|
||||
|
||||
// PUSH 10
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&1u32.to_le_bytes());
|
||||
|
||||
// LT (counter < 10)
|
||||
rom.extend_from_slice(&(OpCode::Lt as u16).to_le_bytes());
|
||||
|
||||
// JMP_IF_FALSE to end
|
||||
rom.extend_from_slice(&(OpCode::JmpIfFalse as u16).to_le_bytes());
|
||||
let jmp_placeholder = rom.len();
|
||||
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||
|
||||
// PUSH 1
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&2u32.to_le_bytes());
|
||||
|
||||
// ADD (increment counter)
|
||||
rom.extend_from_slice(&(OpCode::Add as u16).to_le_bytes());
|
||||
|
||||
// JMP to start
|
||||
rom.extend_from_slice(&(OpCode::Jmp as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&(loop_start as u32).to_le_bytes());
|
||||
|
||||
// End
|
||||
let loop_end = rom.len();
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
// 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 call(&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)
|
||||
rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0u32.to_le_bytes());
|
||||
// CALL_NATIVE 0x1001
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x1001u32.to_le_bytes());
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
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::Machine;
|
||||
use crate::model::Cartridge;
|
||||
|
||||
let mut machine = Machine::new();
|
||||
|
||||
// 1. Verifica que não tem cartucho inicialmente
|
||||
let mut rom = Vec::new();
|
||||
// CALL_NATIVE 0x0001 (has_cart)
|
||||
rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
rom.extend_from_slice(&0x0001u32.to_le_bytes());
|
||||
rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
|
||||
machine.vm = VirtualMachine::new(rom, vec![]);
|
||||
let mut vm = std::mem::take(&mut machine.vm);
|
||||
vm.run_budget(100, &mut machine).unwrap();
|
||||
machine.vm = vm;
|
||||
|
||||
assert_eq!(machine.vm.pop().unwrap(), Value::Boolean(false));
|
||||
|
||||
// 2. Adiciona um cartucho e roda
|
||||
let mut cart_rom = Vec::new();
|
||||
// PUSH_CONST 0
|
||||
cart_rom.extend_from_slice(&(OpCode::PushConst as u16).to_le_bytes());
|
||||
cart_rom.extend_from_slice(&0u32.to_le_bytes());
|
||||
cart_rom.extend_from_slice(&(OpCode::Halt as u16).to_le_bytes());
|
||||
let cart_pool = vec![Value::Integer(42)];
|
||||
|
||||
let cart = Cartridge::new(cart_rom, cart_pool);
|
||||
machine.load_cartridge(cart);
|
||||
|
||||
// Código para rodar o cartucho
|
||||
let mut boot_rom = Vec::new();
|
||||
// CALL_NATIVE 0x0002 (run_cart)
|
||||
boot_rom.extend_from_slice(&(OpCode::Syscall as u16).to_le_bytes());
|
||||
boot_rom.extend_from_slice(&0x0002u32.to_le_bytes());
|
||||
|
||||
machine.vm = VirtualMachine::new(boot_rom, vec![]);
|
||||
let mut vm = std::mem::take(&mut machine.vm);
|
||||
vm.run_budget(1000, &mut machine).unwrap();
|
||||
machine.vm = vm;
|
||||
|
||||
// Após o run_budget, a VM deve ter executado o cartucho
|
||||
assert_eq!(machine.vm.operand_stack.last(), Some(&Value::Integer(42)));
|
||||
assert!(machine.vm.halted);
|
||||
}
|
||||
}
|
||||
@ -4,7 +4,7 @@
|
||||
|
||||
## 1. Visão Geral
|
||||
|
||||
A **PROMETEU VM** é uma máquina virtual:
|
||||
A **PROMETEU VM** é uma máquina virtual obrigatória e sempre presente no hardware lógico:
|
||||
|
||||
* **stack-based**
|
||||
* determinística
|
||||
@ -58,13 +58,14 @@ Propriedades:
|
||||
|
||||
## 3. Tipos Fundamentais
|
||||
|
||||
| Tipo | Descrição |
|
||||
| --------- | ---------------------- |
|
||||
| `number` | ponto flutuante 64-bit |
|
||||
| `boolean` | verdadeiro/falso |
|
||||
| `string` | UTF-8 imutável |
|
||||
| `null` | ausência de valor |
|
||||
| `ref` | referência ao heap |
|
||||
| Tipo | Descrição |
|
||||
| --------- | ----------------------- |
|
||||
| `integer` | inteiro 64-bit (signed) |
|
||||
| `float` | ponto flutuante 64-bit |
|
||||
| `boolean` | verdadeiro/falso |
|
||||
| `string` | UTF-8 imutável |
|
||||
| `null` | ausência de valor |
|
||||
| `ref` | referência ao heap |
|
||||
|
||||
Não existem:
|
||||
|
||||
@ -200,19 +201,26 @@ Heap é:
|
||||
|
||||
### 6.8 Periféricos (Syscalls)
|
||||
|
||||
| Instrução | Ciclos | Descrição |
|
||||
| ---------------- | -------- | ------------------- |
|
||||
| `CALL_NATIVE id` | variável | Chamada ao hardware |
|
||||
| Instrução | Ciclos | Descrição |
|
||||
|--------------| -------- | ------------------- |
|
||||
| `SYSCALL id` | variável | Chamada ao hardware |
|
||||
|
||||
Exemplos:
|
||||
#### Syscalls Implementadas (v0.1)
|
||||
|
||||
* `gfx.clear`
|
||||
* `gfx.fillRect`
|
||||
* `gfx.present`
|
||||
* `input.get`
|
||||
* `audio.play`
|
||||
| ID | Nome | Argumentos (Pilha) | Retorno |
|
||||
| ---------- | ----------------- | ---------------------------- | ------- |
|
||||
| `0x0001` | `system.has_cart` | - | `bool` |
|
||||
| `0x0002` | `system.run_cart` | - | - |
|
||||
| `0x1001` | `gfx.clear` | `color_idx` | - |
|
||||
| `0x1002` | `gfx.draw_rect` | `x, y, w, h, color_idx` | - |
|
||||
| `0x2001` | `input.get_pad` | `button_id` | `bool` |
|
||||
| `0x3001` | `audio.play` | `s_id, v_id, vol, pan, pitch`| - |
|
||||
|
||||
O custo depende do periférico e entra no CAP.
|
||||
**IDs de Botões:**
|
||||
- `0`: Up, `1`: Down, `2`: Left, `3`: Right
|
||||
- `4`: A, `5`: B, `6`: X, `7`: Y
|
||||
- `8`: L, `9`: R
|
||||
- `10`: Start, `11`: Select
|
||||
|
||||
---
|
||||
|
||||
@ -361,7 +369,26 @@ Efeito prático:
|
||||
|
||||
---
|
||||
|
||||
## 13. Boot e Cartucho
|
||||
## 13. Boot ROM e Estado Inicial
|
||||
|
||||
A **PROMETEU VM** nunca inicia em um estado totalmente "vazio" ou inativo.
|
||||
|
||||
### 13.1 O Boot ROM
|
||||
Se a máquina for inicializada sem um cartucho específico carregado, a VM executa um **Boot ROM padrão**. Este é um pequeno programa em bytecode embutido no core que:
|
||||
* Emite um som de "plim" ao iniciar.
|
||||
* Realiza um ciclo básico de limpeza de tela (preto).
|
||||
* Exibe um quadrado centralizado (índigo).
|
||||
* Serve como indicador visual e auditivo de que o hardware lógico está operacional.
|
||||
|
||||
### 13.2 Ciclo de Boot
|
||||
1. O Core é inicializado via `Machine::new()`.
|
||||
2. A VM é carregada com o Boot ROM via `VirtualMachine::default()`.
|
||||
3. O PC (Program Counter) é definido como `0`.
|
||||
4. A execução começa imediatamente no primeiro `step_frame`.
|
||||
|
||||
---
|
||||
|
||||
## 14. Boot e Cartucho
|
||||
|
||||
O core:
|
||||
|
||||
@ -379,7 +406,7 @@ O cartucho:
|
||||
|
||||
---
|
||||
|
||||
## 13. Extensibilidade
|
||||
## 15. Extensibilidade
|
||||
|
||||
O Instruction Set é versionado.
|
||||
|
||||
@ -394,7 +421,7 @@ Nenhuma instrução existente muda de significado.
|
||||
|
||||
---
|
||||
|
||||
## 14. Resumo
|
||||
## 16. Resumo
|
||||
|
||||
* VM stack-based
|
||||
* custo explícito
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user