2024-01-27 17:24:59 -06:00
|
|
|
mod audio;
|
2023-08-10 13:58:34 -05:00
|
|
|
mod card;
|
2023-10-12 13:11:38 -05:00
|
|
|
mod cpu;
|
2024-01-24 14:31:01 -06:00
|
|
|
mod frontpanel;
|
2023-08-10 13:58:34 -05:00
|
|
|
mod ram;
|
|
|
|
|
2024-01-27 17:24:59 -06:00
|
|
|
use std::{path::Path, sync::mpsc::Sender};
|
2023-08-10 13:58:34 -05:00
|
|
|
|
2024-01-27 17:24:59 -06:00
|
|
|
use audio::{AudioMessage, AudioThread};
|
2023-08-10 13:58:34 -05:00
|
|
|
use cpu::{MemCycle, Status, I8080};
|
2024-01-25 10:34:10 -06:00
|
|
|
use device_query::DeviceState;
|
2024-01-24 14:31:01 -06:00
|
|
|
use eframe::{
|
2024-01-29 11:02:37 -06:00
|
|
|
egui::{self, menu, Button, Label, TextureHandle, TextureOptions},
|
2024-01-24 14:31:01 -06:00
|
|
|
NativeOptions,
|
|
|
|
};
|
2023-08-10 13:58:34 -05:00
|
|
|
use egui_modal::Modal;
|
|
|
|
use rand::RngCore;
|
2024-01-23 17:10:03 -06:00
|
|
|
use rfd::FileDialog;
|
2023-08-10 13:58:34 -05:00
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
|
2024-01-29 11:02:37 -06:00
|
|
|
use crate::frontpanel::{switch::SwitchState, Frontpanel, FrontpanelInteraction, FrontpanelState};
|
2024-01-24 14:31:01 -06:00
|
|
|
|
2023-08-10 13:58:34 -05:00
|
|
|
fn main() -> Result<(), eframe::Error> {
|
|
|
|
env_logger::init();
|
2024-01-27 17:24:59 -06:00
|
|
|
let audio_tx = AudioThread::init();
|
2023-08-10 13:58:34 -05:00
|
|
|
eframe::run_native(
|
|
|
|
"Altair 8800 Emulator",
|
2024-01-24 10:50:35 -06:00
|
|
|
NativeOptions::default(),
|
2024-01-27 17:24:59 -06:00
|
|
|
Box::new(|cc| Box::new(AltairEmulator::new(cc, audio_tx))),
|
2023-08-10 13:58:34 -05:00
|
|
|
)
|
|
|
|
}
|
|
|
|
|
|
|
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
|
|
|
struct Options {
|
|
|
|
fan_enabled: bool,
|
|
|
|
}
|
|
|
|
|
|
|
|
struct AltairEmulator {
|
2024-01-25 10:34:10 -06:00
|
|
|
textures: frontpanel::Textures,
|
2023-08-10 13:58:34 -05:00
|
|
|
mem: [u8; 65536],
|
|
|
|
cpu: I8080,
|
|
|
|
running: bool,
|
2024-01-27 17:24:59 -06:00
|
|
|
audio_tx: Sender<AudioMessage>,
|
2023-08-10 13:58:34 -05:00
|
|
|
options: Options,
|
|
|
|
option_window: Option<OptionWindow>,
|
|
|
|
device_state: DeviceState,
|
|
|
|
kbd_newdown: bool,
|
|
|
|
kbd_olddown: bool,
|
2024-01-29 11:02:37 -06:00
|
|
|
fp_state: FrontpanelState,
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
impl AltairEmulator {
|
2024-01-27 17:24:59 -06:00
|
|
|
fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self {
|
2023-08-10 13:58:34 -05:00
|
|
|
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
|
2023-10-12 13:11:38 -05:00
|
|
|
// 3: 107
|
2023-08-10 13:58:34 -05:00
|
|
|
// 4: 072 021 000
|
|
|
|
// 7: 200
|
|
|
|
// 10: 062 022 000
|
|
|
|
// 13: 166
|
|
|
|
// 20: 004
|
|
|
|
// 21: 005
|
|
|
|
Self {
|
2024-01-25 10:34:10 -06:00
|
|
|
textures: frontpanel::Textures::new(&cc.egui_ctx),
|
2023-08-10 13:58:34 -05:00
|
|
|
mem,
|
|
|
|
cpu,
|
|
|
|
running: false,
|
2024-01-27 17:24:59 -06:00
|
|
|
audio_tx,
|
2023-08-10 13:58:34 -05:00
|
|
|
options,
|
|
|
|
option_window: None,
|
|
|
|
device_state: DeviceState::new(),
|
|
|
|
kbd_newdown: false,
|
|
|
|
kbd_olddown: false,
|
2024-01-29 11:02:37 -06:00
|
|
|
fp_state: FrontpanelState::new(),
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn update_fp(&mut self) {
|
|
|
|
let cycle = self.cpu.get_mem_cycle();
|
2024-01-29 11:02:37 -06:00
|
|
|
self.fp_state.set_status(cycle.get_status());
|
2023-08-10 13:58:34 -05:00
|
|
|
match cycle {
|
|
|
|
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => {
|
2024-01-29 11:02:37 -06:00
|
|
|
self.fp_state.set_addr(a);
|
|
|
|
self.fp_state.set_data(self.mem[a as usize]);
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
MemCycle::Write(a, _) | MemCycle::StackWrite(a, _) | MemCycle::Out(a, _) => {
|
2024-01-29 11:02:37 -06:00
|
|
|
self.fp_state.set_addr(a);
|
|
|
|
self.fp_state.set_data(0xff);
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
2024-01-23 16:01:14 -06:00
|
|
|
MemCycle::In(a) => {
|
2024-01-29 11:02:37 -06:00
|
|
|
self.fp_state.set_addr(a);
|
|
|
|
self.fp_state.set_data(0);
|
2024-01-24 14:31:01 -06:00
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
MemCycle::Inta(_) => todo!(),
|
|
|
|
MemCycle::Hlta(_) => {
|
2024-01-29 11:02:37 -06:00
|
|
|
self.fp_state.set_addr(0xffff);
|
|
|
|
self.fp_state.set_data(0xff);
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
MemCycle::IntaHlt(_) => todo!(),
|
|
|
|
}
|
|
|
|
}
|
2024-01-29 11:02:37 -06:00
|
|
|
|
|
|
|
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();
|
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
impl eframe::App for AltairEmulator {
|
|
|
|
fn save(&mut self, storage: &mut dyn eframe::Storage) {
|
|
|
|
eframe::set_value(storage, "options", &self.options);
|
|
|
|
}
|
2024-01-29 11:02:37 -06:00
|
|
|
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
|
2023-08-10 13:58:34 -05:00
|
|
|
ctx.set_pixels_per_point(1.0);
|
2024-01-25 10:34:10 -06:00
|
|
|
// frame.set_window_size((800.0, 333.0).into());
|
2023-08-10 13:58:34 -05:00
|
|
|
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()
|
|
|
|
{
|
2024-01-24 10:50:35 -06:00
|
|
|
self.option_window = Some(OptionWindow::new(ctx, self.options));
|
|
|
|
ui.close_menu();
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
2024-01-23 17:10:03 -06:00
|
|
|
if ui.button("Load binary file").clicked() {
|
|
|
|
let ihex_exts = ["hex", "mcs", "int", "ihex", "ihe", "ihx"];
|
|
|
|
let file = FileDialog::new()
|
2024-01-24 14:31:01 -06:00
|
|
|
.add_filter(
|
|
|
|
"Binary files",
|
|
|
|
&["bin", "img", "hex", "mcs", "int", "ihex", "ihe", "ihx"],
|
|
|
|
)
|
2024-01-23 17:10:03 -06:00
|
|
|
.add_filter("All files", &["*"])
|
|
|
|
.pick_file();
|
|
|
|
if let Some(file) = file {
|
2024-01-24 14:31:01 -06:00
|
|
|
if file.extension().map_or(false, |ext| {
|
|
|
|
ihex_exts.contains(&ext.to_str().unwrap_or(""))
|
|
|
|
}) {
|
2024-01-23 17:10:03 -06:00
|
|
|
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;
|
|
|
|
}
|
2024-01-24 14:31:01 -06:00
|
|
|
}
|
2024-01-23 17:10:03 -06:00
|
|
|
ihex::Record::StartLinearAddress(_) => todo!(),
|
|
|
|
_ => unimplemented!(),
|
|
|
|
};
|
|
|
|
}
|
|
|
|
} else {
|
|
|
|
// Raw binary
|
|
|
|
todo!();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
2024-01-29 11:02:37 -06:00
|
|
|
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::ActionSwClicked(sw, state) => {
|
|
|
|
if self.fp_state.power() {
|
|
|
|
match sw {
|
|
|
|
frontpanel::ActionSwitch::RunStop => {
|
|
|
|
if state == SwitchState::Up {
|
|
|
|
self.running = false;
|
|
|
|
} else {
|
|
|
|
self.running = true;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
frontpanel::ActionSwitch::SingleStep => {
|
|
|
|
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 {
|
|
|
|
// 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 {
|
|
|
|
// 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();
|
|
|
|
}
|
2024-01-25 10:34:10 -06:00
|
|
|
// dbg!(ui.input(|input| input.pointer.latest_pos()));
|
|
|
|
// dbg!(ui.input(|input| input.pointer.latest_pos()));
|
2024-01-24 14:31:01 -06:00
|
|
|
// let (_, fp_rect) = ui.allocate_space((800.0, 333.0).into());
|
|
|
|
// image_topleft(fp_rect.left_top(), &self.textures.fp, ui);
|
|
|
|
// for led in &LEDS {
|
|
|
|
// let led_data = match led.source {
|
|
|
|
// LedSource::Protect => 0x0,
|
|
|
|
// LedSource::Iff => 0x0,
|
|
|
|
// LedSource::Run => 0x0,
|
|
|
|
// LedSource::CpuStatus => u16::from(self.fp_status.bits()),
|
|
|
|
// LedSource::Data => u16::from(self.fp_data),
|
|
|
|
// LedSource::Address => self.fp_address,
|
|
|
|
// };
|
|
|
|
// let led_on = (led_data & led.mask) > 0;
|
|
|
|
// let texture = if led_on {
|
|
|
|
// &self.textures.led_on
|
|
|
|
// } else {
|
|
|
|
// &self.textures.led_off
|
|
|
|
// };
|
|
|
|
// image_center(led.pos + fp_rect.left_top().to_vec2(), texture, ui);
|
|
|
|
// }
|
|
|
|
// let pointer_state = ctx.input(|inp| inp.pointer.clone());
|
|
|
|
// let interact_pos = pointer_state.interact_pos();
|
|
|
|
// let mut switch_clicked = false;
|
|
|
|
// fn bool_to_texture(state: bool, textures: &Textures) -> &TextureHandle {
|
|
|
|
// if state {
|
|
|
|
// &textures.sw_up
|
|
|
|
// } else {
|
|
|
|
// &textures.sw_down
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// fn state_to_texture(state: SwitchState, textures: &Textures) -> &TextureHandle {
|
|
|
|
// match state {
|
|
|
|
// SwitchState::Up => &textures.sw_up,
|
|
|
|
// SwitchState::Neut => &textures.sw_neut,
|
|
|
|
// SwitchState::Down => &textures.sw_down,
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// for (i, switch) in SWITCHES.iter().enumerate() {
|
|
|
|
// let pos = switch.pos + fp_rect.left_top().to_vec2();
|
|
|
|
// let texture = match i {
|
|
|
|
// 0 => bool_to_texture(!self.power, &self.textures),
|
|
|
|
// (1..=16) => {
|
|
|
|
// bool_to_texture((self.ad_sws & (1 << (16 - i))) > 0, &self.textures)
|
|
|
|
// }
|
|
|
|
// 17 => state_to_texture(self.runstop, &self.textures),
|
|
|
|
// 18 => state_to_texture(self.single_step, &self.textures),
|
|
|
|
// 19 => state_to_texture(self.exam, &self.textures),
|
|
|
|
// 20 => state_to_texture(self.dep, &self.textures),
|
|
|
|
// 21 => state_to_texture(self.reset, &self.textures),
|
|
|
|
// 22 => state_to_texture(self.prot, &self.textures),
|
|
|
|
// 23 => state_to_texture(self.aux1, &self.textures),
|
|
|
|
// 24 => state_to_texture(self.aux2, &self.textures),
|
|
|
|
// _ => unreachable!(),
|
|
|
|
// };
|
|
|
|
// image_center(pos, texture, ui);
|
|
|
|
// let interacted = interact_pos.map(|interact_pos| {
|
|
|
|
// Rect::from_center_size(interact_pos, (10.0, 24.0).into()).contains(pos)
|
|
|
|
// && !disable_fp_sws
|
|
|
|
// });
|
|
|
|
// if pointer_state.primary_clicked() && interacted.unwrap() {
|
|
|
|
// match i {
|
|
|
|
// 0 => {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// self.power = !self.power;
|
|
|
|
// if self.options.fan_enabled {
|
|
|
|
// self.fan_audio_tx.send(self.power).unwrap();
|
|
|
|
// }
|
|
|
|
// if self.power {
|
|
|
|
// self.cpu = I8080::new();
|
|
|
|
// self.update_fp();
|
|
|
|
// } else {
|
|
|
|
// self.running = false;
|
|
|
|
// self.fp_status = Status::empty();
|
|
|
|
// self.fp_data = 0;
|
|
|
|
// self.fp_address = 0;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// (1..=16) => {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// if (self.ad_sws & (1 << (16 - i))) > 0 {
|
|
|
|
// self.ad_sws &= !(1 << (16 - i));
|
|
|
|
// } else {
|
|
|
|
// self.ad_sws |= 1 << (16 - i);
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// _ => (),
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if pointer_state.primary_down() && interacted.unwrap() {
|
|
|
|
// if (17..=24).contains(&i) && self.mouse_newdown {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// }
|
|
|
|
// let newstate = if interact_pos.unwrap().y > pos.y {
|
|
|
|
// SwitchState::Down
|
|
|
|
// } else {
|
|
|
|
// SwitchState::Up
|
|
|
|
// };
|
|
|
|
// match i {
|
|
|
|
// 17 => self.runstop = newstate,
|
|
|
|
// 18 => self.single_step = newstate,
|
|
|
|
// 19 => self.exam = newstate,
|
|
|
|
// 20 => self.dep = newstate,
|
|
|
|
// 21 => self.reset = newstate,
|
|
|
|
// 22 => self.prot = newstate,
|
|
|
|
// 23 => self.aux1 = newstate,
|
|
|
|
// 24 => self.aux2 = newstate,
|
|
|
|
// _ => (),
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// match i {
|
|
|
|
// 17 => self.runstop = SwitchState::Neut,
|
|
|
|
// 18 => self.single_step = SwitchState::Neut,
|
|
|
|
// 19 => self.exam = SwitchState::Neut,
|
|
|
|
// 20 => self.dep = SwitchState::Neut,
|
|
|
|
// 21 => self.reset = SwitchState::Neut,
|
|
|
|
// 22 => self.prot = SwitchState::Neut,
|
|
|
|
// 23 => self.aux1 = SwitchState::Neut,
|
|
|
|
// 24 => self.aux2 = SwitchState::Neut,
|
|
|
|
// _ => (),
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if !disable_fp_sws {
|
|
|
|
// let mut kbd_ad = None;
|
|
|
|
// let pressed_keys = self.device_state.query_keymap();
|
|
|
|
// if pressed_keys
|
|
|
|
// .iter()
|
|
|
|
// .filter(|&&k| k != Keycode::LShift && k != Keycode::RShift)
|
|
|
|
// .count()
|
|
|
|
// != 0
|
|
|
|
// {
|
|
|
|
// if self.kbd_newdown {
|
|
|
|
// self.kbd_olddown = true;
|
|
|
|
// self.kbd_newdown = false;
|
|
|
|
// } else if !self.kbd_newdown && !self.kbd_olddown {
|
|
|
|
// self.kbd_newdown = true;
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// self.kbd_newdown = false;
|
|
|
|
// self.kbd_olddown = false;
|
|
|
|
// }
|
|
|
|
// if self.kbd_newdown {
|
|
|
|
// if pressed_keys.contains(&Keycode::W)
|
|
|
|
// || pressed_keys.contains(&Keycode::E)
|
|
|
|
// || pressed_keys.contains(&Keycode::R)
|
|
|
|
// || pressed_keys.contains(&Keycode::T)
|
|
|
|
// || pressed_keys.contains(&Keycode::Y)
|
|
|
|
// || pressed_keys.contains(&Keycode::U)
|
|
|
|
// || pressed_keys.contains(&Keycode::I)
|
|
|
|
// || pressed_keys.contains(&Keycode::O)
|
|
|
|
// || pressed_keys.contains(&Keycode::S)
|
|
|
|
// || pressed_keys.contains(&Keycode::D)
|
|
|
|
// || pressed_keys.contains(&Keycode::F)
|
|
|
|
// || pressed_keys.contains(&Keycode::G)
|
|
|
|
// || pressed_keys.contains(&Keycode::H)
|
|
|
|
// || pressed_keys.contains(&Keycode::J)
|
|
|
|
// || pressed_keys.contains(&Keycode::K)
|
|
|
|
// || pressed_keys.contains(&Keycode::L)
|
|
|
|
// {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::LShift)
|
|
|
|
// || pressed_keys.contains(&Keycode::RShift)
|
|
|
|
// {
|
|
|
|
// if pressed_keys.contains(&Keycode::Key1) {
|
|
|
|
// kbd_ad = Some(15);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key2) {
|
|
|
|
// kbd_ad = Some(14);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key3) {
|
|
|
|
// kbd_ad = Some(13);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key4) {
|
|
|
|
// kbd_ad = Some(12);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key5) {
|
|
|
|
// kbd_ad = Some(11);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key6) {
|
|
|
|
// kbd_ad = Some(10);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key7) {
|
|
|
|
// kbd_ad = Some(9);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key8) {
|
|
|
|
// kbd_ad = Some(8);
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// if pressed_keys.contains(&Keycode::Key1) {
|
|
|
|
// kbd_ad = Some(7);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key2) {
|
|
|
|
// kbd_ad = Some(6);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key3) {
|
|
|
|
// kbd_ad = Some(5);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key4) {
|
|
|
|
// kbd_ad = Some(4);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key5) {
|
|
|
|
// kbd_ad = Some(3);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key6) {
|
|
|
|
// kbd_ad = Some(2);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key7) {
|
|
|
|
// kbd_ad = Some(1);
|
|
|
|
// } else if pressed_keys.contains(&Keycode::Key8) {
|
|
|
|
// kbd_ad = Some(0);
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::Q) {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// self.power = !self.power;
|
|
|
|
// if self.options.fan_enabled {
|
|
|
|
// self.fan_audio_tx.send(self.power).unwrap();
|
|
|
|
// }
|
|
|
|
// if self.power {
|
|
|
|
// self.cpu = I8080::new();
|
|
|
|
// self.update_fp();
|
|
|
|
// } else {
|
|
|
|
// self.running = false;
|
|
|
|
// self.fp_status = Status::empty();
|
|
|
|
// self.fp_data = 0;
|
|
|
|
// self.fp_address = 0;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::W) {
|
|
|
|
// self.runstop = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::E) {
|
|
|
|
// self.single_step = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::R) {
|
|
|
|
// self.exam = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::T) {
|
|
|
|
// self.dep = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::Y) {
|
|
|
|
// self.reset = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::U) {
|
|
|
|
// self.prot = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::I) {
|
|
|
|
// self.aux1 = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::O) {
|
|
|
|
// self.aux2 = SwitchState::Up;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::S) {
|
|
|
|
// self.runstop = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::D) {
|
|
|
|
// self.single_step = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::F) {
|
|
|
|
// self.exam = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::G) {
|
|
|
|
// self.dep = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::H) {
|
|
|
|
// self.reset = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::J) {
|
|
|
|
// self.prot = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::K) {
|
|
|
|
// self.aux1 = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if pressed_keys.contains(&Keycode::L) {
|
|
|
|
// self.aux2 = SwitchState::Down;
|
|
|
|
// }
|
|
|
|
// if let Some(kbd_ad) = kbd_ad {
|
|
|
|
// switch_clicked = true;
|
|
|
|
// if (self.ad_sws & (1 << kbd_ad)) > 0 {
|
|
|
|
// self.ad_sws &= !(1 << kbd_ad);
|
|
|
|
// } else {
|
|
|
|
// self.ad_sws |= 1 << kbd_ad;
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if switch_clicked {
|
|
|
|
// self.main_audio_tx
|
|
|
|
// .send(AudioMessage::PlaySwitchClick)
|
|
|
|
// .unwrap();
|
|
|
|
// }
|
|
|
|
// if pointer_state.primary_down() {
|
|
|
|
// if self.mouse_newdown {
|
|
|
|
// self.mouse_olddown = true;
|
|
|
|
// self.mouse_newdown = false;
|
|
|
|
// } else if !self.mouse_newdown && !self.mouse_olddown {
|
|
|
|
// self.mouse_newdown = true;
|
|
|
|
// }
|
|
|
|
// } else {
|
|
|
|
// self.mouse_newdown = false;
|
|
|
|
// self.mouse_olddown = false;
|
|
|
|
// }
|
|
|
|
// if switch_clicked && self.power {
|
|
|
|
// if !self.running {
|
|
|
|
// if self.exam == SwitchState::Up {
|
|
|
|
// // Assume M1
|
|
|
|
// self.cpu.finish_m_cycle(0xC3); // JMP
|
|
|
|
// self.cpu.finish_m_cycle(self.ad_sws as u8);
|
|
|
|
// self.cpu.finish_m_cycle((self.ad_sws >> 8) as u8);
|
|
|
|
// self.update_fp();
|
|
|
|
// }
|
|
|
|
// if self.exam == SwitchState::Down {
|
|
|
|
// // Assume M1
|
|
|
|
// self.cpu.finish_m_cycle(0x0); // NOP
|
|
|
|
// self.update_fp();
|
|
|
|
// }
|
|
|
|
// if self.dep == SwitchState::Up {
|
|
|
|
// // Assume M1
|
|
|
|
// self.mem[self.cpu.get_mem_cycle().address() as usize] = self.ad_sws as u8;
|
|
|
|
// self.update_fp();
|
|
|
|
// }
|
|
|
|
// if self.dep == SwitchState::Down {
|
|
|
|
// // Assume M1
|
|
|
|
// self.cpu.finish_m_cycle(0x0); // NOP
|
|
|
|
// self.mem[self.cpu.get_mem_cycle().address() as usize] = self.ad_sws as u8;
|
|
|
|
// self.update_fp();
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if self.runstop == SwitchState::Up {
|
|
|
|
// self.running = false;
|
|
|
|
// }
|
|
|
|
// if self.runstop == SwitchState::Down {
|
|
|
|
// self.running = true;
|
|
|
|
// }
|
|
|
|
// if self.reset == SwitchState::Up {
|
|
|
|
// self.cpu.reset();
|
|
|
|
// self.update_fp();
|
|
|
|
// }
|
|
|
|
// }
|
|
|
|
// if ((switch_clicked && self.single_step != SwitchState::Neut) || self.running)
|
|
|
|
// && self.power
|
|
|
|
// {
|
|
|
|
// 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();
|
|
|
|
// }
|
2023-08-10 13:58:34 -05:00
|
|
|
});
|
2024-01-24 14:31:01 -06:00
|
|
|
|
2024-01-29 11:02:37 -06:00
|
|
|
let old_fan_enabled = self.options.fan_enabled;
|
2023-08-10 13:58:34 -05:00
|
|
|
if let Some(option_window) = self.option_window.as_mut() {
|
2024-01-24 10:50:35 -06:00
|
|
|
if option_window.draw(ctx, &mut self.options) {
|
2023-08-10 13:58:34 -05:00
|
|
|
self.option_window = None;
|
|
|
|
}
|
|
|
|
}
|
2024-01-29 11:02:37 -06:00
|
|
|
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();
|
|
|
|
}
|
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
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();
|
2023-10-12 13:11:38 -05:00
|
|
|
Self {
|
|
|
|
options,
|
|
|
|
category: OptionsCategory::General,
|
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
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");
|
2023-10-12 13:11:38 -05:00
|
|
|
}
|
2023-08-10 13:58:34 -05:00
|
|
|
OptionsCategory::Cards => {
|
|
|
|
ui.heading("TODO");
|
|
|
|
}
|
|
|
|
}
|
|
|
|
modal.buttons(ui, |ui| {
|
|
|
|
if ui.button("Apply").clicked() {
|
2024-01-24 10:50:35 -06:00
|
|
|
*options = self.options;
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
if modal.button(ui, "OK").clicked() {
|
2024-01-24 10:50:35 -06:00
|
|
|
*options = self.options;
|
2023-08-10 13:58:34 -05:00
|
|
|
}
|
|
|
|
modal.caution_button(ui, "Cancel");
|
|
|
|
});
|
|
|
|
});
|
|
|
|
!modal.is_open()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
struct Textures {
|
|
|
|
fp: TextureHandle,
|
|
|
|
sw_up: TextureHandle,
|
|
|
|
sw_neut: TextureHandle,
|
|
|
|
sw_down: TextureHandle,
|
|
|
|
led_off: TextureHandle,
|
|
|
|
led_on: TextureHandle,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl Textures {
|
|
|
|
fn new(ctx: &egui::Context) -> Self {
|
|
|
|
Self {
|
|
|
|
fp: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"fp",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/altair800.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
sw_up: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"sw_up",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/Togup.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
sw_neut: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"sw_neut",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/Togneut.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
sw_down: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"sw_down",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/Togdown.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
led_off: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"led_off",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/Led_off.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
led_on: Self::load_texture(
|
|
|
|
ctx,
|
|
|
|
"led_on",
|
|
|
|
"/home/pjht/projects/altair_emu/resources/Led_on.png",
|
|
|
|
)
|
|
|
|
.unwrap(),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
fn load_texture(
|
|
|
|
ctx: &egui::Context,
|
|
|
|
name: impl Into<String>,
|
|
|
|
path: impl AsRef<Path>,
|
|
|
|
) -> Result<TextureHandle, image::ImageError> {
|
|
|
|
let image = image::io::Reader::open(path.as_ref())?.decode()?;
|
|
|
|
let image = egui::ColorImage::from_rgba_unmultiplied(
|
|
|
|
[image.width() as _, image.height() as _],
|
|
|
|
image.to_rgba8().as_flat_samples().as_slice(),
|
|
|
|
);
|
|
|
|
Ok(ctx.load_texture(name, image, TextureOptions::LINEAR))
|
|
|
|
}
|
|
|
|
}
|