altair_emu/src/main.rs

1135 lines
40 KiB
Rust
Raw Normal View History

2023-08-10 13:58:34 -05:00
mod card;
2023-10-12 13:11:38 -05:00
mod cpu;
2023-08-10 13:58:34 -05:00
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;
2024-01-23 17:10:03 -06:00
use rfd::FileDialog;
2023-08-10 13:58:34 -05:00
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
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 {
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;
}
2024-01-23 16:01:14 -06:00
MemCycle::In(a) => {
self.fp_address = a;
self.fp_data = 0;
},
2023-08-10 13:58:34 -05:00
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()
}
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()
.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!();
}
}
}
2023-08-10 13:58:34 -05:00
});
});
});
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)
2023-10-12 13:11:38 -05:00
|| 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)
2023-08-10 13:58:34 -05:00
{
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) {
2023-10-12 13:11:38 -05:00
self.runstop = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::E) {
2023-10-12 13:11:38 -05:00
self.single_step = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::R) {
2023-10-12 13:11:38 -05:00
self.exam = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::T) {
2023-10-12 13:11:38 -05:00
self.dep = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::Y) {
2023-10-12 13:11:38 -05:00
self.reset = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::U) {
2023-10-12 13:11:38 -05:00
self.prot = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::I) {
2023-10-12 13:11:38 -05:00
self.aux1 = SwitchState::Up;
2023-08-10 13:58:34 -05:00
}
if pressed_keys.contains(&Keycode::O) {
2023-10-12 13:11:38 -05:00
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;
2023-08-10 13:58:34 -05:00
}
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
}
2024-01-23 16:01:14 -06:00
MemCycle::In(_) => 0,
MemCycle::Out(_, _) => 0,
2023-08-10 13:58:34 -05:00
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();
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() {
*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,
},
];