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}