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}