Extract emulator state into separate struct

This commit is contained in:
pjht 2024-01-30 13:30:12 -06:00
parent e5401824c3
commit 2bd3b4e489
Signed by: pjht
GPG Key ID: CA239FC6934E6F3A

View File

@ -54,173 +54,21 @@ impl Options {
struct AltairEmulator { struct AltairEmulator {
textures: Textures, textures: Textures,
option_window: Option<OptionWindow>,
state: EmuState,
}
struct EmuState {
mem: [u8; 65536], mem: [u8; 65536],
cpu: I8080, cpu: I8080,
running: bool, running: bool,
audio_tx: Sender<AudioMessage>, audio_tx: Sender<AudioMessage>,
options: Options, options: Options,
option_window: Option<OptionWindow>,
fp_state: FrontpanelState, fp_state: FrontpanelState,
} }
impl AltairEmulator { impl EmuState {
fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self { fn handle_fp_interaction(&mut self, interaction: FrontpanelInteraction) {
let options = if cc.storage.unwrap().get_string("options").is_none() {
Options {
mute: false,
fan_enabled: true,
volume: 1.0,
}
} else {
eframe::get_value(cc.storage.unwrap(), "options").unwrap()
};
if options.mute {
audio_tx.send(AudioMessage::SetVolume(0.0)).unwrap();
} else {
audio_tx
.send(AudioMessage::SetVolume(options.volume))
.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),
mem,
cpu,
running: false,
audio_tx,
options,
option_window: None,
fp_state: FrontpanelState::new(),
}
}
fn update_fp(&mut self) {
let cycle = self.cpu.get_mem_cycle();
self.fp_state.set_status(cycle.get_status());
match cycle {
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => {
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_state.set_addr(a);
self.fp_state.set_data(0xff);
}
MemCycle::In(a) => {
self.fp_state.set_addr(a);
self.fp_state.set_data(0);
}
MemCycle::Inta(_) => todo!(),
MemCycle::Hlta(_) => {
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);
}
fn show_menubar(&mut self, ui: &mut Ui) {
menu::bar(ui, |ui| {
menu::menu_button(ui, "Edit", |ui| {
if ui
.add_enabled(self.option_window.is_none(), Button::new("Options"))
.clicked()
{
self.option_window = Some(OptionWindow::new(ui.ctx(), self.options));
ui.close_menu();
}
if ui.button("Load binary file").clicked() {
let ihex_exts = ["hex", "mcs", "int", "ihex", "ihe", "ihx"];
let file = FileDialog::new()
.add_filter(
"Binary files",
&["bin", "img", "hex", "mcs", "int", "ihex", "ihe", "ihx"],
)
.add_filter("All files", &["*"])
.pick_file();
if let Some(file) = file {
if file
.extension()
.map_or(false, |ext| ihex_exts.contains(&ext.to_str().unwrap_or("")))
{
let data = std::fs::read_to_string(file).unwrap();
for record in ihex::Reader::new(&data) {
let record = record.unwrap();
match record {
ihex::Record::Data { offset, value } => {
for (i, &byte) in value.iter().enumerate() {
self.mem[offset as usize + i] = byte;
}
}
ihex::Record::StartLinearAddress(_) => todo!(),
_ => unimplemented!(),
};
}
} else {
// Raw binary
todo!();
}
}
}
});
});
}
fn show_frontpanel(&mut self, ui: &mut Ui) {
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(); self.audio_tx.send(AudioMessage::PlaySwitchClick).unwrap();
match interaction { match interaction {
FrontpanelInteraction::ActionSwChanged(sw, state) => { FrontpanelInteraction::ActionSwChanged(sw, state) => {
@ -294,30 +142,62 @@ impl AltairEmulator {
} }
} }
} }
}
}
impl eframe::App for AltairEmulator { fn update_fp(&mut self) {
fn save(&mut self, storage: &mut dyn eframe::Storage) { let cycle = self.cpu.get_mem_cycle();
eframe::set_value(storage, "options", &self.options); self.fp_state.set_status(cycle.get_status());
match cycle {
MemCycle::Fetch(a) | MemCycle::Read(a) | MemCycle::StackRead(a) => {
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_state.set_addr(a);
self.fp_state.set_data(0xff);
}
MemCycle::In(a) => {
self.fp_state.set_addr(a);
self.fp_state.set_data(0);
}
MemCycle::Inta(_) => todo!(),
MemCycle::Hlta(_) => {
self.fp_state.set_addr(0xffff);
self.fp_state.set_data(0xff);
}
MemCycle::IntaHlt(_) => todo!(),
}
} }
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.set_pixels_per_point(1.0);
egui::TopBottomPanel::top("menu").show(ctx, |ui| self.show_menubar(ui));
egui::CentralPanel::default().show(ctx, |ui| self.show_frontpanel(ui));
if self.running {
self.run_cpu_cycle();
self.update_fp();
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
} }
if let Some(option_window) = self.option_window.as_mut() { MemCycle::In(_) => 0,
if option_window.draw(ctx, &mut self.options) { MemCycle::Out(_, _) => 0,
self.option_window = None; MemCycle::Inta(_) => todo!(),
MemCycle::Hlta(_) => {
self.running = false;
0
} }
MemCycle::IntaHlt(_) => todo!(),
};
self.cpu.finish_m_cycle(data);
}
fn update_options(&mut self, new_opts: Options) {
self.options = new_opts;
if self.options.mute { if self.options.mute {
self.audio_tx.send(AudioMessage::SetVolume(0.0)).unwrap(); self
.audio_tx
.send(AudioMessage::SetVolume(0.0))
.unwrap();
} else { } else {
self.audio_tx self
.audio_tx
.send(AudioMessage::SetVolume(self.options.volume)) .send(AudioMessage::SetVolume(self.options.volume))
.unwrap(); .unwrap();
} }
@ -327,8 +207,146 @@ impl eframe::App for AltairEmulator {
self.audio_tx.send(AudioMessage::FanOff).unwrap(); self.audio_tx.send(AudioMessage::FanOff).unwrap();
} }
} }
}
if self.running { impl AltairEmulator {
fn new(cc: &eframe::CreationContext<'_>, audio_tx: Sender<AudioMessage>) -> Self {
let options = if cc.storage.unwrap().get_string("options").is_none() {
Options {
mute: false,
fan_enabled: true,
volume: 1.0,
}
} else {
eframe::get_value(cc.storage.unwrap(), "options").unwrap()
};
if options.mute {
audio_tx.send(AudioMessage::SetVolume(0.0)).unwrap();
} else {
audio_tx
.send(AudioMessage::SetVolume(options.volume))
.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),
option_window: None,
state: EmuState {
mem,
cpu,
running: false,
audio_tx,
options,
fp_state: FrontpanelState::new(),
},
}
}
fn show_menubar(&mut self, ui: &mut Ui) {
menu::bar(ui, |ui| {
menu::menu_button(ui, "Edit", |ui| {
if ui
.add_enabled(self.option_window.is_none(), Button::new("Options"))
.clicked()
{
self.option_window = Some(OptionWindow::new(ui.ctx(), &self.state));
ui.close_menu();
}
if ui.button("Load binary file").clicked() {
let ihex_exts = ["hex", "mcs", "int", "ihex", "ihe", "ihx"];
let file = FileDialog::new()
.add_filter(
"Binary files",
&["bin", "img", "hex", "mcs", "int", "ihex", "ihe", "ihx"],
)
.add_filter("All files", &["*"])
.pick_file();
if let Some(file) = file {
if file
.extension()
.map_or(false, |ext| ihex_exts.contains(&ext.to_str().unwrap_or("")))
{
let data = std::fs::read_to_string(file).unwrap();
for record in ihex::Reader::new(&data) {
let record = record.unwrap();
match record {
ihex::Record::Data { offset, value } => {
for (i, &byte) in value.iter().enumerate() {
self.state.mem[offset as usize + i] = byte;
}
}
ihex::Record::StartLinearAddress(_) => todo!(),
_ => unimplemented!(),
};
}
} else {
// Raw binary
todo!();
}
}
}
});
});
}
fn show_frontpanel(&mut self, ui: &mut Ui) {
let mut fp = Frontpanel::new(&self.textures, &mut self.state.fp_state);
ui.add(&mut fp);
let interaction = fp.interaction();
if let Some(interaction) = interaction {
self.state.handle_fp_interaction(interaction);
}
}
}
impl eframe::App for AltairEmulator {
fn save(&mut self, storage: &mut dyn eframe::Storage) {
eframe::set_value(storage, "options", &self.state.options);
}
fn update(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
ctx.set_pixels_per_point(1.0);
egui::TopBottomPanel::top("menu").show(ctx, |ui| self.show_menubar(ui));
egui::CentralPanel::default().show(ctx, |ui| self.show_frontpanel(ui));
if self.state.running {
self.state.run_cpu_cycle();
self.state.update_fp();
}
if let Some(option_window) = self.option_window.as_mut() {
if option_window.draw(ctx, &mut self.state) {
self.option_window = None;
}
}
if self.state.running {
ctx.request_repaint(); ctx.request_repaint();
} }
} }
@ -346,14 +364,14 @@ enum OptionsCategory {
} }
impl OptionWindow { impl OptionWindow {
fn new(ctx: &egui::Context, options: Options) -> Self { fn new(ctx: &egui::Context, state: &EmuState) -> Self {
Modal::new(ctx, "options_modal").open(); Modal::new(ctx, "options_modal").open();
Self { Self {
options, options: state.options.clone(),
category: OptionsCategory::General, category: OptionsCategory::General,
} }
} }
fn draw(&mut self, ctx: &egui::Context, options: &mut Options) -> bool { fn draw(&mut self, ctx: &egui::Context, state: &mut EmuState) -> bool {
let modal = Modal::new(ctx, "options_modal"); let modal = Modal::new(ctx, "options_modal");
modal.show(|ui| { modal.show(|ui| {
modal.title(ui, "Options"); modal.title(ui, "Options");
@ -373,10 +391,10 @@ impl OptionWindow {
} }
modal.buttons(ui, |ui| { modal.buttons(ui, |ui| {
if ui.button("Apply").clicked() { if ui.button("Apply").clicked() {
*options = self.options; state.update_options(self.options);
} }
if modal.button(ui, "OK").clicked() { if modal.button(ui, "OK").clicked() {
*options = self.options; state.update_options(self.options);
} }
modal.caution_button(ui, "Cancel"); modal.caution_button(ui, "Cancel");
}); });