2023-08-10 13:58:34 -05:00
mod card ;
2023-10-12 13:11:38 -05:00
mod cpu ;
2024-01-24 14:31:01 -06:00
mod frontpanel ;
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 } ;
2024-01-24 14:31:01 -06:00
use eframe ::{
egui ::{ self , menu , Button , Pos2 , Rect , TextureHandle , TextureOptions , Ui } ,
NativeOptions ,
} ;
2023-08-10 13:58:34 -05:00
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 } ;
2024-01-24 14:31:01 -06:00
use crate ::frontpanel ::switch ;
2023-08-10 13:58:34 -05:00
#[ 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 ( ) {
2024-01-24 10:50:35 -06:00
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 ( ) ) ;
2023-08-10 13:58:34 -05:00
}
let mut fan_shutdown_start : Option < ( Instant , Duration ) > = None ;
2024-01-24 10:50:35 -06:00
for msg in & rx {
2023-08-10 13:58:34 -05:00
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 ( ) ;
2024-01-24 10:50:35 -06:00
drop ( sl ) ;
2023-08-10 13:58:34 -05:00
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 ( ) ;
2024-01-24 10:50:35 -06:00
drop ( sl ) ;
2023-08-10 13:58:34 -05:00
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 ) ;
2024-01-24 10:50:35 -06:00
drop ( sl ) ;
2023-08-10 13:58:34 -05:00
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
} ;
2024-01-24 10:50:35 -06:00
for msg in & rx {
2023-08-10 13:58:34 -05:00
match msg {
AudioMessage ::PlaySwitchClick = > {
sl . lock ( ) . play ( & head_step ) ;
}
}
}
} ) ;
eframe ::run_native (
" Altair 8800 Emulator " ,
2024-01-24 10:50:35 -06:00
NativeOptions ::default ( ) ,
2023-08-10 13:58:34 -05:00
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 ,
2024-01-24 14:31:01 -06:00
dep : switch ::SwitchState ,
2023-08-10 13:58:34 -05:00
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 ,
2024-01-24 14:31:01 -06:00
dep : switch ::SwitchState ::Neut ,
2023-08-10 13:58:34 -05:00
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 ;
2024-01-24 14:31:01 -06:00
}
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 ( )
{
2024-01-24 10:50:35 -06:00
self . option_window = Some ( OptionWindow ::new ( ctx , self . options ) ) ;
ui . close_menu ( ) ;
2023-08-10 13:58:34 -05:00
}
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 ( )
2024-01-24 14:31:01 -06:00
. add_filter (
" Binary files " ,
& [ " bin " , " img " , " hex " , " mcs " , " int " , " ihex " , " ihe " , " ihx " ] ,
)
2024-01-23 17:10:03 -06:00
. add_filter ( " All files " , & [ " * " ] )
. pick_file ( ) ;
if let Some ( file ) = file {
2024-01-24 14:31:01 -06:00
if file . extension ( ) . map_or ( false , | ext | {
ihex_exts . contains ( & ext . to_str ( ) . unwrap_or ( " " ) )
} ) {
2024-01-23 17:10:03 -06:00
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 ;
}
2024-01-24 14:31:01 -06:00
}
2024-01-23 17:10:03 -06:00
ihex ::Record ::StartLinearAddress ( _ ) = > todo! ( ) ,
_ = > unimplemented! ( ) ,
} ;
}
} else {
// Raw binary
todo! ( ) ;
}
}
}
2023-08-10 13:58:34 -05:00
} ) ;
} ) ;
} ) ;
egui ::CentralPanel ::default ( ) . show ( ctx , | ui | {
2024-01-24 14:31:01 -06:00
// 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 ) ) ;
2023-08-10 13:58:34 -05:00
} ) ;
2024-01-24 14:31:01 -06:00
2023-08-10 13:58:34 -05:00
let old_fan_enabled = self . options . fan_enabled ;
if let Some ( option_window ) = self . option_window . as_mut ( ) {
2024-01-24 10:50:35 -06:00
if option_window . draw ( ctx , & mut self . options ) {
2023-08-10 13:58:34 -05:00
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 ( ) {
2024-01-24 10:50:35 -06:00
* options = self . options ;
2023-08-10 13:58:34 -05:00
}
if modal . button ( ui , " OK " ) . clicked ( ) {
2024-01-24 10:50:35 -06:00
* options = self . options ;
2023-08-10 13:58:34 -05:00
}
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 ,
} ,
] ;