1use crate::genesis::m68kdebug::M68kDebugMemoryMap; 2use crate::genesis::widgets::{BreakpointWindowResponse, BreakpointsWidget}; 3use crate::{AddressSet, non_selectable_label}; 4use egui::style::ScrollStyle; 5use egui::{Align, CentralPanel, Grid, Layout, Panel, RichText, TextEdit, Ui, Window}; 6use egui_extras::{Column, TableBuilder}; 7use genesis_core::api::debug::{GenesisDebugState, Z80BreakStatus, Z80Breakpoint}; 8use z80_emu::{DisassembledInstruction, MemoryAccess, Z80}; 9 10const DISASSEMBLY_WINDOW_TITLE: &str = "Z80 Disassembly"; 11const BREAKPOINTS_WINDOW_TITLE: &str = "Z80 Breakpoints"; 12 13#[derive(Debug, Clone, Copy, PartialEq, Eq)] 14pub enum Z80BreakCommand { 15 Pause, 16 Resume, 17 Step, 18} 19 20pub struct Z80DebugWindowState { 21 pub disassembly_open: bool, 22 pub disassembly_address: u16, 23 pub disassembly_end_address: Option<u16>, 24 pub disassembly_addr_changed: bool, 25 pub jump_to_address: String, 26 pub disassembly_table_offset: f32, 27 pub disassembly_table_height: f32, 28 pub disassembly_selected_pcs: AddressSet<u16>, 29 pub break_status_last_frame: Z80BreakStatus, 30 pub breakpoints_open: bool, 31 pub breakpoints: BreakpointsWidget<u16>, 32} 33 34impl Z80DebugWindowState { 35 pub fn new() -> Self { 36 Self { 37 disassembly_open: false, 38 disassembly_address: 0, 39 disassembly_end_address: None, 40 disassembly_addr_changed: false, 41 jump_to_address: String::new(), 42 disassembly_table_offset: 0.0, 43 disassembly_table_height: 1.0, 44 disassembly_selected_pcs: AddressSet::new(), 45 break_status_last_frame: Z80BreakStatus::default(), 46 breakpoints_open: false, 47 breakpoints: BreakpointsWidget::new("z80_breakpoints"), 48 } 49 } 50 51 pub fn open_disassembly_window(&mut self, ctx: &egui::Context) { 52 self.disassembly_open = true; 53 crate::move_to_top(ctx, DISASSEMBLY_WINDOW_TITLE); 54 } 55 56 pub fn open_breakpoints_window(&mut self, ctx: &egui::Context) { 57 self.breakpoints_open = true; 58 crate::move_to_top(ctx, BREAKPOINTS_WINDOW_TITLE); 59 } 60 61 fn maybe_change_disassembly_address(&mut self, address: u16) { 62 if self 63 .disassembly_end_address 64 .is_none_or(|end_addr| !(self.disassembly_address..end_addr).contains(&address)) 65 { 66 self.change_disassembly_address(address); 67 } 68 } 69 70 fn change_disassembly_address(&mut self, address: u16) { 71 self.disassembly_address = address; 72 self.disassembly_addr_changed = true; 73 } 74} 75 76pub trait Z80MemoryMap { 77 fn peek(&self, address: u16) -> Option<u8>; 78} 79 80pub struct GenesisZ80MemoryMap<'a> { 81 pub audio_ram: &'a [u8], 82 pub z80_memory_bank: u32, 83 pub m68k_map: &'a dyn M68kDebugMemoryMap, 84} 85 86impl<'a> GenesisZ80MemoryMap<'a> { 87 pub fn new(debug_state: &'a GenesisDebugState, m68k_map: &'a dyn M68kDebugMemoryMap) -> Self { 88 Self { 89 audio_ram: debug_state.audio_ram.as_ref(), 90 z80_memory_bank: debug_state.z80_bank_number, 91 m68k_map, 92 } 93 } 94} 95 96impl Z80MemoryMap for GenesisZ80MemoryMap<'_> { 97 fn peek(&self, address: u16) -> Option<u8> { 98 match address { 99 0x0000..=0x3FFF => Some(self.audio_ram[(address & 0x1FFF) as usize]), 100 0x8000..=0xFFFF => { 101 let m68k_addr = (self.z80_memory_bank << 15) | u32::from(address & 0x7FFF); 102 self.m68k_map.peek(m68k_addr).map(|word| word.to_be_bytes()[(address & 1) as usize]) 103 } 104 _ => None, 105 } 106 } 107} 108 109pub fn render_disassembly_window( 110 ctx: &egui::Context, 111 z80: &Z80, 112 memory_map: impl Z80MemoryMap, 113 state: &mut Z80DebugWindowState, 114 break_status: Z80BreakStatus, 115 handle_command: Option<impl FnMut(Z80BreakCommand)>, 116) { 117 if break_status.breaking && break_status != state.break_status_last_frame { 118 let mut move_to_pc = break_status.pc; 119 for previous_pc in break_status.previous_pcs { 120 if previous_pc > move_to_pc || previous_pc < move_to_pc.saturating_sub(16) { 121 break; 122 } 123 move_to_pc = previous_pc; 124 } 125 126 state.maybe_change_disassembly_address(move_to_pc); 127 state.disassembly_open = true; 128 crate::move_to_top(ctx, DISASSEMBLY_WINDOW_TITLE); 129 } 130 state.break_status_last_frame = break_status; 131 132 let mut open = state.disassembly_open; 133 Window::new(DISASSEMBLY_WINDOW_TITLE) 134 .open(&mut open) 135 .constrain(false) 136 .resizable([true, true]) 137 .default_pos(crate::rand_window_pos()) 138 .default_width(700.0) 139 .show(ctx, |ui| { 140 if let Some(handle_command) = handle_command { 141 render_disasm_top_panel(state, handle_command, ui); 142 } 143 144 render_disasm_right_panel(z80, state, ui); 145 render_disasm_central_panel(z80, memory_map, state, break_status, ui); 146 }); 147 state.disassembly_open = open; 148} 149 150fn render_disasm_top_panel( 151 state: &mut Z80DebugWindowState, 152 mut handle_command: impl FnMut(Z80BreakCommand), 153 ui: &mut Ui, 154) { 155 Panel::top("z80_top_panel").show_inside(ui, |ui| { 156 ui.horizontal(|ui| { 157 if ui.button("Pause").clicked() { 158 handle_command(Z80BreakCommand::Pause); 159 } 160 161 if ui.button("Resume").clicked() { 162 handle_command(Z80BreakCommand::Resume); 163 } 164 165 if ui.button("Step").clicked() { 166 handle_command(Z80BreakCommand::Step); 167 } 168 169 ui.with_layout(Layout::right_to_left(Align::Min), |ui| { 170 if ui.button("Breakpoints").clicked() { 171 state.open_breakpoints_window(ui.ctx()); 172 } 173 }); 174 }); 175 176 ui.add_space(3.0); 177 }); 178} 179 180fn render_disasm_right_panel(z80: &Z80, state: &mut Z80DebugWindowState, ui: &mut Ui) { 181 Panel::right("z80_right_panel").show_inside(ui, |ui| { 182 ui.horizontal(|ui| { 183 let text_resp = 184 ui.add(TextEdit::singleline(&mut state.jump_to_address).desired_width(40.0)); 185 let button_resp = ui.button("Jump to address"); 186 187 let should_jump = button_resp.clicked() 188 || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))); 189 if should_jump && let Ok(address) = u16::from_str_radix(&state.jump_to_address, 16) { 190 state.change_disassembly_address(address); 191 } 192 }); 193 194 ui.add_space(3.0); 195 196 if ui.button("Jump to PC").clicked() { 197 state.change_disassembly_address(z80.pc()); 198 } 199 200 ui.separator(); 201 202 let registers = z80.registers(); 203 Grid::new("z80_registers_grid").show(ui, |ui| { 204 for ((label0, value0), (label1, value1)) in [ 205 (("A", registers.a), ("F", u8::from(registers.f))), 206 (("B", registers.b), ("C", registers.c)), 207 (("D", registers.d), ("E", registers.e)), 208 (("H", registers.h), ("L", registers.l)), 209 ] { 210 ui.label(label0); 211 ui.label(monospace_u8(value0)); 212 ui.label(label1); 213 ui.label(monospace_u8(value1)); 214 ui.end_row(); 215 } 216 217 ui.label("HL"); 218 ui.label(monospace_u16(u16::from_be_bytes([registers.h, registers.l]))); 219 ui.label("SP"); 220 ui.label(monospace_u16(registers.sp)); 221 ui.end_row(); 222 223 ui.label("IX"); 224 ui.label(monospace_u16(registers.ix)); 225 ui.label("IY"); 226 ui.label(monospace_u16(registers.iy)); 227 ui.end_row(); 228 229 ui.label("PC"); 230 ui.label(monospace_u16(registers.pc)); 231 ui.end_row(); 232 233 ui.label("IFF1"); 234 ui.label(monospace_bool(registers.iff1)); 235 ui.label("IFF2"); 236 ui.label(monospace_bool(registers.iff2)); 237 ui.end_row(); 238 239 ui.label("I"); 240 ui.label(monospace_u8(registers.i)); 241 ui.label("R"); 242 ui.label(monospace_u8(registers.r)); 243 ui.end_row(); 244 245 for ((label0, value0), (label1, value1)) in [ 246 (("A'", registers.ap), ("F'", u8::from(registers.fp))), 247 (("B'", registers.bp), ("C'", registers.cp)), 248 (("D'", registers.dp), ("E'", registers.ep)), 249 (("H'", registers.hp), ("L'", registers.lp)), 250 ] { 251 ui.label(label0); 252 ui.label(monospace_u8(value0)); 253 ui.label(label1); 254 ui.label(monospace_u8(value1)); 255 ui.end_row(); 256 } 257 }); 258 259 ui.add_space(3.0); 260 261 ui.label( 262 RichText::new(format!( 263 "C={} N={} V={} H={} Z={} S={}", 264 u8::from(registers.f.carry), 265 u8::from(registers.f.subtract), 266 u8::from(registers.f.overflow), 267 u8::from(registers.f.half_carry), 268 u8::from(registers.f.zero), 269 u8::from(registers.f.sign), 270 )) 271 .monospace(), 272 ); 273 }); 274} 275 276fn render_disasm_central_panel( 277 z80: &Z80, 278 memory_map: impl Z80MemoryMap, 279 state: &mut Z80DebugWindowState, 280 break_status: Z80BreakStatus, 281 ui: &mut Ui, 282) { 283 let ctx = ui.ctx().clone(); 284 285 CentralPanel::default().show_inside(ui, |ui| { 286 ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() }; 287 288 let mut table_builder = TableBuilder::new(ui) 289 .column(Column::auto().at_least(10.0)) 290 .column(Column::auto().at_least(60.0)) 291 .column(Column::auto().at_least(150.0)) 292 .column(Column::remainder()) 293 .striped(true) 294 .sense(egui::Sense::click()); 295 296 if state.disassembly_addr_changed { 297 state.disassembly_addr_changed = false; 298 table_builder = table_builder.scroll_to_row(0, Some(Align::Min)); 299 } else if crate::window_on_top(&ctx, DISASSEMBLY_WINDOW_TITLE) { 300 let keys = crate::scroll_keys_pressed(&ctx); 301 if let Some(offset) = keys.relative_scroll_offset(state.disassembly_table_height) { 302 table_builder = 303 table_builder.vertical_scroll_offset(state.disassembly_table_offset + offset); 304 } 305 } 306 307 let z80_pc = if break_status.breaking { break_status.pc } else { z80.pc() }; 308 309 let highlight_color = crate::highlight_color(ctx.theme()); 310 311 let scroll_output = table_builder.body(|mut body| { 312 let mut pc = state.disassembly_address; 313 let mut instruction = DisassembledInstruction::new(); 314 315 for _ in 0..100 { 316 body.row(15.0, |mut row| { 317 let is_pc_row = pc == z80_pc; 318 let original_pc = pc; 319 320 row.set_selected(state.disassembly_selected_pcs.contains(pc)); 321 322 row.col(|ui| { 323 state.breakpoints.render_clickable_widget( 324 pc, 325 format!("z80_break_row_{pc}"), 326 ui, 327 ); 328 329 if is_pc_row { 330 ui.add(non_selectable_label( 331 RichText::new("→").monospace().color(highlight_color), 332 )); 333 } 334 }); 335 336 row.col(|ui| { 337 let mut text = monospace_u16(pc); 338 if is_pc_row { 339 text = text.color(highlight_color); 340 } 341 342 ui.add(non_selectable_label(text)); 343 }); 344 345 z80_emu::disassemble_into(&mut instruction, pc, || { 346 let byte = memory_map.peek(pc).unwrap_or(0xFF); 347 pc = pc.wrapping_add(1); 348 byte 349 }); 350 351 row.col(|ui| { 352 let mut text = RichText::new(&instruction.text).monospace(); 353 if is_pc_row { 354 text = text.color(highlight_color); 355 } 356 357 ui.add(non_selectable_label(text)); 358 }); 359 360 row.col(|ui| { 361 if let Some(memory_access) = instruction.memory_access { 362 let address = memory_access.resolve_address(z80); 363 let value = 364 memory_map.peek(address).map(|value| format!("0x{value:02X}")); 365 366 let text = match (memory_access, value) { 367 (MemoryAccess::Absolute(..), Some(value)) => { 368 monospace_text(format!("; = {value}")) 369 } 370 (MemoryAccess::Absolute(..), None) => return, 371 (MemoryAccess::Indirect(..), Some(value)) => { 372 monospace_text(format!("; ${address:04X} = {value}")) 373 } 374 (MemoryAccess::Indirect(..), None) => { 375 monospace_text(format!("; ${address:04X}")) 376 } 377 }; 378 ui.add(non_selectable_label(text)); 379 } 380 }); 381 382 if row.response().clicked() { 383 state 384 .disassembly_selected_pcs 385 .handle_click(original_pc, ctx.input(|i| i.modifiers)); 386 } 387 }); 388 } 389 390 state.disassembly_end_address = Some(pc); 391 }); 392 state.disassembly_table_offset = scroll_output.state.offset.y; 393 state.disassembly_table_height = scroll_output.inner_rect.height(); 394 }); 395} 396 397pub fn render_breakpoints_window( 398 ctx: &egui::Context, 399 state: &mut Z80DebugWindowState, 400 update_breakpoints: impl FnOnce(Vec<Z80Breakpoint>), 401) { 402 let response = state.breakpoints.show_window( 403 ctx, 404 BREAKPOINTS_WINDOW_TITLE, 405 &mut state.breakpoints_open, 406 |_| BreakpointWindowResponse::NotChanged, 407 ); 408 if response == BreakpointWindowResponse::Changed { 409 let breakpoints = state 410 .breakpoints 411 .breakpoints() 412 .iter() 413 .map(|breakpoint| Z80Breakpoint { 414 start_address: breakpoint.start_address, 415 end_address: breakpoint.end_address, 416 read: breakpoint.read, 417 write: breakpoint.write, 418 execute: breakpoint.execute, 419 }) 420 .collect(); 421 422 update_breakpoints(breakpoints); 423 } 424} 425 426fn monospace_text(value: impl Into<String>) -> RichText { 427 RichText::new(value).monospace() 428} 429 430fn monospace_bool(value: bool) -> RichText { 431 RichText::new(["0", "1"][usize::from(value)]).monospace() 432} 433 434fn monospace_u8(value: u8) -> RichText { 435 RichText::new(format!("{value:02X}")).monospace() 436} 437 438fn monospace_u16(value: u16) -> RichText { 439 RichText::new(format!("{value:04X}")).monospace() 440}