mod card; mod cpu; 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::(); 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.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, 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: 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, 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.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) || 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(_) => 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, 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, }, ];