381 lines
14 KiB
Rust
381 lines
14 KiB
Rust
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, Slider},
|
|
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 {
|
|
#[serde(default = "Options::default_mute")]
|
|
mute: bool,
|
|
#[serde(default = "Options::default_fan_enabled")]
|
|
fan_enabled: bool,
|
|
#[serde(default = "Options::default_volume")]
|
|
volume: f32,
|
|
}
|
|
|
|
impl Options {
|
|
fn default_mute() -> bool {
|
|
false
|
|
}
|
|
fn default_fan_enabled() -> bool {
|
|
true
|
|
}
|
|
fn default_volume() -> f32 {
|
|
100.0
|
|
}
|
|
}
|
|
|
|
struct AltairEmulator {
|
|
textures: Textures,
|
|
mem: [u8; 65536],
|
|
cpu: I8080,
|
|
running: bool,
|
|
audio_tx: Sender<AudioMessage>,
|
|
options: Options,
|
|
option_window: Option<OptionWindow>,
|
|
fp_state: FrontpanelState,
|
|
}
|
|
|
|
impl AltairEmulator {
|
|
fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self {
|
|
let options = if cc.storage.unwrap().get_string("options").is_none() {
|
|
Options { mute: false, fan_enabled: true, volume: 1.0 }
|
|
} else {
|
|
eframe::get_value(cc.storage.unwrap(), "options").unwrap()
|
|
};
|
|
if options.mute {
|
|
audio_tx.send(AudioMessage::SetVolume(0.0)).unwrap();
|
|
} else {
|
|
audio_tx.send(AudioMessage::SetVolume(options.volume)).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 self.options.mute {
|
|
self.audio_tx.send(AudioMessage::SetVolume(0.0)).unwrap();
|
|
} else {
|
|
self.audio_tx.send(AudioMessage::SetVolume(self.options.volume)).unwrap();
|
|
}
|
|
}
|
|
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.mute, "Mute");
|
|
ui.checkbox(&mut self.options.fan_enabled, "Fan enabled");
|
|
ui.add(Slider::new(&mut self.options.volume, 0.0..=100.0).text("Volume"));
|
|
}
|
|
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()
|
|
}
|
|
}
|