2023-08-10 13:58:34 -05:00
mod card ;
2023-10-12 13:11:38 -05:00
mod cpu ;
2023-08-10 13:58:34 -05:00
mod ram ;
use std ::{
path ::Path ,
sync ::{ mpsc ::Sender , Arc } ,
thread ,
time ::{ Duration , Instant } ,
} ;
use cpu ::{ MemCycle , Status , I8080 } ;
use device_query ::{ DeviceState , Keycode } ;
2024-01-24 10:50:35 -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 } ;
#[ 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 ,
dep : SwitchState ,
reset : SwitchState ,
prot : SwitchState ,
aux1 : SwitchState ,
aux2 : SwitchState ,
fp_address : u16 ,
fp_data : u8 ,
fp_status : Status ,
mem : [ u8 ; 65536 ] ,
cpu : I8080 ,
mouse_newdown : bool ,
mouse_olddown : bool ,
running : bool ,
main_audio_tx : Sender < AudioMessage > ,
fan_audio_tx : Sender < bool > ,
options : Options ,
option_window : Option < OptionWindow > ,
device_state : DeviceState ,
kbd_newdown : bool ,
kbd_olddown : bool ,
}
impl AltairEmulator {
fn new (
cc : & eframe ::CreationContext < '_ > ,
main_audio_tx : Sender < AudioMessage > ,
fan_audio_tx : Sender < bool > ,
) -> Self {
let options = if cc . storage . unwrap ( ) . get_string ( " options " ) . is_none ( ) {
Options { fan_enabled : true }
} else {
eframe ::get_value ( cc . storage . unwrap ( ) , " options " ) . unwrap ( )
} ;
let mut mem = [ 0 ; 65536 ] ;
let cpu = I8080 ::new ( ) ;
rand ::thread_rng ( ) . fill_bytes ( & mut mem ) ;
mem [ 0x0 ] = 0x3a ;
mem [ 0x1 ] = 0x10 ;
mem [ 0x2 ] = 0x00 ;
mem [ 0x3 ] = 0x47 ;
mem [ 0x4 ] = 0x3a ;
mem [ 0x5 ] = 0x11 ;
mem [ 0x6 ] = 0x00 ;
mem [ 0x7 ] = 0x80 ;
mem [ 0x8 ] = 0x32 ;
mem [ 0x9 ] = 0x12 ;
mem [ 0xa ] = 0x00 ;
mem [ 0xb ] = 0x76 ;
mem [ 0x10 ] = 0x4 ;
mem [ 0x11 ] = 0x5 ;
// 0: 072 020 000
2023-10-12 13:11:38 -05:00
// 3: 107
2023-08-10 13:58:34 -05:00
// 4: 072 021 000
// 7: 200
// 10: 062 022 000
// 13: 166
// 20: 004
// 21: 005
Self {
textures : Textures ::new ( & cc . egui_ctx ) ,
ad_sws : 0x0 ,
power : false ,
runstop : SwitchState ::Neut ,
single_step : SwitchState ::Neut ,
exam : SwitchState ::Neut ,
dep : SwitchState ::Neut ,
reset : SwitchState ::Neut ,
prot : SwitchState ::Neut ,
aux1 : SwitchState ::Neut ,
aux2 : SwitchState ::Neut ,
mem ,
fp_address : 0 ,
fp_data : 0 ,
fp_status : Status ::empty ( ) ,
cpu ,
mouse_newdown : false ,
mouse_olddown : false ,
running : false ,
main_audio_tx ,
fan_audio_tx ,
options ,
option_window : None ,
device_state : DeviceState ::new ( ) ,
kbd_newdown : false ,
kbd_olddown : false ,
}
}
fn update_fp ( & mut self ) {
let cycle = self . cpu . get_mem_cycle ( ) ;
self . fp_status = cycle . get_status ( ) ;
match cycle {
MemCycle ::Fetch ( a ) | MemCycle ::Read ( a ) | MemCycle ::StackRead ( a ) = > {
self . fp_address = a ;
self . fp_data = self . mem [ a as usize ] ;
}
MemCycle ::Write ( a , _ ) | MemCycle ::StackWrite ( a , _ ) | MemCycle ::Out ( a , _ ) = > {
self . fp_address = a ;
self . fp_data = 0xff ;
}
2024-01-23 16:01:14 -06:00
MemCycle ::In ( a ) = > {
self . fp_address = a ;
self . fp_data = 0 ;
} ,
2023-08-10 13:58:34 -05:00
MemCycle ::Inta ( _ ) = > todo! ( ) ,
MemCycle ::Hlta ( _ ) = > {
self . fp_data = 0xff ;
self . fp_address = 0xffff ;
}
MemCycle ::IntaHlt ( _ ) = > todo! ( ) ,
}
}
}
impl eframe ::App for AltairEmulator {
fn save ( & mut self , storage : & mut dyn eframe ::Storage ) {
eframe ::set_value ( storage , " options " , & self . options ) ;
}
fn update ( & mut self , ctx : & egui ::Context , frame : & mut eframe ::Frame ) {
fn image_topleft ( pos : impl Into < Pos2 > , texture : & TextureHandle , ui : & mut Ui ) {
ui . allocate_ui_at_rect ( Rect ::from_min_size ( pos . into ( ) , texture . size_vec2 ( ) ) , | ui | {
ui . image ( texture , texture . size_vec2 ( ) ) ;
} ) ;
}
fn image_center ( pos : impl Into < Pos2 > , texture : & TextureHandle , ui : & mut Ui ) {
ui . allocate_ui_at_rect (
Rect ::from_center_size ( pos . into ( ) , texture . size_vec2 ( ) ) ,
| ui | {
ui . image ( texture , texture . size_vec2 ( ) ) ;
} ,
) ;
}
ctx . set_pixels_per_point ( 1.0 ) ;
frame . set_window_size ( ( 800.0 , 333.0 ) . into ( ) ) ;
let mut disable_fp_sws = self . option_window . is_some ( ) ;
egui ::TopBottomPanel ::top ( " menu " ) . show ( ctx , | ui | {
menu ::bar ( ui , | ui | {
menu ::menu_button ( ui , " Edit " , | ui | {
disable_fp_sws = true ;
if ui
. add_enabled ( self . option_window . is_none ( ) , Button ::new ( " Options " ) )
. clicked ( )
{
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 ( )
. add_filter ( " Binary files " , & [ " bin " , " img " , " hex " , " mcs " , " int " , " ihex " , " ihe " , " ihx " ] )
. add_filter ( " All files " , & [ " * " ] )
. pick_file ( ) ;
if let Some ( file ) = file {
if file . extension ( ) . map_or ( false , | ext | ihex_exts . contains ( & ext . to_str ( ) . unwrap_or ( " " ) ) ) {
let data = std ::fs ::read_to_string ( file ) . unwrap ( ) ;
for record in ihex ::Reader ::new ( & data ) {
let record = record . unwrap ( ) ;
match record {
ihex ::Record ::Data { offset , value } = > {
for ( i , & byte ) in value . iter ( ) . enumerate ( ) {
self . mem [ offset as usize + i ] = byte ;
}
} ,
ihex ::Record ::StartLinearAddress ( _ ) = > todo! ( ) ,
_ = > unimplemented! ( ) ,
} ;
}
} else {
// Raw binary
todo! ( ) ;
}
}
}
2023-08-10 13:58:34 -05:00
} ) ;
} ) ;
} ) ;
egui ::CentralPanel ::default ( ) . show ( ctx , | ui | {
let ( _ , fp_rect ) = ui . allocate_space ( ( 800.0 , 333.0 ) . into ( ) ) ;
image_topleft ( fp_rect . left_top ( ) , & self . textures . fp , ui ) ;
for led in & LEDS {
let led_data = match led . source {
LedSource ::Protect = > 0x0 ,
LedSource ::Iff = > 0x0 ,
LedSource ::Run = > 0x0 ,
2024-01-24 10:50:35 -06:00
LedSource ::CpuStatus = > u16 ::from ( self . fp_status . bits ( ) ) ,
LedSource ::Data = > u16 ::from ( self . fp_data ) ,
2023-08-10 13:58:34 -05:00
LedSource ::Address = > self . fp_address ,
} ;
let led_on = ( led_data & led . mask ) > 0 ;
let texture = if led_on {
& self . textures . led_on
} else {
& self . textures . led_off
} ;
image_center ( led . pos + fp_rect . left_top ( ) . to_vec2 ( ) , texture , ui ) ;
}
let pointer_state = ctx . input ( | inp | inp . pointer . clone ( ) ) ;
let interact_pos = pointer_state . interact_pos ( ) ;
let mut switch_clicked = false ;
for ( i , switch ) in SWITCHES . iter ( ) . enumerate ( ) {
let pos = switch . pos + fp_rect . left_top ( ) . to_vec2 ( ) ;
fn bool_to_texture ( state : bool , textures : & Textures ) -> & TextureHandle {
if state {
& textures . sw_up
} else {
& textures . sw_down
}
}
fn state_to_texture ( state : SwitchState , textures : & Textures ) -> & TextureHandle {
match state {
SwitchState ::Up = > & textures . sw_up ,
SwitchState ::Neut = > & textures . sw_neut ,
SwitchState ::Down = > & textures . sw_down ,
}
}
let texture = match i {
0 = > bool_to_texture ( ! self . power , & self . textures ) ,
( 1 ..= 16 ) = > {
bool_to_texture ( ( self . ad_sws & ( 1 < < ( 16 - i ) ) ) > 0 , & self . textures )
}
17 = > state_to_texture ( self . runstop , & self . textures ) ,
18 = > state_to_texture ( self . single_step , & self . textures ) ,
19 = > state_to_texture ( self . exam , & self . textures ) ,
20 = > state_to_texture ( self . dep , & self . textures ) ,
21 = > state_to_texture ( self . reset , & self . textures ) ,
22 = > state_to_texture ( self . prot , & self . textures ) ,
23 = > state_to_texture ( self . aux1 , & self . textures ) ,
24 = > state_to_texture ( self . aux2 , & self . textures ) ,
_ = > unreachable! ( ) ,
} ;
image_center ( pos , texture , ui ) ;
let interacted = interact_pos . map ( | interact_pos | {
Rect ::from_center_size ( interact_pos , ( 10.0 , 24.0 ) . into ( ) ) . contains ( pos )
& & ! disable_fp_sws
} ) ;
if pointer_state . primary_clicked ( ) & & interacted . unwrap ( ) {
match i {
0 = > {
switch_clicked = true ;
self . power = ! self . power ;
if self . options . fan_enabled {
self . fan_audio_tx . send ( self . power ) . unwrap ( ) ;
}
if self . power {
self . cpu = I8080 ::new ( ) ;
self . update_fp ( ) ;
} else {
self . running = false ;
self . fp_status = Status ::empty ( ) ;
self . fp_data = 0 ;
self . fp_address = 0 ;
}
}
( 1 ..= 16 ) = > {
switch_clicked = true ;
if ( self . ad_sws & ( 1 < < ( 16 - i ) ) ) > 0 {
self . ad_sws & = ! ( 1 < < ( 16 - i ) ) ;
} else {
self . ad_sws | = 1 < < ( 16 - i ) ;
}
}
_ = > ( ) ,
}
}
if pointer_state . primary_down ( ) & & interacted . unwrap ( ) {
if ( 17 ..= 24 ) . contains ( & i ) & & self . mouse_newdown {
switch_clicked = true ;
}
let newstate = if interact_pos . unwrap ( ) . y > pos . y {
SwitchState ::Down
} else {
SwitchState ::Up
} ;
match i {
17 = > self . runstop = newstate ,
18 = > self . single_step = newstate ,
19 = > self . exam = newstate ,
20 = > self . dep = newstate ,
21 = > self . reset = newstate ,
22 = > self . prot = newstate ,
23 = > self . aux1 = newstate ,
24 = > self . aux2 = newstate ,
_ = > ( ) ,
}
} else {
match i {
17 = > self . runstop = SwitchState ::Neut ,
18 = > self . single_step = SwitchState ::Neut ,
19 = > self . exam = SwitchState ::Neut ,
20 = > self . dep = SwitchState ::Neut ,
21 = > self . reset = SwitchState ::Neut ,
22 = > self . prot = SwitchState ::Neut ,
23 = > self . aux1 = SwitchState ::Neut ,
24 = > self . aux2 = SwitchState ::Neut ,
_ = > ( ) ,
}
}
}
if ! disable_fp_sws {
let mut kbd_ad = None ;
let pressed_keys = self . device_state . query_keymap ( ) ;
if pressed_keys
. iter ( )
. filter ( | & & k | k ! = Keycode ::LShift & & k ! = Keycode ::RShift )
. count ( )
! = 0
{
if self . kbd_newdown {
self . kbd_olddown = true ;
self . kbd_newdown = false ;
} else if ! self . kbd_newdown & & ! self . kbd_olddown {
self . kbd_newdown = true ;
}
} else {
self . kbd_newdown = false ;
self . kbd_olddown = false ;
}
if self . kbd_newdown {
if pressed_keys . contains ( & Keycode ::W )
| | pressed_keys . contains ( & Keycode ::E )
| | pressed_keys . contains ( & Keycode ::R )
| | pressed_keys . contains ( & Keycode ::T )
| | pressed_keys . contains ( & Keycode ::Y )
| | pressed_keys . contains ( & Keycode ::U )
| | pressed_keys . contains ( & Keycode ::I )
| | pressed_keys . contains ( & Keycode ::O )
2023-10-12 13:11:38 -05:00
| | pressed_keys . contains ( & Keycode ::S )
| | pressed_keys . contains ( & Keycode ::D )
| | pressed_keys . contains ( & Keycode ::F )
| | pressed_keys . contains ( & Keycode ::G )
| | pressed_keys . contains ( & Keycode ::H )
| | pressed_keys . contains ( & Keycode ::J )
| | pressed_keys . contains ( & Keycode ::K )
| | pressed_keys . contains ( & Keycode ::L )
2023-08-10 13:58:34 -05:00
{
switch_clicked = true ;
}
if pressed_keys . contains ( & Keycode ::LShift )
| | pressed_keys . contains ( & Keycode ::RShift )
{
if pressed_keys . contains ( & Keycode ::Key1 ) {
kbd_ad = Some ( 15 ) ;
} else if pressed_keys . contains ( & Keycode ::Key2 ) {
kbd_ad = Some ( 14 ) ;
} else if pressed_keys . contains ( & Keycode ::Key3 ) {
kbd_ad = Some ( 13 ) ;
} else if pressed_keys . contains ( & Keycode ::Key4 ) {
kbd_ad = Some ( 12 ) ;
} else if pressed_keys . contains ( & Keycode ::Key5 ) {
kbd_ad = Some ( 11 ) ;
} else if pressed_keys . contains ( & Keycode ::Key6 ) {
kbd_ad = Some ( 10 ) ;
} else if pressed_keys . contains ( & Keycode ::Key7 ) {
kbd_ad = Some ( 9 ) ;
} else if pressed_keys . contains ( & Keycode ::Key8 ) {
kbd_ad = Some ( 8 ) ;
}
} else {
if pressed_keys . contains ( & Keycode ::Key1 ) {
kbd_ad = Some ( 7 ) ;
} else if pressed_keys . contains ( & Keycode ::Key2 ) {
kbd_ad = Some ( 6 ) ;
} else if pressed_keys . contains ( & Keycode ::Key3 ) {
kbd_ad = Some ( 5 ) ;
} else if pressed_keys . contains ( & Keycode ::Key4 ) {
kbd_ad = Some ( 4 ) ;
} else if pressed_keys . contains ( & Keycode ::Key5 ) {
kbd_ad = Some ( 3 ) ;
} else if pressed_keys . contains ( & Keycode ::Key6 ) {
kbd_ad = Some ( 2 ) ;
} else if pressed_keys . contains ( & Keycode ::Key7 ) {
kbd_ad = Some ( 1 ) ;
} else if pressed_keys . contains ( & Keycode ::Key8 ) {
kbd_ad = Some ( 0 ) ;
}
if pressed_keys . contains ( & Keycode ::Q ) {
switch_clicked = true ;
self . power = ! self . power ;
if self . options . fan_enabled {
self . fan_audio_tx . send ( self . power ) . unwrap ( ) ;
}
if self . power {
self . cpu = I8080 ::new ( ) ;
self . update_fp ( ) ;
} else {
self . running = false ;
self . fp_status = Status ::empty ( ) ;
self . fp_data = 0 ;
self . fp_address = 0 ;
}
}
}
}
if pressed_keys . contains ( & Keycode ::W ) {
2023-10-12 13:11:38 -05:00
self . runstop = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::E ) {
2023-10-12 13:11:38 -05:00
self . single_step = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::R ) {
2023-10-12 13:11:38 -05:00
self . exam = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::T ) {
2023-10-12 13:11:38 -05:00
self . dep = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::Y ) {
2023-10-12 13:11:38 -05:00
self . reset = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::U ) {
2023-10-12 13:11:38 -05:00
self . prot = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::I ) {
2023-10-12 13:11:38 -05:00
self . aux1 = SwitchState ::Up ;
2023-08-10 13:58:34 -05:00
}
if pressed_keys . contains ( & Keycode ::O ) {
2023-10-12 13:11:38 -05:00
self . aux2 = SwitchState ::Up ;
}
if pressed_keys . contains ( & Keycode ::S ) {
self . runstop = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::D ) {
self . single_step = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::F ) {
self . exam = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::G ) {
self . dep = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::H ) {
self . reset = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::J ) {
self . prot = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::K ) {
self . aux1 = SwitchState ::Down ;
}
if pressed_keys . contains ( & Keycode ::L ) {
self . aux2 = SwitchState ::Down ;
2023-08-10 13:58:34 -05:00
}
if let Some ( kbd_ad ) = kbd_ad {
switch_clicked = true ;
if ( self . ad_sws & ( 1 < < kbd_ad ) ) > 0 {
self . ad_sws & = ! ( 1 < < kbd_ad ) ;
} else {
self . ad_sws | = 1 < < kbd_ad ;
}
}
}
if switch_clicked {
self . main_audio_tx
. send ( AudioMessage ::PlaySwitchClick )
. unwrap ( ) ;
}
if pointer_state . primary_down ( ) {
if self . mouse_newdown {
self . mouse_olddown = true ;
self . mouse_newdown = false ;
} else if ! self . mouse_newdown & & ! self . mouse_olddown {
self . mouse_newdown = true ;
}
} else {
self . mouse_newdown = false ;
self . mouse_olddown = false ;
}
if switch_clicked & & self . power {
if ! self . running {
if self . exam = = SwitchState ::Up {
// Assume M1
self . cpu . finish_m_cycle ( 0xC3 ) ; // JMP
self . cpu . finish_m_cycle ( self . ad_sws as u8 ) ;
self . cpu . finish_m_cycle ( ( self . ad_sws > > 8 ) as u8 ) ;
self . update_fp ( ) ;
}
if self . exam = = SwitchState ::Down {
// Assume M1
self . cpu . finish_m_cycle ( 0x0 ) ; // NOP
self . update_fp ( ) ;
}
if self . dep = = SwitchState ::Up {
// Assume M1
self . mem [ self . cpu . get_mem_cycle ( ) . address ( ) as usize ] = self . ad_sws as u8 ;
self . update_fp ( ) ;
}
if self . dep = = SwitchState ::Down {
// Assume M1
self . cpu . finish_m_cycle ( 0x0 ) ; // NOP
self . mem [ self . cpu . get_mem_cycle ( ) . address ( ) as usize ] = self . ad_sws as u8 ;
self . update_fp ( ) ;
}
}
if self . runstop = = SwitchState ::Up {
self . running = false ;
}
if self . runstop = = SwitchState ::Down {
self . running = true ;
}
if self . reset = = SwitchState ::Up {
self . cpu . reset ( ) ;
self . update_fp ( ) ;
}
}
if ( ( switch_clicked & & self . single_step ! = SwitchState ::Neut ) | | self . running )
& & self . power
{
let cycle = self . cpu . get_mem_cycle ( ) ;
let data = match cycle {
MemCycle ::Fetch ( a ) | MemCycle ::Read ( a ) | MemCycle ::StackRead ( a ) = > {
self . mem [ a as usize ]
}
MemCycle ::Write ( a , d ) | MemCycle ::StackWrite ( a , d ) = > {
self . mem [ a as usize ] = d ;
0
}
2024-01-23 16:01:14 -06:00
MemCycle ::In ( _ ) = > 0 ,
MemCycle ::Out ( _ , _ ) = > 0 ,
2023-08-10 13:58:34 -05:00
MemCycle ::Inta ( _ ) = > todo! ( ) ,
MemCycle ::Hlta ( _ ) = > {
self . running = false ;
0
}
MemCycle ::IntaHlt ( _ ) = > todo! ( ) ,
} ;
self . cpu . finish_m_cycle ( data ) ;
self . update_fp ( ) ;
}
} ) ;
let old_fan_enabled = self . options . fan_enabled ;
if let Some ( option_window ) = self . option_window . as_mut ( ) {
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 ,
} ,
] ;