1use egui::scroll_area::ScrollBarVisibility; 2use egui::style::ScrollStyle; 3use egui::{ 4 Align, CentralPanel, Color32, Context, FontId, Id, LayerId, Order, Panel, RichText, TextEdit, 5 Ui, Window, 6}; 7use egui_extras::{Column, TableBuilder}; 8use jgenesis_common::debug::{DebugMemoryView, Endian}; 9use rfd::FileDialog; 10use std::fs::File; 11use std::io::BufWriter; 12use std::path::Path; 13use std::{array, cmp, io}; 14 15const MONOSPACE: FontId = FontId::monospace(12.0); 16 17#[derive(Debug, Clone, Copy, PartialEq, Eq)] 18pub enum MemoryViewerColumns { 19 Byte, 20 Word, 21 Longword, 22} 23 24impl MemoryViewerColumns { 25 fn num_columns(self) -> usize { 26 match self { 27 Self::Byte => 16, 28 Self::Word => 8, 29 Self::Longword => 4, 30 } 31 } 32 33 fn column_width(self) -> f32 { 34 match self { 35 Self::Byte => 15.0, 36 Self::Word => 35.0, 37 Self::Longword => 80.0, 38 } 39 } 40} 41 42#[derive(Debug, Clone)] 43pub struct MemoryViewerState { 44 pub window_title: String, 45 pub default_file_name: Option<String>, 46 pub endian: Endian, 47 pub columns: MemoryViewerColumns, 48 pub open: bool, 49 pub goto_text: String, 50 pub goto_address: Option<usize>, 51 pub highlight_address: Option<usize>, 52 pub set_address_text: String, 53 pub table_offset: f32, 54 pub table_height: f32, 55 pub value_text: String, 56 pub set_invalid: bool, 57 pub editable: bool, 58} 59 60impl MemoryViewerState { 61 pub fn new(name: &str, endian: Endian) -> Self { 62 Self { 63 window_title: format!("{name} Viewer"), 64 default_file_name: None, 65 endian, 66 columns: MemoryViewerColumns::Word, 67 open: false, 68 goto_text: String::new(), 69 goto_address: None, 70 highlight_address: None, 71 set_address_text: String::new(), 72 table_offset: 0.0, 73 table_height: 1.0, 74 value_text: String::new(), 75 set_invalid: false, 76 editable: false, 77 } 78 } 79 80 pub fn with_default_file_name(mut self, name: String) -> Self { 81 self.default_file_name = Some(name); 82 self 83 } 84 85 pub fn with_editable(mut self) -> Self { 86 self.editable = true; 87 self 88 } 89 90 pub fn open_window(&mut self, ctx: &Context) { 91 self.open = true; 92 ctx.move_to_top(LayerId::new(Order::Middle, Id::new(&self.window_title))); 93 } 94} 95 96pub fn render(ctx: &Context, memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) { 97 let memory_len = memory.len(); 98 if memory_len == 0 { 99 return; 100 } 101 102 let mut open = state.open; 103 104 Window::new(&state.window_title) 105 .open(&mut open) 106 .constrain(false) 107 .default_pos([50.0, crate::rand_window_pos()[1]]) 108 .default_width(800.0) 109 .show(ctx, |ui| { 110 render_right_panel(memory, state, ui); 111 render_central_panel(memory, state, memory_len, ui); 112 }); 113 114 state.open = open; 115} 116 117fn render_right_panel( 118 memory: &mut dyn DebugMemoryView, 119 state: &mut MemoryViewerState, 120 ui: &mut Ui, 121) { 122 Panel::right(format!("{}_right", state.window_title)).resizable(false).show_inside(ui, |ui| { 123 ui.heading("Go to address"); 124 125 ui.horizontal(|ui| { 126 let resp = ui.add( 127 TextEdit::singleline(&mut state.goto_text).desired_width(70.0).font(MONOSPACE), 128 ); 129 let enter_pressed = resp.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)); 130 131 let goto_address = ui.button("Go").clicked() || enter_pressed; 132 133 if goto_address && let Ok(address) = usize::from_str_radix(&state.goto_text, 16) { 134 state.goto_address = Some(address); 135 } 136 }); 137 138 if state.editable { 139 ui.add_space(20.0); 140 141 ui.heading("Edit memory"); 142 143 ui.horizontal(|ui| { 144 ui.label("Address"); 145 146 ui.add( 147 TextEdit::singleline(&mut state.set_address_text) 148 .desired_width(70.0) 149 .font(MONOSPACE), 150 ); 151 }); 152 153 ui.horizontal(|ui| { 154 ui.label("Value"); 155 156 ui.add( 157 TextEdit::singleline(&mut state.value_text).desired_width(70.0).font(MONOSPACE), 158 ); 159 }); 160 161 if ui.button("Set byte (8-bit)").clicked() { 162 try_set_byte(memory, state); 163 } 164 165 if ui.button("Set word (16-bit)").clicked() { 166 try_set_word(memory, state); 167 } 168 169 if ui.button("Set longword (32-bit)").clicked() { 170 try_set_longword(memory, state); 171 } 172 173 if state.set_invalid { 174 ui.colored_label(Color32::RED, "Invalid address or value"); 175 } 176 } 177 178 ui.add_space(20.0); 179 180 ui.heading("Column size"); 181 ui.radio_value(&mut state.columns, MemoryViewerColumns::Byte, "Byte (8-bit)"); 182 ui.radio_value(&mut state.columns, MemoryViewerColumns::Word, "Word (16-bit)"); 183 ui.radio_value(&mut state.columns, MemoryViewerColumns::Longword, "Longword (32-bit)"); 184 185 ui.add_space(20.0); 186 187 ui.heading("Endianness"); 188 ui.radio_value(&mut state.endian, Endian::Big, "Big-endian"); 189 ui.radio_value(&mut state.endian, Endian::Little, "Little-endian"); 190 191 ui.add_space(20.0); 192 193 if ui.button("Export to file...").clicked() { 194 export_to_file(memory, state); 195 } 196 }); 197} 198 199fn render_central_panel( 200 memory: &mut dyn DebugMemoryView, 201 state: &mut MemoryViewerState, 202 memory_len: usize, 203 ui: &mut Ui, 204) { 205 const ROW_HEIGHT: f32 = 15.0; 206 207 let ctx = ui.ctx().clone(); 208 209 let highlight_color = crate::highlight_color(ctx.theme()); 210 211 CentralPanel::default().show_inside(ui, |ui| { 212 ui.spacing_mut().scroll = ScrollStyle { bar_width: 10.0, ..ScrollStyle::solid() }; 213 214 let table_rows = memory_len / 16 + usize::from(!memory_len.is_multiple_of(16)); 215 let address_len = format!("{:X}", memory_len - 1).len(); 216 217 let num_columns = state.columns.num_columns(); 218 let column_width = state.columns.column_width(); 219 220 let mut builder = TableBuilder::new(ui) 221 .scroll_bar_visibility(ScrollBarVisibility::AlwaysVisible) 222 .column(Column::auto().at_least(60.0)) // Address 223 .columns(Column::auto().at_least(column_width), num_columns) // Byte/word/longword values 224 .column(Column::exact(10.0)) // Spacing 225 .column(Column::auto().at_least(125.0)) // ASCII representation 226 .column(Column::remainder().at_least(10.0)); // Spacing 227 228 if let Some(address) = state.goto_address.take() { 229 builder = 230 builder.scroll_to_row(cmp::min(table_rows - 1, address / 16), Some(Align::Center)); 231 state.highlight_address = Some(address); 232 } else if crate::window_on_top(&ctx, &state.window_title) { 233 let keys = crate::scroll_keys_pressed(&ctx); 234 if let Some(offset) = keys.relative_scroll_offset(state.table_height) { 235 builder = builder.vertical_scroll_offset(state.table_offset + offset); 236 } 237 } 238 239 let scroll_output = builder.body(|body| { 240 body.rows(ROW_HEIGHT, table_rows, |mut row| { 241 let address = 16 * row.index(); 242 let data: [_; 16] = array::from_fn(|i| memory.read(address + i)); 243 244 row.col(|ui| { 245 ui.label(RichText::new(fmt_address(address, address_len)).monospace()); 246 }); 247 248 match state.columns { 249 MemoryViewerColumns::Byte => { 250 for (i, byte) in data.into_iter().enumerate() { 251 if address + i < memory_len { 252 row.col(|ui| { 253 let mut text = RichText::new(format!("{byte:02X}")).monospace(); 254 if state.highlight_address == Some(address + i) { 255 text = text.color(highlight_color); 256 } 257 ui.label(text.monospace()); 258 }); 259 } 260 } 261 } 262 MemoryViewerColumns::Word => { 263 let to_bytes = match state.endian { 264 Endian::Big => u16::from_be_bytes, 265 Endian::Little => u16::from_le_bytes, 266 }; 267 268 for (i, &chunk) in data.as_chunks::<2>().0.iter().enumerate() { 269 if address + 2 * i + 1 < memory_len { 270 let word = to_bytes(chunk); 271 272 row.col(|ui| { 273 let mut text = RichText::new(format!("{word:04X}")).monospace(); 274 if state 275 .highlight_address 276 .is_some_and(|highlight| highlight & !1 == address + 2 * i) 277 { 278 text = text.color(highlight_color); 279 } 280 ui.label(text); 281 }); 282 } 283 } 284 } 285 MemoryViewerColumns::Longword => { 286 let to_bytes = match state.endian { 287 Endian::Big => u32::from_be_bytes, 288 Endian::Little => u32::from_le_bytes, 289 }; 290 291 for (i, &chunk) in data.as_chunks::<4>().0.iter().enumerate() { 292 if address + 4 * i + 3 < memory_len { 293 let longword = to_bytes(chunk); 294 295 row.col(|ui| { 296 let mut text = 297 RichText::new(format!("{longword:08X}")).monospace(); 298 if state 299 .highlight_address 300 .is_some_and(|highlight| highlight & !3 == address + 4 * i) 301 { 302 text = text.color(highlight_color); 303 } 304 ui.label(text); 305 }); 306 } 307 } 308 } 309 } 310 311 // Empty column for spacing 312 row.col(|_ui| {}); 313 314 row.col(|ui| { 315 let ascii_text = data 316 .into_iter() 317 .map(|byte| { 318 if byte.is_ascii() && !byte.is_ascii_control() { 319 byte as char 320 } else { 321 '.' 322 } 323 }) 324 .collect::<String>(); 325 326 ui.label(RichText::new(ascii_text).monospace()); 327 }); 328 329 // Hack to make scroll bar not overlap the rightmost data column 330 row.col(|_ui| {}); 331 }); 332 }); 333 state.table_offset = scroll_output.state.offset.y; 334 state.table_height = scroll_output.inner_rect.height(); 335 }); 336} 337 338fn fmt_address(address: usize, len: usize) -> String { 339 use std::fmt::Write; 340 341 let mut s = String::with_capacity(len); 342 let _ = write!(s, "{address:X}"); 343 while s.len() < len { 344 s.insert(0, '0'); 345 } 346 347 s 348} 349 350fn try_set_byte(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) { 351 let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else { 352 state.set_invalid = true; 353 return; 354 }; 355 356 let Ok(byte) = u8::from_str_radix(&state.value_text, 16) else { 357 state.set_invalid = true; 358 return; 359 }; 360 361 memory.write(address, byte); 362 363 state.goto_address = Some(address); 364 state.set_invalid = false; 365} 366 367fn try_set_word(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) { 368 let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else { 369 state.set_invalid = true; 370 return; 371 }; 372 373 let Ok(word) = u16::from_str_radix(&state.value_text, 16) else { 374 state.set_invalid = true; 375 return; 376 }; 377 378 let [first, second] = match state.endian { 379 Endian::Big => word.to_be_bytes(), 380 Endian::Little => word.to_le_bytes(), 381 }; 382 383 memory.write(address & !1, first); 384 memory.write(address | 1, second); 385 386 state.goto_address = Some(address); 387 state.set_invalid = false; 388} 389 390fn try_set_longword(memory: &mut dyn DebugMemoryView, state: &mut MemoryViewerState) { 391 let Ok(address) = usize::from_str_radix(&state.set_address_text, 16) else { 392 state.set_invalid = true; 393 return; 394 }; 395 396 let Ok(longword) = u32::from_str_radix(&state.value_text, 16) else { 397 state.set_invalid = true; 398 return; 399 }; 400 401 let bytes = match state.endian { 402 Endian::Big => longword.to_be_bytes(), 403 Endian::Little => longword.to_le_bytes(), 404 }; 405 406 for (i, byte) in bytes.into_iter().enumerate() { 407 memory.write((address & !3) | i, byte); 408 } 409 410 state.goto_address = Some(address); 411 state.set_invalid = false; 412} 413 414fn export_to_file(memory: &mut dyn DebugMemoryView, state: &MemoryViewerState) { 415 let file_name = state.default_file_name.clone().unwrap_or("memory.bin".into()); 416 417 let Some(path) = 418 FileDialog::new().set_file_name(file_name).add_filter("bin", &["bin"]).save_file() 419 else { 420 return; 421 }; 422 423 if let Err(err) = try_export_to_file(&path, memory) { 424 log::error!("Error saving to path '{}': {err}", path.display()); 425 } 426} 427 428fn try_export_to_file(path: &Path, memory: &mut dyn DebugMemoryView) -> io::Result<()> { 429 use std::io::Write; 430 431 let file = File::create(path)?; 432 let mut writer = BufWriter::new(file); 433 434 let len = memory.len(); 435 for address in 0..len { 436 writer.write_all(&[memory.read(address)])?; 437 } 438 439 Ok(()) 440}