1use crate::genesis::widgets::{BreakpointWindowResponse, BreakpointsWidget, U24}; 2use crate::{AddressSet, non_selectable_label}; 3use egui::style::ScrollStyle; 4use egui::{Align, CentralPanel, Grid, Layout, Panel, RichText, TextEdit, Ui, Window}; 5use egui_extras::{Column, TableBuilder}; 6use genesis_components::cartridge::Cartridge; 7use genesis_core::api::debug::{ 8 DebugPendingWrite, GenesisDebugState, M68000BreakStatus, M68000Breakpoint, M68000Breakpoints, 9}; 10use jgenesis_common::num::{GetBit, U16Ext}; 11use m68000_emu::disassemble::{DisassembledInstruction, MemoryAccess, MemoryReadType}; 12use m68000_emu::{M68000, OpSize}; 13use segacd_core::WordRam; 14use segacd_core::api::debug::SegaCdDebugState; 15 16pub trait M68kInterruptBreakpoints { 17 fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse; 18 19 fn levels(&self) -> Vec<u8>; 20} 21 22pub struct DummyM68kInterruptBreakpoints; 23 24impl M68kInterruptBreakpoints for DummyM68kInterruptBreakpoints { 25 fn render(&mut self, _ui: &mut Ui) -> BreakpointWindowResponse { 26 BreakpointWindowResponse::NotChanged 27 } 28 29 fn levels(&self) -> Vec<u8> { 30 vec![] 31 } 32} 33 34#[derive(Debug, Clone, Default)] 35pub struct Main68kInterruptBreakpoints { 36 vertical: bool, 37 horizontal: bool, 38} 39 40impl M68kInterruptBreakpoints for Main68kInterruptBreakpoints { 41 fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse { 42 ui.separator(); 43 44 let mut changed = false; 45 46 changed |= ui.checkbox(&mut self.vertical, "Break on vertical interrupt (INT6)").changed(); 47 changed |= 48 ui.checkbox(&mut self.horizontal, "Break on horizontal interrupt (INT4)").changed(); 49 50 BreakpointWindowResponse::from_changed(changed) 51 } 52 53 fn levels(&self) -> Vec<u8> { 54 let mut levels = vec![]; 55 56 if self.vertical { 57 levels.push(6); 58 } 59 60 if self.horizontal { 61 levels.push(4); 62 } 63 64 levels 65 } 66} 67 68#[derive(Debug, Clone, Default)] 69pub struct Sub68kInterruptBreakpoints { 70 cdc: bool, 71 cdd: bool, 72 timer: bool, 73 software: bool, 74 graphics: bool, 75} 76 77impl M68kInterruptBreakpoints for Sub68kInterruptBreakpoints { 78 fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse { 79 ui.separator(); 80 81 let mut changed = false; 82 83 for (field, label) in [ 84 (&mut self.cdc, "Break on CDC interrupt (INT5)"), 85 (&mut self.cdd, "Break on CDD interrupt (INT4)"), 86 (&mut self.timer, "Break on timer interrupt (INT3)"), 87 (&mut self.software, "Break on software interrupt (INT2)"), 88 (&mut self.graphics, "Break on graphics interrupt (INT1)"), 89 ] { 90 changed |= ui.checkbox(field, label).changed(); 91 } 92 93 BreakpointWindowResponse::from_changed(changed) 94 } 95 96 fn levels(&self) -> Vec<u8> { 97 [(self.cdc, 5), (self.cdd, 4), (self.timer, 3), (self.software, 2), (self.graphics, 1)] 98 .into_iter() 99 .filter_map(|(field, level)| field.then_some(level)) 100 .collect() 101 } 102} 103 104#[derive(Debug, Clone, Copy, PartialEq, Eq)] 105pub enum M68kBreakCommand { 106 Pause, 107 Resume, 108 Step, 109} 110 111pub struct M68kDebugWindowState { 112 pub disassembly_window_title: String, 113 pub breakpoints_window_title: String, 114 pub disassembly_open: bool, 115 pub breakpoints_open: bool, 116 pub disassemble_start: u32, 117 pub disassemble_end_addr: Option<u32>, 118 pub disassemble_jump_addr: String, 119 pub disassemble_reset_table: bool, 120 pub disassemble_table_offset: f32, 121 pub disassemble_table_height: f32, 122 pub disassemble_selected_pcs: AddressSet<u32>, 123 pub break_status_last_frame: M68000BreakStatus, 124 pub breakpoints: BreakpointsWidget<U24>, 125 pub interrupt_breakpoints: Box<dyn M68kInterruptBreakpoints>, 126} 127 128impl M68kDebugWindowState { 129 pub fn new_default_titles() -> Self { 130 Self::new_with_titles("68000 Disassembly", "68000 Breakpoints") 131 } 132 133 pub fn new_with_titles( 134 disassembly_window_title: impl Into<String>, 135 breakpoints_window_title: impl Into<String>, 136 ) -> Self { 137 let disassembly_window_title = disassembly_window_title.into(); 138 let breakpoints_window_title = breakpoints_window_title.into(); 139 let breakpoints = BreakpointsWidget::new(&breakpoints_window_title); 140 141 Self { 142 disassembly_window_title, 143 breakpoints_window_title, 144 disassembly_open: false, 145 breakpoints_open: false, 146 disassemble_start: 0, 147 disassemble_end_addr: None, 148 disassemble_jump_addr: String::new(), 149 disassemble_reset_table: false, 150 disassemble_table_offset: 0.0, 151 disassemble_table_height: 1.0, 152 disassemble_selected_pcs: AddressSet::new(), 153 break_status_last_frame: M68000BreakStatus::default(), 154 breakpoints, 155 interrupt_breakpoints: Box::new(DummyM68kInterruptBreakpoints), 156 } 157 } 158 159 pub fn with_interrupt_breakpoints( 160 mut self, 161 interrupt_breakpoints: Box<dyn M68kInterruptBreakpoints>, 162 ) -> Self { 163 self.interrupt_breakpoints = interrupt_breakpoints; 164 self 165 } 166 167 pub fn open_disassembly_window(&mut self, ctx: &egui::Context) { 168 self.disassembly_open = true; 169 crate::move_to_top(ctx, &self.disassembly_window_title); 170 } 171 172 pub fn open_breakpoints_window(&mut self, ctx: &egui::Context) { 173 self.breakpoints_open = true; 174 crate::move_to_top(ctx, &self.breakpoints_window_title); 175 } 176 177 fn maybe_move_disassembly_table(&mut self, address: u32) { 178 if self 179 .disassemble_end_addr 180 .is_none_or(|end_addr| !(self.disassemble_start..end_addr).contains(&address)) 181 { 182 self.move_disassembly_table(address); 183 } 184 } 185 186 fn move_disassembly_table(&mut self, address: u32) { 187 self.disassemble_start = address; 188 self.disassemble_reset_table = true; 189 } 190} 191 192pub trait M68kDebugMemoryMap { 193 fn peek(&self, address: u32) -> Option<u16>; 194 195 fn extra_info(&self) -> Vec<(&'static str, String)> { 196 vec![] 197 } 198} 199 200#[derive(Debug, Clone, Copy)] 201pub struct Genesis68kMemoryMap<'a> { 202 pub cartridge: Option<&'a Cartridge>, 203 pub working_ram: &'a [u16], 204 pub audio_ram: &'a [u8], 205 pub main_pending_writes: &'a [DebugPendingWrite], 206 pub sega_cd: Option<SegaCd68kMemoryMap<'a>>, 207 pub sega_32x: Option<Sega32X68kMemoryMap>, 208} 209 210#[derive(Debug, Clone, Copy)] 211pub struct SegaCd68kMemoryMap<'a> { 212 pub bios_rom: &'a [u16], 213 pub prg_ram: &'a [u16], 214 pub word_ram: &'a WordRam, 215 pub prg_ram_base_addr: usize, 216} 217 218impl SegaCd68kMemoryMap<'_> { 219 fn peek(&self, address: u32) -> u16 { 220 if address & 0x200000 == 0 { 221 if address & 0x20000 == 0 { 222 self.bios_rom[((address & 0x1FFFF) >> 1) as usize] 223 } else { 224 let prg_ram_addr = self.prg_ram_base_addr | (address & 0x1FFFF) as usize; 225 self.prg_ram[prg_ram_addr >> 1] 226 } 227 } else { 228 let msb = self.word_ram.main_cpu_read_ram(address); 229 let lsb = self.word_ram.main_cpu_read_ram(address + 1); 230 u16::from_be_bytes([msb, lsb]) 231 } 232 } 233} 234 235#[derive(Debug, Clone, Copy)] 236pub struct Sega32X68kMemoryMap { 237 pub banked_rom_base_addr: u32, 238} 239 240impl<'a> Genesis68kMemoryMap<'a> { 241 pub fn new(debug_state: &'a GenesisDebugState) -> Self { 242 Self { 243 cartridge: debug_state.cartridge.as_ref(), 244 working_ram: debug_state.working_ram.as_ref(), 245 audio_ram: debug_state.audio_ram.as_ref(), 246 main_pending_writes: debug_state.pending_writes.as_slice(), 247 sega_cd: debug_state.sega_cd.as_ref().map(|sega_cd| SegaCd68kMemoryMap { 248 bios_rom: sega_cd.bios_rom(), 249 prg_ram: sega_cd.prg_ram(), 250 word_ram: sega_cd.word_ram(), 251 prg_ram_base_addr: usize::from(sega_cd.main_cpu_prg_ram_bank()) << 17, 252 }), 253 sega_32x: debug_state.sega_32x.as_ref().map(|sega_32x| Sega32X68kMemoryMap { 254 banked_rom_base_addr: u32::from(sega_32x.m68k_rom_bank()) << 20, 255 }), 256 } 257 } 258} 259 260impl M68kDebugMemoryMap for Genesis68kMemoryMap<'_> { 261 fn peek(&self, address: u32) -> Option<u16> { 262 let address = address & 0xFFFFFF & !1; 263 264 match address { 265 0x000000..=0x3FFFFF => { 266 if let Some(cartridge) = self.cartridge { 267 Some(cartridge.peek_word(address)) 268 } else { 269 self.sega_cd.as_ref().map(|sega_cd| sega_cd.peek(address)) 270 } 271 } 272 0x400000..=0x7FFFFF => match (self.cartridge, self.sega_cd) { 273 (Some(_cartridge), Some(sega_cd)) => Some(sega_cd.peek(address)), 274 (Some(cartridge), None) => Some(cartridge.peek_word(address)), 275 (None, _) => None, 276 }, 277 0x880000..=0x8FFFFF 278 if self.sega_32x.is_some() 279 && let Some(cartridge) = self.cartridge => 280 { 281 Some(cartridge.peek_word(address & 0x7FFFF)) 282 } 283 0x900000..=0x9FFFFF 284 if let Some(sega_32x) = self.sega_32x 285 && let Some(cartridge) = self.cartridge => 286 { 287 let cartridge_addr = sega_32x.banked_rom_base_addr | (address & 0xFFFFF); 288 Some(cartridge.peek_word(cartridge_addr)) 289 } 290 0xA00000..=0xA03FFF | 0xA08000..=0xA0BFFF => { 291 let byte = self.audio_ram[(address & 0x1FFF) as usize]; 292 Some(u16::from_le_bytes([byte, byte])) 293 } 294 0xE00000..=0xFFFFFF => Some(self.working_ram[((address & 0xFFFF) >> 1) as usize]), 295 _ => None, 296 } 297 } 298 299 fn extra_info(&self) -> Vec<(&'static str, String)> { 300 let mut extra_info = Vec::new(); 301 302 if !self.main_pending_writes.is_empty() { 303 extra_info.push(("Buffered writes", String::new())); 304 305 for &write in self.main_pending_writes { 306 match write { 307 DebugPendingWrite::Word { address, value } => { 308 extra_info.push(("", format!("{address:06X} {value:04X}"))); 309 } 310 DebugPendingWrite::Byte { address, value } => { 311 extra_info.push(("", format!("{address:06X} {value:02X}"))); 312 } 313 } 314 } 315 } 316 317 if let Some(sega_cd) = self.sega_cd { 318 extra_info.push(( 319 "PRG RAM Bank", 320 format!( 321 "{} ({:05X}-{:05X})", 322 sega_cd.prg_ram_base_addr >> 17, 323 sega_cd.prg_ram_base_addr, 324 sega_cd.prg_ram_base_addr | 0x1FFFF 325 ), 326 )); 327 } 328 329 if let Some(sega_32x) = self.sega_32x { 330 let banked_rom_range = format!( 331 "{} ({:06X}-{:06X})", 332 sega_32x.banked_rom_base_addr >> 20, 333 sega_32x.banked_rom_base_addr, 334 sega_32x.banked_rom_base_addr | 0xFFFFF 335 ); 336 extra_info.push(("32X ROM Bank", banked_rom_range)); 337 } 338 339 extra_info 340 } 341} 342 343pub struct SegaCdSubMemoryMap<'a> { 344 prg_ram: &'a [u16], 345 word_ram: &'a WordRam, 346} 347 348impl<'a> SegaCdSubMemoryMap<'a> { 349 pub fn new(debug_state: &'a SegaCdDebugState) -> Self { 350 Self { prg_ram: debug_state.prg_ram(), word_ram: debug_state.word_ram() } 351 } 352} 353 354impl M68kDebugMemoryMap for SegaCdSubMemoryMap<'_> { 355 fn peek(&self, address: u32) -> Option<u16> { 356 let address = address & 0x0FFFFF; 357 358 let word = match address { 359 0x00000..=0x7FFFF => self.prg_ram[(address >> 1) as usize], 360 0x80000..=0xBFFFF => { 361 let msb = self.word_ram.sub_cpu_peek_ram(address); 362 let lsb = self.word_ram.sub_cpu_peek_ram(address + 1); 363 u16::from_be_bytes([msb, lsb]) 364 } 365 _ => return None, 366 }; 367 368 Some(word) 369 } 370} 371 372pub fn render_disassembly_window( 373 ctx: &egui::Context, 374 m68k: &M68000, 375 memory_map: &dyn M68kDebugMemoryMap, 376 state: &mut M68kDebugWindowState, 377 break_status: M68000BreakStatus, 378 handle_command: Option<impl FnMut(M68kBreakCommand)>, 379) { 380 if break_status.breaking && break_status != state.break_status_last_frame { 381 let mut move_to_pc = break_status.pc; 382 for previous_pc in break_status.previous_pcs { 383 if previous_pc > move_to_pc || previous_pc < move_to_pc.saturating_sub(16) { 384 break; 385 } 386 move_to_pc = previous_pc; 387 } 388 389 state.maybe_move_disassembly_table(move_to_pc); 390 state.disassembly_open = true; 391 crate::move_to_top(ctx, &state.disassembly_window_title); 392 } 393 state.break_status_last_frame = break_status; 394 395 // Prevent window from spawning partially offscreen due to large default width 396 let default_pos = [50.0, crate::rand_window_pos()[1]]; 397 398 let mut open = state.disassembly_open; 399 Window::new(&state.disassembly_window_title) 400 .open(&mut open) 401 .constrain(false) 402 .resizable([true, true]) 403 .default_pos(default_pos) 404 .default_width(800.0) 405 .show(ctx, |ui| { 406 if let Some(handle_command) = handle_command { 407 render_disasm_top_panel(state, handle_command, ui); 408 } 409 410 render_disasm_right_panel(m68k, memory_map, state, ui); 411 render_disasm_central_panel(m68k, memory_map, state, break_status, ui); 412 }); 413 state.disassembly_open = open; 414} 415 416fn render_disasm_top_panel( 417 state: &mut M68kDebugWindowState, 418 mut handle_command: impl FnMut(M68kBreakCommand), 419 ui: &mut Ui, 420) { 421 Panel::top(format!("{}_top_panel", state.disassembly_window_title)).show_inside(ui, |ui| { 422 ui.horizontal(|ui| { 423 if ui.button("Pause").clicked() { 424 handle_command(M68kBreakCommand::Pause); 425 } 426 427 if ui.button("Resume").clicked() { 428 handle_command(M68kBreakCommand::Resume); 429 } 430 431 if ui.button("Step").clicked() { 432 handle_command(M68kBreakCommand::Step); 433 } 434 435 ui.with_layout(Layout::right_to_left(Align::Min), |ui| { 436 if ui.button("Breakpoints").clicked() { 437 state.open_breakpoints_window(ui.ctx()); 438 } 439 }); 440 }); 441 442 ui.add_space(5.0); 443 }); 444} 445 446fn render_disasm_right_panel( 447 m68k: &M68000, 448 memory_map: &dyn M68kDebugMemoryMap, 449 state: &mut M68kDebugWindowState, 450 ui: &mut Ui, 451) { 452 Panel::right(format!("{}_right_panel", state.disassembly_window_title)).show_inside(ui, |ui| { 453 ui.horizontal(|ui| { 454 let text_resp = 455 ui.add(TextEdit::singleline(&mut state.disassemble_jump_addr).desired_width(60.0)); 456 let button_resp = ui.button("Jump to address"); 457 458 let should_jump = button_resp.clicked() 459 || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))); 460 if should_jump 461 && let Ok(address) = u32::from_str_radix(&state.disassemble_jump_addr, 16) 462 { 463 state.move_disassembly_table(address & 0xFFFFFF & !1); 464 } 465 }); 466 467 ui.add_space(3.0); 468 469 if ui.button("Jump to PC").clicked() { 470 state.move_disassembly_table(m68k.pc()); 471 } 472 473 ui.separator(); 474 475 let data_registers = m68k.data_registers(); 476 let address_registers = m68k.address_registers(); 477 let status_register = m68k.status_register(); 478 479 Grid::new(format!("{}_registers", state.disassembly_window_title)).show(ui, |ui| { 480 for i in 0..7 { 481 ui.label(format!("D{i}")); 482 ui.label(monospace_u32(data_registers[i])); 483 484 ui.label(format!("A{i}")); 485 ui.label(monospace_u32(address_registers[i])); 486 487 ui.end_row(); 488 } 489 490 ui.label("D7"); 491 ui.label(monospace_u32(data_registers[7])); 492 493 ui.label("A7"); 494 ui.label(monospace_u32(m68k.stack_pointer())); 495 496 ui.end_row(); 497 498 ui.label("SSP"); 499 ui.label(monospace_u32(m68k.supervisor_stack_pointer())); 500 501 ui.label("USP"); 502 ui.label(monospace_u32(m68k.user_stack_pointer())); 503 504 ui.end_row(); 505 506 ui.label("SR"); 507 ui.label(monospace_u16(status_register)); 508 509 ui.label("PC"); 510 ui.label(monospace_u32(m68k.pc())); 511 512 ui.end_row(); 513 }); 514 515 ui.horizontal(|ui| { 516 ui.label("CCR"); 517 518 ui.add_space(20.0); 519 520 let carry: u8 = status_register.bit(0).into(); 521 let overflow: u8 = status_register.bit(1).into(); 522 let zero: u8 = status_register.bit(2).into(); 523 let negative: u8 = status_register.bit(3).into(); 524 let extend: u8 = status_register.bit(4).into(); 525 ui.label( 526 RichText::new(format!("C={carry} V={overflow} Z={zero} N={negative} X={extend}")) 527 .monospace(), 528 ); 529 }); 530 531 for (label, text) in memory_map.extra_info() { 532 ui.horizontal(|ui| { 533 ui.label(label); 534 ui.add_space(5.0); 535 ui.label(RichText::new(text).monospace()); 536 }); 537 } 538 }); 539} 540 541fn render_disasm_central_panel( 542 m68k: &M68000, 543 memory_map: &dyn M68kDebugMemoryMap, 544 state: &mut M68kDebugWindowState, 545 break_status: M68000BreakStatus, 546 ui: &mut Ui, 547) { 548 let ctx = ui.ctx().clone(); 549 550 CentralPanel::default().show_inside(ui, |ui| { 551 ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() }; 552 553 let mut table_builder = TableBuilder::new(ui) 554 .column(Column::auto().at_least(10.0)) 555 .column(Column::auto().at_least(60.0)) 556 .column(Column::auto().at_least(270.0)) 557 .column(Column::remainder()) 558 .striped(true) 559 .sense(egui::Sense::click()); 560 561 if state.disassemble_reset_table { 562 state.disassemble_reset_table = false; 563 table_builder = table_builder.scroll_to_row(0, Some(Align::Min)); 564 } else if crate::window_on_top(&ctx, &state.disassembly_window_title) { 565 let keys = crate::scroll_keys_pressed(&ctx); 566 if let Some(offset) = keys.relative_scroll_offset(state.disassemble_table_height) { 567 table_builder = 568 table_builder.vertical_scroll_offset(state.disassemble_table_offset + offset); 569 } 570 } 571 572 let m68k_pc = if break_status.breaking { break_status.pc } else { m68k.pc() }; 573 574 let highlight_color = crate::highlight_color(ctx.theme()); 575 576 let scroll_output = table_builder.body(|mut body| { 577 let mut pc = state.disassemble_start; 578 let mut instruction = DisassembledInstruction::new(); 579 580 for _ in 0..100 { 581 body.row(15.0, |mut row| { 582 let is_pc_row = pc == m68k_pc; 583 let original_pc = pc; 584 585 row.set_selected(state.disassemble_selected_pcs.contains(pc)); 586 587 row.col(|ui| { 588 state.breakpoints.render_clickable_widget( 589 U24::new(pc), 590 format!("{}_break_row_{pc}", state.breakpoints_window_title), 591 ui, 592 ); 593 594 if is_pc_row { 595 ui.add(non_selectable_label(monospace_str("→").color(highlight_color))); 596 } 597 }); 598 599 row.col(|ui| { 600 let mut text = monospace_u24(pc); 601 if is_pc_row { 602 text = text.color(highlight_color); 603 } 604 605 ui.add(non_selectable_label(text)); 606 }); 607 608 m68000_emu::disassemble::disassemble_into(&mut instruction, pc, || { 609 let word = memory_map.peek(pc).unwrap_or(0xFFFF); 610 pc = (pc + 2) & 0xFFFFFF; 611 word 612 }); 613 614 row.col(|ui| { 615 let mut text = RichText::new(&instruction.text).monospace(); 616 if is_pc_row { 617 text = text.color(highlight_color); 618 } 619 620 ui.add(non_selectable_label(text)); 621 }); 622 623 row.col(|ui| { 624 if let Some(memory_access) = 625 instruction.memory_read.or(instruction.memory_write) 626 { 627 render_memory_access_col( 628 memory_access, 629 m68k, 630 memory_map, 631 &instruction, 632 ui, 633 ); 634 } 635 }); 636 637 if row.response().clicked() { 638 state 639 .disassemble_selected_pcs 640 .handle_click(original_pc, ctx.input(|i| i.modifiers)); 641 } 642 }); 643 } 644 645 state.disassemble_end_addr = Some(pc); 646 }); 647 state.disassemble_table_offset = scroll_output.state.offset.y; 648 state.disassemble_table_height = scroll_output.inner_rect.height(); 649 }); 650} 651 652fn render_memory_access_col( 653 memory_read: MemoryAccess, 654 m68k: &M68000, 655 memory_map: &dyn M68kDebugMemoryMap, 656 instruction: &DisassembledInstruction, 657 ui: &mut Ui, 658) { 659 let (mut address, size) = memory_read.resolve_address(m68k); 660 address &= 0xFFFFFF; 661 662 let value = match size { 663 OpSize::Byte => memory_map 664 .peek(address) 665 .map(|word| if !address.bit(0) { word.msb().into() } else { word.lsb().into() }), 666 OpSize::Word => memory_map.peek(address).map(u32::from), 667 OpSize::LongWord => { 668 let high = memory_map.peek(address); 669 let low = memory_map.peek(address.wrapping_add(2)); 670 match (high, low) { 671 (Some(high), Some(low)) => Some(u32::from(low) | (u32::from(high) << 16)), 672 (Some(high), None) => Some(u32::from(high) << 16), 673 (None, Some(low)) => Some(low.into()), 674 (None, None) => None, 675 } 676 } 677 }; 678 679 let value_str = value.map(|value| match size { 680 OpSize::Byte => format!("0x{value:02X}"), 681 OpSize::Word => format!("0x{value:04X}"), 682 OpSize::LongWord => format!("0x{value:08X}"), 683 }); 684 685 let text = match (memory_read, instruction.memory_read_type, value_str) { 686 (MemoryAccess::Absolute { .. }, MemoryReadType::Read, Some(value_str)) => { 687 monospace_str(format!("; = {value_str}")) 688 } 689 (MemoryAccess::Absolute { .. }, ..) => return, 690 (_, MemoryReadType::Read, Some(value_str)) => { 691 monospace_str(format!("; ${address:06X} = {value_str}")) 692 } 693 (_, MemoryReadType::Read, None) | (_, MemoryReadType::EffectiveAddress, _) => { 694 monospace_str(format!("; ${address:06X}")) 695 } 696 }; 697 698 ui.add(non_selectable_label(text)); 699} 700 701pub fn render_breakpoints_window( 702 ctx: &egui::Context, 703 state: &mut M68kDebugWindowState, 704 mut update_breakpoints: impl FnMut(M68000Breakpoints), 705) { 706 let response = state.breakpoints.show_window( 707 ctx, 708 &state.breakpoints_window_title, 709 &mut state.breakpoints_open, 710 |ui| state.interrupt_breakpoints.render(ui), 711 ); 712 if response == BreakpointWindowResponse::Changed { 713 let memory_breakpoints = state 714 .breakpoints 715 .breakpoints() 716 .iter() 717 .map(|breakpoint| M68000Breakpoint { 718 start_address: breakpoint.start_address.get(), 719 end_address: breakpoint.end_address.get(), 720 read: breakpoint.read, 721 write: breakpoint.write, 722 execute: breakpoint.execute, 723 }) 724 .collect(); 725 726 update_breakpoints(M68000Breakpoints { 727 memory: memory_breakpoints, 728 interrupt: state.interrupt_breakpoints.levels(), 729 }); 730 } 731} 732 733fn monospace_u16(value: u16) -> RichText { 734 RichText::new(format!("{value:04X}")).monospace() 735} 736 737fn monospace_u24(value: u32) -> RichText { 738 RichText::new(format!("{:06X}", value & 0xFFFFFF)).monospace() 739} 740 741fn monospace_u32(value: u32) -> RichText { 742 RichText::new(format!("{value:08X}")).monospace() 743} 744 745fn monospace_str(s: impl Into<String>) -> RichText { 746 RichText::new(s).monospace() 747}