#[cfg(feature = "uses_uxn")]
pub mod uxn {
use raven_uxn::{Backend, Uxn};
use raven_varvara::Varvara;
use std::path::Path;
use std::sync::{Arc, Mutex};
pub struct UxnModule {
pub uxn: Arc<Mutex<Uxn<'static>>>,
pub varvara: Option<Varvara>,
}
impl UxnModule {
pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
static mut RAM: [u8; 65536] = [0; 65536];
let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
let mut uxn = Uxn::new(ram, Backend::Interpreter);
let varvara = Varvara::default();
if let Some(path) = rom_path {
let rom = std::fs::read(path).map_err(|e| format!("Failed to read ROM: {e}"))?;
uxn.reset(&rom);
}
Ok(UxnModule {
uxn: Arc::new(Mutex::new(uxn)),
varvara: Some(varvara),
})
}
pub fn reset(&self, rom: &[u8]) {
let mut uxn = self.uxn.lock().unwrap();
uxn.reset(rom);
}
pub fn load_rom(&self, rom_path: &Path) -> Result<(), String> {
let rom = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM: {e}"))?;
self.reset(&rom);
Ok(())
}
}
}
#[cfg(not(feature = "uses_uxn"))]
pub mod uxn {
}
use log::{error, info};
use std::path::Path;
use std::sync::{Arc, Mutex};
use raven_uxn::{Backend, Uxn};
use raven_varvara::Key;
use raven_varvara::MouseState;
use raven_varvara::Varvara;
pub struct UxnModule {
pub uxn: Arc<Mutex<Uxn<'static>>>,
pub varvara: Option<Varvara>,
}
impl UxnModule {
pub fn new(rom_path: Option<&Path>) -> Result<Self, String> {
static mut RAM: [u8; 65536] = [0; 65536];
let ram: &'static mut [u8; 65536] = unsafe { &mut RAM };
let mut uxn = Uxn::new(ram, Backend::Interpreter);
let varvara = Varvara::default();
if let Some(path) = rom_path {
let rom = std::fs::read(path).map_err(|e| format!("Failed to read ROM: {e}"))?;
uxn.reset(&rom);
}
Ok(UxnModule {
uxn: Arc::new(Mutex::new(uxn)),
varvara: Some(varvara),
})
}
pub fn reset(&self, rom: &[u8]) {
let mut uxn = self.uxn.lock().unwrap();
uxn.reset(rom);
}
pub fn load_rom(&self, rom_path: &Path) -> Result<(), String> {
let rom = std::fs::read(rom_path).map_err(|e| format!("Failed to read ROM: {e}"))?;
self.reset(&rom);
Ok(())
}
}
#[cfg(feature = "egui")]
pub mod egui_ui {
use super::*;
use egui::{CollapsingHeader, Ui};
pub fn show_uxn_panel(ui: &mut Ui, _uxn_mod: &UxnModule) {
CollapsingHeader::new("Uxn VM State").show(ui, |ui| {
ui.label("Uxn state display not implemented (no UxnState in raven_uxn)");
});
}
}
use eframe::egui;
use std::sync::mpsc;
#[derive(Debug)]
pub enum Event {
LoadRom(Vec<u8>),
SetMuted(bool),
Console(u8),
}
pub struct UxnApp<'a> {
vm: Uxn<'a>,
dev: Varvara,
scale: f32,
size: (u16, u16),
next_frame: f64,
scroll: (f32, f32),
cursor_pos: Option<(f32, f32)>,
texture: egui::TextureHandle,
event_rx: mpsc::Receiver<Event>,
resized: Option<Box<dyn FnMut(u16, u16)>>,
window_mode: String,
aspect_ratio: f32,
auto_rom_select: bool,
auto_timer: f64,
auto_index: usize,
auto_roms: Vec<Vec<u8>>,
auto_rom_labels: Vec<String>,
on_rom_change: Option<Box<dyn Fn(&str) + Send + Sync>>,
current_rom_label: Option<String>,
}
impl<'a> UxnApp<'a> {
pub fn set_rom_label<S: Into<String>>(&mut self, label: S) {
self.current_rom_label = Some(label.into());
}
pub fn new_with_mode(
mut vm: Uxn<'a>,
mut dev: Varvara,
size: (u16, u16),
scale: f32,
event_rx: mpsc::Receiver<Event>,
ctx: &egui::Context,
window_mode: String,
auto_roms: Vec<Vec<u8>>,
auto_rom_labels: Vec<String>,
auto_rom_select: bool,
) -> Self {
vm.run(&mut dev, 0x100);
dev.redraw(&mut vm);
let w = usize::from(size.0);
let h = usize::from(size.1);
let image = egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
let texture = ctx.load_texture("frame", image, egui::TextureOptions::NEAREST);
let aspect_ratio = w as f32 / h as f32;
let mut auto_index = 0;
let mut auto_timer = 0.0;
let mut current_rom_label = None;
if auto_rom_select && !auto_roms.is_empty() {
println!(
"[AUTO ROM CYCLING] Enabled. Cycling through {} ROMs every 10s.",
auto_roms.len()
);
let rom = &auto_roms[0];
let _ = vm.reset(rom);
dev.reset(rom);
vm.run(&mut dev, 0x100);
dev.redraw(&mut vm);
auto_index = 0;
auto_timer = 0.0;
if !auto_rom_labels.is_empty() {
current_rom_label = Some(auto_rom_labels[0].clone());
} else {
current_rom_label = Some("ROM 1".to_string());
}
}
UxnApp {
vm,
dev,
scale,
size,
next_frame: 0.0,
event_rx,
resized: None,
scroll: (0.0, 0.0),
cursor_pos: None,
texture,
window_mode,
aspect_ratio,
auto_rom_select,
auto_timer,
auto_index,
auto_roms,
on_rom_change: None,
current_rom_label,
auto_rom_labels,
}
}
pub fn set_on_rom_change<F>(&mut self, f: F)
where
F: Fn(&str) + Send + Sync + 'static,
{
self.on_rom_change = Some(Box::new(f));
}
pub fn set_resize_callback(&mut self, f: Box<dyn FnMut(u16, u16)>) {
self.resized = Some(f);
}
fn load_rom(&mut self, data: &[u8]) -> anyhow::Result<()> {
let data = self.vm.reset(data);
self.dev.reset(data);
self.vm.run(&mut self.dev, 0x100);
let out = self.dev.output(&self.vm);
out.check()?;
let label = self.current_rom_label.as_deref().unwrap_or("[unknown ROM]");
if let Some(cb) = &self.on_rom_change {
cb(label);
}
Ok(())
}
}
impl eframe::App for UxnApp<'_> {
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
if self.auto_rom_select && !self.auto_roms.is_empty() {
let dt = ctx.input(|i| i.stable_dt) as f64;
log::debug!(
"[AUTO ROM CYCLING] auto_timer: {:.3}, dt: {:.3}, auto_index: {}, roms: {}",
self.auto_timer,
dt,
self.auto_index,
self.auto_roms.len()
);
self.auto_timer += dt;
if self.auto_timer == 0.0 && self.auto_index == 0 {
log::debug!("[AUTO ROM CYCLING] First ROM already loaded.");
} else if self.auto_timer > 10.0 {
log::debug!(
"[AUTO ROM CYCLING] Switching ROM: {} -> {}",
self.auto_index,
(self.auto_index + 1) % self.auto_roms.len()
);
self.auto_timer = 0.0;
self.auto_index = (self.auto_index + 1) % self.auto_roms.len();
let rom = self.auto_roms[self.auto_index].clone();
let label = if self.auto_index < self.auto_rom_labels.len() {
self.auto_rom_labels[self.auto_index].clone()
} else {
format!("ROM {}", self.auto_index + 1)
};
self.set_rom_label(label);
let _ = self.load_rom(&rom);
}
}
while let Ok(e) = self.event_rx.try_recv() {
match e {
Event::LoadRom(data) => {
if let Err(e) = self.load_rom(&data) {
error!("could not load rom: {e:?}");
}
}
Event::SetMuted(m) => {
self.dev.audio_set_muted(m);
}
Event::Console(b) => {
self.dev.console(&mut self.vm, b);
}
}
}
ctx.request_repaint();
ctx.input(|i| {
while i.time >= self.next_frame {
self.next_frame += 0.0166667;
self.dev.redraw(&mut self.vm);
}
if i.raw.dropped_files.len() == 1 {
let target = &i.raw.dropped_files[0];
let r = if let Some(path) = &target.path {
let data = std::fs::read(path).expect("failed to read file");
info!("loading {} bytes from {path:?}", data.len());
self.load_rom(&data)
} else if let Some(data) = &target.bytes {
self.load_rom(data)
} else {
Ok(())
};
if let Err(e) = r {
error!("could not load ROM: {e:?}");
}
}
let shift_held = i.modifiers.shift;
for e in i.events.iter() {
match e {
egui::Event::Text(s) => {
const RAW_CHARS: [u8; 16] = [
b'"', b'\'', b'{', b'}', b'_', b')', b'(', b'*', b'&', b'^', b'%',
b'$', b'#', b'@', b'!', b'~',
];
for c in s.bytes() {
if RAW_CHARS.contains(&c) {
self.dev.char(&mut self.vm, c);
}
}
}
egui::Event::Key {
key,
pressed,
repeat,
..
} => {
if let Some(k) = decode_key(*key, shift_held) {
if *pressed {
self.dev.pressed(&mut self.vm, k, *repeat);
} else {
self.dev.released(&mut self.vm, k);
}
}
}
_ => (),
}
}
for (b, k) in [
(i.modifiers.ctrl, Key::Ctrl),
(i.modifiers.alt, Key::Alt),
(i.modifiers.shift, Key::Shift),
] {
if b {
self.dev.pressed(&mut self.vm, k, false)
} else {
self.dev.released(&mut self.vm, k)
}
}
let ptr = &i.pointer;
if let Some(p) = ptr.latest_pos() {
self.cursor_pos = Some((p.x / self.scale, p.y / self.scale));
}
let buttons = [
egui::PointerButton::Primary,
egui::PointerButton::Middle,
egui::PointerButton::Secondary,
]
.into_iter()
.enumerate()
.map(|(i, b)| (ptr.button_down(b) as u8) << i)
.fold(0, |a, b| a | b);
let m = MouseState {
pos: self.cursor_pos.unwrap_or((0.0, 0.0)),
scroll: std::mem::take(&mut self.scroll),
buttons,
};
self.dev.mouse(&mut self.vm, m);
i.time
});
self.dev.audio(&mut self.vm);
let out = self.dev.output(&self.vm);
if out.hide_mouse {
ctx.set_cursor_icon(egui::CursorIcon::None);
}
if self.size != out.size {
let current_window_size = ctx.input(|i| {
i.viewport()
.inner_rect
.map_or(egui::Vec2::ZERO, |rect| rect.size())
});
let new_size = egui::Vec2::new(out.size.0 as f32, out.size.1 as f32) * self.scale;
if new_size.x > current_window_size.x || new_size.y > current_window_size.y {
info!("resizing window to {:?}", out.size);
self.size = out.size;
let mut size = new_size;
if self.window_mode == "proportional" {
let aspect = self.aspect_ratio;
let w = size.x;
let h = size.y;
let (new_w, new_h) = if w / h > aspect {
(h * aspect, h)
} else {
(w, w / aspect)
};
size = egui::Vec2::new(new_w, new_h);
}
ctx.send_viewport_cmd(egui::ViewportCommand::InnerSize(size));
if let Some(f) = self.resized.as_mut() {
f(out.size.0, out.size.1);
}
}
}
let w = out.size.0 as usize;
let h = out.size.1 as usize;
let mut image = egui::ColorImage::new([w, h], vec![egui::Color32::BLACK; w * h]);
for (i, o) in out.frame.chunks(4).zip(image.pixels.iter_mut()) {
*o = egui::Color32::from_rgba_unmultiplied(i[2], i[1], i[0], i[3]);
}
self.texture.set(image, egui::TextureOptions::NEAREST);
egui::CentralPanel::default()
.frame(egui::Frame::default().fill(egui::Color32::BLACK))
.show(ctx, |ui| {
let available = ui.available_size();
let frame_size = egui::Vec2::new(
out.size.0 as f32 * self.scale,
out.size.1 as f32 * self.scale,
);
let offset = egui::Vec2::new(
(available.x - frame_size.x) * 0.5,
(available.y - frame_size.y) * 0.5,
);
let top_left = ui.min_rect().min + offset;
let mut mesh = egui::Mesh::with_texture(self.texture.id());
mesh.add_rect_with_uv(
egui::Rect {
min: top_left,
max: top_left + frame_size,
},
egui::Rect {
min: egui::Pos2::new(0.0, 0.0),
max: egui::Pos2::new(1.0, 1.0),
},
egui::Color32::WHITE,
);
ui.painter().add(egui::Shape::mesh(mesh));
if self.auto_rom_select && !self.auto_roms.is_empty() {
ui.label(format!(
"[AUTO ROM CYCLING] Switching ROM every 10s. ROMs loaded: {}",
self.auto_roms.len()
));
}
});
out.check().expect("failed to print output?");
}
}
pub fn decode_key(k: egui::Key, shift: bool) -> Option<Key> {
let c = match (k, shift) {
(egui::Key::ArrowUp, _) => Key::Up,
(egui::Key::ArrowDown, _) => Key::Down,
(egui::Key::ArrowLeft, _) => Key::Left,
(egui::Key::ArrowRight, _) => Key::Right,
(egui::Key::Home, _) => Key::Home,
(egui::Key::Num0, false) => Key::Char(b'0'),
(egui::Key::Num0, true) => Key::Char(b')'),
(egui::Key::Num1, false) => Key::Char(b'1'),
(egui::Key::Num1, true) => Key::Char(b'!'),
(egui::Key::Num2, false) => Key::Char(b'2'),
(egui::Key::Num2, true) => Key::Char(b'@'),
(egui::Key::Num3, false) => Key::Char(b'3'),
(egui::Key::Num3, true) => Key::Char(b'#'),
(egui::Key::Num4, false) => Key::Char(b'4'),
(egui::Key::Num4, true) => Key::Char(b'$'),
(egui::Key::Num5, false) => Key::Char(b'5'),
(egui::Key::Num5, true) => Key::Char(b'5'),
(egui::Key::Num6, false) => Key::Char(b'6'),
(egui::Key::Num6, true) => Key::Char(b'^'),
(egui::Key::Num7, false) => Key::Char(b'7'),
(egui::Key::Num7, true) => Key::Char(b'&'),
(egui::Key::Num8, false) => Key::Char(b'8'),
(egui::Key::Num8, true) => Key::Char(b'*'),
(egui::Key::Num9, false) => Key::Char(b'9'),
(egui::Key::Num9, true) => Key::Char(b'('),
(egui::Key::A, false) => Key::Char(b'a'),
(egui::Key::A, true) => Key::Char(b'A'),
(egui::Key::B, false) => Key::Char(b'b'),
(egui::Key::B, true) => Key::Char(b'B'),
(egui::Key::C, false) => Key::Char(b'c'),
(egui::Key::C, true) => Key::Char(b'C'),
(egui::Key::D, false) => Key::Char(b'd'),
(egui::Key::D, true) => Key::Char(b'D'),
(egui::Key::E, false) => Key::Char(b'e'),
(egui::Key::E, true) => Key::Char(b'E'),
(egui::Key::F, false) => Key::Char(b'f'),
(egui::Key::F, true) => Key::Char(b'F'),
(egui::Key::G, false) => Key::Char(b'g'),
(egui::Key::G, true) => Key::Char(b'G'),
(egui::Key::H, false) => Key::Char(b'h'),
(egui::Key::H, true) => Key::Char(b'H'),
(egui::Key::I, false) => Key::Char(b'i'),
(egui::Key::I, true) => Key::Char(b'I'),
(egui::Key::J, false) => Key::Char(b'j'),
(egui::Key::J, true) => Key::Char(b'J'),
(egui::Key::K, false) => Key::Char(b'k'),
(egui::Key::K, true) => Key::Char(b'K'),
(egui::Key::L, false) => Key::Char(b'l'),
(egui::Key::L, true) => Key::Char(b'L'),
(egui::Key::M, false) => Key::Char(b'm'),
(egui::Key::M, true) => Key::Char(b'M'),
(egui::Key::N, false) => Key::Char(b'n'),
(egui::Key::N, true) => Key::Char(b'N'),
(egui::Key::O, false) => Key::Char(b'o'),
(egui::Key::O, true) => Key::Char(b'O'),
(egui::Key::P, false) => Key::Char(b'p'),
(egui::Key::P, true) => Key::Char(b'P'),
(egui::Key::Q, false) => Key::Char(b'q'),
(egui::Key::Q, true) => Key::Char(b'Q'),
(egui::Key::R, false) => Key::Char(b'r'),
(egui::Key::R, true) => Key::Char(b'R'),
(egui::Key::S, false) => Key::Char(b's'),
(egui::Key::S, true) => Key::Char(b'S'),
(egui::Key::T, false) => Key::Char(b't'),
(egui::Key::T, true) => Key::Char(b'T'),
(egui::Key::U, false) => Key::Char(b'u'),
(egui::Key::U, true) => Key::Char(b'U'),
(egui::Key::V, false) => Key::Char(b'v'),
(egui::Key::V, true) => Key::Char(b'V'),
(egui::Key::W, false) => Key::Char(b'w'),
(egui::Key::W, true) => Key::Char(b'W'),
(egui::Key::X, false) => Key::Char(b'x'),
(egui::Key::X, true) => Key::Char(b'X'),
(egui::Key::Y, false) => Key::Char(b'y'),
(egui::Key::Y, true) => Key::Char(b'Y'),
(egui::Key::Z, false) => Key::Char(b'z'),
(egui::Key::Z, true) => Key::Char(b'Z'),
(egui::Key::Backtick, false) => Key::Char(b'`'),
(egui::Key::Backtick, true) => Key::Char(b'~'),
(egui::Key::Backslash, _) => Key::Char(b'\\'),
(egui::Key::Pipe, _) => Key::Char(b'|'),
(egui::Key::Comma, false) => Key::Char(b','),
(egui::Key::Comma, true) => Key::Char(b'<'),
(egui::Key::Equals, _) => Key::Char(b'='),
(egui::Key::Plus, _) => Key::Char(b'+'),
(egui::Key::OpenBracket, false) => Key::Char(b'['),
(egui::Key::OpenBracket, true) => Key::Char(b'{'),
(egui::Key::Minus, false) => Key::Char(b'-'),
(egui::Key::Minus, true) => Key::Char(b'_'),
(egui::Key::Period, false) => Key::Char(b'.'),
(egui::Key::Period, true) => Key::Char(b'>'),
(egui::Key::CloseBracket, false) => Key::Char(b']'),
(egui::Key::CloseBracket, true) => Key::Char(b'}'),
(egui::Key::Semicolon, _) => Key::Char(b';'),
(egui::Key::Colon, _) => Key::Char(b':'),
(egui::Key::Slash, _) => Key::Char(b'/'),
(egui::Key::Questionmark, _) => Key::Char(b'?'),
(egui::Key::Space, _) => Key::Char(b' '),
(egui::Key::Tab, _) => Key::Char(b'\t'),
(egui::Key::Enter, _) => Key::Char(b'\r'),
(egui::Key::Backspace, _) => Key::Char(0x08),
_ => return None,
};
Some(c)
}