1076 lines
37 KiB
Rust
1076 lines
37 KiB
Rust
mod cpu;
|
|
mod card;
|
|
mod ram;
|
|
|
|
use std::{
|
|
path::Path,
|
|
sync::{mpsc::Sender, Arc},
|
|
thread,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use cpu::{MemCycle, Status, I8080};
|
|
use device_query::{DeviceState, Keycode};
|
|
use eframe::egui::{self, menu, Button, Pos2, Rect, TextureHandle, TextureOptions, Ui};
|
|
use egui_modal::Modal;
|
|
use log::{debug, trace, warn};
|
|
use parking_lot::Mutex;
|
|
use rand::RngCore;
|
|
use serde::{Deserialize, Serialize};
|
|
use soloud::{audio, AudioExt, LoadExt, Soloud};
|
|
|
|
#[derive(Clone, Debug)]
|
|
enum AudioMessage {
|
|
PlaySwitchClick,
|
|
}
|
|
|
|
fn main() -> Result<(), eframe::Error> {
|
|
env_logger::init();
|
|
let (main_audio_tx, main_audio_rx) = std::sync::mpsc::channel::<AudioMessage>();
|
|
let (fan_audio_tx, fan_audio_rx) = std::sync::mpsc::channel::<bool>();
|
|
let sl = Arc::new(Mutex::new(Soloud::default().unwrap()));
|
|
let fan_sl = Arc::clone(&sl);
|
|
thread::spawn(move || {
|
|
let sl = fan_sl;
|
|
let rx = fan_audio_rx;
|
|
let fan_startup = {
|
|
let mut fan_startup = audio::Wav::default();
|
|
fan_startup
|
|
.load("/home/pjht/projects/altair_emu/resources/fan1.wav")
|
|
.unwrap();
|
|
fan_startup
|
|
};
|
|
let fan_loop = {
|
|
let mut fan_loop = audio::Wav::default();
|
|
fan_loop
|
|
.load("/home/pjht/projects/altair_emu/resources/fan2.wav")
|
|
.unwrap();
|
|
fan_loop
|
|
};
|
|
let fan_shutdown = {
|
|
let mut fan_shutdown = audio::Wav::default();
|
|
fan_shutdown
|
|
.load("/home/pjht/projects/altair_emu/resources/fan3.wav")
|
|
.unwrap();
|
|
fan_shutdown
|
|
};
|
|
debug!(target: "altair_emu::fan_thread", "Fan startup length: {}s. Fan shutdown length: {}s", fan_startup.length(), fan_shutdown.length());
|
|
if fan_startup.length() != fan_shutdown.length() {
|
|
warn!(target: "altair_emu::fan_thread", "Fan startup sound and shutdown sounds do not have the same length! ({}s and {}s, respectively.) This may cause audio glitches.", fan_startup.length(), fan_shutdown.length())
|
|
}
|
|
let mut fan_shutdown_start: Option<(Instant, Duration)> = None;
|
|
for msg in rx.iter() {
|
|
if msg {
|
|
trace!(target: "altair_emu::fan_thread", "Start fan");
|
|
let fan_startup_start = if let Some(fan_shutdown_start) = fan_shutdown_start.take()
|
|
{
|
|
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound playing");
|
|
let sl = sl.lock();
|
|
sl.stop_all();
|
|
let fan_shutdown_elapsed = fan_shutdown_start.0.elapsed().as_secs_f64()
|
|
+ fan_shutdown_start.1.as_secs_f64();
|
|
if fan_shutdown_elapsed >= fan_shutdown.length() {
|
|
trace!(target: "altair_emu::fan_thread", "Shutdown elapsed > shutdown length, not actually playing");
|
|
let fan_startup_start = (Instant::now(), Duration::ZERO);
|
|
sl.play(&fan_startup);
|
|
fan_startup_start
|
|
} else {
|
|
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound stopped, elapsed time {}s", fan_shutdown_elapsed);
|
|
let fan_startup_start = (
|
|
Instant::now(),
|
|
Duration::from_secs_f64(fan_shutdown.length() - fan_shutdown_elapsed),
|
|
);
|
|
let fan_startup_handle = sl.play(&fan_startup);
|
|
sl.seek(
|
|
fan_startup_handle,
|
|
fan_shutdown.length() - fan_shutdown_elapsed,
|
|
)
|
|
.unwrap();
|
|
trace!(target: "altair_emu::fan_thread", "Fan startup playing at offset {}s", fan_shutdown.length() - fan_shutdown_elapsed);
|
|
fan_startup_start
|
|
}
|
|
} else {
|
|
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound not playing");
|
|
let fan_startup_start = (Instant::now(), Duration::ZERO);
|
|
sl.lock().play(&fan_startup);
|
|
fan_startup_start
|
|
};
|
|
let mut msg = None;
|
|
while sl.lock().voice_count() > 0 {
|
|
if let Ok(r_msg) = rx.try_recv() {
|
|
trace!(target: "altair_emu::fan_thread", "Got message {} while waiting for end of startup sound", r_msg);
|
|
msg = Some(r_msg);
|
|
break;
|
|
}
|
|
std::thread::sleep(std::time::Duration::from_millis(10));
|
|
}
|
|
if let Some(msg) = msg {
|
|
if msg {
|
|
panic!();
|
|
} else {
|
|
trace!(target: "altair_emu::fan_thread", "Stop fan in startup");
|
|
let sl = sl.lock();
|
|
sl.stop_all();
|
|
let fan_startup_elapsed = fan_startup_start.0.elapsed().as_secs_f64()
|
|
+ fan_startup_start.1.as_secs_f64();
|
|
trace!(target: "altair_emu::fan_thread", "Fan startup sound stopped, elapsed time {}s", fan_startup_elapsed);
|
|
fan_shutdown_start = Some((
|
|
Instant::now(),
|
|
Duration::from_secs_f64(fan_startup.length() - fan_startup_elapsed),
|
|
));
|
|
let fan_shutdown_handle = sl.play(&fan_shutdown);
|
|
sl.seek(
|
|
fan_shutdown_handle,
|
|
fan_startup.length() - fan_startup_elapsed,
|
|
)
|
|
.unwrap();
|
|
trace!(target: "altair_emu::fan_thread", "Fan shutdown playing at offset {}s", fan_startup.length() - fan_startup_elapsed);
|
|
}
|
|
continue;
|
|
}
|
|
let mut sl = sl.lock();
|
|
let handle = sl.play(&fan_loop);
|
|
sl.set_looping(handle, true);
|
|
trace!(target: "altair_emu::fan_thread", "Fan loop started, start done");
|
|
} else {
|
|
trace!(target: "altair_emu::fan_thread", "Stop fan");
|
|
sl.lock().stop_all();
|
|
trace!(target: "altair_emu::fan_thread", "Fan sound stopped");
|
|
fan_shutdown_start = Some((Instant::now(), Duration::ZERO));
|
|
sl.lock().play(&fan_shutdown);
|
|
trace!(target: "altair_emu::fan_thread", "Fan shutdown sound playing, stop done");
|
|
}
|
|
}
|
|
});
|
|
thread::spawn(move || {
|
|
let rx = main_audio_rx;
|
|
let head_step = {
|
|
let mut head_step = audio::Wav::default();
|
|
head_step
|
|
.load("/home/pjht/projects/altair_emu/resources/click2.wav")
|
|
.unwrap();
|
|
head_step
|
|
};
|
|
for msg in rx.iter() {
|
|
match msg {
|
|
AudioMessage::PlaySwitchClick => {
|
|
sl.lock().play(&head_step);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
eframe::run_native(
|
|
"Altair 8800 Emulator",
|
|
Default::default(),
|
|
Box::new(|cc| Box::new(AltairEmulator::new(cc, main_audio_tx, fan_audio_tx))),
|
|
)
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, Serialize, Deserialize)]
|
|
struct Options {
|
|
fan_enabled: bool,
|
|
}
|
|
|
|
struct AltairEmulator {
|
|
textures: Textures,
|
|
ad_sws: u16,
|
|
power: bool,
|
|
runstop: SwitchState,
|
|
single_step: SwitchState,
|
|
exam: SwitchState,
|
|
dep: SwitchState,
|
|
reset: SwitchState,
|
|
prot: SwitchState,
|
|
aux1: SwitchState,
|
|
aux2: SwitchState,
|
|
fp_address: u16,
|
|
fp_data: u8,
|
|
fp_status: Status,
|
|
mem: [u8; 65536],
|
|
cpu: I8080,
|
|
mouse_newdown: bool,
|
|
mouse_olddown: bool,
|
|
running: bool,
|
|
main_audio_tx: Sender<AudioMessage>,
|
|
fan_audio_tx: Sender<bool>,
|
|
options: Options,
|
|
option_window: Option<OptionWindow>,
|
|
device_state: DeviceState,
|
|
kbd_newdown: bool,
|
|
kbd_olddown: bool,
|
|
}
|
|
|
|
impl AltairEmulator {
|
|
fn new(
|
|
cc: &eframe::CreationContext<'_>,
|
|
main_audio_tx: Sender<AudioMessage>,
|
|
fan_audio_tx: Sender<bool>,
|
|
) -> 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),
|
|
ad_sws: 0x0,
|
|
power: false,
|
|
runstop: SwitchState::Neut,
|
|
single_step: SwitchState::Neut,
|
|
exam: SwitchState::Neut,
|
|
dep: SwitchState::Neut,
|
|
reset: SwitchState::Neut,
|
|
prot: SwitchState::Neut,
|
|
aux1: SwitchState::Neut,
|
|
aux2: SwitchState::Neut,
|
|
mem,
|
|
fp_address: 0,
|
|
fp_data: 0,
|
|
fp_status: Status::empty(),
|
|
cpu,
|
|
mouse_newdown: false,
|
|
mouse_olddown: false,
|
|
running: false,
|
|
main_audio_tx,
|
|
fan_audio_tx,
|
|
options,
|
|
option_window: None,
|
|
device_state: DeviceState::new(),
|
|
kbd_newdown: false,
|
|
kbd_olddown: false,
|
|
}
|
|
}
|
|
|
|
fn update_fp(&mut self) {
|
|
let cycle = self.cpu.get_mem_cycle();
|
|
self.fp_status = cycle.get_status();
|
|
match cycle {
|
|
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => {
|
|
self.fp_address = a;
|
|
self.fp_data = self.mem[a as usize];
|
|
}
|
|
MemCycle::Write(a, _) | MemCycle::StackWrite(a, _) | MemCycle::Out(a, _) => {
|
|
self.fp_address = a;
|
|
self.fp_data = 0xff;
|
|
}
|
|
MemCycle::In(_) => todo!(),
|
|
MemCycle::Inta(_) => todo!(),
|
|
MemCycle::Hlta(_) => {
|
|
self.fp_data = 0xff;
|
|
self.fp_address = 0xffff;
|
|
}
|
|
MemCycle::IntaHlt(_) => todo!(),
|
|
}
|
|
}
|
|
}
|
|
|
|
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) {
|
|
fn image_topleft(pos: impl Into<Pos2>, texture: &TextureHandle, ui: &mut Ui) {
|
|
ui.allocate_ui_at_rect(Rect::from_min_size(pos.into(), texture.size_vec2()), |ui| {
|
|
ui.image(texture, texture.size_vec2());
|
|
});
|
|
}
|
|
|
|
fn image_center(pos: impl Into<Pos2>, texture: &TextureHandle, ui: &mut Ui) {
|
|
ui.allocate_ui_at_rect(
|
|
Rect::from_center_size(pos.into(), texture.size_vec2()),
|
|
|ui| {
|
|
ui.image(texture, texture.size_vec2());
|
|
},
|
|
);
|
|
}
|
|
|
|
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.clone()));
|
|
ui.close_menu()
|
|
}
|
|
});
|
|
});
|
|
});
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
|
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 => self.fp_status.bits() as u16,
|
|
LedSource::Data => self.fp_data as u16,
|
|
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;
|
|
for (i, switch) in SWITCHES.iter().enumerate() {
|
|
let pos = switch.pos + fp_rect.left_top().to_vec2();
|
|
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,
|
|
}
|
|
}
|
|
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)
|
|
{
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
let newstate = if pressed_keys.contains(&Keycode::LShift)
|
|
|| pressed_keys.contains(&Keycode::RShift)
|
|
{
|
|
SwitchState::Down
|
|
} else {
|
|
SwitchState::Up
|
|
};
|
|
if pressed_keys.contains(&Keycode::W) {
|
|
self.runstop = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::E) {
|
|
self.single_step = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::R) {
|
|
self.exam = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::T) {
|
|
self.dep = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::Y) {
|
|
self.reset = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::U) {
|
|
self.prot = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::I) {
|
|
self.aux1 = newstate;
|
|
}
|
|
if pressed_keys.contains(&Keycode::O) {
|
|
self.aux2 = newstate;
|
|
}
|
|
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(_) => todo!(),
|
|
MemCycle::Out(_, _) => todo!(),
|
|
MemCycle::Inta(_) => todo!(),
|
|
MemCycle::Hlta(_) => {
|
|
self.running = false;
|
|
0
|
|
}
|
|
MemCycle::IntaHlt(_) => todo!(),
|
|
};
|
|
self.cpu.finish_m_cycle(data);
|
|
self.update_fp();
|
|
}
|
|
});
|
|
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.power {
|
|
self.fan_audio_tx.send(self.options.fan_enabled).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.clone();
|
|
}
|
|
if modal.button(ui, "OK").clicked() {
|
|
*options = self.options.clone();
|
|
}
|
|
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))
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
enum SwitchState {
|
|
Up,
|
|
Neut,
|
|
Down,
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct SwitchInfo {
|
|
pos: Pos2,
|
|
}
|
|
|
|
static SWITCHES: [SwitchInfo; 25] = [
|
|
SwitchInfo {
|
|
pos: Pos2::new(43.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(183.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(228.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(257.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(285.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(330.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(359.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(388.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(433.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(461.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(491.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(536.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(564.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(593.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(638.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(667.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(696.0, 175.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(190.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(248.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(305.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(363.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(420.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(478.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(535.0, 234.0),
|
|
},
|
|
SwitchInfo {
|
|
pos: Pos2::new(593.0, 234.0),
|
|
},
|
|
];
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum LedSource {
|
|
Protect,
|
|
Iff,
|
|
Run,
|
|
CpuStatus,
|
|
Data,
|
|
Address,
|
|
}
|
|
|
|
struct LedInfo {
|
|
pos: Pos2,
|
|
source: LedSource,
|
|
mask: u16,
|
|
}
|
|
|
|
static LEDS: [LedInfo; 36] = [
|
|
LedInfo {
|
|
pos: Pos2::new(118.0, 58.0),
|
|
source: LedSource::Protect,
|
|
mask: 0xFF,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(89.0, 58.0),
|
|
source: LedSource::Iff,
|
|
mask: 0xFF,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(89.0, 117.0),
|
|
source: LedSource::Run,
|
|
mask: 0x0,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(118.0, 117.0),
|
|
source: LedSource::Iff,
|
|
mask: 0x0,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(149.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x80,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(179.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x40,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(209.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x20,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(239.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x10,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(269.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x8,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(299.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x4,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(329.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x2,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(359.0, 58.0),
|
|
source: LedSource::CpuStatus,
|
|
mask: 0x1,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(461.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x80,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(491.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x40,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(536.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x20,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(564.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x10,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(593.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x8,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(638.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x4,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(667.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x2,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(696.0, 58.0),
|
|
source: LedSource::Data,
|
|
mask: 0x1,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(182.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x8000,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(227.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x4000,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(256.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x2000,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(285.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x1000,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(330.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x800,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(359.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x400,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(388.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x200,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(433.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x100,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(461.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x80,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(490.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x40,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(536.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x20,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(564.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x10,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(593.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x8,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(638.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x4,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(667.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x2,
|
|
},
|
|
LedInfo {
|
|
pos: Pos2::new(696.0, 117.0),
|
|
source: LedSource::Address,
|
|
mask: 0x1,
|
|
},
|
|
];
|