mod audio; mod card; mod cpu; mod frontpanel; mod ram; use std::sync::mpsc::Sender; use audio::{AudioMessage, AudioThread}; use cpu::{MemCycle, Status, I8080}; use eframe::{ egui::{self, menu, Button}, NativeOptions, }; use egui_modal::Modal; use frontpanel::Textures; use rand::RngCore; use rfd::FileDialog; use serde::{Deserialize, Serialize}; use crate::frontpanel::{switch::SwitchState, Frontpanel, FrontpanelInteraction, FrontpanelState}; fn main() -> Result<(), eframe::Error> { env_logger::init(); let audio_tx = AudioThread::init(); eframe::run_native( "Altair 8800 Emulator", NativeOptions::default(), Box::new(|cc| Box::new(AltairEmulator::new(cc, audio_tx))), ) } #[derive(Debug, Copy, Clone, Serialize, Deserialize)] struct Options { fan_enabled: bool, } struct AltairEmulator { textures: Textures, mem: [u8; 65536], cpu: I8080, running: bool, audio_tx: Sender, options: Options, option_window: Option, fp_state: FrontpanelState, } impl AltairEmulator { fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender) -> Self { let options = if cc.storage.unwrap().get_string("options").is_none() { Options { fan_enabled: true } } else { eframe::get_value(cc.storage.unwrap(), "options").unwrap() }; let mut mem = [0; 65536]; let cpu = I8080::new(); rand::thread_rng().fill_bytes(&mut mem); mem[0x0] = 0x3a; mem[0x1] = 0x10; mem[0x2] = 0x00; mem[0x3] = 0x47; mem[0x4] = 0x3a; mem[0x5] = 0x11; mem[0x6] = 0x00; mem[0x7] = 0x80; mem[0x8] = 0x32; mem[0x9] = 0x12; mem[0xa] = 0x00; mem[0xb] = 0x76; mem[0x10] = 0x4; mem[0x11] = 0x5; // 0: 072 020 000 // 3: 107 // 4: 072 021 000 // 7: 200 // 10: 062 022 000 // 13: 166 // 20: 004 // 21: 005 Self { textures: Textures::new(&cc.egui_ctx), mem, cpu, running: false, audio_tx, options, option_window: None, fp_state: FrontpanelState::new(), } } fn update_fp(&mut self) { let cycle = self.cpu.get_mem_cycle(); self.fp_state.set_status(cycle.get_status()); match cycle { MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => { self.fp_state.set_addr(a); self.fp_state.set_data(self.mem[a as usize]); } MemCycle::Write(a, _) | MemCycle::StackWrite(a, _) | MemCycle::Out(a, _) => { self.fp_state.set_addr(a); self.fp_state.set_data(0xff); } MemCycle::In(a) => { self.fp_state.set_addr(a); self.fp_state.set_data(0); } MemCycle::Inta(_) => todo!(), MemCycle::Hlta(_) => { self.fp_state.set_addr(0xffff); self.fp_state.set_data(0xff); } MemCycle::IntaHlt(_) => todo!(), } } fn run_cpu_cycle(&mut self) { let cycle = self.cpu.get_mem_cycle(); let data = match cycle { MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => self.mem[a as usize], MemCycle::Write(a, d) | MemCycle::StackWrite(a, d) => { self.mem[a as usize] = d; 0 } MemCycle::In(_) => 0, MemCycle::Out(_, _) => 0, MemCycle::Inta(_) => todo!(), MemCycle::Hlta(_) => { self.running = false; 0 } MemCycle::IntaHlt(_) => todo!(), }; self.cpu.finish_m_cycle(data); self.update_fp(); } } impl eframe::App for AltairEmulator { fn save(&mut self, storage: &mut dyn eframe::Storage) { eframe::set_value(storage, "options", &self.options); } fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { ctx.set_pixels_per_point(1.0); // frame.set_window_size((800.0, 333.0).into()); let mut disable_fp_sws = self.option_window.is_some(); egui::TopBottomPanel::top("menu").show(ctx, |ui| { menu::bar(ui, |ui| { menu::menu_button(ui, "Edit", |ui| { disable_fp_sws = true; if ui .add_enabled(self.option_window.is_none(), Button::new("Options")) .clicked() { self.option_window = Some(OptionWindow::new(ctx, self.options)); ui.close_menu(); } if ui.button("Load binary file").clicked() { let ihex_exts = ["hex", "mcs", "int", "ihex", "ihe", "ihx"]; let file = FileDialog::new() .add_filter( "Binary files", &["bin", "img", "hex", "mcs", "int", "ihex", "ihe", "ihx"], ) .add_filter("All files", &["*"]) .pick_file(); if let Some(file) = file { if file.extension().map_or(false, |ext| { ihex_exts.contains(&ext.to_str().unwrap_or("")) }) { let data = std::fs::read_to_string(file).unwrap(); for record in ihex::Reader::new(&data) { let record = record.unwrap(); match record { ihex::Record::Data { offset, value } => { for (i, &byte) in value.iter().enumerate() { self.mem[offset as usize + i] = byte; } } ihex::Record::StartLinearAddress(_) => todo!(), _ => unimplemented!(), }; } } else { // Raw binary todo!(); } } } }); }); }); egui::CentralPanel::default().show(ctx, |ui| { let mut fp = Frontpanel::new(&self.textures, &mut self.fp_state); ui.add(&mut fp); let interaction = fp.interaction(); if let Some(interaction) = interaction { self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap(); match interaction { FrontpanelInteraction::ActionSwChanged(sw, state) => { if self.fp_state.power() { match sw { frontpanel::ActionSwitch::RunStop => { if state == SwitchState::Up { println!("STOP RUN"); self.running = false; } else if state == SwitchState::Down { println!("START RUN"); self.running = true; } } frontpanel::ActionSwitch::SingleStep => { if state == SwitchState::Up { self.run_cpu_cycle(); } } frontpanel::ActionSwitch::Examine => { if state == SwitchState::Up { // Assume M1 self.cpu.finish_m_cycle(0xC3); // JMP self.cpu.finish_m_cycle(self.fp_state.ad_sws() as u8); self.cpu .finish_m_cycle((self.fp_state.ad_sws() >> 8) as u8); self.update_fp(); } else if state == SwitchState::Down { // Assume M1 self.cpu.finish_m_cycle(0x0); // NOP self.update_fp(); } } frontpanel::ActionSwitch::Deposit => { if state == SwitchState::Up { // Assume M1 self.mem[self.cpu.get_mem_cycle().address() as usize] = self.fp_state.ad_sws() as u8; self.update_fp(); } else if state == SwitchState::Down { // Assume M1 self.cpu.finish_m_cycle(0x0); // NOP self.mem[self.cpu.get_mem_cycle().address() as usize] = self.fp_state.ad_sws() as u8; self.update_fp(); } } frontpanel::ActionSwitch::Reset => { if state == SwitchState::Up { self.cpu.reset(); self.update_fp(); } } frontpanel::ActionSwitch::Protect => (), frontpanel::ActionSwitch::Aux1 => (), frontpanel::ActionSwitch::Aux2 => (), } } } FrontpanelInteraction::AdChanged(_) => (), FrontpanelInteraction::PowerChanged(pwr) => { if pwr { self.audio_tx.send(AudioMessage::FanOn).unwrap(); self.cpu = I8080::new(); self.update_fp(); } else { self.audio_tx.send(AudioMessage::FanOff).unwrap(); self.running = false; self.fp_state.set_status(Status::empty()); self.fp_state.set_addr(0); self.fp_state.set_data(0); } } } } if self.running { self.run_cpu_cycle(); } }); let old_fan_enabled = self.options.fan_enabled; if let Some(option_window) = self.option_window.as_mut() { if option_window.draw(ctx, &mut self.options) { self.option_window = None; } } if (old_fan_enabled != self.options.fan_enabled) && self.fp_state.power() { if self.options.fan_enabled { self.audio_tx.send(AudioMessage::FanOn).unwrap(); } else { self.audio_tx.send(AudioMessage::FanOff).unwrap(); } } if self.running { ctx.request_repaint(); } } } struct OptionWindow { options: Options, category: OptionsCategory, } #[derive(PartialEq, Eq)] enum OptionsCategory { General, Cards, } impl OptionWindow { fn new(ctx: &egui::Context, options: Options) -> Self { Modal::new(ctx, "options_modal").open(); Self { options, category: OptionsCategory::General, } } fn draw(&mut self, ctx: &egui::Context, options: &mut Options) -> bool { let modal = Modal::new(ctx, "options_modal"); modal.show(|ui| { modal.title(ui, "Options"); ui.horizontal(|ui| { ui.selectable_value(&mut self.category, OptionsCategory::General, "General"); ui.selectable_value(&mut self.category, OptionsCategory::Cards, "Cards"); }); match self.category { OptionsCategory::General => { ui.checkbox(&mut self.options.fan_enabled, "Fan enabled"); } OptionsCategory::Cards => { ui.heading("TODO"); } } modal.buttons(ui, |ui| { if ui.button("Apply").clicked() { *options = self.options; } if modal.button(ui, "OK").clicked() { *options = self.options; } modal.caution_button(ui, "Cancel"); }); }); !modal.is_open() } }