lib.rsannotatedlib.rssource423 lines · 12.3 KB · raw
1pub mod gb;
2pub mod gba;
3pub mod genesis;
4mod memviewer;
5pub mod nes;
6pub mod pce;
7mod process;
8pub mod smsgg;
9pub mod snes;
10
11use egui::epaint::ImageDelta;
12use egui::{
13    Button, Color32, ColorImage, Id, ImageData, LayerId, Order, Response, ScrollArea,
14    TextureFilter, TextureOptions, TextureWrapMode, ThemePreference, Ui, Widget, WidgetText,
15};
16use egui_extras::{Column, TableBuilder};
17use egui_sdl3_wgpu::{FrameCreateError, FrameOptions, FrameRunEffect, FrameRunError};
18use jgenesis_common::frontend::Color;
19use jgenesis_native_config::EguiTheme;
20use jgenesis_renderer::config::RendererConfig;
21pub use process::{
22    DebugFn, DebugRenderFn, DebuggerMainProcess, DebuggerRunnerProcess, clone_debug_fn,
23    null_debug_fn, partial_clone_debug_fn,
24};
25use sdl3::VideoSubsystem;
26use sdl3::event::Event;
27use std::array;
28use std::collections::HashSet;
29use std::fmt::Debug;
30use std::hash::Hash;
31use std::sync::Arc;
32use std::time::Duration;
33use thiserror::Error;
34
35#[derive(Debug, Error)]
36pub enum DebuggerError {
37    #[error("Error creating egui/SDL3/wgpu frame: {0}")]
38    FrameCreate(#[from] FrameCreateError),
39    #[error("Error rendering debugger window: {0}")]
40    FrameRun(#[from] FrameRunError),
41}
42
43pub struct DebugRenderContext<'a> {
44    egui_ui: &'a mut Ui,
45    device: &'a wgpu::Device,
46    queue: &'a wgpu::Queue,
47    renderer: &'a mut egui_wgpu::Renderer,
48}
49
50pub struct DebuggerWindow {
51    frame: egui_sdl3_wgpu::Frame,
52    debugger_process: Box<dyn DebuggerMainProcess>,
53}
54
55impl DebuggerWindow {
56    /// # Errors
57    ///
58    /// Propagates any errors encountered while initializing the window or the wgpu renderer.
59    pub fn new(
60        video: &VideoSubsystem,
61        egui_theme: EguiTheme,
62        render_config: &RendererConfig,
63        debugger_process: Box<dyn DebuggerMainProcess>,
64    ) -> Result<Self, DebuggerError> {
65        let options = FrameOptions {
66            window_width: 925,
67            window_height: 790,
68            egui_theme: egui_theme_preference(egui_theme),
69            install_egui_image_loaders: true,
70            wgpu_backends: render_config.wgpu_backend.to_wgpu(),
71            wgpu_power_preference: render_config.wgpu_power_preference.to_wgpu(),
72            ..FrameOptions::default()
73        };
74
75        let frame = egui_sdl3_wgpu::Frame::new("Memory Viewer", video, options)?;
76
77        Ok(Self { frame, debugger_process })
78    }
79
80    /// Update internal state and render the debugger frontend.
81    ///
82    /// # Errors
83    ///
84    /// Propagates any errors encountered while rendering.
85    pub fn update(&mut self) -> Result<FrameRunEffect, DebuggerError> {
86        let effect = self.frame.run(&mut |ui, ctx| {
87            // Workaround for https://github.com/emilk/egui/issues/8092
88            #[cfg(debug_assertions)]
89            ui.global_style_mut(|style| style.debug.warn_if_rect_changes_id = false);
90
91            let debug_ctx = DebugRenderContext {
92                egui_ui: ui,
93                device: ctx.device,
94                queue: ctx.queue,
95                renderer: ctx.renderer,
96            };
97            if let Err(err) = self.debugger_process.run(debug_ctx) {
98                log::error!("Error rendering debugger window: {err}");
99            }
100
101            // Ensure window updates at 60 FPS minimum; displayed graphics can change as the
102            // emulator runs
103            ui.request_repaint_after(Duration::from_micros(16666));
104        })?;
105
106        Ok(effect)
107    }
108
109    pub fn update_egui_theme(&mut self, egui_theme: EguiTheme) {
110        self.frame.update_egui_theme(egui_theme_preference(egui_theme));
111    }
112
113    pub fn handle_sdl_event(&mut self, event: &Event) {
114        self.frame.handle_sdl_event(event);
115    }
116
117    pub fn window_id(&self) -> u32 {
118        self.frame.window_id()
119    }
120}
121
122fn screen_width(ctx: &egui::Context) -> f32 {
123    let window_margin = ctx.global_style().spacing.window_margin;
124    ctx.content_rect().width() - f32::from(window_margin.left) - f32::from(window_margin.right)
125}
126
127fn egui_theme_preference(egui_theme: EguiTheme) -> ThemePreference {
128    match egui_theme {
129        EguiTheme::SystemDefault => ThemePreference::System,
130        EguiTheme::Dark => ThemePreference::Dark,
131        EguiTheme::Light => ThemePreference::Light,
132    }
133}
134
135fn create_texture(
136    label: &str,
137    width: u32,
138    height: u32,
139    device: &wgpu::Device,
140    renderer: &mut egui_wgpu::Renderer,
141) -> (wgpu::Texture, egui::TextureId) {
142    let wgpu_texture = device.create_texture(&wgpu::TextureDescriptor {
143        label: Some(label),
144        size: wgpu::Extent3d { width, height, depth_or_array_layers: 1 },
145        mip_level_count: 1,
146        sample_count: 1,
147        dimension: wgpu::TextureDimension::D2,
148        format: wgpu::TextureFormat::Rgba8UnormSrgb,
149        usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
150        view_formats: &[],
151    });
152    let texture_view = wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
153
154    let egui_texture =
155        renderer.register_native_texture(device, &texture_view, wgpu::FilterMode::Nearest);
156
157    (wgpu_texture, egui_texture)
158}
159
160struct SelectableButton<'a, T> {
161    label: WidgetText,
162    current_value: &'a mut T,
163    alternative: T,
164}
165
166impl<'a, T> SelectableButton<'a, T> {
167    fn new(label: impl Into<WidgetText>, current_value: &'a mut T, alternative: T) -> Self {
168        Self { label: label.into(), current_value, alternative }
169    }
170}
171
172impl<T: Copy + PartialEq> Widget for SelectableButton<'_, T> {
173    fn ui(self, ui: &mut Ui) -> Response {
174        let response =
175            Button::new(self.label).selected(*self.current_value == self.alternative).ui(ui);
176        if response.clicked() {
177            *self.current_value = self.alternative;
178        }
179        response
180    }
181}
182
183fn write_textures(
184    wgpu_texture: &wgpu::Texture,
185    egui_texture: egui::TextureId,
186    data: &[u8],
187    ctx: &mut DebugRenderContext<'_>,
188) {
189    ctx.queue.write_texture(
190        wgpu::TexelCopyTextureInfo {
191            texture: wgpu_texture,
192            mip_level: 0,
193            origin: wgpu::Origin3d::ZERO,
194            aspect: wgpu::TextureAspect::All,
195        },
196        data,
197        wgpu::TexelCopyBufferLayout {
198            offset: 0,
199            bytes_per_row: Some(wgpu_texture.width() * 4),
200            rows_per_image: None,
201        },
202        wgpu_texture.size(),
203    );
204
205    let texture_view = wgpu_texture.create_view(&wgpu::TextureViewDescriptor::default());
206    ctx.renderer.update_egui_texture_from_wgpu_texture(
207        ctx.device,
208        &texture_view,
209        wgpu::FilterMode::Nearest,
210        egui_texture,
211    );
212}
213
214fn update_egui_texture(
215    ctx: &egui::Context,
216    size: [usize; 2],
217    image: &[Color],
218    texture: &mut Option<egui::TextureId>,
219) -> egui::TextureId {
220    let tex_manager = ctx.tex_manager();
221
222    match *texture {
223        Some(texture) => {
224            tex_manager.write().set(
225                texture,
226                ImageDelta {
227                    image: ImageData::Color(Arc::new(ColorImage::from_rgba_unmultiplied(
228                        size,
229                        bytemuck::cast_slice(image),
230                    ))),
231                    options: TextureOptions {
232                        magnification: TextureFilter::Nearest,
233                        minification: TextureFilter::Nearest,
234                        wrap_mode: TextureWrapMode::ClampToEdge,
235                        mipmap_mode: None,
236                    },
237                    pos: None,
238                },
239            );
240
241            texture
242        }
243        None => {
244            let id = tex_manager.write().alloc(
245                "cram_texture".into(),
246                ImageData::Color(Arc::new(ColorImage::from_rgba_unmultiplied(
247                    size,
248                    bytemuck::cast_slice(image),
249                ))),
250                TextureOptions {
251                    magnification: TextureFilter::Nearest,
252                    minification: TextureFilter::Nearest,
253                    wrap_mode: TextureWrapMode::ClampToEdge,
254                    mipmap_mode: None,
255                },
256            );
257            *texture = Some(id);
258
259            id
260        }
261    }
262}
263
264fn render_registers_window(
265    ctx: &egui::Context,
266    window_title: &str,
267    open: &mut bool,
268    render_registers: impl FnOnce(&mut Ui),
269) {
270    egui::Window::new(window_title)
271        .open(open)
272        .constrain(false)
273        .default_pos(rand_window_pos())
274        .show(ctx, |ui| {
275            ScrollArea::vertical().show(ui, |ui| {
276                brighten_faint_bg_color(ui);
277
278                render_registers(ui);
279            });
280        });
281}
282
283fn brighten_faint_bg_color(ui: &mut Ui) {
284    // Make stripes a little lighter
285    let color = ui.visuals_mut().faint_bg_color;
286    ui.visuals_mut().faint_bg_color = Color32::from_rgba_premultiplied(
287        color.r().saturating_add(5),
288        color.g().saturating_add(5),
289        color.b().saturating_add(5),
290        color.a(),
291    );
292}
293
294fn render_registers_table(ui: &mut Ui, register: &str, values: &[(&str, &str)]) {
295    TableBuilder::new(ui)
296        .id_salt(register)
297        .column(Column::exact(200.0))
298        .column(Column::exact(150.0))
299        .vscroll(false)
300        .striped(true)
301        .header(25.0, |mut header| {
302            header.col(|ui| {
303                ui.heading(register);
304            });
305        })
306        .body(|mut body| {
307            for &(field, value) in values {
308                body.row(17.0, |mut row| {
309                    row.col(|ui| {
310                        ui.label(field);
311                    });
312
313                    row.col(|ui| {
314                        ui.label(value);
315                    });
316                });
317            }
318        });
319}
320
321fn dump_registers_callback(ui: &mut Ui) -> impl FnMut(&str, &[(&str, &str)]) {
322    let mut first = true;
323
324    move |register, values| {
325        if !first {
326            ui.separator();
327        }
328        first = false;
329
330        render_registers_table(ui, register, values);
331    }
332}
333
334// When an egui Window is created with constrain=false, letting egui decide the default position can
335// cause the window to spawn offscreen. Instead, for windows that aren't initially open, spawn them
336// in a random position near-ish the top-left corner of the screen
337fn rand_window_pos() -> [f32; 2] {
338    array::from_fn(|_| 100.0 + rand::random_range(-50.0..=50.0))
339}
340
341fn move_to_top(ctx: &egui::Context, id: impl Hash) {
342    ctx.move_to_top(LayerId::new(Order::Middle, Id::new(id)));
343}
344
345fn window_on_top(ctx: &egui::Context, id: impl Hash) -> bool {
346    ctx.top_layer_id() == Some(LayerId::new(Order::Middle, Id::new(id)))
347}
348
349#[derive(Debug, Clone, Copy, PartialEq, Eq)]
350struct ScrollKeys {
351    up: bool,
352    down: bool,
353    page_up: bool,
354    page_down: bool,
355}
356
357impl ScrollKeys {
358    fn relative_scroll_offset(self, visible_height: f32) -> Option<f32> {
359        const ASSUMED_ROW_HEIGHT: f32 = 15.0;
360
361        if self.page_up && !self.page_down {
362            Some(-visible_height * 0.9)
363        } else if !self.page_up && self.page_down {
364            Some(visible_height * 0.9)
365        } else if self.up && !self.down {
366            Some(-ASSUMED_ROW_HEIGHT)
367        } else if !self.up && self.down {
368            Some(ASSUMED_ROW_HEIGHT)
369        } else {
370            None
371        }
372    }
373}
374
375fn scroll_keys_pressed(ctx: &egui::Context) -> ScrollKeys {
376    let [up, down, page_up, page_down] = ctx.input(|i| {
377        [egui::Key::ArrowUp, egui::Key::ArrowDown, egui::Key::PageUp, egui::Key::PageDown]
378            .map(|key| i.key_pressed(key))
379    });
380
381    ScrollKeys { up, down, page_up, page_down }
382}
383
384fn highlight_color(theme: egui::Theme) -> Color32 {
385    match theme {
386        egui::Theme::Dark => Color32::GREEN,
387        egui::Theme::Light => Color32::BLUE,
388    }
389}
390
391fn non_selectable_label(text: impl Into<WidgetText>) -> egui::Label {
392    egui::Label::new(text).selectable(false)
393}
394
395fn normalize_position(pos: egui::Pos2, interact_rect: egui::Rect) -> egui::Vec2 {
396    (pos - interact_rect.min) / interact_rect.size()
397}
398
399struct AddressSet<T>(HashSet<T>);
400
401impl<T: Copy + Eq + Hash> AddressSet<T> {
402    fn new() -> Self {
403        Self(HashSet::new())
404    }
405
406    fn contains(&self, value: T) -> bool {
407        self.0.contains(&value)
408    }
409
410    fn handle_click(&mut self, value: T, modifiers: egui::Modifiers) {
411        let existed = self.0.contains(&value);
412
413        if !modifiers.ctrl {
414            self.0.clear();
415        }
416
417        if !existed {
418            self.0.insert(value);
419        } else {
420            self.0.remove(&value);
421        }
422    }
423}