From 9706b052cb5f5e619d1c6b662f9b3f69cd633715 Mon Sep 17 00:00:00 2001 From: pjht Date: Mon, 29 Jan 2024 11:02:37 -0600 Subject: [PATCH] Connect front panel to emulator --- src/frontpanel.rs | 158 +++++++++++---- src/main.rs | 484 ++++++++++++---------------------------------- 2 files changed, 241 insertions(+), 401 deletions(-) diff --git a/src/frontpanel.rs b/src/frontpanel.rs index a9f095e..ea848bd 100644 --- a/src/frontpanel.rs +++ b/src/frontpanel.rs @@ -1,11 +1,12 @@ use std::{path::Path, sync::mpsc::Sender}; +use device_query::Keycode; use eframe::{ egui::{self, Id, Painter, Response, Sense, TextureOptions, Ui, Widget}, epaint::{pos2, vec2, Color32, Pos2, Rect, TextureHandle}, }; -use crate::audio::AudioMessage; +use crate::{audio::AudioMessage, cpu::Status}; use self::switch::SwitchState; @@ -15,8 +16,27 @@ pub mod switch; const NULL_UV: Rect = Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); const NULL_TINT: Color32 = Color32::WHITE; -#[derive(Copy, Clone, Default)] -struct FrontpanelState { +#[derive(Clone, Copy, Debug)] +pub enum ActionSwitch { + RunStop, + SingleStep, + Examine, + Deposit, + Reset, + Protect, + Aux1, + Aux2, +} + +#[derive(Clone, Copy, Debug)] +pub enum FrontpanelInteraction { + ActionSwClicked(ActionSwitch, SwitchState), + AdChanged(u16), + PowerChanged(bool), +} + +#[derive(Copy, Clone)] +pub struct FrontpanelState { ad_sws: u16, power: bool, runstop: SwitchState, @@ -27,27 +47,73 @@ struct FrontpanelState { prot: SwitchState, aux1: SwitchState, aux2: SwitchState, + addr: u16, + data: u8, + status: Status, } -pub struct Frontpanel<'a> { - id: Id, - textures: &'a Textures, - audio_tx: Sender, -} - -impl<'a> Frontpanel<'a> { - pub fn new(id: Id, textures: &'a Textures, audio_tx: Sender) -> Self { +impl FrontpanelState { + pub fn new() -> Self { Self { - id, - textures, - audio_tx, + ad_sws: 0, + 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, + addr: 0, + data: 0, + status: Status::empty(), } } + + pub fn ad_sws(&self) -> u16 { + self.ad_sws + } + + pub fn power(&self) -> bool { + self.power + } + + pub fn set_addr(&mut self, addr: u16) { + self.addr = addr; + } + + pub fn set_data(&mut self, data: u8) { + self.data = data; + } + + pub fn set_status(&mut self, status: Status) { + self.status = status; + } } -impl Widget for Frontpanel<'_> { +pub struct Frontpanel<'a> { + textures: &'a Textures, + state: &'a mut FrontpanelState, + interaction: Option, +} + +impl<'a> Frontpanel<'a> { + pub fn new(textures: &'a Textures, state: &'a mut FrontpanelState) -> Self { + Self { + textures, + state, + interaction: None, + } + } + + pub fn interaction(&self) -> Option { + self.interaction + } +} + +impl Widget for &mut Frontpanel<'_> { fn ui(self, ui: &mut Ui) -> egui::Response { - let mut state: FrontpanelState = ui.data(|data| data.get_temp(self.id).unwrap_or_default()); let sw_textures = switch::Textures::new( self.textures.sw_up.clone(), self.textures.sw_neut.clone(), @@ -68,7 +134,14 @@ impl Widget for Frontpanel<'_> { ui.image(&self.textures.fp); for led in &LEDS { let pos = led.pos + fp_rect.left_top().to_vec2(); - let led_data = 0xAAAA; + let led_data = match led.source { + LedSource::Protect => 0x0, + LedSource::Iff => 0x0, + LedSource::Run => 0x0, + LedSource::CpuStatus => u16::from(self.state.status.bits()), + LedSource::Data => u16::from(self.state.data), + LedSource::Address => self.state.addr, + }; let led_on = (led_data & led.mask) > 0; ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(16.0, 16.0)), |ui| { ui.add(led::Led::new(led_on, &led_textures)); @@ -81,44 +154,42 @@ impl Widget for Frontpanel<'_> { let pos = switch.pos + fp_rect.left_top().to_vec2(); if i == 0 { ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| { - let mut power_inv = !state.power; + let mut power_inv = !self.state.power; if ui .add(switch::ToggleSwitch::new(&mut power_inv, &sw_textures)) .drag_started() { - if !power_inv { - self.audio_tx.send(AudioMessage::FanOn).unwrap(); - } else { - self.audio_tx.send(AudioMessage::FanOff).unwrap(); - } - self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap(); + self.state.power = !power_inv; + self.interaction = + Some(FrontpanelInteraction::PowerChanged(self.state.power)); }; - state.power = !power_inv; }); } if (1..17).contains(&i) { let bit_mask = 1 << (16 - i); - let mut sw_state = state.ad_sws & bit_mask > 0; + let mut sw_state = self.state.ad_sws & bit_mask > 0; ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| { if ui .add(switch::ToggleSwitch::new(&mut sw_state, &sw_textures)) .drag_started() { - self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap(); + self.state.ad_sws = + (self.state.ad_sws & !(bit_mask)) | ((sw_state as u16) << (16 - i)); + self.interaction = + Some(FrontpanelInteraction::AdChanged(self.state.ad_sws)); }; }); - state.ad_sws = (state.ad_sws & !(bit_mask)) | ((sw_state as u16) << (16 - i)); } if (17..25).contains(&i) { let state = match i { - 17 => &mut state.runstop, - 18 => &mut state.single_step, - 19 => &mut state.exam, - 20 => &mut state.dep, - 21 => &mut state.reset, - 22 => &mut state.prot, - 23 => &mut state.aux1, - 24 => &mut state.aux2, + 17 => &mut self.state.runstop, + 18 => &mut self.state.single_step, + 19 => &mut self.state.exam, + 20 => &mut self.state.dep, + 21 => &mut self.state.reset, + 22 => &mut self.state.prot, + 23 => &mut self.state.aux1, + 24 => &mut self.state.aux2, _ => unreachable!(), }; ui.allocate_ui_at_rect(Rect::from_center_size(pos, vec2(11.0, 25.0)), |ui| { @@ -126,13 +197,24 @@ impl Widget for Frontpanel<'_> { .add(switch::ThreePosSwitch::new(state, &sw_textures)) .drag_started() { - self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap(); + let sw = match i { + 17 => ActionSwitch::RunStop, + 18 => ActionSwitch::SingleStep, + 19 => ActionSwitch::Examine, + 20 => ActionSwitch::Deposit, + 21 => ActionSwitch::Reset, + 22 => ActionSwitch::Protect, + 23 => ActionSwitch::Aux1, + 24 => ActionSwitch::Aux2, + _ => unreachable!(), + }; + self.interaction = + Some(FrontpanelInteraction::ActionSwClicked(sw, *state)); }; }); } } }); - ui.data_mut(|data| data.insert_temp(self.id, state)); resp } } diff --git a/src/main.rs b/src/main.rs index f11fec0..e00bd84 100644 --- a/src/main.rs +++ b/src/main.rs @@ -10,7 +10,7 @@ use audio::{AudioMessage, AudioThread}; use cpu::{MemCycle, Status, I8080}; use device_query::DeviceState; use eframe::{ - egui::{self, menu, Button, Id, Label, Pos2, Rect, TextureHandle, TextureOptions, Ui}, + egui::{self, menu, Button, Label, TextureHandle, TextureOptions}, NativeOptions, }; use egui_modal::Modal; @@ -18,7 +18,7 @@ use rand::RngCore; use rfd::FileDialog; use serde::{Deserialize, Serialize}; -use crate::frontpanel::{switch, Frontpanel}; +use crate::frontpanel::{switch::SwitchState, Frontpanel, FrontpanelInteraction, FrontpanelState}; fn main() -> Result<(), eframe::Error> { env_logger::init(); @@ -37,23 +37,8 @@ struct Options { struct AltairEmulator { textures: frontpanel::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, audio_tx: Sender, options: Options, @@ -61,6 +46,7 @@ struct AltairEmulator { device_state: DeviceState, kbd_newdown: bool, kbd_olddown: bool, + fp_state: FrontpanelState, } impl AltairEmulator { @@ -103,23 +89,8 @@ impl AltairEmulator { // 21: 005 Self { textures: frontpanel::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, audio_tx, options, @@ -127,55 +98,62 @@ impl AltairEmulator { device_state: DeviceState::new(), kbd_newdown: false, kbd_olddown: false, + fp_state: FrontpanelState::new(), } } fn update_fp(&mut self) { let cycle = self.cpu.get_mem_cycle(); - self.fp_status = cycle.get_status(); + self.fp_state.set_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]; + self.fp_state.set_addr(a); + self.fp_state.set_data(self.mem[a as usize]); } MemCycle::Write(a, _) | MemCycle::StackWrite(a, _) | MemCycle::Out(a, _) => { - self.fp_address = a; - self.fp_data = 0xff; + self.fp_state.set_addr(a); + self.fp_state.set_data(0xff); } MemCycle::In(a) => { - self.fp_address = a; - self.fp_data = 0; + self.fp_state.set_addr(a); + self.fp_state.set_data(0); } MemCycle::Inta(_) => todo!(), MemCycle::Hlta(_) => { - self.fp_data = 0xff; - self.fp_address = 0xffff; + self.fp_state.set_addr(0xffff); + self.fp_state.set_data(0xff); } MemCycle::IntaHlt(_) => todo!(), } } + + fn run_cpu_cycle(&mut self) { + let cycle = self.cpu.get_mem_cycle(); + let data = match cycle { + MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => self.mem[a as usize], + MemCycle::Write(a, d) | MemCycle::StackWrite(a, d) => { + self.mem[a as usize] = d; + 0 + } + MemCycle::In(_) => 0, + MemCycle::Out(_, _) => 0, + MemCycle::Inta(_) => todo!(), + MemCycle::Hlta(_) => { + self.running = false; + 0 + } + MemCycle::IntaHlt(_) => todo!(), + }; + self.cpu.finish_m_cycle(data); + self.update_fp(); + } } 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); - }); - } - - 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); - }, - ); - } - + fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) { 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(); @@ -226,17 +204,84 @@ impl eframe::App for AltairEmulator { }); }); egui::CentralPanel::default().show(ctx, |ui| { - ui.style_mut().debug.debug_on_hover = true; - ui.add(Frontpanel::new( - Id::new("frontpanel"), - &self.textures, - self.audio_tx.clone(), - )); - ui.add(Frontpanel::new( - Id::new("frontpanel2"), - &self.textures, - self.audio_tx.clone(), - )); + let mut fp = Frontpanel::new(&self.textures, &mut self.fp_state); + ui.add(&mut fp); + let interaction = fp.interaction(); + if let Some(interaction) = interaction { + self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap(); + match interaction { + FrontpanelInteraction::ActionSwClicked(sw, state) => { + if self.fp_state.power() { + match sw { + frontpanel::ActionSwitch::RunStop => { + if state == SwitchState::Up { + self.running = false; + } else { + self.running = true; + } + } + frontpanel::ActionSwitch::SingleStep => { + self.run_cpu_cycle(); + } + frontpanel::ActionSwitch::Examine => { + if state == SwitchState::Up { + // Assume M1 + self.cpu.finish_m_cycle(0xC3); // JMP + self.cpu.finish_m_cycle(self.fp_state.ad_sws() as u8); + self.cpu + .finish_m_cycle((self.fp_state.ad_sws() >> 8) as u8); + self.update_fp(); + } else { + // Assume M1 + self.cpu.finish_m_cycle(0x0); // NOP + self.update_fp(); + } + } + frontpanel::ActionSwitch::Deposit => { + if state == SwitchState::Up { + // Assume M1 + self.mem[self.cpu.get_mem_cycle().address() as usize] = + self.fp_state.ad_sws() as u8; + self.update_fp(); + } else { + // Assume M1 + self.cpu.finish_m_cycle(0x0); // NOP + self.mem[self.cpu.get_mem_cycle().address() as usize] = + self.fp_state.ad_sws() as u8; + self.update_fp(); + } + } + frontpanel::ActionSwitch::Reset => { + if state == SwitchState::Up { + self.cpu.reset(); + self.update_fp(); + } + } + frontpanel::ActionSwitch::Protect => (), + frontpanel::ActionSwitch::Aux1 => (), + frontpanel::ActionSwitch::Aux2 => (), + } + } + } + FrontpanelInteraction::AdChanged(_) => (), + FrontpanelInteraction::PowerChanged(pwr) => { + if pwr { + self.audio_tx.send(AudioMessage::FanOn).unwrap(); + self.cpu = I8080::new(); + self.update_fp(); + } else { + self.audio_tx.send(AudioMessage::FanOff).unwrap(); + self.running = false; + self.fp_state.set_status(Status::empty()); + self.fp_state.set_addr(0); + self.fp_state.set_data(0); + } + } + } + } + if self.running { + self.run_cpu_cycle(); + } // dbg!(ui.input(|input| input.pointer.latest_pos())); // dbg!(ui.input(|input| input.pointer.latest_pos())); // let (_, fp_rect) = ui.allocate_space((800.0, 333.0).into()); @@ -589,19 +634,21 @@ impl eframe::App for AltairEmulator { // self.cpu.finish_m_cycle(data); // self.update_fp(); // } - // - ui.add(Label::new("Hello")); }); - // let old_fan_enabled = self.options.fan_enabled; + 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 (old_fan_enabled != self.options.fan_enabled) && self.fp_state.power() { + if self.options.fan_enabled { + self.audio_tx.send(AudioMessage::FanOn).unwrap(); + } else { + self.audio_tx.send(AudioMessage::FanOff).unwrap(); + } + } if self.running { ctx.request_repaint(); } @@ -721,292 +768,3 @@ impl Textures { 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, - }, -];