mod card; mod cpu; mod frontpanel; 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}, NativeOptions, }; use egui_modal::Modal; use log::{debug, trace, warn}; use parking_lot::Mutex; use rand::RngCore; use rfd::FileDialog; use serde::{Deserialize, Serialize}; use soloud::{audio, AudioExt, LoadExt, Soloud}; use crate::frontpanel::switch; #[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::(); let (fan_audio_tx, fan_audio_rx) = std::sync::mpsc::channel::(); 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 { 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(); drop(sl); 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(); drop(sl); 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); drop(sl); 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 { match msg { AudioMessage::PlaySwitchClick => { sl.lock().play(&head_step); } } } }); eframe::run_native( "Altair 8800 Emulator", NativeOptions::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: switch::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, fan_audio_tx: Sender, options: Options, option_window: Option, device_state: DeviceState, kbd_newdown: bool, kbd_olddown: bool, } impl AltairEmulator { fn new( cc: &eframe::CreationContext<'_>, main_audio_tx: Sender, fan_audio_tx: Sender, ) -> Self { let options = if cc.storage.unwrap().get_string("options").is_none() { Options { fan_enabled: true } } else { eframe::get_value(cc.storage.unwrap(), "options").unwrap() }; let mut mem = [0; 65536]; let cpu = I8080::new(); rand::thread_rng().fill_bytes(&mut mem); mem[0x0] = 0x3a; mem[0x1] = 0x10; mem[0x2] = 0x00; mem[0x3] = 0x47; mem[0x4] = 0x3a; mem[0x5] = 0x11; mem[0x6] = 0x00; mem[0x7] = 0x80; mem[0x8] = 0x32; mem[0x9] = 0x12; mem[0xa] = 0x00; mem[0xb] = 0x76; mem[0x10] = 0x4; mem[0x11] = 0x5; // 0: 072 020 000 // 3: 107 // 4: 072 021 000 // 7: 200 // 10: 062 022 000 // 13: 166 // 20: 004 // 21: 005 Self { textures: Textures::new(&cc.egui_ctx), ad_sws: 0x0, power: false, runstop: SwitchState::Neut, single_step: SwitchState::Neut, exam: SwitchState::Neut, dep: switch::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(a) => { self.fp_address = a; self.fp_data = 0; } 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, 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, 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)); 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 (_, 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(); // } let sw_texts = switch::Textures::new( self.textures.sw_up.clone(), self.textures.sw_neut.clone(), self.textures.sw_down.clone(), ); ui.add(switch::Switch::new(&mut self.dep, &sw_texts)); }); 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; } if modal.button(ui, "OK").clicked() { *options = self.options; } 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, path: impl AsRef, ) -> Result { 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, }, ];