1use crate::genesis::widgets::{BreakpointWindowResponse, BreakpointsWidget}; 2use crate::{AddressSet, non_selectable_label}; 3use egui::style::ScrollStyle; 4use egui::{Align, Grid, Layout, RichText, TextEdit, Ui, Window}; 5use egui_extras::{Column, TableBuilder}; 6use genesis_components::cartridge::Cartridge; 7use genesis_core::api::debug::{ 8 GenesisDebugCommand, GenesisDebugState, Sh2BreakStatus, Sh2Breakpoint, Sh2Breakpoints, 9}; 10use s32x_core::WhichCpu; 11use s32x_core::api::debug::Sega32XDebugState; 12use sh2_emu::{DisassembledInstruction, MemoryAccessSize, ReadType, Sh2}; 13use std::ops::Range; 14use std::sync::mpsc::Sender; 15 16#[derive(Debug, Clone, Copy, PartialEq, Eq)] 17pub enum DisassemblyArea { 18 CartridgeRom { cached: bool }, 19 Sdram { cached: bool }, 20 Cache, 21} 22 23impl DisassemblyArea { 24 fn address_range(self) -> Range<usize> { 25 match self { 26 Self::CartridgeRom { cached } => { 27 let start_address = 0x02000000 | (usize::from(!cached) << 29); 28 start_address..start_address + 0x400000 29 } 30 Self::Sdram { cached } => { 31 let start_address = 0x06000000 | (usize::from(!cached) << 29); 32 start_address..start_address + 0x40000 33 } 34 Self::Cache => 0xC0000000..0xC0001000, 35 } 36 } 37 38 fn from_address(address: u32) -> Option<Self> { 39 match address { 40 0x02000000..=0x023FFFFF => Some(Self::CartridgeRom { cached: true }), 41 0x22000000..=0x223FFFFF => Some(Self::CartridgeRom { cached: false }), 42 0x06000000..=0x0603FFFF => Some(Self::Sdram { cached: true }), 43 0x26000000..=0x2603FFFF => Some(Self::Sdram { cached: false }), 44 0xC0000000..=0xC0000FFF => Some(Self::Cache), 45 _ => None, 46 } 47 } 48 49 fn read_address( 50 self, 51 address: u32, 52 cpu: &Sh2, 53 debug_state: &Sega32XDebugState, 54 cartridge: Option<&Cartridge>, 55 ) -> u16 { 56 match self { 57 Self::Sdram { cached } => { 58 if cached && let Some(word) = cpu.peek_cache(address) { 59 return word; 60 } 61 62 debug_state.sdram.get(((address & 0x01FFFFFF) >> 1) as usize).copied().unwrap_or(0) 63 } 64 Self::CartridgeRom { cached } => { 65 if cached && let Some(word) = cpu.peek_cache(address) { 66 return word; 67 } 68 69 let Some(cartridge) = cartridge else { return 0 }; 70 let cartridge_addr = address & 0x3FFFFF & !1; 71 cartridge.peek_word(cartridge_addr) 72 } 73 Self::Cache => cpu.peek_data_array(address), 74 } 75 } 76} 77 78#[derive(Debug, Clone, Default)] 79pub struct S32XInterruptBreakpoints { 80 vertical: bool, 81 horizontal: bool, 82 command: bool, 83 pwm: bool, 84 level_5: bool, 85 level_4: bool, 86 level_3: bool, 87 level_2: bool, 88} 89 90impl S32XInterruptBreakpoints { 91 fn render(&mut self, ui: &mut Ui) -> BreakpointWindowResponse { 92 ui.separator(); 93 94 let mut changed = false; 95 96 for (field, label) in [ 97 (&mut self.vertical, "Break on vertical interrupt (Level 12)"), 98 (&mut self.horizontal, "Break on horizontal interrupt (Level 10)"), 99 (&mut self.command, "Break on command interrupt (Level 8)"), 100 (&mut self.pwm, "Break on PWM interrupt (Level 6)"), 101 (&mut self.level_5, "Break on level 5 interrupt"), 102 (&mut self.level_4, "Break on level 4 interrupt"), 103 (&mut self.level_3, "Break on level 3 interrupt"), 104 (&mut self.level_2, "Break on level 2 interrupt"), 105 ] { 106 changed |= ui.checkbox(field, label).changed(); 107 } 108 109 BreakpointWindowResponse::from_changed(changed) 110 } 111 112 fn levels(&self) -> Vec<u8> { 113 [ 114 (self.vertical, 12), 115 (self.horizontal, 10), 116 (self.command, 8), 117 (self.pwm, 6), 118 (self.level_5, 5), 119 (self.level_4, 4), 120 (self.level_3, 3), 121 (self.level_2, 2), 122 ] 123 .into_iter() 124 .filter_map(|(field, level)| field.then_some(level)) 125 .collect() 126 } 127} 128 129pub struct Sh2DebugWindowState { 130 pub which: WhichCpu, 131 pub disassembly_open: bool, 132 pub breakpoints_open: bool, 133 pub disassembly_area: DisassemblyArea, 134 pub disassembly_address: String, 135 pub disassembly_scroll_row: Option<usize>, 136 pub disassembly_table_offset: f32, 137 pub disassembly_table_height: f32, 138 pub disassembly_selected_pcs: AddressSet<u32>, 139 pub break_status_last_frame: Sh2BreakStatus, 140 pub breakpoints: BreakpointsWidget<u32>, 141 pub interrupt_breakpoints: S32XInterruptBreakpoints, 142} 143 144impl Sh2DebugWindowState { 145 pub fn new(which: WhichCpu) -> Self { 146 Self { 147 which, 148 disassembly_open: false, 149 breakpoints_open: false, 150 disassembly_area: DisassemblyArea::Sdram { cached: true }, 151 disassembly_address: String::new(), 152 disassembly_scroll_row: None, 153 disassembly_table_offset: 0.0, 154 disassembly_table_height: 1.0, 155 disassembly_selected_pcs: AddressSet::new(), 156 break_status_last_frame: Sh2BreakStatus::default(), 157 breakpoints: BreakpointsWidget::new(format!("{which:?}_breakpoints")), 158 interrupt_breakpoints: S32XInterruptBreakpoints::default(), 159 } 160 } 161 162 pub fn open_disassembly_window(&mut self, ctx: &egui::Context) { 163 self.disassembly_open = true; 164 crate::move_to_top(ctx, self.which.disassembly_window_title()); 165 } 166 167 pub fn open_breakpoints_window(&mut self, ctx: &egui::Context) { 168 self.breakpoints_open = true; 169 crate::move_to_top(ctx, self.which.breakpoints_window_title()); 170 } 171 172 fn try_jump_to_address(&mut self, address: u32) { 173 let Some(area) = DisassemblyArea::from_address(address) else { return }; 174 175 self.disassembly_area = area; 176 self.disassembly_scroll_row = Some(((address as usize) - area.address_range().start) / 2); 177 } 178} 179 180trait WhichExt { 181 fn disassembly_window_title(self) -> &'static str; 182 183 fn breakpoints_window_title(self) -> &'static str; 184} 185 186impl WhichExt for WhichCpu { 187 fn disassembly_window_title(self) -> &'static str { 188 match self { 189 WhichCpu::Master => "Master SH-2 Disassembly", 190 WhichCpu::Slave => "Slave SH-2 Disassembly", 191 } 192 } 193 194 fn breakpoints_window_title(self) -> &'static str { 195 match self { 196 WhichCpu::Master => "Master SH-2 Breakpoints", 197 WhichCpu::Slave => "Slave SH-2 Breakpoints", 198 } 199 } 200} 201 202pub fn render_disassembly_window( 203 ctx: &egui::Context, 204 debug_state: &mut GenesisDebugState, 205 window_state: &mut Sh2DebugWindowState, 206 command_sender: &Sender<GenesisDebugCommand>, 207 break_status: Sh2BreakStatus, 208) { 209 let window_title = window_state.which.disassembly_window_title(); 210 211 if break_status.breaking && break_status != window_state.break_status_last_frame { 212 window_state.try_jump_to_address(break_status.pc); 213 window_state.disassembly_open = true; 214 crate::move_to_top(ctx, window_title); 215 } 216 window_state.break_status_last_frame = break_status; 217 218 let Some(sega_32x) = &debug_state.sega_32x else { return }; 219 220 let sh2 = match window_state.which { 221 WhichCpu::Master => sega_32x.sh2_master.clone(), 222 WhichCpu::Slave => sega_32x.sh2_slave.clone(), 223 }; 224 225 let default_pos = [50.0, crate::rand_window_pos()[1]]; 226 227 let mut open = window_state.disassembly_open; 228 Window::new(window_title) 229 .open(&mut open) 230 .constrain(false) 231 .resizable([true, true]) 232 .default_pos(default_pos) 233 .default_size([800.0, 550.0]) 234 .show(ctx, |ui| { 235 render_disasm_top_panel(window_state, command_sender, window_title, ui); 236 render_disasm_right_panel(&sh2, window_state, window_title, ui); 237 render_disasm_central_panel(&sh2, debug_state, window_state, break_status, ui); 238 }); 239 window_state.disassembly_open = open; 240} 241 242fn render_disasm_top_panel( 243 state: &mut Sh2DebugWindowState, 244 command_sender: &Sender<GenesisDebugCommand>, 245 window_title: &str, 246 ui: &mut Ui, 247) { 248 egui::Panel::top(format!("{window_title}_top_panel")).show_inside(ui, |ui| { 249 ui.horizontal(|ui| { 250 if ui.button("Pause").clicked() { 251 let _ = command_sender.send(GenesisDebugCommand::BreakPauseSh2(state.which)); 252 } 253 254 if ui.button("Resume").clicked() { 255 let _ = command_sender.send(GenesisDebugCommand::BreakResume); 256 } 257 258 if ui.button("Step").clicked() { 259 let _ = command_sender.send(GenesisDebugCommand::BreakStepSh2(state.which)); 260 } 261 262 ui.with_layout(Layout::right_to_left(Align::Min), |ui| { 263 if ui.button("Breakpoints").clicked() { 264 state.open_breakpoints_window(ui.ctx()); 265 } 266 }); 267 }); 268 269 ui.add_space(3.0); 270 }); 271} 272 273fn render_disasm_right_panel( 274 sh2: &Sh2, 275 state: &mut Sh2DebugWindowState, 276 window_title: &str, 277 ui: &mut Ui, 278) { 279 egui::Panel::right(format!("{window_title}_left_panel")).min_size(250.0).show_inside( 280 ui, 281 |ui| { 282 ui.heading("Disassembly Area"); 283 284 for (value, label) in [ 285 (DisassemblyArea::Sdram { cached: true }, "SDRAM (Cached)"), 286 (DisassemblyArea::Sdram { cached: false }, "SDRAM (Uncached)"), 287 (DisassemblyArea::CartridgeRom { cached: true }, "ROM (Cached)"), 288 (DisassemblyArea::CartridgeRom { cached: false }, "ROM (Uncached)"), 289 (DisassemblyArea::Cache, "CPU Cache"), 290 ] { 291 ui.radio_value(&mut state.disassembly_area, value, label); 292 } 293 294 ui.horizontal(|ui| { 295 let text_resp = ui 296 .add(TextEdit::singleline(&mut state.disassembly_address).desired_width(80.0)); 297 let button_resp = ui.button("Jump to address"); 298 299 let should_jump = button_resp.clicked() 300 || (text_resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter))); 301 if should_jump 302 && let Ok(address) = u32::from_str_radix(&state.disassembly_address, 16) 303 { 304 state.try_jump_to_address(address); 305 } 306 }); 307 308 ui.add_space(3.0); 309 310 if ui.button("Jump to PC").clicked() { 311 state.try_jump_to_address(sh2.pc()); 312 } 313 314 ui.separator(); 315 316 let registers = sh2.registers(); 317 Grid::new(format!("{window_title}_reg_grid")).num_columns(4).spacing([10.0, 3.0]).show( 318 ui, 319 |ui| { 320 for i in 0..8 { 321 for r in [i, i + 8] { 322 ui.label(format!("R{r}")); 323 ui.label(monospace_u32(registers.gpr[r])); 324 } 325 ui.end_row(); 326 } 327 328 ui.label("SR"); 329 ui.label(monospace_u32(registers.sr.into())); 330 ui.label("VBR"); 331 ui.label(monospace_u32(registers.vbr)); 332 ui.end_row(); 333 334 ui.label("GBR"); 335 ui.label(monospace_u32(registers.gbr)); 336 ui.label("PR"); 337 ui.label(monospace_u32(registers.pr)); 338 ui.end_row(); 339 340 ui.label("MACH"); 341 ui.label(monospace_u32(registers.mach)); 342 ui.label("MACL"); 343 ui.label(monospace_u32(registers.macl)); 344 ui.end_row(); 345 346 ui.label("PC"); 347 ui.label(monospace_u32(registers.pc)); 348 ui.end_row(); 349 }, 350 ); 351 352 ui.label( 353 RichText::new(format!( 354 "T={} S={} Q={} M={}", 355 u8::from(registers.sr.t), 356 u8::from(registers.sr.s), 357 u8::from(registers.sr.q), 358 u8::from(registers.sr.m) 359 )) 360 .monospace(), 361 ); 362 }, 363 ); 364} 365 366fn render_disasm_central_panel( 367 sh2: &Sh2, 368 debug_state: &mut GenesisDebugState, 369 state: &mut Sh2DebugWindowState, 370 break_status: Sh2BreakStatus, 371 ui: &mut Ui, 372) { 373 let Some(sega_32x) = &mut debug_state.sega_32x else { return }; 374 let cartridge = debug_state.cartridge.as_ref(); 375 376 let ctx = ui.ctx().clone(); 377 378 egui::CentralPanel::default().show_inside(ui, |ui| { 379 let disassembly_area = state.disassembly_area; 380 let address_range = disassembly_area.address_range(); 381 382 ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() }; 383 384 let mut table_builder = TableBuilder::new(ui) 385 .striped(true) 386 .column(Column::auto().at_least(10.0)) 387 .column(Column::auto().at_least(80.0)) 388 .column(Column::auto().at_least(150.0)) 389 .column(Column::remainder()) 390 .sense(egui::Sense::click()); 391 392 if let Some(scroll_to_row) = state.disassembly_scroll_row.take() { 393 table_builder = table_builder.scroll_to_row(scroll_to_row, Some(Align::Center)); 394 } else if crate::window_on_top(&ctx, state.which.disassembly_window_title()) { 395 let keys = crate::scroll_keys_pressed(&ctx); 396 if let Some(offset) = keys.relative_scroll_offset(state.disassembly_table_height) { 397 table_builder = 398 table_builder.vertical_scroll_offset(state.disassembly_table_offset + offset); 399 } 400 } 401 402 let sh2_pc = (if break_status.breaking { break_status.pc } else { sh2.pc() }) as usize; 403 let pc_row_index = 404 address_range.contains(&sh2_pc).then(|| (sh2_pc - address_range.start) / 2); 405 406 let mut disassembled = DisassembledInstruction::new(); 407 408 let highlight_color = crate::highlight_color(ctx.theme()); 409 410 let scroll_output = table_builder.body(|body| { 411 body.rows(15.0, (address_range.end - address_range.start) / 2, |mut row| { 412 let is_pc_row = pc_row_index == Some(row.index()); 413 let address = (address_range.start + 2 * row.index()) as u32; 414 415 row.set_selected(state.disassembly_selected_pcs.contains(address)); 416 417 row.col(|ui| { 418 state.breakpoints.render_clickable_widget( 419 address, 420 format!("{}_break_row_{address}", state.which.disassembly_window_title()), 421 ui, 422 ); 423 424 if is_pc_row { 425 ui.add(non_selectable_label( 426 RichText::new("→").monospace().color(highlight_color), 427 )); 428 } 429 }); 430 431 row.col(|ui| { 432 let mut text = monospace_u32(address); 433 if is_pc_row { 434 text = text.color(highlight_color); 435 } 436 437 ui.add(non_selectable_label(text)); 438 }); 439 440 let opcode = disassembly_area.read_address(address, sh2, sega_32x, cartridge); 441 sh2_emu::disassemble_into(address, opcode, &mut disassembled); 442 443 row.col(|ui| { 444 let mut text = RichText::new(&disassembled.text).monospace(); 445 if is_pc_row { 446 text = text.color(highlight_color); 447 } 448 449 ui.add(non_selectable_label(text)); 450 }); 451 452 row.col(|ui| { 453 let memory_access = disassembled.memory_read.or(disassembled.memory_write); 454 let Some((memory_access, size)) = memory_access else { return }; 455 456 let address = memory_access.resolve_address(size, sh2); 457 458 let value = DisassemblyArea::from_address(address).map(|area| { 459 let word = area.read_address(address, sh2, sega_32x, cartridge); 460 match size { 461 MemoryAccessSize::Byte => { 462 let byte = word.to_be_bytes()[(address & 1) as usize]; 463 format!("0x{byte:02X}") 464 } 465 MemoryAccessSize::Word => format!("0x{word:04X}"), 466 MemoryAccessSize::Longword => { 467 let low = area.read_address( 468 address.wrapping_add(2), 469 sh2, 470 sega_32x, 471 cartridge, 472 ); 473 let longword = u32::from(low) | (u32::from(word) << 16); 474 format!("0x{longword:08X}") 475 } 476 } 477 }); 478 479 let text = match (disassembled.memory_read_type, value) { 480 (ReadType::Load, Some(value)) => { 481 RichText::new(format!("; ${address:08X} = {value}")).monospace() 482 } 483 (ReadType::Load, None) | (ReadType::EffectiveAddress, _) => { 484 RichText::new(format!("; ${address:08X}")).monospace() 485 } 486 }; 487 ui.add(non_selectable_label(text)); 488 }); 489 490 if row.response().clicked() { 491 state 492 .disassembly_selected_pcs 493 .handle_click(address, ctx.input(|i| i.modifiers)); 494 } 495 }); 496 }); 497 state.disassembly_table_offset = scroll_output.state.offset.y; 498 state.disassembly_table_height = scroll_output.inner_rect.height(); 499 }); 500} 501 502pub fn render_breakpoints_window( 503 ctx: &egui::Context, 504 state: &mut Sh2DebugWindowState, 505 command_sender: &Sender<GenesisDebugCommand>, 506) { 507 let window_title = state.which.breakpoints_window_title(); 508 let response = 509 state.breakpoints.show_window(ctx, window_title, &mut state.breakpoints_open, |ui| { 510 state.interrupt_breakpoints.render(ui) 511 }); 512 if response == BreakpointWindowResponse::Changed { 513 let memory_breakpoints = state 514 .breakpoints 515 .breakpoints() 516 .iter() 517 .map(|breakpoint| Sh2Breakpoint { 518 start_address: breakpoint.start_address, 519 end_address: breakpoint.end_address, 520 read: breakpoint.read, 521 write: breakpoint.write, 522 execute: breakpoint.execute, 523 }) 524 .collect(); 525 526 let _ = command_sender.send(GenesisDebugCommand::UpdateSh2Breakpoints( 527 state.which, 528 Sh2Breakpoints { 529 memory: memory_breakpoints, 530 interrupt: state.interrupt_breakpoints.levels(), 531 }, 532 )); 533 } 534} 535 536fn monospace_u32(value: u32) -> RichText { 537 RichText::new(format!("{value:08X}")).monospace() 538}