1mod m68kdebug;
2mod psgdebug;
3mod sh2debug;
4mod widgets;
5mod ym2612debug;
6mod z80debug;
7
8use crate::genesis::m68kdebug::{
9    Genesis68kMemoryMap, M68kBreakCommand, M68kDebugWindowState, Main68kInterruptBreakpoints,
10    SegaCdSubMemoryMap, Sub68kInterruptBreakpoints,
11};
12use crate::genesis::sh2debug::Sh2DebugWindowState;
13use crate::genesis::ym2612debug::Ym2612DebugWindowState;
14use crate::genesis::z80debug::{GenesisZ80MemoryMap, Z80BreakCommand, Z80DebugWindowState};
15use crate::memviewer::MemoryViewerState;
16use crate::process::{DebuggerProcesses, RunTillNextResult};
17use crate::{DebugRenderContext, DebuggerMainProcess, DebuggerRunnerProcess, memviewer};
18use egui::scroll_area::ScrollBarVisibility;
19use egui::{
20    CentralPanel, Color32, CornerRadius, Grid, Image, Panel, Popup, Pos2, Rect, ScrollArea, Sense,
21    Stroke, StrokeKind, Ui, UiKind, Vec2, Window,
22};
23use egui_extras::{Column, TableBuilder};
24use genesis_components::debug::{CopySpriteAttributesResult, CramEntry, SpriteAttributeEntry};
25use genesis_components::vdp::{ColorModifier, DataPortLocation};
26use genesis_config::GenesisInputs;
27use genesis_core::GenesisEmulator;
28use genesis_core::api::debug::{
29    GenesisDebugCommand, GenesisDebugState, GenesisDebugger, GenesisDebuggerHandle,
30    GenesisMemoryArea,
31};
32use jgenesis_common::debug::{DebugMemoryView, DebugViewWithWriteHook, Endian};
33use jgenesis_common::frontend::{
34    AudioOutput, Color, InputPoller, Renderer, SaveWriter, TickEffect,
35};
36use jgenesis_common::num::GetBit;
37use jgenesis_common::sync::{SharedVarReceiver, SharedVarSender};
38use s32x_core::WhichCpu;
39use s32x_core::api::debug::{S32XMemoryArea, Sega32XDebugState};
40use segacd_core::api::debug::SegaCdMemoryArea;
41use sh2_emu::{CacheMode, DmaAddressMode, DmaTransferUnit, Sh2};
42use std::collections::HashMap;
43use std::error::Error;
44use std::hash::Hash;
45use std::iter;
46
47const CRAM_WINDOW_TITLE: &str = "CRAM";
48const VRAM_WINDOW_TITLE: &str = "VRAM";
49const H_SCROLL_WINDOW_TITLE: &str = "H Scroll Table";
50const SPRITE_ATTRIBUTES_WINDOW_TITLE: &str = "Sprite Attribute Table";
51const S32X_PALETTE_WINDOW_TITLE: &str = "32X Palette RAM";
52
53const VDP_REGISTERS_WINDOW_TITLE: &str = "VDP Registers";
54const VDP_STATE_WINDOW_TITLE: &str = "VDP State";
55const S32X_SYSTEM_REGISTERS_WINDOW_TITLE: &str = "32X System Registers";
56const S32X_VDP_REGISTERS_WINDOW_TITLE: &str = "32X VDP Registers";
57const S32X_PWM_REGISTERS_WINDOW_TITLE: &str = "32X PWM Registers";
58
59const MASTER_SH2_REGISTERS_WINDOW_TITLE: &str = "Master SH-2 Registers";
60const SLAVE_SH2_REGISTERS_WINDOW_TITLE: &str = "Slave SH-2 Registers";
61
62const CRAM_ROWS: usize = 4;
63const CRAM_COLS: usize = 16;

Manually position open-by-default windows so they don't spawn over the top panel

