perilus

RISC-V processor built with Chisel
git clone https://git.bradleygannon.com/perilus.git
Log | Files | Refs | Submodules | README

app.rs (25458B)


      1 use std::ops::Range;
      2 use std::path::PathBuf;
      3 use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH};
      4 
      5 use eframe::egui;
      6 use egui::{Color32, FontId, Key, Modifiers};
      7 use perilus_core::perilus::{Perilus, Register};
      8 
      9 use crate::state::{Focus, RunState, ViewMode, row_digits};
     10 
     11 pub(crate) const FONT_SIZE: f32 = 16.0;
     12 pub(crate) const ROW_H: f32 = 22.0;
     13 pub(crate) const GROUP_GAP: f32 = 10.0;
     14 pub(crate) const ADDR_W: f32 = 76.0;
     15 pub(crate) const DIVIDER_GAP: f32 = 9.0;
     16 pub(crate) const REG_LABEL_W: f32 = 58.0;
     17 pub(crate) const REG_COL_GAP: f32 = 20.0;
     18 pub(crate) const BYTES_PER_ROW: usize = 32;
     19 pub(crate) const WORDS_PER_ROW: usize = 8;
     20 pub(crate) const REG_COLS: usize = 4;
     21 pub(crate) const REG_ROWS: usize = 8;
     22 pub(crate) const NUM_CELLS: usize = REG_COLS * REG_ROWS;
     23 pub(crate) const PC_COLOR: Color32 = Color32::from_rgb(40, 200, 40);
     24 
     25 const MAX_RUN_CYCLES: u64 = 50_000_000;
     26 const FREE_RUN_BUDGET: Duration = Duration::from_millis(8);
     27 const CTRL_SHIFT: Modifiers = Modifiers {
     28     ctrl: true,
     29     shift: true,
     30     alt: false,
     31     mac_cmd: false,
     32     command: false,
     33 };
     34 
     35 // Hex keys for the nibble editor.
     36 const HEX_KEYS: [(Key, u8); 16] = [
     37     (Key::Num0, 0),
     38     (Key::Num1, 1),
     39     (Key::Num2, 2),
     40     (Key::Num3, 3),
     41     (Key::Num4, 4),
     42     (Key::Num5, 5),
     43     (Key::Num6, 6),
     44     (Key::Num7, 7),
     45     (Key::Num8, 8),
     46     (Key::Num9, 9),
     47     (Key::A, 10),
     48     (Key::B, 11),
     49     (Key::C, 12),
     50     (Key::D, 13),
     51     (Key::E, 14),
     52     (Key::F, 15),
     53 ];
     54 
     55 #[derive(Clone, Copy)]
     56 pub(crate) enum Direction {
     57     Left,
     58     Right,
     59     Up,
     60     Down,
     61 }
     62 
     63 #[derive(Default)]
     64 struct Actions {
     65     step: bool,
     66     next_instruction: bool,
     67     reset: bool,
     68     free_run: bool,
     69     run_to_cursor: bool,
     70     stop: bool,
     71     focus_registers: bool,
     72     focus_memory: bool,
     73     focus_address: bool,
     74     follow: bool,
     75     names: bool,
     76     view: bool,
     77     set_pc_cursor: bool,
     78     clear: bool,
     79     group_left: bool,
     80     group_right: bool,
     81     page_up: bool,
     82     page_down: bool,
     83     left: bool,
     84     right: bool,
     85     up: bool,
     86     down: bool,
     87     backspace: bool,
     88     trace: bool,
     89     nibble: Option<u8>,
     90 }
     91 
     92 pub(crate) struct SimApp {
     93     pub(crate) perilus: Perilus,
     94     pub(crate) current_pc: u32,
     95     pub(crate) run_state: RunState,
     96     pub(crate) view: ViewMode,
     97     pub(crate) show_aliases: bool,
     98     pub(crate) focus: Focus,
     99     /// Memory cursor: byte address and nibble (0 = high, 1 = low).
    100     pub(crate) mem_addr: u32,
    101     pub(crate) mem_nib: u8,
    102     /// Register-grid cursor: cell index (0 = PC, 1..=31 = x1..x31) and nibble.
    103     pub(crate) reg_index: u8,
    104     pub(crate) reg_nib: u8,
    105     pub(crate) address_input: String,
    106     pub(crate) follow_pc: bool,
    107     pub(crate) follow_pc_last: u32,
    108     pub(crate) focus_address: bool,
    109     pub(crate) scroll_to_row: Option<usize>,
    110     pub(crate) mem_viewport: Range<usize>,
    111     pub(crate) status: String,
    112     pub(crate) tracing: bool,
    113     pub(crate) trace_file: PathBuf,
    114     run_cycles: u64,
    115 }
    116 
    117 impl SimApp {
    118     pub(crate) fn new() -> SimApp {
    119         let perilus = Perilus::init();
    120         let current_pc = perilus.get_pc();
    121         SimApp {
    122             perilus,
    123             current_pc,
    124             run_state: RunState::Idle,
    125             view: ViewMode::Word,
    126             show_aliases: true,
    127             focus: Focus::Registers,
    128             mem_addr: 3,
    129             mem_nib: 0,
    130             reg_index: 0,
    131             reg_nib: 0,
    132             address_input: String::new(),
    133             follow_pc: false,
    134             follow_pc_last: current_pc,
    135             focus_address: false,
    136             scroll_to_row: None,
    137             mem_viewport: 0..0,
    138             status: "Ready".into(),
    139             tracing: false,
    140             trace_file: PathBuf::new(),
    141             run_cycles: 0,
    142         }
    143     }
    144 
    145     /// Captures the model PC once the control unit is back at an instruction boundary.
    146     pub(crate) fn sync_pc(&mut self) {
    147         if self.perilus.get_control_unit_state() == 0 {
    148             self.current_pc = self.perilus.get_pc();
    149         }
    150     }
    151 
    152     pub(crate) fn reset(&mut self) {
    153         self.perilus.reset();
    154         self.run_state = RunState::Idle;
    155         self.sync_pc();
    156         self.status = "Reset".into();
    157     }
    158 
    159     /// Pulse the clock exactly once.
    160     pub(crate) fn step_cycle(&mut self) {
    161         self.perilus.pulse_clock();
    162         self.sync_pc();
    163         self.status = "Stepped one cycle".into();
    164     }
    165 
    166     /// Run until the control unit returns to an instruction boundary.
    167     pub(crate) fn next_instruction(&mut self) {
    168         const MAX_STATES_PER_INSTRUCTION: u32 = 64;
    169         self.perilus.pulse_clock();
    170         let mut states = 1;
    171         while self.perilus.get_control_unit_state() != 0 && states < MAX_STATES_PER_INSTRUCTION {
    172             self.perilus.pulse_clock();
    173             states += 1;
    174         }
    175         self.sync_pc();
    176         if self.perilus.get_control_unit_state() == 0 {
    177             self.status = "Stepped one instruction".into();
    178         } else {
    179             self.status = "Stopped: control unit did not return to fetch".into();
    180         }
    181     }
    182 
    183     pub(crate) fn run_to_pc(&mut self, target: u32) {
    184         self.run_cycles = 0;
    185         self.run_state = RunState::RunToPc(target);
    186     }
    187 
    188     pub(crate) fn run_to_input_pc(&mut self) {
    189         match parse_hex(&self.address_input) {
    190             Some(target) => self.run_to_pc(target),
    191             None => self.status = format!("Invalid PC target {:?}", self.address_input),
    192         }
    193     }
    194 
    195     pub(crate) fn toggle_free_run(&mut self) {
    196         self.run_state = match self.run_state {
    197             RunState::FreeRun => RunState::Idle,
    198             _ => RunState::FreeRun,
    199         };
    200     }
    201 
    202     pub(crate) fn toggle_follow(&mut self) {
    203         self.follow_pc = !self.follow_pc;
    204         self.follow_pc_last = self.current_pc;
    205         self.status = format!("Follow PC {}", if self.follow_pc { "on" } else { "off" });
    206     }
    207 
    208     pub(crate) fn toggle_trace(&mut self) {
    209         if self.tracing {
    210             self.perilus.stop_trace();
    211             self.status = format!("Closed trace file {}", self.trace_file.display());
    212             self.tracing = false;
    213         } else {
    214             let timestamp = SystemTime::now()
    215                 .duration_since(UNIX_EPOCH)
    216                 .unwrap()
    217                 .as_secs();
    218             let iso = chrono_lite(timestamp);
    219             let trace_file = std::env::temp_dir().join(format!("perilus-{iso}.vcd"));
    220             match self.perilus.start_trace(trace_file.clone()) {
    221                 Ok(()) => {
    222                     self.status = format!("Opened trace file {}", trace_file.display());
    223                     self.tracing = true;
    224                     self.trace_file = trace_file;
    225                 }
    226                 Err(e) => {
    227                     self.status = format!("Trace start failed: {e}");
    228                 }
    229             }
    230         }
    231     }
    232 
    233     pub(crate) fn stop(&mut self) {
    234         if self.run_state != RunState::Idle {
    235             self.status = "Stopped".into();
    236         }
    237         self.run_state = RunState::Idle;
    238     }
    239 
    240     pub(crate) fn save_memory(&mut self) {
    241         if let Some(path) = rfd::FileDialog::new()
    242             .set_file_name("memory.bin")
    243             .save_file()
    244         {
    245             self.status = match self.perilus.save_file(path.clone()) {
    246                 Ok(()) => format!("Saved memory to {}", path.display()),
    247                 Err(e) => format!("Save failed: {e}"),
    248             };
    249         }
    250     }
    251 
    252     pub(crate) fn load_memory(&mut self) {
    253         if let Some(path) = rfd::FileDialog::new().pick_file() {
    254             self.load_path(path);
    255         }
    256     }
    257 
    258     pub(crate) fn load_path(&mut self, path: std::path::PathBuf) {
    259         self.status = match self.perilus.load_file(path.clone()) {
    260             Ok(()) => format!("Loaded memory from {}", path.display()),
    261             Err(e) => format!("Load failed: {e}"),
    262         };
    263         self.sync_pc();
    264     }
    265 
    266     pub(crate) fn jump_to_address(&mut self) {
    267         match parse_hex(&self.address_input) {
    268             Some(addr) => {
    269                 let addr = addr & (Perilus::MEMORY_SIZE_BYTES as u32 - 1);
    270                 self.mem_addr = addr;
    271                 self.mem_nib = 0;
    272                 self.scroll_to_row = Some(addr as usize / BYTES_PER_ROW);
    273                 self.focus = Focus::Memory;
    274                 self.status = format!("Selection moved to 0x{addr:08x}");
    275             }
    276             None => self.status = format!("Invalid address {:?}", self.address_input),
    277         }
    278     }
    279 
    280     /// Word containing the memory cursor.
    281     pub(crate) fn cursor_word(&self) -> u32 {
    282         self.mem_addr & !3
    283     }
    284 
    285     pub(crate) fn set_pc_to_cursor(&mut self) {
    286         let pc = self.cursor_word();
    287         self.perilus.set_pc(pc);
    288         self.current_pc = pc;
    289         self.status = format!("Set PC to 0x{pc:08x}");
    290     }
    291 
    292     pub(crate) fn run_to_cursor(&mut self) {
    293         self.run_to_pc(self.cursor_word());
    294     }
    295 
    296     fn move_cursor(&mut self, direction: Direction) {
    297         match self.focus {
    298             Focus::Memory => self.move_memory(direction),
    299             Focus::Registers => self.move_register(direction),
    300         }
    301     }
    302 
    303     fn move_memory(&mut self, direction: Direction) {
    304         let order = row_digits(self.view);
    305         let row = (self.mem_addr / BYTES_PER_ROW as u32) as usize;
    306         let offset = self.mem_addr % BYTES_PER_ROW as u32;
    307         let col = order
    308             .iter()
    309             .position(|&(o, n)| o == offset && n == self.mem_nib)
    310             .unwrap_or(0);
    311         let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW;
    312         let (row, col) = match direction {
    313             Direction::Left if col == 0 && row > 0 => (row - 1, 63),
    314             Direction::Left if col > 0 => (row, col - 1),
    315             Direction::Right if col == 63 && row + 1 < total_rows => (row + 1, 0),
    316             Direction::Right if col < 63 => (row, col + 1),
    317             Direction::Up if row > 0 => (row - 1, col),
    318             Direction::Down if row + 1 < total_rows => (row + 1, col),
    319             _ => (row, col),
    320         };
    321         self.mem_addr = row as u32 * BYTES_PER_ROW as u32 + order[col].0;
    322         self.mem_nib = order[col].1;
    323     }
    324 
    325     /// Move to the first digit of the previous/next word (word view) or byte
    326     /// (byte view).
    327     fn move_memory_group(&mut self, forward: bool) {
    328         let order = row_digits(self.view);
    329         let row = (self.mem_addr / BYTES_PER_ROW as u32) as usize;
    330         let offset = self.mem_addr % BYTES_PER_ROW as u32;
    331         let col = order
    332             .iter()
    333             .position(|&(o, n)| o == offset && n == self.mem_nib)
    334             .unwrap_or(0);
    335         let group = match self.view {
    336             ViewMode::Word => 8,
    337             ViewMode::Byte => 2,
    338         };
    339         let groups_per_row = 64 / group;
    340         let index = col / group;
    341         let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW;
    342         let (row, col) = if forward {
    343             if index + 1 < groups_per_row {
    344                 (row, (index + 1) * group)
    345             } else if row + 1 < total_rows {
    346                 (row + 1, 0)
    347             } else {
    348                 (row, col)
    349             }
    350         } else if index > 0 {
    351             (row, (index - 1) * group)
    352         } else if row > 0 {
    353             (row - 1, (groups_per_row - 1) * group)
    354         } else {
    355             (row, col)
    356         };
    357         self.mem_addr = row as u32 * BYTES_PER_ROW as u32 + order[col].0;
    358         self.mem_nib = order[col].1;
    359     }
    360 
    361     fn move_register(&mut self, direction: Direction) {
    362         match direction {
    363             Direction::Left if self.reg_nib == 0 && self.reg_index > 0 => {
    364                 self.reg_index -= 1;
    365                 self.reg_nib = 7;
    366             }
    367             Direction::Left if self.reg_nib > 0 => self.reg_nib -= 1,
    368             Direction::Right if self.reg_nib == 7 && (self.reg_index as usize) < NUM_CELLS - 1 => {
    369                 self.reg_index += 1;
    370                 self.reg_nib = 0;
    371             }
    372             Direction::Right if self.reg_nib < 7 => self.reg_nib += 1,
    373             Direction::Up if self.reg_index > 0 => self.reg_index -= 1,
    374             Direction::Down if (self.reg_index as usize) < NUM_CELLS - 1 => self.reg_index += 1,
    375             _ => {}
    376         }
    377     }
    378 
    379     fn edit_nibble(&mut self, nibble: u8) {
    380         match self.focus {
    381             Focus::Memory => {
    382                 let addr = self.mem_addr;
    383                 let byte = self.perilus.get_memory_byte(addr);
    384                 let byte = if self.mem_nib == 0 {
    385                     (byte & 0x0f) | (nibble << 4)
    386                 } else {
    387                     (byte & 0xf0) | nibble
    388                 };
    389                 self.perilus.set_memory_byte(addr, byte);
    390                 self.move_memory(Direction::Right);
    391             }
    392             Focus::Registers => {
    393                 let shift = (7 - self.reg_nib as u32) * 4;
    394                 if self.reg_index == 0 {
    395                     let value = (self.current_pc & !(0xf << shift)) | ((nibble as u32) << shift);
    396                     self.perilus.set_pc(value);
    397                     self.current_pc = value;
    398                 } else {
    399                     let index = self.reg_index as usize;
    400                     let value = self.perilus.get_registers()[index];
    401                     let value = (value & !(0xf << shift)) | ((nibble as u32) << shift);
    402                     if let Some(reg) = Register::new(index as u32) {
    403                         self.perilus.set_register(reg, value);
    404                     }
    405                 }
    406                 self.move_register(Direction::Right);
    407             }
    408         }
    409     }
    410 
    411     fn backspace(&mut self) {
    412         match self.focus {
    413             Focus::Memory => {
    414                 let order = row_digits(self.view);
    415                 let offset = self.mem_addr % BYTES_PER_ROW as u32;
    416                 let col = order
    417                     .iter()
    418                     .position(|&(o, n)| o == offset && n == self.mem_nib)
    419                     .unwrap_or(0);
    420                 if self.mem_addr < BYTES_PER_ROW as u32 && col == 0 {
    421                     return;
    422                 }
    423                 let addr = self.mem_addr;
    424                 let byte = self.perilus.get_memory_byte(addr);
    425                 let byte = if self.mem_nib == 0 {
    426                     byte & 0x0f
    427                 } else {
    428                     byte & 0xf0
    429                 };
    430                 self.perilus.set_memory_byte(addr, byte);
    431                 self.move_memory(Direction::Left);
    432             }
    433             Focus::Registers => {
    434                 if self.reg_index == 0 && self.reg_nib == 0 {
    435                     return;
    436                 }
    437                 let shift = (7 - self.reg_nib as u32) * 4;
    438                 if self.reg_index == 0 {
    439                     let value = self.current_pc & !(0xf << shift);
    440                     self.perilus.set_pc(value);
    441                     self.current_pc = value;
    442                 } else {
    443                     let index = self.reg_index as usize;
    444                     let value = self.perilus.get_registers()[index] & !(0xf << shift);
    445                     if let Some(reg) = Register::new(index as u32) {
    446                         self.perilus.set_register(reg, value);
    447                     }
    448                 }
    449                 self.move_register(Direction::Left);
    450             }
    451         }
    452     }
    453 
    454     fn clear_cursor(&mut self) {
    455         match self.focus {
    456             Focus::Memory => match self.view {
    457                 ViewMode::Word => {
    458                     let word = self.mem_addr / 4;
    459                     self.perilus.set_memory(word, 0);
    460                     self.status = format!("Cleared word 0x{:08x}", word * 4);
    461                 }
    462                 ViewMode::Byte => {
    463                     self.perilus.set_memory_byte(self.mem_addr, 0);
    464                     self.status = format!("Cleared byte 0x{:08x}", self.mem_addr);
    465                 }
    466             },
    467             Focus::Registers => {
    468                 if self.reg_index == 0 {
    469                     self.perilus.set_pc(0);
    470                     self.current_pc = 0;
    471                     self.status = "Cleared PC".into();
    472                 } else if let Some(reg) = Register::new(self.reg_index as u32) {
    473                     self.perilus.set_register(reg, 0);
    474                     self.status = format!("Cleared x{}", self.reg_index);
    475                 }
    476             }
    477         }
    478     }
    479 
    480     fn scroll_page(&mut self, down: bool) {
    481         let total_rows = Perilus::MEMORY_SIZE_WORDS / WORDS_PER_ROW;
    482         let page = self.mem_viewport.len().max(1) as isize;
    483         let top = self.mem_viewport.start as isize;
    484         let new_top = if down {
    485             (top + page).min(total_rows as isize - 1)
    486         } else {
    487             (top - page).max(0)
    488         };
    489         self.scroll_to_row = Some(new_top as usize);
    490     }
    491 
    492     fn handle_keys(&mut self, ui: &egui::Ui) {
    493         let ctx = ui.ctx();
    494         if ctx.text_edit_focused() {
    495             return;
    496         }
    497         let actions = ctx.input_mut(|i| {
    498             let mut actions = Actions {
    499                 step: i.consume_key(Modifiers::NONE, Key::Space),
    500                 stop: i.consume_key(Modifiers::NONE, Key::Escape),
    501                 backspace: i.consume_key(Modifiers::NONE, Key::Backspace),
    502                 next_instruction: i.consume_key(Modifiers::NONE, Key::N),
    503                 focus_address: i.consume_key(Modifiers::NONE, Key::G),
    504                 names: i.consume_key(Modifiers::NONE, Key::X),
    505                 view: i.consume_key(Modifiers::NONE, Key::V),
    506                 trace: i.consume_key(Modifiers::NONE, Key::T),
    507                 clear: i.consume_key(Modifiers::SHIFT, Key::C),
    508                 follow: i.consume_key(Modifiers::SHIFT, Key::P),
    509                 set_pc_cursor: i.consume_key(Modifiers::NONE, Key::P),
    510                 ..Default::default()
    511             };
    512             actions.run_to_cursor = i.consume_key(CTRL_SHIFT, Key::Space);
    513             actions.free_run = i.consume_key(Modifiers::CTRL, Key::Space);
    514             actions.reset = i.consume_key(Modifiers::CTRL, Key::R);
    515             actions.focus_registers = i.consume_key(Modifiers::NONE, Key::R);
    516             actions.focus_memory = i.consume_key(Modifiers::NONE, Key::M);
    517             actions.group_left = i.consume_key(Modifiers::CTRL, Key::ArrowLeft)
    518                 || i.consume_key(Modifiers::CTRL, Key::H);
    519             actions.group_right = i.consume_key(Modifiers::CTRL, Key::ArrowRight)
    520                 || i.consume_key(Modifiers::CTRL, Key::L);
    521             actions.page_up = i.consume_key(Modifiers::NONE, Key::PageUp)
    522                 || i.consume_key(Modifiers::CTRL, Key::U);
    523             actions.page_down = i.consume_key(Modifiers::NONE, Key::PageDown)
    524                 || i.consume_key(Modifiers::CTRL, Key::D);
    525             actions.left = i.consume_key(Modifiers::NONE, Key::ArrowLeft)
    526                 || i.consume_key(Modifiers::NONE, Key::H);
    527             actions.right = i.consume_key(Modifiers::NONE, Key::ArrowRight)
    528                 || i.consume_key(Modifiers::NONE, Key::L);
    529             actions.up = i.consume_key(Modifiers::NONE, Key::ArrowUp)
    530                 || i.consume_key(Modifiers::NONE, Key::K);
    531             actions.down = i.consume_key(Modifiers::NONE, Key::ArrowDown)
    532                 || i.consume_key(Modifiers::NONE, Key::J);
    533             for (key, value) in HEX_KEYS {
    534                 if i.consume_key(Modifiers::NONE, key) {
    535                     actions.nibble = Some(value);
    536                 }
    537             }
    538             actions
    539         });
    540 
    541         if actions.run_to_cursor {
    542             self.run_to_cursor();
    543         }
    544         if actions.free_run {
    545             self.toggle_free_run();
    546         }
    547         if actions.step {
    548             self.step_cycle();
    549         }
    550         if actions.next_instruction {
    551             self.next_instruction();
    552         }
    553         if actions.reset {
    554             self.reset();
    555         }
    556         if actions.focus_registers {
    557             self.focus = Focus::Registers;
    558         }
    559         if actions.focus_memory {
    560             self.focus = Focus::Memory;
    561         }
    562         if actions.set_pc_cursor {
    563             self.set_pc_to_cursor();
    564         }
    565         if actions.stop {
    566             self.stop();
    567         }
    568         if actions.focus_address {
    569             self.focus_address = true;
    570         }
    571         if actions.follow {
    572             self.toggle_follow();
    573         }
    574         if actions.names {
    575             self.show_aliases = !self.show_aliases;
    576         }
    577         if actions.view {
    578             self.view = match self.view {
    579                 ViewMode::Word => ViewMode::Byte,
    580                 ViewMode::Byte => ViewMode::Word,
    581             };
    582         }
    583         if actions.trace {
    584             self.toggle_trace();
    585         }
    586         if actions.clear {
    587             self.clear_cursor();
    588         }
    589         if actions.group_left && self.focus == Focus::Memory {
    590             self.move_memory_group(false);
    591         }
    592         if actions.group_right && self.focus == Focus::Memory {
    593             self.move_memory_group(true);
    594         }
    595         if actions.page_up {
    596             self.scroll_page(false);
    597         }
    598         if actions.page_down {
    599             self.scroll_page(true);
    600         }
    601         let direction = if actions.left {
    602             Some(Direction::Left)
    603         } else if actions.right {
    604             Some(Direction::Right)
    605         } else if actions.up {
    606             Some(Direction::Up)
    607         } else if actions.down {
    608             Some(Direction::Down)
    609         } else {
    610             None
    611         };
    612         if let Some(direction) = direction {
    613             self.move_cursor(direction);
    614         }
    615         if let Some(nibble) = actions.nibble {
    616             self.edit_nibble(nibble);
    617         }
    618         if actions.backspace {
    619             self.backspace();
    620         }
    621     }
    622 }
    623 
    624 impl eframe::App for SimApp {
    625     fn logic(&mut self, ctx: &egui::Context, _frame: &mut eframe::Frame) {
    626         match self.run_state {
    627             RunState::Idle => {}
    628             RunState::FreeRun => {
    629                 let start = Instant::now();
    630                 while start.elapsed() < FREE_RUN_BUDGET {
    631                     self.perilus.pulse_clock();
    632                 }
    633                 self.sync_pc();
    634                 ctx.request_repaint();
    635             }
    636             RunState::RunToPc(target) => {
    637                 let start = Instant::now();
    638                 loop {
    639                     if self.perilus.get_control_unit_state() == 0 && self.perilus.get_pc() == target
    640                     {
    641                         self.status = format!("Reached PC 0x{target:08x}");
    642                         self.run_state = RunState::Idle;
    643                         break;
    644                     }
    645                     self.perilus.pulse_clock();
    646                     self.run_cycles += 1;
    647                     if self.run_cycles >= MAX_RUN_CYCLES {
    648                         self.status = format!(
    649                             "Stopped: did not reach PC 0x{target:08x} after {MAX_RUN_CYCLES} cycles"
    650                         );
    651                         self.run_state = RunState::Idle;
    652                         break;
    653                     }
    654                     if start.elapsed() >= FREE_RUN_BUDGET {
    655                         ctx.request_repaint();
    656                         break;
    657                     }
    658                 }
    659                 self.sync_pc();
    660             }
    661         }
    662     }
    663 
    664     fn ui(&mut self, ui: &mut egui::Ui, _frame: &mut eframe::Frame) {
    665         self.handle_keys(ui);
    666         let top_frame = egui::Frame::side_top_panel(ui.style()).shadow(egui::Shadow::NONE);
    667         egui::Panel::top("registers_panel")
    668             .frame(top_frame)
    669             .show_separator_line(false)
    670             .show(ui, |ui| {
    671                 ui.add_space(4.0);
    672                 self.registers_ui(ui);
    673                 ui.add_space(4.0);
    674             });
    675         let bottom_frame = egui::Frame::side_top_panel(ui.style()).shadow(egui::Shadow::NONE);
    676         egui::Panel::bottom("controls_panel")
    677             .frame(bottom_frame)
    678             .show_separator_line(false)
    679             .show(ui, |ui| {
    680                 ui.add_space(4.0);
    681                 self.controls_ui(ui);
    682                 ui.add_space(4.0);
    683             });
    684         egui::CentralPanel::default().show(ui, |ui| {
    685             self.memory_ui(ui);
    686         });
    687     }
    688 }
    689 
    690 pub(crate) fn char_width(ui: &egui::Ui, font: &FontId) -> f32 {
    691     ui.painter()
    692         .layout_no_wrap("0".to_owned(), font.clone(), Color32::WHITE)
    693         .size()
    694         .x
    695 }
    696 
    697 fn parse_hex(text: &str) -> Option<u32> {
    698     let trimmed = text.trim();
    699     let digits = trimmed
    700         .strip_prefix("0x")
    701         .or_else(|| trimmed.strip_prefix("0X"))
    702         .unwrap_or(trimmed);
    703     if digits.is_empty() {
    704         return None;
    705     }
    706     u32::from_str_radix(digits, 16).ok()
    707 }
    708 
    709 /// Format Unix seconds as an ISO-8601 UTC string like `2026-10-04T14:28:19Z`.
    710 fn chrono_lite(secs: u64) -> String {
    711     let days = secs / 86_400;
    712     let mut rem = secs % 86_400;
    713     let hours = rem / 3600;
    714     rem %= 3600;
    715     let minutes = rem / 60;
    716     let seconds = rem % 60;
    717 
    718     // Days since epoch → year/month/day (algorithm from Howard Hinnant)
    719     let z = days + 719_468;
    720     let era = z / 146_097;
    721     let doe = z - era * 146_097;
    722     let yoe = (doe - doe / 1461 + doe / 36_524 - doe / 146_096) / 365;
    723     let y = yoe + era * 400;
    724     let doy = doe - (365 * yoe + yoe / 4 - yoe / 100);
    725     let mp = (5 * doy + 2) / 153;
    726     let d = doy - (153 * mp + 2) / 5 + 1;
    727     let m = if mp < 10 { mp + 3 } else { mp - 9 };
    728     let year = if m <= 2 { y + 1 } else { y };
    729 
    730     format!(
    731         "{year:04}-{:02}-{:02}T{:02}:{:02}:{:02}Z",
    732         m, d, hours, minutes, seconds
    733     )
    734 }