66const CRAM_DEFAULT_POS: [f32; 2] = [16.0, 38.0];
67const VRAM_DEFAULT_POS: [f32; 2] = [16.0, 327.0];
69#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
70pub(crate) enum MemoryArea {
71    Genesis(GenesisMemoryArea),
72    SegaCd(SegaCdMemoryArea),
73    Sega32X(S32XMemoryArea),
74}
75
76impl MemoryArea {
77    const ALL: &'static [Self] = &[
78        Self::Genesis(GenesisMemoryArea::CartridgeRom),
79        Self::Genesis(GenesisMemoryArea::WorkingRam),
80        Self::Genesis(GenesisMemoryArea::AudioRam),
81        Self::Genesis(GenesisMemoryArea::Vram),
82        Self::Genesis(GenesisMemoryArea::Cram),
83        Self::Genesis(GenesisMemoryArea::Vsram),
84        Self::SegaCd(SegaCdMemoryArea::BiosRom),
85        Self::SegaCd(SegaCdMemoryArea::PrgRam),
86        Self::SegaCd(SegaCdMemoryArea::WordRam),
87        Self::SegaCd(SegaCdMemoryArea::PcmRam),
88        Self::SegaCd(SegaCdMemoryArea::CdcRam),
89        Self::Sega32X(S32XMemoryArea::Sdram),
90        Self::Sega32X(S32XMemoryArea::MasterSh2Cache),
91        Self::Sega32X(S32XMemoryArea::SlaveSh2Cache),
92        Self::Sega32X(S32XMemoryArea::FrameBuffer0),
93        Self::Sega32X(S32XMemoryArea::FrameBuffer1),
94        Self::Sega32X(S32XMemoryArea::PaletteRam),
95    ];
96
97    fn name(self) -> &'static str {
98        match self {
99            Self::Genesis(area) => match area {
100                GenesisMemoryArea::CartridgeRom => "Cartridge ROM",
101                GenesisMemoryArea::WorkingRam => "Working RAM",
102                GenesisMemoryArea::AudioRam => "Audio RAM",
103                GenesisMemoryArea::Vram => "VRAM",
104                GenesisMemoryArea::Cram => "CRAM",
105                GenesisMemoryArea::Vsram => "VSRAM",
106            },
107            Self::SegaCd(area) => match area {
108                SegaCdMemoryArea::BiosRom => "BIOS ROM",
109                SegaCdMemoryArea::PrgRam => "PRG RAM",
110                SegaCdMemoryArea::WordRam => "Word RAM",
111                SegaCdMemoryArea::PcmRam => "PCM Waveform RAM",
112                SegaCdMemoryArea::CdcRam => "CDC Buffer RAM",
113            },
114            Self::Sega32X(area) => match area {
115                S32XMemoryArea::Sdram => "32X SDRAM",
116                S32XMemoryArea::MasterSh2Cache => "Master SH-2 Cache",
117                S32XMemoryArea::SlaveSh2Cache => "Slave SH-2 Cache",
118                S32XMemoryArea::FrameBuffer0 => "32X Frame Buffer 0",
119                S32XMemoryArea::FrameBuffer1 => "32X Frame Buffer 1",
120                S32XMemoryArea::PaletteRam => "32X Palette RAM",
121            },
122        }
123    }
124
125    fn default_file_name(self) -> &'static str {
126        match self {
127            Self::Genesis(area) => match area {
128                GenesisMemoryArea::CartridgeRom => "rom.bin",
129                GenesisMemoryArea::WorkingRam => "wram.bin",
130                GenesisMemoryArea::AudioRam => "audioram.bin",
131                GenesisMemoryArea::Vram => "vram.bin",
132                GenesisMemoryArea::Cram => "cram.bin",
133                GenesisMemoryArea::Vsram => "vsram.bin",
134            },
135            Self::SegaCd(area) => match area {
136                SegaCdMemoryArea::BiosRom => "bios.bin",
137                SegaCdMemoryArea::PrgRam => "prgram.bin",
138                SegaCdMemoryArea::WordRam => "wordram.bin",
139                SegaCdMemoryArea::PcmRam => "pcmram.bin",
140                SegaCdMemoryArea::CdcRam => "cdcram.bin",
141            },
142            Self::Sega32X(area) => match area {
143                S32XMemoryArea::Sdram => "sdram.bin",
144                S32XMemoryArea::MasterSh2Cache => "mcache.bin",
145                S32XMemoryArea::SlaveSh2Cache => "scache.bin",
146                S32XMemoryArea::FrameBuffer0 => "fb0.bin",
147                S32XMemoryArea::FrameBuffer1 => "fb1.bin",
148                S32XMemoryArea::PaletteRam => "paletteram.bin",
149            },
150        }
151    }
152
153    fn new_states() -> HashMap<Self, MemoryViewerState> {
154        Self::ALL
155            .iter()
156            .map(|&area| {
157                (
158                    area,
159                    MemoryViewerState::new(area.name(), Endian::Big)
160                        .with_default_file_name(area.default_file_name().into())
161                        .with_editable(),
162                )
163            })
164            .collect()
165    }
166}
167
168struct CramWindowState {
169    open: bool,
170    modifier: ColorModifier,
171    selected_color: usize,
172    popup_contains_pointer: bool,
173    entry_buffer: Box<[CramEntry; CRAM_ROWS * CRAM_COLS]>,
174    color_buffer: Box<[Color; CRAM_ROWS * CRAM_COLS]>,
175    texture: Option<egui::TextureId>,
176}
177
178impl CramWindowState {
179    fn new() -> Self {
180        Self {
181            open: true,
182            modifier: ColorModifier::None,
183            selected_color: 0,
184            popup_contains_pointer: false,
185            entry_buffer: vec![CramEntry::default(); CRAM_ROWS * CRAM_COLS]
186                .into_boxed_slice()
187                .try_into()
188                .unwrap(),
189            color_buffer: vec![Color::default(); CRAM_ROWS * CRAM_COLS]
190                .into_boxed_slice()
191                .try_into()
192                .unwrap(),
193            texture: None,
194        }
195    }
196}
197
198struct VramWindowState {
199    open: bool,
200    palette: u8,
201    buffer: Box<[Color; 2048 * 64]>,
202    texture: Option<egui::TextureId>,
203}
204
205impl VramWindowState {
206    fn new() -> Self {
207        Self {
208            open: true,
209            palette: 0,
210            buffer: vec![Color::default(); 2048 * 64].into_boxed_slice().try_into().unwrap(),
211            texture: None,
212        }
213    }
214}
215
216struct HScrollWindowState {
217    open: bool,
218    buffer: Box<[(u16, u16); 256]>,
219}
220
221impl HScrollWindowState {
222    fn new() -> Self {
223        Self { open: false, buffer: vec![(0, 0); 256].into_boxed_slice().try_into().unwrap() }
224    }
225}
226
227struct SpriteAttributesWindowState {
228    open: bool,
229    buffer: Box<[SpriteAttributeEntry; 80]>,
230    adjust_coordinates: bool,
231}
232
233impl SpriteAttributesWindowState {
234    fn new() -> Self {
235        Self {
236            open: false,
237            buffer: vec![SpriteAttributeEntry::default(); 80]
238                .into_boxed_slice()
239                .try_into()
240                .unwrap(),
241            adjust_coordinates: false,
242        }
243    }
244}
245
246struct S32XPaletteRamState {
247    open: bool,
248    selected_color: usize,
249    popup_contains_pointer: bool,
250    entry_buffer: Box<[CramEntry; 256]>,
251    color_buffer: Box<[Color; 256]>,
252    texture: Option<egui::TextureId>,
253}
254
255impl S32XPaletteRamState {
256    fn new() -> Self {
257        Self {
258            open: false,
259            selected_color: 0,
260            popup_contains_pointer: false,
261            entry_buffer: vec![CramEntry::default(); 256].into_boxed_slice().try_into().unwrap(),
262            color_buffer: vec![Color::default(); 256].into_boxed_slice().try_into().unwrap(),
263            texture: None,
264        }
265    }
266}
267
268struct State {
269    memory_viewers: HashMap<MemoryArea, MemoryViewerState>,
270    memory_edit_hook: Box<dyn FnMut(MemoryArea, usize, u8)>,
271    cram: CramWindowState,
272    vram: VramWindowState,
273    h_scroll: HScrollWindowState,
274    sprite_attributes: SpriteAttributesWindowState,
275    s32x_palette: S32XPaletteRamState,
276    z80: Z80DebugWindowState,
277    m68k: M68kDebugWindowState,
278    m68k_sub: M68kDebugWindowState,
279    sh2_master: Sh2DebugWindowState,
280    sh2_slave: Sh2DebugWindowState,
281    vdp_registers_open: bool,
282    vdp_state_open: bool,
283    ym2612: Ym2612DebugWindowState,
284    psg_open: bool,
285    master_sh2_registers_open: bool,
286    slave_sh2_registers_open: bool,
287    s32x_system_registers_open: bool,
288    s32x_vdp_registers_open: bool,
289    s32x_pwm_registers_open: bool,
290}
291
292impl State {
293    fn new(memory_edit_hook: Box<dyn FnMut(MemoryArea, usize, u8)>) -> Self {
294        Self {
295            memory_viewers: MemoryArea::new_states(),
296            memory_edit_hook,
297            cram: CramWindowState::new(),
298            vram: VramWindowState::new(),
299            h_scroll: HScrollWindowState::new(),
300            sprite_attributes: SpriteAttributesWindowState::new(),
301            s32x_palette: S32XPaletteRamState::new(),
302            z80: Z80DebugWindowState::new(),
303            m68k: M68kDebugWindowState::new_default_titles()
304                .with_interrupt_breakpoints(Box::new(Main68kInterruptBreakpoints::default())),
305            m68k_sub: M68kDebugWindowState::new_with_titles(
306                "Sub 68000 Disassembly",
307                "Sub 68000 Breakpoints",
308            )
309            .with_interrupt_breakpoints(Box::new(Sub68kInterruptBreakpoints::default())),
310            sh2_master: Sh2DebugWindowState::new(WhichCpu::Master),
311            sh2_slave: Sh2DebugWindowState::new(WhichCpu::Slave),
312            vdp_registers_open: false,
313            vdp_state_open: false,
314            ym2612: Ym2612DebugWindowState::new(),
315            psg_open: false,
316            master_sh2_registers_open: false,
317            slave_sh2_registers_open: false,
318            s32x_system_registers_open: false,
319            s32x_vdp_registers_open: false,
320            s32x_pwm_registers_open: false,
321        }
322    }
323}
324
325pub(crate) struct GenesisDebuggerFrontendState<'a> {
326    state: &'a mut GenesisDebugState,
327    debugger_handle: &'a GenesisDebuggerHandle,
328}
329
330impl GenesisDebuggerFrontendState<'_> {
331    fn has_memory(&self, memory_area: MemoryArea) -> bool {
332        match memory_area {
333            MemoryArea::Genesis(GenesisMemoryArea::CartridgeRom) => self.state.cartridge.is_some(),
334            MemoryArea::Genesis(_) => true,
335            MemoryArea::SegaCd(_) => self.state.sega_cd.is_some(),
336            MemoryArea::Sega32X(_) => self.state.sega_32x.is_some(),
337        }
338    }
339
340    fn debug_memory_view(
341        &mut self,
342        memory_area: MemoryArea,
343    ) -> Option<Box<dyn DebugMemoryView + '_>> {
344        match memory_area {
345            MemoryArea::Genesis(memory_area) => Some(self.state.memory_view(memory_area)),
346            MemoryArea::SegaCd(memory_area) => {
347                self.state.sega_cd.as_mut().map(|sega_cd| sega_cd.scd_memory_view(memory_area))
348            }
349            MemoryArea::Sega32X(memory_area) => {
350                self.state.sega_32x.as_mut().map(|sega_32x| sega_32x.s32x_memory_view(memory_area))
351            }
352        }
353    }
354}
355
356pub(crate) type GenesisDebugRenderFn =
357    dyn FnMut(DebugRenderContext<'_>, GenesisDebuggerFrontendState<'_>);
358
359pub(crate) fn render_fn(
360    memory_edit_hook: Box<dyn FnMut(MemoryArea, usize, u8)>,
361) -> Box<GenesisDebugRenderFn> {
362    let mut state = State::new(memory_edit_hook);
363    Box::new(move |ctx, mut emu_state| render(ctx, &mut emu_state, &mut state))
364}
365
366fn render(
367    ctx: DebugRenderContext<'_>,
368    debug_state: &mut GenesisDebuggerFrontendState<'_>,
369    state: &mut State,
370) {
371    Panel::top("gen_debug_top").show_inside(ctx.egui_ui, |ui| {
372        egui::MenuBar::new().ui(ui, |ui| {
373            ui.menu_button("Memory Viewers", |ui| {
374                for &memory_area in MemoryArea::ALL {
375                    if !debug_state.has_memory(memory_area) {
376                        continue;
377                    }
378
379                    if ui.button(memory_area.name()).clicked() {
380                        if let Some(memviewer_state) = state.memory_viewers.get_mut(&memory_area) {
381                            memviewer_state.open_window(ui);
382                        }
383                        ui.close_kind(UiKind::Menu);
384                    }
385                }
386            });
387
388            ui.menu_button("Register Viewers", |ui| {
389                if ui.button("VDP Registers").clicked() {
390                    state.vdp_registers_open = true;
391                    crate::move_to_top(ui, VDP_REGISTERS_WINDOW_TITLE);
392                    ui.close_kind(UiKind::Menu);
393                }
394
395                if ui.button("VDP State").clicked() {
396                    state.vdp_state_open = true;
397                    crate::move_to_top(ui, VDP_STATE_WINDOW_TITLE);
398                    ui.close_kind(UiKind::Menu);
399                }
400
401                if ui.button("YM2612").clicked() {
402                    state.ym2612.open_window(ui);
403                    ui.close_kind(UiKind::Menu);
404                }
405
406                if ui.button("SN76489").clicked() {
407                    state.psg_open = true;
408                    crate::move_to_top(ui, psgdebug::WINDOW_TITLE);
409                    ui.close_kind(UiKind::Menu);
410                }
411
412                if debug_state.state.sega_32x.is_some() {
413                    if ui.button("Master SH-2 Registers").clicked() {
414                        state.master_sh2_registers_open = true;
415                        crate::move_to_top(ui, MASTER_SH2_REGISTERS_WINDOW_TITLE);
416                        ui.close_kind(UiKind::Menu);
417                    }
418
419                    if ui.button("Slave SH-2 Registers").clicked() {
420                        state.slave_sh2_registers_open = true;
421                        crate::move_to_top(ui, SLAVE_SH2_REGISTERS_WINDOW_TITLE);
422                        ui.close_kind(UiKind::Menu);
423                    }
424
425                    if ui.button("32X System Registers").clicked() {
426                        state.s32x_system_registers_open = true;
427                        crate::move_to_top(ui, S32X_SYSTEM_REGISTERS_WINDOW_TITLE);
428                        ui.close_kind(UiKind::Menu);
429                    }
430
431                    if ui.button("32X VDP").clicked() {
432                        state.s32x_vdp_registers_open = true;
433                        crate::move_to_top(ui, S32X_VDP_REGISTERS_WINDOW_TITLE);
434                        ui.close_kind(UiKind::Menu);
435                    }
436
437                    if ui.button("32X PWM").clicked() {
438                        state.s32x_pwm_registers_open = true;
439                        crate::move_to_top(ui, S32X_PWM_REGISTERS_WINDOW_TITLE);
440                        ui.close_kind(UiKind::Menu);
441                    }
442                }
443            });
444
445            ui.menu_button("Video Memory", |ui| {
446                if ui.button("CRAM").clicked() {
447                    state.cram.open = true;
448                    crate::move_to_top(ui, CRAM_WINDOW_TITLE);
449                    ui.close_kind(UiKind::Menu);
450                }
451
452                if ui.button("VRAM").clicked() {
453                    state.vram.open = true;
454                    crate::move_to_top(ui, VRAM_WINDOW_TITLE);
455                    ui.close_kind(UiKind::Menu);
456                }
457
458                if ui.button("Sprite Attributes").clicked() {
459                    state.sprite_attributes.open = true;
460                    crate::move_to_top(ui, SPRITE_ATTRIBUTES_WINDOW_TITLE);
461                    ui.close_kind(UiKind::Menu);
462                }
463
464                if ui.button("H Scroll Table").clicked() {
465                    state.h_scroll.open = true;
466                    crate::move_to_top(ui, H_SCROLL_WINDOW_TITLE);
467                    ui.close_kind(UiKind::Menu);
468                }
469
470                if debug_state.state.sega_32x.is_some() && ui.button("32X Palette RAM").clicked() {
471                    state.s32x_palette.open = true;
472                    crate::move_to_top(ui, S32X_PALETTE_WINDOW_TITLE);
473                    ui.close_kind(UiKind::Menu);
474                }
475            });
476
477            ui.menu_button("CPU Debuggers", |ui| {
478                if ui.button("68000").clicked() {
479                    state.m68k.open_disassembly_window(ui);
480                    ui.close_kind(UiKind::Menu);
481                }
482
483                if debug_state.state.sega_cd.is_some() && ui.button("Sub 68000").clicked() {
484                    state.m68k_sub.open_disassembly_window(ui);
485                    ui.close_kind(UiKind::Menu);
486                }
487
488                if ui.button("Z80").clicked() {
489                    state.z80.open_disassembly_window(ui);
490                    ui.close_kind(UiKind::Menu);
491                }
492
493                if debug_state.state.sega_32x.is_some() {
494                    if ui.button("SH-2 Master").clicked() {
495                        state.sh2_master.open_disassembly_window(ui);
496                        ui.close_kind(UiKind::Menu);
497                    }
498
499                    if ui.button("SH-2 Slave").clicked() {
500                        state.sh2_slave.open_disassembly_window(ui);
501                        ui.close_kind(UiKind::Menu);
502                    }
503                }
504            });
505        });
506    });
507
508    render_memory_viewer_windows(
509        ctx.egui_ui,
510        debug_state,
511        &mut state.memory_viewers,
512        &mut state.memory_edit_hook,
513    );
514
515    render_vdp_registers_window(ctx.egui_ui, debug_state, &mut state.vdp_registers_open);
516
517    render_vdp_state_window(ctx.egui_ui, debug_state, &mut state.vdp_state_open);
518
519    ym2612debug::render_debug_window(
520        ctx.egui_ui,
521        debug_state.state.ym2612.debug_view(),
522        &mut state.ym2612,
523    );
524
525    psgdebug::render_debug_window(ctx.egui_ui, &debug_state.state.psg, &mut state.psg_open);
526
527    let screen_width = crate::screen_width(ctx.egui_ui);
528
529    render_cram_window(ctx.egui_ui, screen_width, debug_state, &mut state.cram);
530    render_vram_window(ctx.egui_ui, screen_width, debug_state, &mut state.vram);
531    render_h_scroll_window(ctx.egui_ui, debug_state, &mut state.h_scroll);
532    render_sprite_attributes_window(ctx.egui_ui, debug_state, &mut state.sprite_attributes);
533
534    render_m68k_debug_windows(ctx.egui_ui, debug_state, state);
535    render_z80_debug_windows(ctx.egui_ui, debug_state, state);
536
537    if let Some(sega_32x) = &mut debug_state.state.sega_32x {
538        render_sh2_registers_window(
539            ctx.egui_ui,
540            &mut sega_32x.sh2_master,
541            MASTER_SH2_REGISTERS_WINDOW_TITLE,
542            &mut state.master_sh2_registers_open,
543        );
544        render_sh2_registers_window(
545            ctx.egui_ui,
546            &mut sega_32x.sh2_slave,
547            SLAVE_SH2_REGISTERS_WINDOW_TITLE,
548            &mut state.slave_sh2_registers_open,
549        );
550
551        render_32x_palette_window(ctx.egui_ui, sega_32x, &mut state.s32x_palette);
552        render_32x_system_registers_window(
553            ctx.egui_ui,
554            sega_32x,
555            &mut state.s32x_system_registers_open,
556        );
557        render_32x_vdp_registers_window(ctx.egui_ui, sega_32x, &mut state.s32x_vdp_registers_open);
558        render_32x_pwm_registers_window(ctx.egui_ui, sega_32x, &mut state.s32x_pwm_registers_open);
559
560        let debugger_handle = debug_state.debugger_handle;
561        let break_status = debugger_handle.sh2_break_status();
562
563        sh2debug::render_disassembly_window(
564            ctx.egui_ui,
565            debug_state.state,
566            &mut state.sh2_master,
567            &debugger_handle.command_sender,
568            break_status.get(WhichCpu::Master),
569        );
570        sh2debug::render_disassembly_window(
571            ctx.egui_ui,
572            debug_state.state,
573            &mut state.sh2_slave,
574            &debugger_handle.command_sender,
575            break_status.get(WhichCpu::Slave),
576        );
577
578        sh2debug::render_breakpoints_window(
579            ctx.egui_ui,
580            &mut state.sh2_master,
581            &debugger_handle.command_sender,
582        );
583        sh2debug::render_breakpoints_window(
584            ctx.egui_ui,
585            &mut state.sh2_slave,
586            &debugger_handle.command_sender,
587        );
588    }
589}
590
591fn render_m68k_debug_windows(
592    ctx: &egui::Context,
593    debug_state: &mut GenesisDebuggerFrontendState<'_>,
594    state: &mut State,
595) {
596    let debugger_handle = debug_state.debugger_handle;
597
598    {
599        let main_memory_map = Genesis68kMemoryMap::new(debug_state.state);
600
601        m68kdebug::render_disassembly_window(
602            ctx,
603            &debug_state.state.m68k,
604            &main_memory_map,
605            &mut state.m68k,
606            debugger_handle.m68k_break_status(),
607            Some(|command| {
608                let genesis_cmd = match command {
609                    M68kBreakCommand::Pause => GenesisDebugCommand::BreakPause68k,
610                    M68kBreakCommand::Resume => GenesisDebugCommand::BreakResume,
611                    M68kBreakCommand::Step => GenesisDebugCommand::BreakStep68k,
612                };
613                let _ = debugger_handle.send_command(genesis_cmd);
614            }),
615        );
616
617        m68kdebug::render_breakpoints_window(ctx, &mut state.m68k, |breakpoints| {
618            let _ = debugger_handle
619                .send_command(GenesisDebugCommand::Update68kBreakpoints(breakpoints));
620        });
621    }
622
623    if let Some(sega_cd) = &mut debug_state.state.sega_cd {
624        let sub_memory_map = SegaCdSubMemoryMap::new(sega_cd);
625        let sub_cpu = sega_cd.sub_cpu();
626        m68kdebug::render_disassembly_window(
627            ctx,
628            sub_cpu,
629            &sub_memory_map,
630            &mut state.m68k_sub,
631            debugger_handle.sub_cpu_break_status(),
632            Some(|command| {
633                let scd_command = match command {
634                    M68kBreakCommand::Resume => GenesisDebugCommand::BreakResume,
635                    M68kBreakCommand::Pause => GenesisDebugCommand::BreakPauseSub68k,
636                    M68kBreakCommand::Step => GenesisDebugCommand::BreakStepSub68k,
637                };
638                let _ = debugger_handle.send_command(scd_command);
639            }),
640        );
641
642        m68kdebug::render_breakpoints_window(ctx, &mut state.m68k_sub, |breakpoints| {
643            let _ = debugger_handle
644                .send_command(GenesisDebugCommand::UpdateSub68kBreakpoints(breakpoints));
645        });
646    }
647}
648
649fn render_z80_debug_windows(
650    ctx: &egui::Context,
651    debug_state: &mut GenesisDebuggerFrontendState<'_>,
652    state: &mut State,
653) {
654    let memory_map = Genesis68kMemoryMap::new(debug_state.state);
655
656    let debugger_handle = debug_state.debugger_handle;
657
658    z80debug::render_disassembly_window(
659        ctx,
660        &debug_state.state.z80,
661        GenesisZ80MemoryMap::new(debug_state.state, &memory_map),
662        &mut state.z80,
663        debugger_handle.z80_break_status(),
664        Some(|command| {
665            let genesis_command = match command {
666                Z80BreakCommand::Pause => GenesisDebugCommand::BreakPauseZ80,
667                Z80BreakCommand::Resume => GenesisDebugCommand::BreakResume,
668                Z80BreakCommand::Step => GenesisDebugCommand::BreakStepZ80,
669            };
670            let _ = debugger_handle.send_command(genesis_command);
671        }),
672    );
673
674    z80debug::render_breakpoints_window(ctx, &mut state.z80, |breakpoints| {
675        let _ =
676            debugger_handle.send_command(GenesisDebugCommand::UpdateZ80Breakpoints(breakpoints));
677    });
678}
679
680fn render_memory_viewer_windows(
681    egui_ctx: &egui::Context,
682    emu_state: &mut GenesisDebuggerFrontendState<'_>,
683    memory_viewer_states: &mut HashMap<MemoryArea, MemoryViewerState>,
684    memory_edit_hook: &mut dyn FnMut(MemoryArea, usize, u8),
685) {
686    for (&memory_area, state) in memory_viewer_states.iter_mut() {
687        if let Some(memory) = emu_state.debug_memory_view(memory_area) {
688            let mut memory = DebugViewWithWriteHook::new(
689                memory,
690                Box::new(|address, value| memory_edit_hook(memory_area, address, value)),
691            );
692            memviewer::render(egui_ctx, &mut memory, state);
693        }
694    }
695}
696
697fn render_color_info_grid(
698    id: impl Hash,
699    ui: &mut Ui,
700    palette: usize,
701    color_idx: usize,
702    entry: CramEntry,
703) {
704    Grid::new(id).show(ui, |ui| {
705        ui.label("Palette");
706        ui.label(palette.to_string());
707        ui.end_row();
708
709        ui.label("Index");
710        ui.label(color_idx.to_string());
711        ui.end_row();
712
713        ui.label("Color");
714        ui.label(format!("0x{:03X}", entry.value));
715        ui.end_row();
716
717        ui.label("R / G / B");
718        ui.label(format!(
719            "{} / {} / {}",
720            (entry.value >> 1) & 7,
721            (entry.value >> 5) & 7,
722            (entry.value >> 9) & 7
723        ));
724        ui.end_row();
725
726        ui.label("Display");
727        ui.label(format_display_color(entry.color));
728        ui.end_row();
729    });
730}
731
732fn format_display_color(color: Color) -> String {
733    format!("#{:02X}{:02X}{:02X}", color.r, color.g, color.b)
734}
735
736fn render_cram_window(
737    ctx: &egui::Context,
738    screen_width: f32,
739    emu_state: &mut GenesisDebuggerFrontendState<'_>,
740    state: &mut CramWindowState,
741) {
742    Window::new(CRAM_WINDOW_TITLE)
743        .open(&mut state.open)
744        .constrain(false)
745        .default_pos(CRAM_DEFAULT_POS)
746        .default_size([screen_width * 0.95, 225.0])
747        .show(ctx, |ui| {
748            emu_state.state.vdp.copy_cram(state.entry_buffer.as_mut_slice(), state.modifier);
749            for (entry, color) in
750                iter::zip(state.entry_buffer.iter(), state.color_buffer.iter_mut())
751            {
752                *color = entry.color;
753            }
754
755            Panel::right("cram_right_panel").resizable(false).min_size(125.0).show_inside(
756                ui,
757                |ui| {
758                    let cram_entry = state.entry_buffer[state.selected_color];
759
760                    paint_rect_and_advance_cursor(
761                        ui,
762                        ui.max_rect().min,
763                        Vec2::new(50.0, 50.0),
764                        cram_entry.color,
765                    );
766
767                    render_color_info_grid(
768                        "cram_side_panel_grid",
769                        ui,
770                        state.selected_color / 16,
771                        state.selected_color % 16,
772                        cram_entry,
773                    );
774                },
775            );
776
777            CentralPanel::default().show_inside(ui, |ui| {
778                let modifier_resp = ui.horizontal(|ui| {
779                    ui.radio_value(&mut state.modifier, ColorModifier::None, "Normal");
780                    ui.radio_value(&mut state.modifier, ColorModifier::Shadow, "Shadowed");
781                    ui.radio_value(&mut state.modifier, ColorModifier::Highlight, "Highlighted");
782                });
783
784                let mut image_size =
785                    ui.max_rect().size() - Vec2::new(0.0, modifier_resp.response.rect.height());
786                if image_size.y > image_size.x * 0.25 {
787                    image_size.y = image_size.x * 0.25;
788                } else {
789                    image_size.x = image_size.y * 4.0;
790                }
791
792                let texture = crate::update_egui_texture(
793                    ctx,
794                    [CRAM_COLS, CRAM_ROWS],
795                    state.color_buffer.as_slice(),
796                    &mut state.texture,
797                );
798                let image_resp = ui.add(Image::new((texture, image_size)).sense(Sense::click()));
799
800                let mut clicked = image_resp.clicked();
801                let contains_pointer =
802                    image_resp.contains_pointer() || state.popup_contains_pointer;
803                if (clicked || contains_pointer)
804                    && let Some(pos) = ui.input(|i| i.pointer.latest_pos())
805                {
806                    let normalized_pos = crate::normalize_position(pos, image_resp.interact_rect);
807                    let row = (normalized_pos.y * (CRAM_ROWS as f32)) as usize;
808                    let col = (normalized_pos.x * (CRAM_COLS as f32)) as usize;
809
810                    if row < CRAM_ROWS && col < CRAM_COLS {
811                        let buffer_idx = CRAM_COLS * row + col;
812
813                        if contains_pointer {
814                            let cram_entry = state.entry_buffer[buffer_idx];
815
816                            let popup_resp = Popup::from_response(&image_resp)
817                                .at_pointer()
818                                .gap(5.0)
819                                .show(|ui| {
820                                    paint_rect_and_advance_cursor(
821                                        ui,
822                                        ui.max_rect().min + Vec2::new(35.0, 0.0),
823                                        Vec2::new(25.0, 25.0),
824                                        cram_entry.color,
825                                    );
826
827                                    render_color_info_grid(
828                                        "cram_popup_grid",
829                                        ui,
830                                        row,
831                                        col,
832                                        cram_entry,
833                                    );
834                                });
835                            clicked |=
836                                popup_resp.as_ref().is_some_and(|resp| resp.response.clicked());
837                            state.popup_contains_pointer =
838                                popup_resp.is_some_and(|resp| resp.response.contains_pointer());
839                        }
840
841                        if clicked {
842                            state.selected_color = buffer_idx;
843                        }
844                    }
845                }
846            });
847        });
848}
849
850fn paint_rect_and_advance_cursor(ui: &mut Ui, min: Pos2, size: Vec2, fill_color: Color) {
851    let rect = Rect { min, max: min + size };
852    ui.painter().rect(
853        rect,
854        CornerRadius::ZERO,
855        Color32::from_rgb(fill_color.r, fill_color.g, fill_color.b),
856        Stroke::new(1.0_f32, ui.visuals().text_color()),
857        StrokeKind::Middle,
858    );
859    ui.advance_cursor_after_rect(rect);
860}
861
862fn render_vram_window(
863    ctx: &egui::Context,
864    screen_width: f32,
865    emu_state: &mut GenesisDebuggerFrontendState<'_>,
866    state: &mut VramWindowState,
867) {
868    Window::new(VRAM_WINDOW_TITLE)
869        .open(&mut state.open)
870        .constrain(false)
871        .default_pos(VRAM_DEFAULT_POS)
872        .default_width(screen_width * 0.95)
873        .show(ctx, |ui| {
874            ui.horizontal(|ui| {
875                ui.label("Palette");
876
877                for i in 0..4 {
878                    ui.radio_value(&mut state.palette, i, format!("{i}"));
879                }
880            });
881
882            emu_state.state.vdp.copy_vram(state.buffer.as_mut_slice(), state.palette, 64);
883
884            let mut height = ui.available_width() * 0.45;
885            if height > ui.available_height() {
886                height = ui.available_height();
887            }
888            let width = height * 2.0;
889
890            let texture = crate::update_egui_texture(
891                ctx,
892                [64 * 8, 32 * 8],
893                state.buffer.as_slice(),
894                &mut state.texture,
895            );
896            ui.image((texture, Vec2::new(width, height)));
897        });
898}
899
900fn render_h_scroll_window(
901    ctx: &egui::Context,
902    emu_state: &mut GenesisDebuggerFrontendState<'_>,
903    state: &mut HScrollWindowState,
904) {
905    Window::new(H_SCROLL_WINDOW_TITLE)
906        .open(&mut state.open)
907        .constrain(false)
908        .default_pos(crate::rand_window_pos())
909        .default_width(200.0)
910        .show(ctx, |ui| {
911            emu_state.state.vdp.copy_h_scroll(state.buffer.as_mut_slice());
912
913            crate::brighten_faint_bg_color(ui);
914
915            TableBuilder::new(ui)
916                .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
917                .column(Column::auto().at_least(50.0))
918                .columns(Column::auto(), 2)
919                .column(Column::remainder())
920                .striped(true)
921                .header(20.0, |mut header| {
922                    header.col(|ui| {
923                        ui.heading("Line");
924                    });
925                    header.col(|ui| {
926                        ui.heading("Plane A");
927                    });
928                    header.col(|ui| {
929                        ui.heading("Plane B");
930                    });
931                    header.col(|_ui| {});
932                })
933                .body(|body| {
934                    body.rows(18.0, 256, |mut row| {
935                        let line = row.index();
936                        let (h_scroll_a, h_scroll_b) = state.buffer[line];
937
938                        row.col(|ui| {
939                            ui.label(line.to_string());
940                        });
941                        row.col(|ui| {
942                            ui.label(h_scroll_a.to_string());
943                        });
944                        row.col(|ui| {
945                            ui.label(h_scroll_b.to_string());
946                        });
947                        row.col(|_ui| {});
948                    });
949                });
950        });
951}
952
953fn render_sprite_attributes_window(
954    ctx: &egui::Context,
955    emu_state: &mut GenesisDebuggerFrontendState<'_>,
956    state: &mut SpriteAttributesWindowState,
957) {
958    Window::new(SPRITE_ATTRIBUTES_WINDOW_TITLE)
959        .open(&mut state.open)
960        .constrain(false)
961        .default_pos(crate::rand_window_pos())
962        .default_width(500.0)
963        .show(ctx, |ui| {
964            let CopySpriteAttributesResult { sprite_table_len, top_left_x, top_left_y } =
965                emu_state.state.vdp.copy_sprite_attributes(state.buffer.as_mut_slice());
966
967            ui.checkbox(&mut state.adjust_coordinates, "Shift coordinates to top-left of screen");
968
969            let (x_offset, y_offset) = if state.adjust_coordinates {
970                (-i32::from(top_left_x), -i32::from(top_left_y))
971            } else {
972                (0, 0)
973            };
974
975            crate::brighten_faint_bg_color(ui);
976
977            TableBuilder::new(ui)
978                .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible)
979                .columns(Column::auto().at_least(50.0), 11)
980                .column(Column::remainder())
981                .striped(true)
982                .header(20.0, |mut header| {
983                    for heading in [
984                        "Index",
985                        "Tile",
986                        "X",
987                        "Y",
988                        "Cell Size",
989                        "Palette",
990                        "Priority",
991                        "H Flip",
992                        "V Flip",
993                        "Link",
994                    ] {
995                        header.col(|ui| {
996                            ui.heading(heading);
997                        });
998                    }
999                    header.col(|_ui| {});
1000                })
1001                .body(|body| {
1002                    body.rows(18.0, sprite_table_len as usize, |mut row| {
1003                        let idx = row.index();
1004                        let sprite = state.buffer[idx];
1005
1006                        for value in [
1007                            idx.to_string(),
1008                            sprite.tile_number.to_string(),
1009                            (i32::from(sprite.x) + x_offset).to_string(),
1010                            (i32::from(sprite.y) + y_offset).to_string(),
1011                            format!("{}x{}", sprite.h_cells, sprite.v_cells),
1012                            sprite.palette.to_string(),
1013                            u8::from(sprite.priority).to_string(),
1014                            u8::from(sprite.h_flip).to_string(),
1015                            u8::from(sprite.v_flip).to_string(),
1016                            sprite.link.to_string(),
1017                        ] {
1018                            row.col(|ui| {
1019                                ui.label(value);
1020                            });
1021                        }
1022                        row.col(|_ui| {});
1023                    });
1024                });
1025        });
1026}
1027
1028fn render_32x_color_grid(id: impl Hash, ui: &mut Ui, index: usize, entry: CramEntry) {
1029    Grid::new(id).show(ui, |ui| {
1030        ui.label("Index");
1031        ui.label(format!("0x{index:02X} ({index})"));
1032        ui.end_row();
1033
1034        ui.label("Color");
1035        ui.label(format!("0x{:04X}", entry.value));
1036        ui.end_row();
1037
1038        let fmt_color = |component: u16| format!("0x{component:02X} ({component})");
1039
1040        ui.label("Red");
1041        ui.label(fmt_color(entry.value & 0x1F));
1042        ui.end_row();
1043
1044        ui.label("Green");
1045        ui.label(fmt_color((entry.value >> 5) & 0x1F));
1046        ui.end_row();
1047
1048        ui.label("Blue");
1049        ui.label(fmt_color((entry.value >> 10) & 0x1F));
1050        ui.end_row();
1051
1052        ui.label("Priority");
1053        ui.label(if entry.value.bit(15) { "1" } else { "0" });
1054        ui.end_row();
1055
1056        ui.label("Display");
1057        ui.label(format_display_color(entry.color));
1058        ui.end_row();
1059    });
1060}
1061
1062fn render_sh2_registers_window(
1063    ctx: &egui::Context,
1064    sh2: &mut Sh2,
1065    window_title: &str,
1066    open: &mut bool,
1067) {
1068    // Roughly 23.01 MHz
1069    const SH2_CLOCK_RATE: f64 = genesis_components::NTSC_GENESIS_MCLK_FREQUENCY
1070        / (genesis_config::NATIVE_M68K_DIVIDER as f64)
1071        * (genesis_config::NATIVE_SH2_MULTIPLIER as f64);
1072
1073    Window::new(window_title)
1074        .open(open)
1075        .constrain(false)
1076        .default_pos(crate::rand_window_pos())
1077        .show(ctx, |ui| {
1078            ScrollArea::vertical().show(ui, |ui| {
1079                let state = sh2.sh7604_debug_state();
1080
1081                crate::render_registers_table(
1082                    ui,
1083                    "Cache Control",
1084                    &[
1085                        ("Enabled", &state.cache.enabled.to_string()),
1086                        (
1087                            "Instruction replacement",
1088                            &state.cache.instruction_replacement_enabled.to_string(),
1089                        ),
1090                        ("Data replacement", &state.cache.data_replacement_enabled.to_string()),
1091                        ("Mode", cache_mode_str(state.cache.mode)),
1092                    ],
1093                );
1094
1095                ui.separator();
1096
1097                crate::render_registers_table(
1098                    ui,
1099                    "Internal Interrupts",
1100                    &[
1101                        ("WDT interrupt level", &state.interrupts.wdt_priority.to_string()),
1102                        ("WDT vector number", &state.interrupts.wdt_vector.to_string()),
1103                        ("DMAC interrupt level", &state.interrupts.dmac_priority.to_string()),
1104                        ("DMA0 vector number", &state.interrupts.dma0_vector.to_string()),
1105                        ("DMA1 vector number", &state.interrupts.dma1_vector.to_string()),
1106                        ("SCI interrupt level", &state.interrupts.sci_priority.to_string()),
1107                        ("SCI vector number", &state.interrupts.sci_vector.to_string()),
1108                    ],
1109                );
1110
1111                ui.separator();
1112
1113                let wdt_frequency = SH2_CLOCK_RATE / (state.wdt.system_clock_divider as f64);
1114
1115                crate::render_registers_table(
1116                    ui,
1117                    "Watchdog Timer (WDT)",
1118                    &[
1119                        ("Enabled", &state.wdt.enabled.to_string()),
1120                        ("Clock divider", &format!("{}", state.wdt.system_clock_divider)),
1121                        ("Frequency", &format!("{} Hz", wdt_frequency.round())),
1122                        ("Counter", &state.wdt.counter.to_string()),
1123                        ("Overflow flag", &state.wdt.overflow_flag.to_string()),
1124                    ],
1125                );
1126
1127                for (i, channel) in state.dmac.channels.iter().enumerate() {
1128                    ui.separator();
1129
1130                    crate::render_registers_table(
1131                        ui,
1132                        &format!("DMA Channel {i} (DMAC)"),
1133                        &[
1134                            (
1135                                "DMA master enabled",
1136                                &state.dmac.operation.dma_master_enabled.to_string(),
1137                            ),
1138                            ("Channel enabled", &channel.control.dma_enabled.to_string()),
1139                            ("Auto request", &channel.control.auto_request.to_string()),
1140                            ("Transfer unit", transfer_unit_str(channel.control.transfer_size)),
1141                            (
1142                                "Source address mode",
1143                                dma_address_mode_str(channel.control.source_address_mode),
1144                            ),
1145                            (
1146                                "Destination address mode",
1147                                dma_address_mode_str(channel.control.destination_address_mode),
1148                            ),
1149                            ("Source address", &format!("${:08X}", channel.source_address)),
1150                            (
1151                                "Destination address",
1152                                &format!("${:08X}", channel.destination_address),
1153                            ),
1154                            ("Length", &format!("0x{:08X}", channel.transfer_count)),
1155                            ("Interrupt enabled", &channel.control.interrupt_enabled.to_string()),
1156                            ("DMA complete flag", &channel.control.dma_complete.to_string()),
1157                        ],
1158                    );
1159                }
1160            });
1161        });
1162}
1163
1164fn cache_mode_str(mode: CacheMode) -> &'static str {
1165    match mode {
1166        CacheMode::FourWay => "4-way",
1167        CacheMode::TwoWay => "2-way",
1168    }
1169}
1170
1171fn transfer_unit_str(unit: DmaTransferUnit) -> &'static str {
1172    match unit {
1173        DmaTransferUnit::Byte => "Byte",
1174        DmaTransferUnit::Word => "Word",
1175        DmaTransferUnit::Longword => "Longword",
1176        DmaTransferUnit::SixteenByte => "16-byte",
1177    }
1178}
1179
1180fn dma_address_mode_str(mode: DmaAddressMode) -> &'static str {
1181    match mode {
1182        DmaAddressMode::Fixed => "Fixed",
1183        DmaAddressMode::AutoIncrement => "Increment",
1184        DmaAddressMode::AutoDecrement => "Decrement",
1185        DmaAddressMode::Invalid => "Invalid",
1186    }
1187}
1188
1189fn render_32x_palette_window(
1190    ctx: &egui::Context,
1191    emu_state: &mut Sega32XDebugState,
1192    state: &mut S32XPaletteRamState,
1193) {
1194    Window::new(S32X_PALETTE_WINDOW_TITLE)
1195        .open(&mut state.open)
1196        .constrain(false)
1197        .default_pos(crate::rand_window_pos())
1198        .default_size([700.0, 550.0])
1199        .show(ctx, |ui| {
1200            emu_state.copy_palette(state.entry_buffer.as_mut_slice());
1201            for (entry, color) in
1202                iter::zip(state.entry_buffer.iter(), state.color_buffer.iter_mut())
1203            {
1204                *color = entry.color;
1205            }
1206
1207            Panel::right("32x_palette_side_panel").resizable(false).min_size(135.0).show_inside(
1208                ui,
1209                |ui| {
1210                    let selected_entry = state.entry_buffer[state.selected_color];
1211
1212                    paint_rect_and_advance_cursor(
1213                        ui,
1214                        ui.max_rect().min,
1215                        Vec2::new(50.0, 50.0),
1216                        selected_entry.color,
1217                    );
1218
1219                    render_32x_color_grid(
1220                        "32x_palette_side_panel_grid",
1221                        ui,
1222                        state.selected_color,
1223                        selected_entry,
1224                    );
1225                },
1226            );
1227
1228            CentralPanel::default().show_inside(ui, |ui| {
1229                let mut image_size = ui.max_rect().size();
1230                if image_size.y > image_size.x {
1231                    image_size.y = image_size.x;
1232                } else {
1233                    image_size.x = image_size.y;
1234                }
1235
1236                let texture = crate::update_egui_texture(
1237                    ctx,
1238                    [16, 16],
1239                    state.color_buffer.as_slice(),
1240                    &mut state.texture,
1241                );
1242                let image_resp = ui.add(Image::new((texture, image_size)).sense(Sense::click()));
1243
1244                let mut clicked = image_resp.clicked();
1245                let contains_pointer =
1246                    image_resp.contains_pointer() || state.popup_contains_pointer;
1247                if (clicked || contains_pointer)
1248                    && let Some(pos) = ui.input(|i| i.pointer.latest_pos())
1249                {
1250                    let normalized_pos = crate::normalize_position(pos, image_resp.interact_rect);
1251                    let row = (normalized_pos.y * 16.0) as usize;
1252                    let col = (normalized_pos.x * 16.0) as usize;
1253
1254                    if row < 16 && col < 16 {
1255                        let buffer_idx = 16 * row + col;
1256
1257                        if contains_pointer {
1258                            let entry = state.entry_buffer[buffer_idx];
1259
1260                            let popup_resp = Popup::from_response(&image_resp)
1261                                .at_pointer()
1262                                .gap(5.0)
1263                                .show(|ui| {
1264                                    paint_rect_and_advance_cursor(
1265                                        ui,
1266                                        ui.max_rect().min + Vec2::new(45.0, 0.0),
1267                                        Vec2::new(25.0, 25.0),
1268                                        entry.color,
1269                                    );
1270
1271                                    render_32x_color_grid(
1272                                        "32x_palette_popup_color_grid",
1273                                        ui,
1274                                        buffer_idx,
1275                                        entry,
1276                                    );
1277                                });
1278                            clicked |=
1279                                popup_resp.as_ref().is_some_and(|resp| resp.response.clicked());
1280                            state.popup_contains_pointer =
1281                                popup_resp.is_some_and(|resp| resp.response.contains_pointer());
1282                        }
1283
1284                        if clicked {
1285                            state.selected_color = buffer_idx;
1286                        }
1287                    }
1288                }
1289            });
1290        });
1291}
1292
1293fn render_vdp_registers_window(
1294    ctx: &egui::Context,
1295    emu_state: &mut GenesisDebuggerFrontendState<'_>,
1296    open: &mut bool,
1297) {
1298    crate::render_registers_window(ctx, "VDP Registers", open, |ui| {
1299        emu_state.state.vdp.dump_registers(crate::dump_registers_callback(ui));
1300    });
1301}
1302
1303fn render_vdp_state_window(
1304    ctx: &egui::Context,
1305    emu_state: &mut GenesisDebuggerFrontendState<'_>,
1306    open: &mut bool,
1307) {
1308    Window::new(VDP_STATE_WINDOW_TITLE)
1309        .open(open)
1310        .constrain(false)
1311        .default_pos(crate::rand_window_pos())
1312        .show(ctx, |ui| {
1313            ScrollArea::vertical().show(ui, |ui| {
1314                let state = emu_state.state.vdp.internal_state();
1315
1316                crate::render_registers_table(
1317                    ui,
1318                    "Timing",
1319                    &[
1320                        ("Scanline", &state.scanline.to_string()),
1321                        ("Scanline MCLK cycles", &state.scanline_mclk.to_string()),
1322                        ("Scanline pixel", &state.pixel.to_string()),
1323                        (
1324                            "Active display pixel range",
1325                            &format!(
1326                                "{}-{}",
1327                                state.active_display_pixels.start,
1328                                state.active_display_pixels.end - 1
1329                            ),
1330                        ),
1331                        ("VBlank flag", ["0", "1"][usize::from(state.vblank_flag)]),
1332                        ("H (Internal)", &format!("0x{:03X}", state.internal_h)),
1333                        ("H (External)", &format!("0x{:02X}", state.external_h)),
1334                        ("V", &format!("0x{:02X}", state.v)),
1335                    ],
1336                );
1337
1338                ui.separator();
1339
1340                crate::render_registers_table(
1341                    ui,
1342                    "Interrupts",
1343                    &[
1344                        (
1345                            "Vertical interrupt pending",
1346                            ["0", "1"][usize::from(state.v_interrupt_pending)],
1347                        ),
1348                        (
1349                            "Horizontal interrupt pending",
1350                            ["0", "1"][usize::from(state.h_interrupt_pending)],
1351                        ),
1352                        ("Horizontal interrupt counter", &state.h_interrupt_counter.to_string()),
1353                    ],
1354                );
1355
1356                ui.separator();
1357
1358                crate::render_registers_table(
1359                    ui,
1360                    "Control Port",
1361                    &[
1362                        ("Mode", &format!("{:?}", state.data_port_mode)),
1363                        ("Location", data_port_location_str(state.data_port_location)),
1364                        ("Address", &format!("${:05X}", state.data_port_address)),
1365                        ("Write flag", &format!("{:?}", state.write_flag)),
1366                        ("DMA active", ["0", "1"][usize::from(state.dma_active)]),
1367                    ],
1368                );
1369            });
1370        });
1371}
1372
1373fn data_port_location_str(location: DataPortLocation) -> &'static str {
1374    match location {
1375        DataPortLocation::Vram => "VRAM",
1376        DataPortLocation::Vram8Bit => "VRAM (8-bit)",
1377        DataPortLocation::Cram => "CRAM",
1378        DataPortLocation::Vsram => "VSRAM",
1379        DataPortLocation::Invalid => "Invalid",
1380    }
1381}
1382
1383fn render_32x_system_registers_window(
1384    ctx: &egui::Context,
1385    emu_state: &mut Sega32XDebugState,
1386    open: &mut bool,
1387) {
1388    crate::render_registers_window(ctx, "32X System Registers", open, |ui| {
1389        emu_state.dump_32x_system_registers(crate::dump_registers_callback(ui));
1390    });
1391}
1392
1393fn render_32x_vdp_registers_window(
1394    ctx: &egui::Context,
1395    emu_state: &mut Sega32XDebugState,
1396    open: &mut bool,
1397) {
1398    crate::render_registers_window(ctx, "32X VDP Registers", open, |ui| {
1399        emu_state.dump_32x_vdp_registers(crate::dump_registers_callback(ui));
1400    });
1401}
1402
1403fn render_32x_pwm_registers_window(
1404    ctx: &egui::Context,
1405    emu_state: &mut Sega32XDebugState,
1406    open: &mut bool,
1407) {
1408    crate::render_registers_window(ctx, "32X PWM Registers", open, |ui| {
1409        emu_state.dump_pwm_registers(crate::dump_registers_callback(ui));
1410    });
1411}
1412
1413struct GenesisDebugRunnerProcess {
1414    state_sender: SharedVarSender<GenesisDebugState>,
1415    debugger: GenesisDebugger,
1416}
1417
1418impl<R, A, I, S> DebuggerRunnerProcess<GenesisEmulator, R, A, I, S> for GenesisDebugRunnerProcess
1419where
1420    R: Renderer,
1421    A: AudioOutput,
1422    I: InputPoller<GenesisInputs>,
1423    S: SaveWriter,
1424{
1425    fn run(
1426        &mut self,
1427        emulator: &mut GenesisEmulator,
1428    ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
1429        let mut debug_view = emulator.as_debug_view();
1430        self.debugger.process_commands(&mut debug_view);
1431        self.state_sender.update(debug_view.to_debug_state());
1432
1433        Ok(())
1434    }
1435
1436    fn run_emulator_till_next_frame(
1437        &mut self,
1438        emulator: &mut GenesisEmulator,
1439        renderer: &mut R,
1440        audio_output: &mut A,
1441        input_poller: &mut I,
1442        save_writer: &mut S,
1443    ) -> RunTillNextResult<GenesisEmulator, R::Err, A::Err, S::Err> {
1444        while emulator.debug_tick(
1445            renderer,
1446            audio_output,
1447            input_poller,
1448            save_writer,
1449            &mut self.debugger,
1450        )? != TickEffect::FrameRendered
1451        {}
1452
1453        Ok(())
1454    }
1455}
1456
1457struct GenesisDebugMainProcess {
1458    debugger_handle: GenesisDebuggerHandle,
1459    state_receiver: SharedVarReceiver<GenesisDebugState>,
1460    render_fn: Box<GenesisDebugRenderFn>,
1461}
1462
1463impl DebuggerMainProcess for GenesisDebugMainProcess {
1464    fn run(
1465        &mut self,
1466        ctx: DebugRenderContext<'_>,
1467    ) -> Result<(), Box<dyn Error + Send + Sync + 'static>> {
1468        let Some(state) = self.state_receiver.get() else { return Ok(()) };
1469        (self.render_fn)(
1470            ctx,
1471            GenesisDebuggerFrontendState { state, debugger_handle: &self.debugger_handle },
1472        );
1473
1474        Ok(())
1475    }
1476}
1477
1478#[must_use]
1479pub fn genesis_debug_fn<R, A, I, S>() -> DebuggerProcesses<GenesisEmulator, R, A, I, S>
1480where
1481    R: Renderer,
1482    A: AudioOutput,
1483    I: InputPoller<GenesisInputs>,
1484    S: SaveWriter,
1485{
1486    let (state_sender, state_receiver) = jgenesis_common::sync::new_shared_var();
1487    let (debugger, debugger_handle) = GenesisDebugger::new(state_sender.clone());
1488
1489    let memory_edit_hook = {
1490        let command_sender = debugger_handle.command_sender.clone();
1491
1492        Box::new(move |memory_area, address, value| {
1493            if let MemoryArea::Genesis(memory_area) = memory_area {
1494                let _ = command_sender.send(GenesisDebugCommand::EditMemory(
1495                    memory_area,
1496                    address,
1497                    value,
1498                ));
1499            }
1500        })
1501    };
1502
1503    let runner_process = GenesisDebugRunnerProcess { state_sender, debugger };
1504    let main_process = GenesisDebugMainProcess {
1505        debugger_handle,
1506        state_receiver,
1507        render_fn: render_fn(memory_edit_hook),
1508    };
1509
1510    (Box::new(runner_process), Box::new(main_process))
1511}