diff --git a/.handoff/cuddly-forging-russell.md b/.handoff/cuddly-forging-russell.md new file mode 100644 index 0000000..9c9a199 --- /dev/null +++ b/.handoff/cuddly-forging-russell.md @@ -0,0 +1,194 @@ +# Plano — Toolkit de UI padronizado e modular (ratatui) + +## Context + +A UI do Raven cresceu orgânica: cada view (settings, cache, tlb, pipeline, run, docs) +reimplementa à mão os mesmos padrões de ratatui. O levantamento (5 agentes, varredura +completa de `src/ui`) mediu a repetição: + +- **~69** `Block::default()` montando o mesmo painel arredondado+título (48 sites quase idênticos). +- **~13** cópias open-coded do triângulo de estilo `selecionado→ACCENT.bold / hover→TEXT.bold / normal→LABEL`, mais **~17** variantes de valor. +- **842** `Span::styled` / **964** `Style::default().fg(...)`, dos quais ~6–9 estilos semânticos cobrem a maioria; o estilo `label` aparece ~200×, `title` ~45×. +- **6** funções `*_btn_style`/`dense_style`/`btn_style` que são a MESMA "toggle chip" duplicada por arquivo, + duplicatas mortas em `run/status.rs`. +- **3** cópias de `centered_rect` + **~10** recomputações do mesmo layout raiz (tab/body/status) entre `view` e `input::mouse` (risco de hitbox driftar). +- Tabelas: só **3** usam o widget `Table` real; o resto é alinhamento manual com `format!("{: Style` + - `title_span(s) -> Span` + - `enum Metric { Cycles, Cpi, Ipc }`; `metric(k) -> Style`; `metric_span(label, val, k) -> Span` + - `enum Badge { Accent, Danger, Idle, Success }`; `badge(text, kind) -> Span` + - `toggle(active, hovered, active_color) -> Style` (a chip de 3 estados unificada) + - `key_hint(key, desc) -> Vec`; `hint_bar(&[(&str,&str)]) -> Line` +- `src/ui/view/components/layout.rs` — geometria pura, **`pub(crate)`** (sem `Frame`): + - `centered_rect(w, h, area)`, `centered_pct(pw, ph, area)`, `centered_width(area, pref, min, margin)` + - `centered_column(area, content_w)` (via `Fill/Length/Fill`) + - `body_footer(area, footer_h) -> (Rect, Rect)`, `header_body(area, &[u16]) -> (Vec, Rect)` + - `app_frame(area) -> (tabs, body, status)` (o frame raiz `[Length(3), Min(5), Length(1)]`) + - `even_columns(area, n) -> Vec` + - `anchored_popup(anchor, pw, ph, term)` (unifica `best_popup_rect` + `tutorial_popup_rect`) + +**Editar:** `src/ui/view/components/mod.rs` (declarar/re-exportar `layout`); `src/ui/view/mod.rs` (declarar `style`). + +**Doc deliverable:** doc-comment de módulo em `components/mod.rs` descrevendo o "jeito Raven" +de escrever UI (qual helper usar pra cada caso). Doc de desenvolvedor via rustdoc — **não** +em `docs/` (esse é didático/estudante). + +--- + +## Fase 1 — Painéis & overlays (chrome) + +Maior ganho de legibilidade por linha. Substitui ~48 painéis e 6 popups. + +**Novos arquivos:** +- `src/ui/view/components/panel.rs`: + - `enum PanelKind { Plain, Accent, Warning, Danger, Custom(Color) }` + - `panel(title, kind) -> Block` (rounded + ALL + border + título estilado) + - `panel_frame(kind) -> Block` (sem título), `panel_square(title_line, border) -> Block` (caixas do pipeline/gantt) + - `handle_bar(border) -> Block` (só `Borders::TOP` — barras de preset/apply/console colapsado) + - `render_panel(f, area, block) -> Rect` (faz `inner()` + `render_widget`, dobra a dança de 3 linhas que aparece ~40×) +- `src/ui/view/components/overlay.rs`: + - `struct OverlayStyle { border, title, bottom }`; `overlay(f, rect, style) -> Rect` (Clear + block + retorna inner) + +**Migrar (representativos — padrão repetido em ~30 arquivos):** +`cache/{config,mod,stats,view/matrix}.rs`, `tlb/{config,entries,mod,page_tree,stats,status,vm_settings}.rs`, +`run/{status,mod,instruction_list,instruction_details/*}.rs`, `pipeline/{mod,config_view,main_view/*}.rs`, +`settings.rs`. Popups: `mod.rs` (exit/help/ELF), `path_input_overlay.rs`, `cache/stats.rs` (snapshot), `tutorial/render.rs`. + +**Cuidado (hitbox):** `input/mouse/cache.rs:137,153` constroem `Block` só pra `.inner()`. Devem usar +o mesmo `panel*()`/`render_panel` que o render, senão a geometria de hit-test diverge. Verificar clique a clique. + +--- + +## Fase 2 — Estilos semânticos, badges & métricas + +Migra os ~600 sites open-coded que mapeiam para os estilos semânticos da Fase 0. + +- Substituir `Style::default().fg(theme::LABEL|TEXT|IDLE|DANGER|RUNNING|PAUSED)` por `style::label()|value()|idle()|danger()|success()|warning()`. +- Títulos de painel via `style::title()`/`title_span()` (acopla com Fase 1). +- Métricas (`Cycles:/CPI:/IPC:`, ~30 sites) → `style::metric_span(...)`. +- Badges/pills (`fg(Black).bg(...)`, 8 sites em `mod.rs`, `path_input_overlay.rs`, `build.rs`) → `style::badge(...)`. +- Hint bars / legendas (footer global em `mod.rs:128-198`, help popup, search overlays) → `style::hint_bar()`/`key_hint()`. +- **Deletar duplicatas mortas:** `run/status.rs` `push_dense_pair`/`value_btn`/`action_btn` (cópias literais de `controls.rs`). +- **Colapsar as 6 toggle-chips:** `cache/mod.rs` `dense_style`+wrappers, `pipeline/mod.rs` `subtab_style`, `tlb/mod.rs` `btn_style` → todos para `style::toggle`. + +Pode ser feito por cluster de arquivos (run / cache / tlb / pipeline / docs-chrome) — vários commits se ficar grande. + +--- + +## Fase 3 — Controles interativos (settings + configs) + +Onde mora o "código mais porco". Aqui a unificação visual acontece. + +**Editar/estender `src/ui/view/components/controls.rs`** (mantém `dense_value`/`dense_action` como primitivas): +- `enum ControlState { Normal, Hovered, Selected, Disabled }` + `from(sel, hov)` + `disabled_if(off)` +- `label_style(state, base_color) -> Style` (o triângulo, com base override pra CPI=`CPI_PANEL`, pipeline=`IDLE`) +- `toggle_row(label, value, state) -> ListItem` (bool unificado true/false verde/vermelho) +- `selector_row(label, value, state, editing) -> ListItem` (enum ciclável; `< v >` quando editando) +- `edit_row(label, display, edit_buf: Option<&str>, state) -> ListItem` (cursor `█` unificado) +- `action(label, color, hovered) -> Span` (absorve os `preset_btn_style` locais duplicados) +- Suportar sufixos compostos: `[?]`, "(no effect — VM off)", marca de validade ` ✗`, hint JIT. + +**Migrar:** `settings.rs` (10 linhas → builders), `cache/config.rs` (closure `field_item` vira adaptador fino), +`tlb/config.rs`, `tlb/vm_settings.rs` (closures `num_val`/`hov`/`editing`), `pipeline/config_view.rs` (`bool_span`). + +**Cuidado:** estado `Disabled` em `vm_settings.rs` também **pula registro de hitbox** (`:177,187,267`). +Render-só nos builders; geometria/hitbox continua no caller (sistema de Cell/`record_*_hitboxes` atual). + +--- + +## Fase 4 — Tabelas & listas + +**Novos arquivos:** +- `src/ui/view/components/tables.rs`: + - `struct Col { header, width: Constraint, align }`, `enum Align` + - `struct RowStyle { selected, hover, zebra, base }` + - `DataTable` builder (sobre o `Table` real: header + zebra + seleção + alinhamento grátis) + - `kv_table(&[(&str,&str)], key_w) -> Vec`, `kv_styled(&[(Span,Span)]) -> Vec` +- `src/ui/view/components/lists.rs`: + - `struct ListRow { line, selected, hover }`, `selectable_list(rows) -> List` (bg de seleção/hover centralizada) + - `visible_window(total, view_h, cursor, scroll) -> (start, end)` (a matemática de scroll repetida em regs/mem/imem/history/entries/page_tree) + +**Migrar (manuais `format!` → toolkit):** docs `instr_ref/render.rs` (apaga `pad_or_truncate`/`render_col_header`/separador manual), +`docs/content/common.rs` (`kv/thead/tsep/trow_wrapped` reexpressos sobre o toolkit) + `syscalls.rs`/`memory_map.rs`, +`cache/stats.rs` (history + metrics), `tlb/stats.rs`; listas: `run/instruction_list.rs`, `run/sidebar.rs` (mem/ELF), `path_input_overlay.rs`. +Centralizar constantes de largura (`TY_W/MNE_W/...`, `COL_*_W`, `set_col_w=5`) e cores de seleção em `theme`. + +**Fora (caso especial, fica bespoke):** matriz sets×ways (`cache/view/matrix.rs` — cores por célula, wrap, h-scroll) +e árvore de páginas (`tlb/page_tree.rs` — indentação/colapso). No máximo reaproveitam `visible_window` e perm-spans. + +--- + +## Fase 5 — Layout & dedup view↔mouse + +Consome `components/layout.rs` (Fase 0) dos dois lados, matando a duplicação. + +- **View renderers** passam a usar `app_frame`, `body_footer`, `header_body`, `even_columns`, `centered_column`: + `mod.rs:50`, `run/mod.rs:49`, `settings.rs:38`, `cache/{config,mod,view,stats}.rs`, `tlb/{config,mod,stats}.rs`, `pipeline/mod.rs`, etc. +- **`input::mouse`** passa a chamar os MESMOS helpers em vez de recomputar: + frame raiz em `mouse/run.rs` (×5), `mouse/cache.rs`, `mouse/run_status.rs`, `mouse/docs.rs`, `mouse.rs`; + e os 3 `centered_rect` (`view/mod.rs:702`, `input/mouse/popups.rs:5`, `tutorial/render.rs:339`) → um só. +- Unificar `best_popup_rect`/`tutorial_popup_rect` → `anchored_popup`. +- Garantir um único valor pro `Min(5)` do body (hoje há divergência `Min(5)` vs sub-split `Min(3)`). + +**Cuidado:** esta é a fase de maior risco de regressão de clique. Testar cada tab/popup com mouse. + +--- + +## Fase 6 — Limpeza, doc e verificação final + +- Remover helpers locais obsoletos que sobraram (closures de estilo por-arquivo, consts duplicadas). +- Finalizar o doc-comment do "jeito Raven" em `components/mod.rs` com exemplos before/after. +- `cargo clippy --all-targets` limpo; rodar com e sem `--features jit`. + +--- + +## Verificação (cada fase) + +1. **Compila/lint:** `cargo build` e `cargo clippy --all-targets -- -D warnings`. +2. **Testes:** `cargo test`. +3. **Visual/manual (skill `run`):** abrir a TUI e percorrer cada tab — Settings, Cache (config/stats/matrix), TLB (config/entries/page-tree/stats/vm), Pipeline (config/main), Run (sidebar/imem/details/console), Docs (instr_ref/syscalls/memory_map). Conferir bordas, títulos, toggles (true/false), seletores, campos editáveis (cursor `█`), métricas, badges. +4. **Mouse (Fases 1,3,5 — crítico):** clicar em toggles/seletores/botões/preset/apply, abrir/fechar popups (exit, help, ELF, path input, snapshot, tutorial), e verificar que hitboxes batem com o render (sem drift). +5. **Diff sanity:** confirmar que o LOC caiu em settings.rs/configs e que nenhum `Block::default()`/triângulo de estilo sobrou nos arquivos migrados (`grep`). + +## Ordem de dependência (resumo) +Fase 0 (fundação) → 1 (painel/overlay, usa style) → 2 (estilos) → 3 (controles, usa style) → 4 (tabelas/listas) → 5 (layout + mouse) → 6 (limpeza/doc). Cada fase = 1+ commit, compilando isolada. diff --git a/.handoff/project_ui_toolkit.md b/.handoff/project_ui_toolkit.md new file mode 100644 index 0000000..51efd64 --- /dev/null +++ b/.handoff/project_ui_toolkit.md @@ -0,0 +1,109 @@ +--- +name: project_ui_toolkit +description: branch refactor/ui-component-toolkit — standardized modular ratatui UI toolkit (7 phases) +metadata: + node_type: memory + type: project + originSessionId: fb504418-1323-439e-97b5-d9887467dd98 +--- + +Branch `refactor/ui-component-toolkit` (from main, since [[project_modularize_refactor]] is a separate branch). Plan: `/home/gaok1/.claude/plans/cuddly-forging-russell.md` — 7 phases (0–6), each phase = 1+ commit, compiles in isolation. + +Goal: build a modular UI toolkit under `src/ui/view/components/` + `src/ui/view/style.rs`, then migrate ALL call sites. Unify visual inconsistencies (one convention per archetype), share layout geometry `pub(crate)` between view and `input::mouse`. + +Unified conventions: edit cursor `█` everywhere; bool → `true`/`false` green/red; popups all `BorderType::Rounded`; panel titles ACCENT.bold (primary) vs LABEL (secondary). + +**Status:** +- Phase 0 DONE (commit 42df316): created `view/style.rs` (semantic Style/Span builders) + `components/layout.rs` (pure geometry, pub(crate)). Both carry `#![allow(dead_code)]` until consumed; removed in phase 6. +- Phase 1 DONE (commit 30f7ad0): `components/panel.rs` (PanelKind, panel/panel_frame/panel_square/handle_bar/render_panel) + `components/overlay.rs` (OverlayStyle/overlay). Migrated all ~48 panels + popups across 31 files (−177 LOC net). `components` + panel/overlay are now `pub(crate)` (so input::mouse + tutorial share them; also unblocks layout sharing in phase 5). mouse.rs hit-test now uses same panel_frame() as render (no drift). Title styles preserved exactly via panel_frame+.title for bespoke colors; only square→round popups changed visually. Left bespoke: bg-fill blocks, the square `[?]` help button (mod.rs:104). +- Phase 2 DONE (commits b00792b + 41d9eff + 35f1a5a): migrated ~358 open-coded `Style::default().fg(theme::{LABEL,TEXT,IDLE,DANGER,RUNNING,PAUSED})` → `style::{label,value,idle,danger,success,warning}()` across 26 view files (exact-equivalent prefix swap preserves chained `.bold()`). Collapsed toggle chips onto `style::toggle` (cache::{level_btn_style,subtab_style}, tlb::btn_style, pipeline::subtab_style — pipeline unifies active+hover→hovered-first; removed cache::dense_style + dead scope_btn_style). Deleted run/status.rs literal dups (push_dense_pair/value_btn/action_btn → components::controls). Metrics → `style::metric(Metric::…)`. User-approved visual changes: global footer (mod.rs) now uses `style::hint_bar` (keys accent-bold); ELF popup pills now `style::badge` (bold). Left bespoke: splash mutated styles, build.rs Red/Green pills, path_input list-selection highlight (Fase 4), docs/content didactic colors (out of scope). +- Phase 3 DONE (commit 9fa2627): extended `components/controls.rs` with the shared control vocabulary — `ControlState{Normal,Hovered,Selected,Disabled}` (+ `from`/`disabled_if`), `label_span()` (the ~14 open-coded label triangles), `bool_value()` (true/false green/red chip), `edit_value()` (single source of the `█` cursor), `field_value()`/`field_row()` (cache/TLB field_item adapters). Migrated all 5 panels: settings.rs (10 rows→builders, `_`→`█` cursor), cache/config.rs + tlb/config.rs (field_item→field_row, `preset_btn_style`/tlb-preset-closure → `dense_action(.., ACCENT, hov)`), vm_settings.rs (num_val→edit_value, render-only — hitbox/geometry stays in caller), pipeline/config_view.rs (Val{Bool,Text} split in the value loop). **User-decided visual changes:** CPI selected row now ACCENT bold (was CPI_PANEL bold); vm_settings + pipeline booleans converged to true/false green/red (vm `[on]/[off]` off-state was gray→now red; pipeline bools were neutral→now green/red, only colored values in that panel). Perms R W X U kept as-is. build ±jit clean; test 343 passed (329 lib + 11 + 3), 0 failed. +- Phase 4 DONE (commit 6f60c4d): `components/lists.rs` (`visible_window(total,view_h,scroll)->(start,end)` w/ unit tests; `ListRow`/`selectable_list`) + `components/tables.rs` (`Align`/`Col`/`DataTable` builder over real Table, `kv_table`/`kv_styled`). Centralized scattered list-selection bgs in theme (`SEL_ROW_BG`/`HOVER_ROW_BG`/`PIN_HOVER_BG`). Migrated: docs/free_page + docs/instr_ref data window → `visible_window`; tlb/entries scroll → `visible_window`; run/sidebar register rows → theme consts. Left bespoke: page_tree/vm_settings (keep their `max_scroll.set()` side-effect); didactic table colors. tables/lists carry `#![allow(dead_code)]` (toolkit offered ahead of consumers — DataTable/kv/selectable_list have no caller yet, deliberate). +- Phase 5 DONE (commit a31527e): killed the root-frame `[Length(3),Min(5),Length(1)]` duplication. `layout::app_frame_chunks(area)->[Rect;3]` (indexable form of `app_frame`). view/mod.rs main ui + help_button_area, and ALL 9 `root_chunks` + the editor `chunks` splits in input/mouse.rs → `app_frame_chunks`. Both private `centered_rect` (view + mouse) now delegate to `layout::centered_rect`. Each swap is bit-identical (same Layout/constraints/split) → geometry provably unchanged. **DEFERRED:** `best_popup_rect`/`tutorial_popup_rect` → `anchored_popup` convergence — the bespoke helpers right-align to the anchor and lack right/left fallbacks, so adopting `anchored_popup` MOVES popups (a real behavior change); left for the interactive mouse pass. +- Phase 6 DONE (commit eaed9c3): finalized the Raven-way doc in components/mod.rs (before/after for panel chrome + root frame); collapsed the tables.rs zebra `if` (only lint the new code introduced). `cargo build`, `cargo build --features jit`, `cargo test` (333 lib + 11 + 3 + 3, 0 failed) all green. clippy clean on the new toolkit modules; broad pre-existing backend clippy baseline untouched/out of scope. `#![allow(dead_code)]` kept on style/layout/tables/lists (documented toolkit surface offered ahead of consumers) — NOT removed, contrary to the original plan's aspiration, because not every helper has a caller and deleting the planned API would be wrong. +- **ALL 7 PHASES (0–6) DONE.** Outstanding before merge: the interactive TUI mouse/visual sweep (plan verification steps 3–4) — cannot be done headless; and the deferred `anchored_popup` popup-position convergence. +- Safety net: plan + this memory copied into gitignored `Raven/.handoff/` (added `/.handoff/` to .gitignore in the phase-3 commit). +- **GOTCHA:** never run repo-wide `cargo fmt` (reformats whole crate → churn in unrelated files); and bare `rustfmt ` reorders imports differently than `cargo fmt` (different edition/config) → spurious churn. Keep edits inline; if formatting needed, scope carefully and revert non-target files. + +**Note:** pre-existing clippy error on this base (main): `never_loop` at `src/ui/app/mod.rs:2725` (correctness lint, deny-by-default). NOT from this work — `cargo clippy --all-targets` fails on it. `cargo build` + `cargo test` are clean. + +--- + +> ⚠️ **STATUS 2026-06-04: the two follow-up sessions below were DISCARDED.** +> The user reverted the working tree (`git checkout`) to clean HEAD (`7a0c091`) +> before handing the repo to a separate large refactor (step-back + journaling). +> `git status` on `src/` is clean — **none of the responsiveness / scrollbar / +> draggable-bar code described below is on disk anymore.** The notes are kept as a +> design record. See the consolidated **PENDING CHECKLIST** at the bottom for what +> to re-do when this work resumes. + +## Follow-up session — responsiveness + scrollbar (DISCARDED — was working tree) + +Post-merge feature work requested by user: "UI totalmente responsiva (esconder colunas ou scrollbar quando não couber)" + bug "scrollbar do cache não respeita o arraste do mouse". Also acted on the quality review (prove/prune the speculative toolkit surface). All built + tested green; **2 PRE-EXISTING test failures** confirmed via `git stash` on clean HEAD: `run_dyn_register_view_uses_register_scroll_keys` + `run_sidebar_wheel_scrolls_registers_in_dyn_register_view` (run sidebar, NOT from this work). + +- **Cache h-scrollbar drag bug — FIXED.** Root cause: on `Down` the code jumped `view_h_scroll` to the click ratio but stored the drag baseline (`hscroll_start`) as the OLD scroll → snap-back on first drag. Unified click+drag onto one absolute mapping `hscroll_pos_from_column(column, track_x, track_w, max)` in `input/mouse.rs`. Removed `hscroll_start` + renamed `hscroll_drag_start_x → hscroll_drag_track_x` (cache_state.rs + app/mod.rs init). +- **Reusable responsive primitives — ADDED + tested.** Chose NOT to bolt onto `DataTable` (poor fit for token-colored tables). `tables::fit_columns(&[ColFit], avail, gap) -> Vec>` — priority-based column drop + flex distribution (4 unit tests). `lists::vertical_scrollbar(f, area, content_len, viewport, offset)` — ratatui Scrollbar overlay, no-op when it fits; caller reserves 1 right column. Re-exported from components/mod.rs (`Align, Col, ColFit, DataTable, fit_columns, vertical_scrollbar`). +- **instr_ref.rs — migrated (proving consumer).** Replaced binary `show_exp`/`col_widths` with `fit_columns` (progressive drop Expands→Operands→Type; Mnemonic+Description always kept) + added vertical scrollbar. Token coloring preserved. Removed `SHOW_EXP_MIN_W`. +- **tlb/entries.rs — migrated to `DataTable` + scrollbar.** DataTable now has a real consumer (kills its dead-code claim). NOTE: header is now bold (DataTable convention) — minor visual change. NOTE: `tlb/stats.rs` is metrics+chart, NOT a table — was a bad DataTable target, so entries proved it instead. +- **cache/stats.rs `render_history_table` — added vertical scrollbar** (reserves right col via `text_w`). +- **STILL DEAD:** `tables::kv_table`/`kv_styled` (0 consumers) → `#![allow(dead_code)]` stays on tables.rs/lists.rs/style.rs/layout.rs. +- **DEFERRED (task #5):** run sidebar register/mem scrollbars — bespoke pinned+offset scroll math + the 2 failing tests live there; needs care + interactive verification. And the kv_* prune/keep decision. +- **NEEDS INTERACTIVE VERIFICATION (headless can't):** cache scrollbar drag feel; instr_ref column-drop + scrollbar on terminal resize; tlb entries scrollbar; cache history scrollbar. +- Not committed yet — awaiting user (visual verification + commit decision). + +### 2nd follow-up — horizontal scroll + DRAGGABLE scrollbars (user feedback) + +User clarified intent: they want SCROLLBARS (incl. horizontal), NOT column hiding; and the bars must be mouse-draggable ("n da pra puxar o scroll com mouse. eu queria que desse"). So: +- **PRUNED `fit_columns` + `ColFit` + 4 tests** (column-hiding was a misread of the original ask). Re-export dropped. +- **Generalized drag math:** `lists::scroll_offset_from_pos(pos, track_start, track_len, max_offset)` (+2 tests) — absolute cursor→offset map. Cache's `hscroll_pos_from_column` DELETED and both its call sites now use this shared fn. +- **Added `lists::horizontal_scrollbar`** (HorizontalBottom). Re-exported `horizontal_scrollbar, scroll_offset_from_pos`. +- **instr_ref.rs reworked:** now shows ALL columns at fixed widths (Description flexes to fill spare width; `col_dims` → `(desc_w, natural_w)`); when `natural_w > content_w` it HORIZONTALLY SCROLLS (Paragraph `.scroll((0, h_off))` on header/sep/rows) with a bottom h-scrollbar — no column hiding. Vertical scrollbar kept. Both bars register `(start,len,cross,max)` track geom into new `docs.sb_v`/`docs.sb_h` Cells; `docs.h_scroll` added; `SbDrag{None,Vert,Horz}` enum + `docs.sb_drag`. +- **mouse.rs Tab::Docs:** `start_docs_scrollbar_drag` (Down: hit-test track, jump, begin drag) + `drag_docs_scrollbar` (Drag: map cursor→offset) + Up clears drag. Both docs bars are now click-to-jump AND draggable. +- Build + `cargo test --lib` green: 333 passed, same 2 pre-existing run-sidebar failures. +- **STILL render-only (task #6):** the tlb/entries + cache-history vertical scrollbars are NOT yet draggable — reuse the same mechanism next. +- **NEEDS INTERACTIVE VERIFICATION:** docs h-scroll appearing on narrow terminals; dragging both docs bars; cache matrix drag still good after the shared-fn refactor. + +--- + +## PENDING CHECKLIST (2026-06-04) — all DISCARDED, to re-do from clean HEAD + +Everything below was implemented + built + tested green this session, then thrown +away with the working-tree revert. Re-implement when UI work resumes (note: the +incoming step-back/journaling refactor may move these files, so re-confirm +locations first). Rough priority order: + +1. **Cache h-scrollbar drag bug.** Matrix h-scrollbar didn't follow the mouse: + on click it jumped to the click ratio but seeded the drag baseline with the + OLD scroll → snap-back on first drag. Fix = one absolute mapping + `column → scroll` shared by click + drag (drop the relative baseline). + Files: `input/mouse.rs` (Tab::Cache drag block), `app/cache_state.rs`, + `app/mod.rs` init. +2. **Reusable scroll primitives** in `view/components`: + `vertical_scrollbar`, `horizontal_scrollbar`, and `scroll_offset_from_pos` + (absolute cursor→offset map, unit-tested). Refactor the cache matrix to use + the shared mapping (delete its local `hscroll_pos_from_column`). +3. **Docs / instr_ref responsiveness (user's explicit ask):** show ALL columns, + never hide; Description flexes to fill; when the natural width overflows, + HORIZONTALLY SCROLL with a bottom h-scrollbar (Paragraph `.scroll((0,h_off))` + on header+sep+rows). Add a vertical scrollbar too. **Both bars must be + mouse-draggable** (click-to-jump + drag) — this was the headline request. + Needs `docs.h_scroll`, scrollbar track-geom cells, and a drag-target enum in + `app/docs_state.rs`; Tab::Docs Down/Drag/Up handling in `input/mouse.rs`. + ⚠️ Do NOT reintroduce column-hiding (`fit_columns`) — user wants scrollbars, + not hidden columns. +4. **Roll out draggable vertical scrollbars** to the other overflowing lists + using the same mechanism: `tlb/entries`, cache history table (`cache/stats`), + and the run sidebar register/mem lists. The run sidebar has bespoke + pinned+separator+offset scroll math AND is where the 2 pre-existing test + failures live — do it carefully, last. +5. **Decide `tables::kv_table`/`kv_styled` fate** — 0 consumers; prune or wire up. +6. **DataTable**: `tlb/stats` is metrics+chart (not a table); `tlb/entries` is the + genuine table to prove `DataTable` on (header goes bold — minor visual change). + +**Pre-existing, NOT ours (verify before blaming any change):** 2 failing tests +`run_dyn_register_view_uses_register_scroll_keys` + +`run_sidebar_wheel_scrolls_registers_in_dyn_register_view` (fail on clean +`7a0c091` too). And clippy `never_loop` at `app/mod.rs:2725`. + +**Always verify interactively (headless can't):** scrollbar drag feel, terminal +resize behavior — open the TUI in a real terminal. diff --git a/docs/en/README.md b/docs/en/README.md index 7e7224b..df4c903 100644 --- a/docs/en/README.md +++ b/docs/en/README.md @@ -53,13 +53,20 @@ Requires Rust 1.75+. No other dependencies. - Export results (`Ctrl+r`) to `.fstats`/`.csv` - CPI configuration: per-class cycle costs (ALU, MUL, DIV, LOAD, STORE, branch, JUMP, FP…) -### Pipeline Simulator (Tab 4) +### Virtual Memory & TLB (Tab 4) +- Four VM modes: **Off**, **Sv32** (auto identity map), **Custom** (a parametric paging scheme you design — levels, index bits, page size), **Manual** (program-driven `satp` + your own page tables) +- Configurable TLB: entries, associativity, six replacement policies, hit latency, miss penalty — with live hit/miss/eviction stats and a rolling hit-rate chart +- Live page-table tree view, per-entry TLB table, and a no-code VM control panel (scheme + page map + TLB geometry) +- Sv32 page faults, trap delegation (`medeleg`/`stvec`), `sret`, and demand paging +- See the **[Virtual memory & TLB guide](virtual-memory.md)** for the full walkthrough + +### Pipeline Simulator (Tab 5) - Five-stage in-order pipeline visualization with per-cycle stepping and run/pause controls - Main and Config subtabs for hazard/history inspection and pipeline configuration - Branch resolve and predictor controls, bypass toggles, and hazard map visualization - Export pipeline configs/results with `Ctrl+e`, `Ctrl+l`, and `Ctrl+r` -### Docs Tab (Tab 5) +### Docs Tab (Tab 6) - Instruction reference for all supported instructions - Run tab key guide @@ -188,6 +195,7 @@ See the **[CLI Reference](cli.md)** for all subcommands and flags. - [Instruction formats (EN)](format.md) — bit layouts, encoding, pseudo-instructions - [Formatos (PT-BR)](../pt-BR/format.md) - [Cache config file reference](cache-config.md) — `.fcache` format, all fields, LN hierarchy, LLM prompt template +- [Virtual memory & TLB guide](virtual-memory.md) — Sv32, the four VM modes, the TLB, page faults, demand paging - `threads-plan.md` — design plan for future multi-core execution using the term `hart` ("hardware thread") to keep the model bare metal and avoid OS-thread semantics - `Program Examples/hart_spawn_visual_demo.fas` — multi-hart example to stress Run/Pipeline views across cores diff --git a/docs/en/cache-config.md b/docs/en/cache-config.md index 778bfb6..6686c56 100644 --- a/docs/en/cache-config.md +++ b/docs/en/cache-config.md @@ -195,6 +195,43 @@ All values are unsigned integers ≥ 0. --- +## Unified TLB (`tlb.*`) + +Raven's MMU is fronted by a single TLB shared by instruction and data translations. Its geometry is saved in the `.fcache` alongside the cache levels, so a config can ship a TLB layout next to the CPI and cache settings. + +``` +# Unified TLB +tlb.entry_count=32 +tlb.associativity=4 +tlb.replacement=lru +tlb.hit_latency=1 +tlb.miss_penalty=20 +``` + +| Key | Type | Valid range | Default | Notes | +|-----|------|-------------|---------|-------| +| `tlb.entry_count` | integer | power of 2, ≥ `tlb.associativity` | `32` | Total TLB entries. | +| `tlb.associativity` | integer | ≥ 1, ≤ `tlb.entry_count` | `4` | Ways per set. `entry_count / associativity` = number of sets. | +| `tlb.replacement` | enum | see note below | `lru` | Eviction policy. | +| `tlb.hit_latency` | integer (cycles) | ≥ 1 | `1` | Cycles charged on every TLB hit. | +| `tlb.miss_penalty` | integer (cycles) | ≥ 0 | `20` | Cycles charged for a page-table walk on a miss. | + +> **Heads up — casing differs from the cache `replacement` key.** The TLB policy is **lowercase**: `lru`, `fifo`, `random`, `lfu`, `clock`, `mru`. (The cache levels use PascalCase `Lru`, `Fifo`, … — they are separate keys.) + +### Virtual-memory mode (`.rcfg` only) + +The VM *mode* and the Custom paging scheme are simulator settings, not cache settings, so they live in the `.rcfg` (sim settings) file rather than the `.fcache`: + +| Key | Type | Values | Notes | +|-----|------|--------|-------| +| `vm_mode` | enum | `off` \| `sv32` \| `custom` \| `manual` | Which VM mode is active (lowercase). | +| `vm_offset_bits` | integer | 12–30 | Page-offset width for **Custom** mode (ignored otherwise). | +| `vm_level_bits` | comma list | e.g. `10,10` | Per-level index widths, top level first, for **Custom** mode. `offset + Σ levels` must equal 32. | + +See the [virtual memory guide](virtual-memory.md) for what these mean and how to use them. + +--- + ## Validation rules Raven will reject the config and show an error if any of these fail: @@ -356,4 +393,10 @@ hit_latency integer 1–999 miss_penalty integer 0–9999 assoc_penalty integer 0–99 (default 1) transfer_width integer 1–512 (default 8) + +tlb.entry_count integer power of 2, ≥ tlb.associativity (default 32) +tlb.associativity integer ≥ 1 (default 4) +tlb.replacement enum lru | fifo | random | lfu | clock | mru (lowercase!) +tlb.hit_latency integer ≥ 1 (default 1) +tlb.miss_penalty integer ≥ 0 (default 20) ``` diff --git a/docs/en/virtual-memory.md b/docs/en/virtual-memory.md index f699137..07a1d72 100644 --- a/docs/en/virtual-memory.md +++ b/docs/en/virtual-memory.md @@ -1,177 +1,599 @@ -# Virtual memory (Sv32) +# Virtual Memory and the TLB (Sv32) > [Leia em Português](../pt-BR/virtual-memory.md) -Raven implements the RISC-V **Sv32** virtual memory scheme: a 2-level page table walked in software by the CPU's MMU, fronted by a configurable TLB. Translation is **off by default** so programs run unchanged; turn it on from **Settings → Virtual Memory** (also persisted in `.rcfg`). +This is study material on **virtual memory in RISC-V** (the **Sv32** scheme) and the **TLB** the simulator puts in front of it. The goal is to teach from the ground up: *why* virtual memory exists, *how* translation happens, *what* the TLB solves, and how you can experiment with all of it in Raven's Virtual Memory tab. -When enabled, the simulator enters **standard VM mode**: it automatically installs an identity page map (VA = PA for the full address space) and activates the TLB — so any program, even a simple loop, immediately shows TLB hits and misses with **no setup code required**. The TLB has its own subtab inside the **Cache** tab where you can configure size, associativity, replacement policy, hit latency, and miss penalty — and watch hits/misses live. +You don't need to open any code to read this. Everything is presented in terms of concepts, diagrams, and example assembly programs. -If you want to study custom address layouts, page faults, or OS-style privilege transitions, you can still write your own page tables and `csrw satp` — your mapping will replace the auto-installed one automatically. +--- + +## Contents + +1. [Why virtual memory exists](#1-why-virtual-memory-exists) +2. [The conceptual model: VA → PA](#2-the-conceptual-model-va--pa) +3. [Page tables: from a giant array to a multi-level tree](#3-page-tables-from-a-giant-array-to-a-multi-level-tree) +4. [Sv32 in detail](#4-sv32-in-detail) +5. [The PTE format](#5-the-pte-format) +6. [Megapages (superpages)](#6-megapages-superpages) +7. [The A and D bits](#7-the-a-and-d-bits) +8. [Permissions and privilege levels](#8-permissions-and-privilege-levels) +9. [Why the TLB exists](#9-why-the-tlb-exists) +10. [TLB organization: sets, ways, indexing](#10-tlb-organization-sets-ways-indexing) +11. [Replacement policies](#11-replacement-policies) +12. [ASIDs and the global flag](#12-asids-and-the-global-flag) +13. [`sfence.vma` and TLB coherence](#13-sfencevma-and-tlb-coherence) +14. [Page faults: causes and trap flow](#14-page-faults-causes-and-trap-flow) +15. [The VM modes: standard, Custom and Manual](#15-the-vm-modes-standard-custom-and-manual) +16. [Performance impact](#16-performance-impact) +17. [The Virtual Memory tab](#17-the-virtual-memory-tab) +18. [Minimal example — standard mode](#18-minimal-example--standard-mode) +19. [Advanced example — custom table](#19-advanced-example--custom-table) +20. [Trap delegation and demand paging](#20-trap-delegation-and-demand-paging) +21. [See also](#21-see-also) --- -## When to enable it +## 1. Why virtual memory exists + +Imagine a computer with no virtual memory. Every program sees physical RAM directly: address `0x1000` in your program is byte `0x1000` on the memory chip. Everything works until you try to run **two programs at once**. + +Four practical problems appear: + +1. **Isolation.** Process A can read or write process B's `0x1000` just by accessing `0x1000`. There is no barrier — a bug in A can corrupt B. +2. **Relocation.** Each program is linked to start at some fixed address (say `0x10000`). If two programs chose the same address, they collide — and even if they don't, it's the OS that decides *where* in RAM each program fits, not the linker. +3. **Fragmentation.** As processes come and go, free memory turns into a jigsaw of holes. There may be 200 MiB free in total but no contiguous 100 MiB block for the next process. +4. **Controlled sharing.** Two instances of `bash` should share the same copy of the code in RAM (saving memory) but need separate stacks (isolation). Without a layer of indirection, it's all or nothing. + +**Virtual memory** solves all four by inserting an indirection between the address the program uses (the **virtual address**, or VA) and the real address in RAM (the **physical address**, or PA). The hardware translates VA → PA on every access. The translation table is controlled by the operating system. As a result: -| Use case | VM on? | -|----------|--------| -| Plain RV32IMAF program, single flat memory | off — same behavior as before | -| Observing TLB activity on any program (standard mode) | **on** | -| Studying page-table walks, A/D bits, page faults | **on** | -| Comparing TLB hit/miss penalties across replacement policies | **on** | -| Running an OS-style kernel (M-mode setup + U-mode user code) | **on** | +- Each process gets its own **virtual address space**. A's `0x1000` translates to a different physical page than B's `0x1000`. +- The linker can assume a fixed address; the OS places the physical pages wherever it likes. +- The "holes" only exist in the physical world. Virtually, each process sees a contiguous space. +- Mapping the *same* physical page at different VAs implements sharing. Marking one copy read-only implements copy-on-write. -The toggle is in `Settings → Virtual Memory`. Default is **off**. With VM off, the MMU is bypassed entirely — no TLB lookups, no walker, no extra cycles. +The indirection has a cost: each memory access becomes potentially several accesses (reading the translation table). The next section shows how the hardware structures that table; later we'll see how the **TLB** cuts the cost to nearly zero in the common case. -### Standard mode (auto identity map) +--- + +## 2. The conceptual model: VA → PA + +The unit of translation isn't a byte; it's a **page** (typically 4 KiB). Addresses within the same page are translated together. + +``` +virtual address (VA) physical address (PA) +┌──────────────┬──────┐ ┌──────────────┬──────┐ +│ Virtual Page │offset│ ─→ │ Physical Page│offset│ +│ Number │ │ │ Number │ │ +│ (VPN) │ │ │ (PPN) │ │ +└──────────────┴──────┘ └──────────────┴──────┘ +``` -Turning on VM is enough. On every assembly, Raven writes a 1024-entry root page table at the last 4 KiB of RAM where each entry is a **megapage** (4 MiB) with identity mapping and full R/W/X/U permissions. The `satp` CSR is set to Sv32 pointing at that table. From that point every fetch and load/store goes through the TLB. +- The **offset** (low bits) passes straight through: byte 7 of virtual page `X` is byte 7 of the matching physical page. +- The hardware only needs to translate VPN → PPN. That translation lives in a structure called the **page table**. -You can still override any part: write a custom `csrw satp` to switch to your own page table; the auto-installed one is just the starting point. +With 4 KiB pages, the offset is 12 bits (`2^12 = 4096`). The remaining bits form the VPN. In RV32 that leaves 20 bits of VPN, so there are `2^20 ≈ 1 million` possible virtual pages per process. --- -## Address translation +## 3. Page tables: from a giant array to a multi-level tree -Sv32 splits a 32-bit virtual address into two 10-bit page-table indices and a 12-bit page offset: +### 3.1 The naive approach +The simplest form would be an array indexed by VPN: given the VPN, read the entry and find the PPN. + +``` +page_table[VPN] = PPN +``` + +For RV32 (20 bits of VPN) that's `2^20` entries × 4 bytes = **4 MiB per process** — and almost all of it zero, because a typical process uses only a fraction of the address space. On 64-bit systems it explodes: billions of entries, terabytes of table. + +### 3.2 The solution: a hierarchy + +Instead of one giant array, we use a **tree**: one table points to subtables, which point to the final pages. Only the branches actually used take up space. + +In Sv32 the hierarchy has 2 levels: a **root table** of 1024 entries, each pointing either to a subtable of 1024 entries (which points to the final pages) or directly to a **megapage** of 4 MiB. + +``` + satp points to the root + ┌─────────────────┐ + │ root PT (1024) │ ← one 4 KiB page + └─┬─────┬─────────┘ + │ │ + ▼ ▼ + ┌────┐ ┌────┐ ← leaf PTs (one per used region) + │1024│ │1024│ + └─┬──┘ └─┬──┘ + ▼ ▼ + physical 4 KiB pages ``` - 31 22 21 12 11 0 + +- Small processes: 1 root + 1–2 leaves = 8–12 KiB of overhead, not 4 MiB. +- Huge processes: pay only for the regions actually populated. + +Walking that tree is called a **page-table walk**, and the hardware does the walk on every access that isn't in the TLB. + +--- + +## 4. Sv32 in detail + +**Sv32** is RISC-V's 32-bit paging scheme. The name means "Supervisor virtual addressing, 32 bits". + +### 4.1 Splitting the virtual address + +The 32-bit VA is cut into three fields: + +``` + 31 22 21 12 11 0 ┌─────────────┬─────────────┬─────────────┐ │ VPN[1] │ VPN[0] │ offset │ +│ (10 bits) │ (10 bits) │ (12 bits) │ └─────────────┴─────────────┴─────────────┘ ``` -Translation walks two levels of PTEs starting at `satp.PPN << 12`: +- `offset` (12 bits) → byte within the page (0..4095). +- `VPN[0]` (10 bits) → index into the **level-0** table (leaf PT). +- `VPN[1]` (10 bits) → index into the **level-1** table (root PT). + +`2^10 = 1024` entries per table × 4 bytes/entry = **4 KiB per table** — exactly the size of a page. Convenient: each table fits in one page and is addressed by a single PPN. + +### 4.2 The walk algorithm -1. **L1 PTE** at `(satp.PPN << 12) + VPN[1] * 4` — if it's a leaf (R/W/X set), the page is a 4 MiB **megapage** and `VPN[0]` becomes part of the offset. -2. **L0 PTE** at `(L1.PPN << 12) + VPN[0] * 4` — must be a leaf. The final 4 KiB page's physical address is `(L0.PPN << 12) | offset`. +Given a virtual address `vaddr` and the `satp` CSR pointing at the root: + +``` +1. pte1_addr = (satp.PPN << 12) + VPN[1] * 4 + pte1 = mem[pte1_addr] + if !pte1.V → page fault + if pte1 is a leaf → MEGAPAGE (4 MiB), jump straight to the checks + else → continue to level 0 + +2. pte0_addr = (pte1.PPN << 12) + VPN[0] * 4 + pte0 = mem[pte0_addr] + if !pte0.V → page fault + if !pte0 is a leaf → page fault (the last-level PTE must be a leaf) + +3. check permissions (R/W/X for the access type) and privilege (U) +4. paddr = (pte0.PPN << 12) | offset +``` -`A` (accessed) and `D` (dirty) bits are auto-set by the walker when an access succeeds — Raven does **not** trap to let the OS update them, so you can experiment without writing a fault handler. +Note: **two RAM accesses per translation**, plus the original access. Without a TLB, each `lw` becomes potentially 3 memory accesses. + +### 4.3 The `satp` CSR + +The **S**upervisor **A**ddress **T**ranslation and **P**rotection register controls translation: + +``` + 31 30 22 21 0 +┌────┬─────────────┬────────────────────────────┐ +│MODE│ ASID │ PPN │ +│ 1b │ 9 bits │ 22 bits │ +└────┴─────────────┴────────────────────────────┘ +``` + +- **MODE**: `0` = Bare (no translation, VA = PA), `1` = Sv32 (translation active). +- **ASID** (Address Space IDentifier): identifies the process. The TLB uses the ASID to avoid mixing translations from different processes (see §12). +- **PPN**: the physical page number where the root table lives. Byte address = `PPN << 12`. + +Writing `satp` **flushes the TLB**, because switching tables would invalidate every cached translation. --- -## Privilege levels +## 5. The PTE format + +Each **P**age **T**able **E**ntry is 32 bits: + +``` + 31 10 9 8 7 6 5 4 3 2 1 0 +┌───────────────────────┬────┬──┬──┬──┬──┬──┬──┬──┬──┐ +│ PPN (22 b) │RSW │ D│ A│ G│ U│ X│ W│ R│ V│ +└───────────────────────┴────┴──┴──┴──┴──┴──┴──┴──┴──┘ +``` + +| Bit | Name | Function | +|-----|------|----------| +| 0 | V | **Valid**. If 0, the entry doesn't count — any walk reaching it page-faults. | +| 1 | R | **Read**. Page can be read (loads). | +| 2 | W | **Write**. Page can be written (stores). | +| 3 | X | **eXecute**. Page can be fetched as instructions. | +| 4 | U | **User**. Page is accessible in U-mode. Without this bit, only S and M may touch it. | +| 5 | G | **Global**. The entry has no ASID — it matches in any address space. | +| 6 | A | **Accessed**. Some access has touched this page since the last clear. | +| 7 | D | **Dirty**. Some store has touched this page. | +| 8–9 | RSW | Reserved for software (the OS may use it freely). | +| 10–31 | PPN | Physical page number (or a pointer to a subtable, if non-leaf). | + +### 5.1 Encoding the value + +The formula is always: + +``` +PTE = (ppn << 10) | flags +``` + +where `ppn = physical_address >> 12`. Classic mistake: **do not confuse the physical address of a table with the PTE value that points to it.** A leaf PT at address `0x2000` has `ppn = 0x2000 >> 12 = 2`, so the non-leaf PTE pointing to it is `(2 << 10) | 0x1 = 0x801` — **not** `0x2001`. + +### 5.2 Leaf vs non-leaf -| Mode | Notation | Behavior under `satp.MODE = Sv32` | -|------|----------|-----------------------------------| -| Machine | M | Physical by default; translated in didactic mode (override) | -| Supervisor | S | Always translated; cannot touch `U=1` pages (`sstatus.SUM` is not modeled) | -| User | U | Always goes through translation; `U=0` PTEs fault | +The distinction is in the R, W, X bits: -> **Didactic mode note:** In real RISC-V hardware, M-mode always uses physical addresses and ignores `satp`. Raven's didactic VM mode deliberately overrides this so that programs written in M-mode (the default) also produce TLB activity — which is the common case for didactic examples. When you write your own page tables and drop into U-mode via `mret` (Manual mode), the behavior matches hardware exactly. +- **Non-leaf (pointer)**: R=W=X=0, V=1. The PPN points to a subtable. +- **Leaf**: at least one of R/W/X set. The PPN points to a data/code page. -A trap (page fault, ecall, ebreak) switches the CPU to M-mode and saves the previous mode in `mstatus.MPP`. `mret` restores the saved mode and resumes at `mepc`. A fault taken in S- or U-mode can instead be **delegated** to a supervisor handler — see [Trap delegation & demand paging](#trap-delegation--demand-paging) below; `sret` is the S-mode counterpart of `mret`. +Common encodings: + +| Hex | Bits | Meaning | +|-----|------|---------| +| `0x01` | V | Pointer to a subtable | +| `0x0F` | V\|R\|W\|X | Kernel page (code+data, no U) | +| `0x1F` | V\|R\|W\|X\|U | User page (code+data) | +| `0x17` | V\|R\|X\|U | User read-execute page | +| `0x0B` | V\|R\|X | Kernel read-execute page | + +### 5.3 Reserved encoding: W=1, R=0 + +The combination `W=1, R=0` is **reserved** and page-faults during the walk. Intuition: it makes little sense to allow writes without reads — code that writes usually wants to read back. The architecture keeps the encoding for future extensions. --- -## Page-fault traps +## 6. Megapages (superpages) + +A level-1 PTE may be a **leaf**. In that case it maps a contiguous **4 MiB** region (1024 × 4 KiB) with a single PTE. We call this a **megapage** (or superpage). + +``` +VA with a megapage: + 31 22 21 0 +┌─────────────┬─────────────────────────┐ +│ VPN[1] │ offset (22 bits) │ +└─────────────┴─────────────────────────┘ + ↑ + vpn[0] + page offset become one offset +``` -When translation fails, the CPU raises one of: +Why does it matter? -| Cause | Meaning | -|-------|---------| -| `12` | Instruction page fault — fetch could not be translated | -| `13` | Load page fault — `lw`/`lh`/`lb` could not be translated | -| `15` | Store page fault — `sw`/`sh`/`sb` could not be translated | +- **Shorter walk**: 1 RAM access instead of 2. +- **Smaller TLB footprint**: 1 entry covers 4 MiB. Without megapages, mapping 4 MiB would need 1024 entries — more than the whole TLB has. +- **Good for large contiguous regions**: kernel text, frame buffers, identity maps. -The trap fills `mcause`, `mtval` (faulting virtual address), `mepc` (faulting PC), sets `mstatus.MPP` to the previous mode, switches to M-mode, and jumps to `mtvec & ~3`. With `mtvec = 0`, Raven prints the fault to the console and halts — handy when you forget to install a handler. +Constraint: the megapage's PPN must be **4 MiB-aligned** (its low 10 bits zero). Otherwise the architecture treats it as "superpage misaligned" and page-faults. -Unless the cause is **delegated** to supervisor mode (see below), in which case the trap fills the `s*` CSRs and vectors through `stvec` instead, staying in S-mode. +Raven's standard mode (§15) uses megapages to cover the whole address space with 1024 single entries, all identity-mapped. --- -## CSRs and system instructions +## 7. The A and D bits -Raven implements just enough Zicsr + privileged ops to drive Sv32: +Every time a page is touched, the hardware should signal it somehow — otherwise the OS has no way to know: -| CSR | Number | Use | -|--------|--------|-----| -| `satp` | `0x180` | Root page-table PPN + ASID + MODE (1 = Sv32, 0 = Bare) | -| `mstatus` | `0x300` | Saved privilege bits (`MPP`) on trap entry/exit | -| `medeleg` | `0x302` | Machine exception delegation — bit `c` set ⇒ cause `c` is handled in S-mode | -| `mideleg` | `0x303` | Machine interrupt delegation (stored; no async interrupts yet) | -| `mtvec` | `0x305` | Trap-vector base address (M-mode) | -| `mepc` | `0x341` | PC saved on M-mode trap | -| `mcause`| `0x342` | M-mode trap cause | -| `mtval` | `0x343` | M-mode trap value (faulting vaddr for page faults) | -| `sstatus` | `0x100` | Supervisor status — `SPP` (bit 8), `SPIE` (bit 5), `SIE` (bit 1) | -| `stvec` | `0x105` | Trap-vector base address (S-mode) | -| `sscratch` | `0x140` | Scratch register for the supervisor handler | -| `sepc` | `0x141` | PC saved on delegated trap | -| `scause`| `0x142` | Delegated trap cause | -| `stval` | `0x143` | Delegated trap value (faulting vaddr) | +- *Which pages can I safely evict to swap?* (needs the A bit for approximate LRU) +- *Has this page been modified since the last write to disk?* (needs the D bit) -> On real RISC-V hardware, `sstatus` is a restricted *view* of `mstatus` — both names read and write the same shared bits. Here it behaves as an independent register instead. This is a deliberate pedagogical simplification: it keeps the delegation path easy to follow, without the shared-bit aliasing real hardware performs. +The RISC-V spec leaves **two implementation options**: -Instructions: `csrrw / csrrs / csrrc` (and the `i` immediate variants), `mret`, `sret`, `sfence.vma`. Writing `satp` flushes the TLB; `sfence.vma` also flushes (ignoring its `rs1`/`rs2` filters in this release). +1. **Hardware sets the bits directly in RAM** when the walker finishes successfully (`A` always, `D` on a store). Simple for the OS. +2. **Hardware raises a trap** when A or D are clear and the OS updates them. More complex, but spares the hardware from writing the PT. + +Raven chooses option 1, for two reasons: + +- It is didactically simpler: you run a program and watch the A/D bits appear in the PTEs without writing a handler. +- It avoids a second trip through the trap just to update a flag. + +Observable consequence: if you inspect the page table in RAM after running a program, you'll see the A and D bits set on the pages that were actually used and written. That's information a real OS would use to decide what goes to swap first. --- -## Configuring the TLB +## 8. Permissions and privilege levels + +### 8.1 The R/W/X + U bits + +Each leaf PTE carries four protection flags: + +| Bit | Access allowed if set | +|-----|-----------------------| +| R | Loads (`lw`, `lh`, `lb`, …) | +| W | Stores (`sw`, `sh`, `sb`, …) | +| X | Instruction fetches | +| U | User mode may touch the page | + +Combined, they give fine-grained access control: code pages running in userland are usually `R + X + U`; a stack is `R + W + U`; kernel code is `R + X` (no U). + +### 8.2 Privilege: M, S, U + +| Mode | Notation | What it can do | +|------|----------|----------------| +| Machine | M | Full access; ignores translation on real hardware | +| Supervisor | S | Kernel; sees pages without `U`. With SUM=0 it can't see `U` pages. | +| User | U | Application; only sees pages with `U=1` | + +Rules under `satp.MODE = Sv32`: + +- **U**: the PTE must have `U=1`, otherwise page fault. +- **S**: the PTE must have `U=0`. +- **M**: real hardware **bypasses** the MMU entirely in M-mode. + +> **Raven's didactic override:** in the standard (Sv32) and Custom modes, the MMU also translates in M-mode. This is deliberate: most educational programs run in M (there's no privilege setup), so without this override the TLB would stay silent. When you install your own tables and drop into U via `mret` (Manual mode), behavior matches real hardware again. -The TLB UI lives at **Cache → TLB** with three subviews: +### 8.3 The page-fault cause -- **Stats** — hit rate, total hits/misses, page faults, and a rolling 300-cycle hit-rate history. -- **Config** — entries (power of two), associativity, replacement policy (LRU / FIFO / Random / Clock / LFU / MRU), hit latency, miss penalty. Apply to commit. -- **Entries** — per-entry table: VPN | PPN | RWXU | ASID | V | G | A | D | megapage. +The trap cause depends on the access type: -Configuration persists in `.rcfg` (via Cache export/import) so you can ship a TLB layout next to your CPI and cache configs. +| Cause | Type | +|-------|------| +| 12 | Instruction page fault (fetch failed) | +| 13 | Load page fault (load failed) | +| 15 | Store page fault (store failed) | --- -## Performance impact +## 9. Why the TLB exists -Every fetch and load/store gets two pieces of latency from the MMU: +Let's count memory accesses without a TLB. -- **Hit:** `tlb.hit_latency` cycles (default `1`). -- **Miss:** `tlb.miss_penalty` cycles for the walk (default `20`), plus any extra cycles the RAM walker spends fetching PTEs. +A simple `lw t0, 0(t1)`: -In **pipeline mode** the MMU stall lands in `if_stall_cycles` or `mem_stall_cycles` on the corresponding pipeline slot — visible as red MEM/IF stretches in the Gantt view. In **interpreter mode** the stall is added to `extra_cycles` and shows up in `total_program_cycles` / CPI. +1. **Fetch** of the instruction: 1 RAM read → but the PC must be translated → 2 PT accesses + 1 read = **3 accesses**. +2. **Load** of the operand: 1 RAM read → translate `t1` → 2 PT accesses + 1 read = **3 accesses**. + +Total: **6 RAM accesses** to run an instruction that originally cost 2. **A 3× slowdown.** + +And it isn't just slowness: each PT access is also a cache-miss candidate, and the PT tree competes with program data for the D-cache. + +### 9.1 The observation that saves everything + +Programs have **locality**: the same VPN repeats over and over in short time windows (a loop over an array, sequential instruction fetch, etc.). If we cache the `VPN → PPN` translation, the walk happens **once** and the next N accesses to that page are essentially free. + +That cache is called the **Translation Lookaside Buffer (TLB)**. It is the single most important structure for the performance of any system with paging. + +### 9.2 Metrics to watch + +- **Hit rate**: the fraction of translations served from the TLB. Well-behaved programs stay above 99%. +- **Miss penalty**: cycles to do a walk. In Raven, default `20` cycles. +- **Hit latency**: cycles to confirm a hit. Default `1` cycle. + +The TLB's Stats subview plots the hit rate in a rolling 300-cycle window — useful for spotting distinct phases (warmup vs steady state, a working-set change, etc.). --- -## PTE format +## 10. TLB organization: sets, ways, indexing -Each 32-bit PTE stores the **Physical Page Number** in the upper bits and flags in the lower bits: +The TLB is a small cache, so it inherits the same terminology as regular caches: **sets**, **ways**, **associativity**, **replacement policies**. -``` - 31 10 9 8 7 6 5 4 3 2 1 0 -┌───────────────────────┬────┬──┬──┬──┬──┬──┬──┬──┬──┐ -│ PPN (22 b) │RSW │ D│ A│ G│ U│ X│ W│ R│ V│ -└───────────────────────┴────┴──┴──┴──┴──┴──┴──┴──┴──┘ -``` +### 10.1 Set-associative -**Encoding formula:** +Each VPN maps to **one specific set** (via a hash). Within the set, any of the **N ways** (slots) may hold the entry. ``` -PTE value = (ppn << 10) | flags + ┌── way 0 ──┬── way 1 ──┬── way 2 ──┬── way 3 ──┐ +VPN → hash → set k → │ entry │ entry │ entry │ entry │ + └───────────┴───────────┴───────────┴───────────┘ + all N ways compared in parallel ``` -Where `ppn = physical_address >> 12` (physical page number of the target page). +- **Hit**: some way in the set has a matching `vpn` and compatible `asid`. +- **Miss**: no way matches → walk → install into some way (evicting someone if the set is full). + +### 10.2 Megapages and indexing + +A megapage covers 1024 consecutive VPNs. If it were placed only in the set for the *starting* VPN, any probe for a VPN in the middle of the megapage would miss. To avoid that, megapages use a different indexing scheme than 4 KiB pages — effectively living in "their own" sets — and the TLB consults both schemes on every lookup, so large entries are found by all the VPNs they cover. + +You don't have to think about this when writing programs. The detail matters for understanding why a single megapage in standard mode can serve a whole program with a hit rate near 100%. + +### 10.3 Associativity trade-offs + +| Associativity | Advantage | Cost | +|---------------|-----------|------| +| 1-way (direct-mapped) | Simplest hardware | Vulnerable to conflict misses | +| N-way (typically 4–8) | Tolerates collisions | More parallel comparators | +| Fully associative | No conflict misses | Expensive to scale | + +Raven's default is **32 entries, 4-way** → 8 sets. + +--- + +## 11. Replacement policies + +When a set is full, which entry is evicted? Raven offers the same six policies as the D-cache: + +| Policy | Evicts | Best for… | +|--------|--------|-----------| +| **LRU** (Least Recently Used) | The least recently accessed | Default; good at almost everything | +| **FIFO** | The longest-installed entry | Simple hardware, low overhead | +| **LFU** (Least Frequently Used) | The least frequently accessed | Stable working sets | +| **Clock** (second-chance) | Approximates LRU with 1 bit per entry | Classic LRU-vs-FIFO compromise | +| **MRU** (Most Recently Used) | The most recently accessed | Large sequential streams | +| **Random** | Random | Baseline; surprisingly OK | + +Changing the policy at runtime: go to **Virtual Memory → TLB → Settings**, pick the policy, Apply. Apply resets the TLB (all entries become invalid), so the next run starts cold. + +Experiment tip: run the same program twice — once with LRU, once with MRU. Compare the hit rate in the Stats subview. For common access patterns (loops, arrays) LRU wins by a lot. For large sequential scans bigger than the TLB, MRU can surprise you. + +--- + +## 12. ASIDs and the global flag + +### 12.1 The problem + +Imagine two processes, A and B. Both have a translation for VPN `0x1000`, but to different physical pages. If the TLB caches A's translation and the OS switches to B, the next time B accesses `0x1000` it will *hit A's entry* — and read/write the wrong physical page. Catastrophe. + +### 12.2 The traditional fix: full flush + +On every process switch, flush the whole TLB. It works, but throws away useful work — entries for shared pages (kernel, libc) would have to be re-validated. + +### 12.3 The better fix: ASIDs + +Each process gets a unique number (**Address Space ID**) — 9 bits in Sv32. The current ASID lives in `satp`. Each TLB entry carries the ASID it was installed with, and a match is valid only if both `vpn` AND `asid` match. + +Result: a process switch *doesn't need* to flush the TLB. Old entries sleep until they are naturally evicted. + +### 12.4 The G (global) flag + +Pages used by all processes (kernel mappings, for example) can have `G=1`. Global entries **ignore the ASID** on a match — matching the VPN is enough. This saves TLB space because you don't need one entry per ASID. + +--- + +## 13. `sfence.vma` and TLB coherence + +The TLB is a cache of something that lives in RAM (the page table). If the OS modifies the PT — installs a new page, changes permissions, pages something out — old TLB entries become **stale**. + +The **`sfence.vma`** instruction signals the hardware: "invalidate the cached translations; I touched the PT". Variants: + +- `sfence.vma rs1=x0, rs2=x0` → flush everything. +- `sfence.vma rs1=vaddr, rs2=x0` → flush only the `vaddr` entry. +- `sfence.vma rs1=x0, rs2=asid` → flush only the given ASID. +- `sfence.vma rs1=vaddr, rs2=asid` → flush the specific entry. + +Raven implements `sfence.vma` as a **full flush** in this release (`rs1`/`rs2` ignored), which is correct but not optimized. Writing `satp` also flushes, since switching tables invalidates everything. + +--- + +## 14. Page faults: causes and trap flow + +When translation fails — invalid PTE, wrong permission, insufficient privilege, misaligned megapage, reserved encoding — the hardware raises a trap. + +### 14.1 Causes + +| `mcause` | Type | +|----------|------| +| 12 | Instruction page fault | +| 13 | Load page fault | +| 15 | Store page fault | + +### 14.2 The trap flow + +1. Translation fails during a fetch, load or store. +2. The hardware saves the fault context in the CSRs: + - `mcause ← cause` (12, 13 or 15) + - `mtval ← vaddr` that faulted — the handler uses this to learn **which** address caused the fault + - `mepc ← PC` of the faulting instruction — so `mret` returns here + - `mstatus.MPP ← current mode` — so `mret` restores the privilege +3. The hardware switches to M-mode and jumps to `mtvec & ~3` (direct mode; vectored mode is not covered). +4. Your handler decides what to do and returns with `mret`. +5. If `mtvec = 0` (you forgot to set it), Raven prints the fault to the console and halts — so you aren't left chasing a bug. + +Unless the cause is **delegated** to supervisor mode (`medeleg`): in that case the trap fills the `s*` CSRs and vectors through `stvec`, staying in S-mode. See [§20](#20-trap-delegation-and-demand-paging). + +### 14.3 What a real OS would do + +A real OS usually handles the fault like this: + +- If the page was paged out to swap → bring it back from disk, update the PT, return with `mret`. The program never notices. +- If the region is valid but not yet allocated (demand paging) → allocate a physical page, map it, return. +- If the access is genuinely invalid → kill the process (SIGSEGV on Unix). -| PTE type | R W X | Meaning | -|----------|-------|---------| -| Pointer (non-leaf) | 0 0 0 | Points to the next-level table | -| Leaf | at least one set | Maps a page; subject to permission checks | +Raven has no swap and no page allocator — it gives you the primitives to experiment by writing your own handlers. -**Common flag combinations:** +--- + +## 15. The VM modes: standard, Custom and Manual + +Virtual memory in Raven isn't a simple on/off switch: you pick **one of four modes**, each meant for a different moment in your learning. The selector is in the **global Settings tab** or in the **Virtual Memory → Settings** subview (see §17) — either one cycles through the modes. The choice is persisted in the `.rcfg`. + +| Mode | What it does | When to use it | +|------|--------------|----------------| +| **Off** | No translation: VA = PA, the MMU is bypassed and the TLB is left untouched. No extra cycles. | Plain program, flat memory — same behavior as having VM disabled. | +| **Sv32** | The didactic **standard** mode: auto-installs an Sv32 identity map (10+10+12) and forces translation even in M-mode. | See TLB activity on **any** program, with no setup code. | +| **Custom** | Like Sv32, but with a **parametric paging scheme** you design (number of levels, index bits per level, offset). | Test **other ways** of paging — bigger/smaller pages, more/fewer levels. | +| **Manual** | Real RISC-V: M-mode bypasses, the **program** drives `satp` and its own tables. | Study page faults, privilege transitions and hardware-accurate demand paging (§19–§20). | + +### 15.1 Standard mode (Sv32) — experiment with no boilerplate + +Turning translation on with no configuration would be useless: `satp` would be zero (Bare), the initial privilege is M, and nothing would be translated. You'd have to build a page table just to see any TLB activity. The **Sv32** mode fixes this: + +1. On assembly, Raven automatically writes an identity **megapage** map covering the whole space, with `R|W|X|U|V` permissions. +2. `satp` is set to Sv32 pointing at that table. +3. Translation is forced even in M-mode, so any program shows TLB activity. + +Result: **any program**, even a simple `addi`/`blt` with no CSRs at all, produces TLB activity. Pick the mode, assemble, run — done. This lets you study translation-cache behavior (hit rate, replacement policies, the effect of TLB size) before learning to build tables, set `mtvec`, write a handler, and drop into U-mode. + +### 15.2 Custom mode — design your own paging scheme + +The **Custom** mode is the "anything is possible" mode: it installs an auto map like Sv32, but the **shape** of the translation is yours. A paging scheme slices the 32-bit virtual address into fields: + +- **offset bits** — set the page size (`page = 2^offset bytes`; `12` → 4 KiB, `22` → 4 MiB). +- **one index per level** — how many bits select an entry at each page-table level (one level per tree depth; 1 to 4 levels). + +The **one hard rule**: `offset + Σ (index bits of each level) = 32`. The panel shows the page size, depth, and sum with ✓/✗ live — if it's red, `apply` is refused. -| Hex | Bits set | Typical use | -|-----|----------|-------------| -| `0x01` | V | Non-leaf pointer to next-level table | -| `0x0F` | R\|W\|X\|V | Kernel code+data page (no U) | -| `0x1F` | R\|W\|X\|U\|V | User code+data page | -| `0x17` | R\|X\|U\|V | User read-execute (no W) | +Examples to try: -> **Common mistake:** do not confuse the physical *address* of a table with the PTE *value* that points to it. -> A leaf PT at physical address `0x2000` has `ppn = 0x2000 >> 12 = 2`, so the non-leaf PTE value is -> `(2 << 10) | 0x1 = 0x801` — **not** `0x2001`. +| offset | levels | sum | result | +|--------|--------|-----|--------| +| 12 | L1=10, L0=10 | 32 | plain Sv32: 4 KiB pages, 2-level walk | +| 22 | L0=10 | 32 | 4 MiB megapages, single-level walk | +| 12 | L2=8, L1=6, L0=6 | 32 | three levels, smaller per-level tables | + +Bigger/fewer pages → shorter walks and smaller TLB footprint, but coarser mapping. More levels → smaller tables and a deeper tree. It's exactly the trade-off real ISAs face — and here you change it, hit `apply`, reassemble and compare the hit rate in the Stats subview, all without writing assembly. + +### 15.3 Manual mode — the program in charge + +When you want hardware-accurate behavior — no didactic override, with the program installing its own tables and handling faults — use **Manual** mode. Here M-mode bypasses the MMU; translation only kicks in after a `csrw satp` (Sv32) and a drop into U/S-mode. It's the mode required by the advanced examples in §19 and §20. --- -## Standard mode — minimal observable example +## 16. Performance impact + +Every translation adds cycles. Where they show up depends on the execution mode. + +### 16.1 Pipeline mode + +- A **fetch** that misses the TLB → a stall in the **IF** slot (shows as a red stretch in the Gantt view). +- A **load/store** that misses → a stall in the **MEM** slot. +- Hits add `hit_latency` to the same slot. Default `1` cycle — usually overlaps with other latencies. -With VM enabled, **no setup code is needed**. This is enough to see TLB activity: +### 16.2 Interpreter mode + +With no pipeline, the extra cycles become part of the total and feed directly into `total_program_cycles` and CPI. You see the impact in **CPI** on the status bar. + +### 16.3 Quick heuristic + +- Hit rate > 99% → negligible impact. +- Hit rate 90–99% → noticeable; may be worth raising `entry_count` or associativity. +- Hit rate < 90% → the working set doesn't fit; switch to megapages or grow the TLB. + +The Stats subview shows the rolling chart and the totals. Compare before/after a config change using **Apply + Reset Stats**. + +--- + +## 17. The Virtual Memory tab + +The **Virtual Memory** tab is the central panel for everything this document explains. The top header has **four** subviews — **Status · Tree · Settings · TLB** — and the **TLB** subview opens a second header with **Stats · Entries · Settings**. Use `Tab` to cycle. + +### Status +- Live state of `satp` (MODE, ASID, root PPN) and the current privilege level. +- A chip says whether translation is active: `vm=off`, `vm=on · translating`, or `vm=on · inactive` (when `satp=Bare` or privilege is M in Manual mode). It's the quick diagnostic for "why is the TLB empty?". + +### Tree +- The **live page table**, read straight from RAM starting at `satp.PPN`, walked along the active scheme's N levels: pointer PTEs expand into child tables; leaves at any level are (super)pages; long runs of uniform leaves collapse into one summary line. PTEs cached in the TLB are marked **●TLB**. +- It is **read-only** — to change the map or the scheme, use the Settings subview. It's your window into what the MMU actually sees, invaluable when a mapping isn't taking effect. + +### Settings (the VM control panel) +Where you reshape virtual memory **without writing a line of code**. Three blocks, top to bottom: + +1. **Paging scheme** — the shape of the translation (offset + levels). Editable in **Custom** mode; see §15.2. +2. **Page map** — what the auto map installs: `kind` (identity = VA→VA, or offset = shift physical by a fixed delta, handy to watch VPN and PPN diverge in Entries), `R/W/X/U` permissions, the `G` (global) flag, and the `ASID`. +3. **TLB geometry** — a shortcut to the same fields as TLB → Settings. + +Everything is staged: edits only take effect when you hit **apply** (which reinstalls the map and re-points `satp`). **flush tlb** just drops cached translations without touching the map. It's the safe sandbox: break the mapping here and nothing crashes — you just see faults you can reason about. + +### TLB → Stats +- Hit-rate gauge and counters: `Hits`, `Misses`, `Evictions`, `Page Faults`. +- A rolling 300-cycle history of the hit rate. `r` resets the counters; `p` pauses. + +### TLB → Entries +- Per-entry table: `VPN → PPN | R/W/X/U | ASID | V | G | A | D | mega`. +- Useful to confirm a page was cached, or to watch the A/D bits turn on as the program runs. `↑`/`↓` or the mouse wheel scroll the list. + +### TLB → Settings +- `Entries` (power of two), `Associativity`, `Replacement Policy`, `Hit Latency`, `Miss Penalty`, plus small/med/large presets. +- **Apply** commits and resets the TLB; **flush** only invalidates entries. +- Save/load config via Cache export/import (`.fcache` / `.rcfg`); the `[tlb]` block loads along with it (see [Cache config](cache-config.md)). + +--- + +## 18. Minimal example — standard mode + +With VM enabled (Sv32 mode), this is already enough: ```asm .text @@ -179,48 +601,51 @@ With VM enabled, **no setup code is needed**. This is enough to see TLB activity li t1, 100 loop: addi t0, t0, 1 - blt t0, t1, loop # every load/store and fetch hits the TLB + blt t0, t1, loop # every fetch and load/store goes through the TLB li a0, 0 li a7, 93 ecall # exit ``` -Enable **Settings → Virtual Memory** before assembling and watch the TLB Stats subtab fill up. +1. Pick the **Sv32** mode (in the global Settings or **Virtual Memory → Settings**) before assembling. +2. Assemble and run. +3. Go to **Virtual Memory → TLB → Stats** and watch the hit rate climb as the loop reuses its pages. +4. Visit **Entries** to confirm the code's `vpn` appears with `X=1` and `A=1`. --- -## Custom page tables (advanced) +## 19. Advanced example — custom table -If you want to study custom address layouts, page faults, or a real OS-style privilege transition, you can set up your own page tables. Write `csrw satp` to install them; the TLB is flushed automatically and your mapping takes over. +To study page faults, privilege transitions, or your own layouts, pick **Manual** mode (in **Virtual Memory → Settings**) and write your own table — with no didactic override, the program is in charge. ```asm -# Maps VA 0x0000 → PA 0x0000 (R|W|X|U, 4 KiB), then drops into U-mode. +# Maps VA 0x0000 → PA 0x0000 (R|W|X|U, 4 KiB) and drops into U-mode. # -# Memory layout (chosen to avoid overlap with code at 0x0000): -# 0x1000 — root page table (root PT: PPN = 1) -# 0x2000 — leaf page table (leaf PT: PPN = 2) +# Layout (chosen to avoid overlapping the code at 0x0000): +# 0x1000 — root PT (PPN = 1) +# 0x2000 — leaf PT (PPN = 2) .text boot: - # ── 1. Write root PTE ────────────────────────────────────────────── + # ── 1. Write the root PTE ────────────────────────────────────────── # Non-leaf pointer: PPN=2 (leaf PT @ 0x2000), V=1 # Value = (2 << 10) | 0x1 = 0x801 li t0, 0x801 li t1, 0x1000 # root PT lives at PA 0x1000 sw t0, 0(t1) # root_pt[VPN1=0] = 0x801 - # ── 2. Write leaf PTE ────────────────────────────────────────────── + # ── 2. Write the leaf PTE ────────────────────────────────────────── # Leaf: PPN=0 (PA 0x0000), R|W|X|U|V = 0x1F # Value = (0 << 10) | 0x1F = 0x1F li t2, 0x1F li t3, 0x2000 # leaf PT lives at PA 0x2000 sw t2, 0(t3) # leaf_pt[VPN0=0] = 0x1F - # ── 3. Install satp: Sv32 (bit 31), ASID=0, root PPN=1 ──────────── + # ── 3. Install satp: Sv32 (bit 31), ASID=0, root PPN=1 ───────────── li t0, 0x80000001 # bit31=Sv32 | PPN=1 csrw satp, t0 # writing satp flushes the TLB - # ── 4. Set up mret to drop into U-mode ──────────────────────────── + # ── 4. Set up mret to drop into U-mode ───────────────────────────── la t0, user_entry csrw mepc, t0 li t0, 0 # mstatus.MPP = 0b00 = U @@ -229,20 +654,39 @@ boot: user_entry: # Translation active (satp=Sv32, priv=U) — hardware-accurate behavior. - # Every fetch and load/store goes through the TLB. nop halt ``` -The end-to-end shape — PTE layout, fault routing through `mtvec`, `mret` back to U-mode — all fits together exactly as laid out above: build the tables, point `satp` at the root, then drop into U-mode where every fetch and load/store is translated. +Variations to experiment with: + +- Swap `0x1F` for `0x17` (no W) and try a `sw` — store page fault `15`. +- Swap for `0x0F` (no U) — fault `13` in U-mode (no user permission). +- Point the root pointer at `0x2001` instead of `0x801` (wrong PPN) — watch the walker miss. +- Set `mtvec` to a handler that prints `mcause`/`mtval` before calling `mret`. --- -## Trap delegation & demand paging +## 20. Trap delegation and demand paging -So far every fault has gone to M-mode through `mtvec`. Real operating systems run their fault handler in **supervisor** mode and reserve machine mode for firmware. Raven models this with **trap delegation**: set bit `c` of `medeleg` and a fault with cause `c`, taken in S- or U-mode, is routed to the supervisor handler at `stvec` instead — filling `sepc` / `scause` / `stval` and recording the previous mode in `sstatus.SPP`. `sret` is the return instruction, mirroring `mret` over `sstatus`. +So far every fault has gone to M-mode through `mtvec`. Real operating systems run their fault handler in **supervisor** mode and reserve machine mode for firmware. Raven models this with **trap delegation**: set bit `c` of `medeleg` and a fault with cause `c`, taken in S- or U-mode, is routed to the supervisor handler at `stvec` instead — filling `sepc` / `scause` / `stval` and recording the previous mode in `sstatus.SPP`. The return is `sret`, mirroring `mret` over `sstatus`. + +### 20.1 The CSRs and the instruction + +| CSR | Number | Use | +|-----|--------|-----| +| `medeleg` | `0x302` | Exception delegation — bit `c` set ⇒ cause `c` handled in S-mode | +| `mideleg` | `0x303` | Interrupt delegation (stored; no async interrupts yet) | +| `sstatus` | `0x100` | Supervisor status — `SPP` (bit 8), `SPIE` (bit 5), `SIE` (bit 1) | +| `stvec` | `0x105` | Trap-vector base address (S-mode) | +| `sscratch` | `0x140` | Scratch register for the supervisor handler | +| `sepc` | `0x141` | PC saved on a delegated trap | +| `scause` | `0x142` | Delegated trap cause | +| `stval` | `0x143` | Delegated trap value (faulting vaddr) | + +> On real RISC-V hardware, `sstatus` is a restricted *view* of `mstatus` — both names read and write the same shared bits. Here it behaves as an independent register instead. This is a deliberate pedagogical simplification: it keeps the delegation path easy to follow, without the shared-bit aliasing real hardware performs. -This unlocks the classic **demand-paging** pattern: +### 20.2 The demand-paging pattern 1. User code touches a page that isn't mapped yet → **load/store page fault** (cause 13 / 15). 2. Because the cause is delegated (`medeleg`), the CPU vectors to the supervisor handler in S-mode. @@ -251,7 +695,7 @@ This unlocks the classic **demand-paging** pattern: > **⚠ Walker / cache coherence.** The page-table walk reads PTEs **directly from RAM**, while a write-back D-cache may hold a freshly stored value without having written it out yet. So a handler that writes a PTE with a normal `sw` can leave that PTE sitting in the cache: the retried walk still reads the old (empty) entry from RAM and faults forever. For demand-paging programs, **disable the cache** (Cache tab) or switch the D-cache to **write-through** so the handler's store reaches RAM before the walk re-runs. -### Setup (in the M-mode boot code) +### 20.3 Setup (in the M-mode boot code) ```asm # Delegate load (cause 13) and store (cause 15) page faults to S-mode. @@ -264,14 +708,12 @@ This unlocks the classic **demand-paging** pattern: # ... build the initial page tables, csrw satp, then mret into U-mode ... ``` -### The supervisor handler +### 20.4 The supervisor handler ```asm page_fault_handler: - # stval holds the faulting virtual address. Compute its leaf-PTE slot, - # write a fresh mapping (PPN of a free frame | R | U | V), then: - csrr t0, stval # faulting VA - # ... derive leaf slot, store the new PTE into the (kernel-mapped) leaf table ... + csrr t0, stval # faulting virtual address + # ... derive the leaf-PTE slot, store the new PTE into the (kernel-mapped) leaf table ... sfence.vma # drop stale TLB entries sret # return to sepc — the faulting access retries ``` @@ -279,14 +721,15 @@ page_fault_handler: A complete round trip looks like this: boot maps the code/handler/page-table pages, drops into U-mode, faults on an unmapped page, the handler maps it, runs `sfence.vma`, `sret`, and the retried load reads the freshly mapped data. Two layout rules from this pattern are worth repeating: - The **handler's code and the page-table pages must be mapped non-`U`** (kernel-only), because S-mode cannot touch `U=1` pages (`SUM` is not modeled). -- The handler edits the page table *under translation*, so the leaf-table page needs its own identity (`VA = PA`) mapping — the simulator stand-in for a kernel direct map. +- The handler edits the page table *under translation*, so the leaf-table page needs its own identity (`VA = PA`) mapping — the simulator's stand-in for a kernel direct map. -To watch it live: set **Settings → Virtual Memory → Manual**, disable the cache, assemble, and open the **TLB → tree** subview to see PTEs appear as the handler installs them. +To watch it live: pick **Manual** mode (in **Virtual Memory → Settings**), disable the cache, assemble, and open the **Virtual Memory → Tree** subview to see PTEs appear as the handler installs them. --- -## See also +## 21. See also -- [Memory map](memory-allocation.md) — physical address layout used as the backing store -- [Cache config](cache-config.md) — `.fcache` / `.rcfg` fields including the `[tlb]` block -- [Pipeline simulation](pipeline.md) — where MMU stalls show up in the Gantt view +- [Memory map](memory-allocation.md) — the physical address layout used as the backing store. +- [Cache config](cache-config.md) — `.fcache` / `.rcfg` fields including the `[tlb]` block. +- [Pipeline simulation](pipeline.md) — where MMU stalls show up in the Gantt view. +- [Cache simulation](cache.md) — the shared terminology (sets, ways, policies) the TLB inherits. diff --git a/docs/pt-BR/README.md b/docs/pt-BR/README.md index db528f8..fea5013 100644 --- a/docs/pt-BR/README.md +++ b/docs/pt-BR/README.md @@ -63,7 +63,14 @@ Tudo vive em uma única TUI: escreva código, monte, execute passo a passo, insp - Exportar resultados (`Ctrl+r`) para `.fstats` / `.csv` - Matriz visual de cache com scroll horizontal e drag por scrollbar -### Aba Pipeline (Aba 4) +### Aba Virtual Memory / TLB (Aba 4) +- Quatro modos de VM: **Off**, **Sv32** (mapa identidade automático), **Custom** (esquema de paginação paramétrico que você desenha — níveis, bits de índice, tamanho de página), **Manual** (`satp` e tabelas dirigidos pelo programa) +- TLB configurável: entradas, associatividade, seis políticas de substituição, hit latency, miss penalty — com hits/misses/evictions ao vivo e gráfico rolante de hit rate +- Árvore da tabela de páginas ao vivo, tabela de entradas da TLB e painel de controle de VM sem código (esquema + page map + geometria da TLB) +- Page faults Sv32, delegação de traps (`medeleg`/`stvec`), `sret` e demand paging +- Veja o **[guia de Memória Virtual & TLB](virtual-memory.md)** para o passo a passo completo + +### Aba Pipeline (Aba 5) - Visualização de pipeline clássico de 5 estágios com execução ciclo a ciclo - Sub-abas Main e Config para hazards, histórico, bypass e predição de desvio - Exportação/importação de `.pcfg` e exportação de `.pstats` / `.csv` com `Ctrl+e`, `Ctrl+l` e `Ctrl+r` @@ -72,7 +79,7 @@ Tudo vive em uma única TUI: escreva código, monte, execute passo a passo, insp - Custos de ciclo por classe: ALU, MUL, DIV, LOAD, STORE, branch taken/not-taken, JUMP, SYSTEM, FP - Configurável diretamente na aba Cache → Config -### Aba Docs (Aba 5) +### Aba Docs (Aba 6) - Referência de instruções e guia da aba Run embutidos no app --- @@ -172,7 +179,8 @@ Veja a **[Referência da CLI](cli.md)** para todos os subcomandos e flags. - [CLI Reference (EN)](../en/cli.md) - [Formatos de instrução (PT-BR)](format.md) — layouts de bits, encoding, pseudoinstruções - [Guia do simulador de cache (PT-BR)](cache.md) — configuração, métricas, exportação -- [Formats (EN)](../en/format.md) | [Cache (EN)](../en/cache.md) +- [Memória Virtual & TLB (PT-BR)](virtual-memory.md) — Sv32, os quatro modos de VM, a TLB, page faults, demand paging +- [Formats (EN)](../en/format.md) | [Cache (EN)](../en/cache.md) | [Virtual memory (EN)](../en/virtual-memory.md) - `threads-plan.md` — plano de design para execução multi-core futura, usando o termo `hart` ("hardware thread") para manter a modelagem em nível de hardware, não de SO - `Program Examples/hart_spawn_visual_demo.fas` — exemplo multi-hart para forçar atividade simultânea nas abas Run e Pipeline diff --git a/docs/pt-BR/cache-config.md b/docs/pt-BR/cache-config.md index e855a01..1a96fb8 100644 --- a/docs/pt-BR/cache-config.md +++ b/docs/pt-BR/cache-config.md @@ -195,6 +195,43 @@ Todos os valores são inteiros sem sinal ≥ 0. --- +## TLB unificada (`tlb.*`) + +A MMU do Raven é fronteada por uma única TLB compartilhada pelas traduções de instrução e de dados. Sua geometria é salva no `.fcache` junto com os níveis de cache, então uma config pode levar um layout de TLB ao lado dos parâmetros de CPI e cache. + +``` +# Unified TLB +tlb.entry_count=32 +tlb.associativity=4 +tlb.replacement=lru +tlb.hit_latency=1 +tlb.miss_penalty=20 +``` + +| Chave | Tipo | Faixa válida | Padrão | Notas | +|-------|------|--------------|--------|-------| +| `tlb.entry_count` | inteiro | potência de 2, ≥ `tlb.associativity` | `32` | Total de entradas da TLB. | +| `tlb.associativity` | inteiro | ≥ 1, ≤ `tlb.entry_count` | `4` | Ways por set. `entry_count / associativity` = número de sets. | +| `tlb.replacement` | enum | ver nota abaixo | `lru` | Política de substituição. | +| `tlb.hit_latency` | inteiro (ciclos) | ≥ 1 | `1` | Ciclos cobrados em cada hit da TLB. | +| `tlb.miss_penalty` | inteiro (ciclos) | ≥ 0 | `20` | Ciclos cobrados pelo walk da tabela de páginas num miss. | + +> **Atenção — a grafia difere da chave `replacement` do cache.** A política da TLB é em **minúsculas**: `lru`, `fifo`, `random`, `lfu`, `clock`, `mru`. (Os níveis de cache usam PascalCase `Lru`, `Fifo`, … — são chaves separadas.) + +### Modo de memória virtual (somente `.rcfg`) + +O *modo* de VM e o esquema de paginação Custom são configurações do simulador, não do cache, então vivem no arquivo `.rcfg` (sim settings), não no `.fcache`: + +| Chave | Tipo | Valores | Notas | +|-------|------|---------|-------| +| `vm_mode` | enum | `off` \| `sv32` \| `custom` \| `manual` | Qual modo de VM está ativo (minúsculas). | +| `vm_offset_bits` | inteiro | 12–30 | Largura do offset de página no modo **Custom** (ignorado nos demais). | +| `vm_level_bits` | lista por vírgula | ex.: `10,10` | Larguras de índice por nível, do topo para a folha, no modo **Custom**. `offset + Σ níveis` deve somar 32. | + +Veja o [guia de memória virtual](virtual-memory.md) para o que significam e como usá-los. + +--- + ## Regras de validação O Raven rejeitará a config e mostrará um erro se qualquer uma delas falhar: @@ -356,4 +393,10 @@ hit_latency inteiro 1–999 miss_penalty inteiro 0–9999 assoc_penalty inteiro 0–99 (padrão 1) transfer_width inteiro 1–512 (padrão 8) + +tlb.entry_count inteiro potência de 2, ≥ tlb.associativity (padrão 32) +tlb.associativity inteiro ≥ 1 (padrão 4) +tlb.replacement enum lru | fifo | random | lfu | clock | mru (minúsculas!) +tlb.hit_latency inteiro ≥ 1 (padrão 1) +tlb.miss_penalty inteiro ≥ 0 (padrão 20) ``` diff --git a/docs/pt-BR/virtual-memory.md b/docs/pt-BR/virtual-memory.md index 5e2838e..d7aae6a 100644 --- a/docs/pt-BR/virtual-memory.md +++ b/docs/pt-BR/virtual-memory.md @@ -2,7 +2,7 @@ > [Read in English](../en/virtual-memory.md) -Este documento é material de estudo sobre **memória virtual no RISC-V** (esquema **Sv32**) e sobre a **TLB** que o simulador coloca na frente dela. A intenção é ensinar do zero: *por que* memória virtual existe, *como* a tradução acontece, *o que* o TLB resolve, e como você pode experimentar tudo isso na aba TLB do Raven. +Este documento é material de estudo sobre **memória virtual no RISC-V** (esquema **Sv32**) e sobre a **TLB** que o simulador coloca na frente dela. A intenção é ensinar do zero: *por que* memória virtual existe, *como* a tradução acontece, *o que* o TLB resolve, e como você pode experimentar tudo isso na aba Virtual Memory do Raven. Você não precisa abrir nenhum código pra ler este texto. Tudo é apresentado em termos de conceitos, diagramas e programas de exemplo em assembly. @@ -24,9 +24,9 @@ Você não precisa abrir nenhum código pra ler este texto. Tudo é apresentado 12. [ASIDs e a flag global](#12-asids-e-a-flag-global) 13. [`sfence.vma` e coerência da TLB](#13-sfencevma-e-coerência-da-tlb) 14. [Page faults: causas e fluxo do trap](#14-page-faults-causas-e-fluxo-do-trap) -15. [Modo padrão: experimentar sem boilerplate](#15-modo-padrão-experimentar-sem-boilerplate) +15. [Os modos de VM: padrão, Custom e Manual](#15-os-modos-de-vm-padrão-custom-e-manual) 16. [Impacto em performance](#16-impacto-em-performance) -17. [A aba TLB do simulador](#17-a-aba-tlb-do-simulador) +17. [A aba Virtual Memory do simulador](#17-a-aba-virtual-memory-do-simulador) 18. [Exemplo mínimo — modo padrão](#18-exemplo-mínimo--modo-padrão) 19. [Exemplo avançado — tabela customizada](#19-exemplo-avançado--tabela-customizada) 20. [Delegação de traps e demand paging](#20-delegação-de-traps-e-demand-paging) @@ -404,7 +404,7 @@ Quando um set está cheio, qual entrada é despejada? O Raven oferece as mesmas | **MRU** (Most Recently Used) | A mais recentemente acessada | Streams sequenciais grandes | | **Random** | Aleatória | Baseline; surpreendentemente OK | -Mudar política em runtime: vá em **Cache → TLB → Settings**, escolha a política, Apply. O Apply reinicia a TLB (todas as entradas viram inválidas), então a próxima execução começa de cold. +Mudar política em runtime: vá em **Virtual Memory → TLB → Settings**, escolha a política, Apply. O Apply reinicia a TLB (todas as entradas viram inválidas), então a próxima execução começa de cold. Dica de experimento: rode o mesmo programa duas vezes — uma com LRU, outra com MRU. Compare o hit rate na subtab Stats. Pra padrões de acesso comuns (loops, arrays), LRU ganha de longe. Pra varreduras sequenciais grandes maiores que a TLB, MRU pode surpreender. @@ -485,21 +485,49 @@ O Raven não tem swap nem alocador de páginas — ele te dá os primitivos pra --- -## 15. Modo padrão: experimentar sem boilerplate +## 15. Os modos de VM: padrão, Custom e Manual -Ligar VM sem nenhuma configuração extra seria inútil: o `satp` ficaria em zero (Bare), o privilégio inicial é M, e nenhuma tradução aconteceria. Você precisaria escrever uma tabela de páginas manualmente só pra ver qualquer atividade na TLB. +A memória virtual no Raven não é um simples liga/desliga: você escolhe **um entre quatro modos**, cada um pensado para um momento diferente do aprendizado. O seletor fica na **aba Settings global** ou na subaba **Virtual Memory → Settings** (ver §17) — em qualquer um dos dois você cicla entre os modos. A escolha é persistida no `.rcfg`. -O Raven resolve isso com o **modo padrão**: +| Modo | O que faz | Quando usar | +|------|-----------|-------------| +| **Off** | Sem tradução: VA = PA, a MMU é ignorada e a TLB fica intacta. Nenhum ciclo extra. | Programa comum, memória plana — comportamento idêntico a ter VM desligada. | +| **Sv32** | Modo **padrão** didático: instala automaticamente um mapa identidade em Sv32 (10+10+12) e força tradução até em M-mode. | Ver atividade de TLB em **qualquer** programa, sem escrever uma linha de setup. | +| **Custom** | Como o Sv32, mas com um **esquema de paginação paramétrico** que você desenha (nº de níveis, bits por nível, offset). | Testar **outras formas** de paginação — páginas maiores/menores, mais/menos níveis. | +| **Manual** | RISC-V real: M-mode bypassa, o **programa** dirige o `satp` e suas próprias tabelas. | Estudar page faults, transição de privilégio e demand paging fiéis ao hardware (§19–§20). | -1. Ao montar um programa com VM ligada, o Raven escreve automaticamente **1024 PTEs de megapágina** no último bloco de 4 KiB da RAM. Cada entrada `i` mapeia o i-ésimo bloco de 4 MiB pra si mesmo — um **identity mapping** completo do espaço de endereçamento — com permissões `R|W|X|U|V`. -2. O CSR `satp` é configurado pra Sv32 apontando pra essa tabela. +### 15.1 Modo padrão (Sv32) — experimentar sem boilerplate + +Ligar tradução sem nenhuma configuração seria inútil: o `satp` ficaria em zero (Bare), o privilégio inicial é M, e nada seria traduzido. Você teria que montar uma tabela de páginas só pra ver qualquer atividade na TLB. O modo **Sv32** resolve isso: + +1. Ao montar um programa, o Raven escreve automaticamente um mapa de **megapáginas** de identidade cobrindo o espaço inteiro, com permissões `R|W|X|U|V`. +2. O `satp` é configurado pra Sv32 apontando pra essa tabela. 3. A tradução é forçada mesmo em M-mode, pra que qualquer programa veja atividade na TLB. -Resultado: **qualquer programa**, mesmo um `addi`/`blt` simples sem CSR algum, gera atividade na TLB. Você liga o switch em **Settings → Virtual Memory**, monta, roda — pronto. +Resultado: **qualquer programa**, mesmo um `addi`/`blt` simples sem CSR algum, gera atividade na TLB. Você seleciona o modo, monta, roda — pronto. Assim dá pra estudar comportamento de cache de tradução (hit rate, políticas de substituição, efeito do tamanho do TLB) sem antes ter que aprender a montar tabelas, configurar `mtvec`, escrever um handler e cair em U-mode. + +### 15.2 Modo Custom — desenhe o seu próprio esquema de paginação + +O modo **Custom** é o "tudo é possível": ele instala um mapa automático como o Sv32, mas a **forma** da tradução é sua. Um esquema de paginação fatia o endereço virtual de 32 bits em campos: + +- **offset bits** — define o tamanho da página (`página = 2^offset bytes`; `12` → 4 KiB, `22` → 4 MiB). +- **um índice por nível** — quantos bits selecionam uma entrada em cada nível da tabela (um nível por profundidade da árvore; 1 a 4 níveis). + +A **única regra rígida**: `offset + Σ (bits de índice de cada nível) = 32`. O painel mostra ao vivo o tamanho da página, a profundidade e a soma com ✓/✗ — se estiver vermelho, o `apply` é recusado. -Por que isso é didaticamente valioso? Porque você consegue estudar comportamento de cache de tradução (hit rate, políticas de substituição, efeito do tamanho do TLB) sem primeiro ter que aprender a montar tabelas de página, configurar `mtvec`, escrever um handler de fault e cair em U-mode. Tudo isso é importante e está na §19 — mas o modo padrão te deixa começar pelo TLB e ir pros detalhes depois. +Exemplos para experimentar: -Quando você está pronto pra estudar layouts customizados, basta escrever sua própria tabela e fazer `csrw satp` apontando pra ela. A TLB é flushada automaticamente e seu mapeamento substitui o automático. +| offset | níveis | soma | resultado | +|--------|--------|------|-----------| +| 12 | L1=10, L0=10 | 32 | Sv32 puro: páginas de 4 KiB, walk de 2 níveis | +| 22 | L0=10 | 32 | megapáginas de 4 MiB, walk de 1 nível | +| 12 | L2=8, L1=6, L0=6 | 32 | três níveis, tabelas menores por nível | + +Páginas maiores/menos numerosas → walks mais curtos e menor pegada na TLB, mas mapeamento mais grosso. Mais níveis → tabelas menores e árvore mais profunda. É exatamente o trade-off que as ISAs reais enfrentam — e aqui você muda, dá `apply`, remonta e compara o hit rate na subaba Stats, tudo sem escrever assembly. + +### 15.3 Modo Manual — o programa no comando + +Quando você quer o comportamento fiel ao hardware — sem o override didático, com o programa instalando suas próprias tabelas e tratando faults — use o modo **Manual**. Aqui M-mode bypassa a MMU; só depois de `csrw satp` (Sv32) e de cair em U/S-mode a tradução entra em ação. É o modo necessário para os exemplos avançados das §19 e §20. --- @@ -527,31 +555,39 @@ A subtab Stats mostra o gráfico rolante e os totais. Compare antes/depois de mu --- -## 17. A aba TLB do simulador +## 17. A aba Virtual Memory do simulador + +A aba **Virtual Memory** é o painel central de tudo que este documento explica. O cabeçalho de topo tem **quatro** subviews — **Status · Tree · Settings · TLB** — e a subview **TLB** abre um segundo cabeçalho com **Stats · Entries · Settings**. Use `Tab` para ciclar. + +### Status +- Estado vivo do `satp` (MODE, ASID, root PPN) e o nível de privilégio atual. +- Um chip diz se a tradução está ativa: `vm=off`, `vm=on · translating`, ou `vm=on · inactive` (quando `satp=Bare` ou o privilégio é M no modo Manual). É o diagnóstico rápido pra "por que a TLB está vazia?". + +### Tree +- A **tabela de páginas ao vivo**, lida direto da RAM a partir de `satp.PPN`, percorrida seguindo os N níveis do esquema ativo: PTEs ponteiro expandem em tabelas filhas; folhas em qualquer nível são (super)páginas; sequências longas de folhas uniformes colapsam numa linha-resumo. PTEs que estão cacheadas na TLB ganham a marca **●TLB**. +- É **somente-leitura** — para alterar o mapa ou o esquema, use a subview Settings. É a sua janela para o que a MMU realmente enxerga, essencial quando um mapeamento não surte efeito. -A aba **TLB / Virtual Memory** tem 4 subtabs, na ordem **stats · entries · vm · settings**: +### Settings (painel de controle de VM) +Onde você remodela a memória virtual **sem escrever uma linha de código**. Três blocos, de cima para baixo: -### Stats -- Gauge de hit rate. -- Contadores: `Hits`, `Misses`, `Evictions`, `Page Faults`. -- Histórico rolante de 300 ciclos com o hit rate. -- Atalho `r` reseta os contadores; `p` pausa. +1. **Esquema de paginação** — o formato da tradução (offset + níveis). Editável no modo **Custom**; ver §15.2. +2. **Page map** — o que o mapa automático instala: `kind` (identity = VA→VA, ou offset = desloca o físico por um deslocamento fixo, útil pra ver VPN e PPN divergirem em Entries), permissões `R/W/X/U`, flag `G` (global) e `ASID`. +3. **Geometria da TLB** — atalho pros mesmos campos da TLB → Settings. -### Entries -- Tabela por entrada: `VPN | PPN | RWXU | ASID | V | G | A | D | mega`. -- Útil pra confirmar que uma página específica foi cacheada, ou pra ver bits A/D ligando conforme o programa roda. -- `↑` / `↓` rolam a lista. +Tudo é preparado em rascunho: as edições só surtem efeito quando você aperta **apply** (que reinstala o mapa e reaponta o `satp`). **flush tlb** apenas descarta as traduções cacheadas sem mexer no mapa. É o sandbox seguro: quebre o mapeamento aqui e nada trava — você só vê faults que consegue raciocinar. -### VM (Status) -- Mostra o estado vivo do `satp` (MODE, ASID, root PPN) e o `priv_mode`. -- O chip no header diz: `vm=off`, `vm=on · translating`, ou `vm=on · inactive (satp=Bare ou priv=M)`. Diagnóstico rápido pra "por que a TLB tá vazia?". +### TLB → Stats +- Gauge de hit rate e contadores: `Hits`, `Misses`, `Evictions`, `Page Faults`. +- Histórico rolante de 300 ciclos com o hit rate. `r` reseta os contadores; `p` pausa. -### Settings -- `Entries` (potência de 2), `Associativity`, `Replacement Policy`, `Hit Latency`, `Miss Penalty`. -- **Apply + Reset Stats** ou **Apply Keep History**. -- Salvar/carregar config via export/import do Cache (`.fcache` / `.rcfg`); o bloco `[tlb]` carrega junto. +### TLB → Entries +- Tabela por entrada: `VPN → PPN | R/W/X/U | ASID | V | G | A | D | mega`. +- Útil pra confirmar que uma página foi cacheada, ou pra ver os bits A/D ligando conforme o programa roda. `↑`/`↓` ou a roda do mouse rolam a lista. -`Tab` cicla entre as subtabs na mesma ordem do header. +### TLB → Settings +- `Entries` (potência de 2), `Associativity`, `Replacement Policy`, `Hit Latency`, `Miss Penalty`, mais presets small/med/large. +- **Apply** aplica e reseta a TLB; **flush** só invalida as entradas. +- Salvar/carregar config via export/import do Cache (`.fcache` / `.rcfg`); o bloco `[tlb]` carrega junto (ver [Config de cache](cache-config.md)). --- @@ -571,16 +607,16 @@ loop: ecall # exit ``` -1. Ligue **Settings → Virtual Memory** antes de montar. +1. Selecione o modo **Sv32** (em Settings global ou **Virtual Memory → Settings**) antes de montar. 2. Monte e rode. -3. Vá em **Cache → TLB → Stats** e veja a hit rate subir conforme o loop reutiliza as páginas. +3. Vá em **Virtual Memory → TLB → Stats** e veja a hit rate subir conforme o loop reutiliza as páginas. 4. Visite **Entries** pra confirmar que o `vpn` do código aparece com `X=1` e `A=1`. --- ## 19. Exemplo avançado — tabela customizada -Pra estudar page faults, transição de privilégio, ou layouts próprios, escreva a sua própria tabela. +Pra estudar page faults, transição de privilégio, ou layouts próprios, selecione o modo **Manual** (em **Virtual Memory → Settings**) e escreva a sua própria tabela — sem o override didático, o programa fica no comando. ```asm # Mapeia VA 0x0000 → PA 0x0000 (R|W|X|U, 4 KiB) e cai em U-mode. @@ -687,7 +723,7 @@ Uma viagem de ida e volta completa fica assim: boot mapeia as páginas de códig - O **código do handler e as páginas de tabela precisam ser mapeados como não-`U`** (só kernel), porque S-mode não pode tocar páginas `U=1` (`SUM` não é modelado). - O handler edita a tabela *sob tradução*, então a página da leaf table precisa do próprio mapeamento de identidade (`VA = PA`) — o equivalente do simulador a um direct map de kernel. -Pra ver ao vivo: selecione **Settings → Virtual Memory → Manual**, desligue o cache, monte e abra a subaba **TLB → tree** pra ver as PTEs surgindo conforme o handler as instala. +Pra ver ao vivo: selecione o modo **Manual** (em **Virtual Memory → Settings**), desligue o cache, monte e abra a subview **Virtual Memory → Tree** pra ver as PTEs surgindo conforme o handler as instala. --- diff --git a/src/falcon/cache/cache.rs b/src/falcon/cache/cache.rs index 499d9c2..f914f0e 100644 --- a/src/falcon/cache/cache.rs +++ b/src/falcon/cache/cache.rs @@ -33,6 +33,7 @@ impl CacheLine { } } +#[derive(Clone)] pub(crate) struct CacheSet { pub(crate) lines: Vec, pub(crate) lru_order: VecDeque, // front=MRU, back=LRU (LRU & MRU) @@ -157,6 +158,7 @@ impl CacheSet { // ── Cache ──────────────────────────────────────────────────────────────────── +#[derive(Clone)] pub struct Cache { pub config: CacheConfig, pub(crate) sets: Vec, diff --git a/src/falcon/cache/controller.rs b/src/falcon/cache/controller.rs index 0637717..449dfad 100644 --- a/src/falcon/cache/controller.rs +++ b/src/falcon/cache/controller.rs @@ -45,6 +45,25 @@ enum WritebackSource { Extra(usize), } +/// A clone of every part of the cache subsystem **except the RAM** — the cache +/// levels, the MMU/TLB, and the scalar counters. Captured per journaled step so +/// a step-back can restore cache/TLB/stat state wholesale; the (large) RAM is +/// rewound separately via byte-level pre-images, so it never appears here. +/// +/// For didactic configurations (small or disabled caches) this clone is cheap. +#[derive(Clone)] +pub struct CacheSnapshot { + icache: Cache, + dcache: Cache, + extra_levels: Vec, + mmu: Mmu, + instruction_count: u64, + extra_cycles: u64, + step_count: u64, + bypass: bool, + reservations: HashMap, +} + impl CacheController { pub fn new( icfg: CacheConfig, @@ -160,6 +179,36 @@ impl CacheController { &mut self.ram } + /// Clone the cache subsystem (everything but the RAM) into a + /// [`CacheSnapshot`] for the step journal. See that type's docs. + pub fn snapshot_state(&self) -> CacheSnapshot { + CacheSnapshot { + icache: self.icache.clone(), + dcache: self.dcache.clone(), + extra_levels: self.extra_levels.clone(), + mmu: self.mmu.clone(), + instruction_count: self.instruction_count, + extra_cycles: self.extra_cycles, + step_count: self.step_count, + bypass: self.bypass, + reservations: self.reservations.clone(), + } + } + + /// Restore the cache subsystem from a [`CacheSnapshot`]. The RAM is left + /// untouched — the caller rewinds it separately via its byte pre-images. + pub fn restore_state(&mut self, snap: CacheSnapshot) { + self.icache = snap.icache; + self.dcache = snap.dcache; + self.extra_levels = snap.extra_levels; + self.mmu = snap.mmu; + self.instruction_count = snap.instruction_count; + self.extra_cycles = snap.extra_cycles; + self.step_count = snap.step_count; + self.bypass = snap.bypass; + self.reservations = snap.reservations; + } + pub fn reset_stats(&mut self) { self.icache.reset_stats(); self.dcache.reset_stats(); diff --git a/src/falcon/cache/mod.rs b/src/falcon/cache/mod.rs index 86ecded..d78738f 100644 --- a/src/falcon/cache/mod.rs +++ b/src/falcon/cache/mod.rs @@ -9,7 +9,7 @@ mod stats; pub use cache::{Cache, CacheLineView, CacheSetView}; pub use config::CacheConfig; -pub use controller::CacheController; +pub use controller::{CacheController, CacheSnapshot}; pub use policies::{InclusionPolicy, ReplacementPolicy, WriteAllocPolicy, WritePolicy}; pub use presets::{cache_presets, extra_level_presets}; pub use stats::CacheStats; diff --git a/src/falcon/cache/stats.rs b/src/falcon/cache/stats.rs index b832a2c..fac264a 100644 --- a/src/falcon/cache/stats.rs +++ b/src/falcon/cache/stats.rs @@ -2,7 +2,7 @@ use std::collections::{HashMap, VecDeque}; -#[derive(Default)] +#[derive(Default, Clone)] pub struct CacheStats { pub hits: u64, pub misses: u64, diff --git a/src/falcon/machine/journal.rs b/src/falcon/machine/journal.rs new file mode 100644 index 0000000..8929459 --- /dev/null +++ b/src/falcon/machine/journal.rs @@ -0,0 +1,130 @@ +//! The step journal: a bounded stack of reversible state deltas. +//! +//! The clock is the simulator's timeline, so the journal is a stack indexed by +//! clock value. Each entry is a [`ChangeSet`] — the minimum needed to undo one +//! step or edit. [`Machine::stepback`](super::Machine::stepback) pops the top +//! and applies its [`Rewind`]; this module only stores the data, keeping the +//! reversal logic next to the state it touches. + +use std::collections::VecDeque; + +use crate::falcon::cache::CacheSnapshot; +use crate::falcon::registers::Cpu; + +/// How to undo the memory side of a journaled change. The CPU is always +/// restored from [`ChangeSet::cpu_before`]; this says what to do about RAM and +/// the cache subsystem on top of that. +pub(super) enum Rewind { + /// Nothing but registers/PC changed (a `write_reg` / `write_pc` / + /// `write_freg` edit). Restoring the CPU clone is the whole undo. + CpuOnly, + /// A normal step or memory edit: restore the cache subsystem wholesale and + /// replay the RAM pre-images in reverse to undo the byte writes. + Delta { + cache_before: CacheSnapshot, + /// `(addr, old_byte)` pairs in write order; replay back-to-front. + ram_log: Vec<(u32, u8)>, + }, + /// A boundary checkpoint (entry to a GO/JIT burst that writes RAM directly, + /// bypassing the per-byte log). The only safe undo is the full RAM image + /// plus the cache subsystem. + Full { + cache_before: CacheSnapshot, + ram_before: Vec, + }, +} + +/// What a journaled change represents, surfaced by +/// [`Machine::stepback`](super::Machine::stepback) so the caller can refresh the +/// bookkeeping that matches it — a single instruction pops the exec-trace and +/// decrements its run count, whereas an interactive edit or a GO-burst +/// checkpoint touches neither. +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub enum StepbackKind { + /// One *committed* instruction — a sequential single-step, or a pipeline + /// clock cycle in which an instruction retired. The caller pops one + /// exec-trace row and decrements that instruction's run count. + Step, + /// A pipeline clock cycle that committed **nothing** (a stall or bubble). + /// State (CPU/memory/pipeline) still rewinds, but no instruction retired, so + /// the exec-trace and run counts are left untouched. + Cycle, + /// A manual register / float / memory edit. + Edit, + /// A GO/JIT burst boundary checkpoint. + Checkpoint, +} + +/// One reversible unit of history: the CPU as it was, the pipeline snapshot as +/// it was, plus how to undo the memory effects. +/// +/// `S` is the pipeline's reversible snapshot type ([`super::JournaledPipeline::Snapshot`]). +/// For a machine without a pipeline it is `()`, so this collapses to the old +/// CPU+memory change-set at zero cost. +pub(super) struct ChangeSet { + /// Clock value at which this change was recorded (stack key, for display). + pub clock: u64, + /// What kind of change this was, for the caller's post-undo refresh. + pub kind: StepbackKind, + pub cpu_before: Cpu, + /// The pipeline's reversible state as it was *before* this change. Captured + /// by the same journaling methods that snapshot the CPU, so the pipeline can + /// never drift out of the undo history — see [`super::Machine`]. + pub pipe_before: S, + pub rewind: Rewind, +} + +/// A bounded stack of [`ChangeSet`]s. Pushing past the capacity evicts the +/// oldest entry, so memory stays bounded while the most recent history (the +/// part a user can step back through) is always retained. +pub(super) struct StepJournal { + stack: VecDeque>, + cap: usize, +} + +impl StepJournal { + /// Create a journal holding at most `cap` change-sets (`cap >= 1`). + pub fn new(cap: usize) -> Self { + Self { + stack: VecDeque::new(), + cap: cap.max(1), + } + } + + /// Push the newest change-set, evicting the oldest if at capacity. + pub fn push(&mut self, change: ChangeSet) { + if self.stack.len() == self.cap { + self.stack.pop_front(); + } + self.stack.push_back(change); + } + + /// Pop the newest change-set (the next one a step-back undoes). + pub fn pop(&mut self) -> Option> { + self.stack.pop_back() + } + + /// Clock value at the top of the stack, if any. + pub fn top_clock(&self) -> Option { + self.stack.back().map(|c| c.clock) + } + + /// Whether the newest entry is a full-RAM checkpoint. Such an entry holds + /// the entire RAM image, so it stays correct even after untracked writes — + /// the signal `mem_mut_unjournaled` uses to decide it can keep the history. + pub fn top_is_full_checkpoint(&self) -> bool { + matches!(self.stack.back().map(|c| &c.rewind), Some(Rewind::Full { .. })) + } + + pub fn is_empty(&self) -> bool { + self.stack.is_empty() + } + + pub fn len(&self) -> usize { + self.stack.len() + } + + pub fn clear(&mut self) { + self.stack.clear(); + } +} diff --git a/src/falcon/machine/mod.rs b/src/falcon/machine/mod.rs new file mode 100644 index 0000000..88af210 --- /dev/null +++ b/src/falcon/machine/mod.rs @@ -0,0 +1,444 @@ +//! `Machine` — the single sanctioned owner of mutable simulator state. +//! +//! The Run tab needs to *undo* execution (step-back) and *edit* live state +//! (registers, floats, RAM, the PC) without ever leaving the step journal in a +//! lying state. The danger with the old `pub(crate) cpu` / `pub(crate) mem` +//! fields was silence: any new code could mutate them ad-hoc and simply forget +//! to record the change, turning step-back into a source of subtle bugs. +//! +//! `Machine` closes that hole at the type boundary. The `cpu` and `mem` fields +//! are **private**; reads go through the cheap `&`-borrowing accessors +//! ([`Machine::cpu`] / [`Machine::mem`]); and the *only* ways to mutate state +//! are the journaling methods below. The single silent path — +//! [`Machine::cpu_mut_unjournaled`] / [`Machine::mem_mut_unjournaled`] — names +//! itself loudly and clears the journal from the inside, so it cannot quietly +//! corrupt the undo history either. Forgetting to journal is no longer +//! expressible. +//! +//! ## How a change is captured +//! +//! Every runtime RAM write funnels through `Ram::store8`, so the journal +//! records byte-level pre-images there (see [`crate::falcon::memory::Ram`]) +//! rather than wrapping the bus — this is correct even under a write-back cache, +//! where a store may touch only a cache line and an eviction writes RAM at an +//! unrelated address. The cache subsystem (everything but the large RAM) is +//! captured by a cheap clone ([`CacheSnapshot`]). Together they make one step +//! exactly reversible. +//! +//! ## Honest limits +//! +//! Console output already shown is not un-printed (the internal buffer is +//! restored, the terminal is not). Step-back is per the selected hart. GO/JIT +//! bursts and background harts write RAM directly and are only reversible to +//! their last [`Machine::checkpoint`]. These are documented on the methods. + +#![allow(dead_code)] // Phase 1: standalone module; wired into RunState in Phase 2. + +pub mod journal; +pub mod parse; +pub mod types; + +use crate::falcon::cache::CacheController; +use crate::falcon::errors::FalconError; +use crate::falcon::jit::{ExecCtx, ExecOutcome, ExecutionBackend, InterpreterBackend}; +use crate::falcon::memory::Bus; +use crate::falcon::registers::Cpu; +use crate::ui::Console; + +use journal::{ChangeSet, Rewind, StepJournal}; +pub use journal::StepbackKind; +use types::{EditError, FRegId, MemWidth, RegTarget}; + +/// A pipeline whose per-cycle microarchitectural state the [`Machine`] owns and +/// journals, so step-back reverts the pipeline together with the CPU and memory. +/// +/// The pipeline simulator is a UI-layer type ([`crate::ui::pipeline::PipelineSimState`]), +/// so `falcon` cannot name it directly. This trait is the seam: `Machine` is +/// generic over the pipeline, snapshots its reversible state into every +/// change-set, and restores it on undo — without `falcon` depending on `ui`. +/// +/// `Snapshot` holds *only* the reversible execution state (stage latches, +/// functional-unit occupancy, fetch PC, hazard counters, …) — never the UI's +/// view/config state. Because the snapshot is taken by the same journaling +/// methods that snapshot the CPU, the pipeline can never silently drift out of +/// the undo history: that is the whole point of folding it into `Machine`. +pub trait JournaledPipeline { + /// The reversible slice of the pipeline state. `Clone` is not required — + /// each snapshot is produced fresh by [`Self::exec_snapshot`] and moved into + /// the journal, then moved back out on undo. + type Snapshot; + /// Capture the reversible execution state as of *now* (before a tick). + fn exec_snapshot(&self) -> Self::Snapshot; + /// Restore the reversible execution state from a snapshot (on step-back). + fn restore_exec(&mut self, snapshot: Self::Snapshot); +} + +/// The "no pipeline" instantiation: a [`Machine`] that only ever single-steps +/// the interpreter or takes edits. Its snapshot is `()`, so the journal carries +/// zero extra bytes and the pipeline restore is a no-op. +#[derive(Default)] +pub struct NoPipeline; + +impl JournaledPipeline for NoPipeline { + type Snapshot = (); + fn exec_snapshot(&self) {} + fn restore_exec(&mut self, _: ()) {} +} + +/// Default journal depth: how many steps/edits a user can rewind. +const JOURNAL_CAPACITY: usize = 1024; + +/// Owns the CPU, the memory hierarchy, the pipeline, and the step journal, and +/// is the sole gateway for mutating them. See the module docs for the rationale. +/// +/// `P` is the pipeline ([`JournaledPipeline`]); it defaults to [`NoPipeline`] so +/// the interpreter-only and test paths pay nothing for it. +pub struct Machine { + cpu: Cpu, + mem: CacheController, + /// The pipeline simulator. Private like `cpu`/`mem`: execution may only + /// advance it through [`Machine::step_pipeline`], which journals the cycle. + /// UI/config mutation goes through [`Machine::pipeline_mut`] (unjournaled). + pipeline: P, + journal: StepJournal, + /// Reused across steps so the interpreter's hot-branch profile persists. + interp: InterpreterBackend, + /// Logical timeline. Advances by one per recorded change; the value is the + /// stack key of the most recent [`ChangeSet`]. + clock: u64, +} + +impl Machine

{ + pub fn new(cpu: Cpu, mem: CacheController, pipeline: P) -> Self { + Self { + cpu, + mem, + pipeline, + journal: StepJournal::new(JOURNAL_CAPACITY), + interp: InterpreterBackend::new(), + clock: 0, + } + } + + // ── Reads (the ~260 existing sites borrow through these) ── + + pub fn cpu(&self) -> &Cpu { + &self.cpu + } + + pub fn mem(&self) -> &CacheController { + &self.mem + } + + /// Shared read access to the pipeline (rendering, hit-testing, stats). + pub fn pipeline(&self) -> &P { + &self.pipeline + } + + /// Mutable pipeline access for **UI/config** changes only — hover flags, + /// scroll, subtab, gantt cosmetics, forwarding/branch config, resets. This + /// does **not** journal and deliberately does **not** clear the journal: + /// these mutations happen between steps and must not erase undo history. + /// + /// It must never be used to *advance execution* (tick the stages): that is + /// [`Machine::step_pipeline`]'s job, which captures the cycle. Resetting the + /// pipeline stages for a fresh run should be paired with + /// [`Machine::clear_journal`], since the old history no longer applies. + pub fn pipeline_mut(&mut self) -> &mut P { + &mut self.pipeline + } + + // ── Execution ── + + /// Execute one instruction through the interpreter, journaling the change + /// so it can be stepped back. Returns the interpreter's [`ExecOutcome`]. + /// + /// The change-set is recorded even when the step returns an error, so a + /// partially-applied faulting instruction is still reversible. + pub fn step_interpreted( + &mut self, + console: &mut Console, + ) -> Result { + let cpu_before = self.cpu.clone(); + let cache_before = self.mem.snapshot_state(); + self.mem.ram_mut().begin_recording(); + + let outcome = { + let mut ctx = ExecCtx::new(&mut self.cpu, &mut self.mem, console); + self.interp.run_until_yield(&mut ctx) + }; + + let ram_log = self.mem.ram_mut().take_recording(); + self.record(cpu_before, StepbackKind::Step, Rewind::Delta { cache_before, ram_log }); + outcome + } + + /// Point the shared MMU at the CPU's current `satp` / privilege before a + /// sequential step — a background hart may have left it elsewhere. + /// + /// Unlike [`Machine::mem_mut_unjournaled`] this keeps the journal: it writes + /// only MMU metadata (no RAM), the values are a pure function of the CPU + /// `satp`/priv that a step-back restores, and the immediately-following + /// [`Machine::step_interpreted`] snapshots the synced MMU into its + /// change-set. So step-back stays exact without the sync erasing history. + pub fn sync_mmu(&mut self) { + let mmu = self.mem.mmu_mut(); + mmu.satp = crate::falcon::mmu::Satp::new(self.cpu.satp); + mmu.priv_mode = self.cpu.priv_mode; + } + + /// Apply one instruction's worth of cycle/stats accounting after a + /// journaled [`Machine::step_interpreted`]. + /// + /// This mutates the cache subsystem but deliberately does **not** touch the + /// journal: the change-set the step just pushed snapshotted the cache + /// *before* the instruction, so a [`Machine::stepback`] reverts this + /// accounting along with the instruction. Keep it paired with + /// `step_interpreted` (the GO/JIT path accounts cycles through + /// [`Machine::mem_mut_unjournaled`] instead). + pub fn account_step_cycles(&mut self, cpi_cycles: u64) { + self.mem.add_instruction_cycles(cpi_cycles); + self.mem.snapshot_stats(); + } + + /// Record one committed pipeline instruction (cycle accounting for the + /// pipeline backend), **without** touching the journal. Like + /// [`Machine::account_step_cycles`], the change-set pushed by + /// [`Machine::step_pipeline`] snapshotted the cache *before* the cycle, so a + /// step-back reverts this bump along with the cycle. Pair it with + /// `step_pipeline`; routing it through `mem_mut_unjournaled` instead would + /// erase the cycle's change-set. + pub fn account_pipeline_commit(&mut self) { + self.mem.instruction_count = self.mem.instruction_count.saturating_add(1); + self.mem.snapshot_stats(); + } + + /// Advance the pipeline by **one clock cycle**, journaling the whole cycle + /// so step-back reverts it exactly. This is the *only* way execution can + /// touch the pipeline stages, so a tick can never escape the undo history. + /// + /// The closure receives the machine's owned pipeline, CPU, and memory and + /// runs the cycle (typically `pipeline_tick`); its return value is passed + /// back to the caller. Before the tick this snapshots the CPU, the + /// pipeline's reversible state, and the cache subsystem, and records the + /// per-byte RAM pre-images the cycle writes — exactly like a single + /// interpreter step, plus the pipeline latches. + /// + /// The MMU is re-synced to the CPU's `satp`/privilege first (a background + /// hart may have left it elsewhere), through the journal-preserving + /// [`Machine::sync_mmu`] rather than an unjournaled hatch — that earlier + /// erased the history on every cycle and was why pipeline step-back never + /// worked. + /// `committed` inspects the tick's result to classify the cycle: a cycle + /// that retired an instruction is recorded as [`StepbackKind::Step`] (the + /// caller rolls back one exec-trace row on undo), a stall/bubble cycle as + /// [`StepbackKind::Cycle`] (state rewinds, bookkeeping untouched). + pub fn step_pipeline( + &mut self, + tick: impl FnOnce(&mut P, &mut Cpu, &mut CacheController) -> R, + committed: impl FnOnce(&R) -> bool, + ) -> R { + self.sync_mmu(); + let cpu_before = self.cpu.clone(); + let pipe_before = self.pipeline.exec_snapshot(); + let cache_before = self.mem.snapshot_state(); + self.mem.ram_mut().begin_recording(); + + let result = tick(&mut self.pipeline, &mut self.cpu, &mut self.mem); + + let ram_log = self.mem.ram_mut().take_recording(); + let kind = if committed(&result) { + StepbackKind::Step + } else { + StepbackKind::Cycle + }; + self.record_with_pipe( + cpu_before, + pipe_before, + kind, + Rewind::Delta { cache_before, ram_log }, + ); + result + } + + // ── Sanctioned edits (each journaled, each undoable by `stepback`) ── + + /// Write an integer register or the PC. Writing `x0` is rejected + /// ([`EditError::X0Immutable`]); the caller's editor stays open. + pub fn write_reg(&mut self, target: RegTarget, value: u32) -> Result<(), EditError> { + if let RegTarget::X(reg) = target + && reg.is_zero() + { + return Err(EditError::X0Immutable); + } + let cpu_before = self.cpu.clone(); + match target { + RegTarget::X(reg) => self.cpu.write(reg.index(), value), + RegTarget::Pc => self.cpu.pc = value, + } + self.record(cpu_before, StepbackKind::Edit, Rewind::CpuOnly); + Ok(()) + } + + /// Write a float register from raw IEEE-754 bits. + pub fn write_freg(&mut self, freg: FRegId, bits: u32) { + let cpu_before = self.cpu.clone(); + self.cpu.fwrite_bits(freg.index(), bits); + self.record(cpu_before, StepbackKind::Edit, Rewind::CpuOnly); + } + + /// Write a `width`-byte cell at `addr` through the normal cache-aware store + /// path, so subsequent loads see the edit. `value` is the little-endian + /// payload (already range-checked by [`parse::parse_cell`]). + pub fn write_mem( + &mut self, + addr: u32, + width: MemWidth, + value: u64, + ) -> Result<(), FalconError> { + let cpu_before = self.cpu.clone(); + let cache_before = self.mem.snapshot_state(); + self.mem.ram_mut().begin_recording(); + + let result = match width { + MemWidth::B1 => self.mem.store8(addr, value as u8), + MemWidth::B2 => self.mem.store16(addr, value as u16), + MemWidth::B4 => self.mem.store32(addr, value as u32), + }; + + let ram_log = self.mem.ram_mut().take_recording(); + self.record(cpu_before, StepbackKind::Edit, Rewind::Delta { cache_before, ram_log }); + result + } + + // ── Step-back ── + + /// True when there is at least one change to undo. + pub fn can_stepback(&self) -> bool { + !self.journal.is_empty() + } + + /// Undo the most recent journaled change (one step, edit, or back to the + /// last checkpoint). Returns the [`StepbackKind`] of what was undone, or + /// `None` when the journal is empty. + pub fn stepback(&mut self) -> Option { + let change = self.journal.pop()?; + let kind = change.kind; + self.cpu = change.cpu_before; + self.pipeline.restore_exec(change.pipe_before); + match change.rewind { + Rewind::CpuOnly => {} + Rewind::Delta { cache_before, ram_log } => { + self.mem.restore_state(cache_before); + // Replay pre-images back-to-front so overlapping writes land on + // their oldest value. + let ram = self.mem.ram_mut(); + for &(addr, old) in ram_log.iter().rev() { + ram.poke8(addr, old); + } + } + Rewind::Full { cache_before, ram_before } => { + self.mem.restore_state(cache_before); + self.mem.ram_mut().copy_from_slice(&ram_before, ram_before.len()); + } + } + self.clock = self.journal.top_clock().unwrap_or(0); + Some(kind) + } + + /// Push a full-state checkpoint. Used at the boundary of a GO/JIT burst or + /// pipeline run, whose writes bypass the per-byte log — step-back can only + /// rewind to here, not into the burst. + pub fn checkpoint(&mut self) { + let cpu_before = self.cpu.clone(); + let cache_before = self.mem.snapshot_state(); + let ram_before = self.mem.ram().as_bytes().to_vec(); + self.record(cpu_before, StepbackKind::Checkpoint, Rewind::Full { cache_before, ram_before }); + } + + /// Drop all history (reset / program reload / mode switch). + pub fn clear_journal(&mut self) { + self.journal.clear(); + self.clock = 0; + } + + /// Number of change-sets currently retained (≤ [`JOURNAL_CAPACITY`]). + pub fn journal_depth(&self) -> usize { + self.journal.len() + } + + // ── Escape hatches: the name announces "this skips the journal" ── + + /// Mutable CPU access that does **not** journal. For reset / loader / + /// multi-core sync only — all audited. Does not clear existing history, so + /// use only where the timeline is being rebuilt anyway. + pub fn cpu_mut_unjournaled(&mut self) -> &mut Cpu { + &mut self.cpu + } + + /// Mutable memory access that does **not** journal. For reset / loader / + /// GO-JIT / pipeline / MMU-sync only. + /// + /// An unrecorded RAM write would invalidate the byte-level pre-images of + /// every [`Rewind::Delta`] entry, so the journal must usually be dropped + /// here. The one exception is a [`Rewind::Full`] checkpoint on top: it holds + /// the whole RAM image and so stays a correct rewind target across the + /// untracked writes that follow. That is exactly the GO/JIT pattern — + /// [`Machine::checkpoint`] then `mem_mut_unjournaled` for the burst — and it + /// keeps step-back-to-the-boundary working. + pub fn mem_mut_unjournaled(&mut self) -> &mut CacheController { + if !self.journal.top_is_full_checkpoint() { + self.journal.clear(); + self.clock = 0; + } + &mut self.mem + } + + /// Mutable CPU **and** memory at once, without journaling. Callers that need + /// both disjoint borrows simultaneously — the execution `ExecCtx`, the + /// pipeline tick, and the per-step MMU sync — cannot stack two separate + /// `&mut self` accessors, so this hands out the pair in one borrow. Same + /// journal semantics as [`Machine::mem_mut_unjournaled`]. + pub fn cpu_mem_mut_unjournaled(&mut self) -> (&mut Cpu, &mut CacheController) { + if !self.journal.top_is_full_checkpoint() { + self.journal.clear(); + self.clock = 0; + } + (&mut self.cpu, &mut self.mem) + } + + // ── Internal ── + + /// Advance the clock and push one change-set, snapshotting the pipeline + /// *now*. Used by every path that does not itself advance the pipeline + /// (single-step interpreter, edits, checkpoint) — there the current pipeline + /// state *is* the before-state. + fn record(&mut self, cpu_before: Cpu, kind: StepbackKind, rewind: Rewind) { + let pipe_before = self.pipeline.exec_snapshot(); + self.record_with_pipe(cpu_before, pipe_before, kind, rewind); + } + + /// Advance the clock and push one change-set with an explicitly-captured + /// pipeline snapshot. [`Machine::step_pipeline`] uses this because the tick + /// mutates the pipeline, so the before-state must be captured up front. + fn record_with_pipe( + &mut self, + cpu_before: Cpu, + pipe_before: P::Snapshot, + kind: StepbackKind, + rewind: Rewind, + ) { + self.clock += 1; + self.journal.push(ChangeSet { + clock: self.clock, + kind, + cpu_before, + pipe_before, + rewind, + }); + } +} + +#[cfg(test)] +#[path = "../../../tests/support/falcon_machine.rs"] +mod tests; diff --git a/src/falcon/machine/parse.rs b/src/falcon/machine/parse.rs new file mode 100644 index 0000000..6cf854b --- /dev/null +++ b/src/falcon/machine/parse.rs @@ -0,0 +1,117 @@ +//! Parse user-entered cell text into a width-bounded value. +//! +//! The one rule that matters for safety: a value that does not fit the target +//! cell is **rejected** (an [`EditError`]) — never silently truncated. The +//! format mirrors the Run tab's display toggles so what the user types is read +//! back the way they see it. + +use super::types::{EditError, MemWidth}; + +/// How to interpret the typed text, matching the Run tab's `fmt_mode`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum CellFormat { + /// Base-16, with an optional `0x` / `0X` prefix. + Hex, + /// Base-10. Combined with `signed`, accepts a leading `-`. + Dec, + /// Base-2, with an optional `0b` / `0B` prefix. + Bin, + /// Raw bytes, packed little-endian (memory cells only). + Str, +} + +/// Parse `input` into the raw little-endian value to store in a `width`-byte +/// cell, rejecting anything that does not fit. +/// +/// - **Hex** rejects values above [`MemWidth::max_unsigned`]. +/// - **Dec, signed** parses an `i64`, rejects values outside +/// [`MemWidth::signed_range`], then encodes two's-complement into `width`. +/// - **Dec, unsigned** rejects values above [`MemWidth::max_unsigned`]. +/// - **Str** packs the bytes little-endian, rejecting text longer than `width`. +pub fn parse_cell( + input: &str, + width: MemWidth, + fmt: CellFormat, + signed: bool, +) -> Result { + let trimmed = input.trim(); + match fmt { + CellFormat::Hex => parse_hex(trimmed, width), + CellFormat::Dec if signed => parse_signed_dec(trimmed, width), + CellFormat::Dec => parse_unsigned_dec(trimmed, width), + CellFormat::Bin => parse_bin(trimmed, width), + CellFormat::Str => parse_str(input, width), + } +} + +fn parse_bin(input: &str, width: MemWidth) -> Result { + let digits = input + .strip_prefix("0b") + .or_else(|| input.strip_prefix("0B")) + .unwrap_or(input); + let value = u128::from_str_radix(&without_separators(digits), 2) + .map_err(|_| EditError::ParseFailed { input: input.to_string() })?; + fits_unsigned(value, width, false) +} + +fn parse_hex(input: &str, width: MemWidth) -> Result { + let digits = input + .strip_prefix("0x") + .or_else(|| input.strip_prefix("0X")) + .unwrap_or(input); + let value = u128::from_str_radix(&without_separators(digits), 16) + .map_err(|_| EditError::ParseFailed { input: input.to_string() })?; + fits_unsigned(value, width, false) +} + +fn parse_unsigned_dec(input: &str, width: MemWidth) -> Result { + let value: u128 = without_separators(input) + .parse() + .map_err(|_| EditError::ParseFailed { input: input.to_string() })?; + fits_unsigned(value, width, false) +} + +fn parse_signed_dec(input: &str, width: MemWidth) -> Result { + let value: i64 = without_separators(input) + .parse() + .map_err(|_| EditError::ParseFailed { input: input.to_string() })?; + let (lo, hi) = width.signed_range(); + if value < lo || value > hi { + return Err(EditError::OutOfRange { width, signed: true }); + } + // Two's-complement into the cell width: e.g. -128 in B1 → 0x80. + Ok((value as u64) & mask(width)) +} + +fn parse_str(input: &str, width: MemWidth) -> Result { + let bytes = input.as_bytes(); + if bytes.len() > width.bytes() as usize { + return Err(EditError::OutOfRange { width, signed: false }); + } + let mut value = 0u64; + for (i, &b) in bytes.iter().enumerate() { + value |= (b as u64) << (8 * i); + } + Ok(value) +} + +/// Reject `value` if it exceeds the width's unsigned capacity, else narrow to +/// `u64`. +fn fits_unsigned(value: u128, width: MemWidth, signed: bool) -> Result { + if value > width.max_unsigned() as u128 { + Err(EditError::OutOfRange { width, signed }) + } else { + Ok(value as u64) + } +} + +fn mask(width: MemWidth) -> u64 { + width.max_unsigned() +} + +/// Drop `_` digit-group separators so grouped input like `0x1_0000_0000` or +/// `1_000` parses the same way it reads on screen — mirroring Rust literals. +/// Numeric paths only; in a `Str` cell an underscore is a real byte. +fn without_separators(s: &str) -> String { + s.replace('_', "") +} diff --git a/src/falcon/machine/types.rs b/src/falcon/machine/types.rs new file mode 100644 index 0000000..e1b02b2 --- /dev/null +++ b/src/falcon/machine/types.rs @@ -0,0 +1,139 @@ +//! Typed edit targets and the errors a manual edit can produce. +//! +//! These newtypes make illegal edits unrepresentable: a [`RegId`] is always a +//! valid `0..=31` index, a [`MemWidth`] always carries its byte count and +//! range, and [`EditError`] is the single failure currency every editing path +//! speaks. Parsing user text into a value lives in [`super::parse`]. + +use std::fmt; + +/// An integer-register index, guaranteed to be in `0..=31`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct RegId(u8); + +impl RegId { + /// Build a `RegId`, returning `None` for indices outside `0..=31`. + pub fn new(index: u8) -> Option { + (index < 32).then_some(Self(index)) + } + + /// The raw `0..=31` index. + pub fn index(self) -> u8 { + self.0 + } + + /// `x0` is hard-wired to zero and may never be written. + pub fn is_zero(self) -> bool { + self.0 == 0 + } +} + +/// A float-register index, guaranteed to be in `0..=31`. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct FRegId(u8); + +impl FRegId { + pub fn new(index: u8) -> Option { + (index < 32).then_some(Self(index)) + } + + pub fn index(self) -> u8 { + self.0 + } +} + +/// Where a register write lands: a general-purpose register or the PC. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum RegTarget { + X(RegId), + Pc, +} + +/// The byte width of a memory cell, mirroring the Run tab's `mem_view_bytes` +/// setting. Carries the helpers parsing needs to bound a value. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum MemWidth { + B1, + B2, + B4, +} + +impl MemWidth { + /// Map the `mem_view_bytes` setting (1 / 2 / 4) to a width; anything else + /// falls back to a full word. + pub fn from_view_bytes(bytes: u32) -> Self { + match bytes { + 1 => MemWidth::B1, + 2 => MemWidth::B2, + _ => MemWidth::B4, + } + } + + /// Number of bytes this width occupies (1, 2 or 4). + pub fn bytes(self) -> u32 { + match self { + MemWidth::B1 => 1, + MemWidth::B2 => 2, + MemWidth::B4 => 4, + } + } + + /// Number of value bits (8, 16 or 32). + pub fn bits(self) -> u32 { + self.bytes() * 8 + } + + /// Largest unsigned value that fits (e.g. `0xFF` for `B1`). + pub fn max_unsigned(self) -> u64 { + (1u64 << self.bits()) - 1 + } + + /// Inclusive signed range that fits (e.g. `(-128, 127)` for `B1`). + pub fn signed_range(self) -> (i64, i64) { + let half = 1i64 << (self.bits() - 1); + (-half, half - 1) + } +} + +/// Why a manual edit was rejected. Every variant renders to a one-line status +/// message via [`EditError::message`]; the editor stays open on rejection so +/// the user can fix the input rather than losing it. +#[derive(Clone, Debug, PartialEq, Eq)] +pub enum EditError { + /// The text could not be parsed in the active format. + ParseFailed { input: String }, + /// The value parsed but does not fit the target's width. + OutOfRange { width: MemWidth, signed: bool }, + /// `x0` is hard-wired to zero and cannot be written. + X0Immutable, +} + +impl EditError { + /// A short, human-readable explanation for the status line. + pub fn message(&self) -> String { + match self { + EditError::ParseFailed { input } => { + format!("cannot parse \"{input}\"") + } + EditError::OutOfRange { width, signed } => { + let bytes = width.bytes(); + if *signed { + let (lo, hi) = width.signed_range(); + format!("out of range for {bytes}-byte signed cell ({lo}..={hi})") + } else { + format!( + "out of range for {bytes}-byte cell (max 0x{:X})", + width.max_unsigned() + ) + } + } + EditError::X0Immutable => "x0 is hard-wired to zero".to_string(), + } + } +} + +impl fmt::Display for EditError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.write_str(&self.message()) + } +} diff --git a/src/falcon/memory.rs b/src/falcon/memory.rs index 482259e..6276647 100644 --- a/src/falcon/memory.rs +++ b/src/falcon/memory.rs @@ -188,6 +188,13 @@ pub trait Bus { pub struct Ram { data: Vec, reservations: HashMap, + /// When `Some`, every byte mutated by `store8` first appends its + /// `(addr, old_byte)` pre-image here. This is the single chokepoint for + /// *all* runtime RAM writes — direct stores, write-through, and dirty-line + /// writebacks all funnel through `store8` — so the `Machine` journal can + /// rewind a step by replaying these pre-images in reverse. `None` (the + /// default) means recording is off and `store8` pays nothing. + write_log: Option>, } impl Ram { @@ -195,6 +202,7 @@ impl Ram { Self { data: vec![0; size], reservations: HashMap::new(), + write_log: None, } } @@ -213,6 +221,29 @@ impl Ram { pub fn as_bytes(&self) -> &[u8] { &self.data } + + // ── Step-journal recording (see the `falcon::machine` module) ── + + /// Start capturing `(addr, old_byte)` pre-images for every subsequent + /// `store8`. Replaces any in-flight log. + pub fn begin_recording(&mut self) { + self.write_log = Some(Vec::new()); + } + + /// Stop recording and return the pre-images in write order (oldest first). + /// Replay them in *reverse* — each via [`Ram::poke8`] — to undo the writes. + pub fn take_recording(&mut self) -> Vec<(u32, u8)> { + self.write_log.take().unwrap_or_default() + } + + /// Raw byte write that bypasses both recording and reservation tracking. + /// Used only to restore pre-images during a step-back; out-of-bounds + /// addresses are silently ignored (the address was valid when captured). + pub fn poke8(&mut self, addr: u32, val: u8) { + if let Some(slot) = self.data.get_mut(addr as usize) { + *slot = val; + } + } } impl Bus for Ram { @@ -235,6 +266,9 @@ impl Bus for Ram { } fn store8(&mut self, a: u32, v: u8) -> Result<(), FalconError> { if let Some(slot) = self.data.get_mut(a as usize) { + if let Some(log) = self.write_log.as_mut() { + log.push((a, *slot)); + } *slot = v; invalidate_reservations(&mut self.reservations, a, 1); Ok(()) diff --git a/src/falcon/mmu/mod.rs b/src/falcon/mmu/mod.rs index 6fe857f..b90eb2f 100644 --- a/src/falcon/mmu/mod.rs +++ b/src/falcon/mmu/mod.rs @@ -275,6 +275,7 @@ fn perm_bits(p: PtePerms) -> u32 { b } +#[derive(Clone)] pub struct Mmu { pub tlb: Tlb, pub satp: Satp, diff --git a/src/falcon/mmu/tlb.rs b/src/falcon/mmu/tlb.rs index 491f686..a59575e 100644 --- a/src/falcon/mmu/tlb.rs +++ b/src/falcon/mmu/tlb.rs @@ -61,7 +61,7 @@ impl Default for TlbConfig { } } -#[derive(Default)] +#[derive(Default, Clone)] pub struct TlbStats { pub hits: u64, pub misses: u64, @@ -83,6 +83,7 @@ impl TlbStats { } } +#[derive(Clone)] pub struct Tlb { pub config: TlbConfig, pub entries: Vec, diff --git a/src/falcon/mod.rs b/src/falcon/mod.rs index d9e5c33..0468ca1 100644 --- a/src/falcon/mod.rs +++ b/src/falcon/mod.rs @@ -4,6 +4,7 @@ pub mod errors; pub mod exec; pub mod instruction; pub mod jit; +pub mod machine; pub mod memory; pub mod mmu; pub mod registers; diff --git a/src/ui/app/instr_edit.rs b/src/ui/app/instr_edit.rs new file mode 100644 index 0000000..b7ba39f --- /dev/null +++ b/src/ui/app/instr_edit.rs @@ -0,0 +1,458 @@ +//! Instruction-word field editing: encoding formats, per-field seeds, parsing +//! and bit-splicing. Backs the editable Instruction Details panel — the pure +//! logic lives here so the commit path and the renderer share one source of +//! truth for where each field sits in the word. + +use crate::falcon::asm::utils::{parse_imm, parse_reg}; +use ratatui::style::Color; + +// ── Encoding formats ───────────────────────────────────────────────────────── + +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum EncFormat { + R, + I, + S, + B, + U, + J, +} + +impl EncFormat { + pub(crate) fn name(self) -> &'static str { + match self { + EncFormat::R => "R-type", + EncFormat::I => "I-type", + EncFormat::S => "S-type", + EncFormat::B => "B-type", + EncFormat::U => "U-type", + EncFormat::J => "J-type", + } + } + pub(crate) fn segments(self) -> Vec { + seg_list(self) + } +} + +pub(crate) fn detect_format(word: u32) -> EncFormat { + match word & 0x7f { + 0x03 | 0x13 | 0x1b | 0x67 | 0x73 => EncFormat::I, + 0x23 => EncFormat::S, + 0x63 => EncFormat::B, + 0x37 | 0x17 => EncFormat::U, + 0x6f => EncFormat::J, + _ => EncFormat::R, + } +} + +pub(crate) struct Seg { + pub(crate) label: &'static str, + pub(crate) width: u8, + pub(crate) color: Color, +} + +pub(crate) fn seg_list(format: EncFormat) -> Vec { + macro_rules! s { + ($l:expr, $w:expr, $c:expr) => { + Seg { + label: $l, + width: $w, + color: $c, + } + }; + } + use Color::*; + match format { + EncFormat::R => vec![ + s!("funct7", 7, Red), + s!("rs2", 5, LightRed), + s!("rs1", 5, LightMagenta), + s!("fn3", 3, Yellow), + s!("rd", 5, LightGreen), + s!("opcode", 7, Cyan), + ], + EncFormat::I => vec![ + s!("imm[11:0]", 12, Blue), + s!("rs1", 5, LightMagenta), + s!("fn3", 3, Yellow), + s!("rd", 5, LightGreen), + s!("opcode", 7, Cyan), + ], + EncFormat::S => vec![ + s!("imm[11:5]", 7, Blue), + s!("rs2", 5, LightRed), + s!("rs1", 5, LightMagenta), + s!("fn3", 3, Yellow), + s!("imm[4:0]", 5, Blue), + s!("opcode", 7, Cyan), + ], + EncFormat::B => vec![ + s!("i12", 1, Blue), + s!("i10:5", 6, Blue), + s!("rs2", 5, LightRed), + s!("rs1", 5, LightMagenta), + s!("fn3", 3, Yellow), + s!("i4:1", 4, Blue), + s!("i11", 1, Blue), + s!("opcode", 7, Cyan), + ], + EncFormat::U => vec![ + s!("imm[31:12]", 20, Blue), + s!("rd", 5, LightGreen), + s!("opcode", 7, Cyan), + ], + EncFormat::J => vec![ + s!("i20", 1, Blue), + s!("i10:1", 10, Blue), + s!("i11", 1, Blue), + s!("i19:12", 8, Blue), + s!("rd", 5, LightGreen), + s!("opcode", 7, Cyan), + ], + } +} + +// ── Editable fields ────────────────────────────────────────────────────────── + +/// One editable spot in the Instruction Details panel. `Word` is only a hitbox +/// tag (the full hex word edits through `RunEditTarget::Instr`); every other +/// kind edits through `RunEditTarget::InstrField`. +#[derive(Copy, Clone, PartialEq, Eq, Debug)] +pub(crate) enum InstrFieldKind { + /// The full word in hex (header line 1). Hitbox tag only. + Word, + /// The mnemonic line, edited as one line of assembly. + Asm, + /// The full word in binary (header line 1). + Bin, + Rd, + Rs1, + Rs2, + /// The format-dependent immediate (I/S/B/U/J bit layouts). + Imm, + Opcode, + Funct3, + Funct7, + Shamt, +} + +/// Whether `field` exists in the encoding of `word`. `Funct7`/`Shamt` on +/// I-type are only meaningful for the shift instructions (funct3 1 or 5). +pub(crate) fn field_available(word: u32, field: InstrFieldKind) -> bool { + use InstrFieldKind::*; + let format = detect_format(word); + let i_shift = format == EncFormat::I && matches!((word >> 12) & 0x7, 0x1 | 0x5); + match field { + Word | Asm | Bin | Opcode => true, + Rd => matches!(format, EncFormat::R | EncFormat::I | EncFormat::U | EncFormat::J), + Rs1 => matches!(format, EncFormat::R | EncFormat::I | EncFormat::S | EncFormat::B), + Rs2 => matches!(format, EncFormat::R | EncFormat::S | EncFormat::B), + Funct3 => matches!(format, EncFormat::R | EncFormat::I | EncFormat::S | EncFormat::B), + Imm => !matches!(format, EncFormat::R), + Funct7 => format == EncFormat::R || i_shift, + Shamt => i_shift, + } +} + +/// Decode the immediate of `word` exactly as the Decoded section displays it +/// (U-type as the unshifted `imm[31:12]` value). +pub(crate) fn decode_imm(word: u32, format: EncFormat) -> i32 { + match format { + EncFormat::R => 0, + EncFormat::I => (((word >> 20) as i32) << 20) >> 20, + EncFormat::S => { + let imm_lo = (word >> 7) & 0x1f; + let imm_hi = (word >> 25) & 0x7f; + ((((imm_hi << 5) | imm_lo) as i32) << 20) >> 20 + } + EncFormat::B => { + let b12 = (word >> 31) & 1; + let b10_5 = (word >> 25) & 0x3f; + let b4_1 = (word >> 8) & 0xf; + let b11 = (word >> 7) & 1; + ((((b12 << 12) | (b11 << 11) | (b10_5 << 5) | (b4_1 << 1)) as i32) << 19) >> 19 + } + EncFormat::U => ((word & 0xfffff000) as i32) >> 12, + EncFormat::J => { + let b20 = (word >> 31) & 1; + let b10_1 = (word >> 21) & 0x3ff; + let b11 = (word >> 20) & 1; + let b19_12 = (word >> 12) & 0xff; + ((((b20 << 20) | (b19_12 << 12) | (b11 << 11) | (b10_1 << 1)) as i32) << 11) >> 11 + } + } +} + +/// The text an editor on `field` starts from — the value exactly as the +/// details panel renders it (registers as `x{n}`, immediates in decimal, +/// functs in hex). `Asm` seeds with the disassembly so a small tweak doesn't +/// require retyping the whole line; `Word` never reaches here. +pub(crate) fn seed_field(word: u32, field: InstrFieldKind) -> String { + use InstrFieldKind::*; + match field { + Word => String::new(), + Asm => match crate::falcon::decoder::decode(word) { + Ok(_) => crate::ui::view::disasm::disasm_word(word), + Err(_) => String::new(), + }, + Bin => format!("{word:032b}"), + Rd => format!("x{}", (word >> 7) & 0x1f), + Rs1 => format!("x{}", (word >> 15) & 0x1f), + Rs2 => format!("x{}", (word >> 20) & 0x1f), + Imm => format!("{}", decode_imm(word, detect_format(word))), + Opcode => format!("0x{:02x}", word & 0x7f), + Funct3 => format!("0x{:01x}", (word >> 12) & 0x7), + Funct7 => format!("0x{:02x}", (word >> 25) & 0x7f), + Shamt => format!("{}", (word >> 20) & 0x1f), + } +} + +/// Parse the typed text for a numeric field. Registers accept `x11`, ABI names +/// and bare `11`; immediates accept decimal and `0x`/`0b` prefixes; binary is +/// the full 32-bit word. `Word`/`Asm` are handled by their own commit paths. +pub(crate) fn parse_field_value(field: InstrFieldKind, text: &str) -> Result { + use InstrFieldKind::*; + let t = text.trim(); + match field { + Word | Asm => Err("not a numeric field".into()), + Rd | Rs1 | Rs2 => { + if let Ok(n) = t.parse::() { + if n < 32 { + return Ok(n as i64); + } + return Err(format!("register {n} out of range (0..31)")); + } + parse_reg(t) + .map(|r| r as i64) + .ok_or_else(|| format!("\"{t}\" is not a register (x0..x31 or ABI name)")) + } + Imm | Shamt | Opcode | Funct3 | Funct7 => parse_imm(t) + .map(|v| v as i64) + .ok_or_else(|| format!("cannot parse \"{t}\" as a number")), + Bin => { + let digits: String = t + .trim_start_matches("0b") + .trim_start_matches("0B") + .chars() + .filter(|c| *c != '_') + .collect(); + if digits.is_empty() || digits.len() > 32 { + return Err("binary value must have 1..=32 bits".into()); + } + u32::from_str_radix(&digits, 2) + .map(|v| v as i64) + .map_err(|_| format!("cannot parse \"{t}\" as binary")) + } + } +} + +fn set_bits(word: u32, lo: u32, width: u32, value: u32) -> u32 { + let mask = ((1u64 << width) - 1) as u32; + (word & !(mask << lo)) | ((value & mask) << lo) +} + +fn check_range(value: i64, min: i64, max: i64, what: &str) -> Result<(), String> { + if value < min || value > max { + Err(format!("{what} {value} out of range ({min}..{max})")) + } else { + Ok(()) + } +} + +/// Splice `value` into `field` of `word`, range-checked against the field's +/// width (immediates against the format's signed range, branch/jump offsets +/// must be even). Returns the rewritten word. +pub(crate) fn splice_field( + word: u32, + format: EncFormat, + field: InstrFieldKind, + value: i64, +) -> Result { + use InstrFieldKind::*; + match field { + Word | Asm => Err("not a spliceable field".into()), + Bin => { + check_range(value, 0, u32::MAX as i64, "word")?; + Ok(value as u32) + } + Rd => { + check_range(value, 0, 31, "rd")?; + Ok(set_bits(word, 7, 5, value as u32)) + } + Rs1 => { + check_range(value, 0, 31, "rs1")?; + Ok(set_bits(word, 15, 5, value as u32)) + } + Rs2 => { + check_range(value, 0, 31, "rs2")?; + Ok(set_bits(word, 20, 5, value as u32)) + } + Opcode => { + check_range(value, 0, 0x7f, "opcode")?; + Ok(set_bits(word, 0, 7, value as u32)) + } + Funct3 => { + check_range(value, 0, 0x7, "funct3")?; + Ok(set_bits(word, 12, 3, value as u32)) + } + Funct7 => { + check_range(value, 0, 0x7f, "funct7")?; + Ok(set_bits(word, 25, 7, value as u32)) + } + Shamt => { + check_range(value, 0, 31, "shamt")?; + Ok(set_bits(word, 20, 5, value as u32)) + } + Imm => splice_imm(word, format, value), + } +} + +fn splice_imm(word: u32, format: EncFormat, value: i64) -> Result { + match format { + EncFormat::R => Err("R-type has no immediate".into()), + EncFormat::I => { + check_range(value, -(1 << 11), (1 << 11) - 1, "imm")?; + Ok(set_bits(word, 20, 12, value as u32)) + } + EncFormat::S => { + check_range(value, -(1 << 11), (1 << 11) - 1, "offset")?; + let v = value as u32; + let word = set_bits(word, 7, 5, v & 0x1f); + Ok(set_bits(word, 25, 7, (v >> 5) & 0x7f)) + } + EncFormat::B => { + check_range(value, -(1 << 12), (1 << 12) - 1, "offset")?; + if value % 2 != 0 { + return Err(format!("branch offset {value} must be even")); + } + let v = value as u32; + let word = set_bits(word, 8, 4, (v >> 1) & 0xf); + let word = set_bits(word, 25, 6, (v >> 5) & 0x3f); + let word = set_bits(word, 7, 1, (v >> 11) & 1); + Ok(set_bits(word, 31, 1, (v >> 12) & 1)) + } + EncFormat::U => { + // Accept signed 20-bit or the full unsigned 20-bit range, like the + // assembler's `check_u_imm`. + check_range(value, -(1 << 19), 0xFFFFF, "imm[31:12]")?; + Ok(set_bits(word, 12, 20, value as u32)) + } + EncFormat::J => { + check_range(value, -(1 << 20), (1 << 20) - 1, "offset")?; + if value % 2 != 0 { + return Err(format!("jump offset {value} must be even")); + } + let v = value as u32; + let word = set_bits(word, 21, 10, (v >> 1) & 0x3ff); + let word = set_bits(word, 20, 1, (v >> 11) & 1); + let word = set_bits(word, 12, 8, (v >> 12) & 0xff); + Ok(set_bits(word, 31, 1, (v >> 20) & 1)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use InstrFieldKind::*; + + /// `addi a0, zero, 1` — I-type. + const ADDI: u32 = 0x00100513; + /// `add a0, a1, a2` — R-type. + const ADD: u32 = 0x00c58533; + /// `beq a0, a1, 8` — B-type. + const BEQ: u32 = 0x00b50463; + /// `lui a1, 0x1` — U-type. + const LUI: u32 = 0x000015b7; + /// `jal ra, 16` — J-type. + const JAL: u32 = 0x010000ef; + /// `sw a0, 4(sp)` — S-type. + const SW: u32 = 0x00a12223; + + #[test] + fn seed_matches_decoded_fields() { + assert_eq!(seed_field(ADDI, Rd), "x10"); + assert_eq!(seed_field(ADDI, Rs1), "x0"); + assert_eq!(seed_field(ADDI, Imm), "1"); + assert_eq!(seed_field(ADDI, Opcode), "0x13"); + assert_eq!(seed_field(ADDI, Bin), format!("{ADDI:032b}")); + assert_eq!(seed_field(LUI, Imm), "1"); + assert_eq!(seed_field(BEQ, Imm), "8"); + assert_eq!(seed_field(JAL, Imm), "16"); + assert_eq!(seed_field(SW, Imm), "4"); + } + + #[test] + fn splice_same_value_round_trips() { + for &word in &[ADDI, ADD, BEQ, LUI, JAL, SW] { + let format = detect_format(word); + for field in [Rd, Rs1, Rs2, Imm, Opcode, Funct3, Funct7, Shamt, Bin] { + if !field_available(word, field) { + continue; + } + let seed = seed_field(word, field); + let v = parse_field_value(field, &seed).unwrap(); + assert_eq!( + splice_field(word, format, field, v).unwrap(), + word, + "round-trip of {field:?} on {word:#010x}" + ); + } + } + } + + #[test] + fn splice_rd_rewrites_only_rd_bits() { + let v = parse_field_value(Rd, "a1").unwrap(); + assert_eq!(v, 11); + let out = splice_field(ADDI, EncFormat::I, Rd, v).unwrap(); + assert_eq!((out >> 7) & 0x1f, 11); + assert_eq!(out & !(0x1f << 7), ADDI & !(0x1f << 7)); + } + + #[test] + fn registers_accept_bare_numbers_and_abi_names() { + assert_eq!(parse_field_value(Rs1, "11").unwrap(), 11); + assert_eq!(parse_field_value(Rs1, "x11").unwrap(), 11); + assert_eq!(parse_field_value(Rs1, "a1").unwrap(), 11); + assert!(parse_field_value(Rs1, "32").is_err()); + assert!(parse_field_value(Rs1, "q7").is_err()); + } + + #[test] + fn out_of_range_values_are_rejected() { + assert!(splice_field(ADDI, EncFormat::I, Funct3, 9).is_err()); + assert!(splice_field(ADDI, EncFormat::I, Imm, 4096).is_err()); + assert!(splice_field(BEQ, EncFormat::B, Imm, 7).is_err()); // odd offset + assert!(splice_field(ADD, EncFormat::R, Rs2, 32).is_err()); + } + + #[test] + fn branch_offset_splices_all_bit_groups() { + // beq a0, a1, -2: imm[12|11|10:5|4:1] scattered encoding. + let out = splice_field(BEQ, EncFormat::B, Imm, -2).unwrap(); + assert_eq!(decode_imm(out, EncFormat::B), -2); + // Untouched fields survive. + assert_eq!((out >> 15) & 0x1f, (BEQ >> 15) & 0x1f); + assert_eq!((out >> 20) & 0x1f, (BEQ >> 20) & 0x1f); + assert_eq!(out & 0x7f, BEQ & 0x7f); + } + + #[test] + fn jump_offset_round_trips_negative() { + let out = splice_field(JAL, EncFormat::J, Imm, -4).unwrap(); + assert_eq!(decode_imm(out, EncFormat::J), -4); + } + + #[test] + fn shift_fields_only_on_shift_instructions() { + // slli a0, a0, 3 + let slli: u32 = 0x00351513; + assert!(field_available(slli, Shamt)); + assert!(field_available(slli, Funct7)); + assert!(!field_available(ADDI, Shamt)); + assert!(!field_available(ADDI, Funct7)); + let out = splice_field(slli, EncFormat::I, Shamt, 5).unwrap(); + assert_eq!((out >> 20) & 0x1f, 5); + } +} diff --git a/src/ui/app/mod.rs b/src/ui/app/mod.rs index e24fae0..0343db7 100644 --- a/src/ui/app/mod.rs +++ b/src/ui/app/mod.rs @@ -3,6 +3,7 @@ mod cpi; mod docs_state; mod formatting; mod hart; +mod instr_edit; mod run_loop; mod run_state; mod runtime; @@ -29,8 +30,10 @@ pub(crate) use self::docs_state::{ pub(crate) use self::hart::{ HartCoreRuntime, HartLifecycle, is_transparent_single_step_word, step_hart_bg_inner, }; +pub(crate) use self::instr_edit::{EncFormat, InstrFieldKind, Seg, detect_format}; pub(crate) use self::run_state::{ - BuildStats, EditorMode, EditorState, FormatMode, MemRegion, RunButton, RunSpeed, RunState, + BuildStats, EditorMode, EditorState, FormatMode, MemRegion, RunButton, RunEditTarget, RunSpeed, + RunState, }; pub(crate) use self::settings_state::{ RunScope, SETTINGS_ROW_CACHE_ENABLED, SETTINGS_ROW_CPI_START, SETTINGS_ROW_JIT_MODE, @@ -46,6 +49,8 @@ use super::{ view::ui, }; use crate::falcon::cache::CacheConfig; +use crate::falcon::machine::parse::{CellFormat, parse_cell}; +use crate::falcon::machine::types::{MemWidth, RegTarget}; use crate::falcon::{self, CacheController, Cpu}; use crate::ui::platform::Clipboard; use crossterm::{ @@ -229,7 +234,6 @@ pub struct App { pub(super) cache: CacheState, pub(super) tlb: TlbState, pub(super) settings: SettingsState, - pub(super) pipeline: crate::ui::pipeline::PipelineSimState, pub(crate) max_cores: usize, pub(crate) selected_core: usize, pub(crate) run_scope: RunScope, @@ -357,16 +361,19 @@ impl App { show_encoding: false, }, run: RunState { - cpu, + machine: crate::falcon::machine::Machine::new( + cpu, + CacheController::new( + CacheConfig::default(), + CacheConfig::default(), + vec![], + mem_size, + ), + crate::ui::pipeline::PipelineSimState::new(), + ), prev_x: [0; 32], prev_pc: base_pc, mem_size, - mem: CacheController::new( - CacheConfig::default(), - CacheConfig::default(), - vec![], - mem_size, - ), breakpoints: std::collections::HashSet::new(), base_pc, data_base, @@ -393,6 +400,7 @@ impl App { imem_width_start: 34, imem_scroll: 0, hover_imem_addr: None, + last_imem_click: None, imem_inner_height: std::cell::Cell::new(16), imem_collapsed: false, imem_search_open: false, @@ -403,6 +411,10 @@ impl App { imem_search_cursor: 0, imem_search_match_count: 0, details_collapsed: false, + details_addr: None, + last_details_click: None, + details_field_hitboxes: std::cell::RefCell::new(Vec::new()), + details_rendered_addr: std::cell::Cell::new(0), console_height: 5, hover_console_bar: false, hover_console_clear: false, @@ -415,6 +427,10 @@ impl App { step_interval: Duration::from_millis(80), faulted: false, speed: RunSpeed::X1, + go_checkpointed: false, + run_edit: None, + run_edit_buf: String::new(), + run_edit_error: None, comments: std::collections::HashMap::new(), labels: std::collections::HashMap::new(), halt_pcs: std::collections::HashSet::new(), @@ -547,7 +563,6 @@ impl App { tutorial: TutorialState::default(), activity: crate::guided_learning::GuidedLearningState::default(), settings: SettingsState::default(), - pipeline: crate::ui::pipeline::PipelineSimState::new(), max_cores: 4, selected_core: 0, run_scope: RunScope::AllHarts, @@ -582,46 +597,51 @@ impl App { /// Reset the pipeline to the current CPU PC (used after loading a preset). pub(crate) fn pipeline_reset_to_current_pc(&mut self) { - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); } pub(crate) fn assemble_and_load(&mut self) { use falcon::asm::assemble; use falcon::program::{load_bytes, load_words, zero_bytes}; - self.run.prev_x = self.run.cpu.x; + self.run.prev_x = self.run.cpu().x; self.run.mem_size = self.ram_override.unwrap_or(16 * 1024 * 1024); - self.run.cpu = Cpu::default(); - self.run.cpu.pc = self.run.base_pc; - self.run.prev_pc = self.run.cpu.pc; - self.run.cpu.write(2, self.run.mem_size as u32); - self.run.mem = CacheController::new( + *self.run.machine.cpu_mut_unjournaled() = Cpu::default(); + self.run.machine.cpu_mut_unjournaled().pc = self.run.base_pc; + self.run.prev_pc = self.run.cpu().pc; + self.run.machine.cpu_mut_unjournaled().write(2, self.run.mem_size as u32); + *self.run.machine.mem_mut_unjournaled() = CacheController::new( self.cache.pending_icache.clone(), self.cache.pending_dcache.clone(), self.cache.extra_pending.clone(), self.run.mem_size, ); - self.run.mem.bypass = !self.run.cache_enabled; - self.run.mem.mmu_mut().tlb.reconfigure(self.tlb.pending.clone()); + self.run.machine.mem_mut_unjournaled().bypass = !self.run.cache_enabled; + self.run + .machine + .mem_mut_unjournaled() + .mmu_mut() + .tlb + .reconfigure(self.tlb.pending.clone()); self.push_vm_mode_to_mmu(); self.run.faulted = false; match assemble(&self.editor.buf.text(), self.run.base_pc) { Ok(prog) => { // Write directly to RAM (bypass cache) so invalidate() won't discard data - if let Err(e) = load_words(&mut self.run.mem.ram, self.run.base_pc, &prog.text) { + if let Err(e) = load_words(&mut self.run.machine.mem_mut_unjournaled().ram, self.run.base_pc, &prog.text) { self.console.push_error(e.to_string()); self.run.faulted = true; return; } - if let Err(e) = load_bytes(&mut self.run.mem.ram, prog.data_base, &prog.data) { + if let Err(e) = load_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, prog.data_base, &prog.data) { self.console.push_error(e.to_string()); self.run.faulted = true; return; } let bss_base = prog.data_base.saturating_add(prog.data.len() as u32); if prog.bss_size > 0 { - if let Err(e) = zero_bytes(&mut self.run.mem.ram, bss_base, prog.bss_size) { + if let Err(e) = zero_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, bss_base, prog.bss_size) { self.console.push_error(e.to_string()); self.run.faulted = true; return; @@ -631,13 +651,13 @@ impl App { self.run.mem_view_addr = prog.data_base; self.run.mem_region = MemRegion::Data; // Invalidate & reset stats so execution starts from cold cache - self.run.mem.invalidate_all(); - self.run.mem.reset_stats(); + self.run.machine.mem_mut_unjournaled().invalidate_all(); + self.run.machine.mem_mut_unjournaled().reset_stats(); let bss_end = prog .data_base .wrapping_add(prog.data.len() as u32 + prog.bss_size); self.run.heap_start = (bss_end.wrapping_add(15)) & !15; - self.run.cpu.heap_break = self.run.heap_start; + self.run.machine.cpu_mut_unjournaled().heap_break = self.run.heap_start; self.run.comments = prog.comments; self.run.block_comments = prog.block_comments; @@ -660,6 +680,7 @@ impl App { }); self.run.imem_scroll = 0; self.run.hover_imem_addr = None; + self.clear_details_selection(); self.reset_exec_regions_to_loaded_text(); self.sync_pipeline_program_range(); @@ -675,7 +696,7 @@ impl App { self.run.heap_start, ); crate::falcon::mmu::Mmu::install_map_scheme( - &mut self.run.mem.ram, + &mut self.run.machine.mem_mut_unjournaled().ram, root_pa, &scheme, self.tlb.page_map, @@ -683,14 +704,14 @@ impl App { ); let satp_val = crate::falcon::mmu::Mmu::make_satp(root_pa, self.tlb.page_map.asid); - self.run.cpu.satp = satp_val; - let mmu = self.run.mem.mmu_mut(); + self.run.machine.cpu_mut_unjournaled().satp = satp_val; + let mmu = self.run.machine.mem_mut_unjournaled().mmu_mut(); mmu.satp = crate::falcon::mmu::Satp::new(satp_val); mmu.force_translate = true; } // Reset pipeline stages (shares cpu/mem with RunState) - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); self.editor.last_assemble_msg = Some(format!( "Assembled {} instructions, {} data bytes, {} bss bytes.", @@ -764,8 +785,8 @@ impl App { } fn sync_pipeline_program_range(&mut self) { - self.pipeline.set_exec_regions(&self.run.exec_regions); - let regions = self.pipeline.exec_regions.clone(); + let regions = self.run.exec_regions.clone(); + self.run.pipeline_mut().set_exec_regions(®ions); for hart in &mut self.harts { if let Some(p) = hart.pipeline.as_mut() { p.set_exec_regions(®ions); @@ -786,27 +807,32 @@ impl App { self.editor.last_ok_data.as_ref(), self.editor.last_ok_data_base, ) { - self.run.prev_x = self.run.cpu.x; + self.run.prev_x = self.run.cpu().x; self.run.mem_size = self.ram_override.unwrap_or(16 * 1024 * 1024); - self.run.cpu = Cpu::default(); - self.run.cpu.pc = self.run.base_pc; - self.run.prev_pc = self.run.cpu.pc; - self.run.cpu.write(2, self.run.mem_size as u32); - self.run.mem = CacheController::new( + *self.run.machine.cpu_mut_unjournaled() = Cpu::default(); + self.run.machine.cpu_mut_unjournaled().pc = self.run.base_pc; + self.run.prev_pc = self.run.cpu().pc; + self.run.machine.cpu_mut_unjournaled().write(2, self.run.mem_size as u32); + *self.run.machine.mem_mut_unjournaled() = CacheController::new( self.cache.pending_icache.clone(), self.cache.pending_dcache.clone(), self.cache.extra_pending.clone(), self.run.mem_size, ); - self.run.mem.bypass = !self.run.cache_enabled; - self.run.mem.mmu_mut().tlb.reconfigure(self.tlb.pending.clone()); + self.run.machine.mem_mut_unjournaled().bypass = !self.run.cache_enabled; + self.run + .machine + .mem_mut_unjournaled() + .mmu_mut() + .tlb + .reconfigure(self.tlb.pending.clone()); // Inlined `push_vm_mode_to_mmu` (the destructuring `let` above holds // an immutable borrow of `self.editor`, so we can't take `&mut self`). { let (enabled, force_translate) = self.run.vm_mode.flags(); let scheme = self.active_scheme(); let tlb_enabled = self.run.tlb_enabled; - let mmu = self.run.mem.mmu_mut(); + let mmu = self.run.machine.mem_mut_unjournaled().mmu_mut(); mmu.set_scheme(scheme); mmu.enabled = enabled; mmu.force_translate = force_translate; @@ -815,12 +841,12 @@ impl App { self.run.faulted = false; // Write directly to RAM (bypass cache) so invalidate() won't discard data - if let Err(e) = load_words(&mut self.run.mem.ram, self.run.base_pc, text) { + if let Err(e) = load_words(&mut self.run.machine.mem_mut_unjournaled().ram, self.run.base_pc, text) { self.console.push_error(e.to_string()); self.run.faulted = true; return; } - if let Err(e) = load_bytes(&mut self.run.mem.ram, data_base, data) { + if let Err(e) = load_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, data_base, data) { self.console.push_error(e.to_string()); self.run.faulted = true; return; @@ -828,7 +854,7 @@ impl App { if let Some(bss) = self.editor.last_ok_bss_size { if bss > 0 { let bss_base = data_base.saturating_add(data.len() as u32); - if let Err(e) = zero_bytes(&mut self.run.mem.ram, bss_base, bss) { + if let Err(e) = zero_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, bss_base, bss) { self.console.push_error(e.to_string()); self.run.faulted = true; return; @@ -839,14 +865,14 @@ impl App { self.run.data_base = data_base; self.run.mem_view_addr = data_base; self.run.mem_region = MemRegion::Data; - self.run.mem.invalidate_all(); - self.run.mem.reset_stats(); + self.run.machine.mem_mut_unjournaled().invalidate_all(); + self.run.machine.mem_mut_unjournaled().reset_stats(); let bss_sz = self.editor.last_ok_bss_size.unwrap_or(0); let bss_end = data_base .wrapping_add(data.len() as u32) .wrapping_add(bss_sz); self.run.heap_start = (bss_end.wrapping_add(15)) & !15; - self.run.cpu.heap_break = self.run.heap_start; + self.run.machine.cpu_mut_unjournaled().heap_break = self.run.heap_start; self.run.reg_age = [255u8; 32]; self.run.f_age = [255u8; 32]; @@ -868,10 +894,11 @@ impl App { self.rebuild_imem_vrow_cache(); self.run.imem_scroll = 0; self.run.hover_imem_addr = None; + self.clear_details_selection(); self.reset_exec_regions_to_loaded_text(); self.sync_pipeline_program_range(); // Reset pipeline stages so it picks up the reloaded program - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); self.rebuild_harts(); } } @@ -879,7 +906,11 @@ impl App { pub(super) fn restart_simulation(&mut self) { self.run.is_running = false; self.run.faulted = false; - self.run.cpu.ebreak_hit = false; + // Drop step-back history: the timeline (and any GO checkpoint) belongs to + // the program being replaced. + self.run.machine.clear_journal(); + self.run.go_checkpointed = false; + self.run.machine.cpu_mut_unjournaled().ebreak_hit = false; self.run.reg_last_write_pc = [None; 32]; self.run.f_last_write_pc = [None; 32]; self.run.reg_age = [255u8; 32]; @@ -890,16 +921,16 @@ impl App { self.cache.window_start_instr = 0; self.load_last_ok_program(); // Reset pipeline AFTER loading program (cpu.pc is now set correctly) - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); self.rebuild_harts(); // Rebuild JIT backend AFTER load so FullBackend can scan the loaded program. self.rebuild_backend(); } pub(super) fn load_binary(&mut self, bytes: &[u8]) { - self.run.prev_x = self.run.cpu.x; + self.run.prev_x = self.run.cpu().x; self.run.mem_size = self.ram_override.unwrap_or(16 * 1024 * 1024); // default 16 MB for ELF (heap support) - self.run.cpu = Cpu::default(); + *self.run.machine.cpu_mut_unjournaled() = Cpu::default(); self.run.reg_age = [255u8; 32]; self.run.f_age = [255u8; 32]; self.run.reg_last_write_pc = [None; 32]; @@ -907,22 +938,27 @@ impl App { self.run.exec_counts.clear(); self.run.exec_trace.clear(); self.run.mem_access_log.clear(); - self.run.cpu.write(2, self.run.mem_size as u32); - self.run.mem = CacheController::new( + self.run.machine.cpu_mut_unjournaled().write(2, self.run.mem_size as u32); + *self.run.machine.mem_mut_unjournaled() = CacheController::new( self.cache.pending_icache.clone(), self.cache.pending_dcache.clone(), self.cache.extra_pending.clone(), self.run.mem_size, ); - self.run.mem.bypass = !self.run.cache_enabled; - self.run.mem.mmu_mut().tlb.reconfigure(self.tlb.pending.clone()); + self.run.machine.mem_mut_unjournaled().bypass = !self.run.cache_enabled; + self.run + .machine + .mem_mut_unjournaled() + .mmu_mut() + .tlb + .reconfigure(self.tlb.pending.clone()); self.push_vm_mode_to_mmu(); self.run.faulted = false; // ── Detect format and load ─────────────────────────────────────── if bytes.len() >= 4 && &bytes[0..4] == b"\x7fELF" { // ── ELF32 LE RISC-V ───────────────────────────────────────── - let info = match falcon::program::load_elf(bytes, &mut self.run.mem.ram) { + let info = match falcon::program::load_elf(bytes, &mut self.run.machine.mem_mut_unjournaled().ram) { Ok(i) => i, Err(e) => { self.console.push_error(e.to_string()); @@ -931,14 +967,14 @@ impl App { } }; - self.run.cpu.pc = info.entry; + self.run.machine.cpu_mut_unjournaled().pc = info.entry; self.run.prev_pc = info.entry; self.run.base_pc = info.text_base; self.run.data_base = info.data_base; self.run.mem_view_addr = info.data_base; self.run.mem_region = crate::ui::app::MemRegion::Data; - self.run.mem.invalidate_all(); - self.run.mem.reset_stats(); + self.run.machine.mem_mut_unjournaled().invalidate_all(); + self.run.machine.mem_mut_unjournaled().reset_stats(); let elf_data_bytes = info .sections .iter() @@ -950,7 +986,7 @@ impl App { self.run.halt_pcs.clear(); self.run.elf_sections = info.sections; self.run.heap_start = info.heap_start; - self.run.cpu.heap_break = info.heap_start; + self.run.machine.cpu_mut_unjournaled().heap_break = info.heap_start; let mut words = Vec::with_capacity(info.text_bytes.len() / 4); for chunk in info.text_bytes.chunks(4) { @@ -1010,13 +1046,13 @@ impl App { (bytes.to_vec(), Vec::new(), 0) }; - if let Err(e) = load_bytes(&mut self.run.mem.ram, self.run.base_pc, &text_bytes) { + if let Err(e) = load_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, self.run.base_pc, &text_bytes) { self.console.push_error(e.to_string()); self.run.faulted = true; return; } if !data_bytes.is_empty() { - if let Err(e) = load_bytes(&mut self.run.mem.ram, self.run.data_base, &data_bytes) { + if let Err(e) = load_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, self.run.data_base, &data_bytes) { self.console.push_error(e.to_string()); self.run.faulted = true; return; @@ -1024,17 +1060,17 @@ impl App { } if bss_size > 0 { let bss_base = self.run.data_base + data_bytes.len() as u32; - if let Err(e) = zero_bytes(&mut self.run.mem.ram, bss_base, bss_size) { + if let Err(e) = zero_bytes(&mut self.run.machine.mem_mut_unjournaled().ram, bss_base, bss_size) { self.console.push_error(e.to_string()); self.run.faulted = true; return; } } - self.run.cpu.pc = self.run.base_pc; + self.run.machine.cpu_mut_unjournaled().pc = self.run.base_pc; self.run.prev_pc = self.run.base_pc; - self.run.mem.invalidate_all(); - self.run.mem.reset_stats(); + self.run.machine.mem_mut_unjournaled().invalidate_all(); + self.run.machine.mem_mut_unjournaled().reset_stats(); // Heap starts right after BSS, 16-byte aligned let bss_end = self @@ -1043,7 +1079,7 @@ impl App { .wrapping_add(data_bytes.len() as u32) .wrapping_add(bss_size); self.run.heap_start = (bss_end.wrapping_add(15)) & !15; - self.run.cpu.heap_break = self.run.heap_start; + self.run.machine.cpu_mut_unjournaled().heap_break = self.run.heap_start; let mut words = Vec::with_capacity(text_bytes.len() / 4); for chunk in text_bytes.chunks(4) { @@ -1090,15 +1126,16 @@ impl App { self.editor.diag_line_text = None; self.run.imem_scroll = 0; self.run.hover_imem_addr = None; + self.clear_details_selection(); self.reset_exec_regions_to_loaded_text(); self.sync_pipeline_program_range(); // Lock the editor when a binary is loaded; close any stale prompt. self.mode = EditorMode::Command; self.editor.elf_prompt_open = false; // Reset pipeline stages (shares cpu/mem with RunState) - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); self.rebuild_harts(); - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); } /// Convert the currently-loaded ELF into an editable assembly source and load @@ -1301,7 +1338,7 @@ impl App { use crate::falcon::cache::extra_level_presets; let cfg = extra_level_presets()[0].clone(); // Small L2 default self.cache.extra_pending.push(cfg.clone()); - self.run.mem.add_extra_level(cfg); + self.run.machine.mem_mut_unjournaled().add_extra_level(cfg); // Select the newly added level self.cache.selected_level = self.cache.extra_pending.len(); // 1-based (L1=0) } @@ -1417,13 +1454,13 @@ impl App { self.tlb.config_error = Some("entry count must be ≥ associativity".into()); return; } - self.run.mem.mmu_mut().tlb.reconfigure(cfg); + self.run.machine.mem_mut_unjournaled().mmu_mut().tlb.reconfigure(cfg); self.tlb.config_error = None; self.tlb.config_status = Some("Applied (TLB reset)".into()); } pub(crate) fn flush_tlb(&mut self) { - self.run.mem.mmu_mut().tlb.flush(); + self.run.machine.mem_mut_unjournaled().mmu_mut().tlb.flush(); self.tlb.config_status = Some("TLB flushed".into()); self.tlb.config_error = None; } @@ -1574,7 +1611,7 @@ impl App { pub(crate) fn apply_vm_settings(&mut self) { let cfg = self.tlb.pending.clone(); if cfg.entry_count >= cfg.associativity as u16 && cfg.associativity >= 1 { - self.run.mem.mmu_mut().tlb.reconfigure(cfg); + self.run.machine.mem_mut_unjournaled().mmu_mut().tlb.reconfigure(cfg); } self.apply_page_map(); } @@ -1603,10 +1640,10 @@ impl App { let root_pa = scheme.root_pa(self.run.mem_size as u32); let window = (self.run.base_pc.min(self.run.data_base), self.run.heap_start); let spec = self.tlb.pending_map; - Mmu::install_map_scheme(&mut self.run.mem.ram, root_pa, &scheme, spec, window); + Mmu::install_map_scheme(&mut self.run.machine.mem_mut_unjournaled().ram, root_pa, &scheme, spec, window); let satp_val = Mmu::make_satp(root_pa, spec.asid); - self.run.cpu.satp = satp_val; - let mmu = self.run.mem.mmu_mut(); + self.run.machine.cpu_mut_unjournaled().satp = satp_val; + let mmu = self.run.machine.mem_mut_unjournaled().mmu_mut(); mmu.set_scheme(scheme); mmu.satp = Satp::new(satp_val); mmu.force_translate = true; @@ -1632,7 +1669,7 @@ impl App { pub(super) fn remove_last_cache_level(&mut self) { if !self.cache.extra_pending.is_empty() { self.cache.extra_pending.pop(); - self.run.mem.remove_extra_level(); + self.run.machine.mem_mut_unjournaled().remove_extra_level(); let max_level = self.cache.extra_pending.len(); if self.cache.selected_level > max_level { self.cache.selected_level = max_level; @@ -1673,7 +1710,7 @@ impl App { } pub(crate) fn active_imem_exec_region(&self) -> Option { - let pc = self.run.cpu.pc; + let pc = self.run.cpu().pc; let region = self.executable_region_containing(pc)?; if self.imem_in_range(pc) { None @@ -1712,7 +1749,7 @@ impl App { } fn process_pending_exec_map_for_selected(&mut self) { - if let Some(region) = self.run.cpu.pending_exec_map.take() { + if let Some(region) = self.run.machine.cpu_mut_unjournaled().pending_exec_map.take() { self.register_exec_region(region); } } @@ -1780,7 +1817,7 @@ impl App { /// Visual row of the current PC within the full instruction list. pub(super) fn imem_visual_row_of_pc(&self) -> Option { - let pc = self.run.cpu.pc; + let pc = self.run.cpu().pc; if let Some(region) = self.active_imem_exec_region() { return Some(((pc.saturating_sub(region.start)) / 4) as usize); } @@ -1814,7 +1851,7 @@ impl App { return; } if let Some(region) = self.active_imem_exec_region() { - let pc_row = ((self.run.cpu.pc.saturating_sub(region.start)) / 4) as usize; + let pc_row = ((self.run.cpu().pc.saturating_sub(region.start)) / 4) as usize; let max_scroll = ((region.end.saturating_sub(region.start)) / 4) as usize; let max_scroll = max_scroll.saturating_sub(visible); let scroll = self.run.imem_scroll.min(max_scroll); @@ -1886,31 +1923,46 @@ impl App { fn tick(&mut self) { if self.run.is_running { + // A live run owns the state; close any inline editor left open so its + // keystrokes don't fight the running program. + self.cancel_run_edit(); + // A GO/Instant burst writes RAM directly (un-journaled), so take one + // full checkpoint at the burst's first tick — step-back can then + // rewind to just before it. Rate-limited modes single-step through + // the journaling path and need no checkpoint; pipeline modes journal + // per-cycle separately (Phase 4b). + let go_burst = matches!(self.run.speed, RunSpeed::Instant) + && !self.run.pipeline().enabled + && !self.run.pipeline().sequential_mode; + if go_burst && !self.run.go_checkpointed { + self.run.machine.checkpoint(); + self.run.go_checkpointed = true; + } // When pipeline is enabled and we're viewing the Pipeline tab, // use pipeline speed for rate-limiting (educational slow stepping). // Otherwise use run speed. use crate::ui::pipeline::PipelineSpeed; - let use_pipeline_speed = (self.pipeline.enabled || self.pipeline.sequential_mode) + let use_pipeline_speed = (self.run.pipeline().enabled || self.run.pipeline().sequential_mode) && matches!(self.tab, Tab::Pipeline); if use_pipeline_speed { - match self.pipeline.speed { + match self.run.pipeline().speed { PipelineSpeed::Slow => { - if self.pipeline.last_tick.elapsed() >= Duration::from_millis(600) { + if self.run.pipeline().last_tick.elapsed() >= Duration::from_millis(600) { self.single_step(); - self.pipeline.last_tick = Instant::now(); + self.run.pipeline_mut().last_tick = Instant::now(); } } PipelineSpeed::Normal => { - if self.pipeline.last_tick.elapsed() >= Duration::from_millis(300) { + if self.run.pipeline().last_tick.elapsed() >= Duration::from_millis(300) { self.single_step(); - self.pipeline.last_tick = Instant::now(); + self.run.pipeline_mut().last_tick = Instant::now(); } } PipelineSpeed::Fast => { - if self.pipeline.last_tick.elapsed() >= Duration::from_millis(80) { + if self.run.pipeline().last_tick.elapsed() >= Duration::from_millis(80) { self.single_step(); - self.pipeline.last_tick = Instant::now(); + self.run.pipeline_mut().last_tick = Instant::now(); } } PipelineSpeed::Instant => { @@ -1961,6 +2013,10 @@ impl App { } } } + // Arm the next GO burst's one-shot checkpoint once the run has stopped. + if !self.run.is_running { + self.run.go_checkpointed = false; + } // Scroll instruction list to follow PC (skipped in Instant to avoid pointless churn) if self.run.is_running && !matches!(self.run.speed, RunSpeed::Instant) { self.ensure_pc_visible_in_imem(); @@ -1969,11 +2025,11 @@ impl App { // regardless of execution path (sequential or pipeline). match self.run.mem_region { MemRegion::Stack => { - let sp = self.run.cpu.x[2]; + let sp = self.run.cpu().x[2]; self.run.mem_view_addr = sp & !(self.run.mem_view_bytes - 1); } MemRegion::Heap => { - let hb = self.run.cpu.heap_break; + let hb = self.run.cpu().heap_break; self.run.mem_view_addr = hb & !(self.run.mem_view_bytes - 1); } _ => {} @@ -1993,22 +2049,22 @@ impl App { fn finalize_selected_core_after_step(&mut self) { self.process_pending_hart_start_for_selected(); self.process_pending_exec_map_for_selected(); - let heap_break = self.run.cpu.heap_break; + let heap_break = self.run.cpu().heap_break; self.propagate_heap_break(heap_break); - let program_exit = self.run.cpu.exit_code; + let program_exit = self.run.cpu().exit_code; - let lifecycle = if self.run.cpu.local_exit { + let lifecycle = if self.run.cpu().local_exit { // FALCON_HART_EXIT: exit only this hart, leave others running. HartLifecycle::Exited - } else if self.run.cpu.ebreak_hit { + } else if self.run.cpu().ebreak_hit { if self.run.halt_pcs.contains(&self.run.prev_pc) { HartLifecycle::Exited } else { HartLifecycle::Paused } - } else if self.run.faulted || self.pipeline.faulted { + } else if self.run.faulted || self.run.pipeline().faulted { HartLifecycle::Faulted - } else if self.run.cpu.exit_code.is_some() || self.pipeline.halted { + } else if self.run.cpu().exit_code.is_some() || self.run.pipeline().halted { HartLifecycle::Exited } else { HartLifecycle::Running @@ -2028,11 +2084,11 @@ impl App { hart.cpu.exit_code = Some(code); } } - self.run.mem.sync_to_ram(); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; } else if matches!(lifecycle, HartLifecycle::Faulted) { // A fault in any hart stops the whole run. - self.run.mem.sync_to_ram(); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; } else if matches!(lifecycle, HartLifecycle::Paused) { // In AllHarts scope: only stop the run when no other harts are still running. @@ -2046,11 +2102,297 @@ impl App { } } else if !matches!(lifecycle, HartLifecycle::Running) && !self.any_running_harts() { // Last hart finished (halt/local-exit) — stop. - self.run.mem.sync_to_ram(); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; } } + /// Whether a step-back is currently allowed: not mid auto-run, and with at + /// least one journaled change to undo. + /// + /// The journal is the ground truth for what is reversible. The sequential + /// interpreter (`step_interpreted`), each pipeline clock cycle + /// (`step_pipeline`), and GO checkpoints fill it; background harts and + /// program exit/fault mutate state through the un-journaled hatches, which + /// clear it. So a non-empty journal already implies the last activity was a + /// reversible step, pipeline cycle, or GO burst — no separate mode check is + /// needed here. + pub(crate) fn can_stepback_now(&self) -> bool { + !self.run.is_running && self.run.machine.can_stepback() + } + + /// Undo the most recent journaled change — one instruction, one edit, or the + /// whole of the last GO burst (back to its checkpoint) — then refresh the + /// derived run-tab bookkeeping so the view matches the rewound state. + pub(crate) fn stepback_one(&mut self) { + if !self.can_stepback_now() { + return; + } + let before_x = self.run.cpu().x; + let before_f = self.run.cpu().f; + let Some(kind) = self.run.machine.stepback() else { + return; + }; + + let now_x = self.run.cpu().x; + let now_f = self.run.cpu().f; + let pc = self.run.cpu().pc; + + // Highlight the registers/floats the undo reverted and age the rest, + // mirroring the forward single-step bookkeeping. + for i in 0..32 { + if now_x[i] != before_x[i] { + self.run.reg_age[i] = 0; + self.run.reg_last_write_pc[i] = Some(pc); + } else { + self.run.reg_age[i] = self.run.reg_age[i].saturating_add(1).min(8); + } + if now_f[i] != before_f[i] { + self.run.f_age[i] = 0; + self.run.f_last_write_pc[i] = Some(pc); + } else { + self.run.f_age[i] = self.run.f_age[i].saturating_add(1).min(8); + } + } + self.run.prev_x = now_x; + self.run.prev_f = now_f; + self.run.prev_pc = pc; + self.run.dyn_mem_access = None; + + // A committed instruction (sequential step, or a pipeline cycle that + // retired one) owns one exec-trace row and one run-count tick; a + // stall/bubble cycle (`Cycle`), an edit, or a GO checkpoint owns + // neither. Decrement the run count for the *retired* PC — taken from the + // popped trace row, not the rewound `cpu.pc`, since in the pipeline the + // instruction that committed is several stages behind the fetch PC. + if kind == crate::falcon::machine::StepbackKind::Step { + if let Some((trace_pc, _)) = self.run.exec_trace.pop_back() { + if let Some(count) = self.run.exec_counts.get_mut(&trace_pc) { + *count = count.saturating_sub(1); + if *count == 0 { + self.run.exec_counts.remove(&trace_pc); + } + } + } + } + + // Rewinding out of a fault/halt returns to a runnable state. + self.run.faulted = false; + if let Some(runtime) = self.selected_runtime_mut() { + runtime.lifecycle = HartLifecycle::Running; + runtime.faulted = false; + } + self.ensure_pc_visible_in_imem(); + } + + // ── Inline editing of live state (registers / PC / floats / RAM) ──────── + + /// Open the inline editor on `target`, seeding the buffer with the cell's + /// current value. No-op while a run is active — running state is the + /// program's to own, not the user's to hand-edit. + pub(crate) fn begin_run_edit(&mut self, target: RunEditTarget) { + if self.run.is_running { + return; + } + self.run.run_edit_buf = self.run_edit_seed(target); + self.run.run_edit = Some(target); + self.run.run_edit_error = None; + } + + /// Close the inline editor, discarding any in-progress text. + pub(crate) fn cancel_run_edit(&mut self) { + self.run.run_edit = None; + self.run.run_edit_buf.clear(); + self.run.run_edit_error = None; + } + + /// Drop the details panel's pinned instruction (it follows the PC again) + /// and close any instruction editor that referenced the old program. + /// Called when a (re)load replaces the instruction memory. + pub(crate) fn clear_details_selection(&mut self) { + self.run.details_addr = None; + self.run.last_details_click = None; + self.run.details_field_hitboxes.borrow_mut().clear(); + if matches!( + self.run.run_edit, + Some(RunEditTarget::Instr { .. } | RunEditTarget::InstrField { .. }) + ) { + self.cancel_run_edit(); + } + } + + /// Commit the open inline edit: parse the buffer against the target's width + /// and current display format, then write it through the journaling + /// `Machine` mutator so step-back can undo it. On rejection the editor stays + /// open and `run_edit_error` carries the reason, so the input isn't lost. + pub(crate) fn commit_run_edit(&mut self) { + let Some(target) = self.run.run_edit else { + return; + }; + let buf = self.run.run_edit_buf.clone(); + let before_x = self.run.cpu().x; + let before_f = self.run.cpu().f; + + let result: Result<(), String> = match target { + RunEditTarget::Reg(reg) => parse_cell(&buf, MemWidth::B4, self.cell_format(), self.run.show_signed) + .map_err(|e| e.message()) + .and_then(|value| { + self.run.machine.write_reg(reg, value as u32).map_err(|e| e.message()) + }), + RunEditTarget::FReg(freg) => match buf.trim().parse::() { + Ok(value) => { + self.run.machine.write_freg(freg, value.to_bits()); + Ok(()) + } + Err(_) => Err(format!("cannot parse \"{}\" as a float", buf.trim())), + }, + RunEditTarget::Mem { addr, width } => { + parse_cell(&buf, width, self.cell_format(), self.run.show_signed) + .map_err(|e| e.message()) + .and_then(|value| { + self.run.machine.write_mem(addr, width, value).map_err(|e| e.to_string()) + }) + } + RunEditTarget::Instr { addr } => { + parse_cell(&buf, MemWidth::B4, self.cell_format(), self.run.show_signed) + .map_err(|e| e.message()) + .and_then(|value| self.write_instr_word(addr, value as u32)) + } + RunEditTarget::InstrField { addr, field } => { + let current = self.run.mem().peek32(addr).unwrap_or(0); + let new_word = match field { + InstrFieldKind::Asm => match falcon::asm::assemble(&buf, addr) { + Ok(prog) if prog.text.len() == 1 => Ok(prog.text[0]), + Ok(prog) => Err(format!( + "expands to {} instructions — only a single instruction fits here", + prog.text.len() + )), + Err(e) => Err(e.msg), + }, + _ if !instr_edit::field_available(current, field) => Err(format!( + "{:?} is not a field of this instruction format", + field + )), + _ => instr_edit::parse_field_value(field, &buf) + .and_then(|v| instr_edit::splice_field(current, detect_format(current), field, v)), + }; + new_word.and_then(|word| self.write_instr_word(addr, word)) + } + }; + + match result { + Ok(()) => { + // A PC edit must also steer the pipeline: with the pipeline + // enabled (the default) execution fetches from `fetch_pc`, not + // `cpu.pc`, so without this the next step keeps fetching the old + // address. Mirrors the imem PC-redirect click. The redirect is + // reversible state captured by the change-set's pipeline + // snapshot, so step-back undoes it together with the PC write. + if matches!(target, RunEditTarget::Reg(RegTarget::Pc)) && self.run.pipeline().enabled + { + let pc = self.run.cpu().pc; + self.run.pipeline_mut().redirect_pc(pc); + } + self.cancel_run_edit(); + self.refresh_after_edit(before_x, before_f, target); + } + Err(message) => self.run.run_edit_error = Some(message), + } + } + + /// Write a full instruction word through the journaled path shared by the + /// word editor and the per-field editors, so step-back undoes the edit. + fn write_instr_word(&mut self, addr: u32, word: u32) -> Result<(), String> { + self.run + .machine + .write_mem(addr, MemWidth::B4, word as u64) + .map_err(|e| e.to_string())?; + // The JIT may hold a translation of the old word; drop it so the next + // run re-translates from the edited memory. + self.run.backend.invalidate(addr, addr.wrapping_add(4)); + // The pipeline may have already fetched the old word into its latches; + // refetch from the current PC so the stale instruction never executes. + if self.run.pipeline().enabled { + let pc = self.run.cpu().pc; + self.run.pipeline_mut().redirect_pc(pc); + } + Ok(()) + } + + /// Map the Run tab's display format to the parser's [`CellFormat`]. + pub(crate) fn cell_format(&self) -> CellFormat { + match self.run.fmt_mode { + FormatMode::Hex => CellFormat::Hex, + FormatMode::Dec => CellFormat::Dec, + FormatMode::Bin => CellFormat::Bin, + FormatMode::Str => CellFormat::Str, + } + } + + /// The text an editor starts from: the cell's current value, formatted the + /// way it reads on screen (plain digits, no `0x`). `Str` mode seeds empty — + /// rendering non-printable bytes back as `.` would round-trip lossily. + fn run_edit_seed(&self, target: RunEditTarget) -> String { + let plain_word = |value: u32| match self.run.fmt_mode { + FormatMode::Hex => format!("{value:x}"), + FormatMode::Dec if self.run.show_signed => format!("{}", value as i32), + FormatMode::Dec => format!("{value}"), + FormatMode::Bin => format!("{value:b}"), + FormatMode::Str => String::new(), + }; + match target { + RunEditTarget::Reg(RegTarget::Pc) => plain_word(self.run.cpu().pc), + RunEditTarget::Reg(RegTarget::X(reg)) => { + plain_word(self.run.cpu().x[reg.index() as usize]) + } + RunEditTarget::FReg(freg) => { + let value = f32::from_bits(self.run.cpu().f[freg.index() as usize]); + format!("{value}") + } + RunEditTarget::Mem { addr, width } => { + let raw = match width { + MemWidth::B1 => self.run.mem().effective_read8(addr).unwrap_or(0) as u32, + MemWidth::B2 => self.run.mem().effective_read16(addr).unwrap_or(0) as u32, + MemWidth::B4 => self.run.mem().effective_read32(addr).unwrap_or(0), + }; + plain_word(raw) + } + // Seed from `peek32`, the same read the details panel renders. + RunEditTarget::Instr { addr } => plain_word(self.run.mem().peek32(addr).unwrap_or(0)), + RunEditTarget::InstrField { addr, field } => { + instr_edit::seed_field(self.run.mem().peek32(addr).unwrap_or(0), field) + } + } + } + + /// Refresh the Run tab's highlight bookkeeping after a committed edit: + /// light up the register/float it changed and flag the touched memory cell. + fn refresh_after_edit(&mut self, before_x: [u32; 32], before_f: [u32; 32], target: RunEditTarget) { + let pc = self.run.cpu().pc; + let now_x = self.run.cpu().x; + let now_f = self.run.cpu().f; + for i in 0..32 { + if now_x[i] != before_x[i] { + self.run.reg_age[i] = 0; + self.run.reg_last_write_pc[i] = Some(pc); + } + if now_f[i] != before_f[i] { + self.run.f_age[i] = 0; + self.run.f_last_write_pc[i] = Some(pc); + } + } + match target { + RunEditTarget::Mem { addr, width } => { + self.run.mem_access_log.push((addr, width.bytes(), 0)); + } + RunEditTarget::Instr { addr } | RunEditTarget::InstrField { addr, .. } => { + self.run.mem_access_log.push((addr, MemWidth::B4.bytes(), 0)); + } + _ => {} + } + self.ensure_pc_visible_in_imem(); + } + fn any_running_harts(&self) -> bool { self.harts .iter() @@ -2105,8 +2447,8 @@ impl App { h.cpu.exit_code = Some(code); } } - self.run.cpu.exit_code = Some(code); - self.run.mem.sync_to_ram(); + self.run.machine.cpu_mut_unjournaled().exit_code = Some(code); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; return; } @@ -2115,7 +2457,7 @@ impl App { self.harts[core_idx].faulted = matches!(lifecycle, HartLifecycle::Faulted); if matches!(lifecycle, HartLifecycle::Faulted) { - self.run.mem.sync_to_ram(); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; } else if matches!(lifecycle, HartLifecycle::Paused) { // step_all_cores_once is only called in AllHarts scope; keep running @@ -2124,7 +2466,7 @@ impl App { self.run.is_running = false; } } else if !matches!(lifecycle, HartLifecycle::Running) && !self.any_running_harts() { - self.run.mem.sync_to_ram(); + self.run.machine.mem_mut_unjournaled().sync_to_ram(); self.run.is_running = false; } @@ -2157,10 +2499,10 @@ impl App { if self.core_status(self.selected_core) != HartLifecycle::Paused { return; } - self.run.cpu.ebreak_hit = false; + self.run.machine.cpu_mut_unjournaled().ebreak_hit = false; self.run.faulted = false; - self.pipeline.halted = false; - self.pipeline.faulted = false; + self.run.pipeline_mut().halted = false; + self.run.pipeline_mut().faulted = false; if let Some(runtime) = self.selected_runtime_mut() { if runtime.hart_id.is_some() { runtime.lifecycle = HartLifecycle::Running; @@ -2174,43 +2516,44 @@ impl App { // Sequential mode: if the CPU advanced outside the pipeline (e.g. the // user stepped in the Run tab), auto-reset so the visualization starts // fresh from the current PC. - if self.pipeline.sequential_mode { - let all_clear = self.pipeline.stages.iter().all(|s| s.is_none()); + if self.run.pipeline().sequential_mode { + let all_clear = self.run.pipeline().stages.iter().all(|s| s.is_none()); if all_clear - && self.pipeline.fetch_pc != self.run.cpu.pc - && !self.pipeline.halted - && !self.pipeline.faulted + && self.run.pipeline().fetch_pc != self.run.cpu().pc + && !self.run.pipeline().halted + && !self.run.pipeline().faulted { - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); } } - if self.pipeline.halted || self.pipeline.faulted { + if self.run.pipeline().halted || self.run.pipeline().faulted { return false; } - // Restore the selected hart's satp/priv_mode into the shared MMU - // before stepping its pipeline. See B18 in the bug audit. - crate::ui::app::hart::sync_mmu_to_cpu(&mut self.run.mem, &self.run.cpu); - - self.run.prev_x = self.run.cpu.x; - self.run.prev_f = self.run.cpu.f; - self.run.prev_pc = self.run.cpu.pc; + self.run.prev_x = self.run.cpu().x; + self.run.prev_f = self.run.cpu().f; + self.run.prev_pc = self.run.cpu().pc; // Clone CpiConfig to avoid borrow conflict (80 bytes, cheap) let cpi = self.run.cpi_config.clone(); - let commit = crate::ui::pipeline::sim::pipeline_tick( - &mut self.pipeline, - &mut self.run.cpu, - &mut self.run.mem, - &cpi, - &mut self.console, + // One journaled clock cycle. `step_pipeline` re-syncs the MMU to the + // selected hart (journal-preserving), snapshots cpu+mem+pipeline, runs + // the tick on the machine-owned pipeline, and records the change-set — + // so a single step-back rewinds exactly this cycle. The closure borrows + // the console, which is disjoint from `self.run.machine`. + let console = &mut self.console; + let commit = self.run.machine.step_pipeline( + |pipe, cpu, mem| { + crate::ui::pipeline::sim::pipeline_tick(pipe, cpu, mem, &cpi, console) + }, + |commit| commit.is_some(), ); let committed = if let Some(info) = commit { *self.run.exec_counts.entry(info.pc).or_insert(0) += 1; - let word = self.run.mem.peek32(info.pc).unwrap_or(0); + let word = self.run.mem().peek32(info.pc).unwrap_or(0); let disasm = { match falcon::decoder::decode(word) { Ok(instr) => format!("{instr:?}"), @@ -2223,7 +2566,7 @@ impl App { } for i in 0..32usize { - if self.run.cpu.x[i] != self.run.prev_x[i] { + if self.run.cpu().x[i] != self.run.prev_x[i] { self.run.reg_age[i] = 0; self.run.reg_last_write_pc[i] = Some(info.pc); } else { @@ -2231,28 +2574,27 @@ impl App { } } for i in 0..32usize { - if self.run.cpu.f[i] != self.run.prev_f[i] { + if self.run.cpu().f[i] != self.run.prev_f[i] { self.run.f_age[i] = 0; self.run.f_last_write_pc[i] = Some(info.pc); } else { self.run.f_age[i] = self.run.f_age[i].saturating_add(1).min(8); } } - self.run.prev_x = self.run.cpu.x; - self.run.prev_f = self.run.cpu.f; + self.run.prev_x = self.run.cpu().x; + self.run.prev_f = self.run.cpu().f; self.run.prev_pc = info.pc; - self.run.mem.instruction_count = self.run.mem.instruction_count.saturating_add(1); - self.run.mem.snapshot_stats(); + self.run.machine.account_pipeline_commit(); !is_transparent_single_step_word(word) } else { false }; - if self.pipeline.faulted { + if self.run.pipeline().faulted { self.run.faulted = true; } - if self.run.breakpoints.contains(&self.run.cpu.pc) { + if self.run.breakpoints.contains(&self.run.cpu().pc) { self.run.is_running = false; } self.finalize_selected_core_after_step(); @@ -2268,7 +2610,7 @@ impl App { // borrow checker's disjoint-field rules. let exec_regions = self.run.exec_regions.clone(); let mem_size = self.run.mem_size; - let pipeline_enabled = self.pipeline.enabled || self.pipeline.sequential_mode; + let pipeline_enabled = self.run.pipeline().enabled || self.run.pipeline().sequential_mode; // CpiConfig is ~80 bytes; cheap to clone once per round. let cpi = self.run.cpi_config.clone(); @@ -2295,11 +2637,11 @@ impl App { if self.harts[core_idx].lifecycle != HartLifecycle::Running { continue; } - // Disjoint field borrows: self.harts vs self.run.mem vs + // Disjoint field borrows: self.harts vs self.run.machine vs // self.run.backend vs self.console. let faulted = { let hart = &mut self.harts[core_idx]; - let mem = &mut self.run.mem; + let mem = self.run.machine.mem_mut_unjournaled(); let console = &mut self.console; let backend = self.run.backend.as_mut(); step_hart_bg_inner( @@ -2333,10 +2675,10 @@ impl App { .iter() .filter(|h| h.hart_id.is_some()) .map(|h| h.cpu.heap_break) - .chain(std::iter::once(self.run.cpu.heap_break)) + .chain(std::iter::once(self.run.cpu().heap_break)) .max() - .unwrap_or(self.run.cpu.heap_break); - if max_break != self.run.cpu.heap_break { + .unwrap_or(self.run.cpu().heap_break); + if max_break != self.run.cpu().heap_break { self.propagate_heap_break(max_break); } @@ -2344,7 +2686,7 @@ impl App { // exec_counts/exec_trace). Keeps harts[selected].cpu current so that // UI code and tests that read it directly get a consistent view. if let Some(runtime) = self.harts.get_mut(original) { - runtime.cpu = self.run.cpu.clone(); + runtime.cpu = self.run.cpu().clone(); runtime.prev_pc = self.run.prev_pc; runtime.prev_x = self.run.prev_x; runtime.prev_f = self.run.prev_f; @@ -2362,7 +2704,7 @@ impl App { if status == HartLifecycle::Paused { self.resume_selected_hart(); } - if self.pipeline.enabled || self.pipeline.sequential_mode { + if self.run.pipeline().enabled || self.run.pipeline().sequential_mode { self.pipeline_step() } else { self.single_step_selected_sequential(); @@ -2373,7 +2715,7 @@ impl App { fn pipeline_tab_step_once(&mut self) -> bool { // Sequential mode always drives the selected hart through the pipeline // visualizer; other harts stay paused. - if self.pipeline.sequential_mode { + if self.run.pipeline().sequential_mode { return self.pipeline_step(); } if self.max_cores > 1 { @@ -2393,24 +2735,24 @@ impl App { } if matches!(self.tab, Tab::Pipeline) - && (self.pipeline.enabled || self.pipeline.sequential_mode) + && (self.run.pipeline().enabled || self.run.pipeline().sequential_mode) { // Pipeline tab (pipelined or sequential): advance one cycle, then // skip only consecutive cache-only hold cycles. If a cycle advanced // stages or committed, stop immediately so EX/MEM/WB remain visible. let committed = self.pipeline_tab_step_once(); - if committed || !self.pipeline.last_cycle_cache_only { + if committed || !self.run.pipeline().last_cycle_cache_only { if !self.run.is_running { self.ensure_pc_visible_in_imem(); } return; } for _ in 0..1_000_000 { - if self.pipeline.halted || self.pipeline.faulted { + if self.run.pipeline().halted || self.run.pipeline().faulted { break; } let committed = self.pipeline_tab_step_once(); - if committed || !self.pipeline.last_cycle_cache_only { + if committed || !self.run.pipeline().last_cycle_cache_only { break; } } @@ -2423,7 +2765,7 @@ impl App { if self.max_cores > 1 { let all_scope = matches!(self.run_scope, RunScope::AllHarts); - if (self.pipeline.enabled || self.pipeline.sequential_mode) + if (self.run.pipeline().enabled || self.run.pipeline().sequential_mode) && !matches!(self.tab, Tab::Pipeline) { for _ in 0..200 { @@ -2471,12 +2813,12 @@ impl App { return; } - if self.pipeline.enabled || self.pipeline.sequential_mode { + if self.run.pipeline().enabled || self.run.pipeline().sequential_mode { // Run/Cache/other tabs: advance until one instruction commits // Safety limit to prevent infinite loop on stall/halt/fault for _ in 0..200 { let committed = self.pipeline_step(); - if committed || self.pipeline.halted || self.pipeline.faulted { + if committed || self.run.pipeline().halted || self.run.pipeline().faulted { break; } } @@ -2490,18 +2832,20 @@ impl App { } fn single_step_selected_sequential(&mut self) { - // Restore the selected hart's satp/priv_mode into the shared MMU — - // a background hart may have just run with different page tables. - crate::ui::app::hart::sync_mmu_to_cpu(&mut self.run.mem, &self.run.cpu); + // Restore the selected hart's satp/priv_mode into the shared MMU — a + // background hart may have just run with different page tables. Uses the + // journal-preserving sync (it only touches MMU metadata) so the step + // history survives across single-steps. + self.run.machine.sync_mmu(); let go_mode = matches!(self.run.speed, RunSpeed::Instant); for _ in 0..16 { // In GO mode skip the 256-byte register snapshot — reg_age not updated mid-run. if !go_mode { - self.run.prev_x = self.run.cpu.x; - self.run.prev_f = self.run.cpu.f; + self.run.prev_x = self.run.cpu().x; + self.run.prev_f = self.run.cpu().f; } - self.run.prev_pc = self.run.cpu.pc; - let step_pc = self.run.cpu.pc; + self.run.prev_pc = self.run.cpu().pc; + let step_pc = self.run.cpu().pc; if !self.pc_in_executable_region(step_pc) { self.console.push_error(format!( @@ -2512,28 +2856,27 @@ impl App { return; } - let word = self.run.mem.peek32(step_pc).unwrap_or(0); - let cpi_cycles = classify_cpi_cycles(word, &self.run.cpu, &self.run.cpi_config); + let word = self.run.mem().peek32(step_pc).unwrap_or(0); + let cpi_cycles = classify_cpi_cycles(word, self.run.cpu(), &self.run.cpi_config); let mem_access = if go_mode { None } else { - classify_mem_access(word, &self.run.cpu) + classify_mem_access(word, self.run.cpu()) }; let res = std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| { - let mut ctx = crate::falcon::jit::ExecCtx::new( - &mut self.run.cpu, - &mut self.run.mem, - &mut self.console, - ); if go_mode { // Run mode: usa o backend JIT completo (blocos compilados). + // A rajada GO escreve direto na RAM, então passa pelo escape + // hatch não-journalizado. + let (cpu, mem) = self.run.machine.cpu_mem_mut_unjournaled(); + let mut ctx = crate::falcon::jit::ExecCtx::new(cpu, mem, &mut self.console); self.run.backend.run_until_yield(&mut ctx) } else { - // Step mode: sempre 1 instrução via interpretador para que - // trace, highlights e exec_counts sejam por-instrução. - use crate::falcon::jit::ExecutionBackend as _; - crate::falcon::jit::InterpreterBackend::default().run_until_yield(&mut ctx) + // Step mode: 1 instrução via interpretador, journalizada pelo + // `Machine` para que trace/highlights/exec_counts sejam + // por-instrução e o stepback possa revertê-la. + self.run.machine.step_interpreted(&mut self.console) } })); let (alive, jit_instr_count) = match res { @@ -2568,8 +2911,14 @@ impl App { (false, 1) } }; - self.run.mem.add_instruction_cycles(cpi_cycles); - self.run.mem.snapshot_stats(); + if go_mode { + self.run.machine.mem_mut_unjournaled().add_instruction_cycles(cpi_cycles); + self.run.machine.mem_mut_unjournaled().snapshot_stats(); + } else { + // Dobra o accounting de ciclos/stats dentro do passo journalizado, + // sem zerar o journal — o stepback (Fase 4) o desfaz junto. + self.run.machine.account_step_cycles(cpi_cycles); + } // Track every instruction the JIT block executed (not just block entry). // For the interpreter jit_instr_count == 1, so this is equivalent. @@ -2597,7 +2946,7 @@ impl App { } for i in 0..32usize { - if self.run.cpu.x[i] != self.run.prev_x[i] { + if self.run.cpu().x[i] != self.run.prev_x[i] { self.run.reg_age[i] = 0; self.run.reg_last_write_pc[i] = Some(step_pc); } else { @@ -2605,7 +2954,7 @@ impl App { } } for i in 0..32usize { - if self.run.cpu.f[i] != self.run.prev_f[i] { + if self.run.cpu().f[i] != self.run.prev_f[i] { self.run.f_age[i] = 0; self.run.f_last_write_pc[i] = Some(step_pc); } else { @@ -2628,14 +2977,14 @@ impl App { } } - if alive && self.run.breakpoints.contains(&self.run.cpu.pc) { + if alive && self.run.breakpoints.contains(&self.run.cpu().pc) { self.run.is_running = false; } if !alive { if !self.console.reading { - self.run.faulted = self.run.cpu.exit_code.is_none() - && !self.run.cpu.ebreak_hit - && !self.run.cpu.local_exit; + self.run.faulted = self.run.cpu().exit_code.is_none() + && !self.run.cpu().ebreak_hit + && !self.run.cpu().local_exit; } else { self.run.is_running = false; } diff --git a/src/ui/app/run_state.rs b/src/ui/app/run_state.rs index 72927d7..483abc7 100644 --- a/src/ui/app/run_state.rs +++ b/src/ui/app/run_state.rs @@ -1,9 +1,37 @@ use super::CpiConfig; +use super::instr_edit::InstrFieldKind; use crate::falcon::jit::ExecutionBackend; +use crate::falcon::machine::Machine; +use crate::falcon::machine::types::{FRegId, MemWidth, RegTarget}; use crate::falcon::{CacheController, Cpu, registers::ExecRegion}; use crate::ui::editor::Editor; use std::time::{Duration, Instant}; +/// What the Run tab is editing inline, when [`RunState::run_edit`] is `Some`. +/// +/// Every variant commits through a journaling `Machine` mutator +/// ([`Machine::write_reg`] / [`Machine::write_freg`] / [`Machine::write_mem`]), +/// so a manual edit is undoable by step-back exactly like an executed +/// instruction. +#[derive(Clone, Copy)] +pub(crate) enum RunEditTarget { + /// An integer register `x1..=x31` or the PC. `x0` is rejected on commit. + Reg(RegTarget), + /// A float register `f0..=f31`, typed as a decimal value. + FReg(FRegId), + /// A `width`-byte memory cell at the (virtual) address `addr`. + Mem { addr: u32, width: MemWidth }, + /// The 32-bit instruction word at the (virtual) address `addr`, opened + /// from the details panel's word field. Committing also invalidates the + /// JIT range so stale translations never run. + Instr { addr: u32 }, + /// One field of the instruction word at `addr` (a register slot, the + /// immediate, funct bits, the binary view, or the whole mnemonic line as + /// assembly), opened by double-clicking it in the details panel. Commits + /// rewrite the full word through the same path as [`Self::Instr`]. + InstrField { addr: u32, field: InstrFieldKind }, +} + #[derive(PartialEq, Eq, Copy, Clone)] pub(crate) enum EditorMode { Insert, @@ -23,6 +51,7 @@ pub(crate) enum MemRegion { pub(crate) enum FormatMode { Hex, Dec, + Bin, Str, } @@ -75,6 +104,7 @@ pub(crate) enum RunButton { Speed, ExecCount, InstrType, + Stepback, Reset, } @@ -139,13 +169,63 @@ impl RunState { pub(crate) fn vm_enabled(&self) -> bool { self.vm_mode != crate::falcon::mmu::VmMode::Off } + + /// First address shown at the top of the memory view, given the panel's + /// inner height in rows. Auto-following regions (Stack/Access/Heap) center + /// `mem_view_addr`; others anchor it at the top. Shared by the renderer and + /// the click-to-edit hit test so both agree on each row's address. + pub(crate) fn visible_memory_base_addr(&self, lines_override: Option) -> u32 { + let bytes = self.mem_view_bytes.max(1); + let lines = lines_override.unwrap_or(0); + let center = self.mem_region == MemRegion::Stack + || self.mem_region == MemRegion::Access + || self.mem_region == MemRegion::Heap + || (self.show_dyn && matches!(self.dyn_mem_access, Some((_, _, true)))); + let base = if center { + let half = lines / 2; + self.mem_view_addr.saturating_sub(half * bytes) + } else { + self.mem_view_addr + }; + let align_mask = !(bytes - 1); + base & align_mask + } + + /// Shared read access to the CPU. The `~117` `run.cpu` read sites borrow + /// through here; mutation must go through a `Machine` method. + pub(crate) fn cpu(&self) -> &Cpu { + self.machine.cpu() + } + + /// Shared read access to the memory hierarchy. See [`RunState::cpu`]. + pub(crate) fn mem(&self) -> &CacheController { + self.machine.mem() + } + + /// Shared read access to the pipeline simulator. The pipeline lives inside + /// `Machine` so a clock cycle is journaled together with the CPU and memory + /// (see [`crate::falcon::machine::Machine::step_pipeline`]); reads borrow + /// through here. + pub(crate) fn pipeline(&self) -> &crate::ui::pipeline::PipelineSimState { + self.machine.pipeline() + } + + /// Mutable pipeline access for UI/config changes (hover, scroll, subtab, + /// forwarding/branch config, reset). Does **not** journal and does **not** + /// clear history. Never use it to advance execution — that is + /// [`crate::falcon::machine::Machine::step_pipeline`]. + pub(crate) fn pipeline_mut(&mut self) -> &mut crate::ui::pipeline::PipelineSimState { + self.machine.pipeline_mut() + } } pub(crate) struct RunState { - pub(crate) cpu: Cpu, + /// The simulator's CPU + memory hierarchy, owned behind the journaling + /// gateway. Reads go through [`RunState::cpu`] / [`RunState::mem`]; mutation + /// is only expressible via `Machine`'s methods (see its module docs). + pub(crate) machine: Machine, pub(crate) prev_x: [u32; 32], pub(crate) prev_pc: u32, - pub(crate) mem: CacheController, pub(crate) breakpoints: std::collections::HashSet, pub(crate) mem_size: usize, pub(crate) base_pc: u32, @@ -182,6 +262,11 @@ pub(crate) struct RunState { // imem_scroll is now in VISUAL ROWS (not instruction count) pub(crate) imem_scroll: usize, pub(crate) hover_imem_addr: Option, + /// Last left-click on an imem row (`addr`, instant), for double-click + /// detection: a second click on the same row within the threshold + /// redirects the PC there (a single click only selects the row for the + /// details panel). + pub(crate) last_imem_click: Option<(u32, Instant)>, // Set each frame by render so scroll handlers use the correct height pub(crate) imem_inner_height: std::cell::Cell, pub(crate) imem_collapsed: bool, @@ -200,6 +285,18 @@ pub(crate) struct RunState { // Details panel (collapsible) pub(crate) details_collapsed: bool, + /// Click-selected instruction the details panel is pinned to; `None` + /// follows the PC. + pub(crate) details_addr: Option, + /// Last left-click on a details-panel field, for double-click detection: + /// a second click on the same field within the threshold opens its editor. + pub(crate) last_details_click: Option<(InstrFieldKind, Instant)>, + /// Editable-field hitboxes `(field, y, x0, x1)` recorded by the last + /// details render, consumed by the mouse handler. + pub(crate) details_field_hitboxes: std::cell::RefCell>, + /// The address the details panel actually rendered last frame, so a click + /// edits exactly what the user saw. + pub(crate) details_rendered_addr: std::cell::Cell, // Console panel (resizable) pub(crate) console_height: u16, @@ -216,6 +313,19 @@ pub(crate) struct RunState { pub(crate) step_interval: Duration, pub(crate) faulted: bool, pub(crate) speed: RunSpeed, + /// One-shot guard: a full step-back checkpoint has been taken for the + /// current GO/Instant burst. Reset when the run stops. See `App::tick`. + pub(crate) go_checkpointed: bool, + + // ── Inline editing of live state (registers / PC / floats / RAM) ── + /// The cell currently open for inline editing, or `None`. While `Some`, + /// keystrokes feed `run_edit_buf` instead of the normal Run shortcuts. + pub(crate) run_edit: Option, + /// The text typed so far for the open edit (committed on Enter). + pub(crate) run_edit_buf: String, + /// The rejection message from the last failed commit; cleared on the next + /// keystroke. While set, the editor stays open so the user can fix the value. + pub(crate) run_edit_error: Option, // Visible comments from source (#! text), keyed by instruction address pub(crate) comments: std::collections::HashMap, diff --git a/src/ui/app/runtime.rs b/src/ui/app/runtime.rs index d3ca464..8802080 100644 --- a/src/ui/app/runtime.rs +++ b/src/ui/app/runtime.rs @@ -17,7 +17,7 @@ impl App { } pub(crate) fn aggregate_pipeline_snapshot(&self) -> Option { - if !self.pipeline.enabled { + if !self.run.pipeline().enabled { return None; } @@ -44,7 +44,7 @@ impl App { if self.core_hart_id(self.selected_core).is_some() || !matches!(self.core_status(self.selected_core), HartLifecycle::Free) { - accumulate(&self.pipeline); + accumulate(&self.run.pipeline()); } for (idx, hart) in self.harts.iter().enumerate() { @@ -86,19 +86,19 @@ impl App { branch_stalls, fu_stalls, mem_stalls, - bypass: self.pipeline.bypass.summary(), - mode: format!("{:?}", self.pipeline.mode), - branch_resolve: format!("{:?}", self.pipeline.branch_resolve), - branch_predict: format!("{:?}", self.pipeline.predict), + bypass: self.run.pipeline().bypass.summary(), + mode: format!("{:?}", self.run.pipeline().mode), + branch_resolve: format!("{:?}", self.run.pipeline().branch_resolve), + branch_predict: format!("{:?}", self.run.pipeline().predict), }) } pub(crate) fn selected_pipeline_snapshot(&self) -> Option { - if !self.pipeline.enabled { + if !self.run.pipeline().enabled { return None; } - let pipe = &self.pipeline; + let pipe = &self.run.pipeline(); let [ raw_stalls, load_use_stalls, @@ -162,14 +162,14 @@ impl App { pub(in crate::ui) fn set_cache_enabled(&mut self, enabled: bool) { self.run.cache_enabled = enabled; - self.run.mem.bypass = !enabled; - self.run.mem.flush_all(); + self.run.machine.mem_mut_unjournaled().bypass = !enabled; + self.run.machine.mem_mut_unjournaled().flush_all(); self.ensure_visible_tab(); } pub(in crate::ui) fn set_pipeline_enabled(&mut self, enabled: bool) { - self.pipeline.enabled = enabled; - self.pipeline.sequential_mode = !enabled; + self.run.pipeline_mut().enabled = enabled; + self.run.pipeline_mut().sequential_mode = !enabled; self.reconfigure_pipeline_model(); self.ensure_visible_tab(); } @@ -198,7 +198,7 @@ impl App { pub(in crate::ui) fn push_vm_mode_to_mmu(&mut self) { let (enabled, force_translate) = self.run.vm_mode.flags(); let scheme = self.active_scheme(); - let mmu = self.run.mem.mmu_mut(); + let mmu = self.run.machine.mem_mut_unjournaled().mmu_mut(); mmu.set_scheme(scheme); mmu.enabled = enabled; mmu.force_translate = force_translate; @@ -221,7 +221,7 @@ impl App { if !enabled { // Drop all cached translations so re-enabling starts from a clean // slate (no stale PA mappings). - self.run.mem.mmu.flush(); + self.run.machine.mem_mut_unjournaled().mmu.flush(); } else if self.run.jit_kind != crate::falcon::jit::BackendKind::None { // The JIT does not yet invalidate translations on satp/sfence.vma, // so keeping it on with VM would silently run stale code. Demote to @@ -235,10 +235,10 @@ impl App { /// page table (miss + penalty, no hits). Mirrors the flag into the engine. pub(in crate::ui) fn set_tlb_enabled(&mut self, enabled: bool) { self.run.tlb_enabled = enabled; - self.run.mem.mmu.tlb_enabled = enabled; + self.run.machine.mem_mut_unjournaled().mmu.tlb_enabled = enabled; if !enabled { // Drop cached translations so re-enabling starts cold. - self.run.mem.mmu.flush(); + self.run.machine.mem_mut_unjournaled().mmu.flush(); } } @@ -271,7 +271,7 @@ impl App { } BackendKind::Full => { #[cfg(feature = "jit")] - { crate::falcon::jit::make_full_backend(&self.run.cpu, &self.run.mem) } + { crate::falcon::jit::make_full_backend(self.run.cpu(), self.run.mem()) } #[cfg(not(feature = "jit"))] { make_backend(BackendKind::None).unwrap() } } @@ -280,14 +280,14 @@ impl App { pub(crate) fn reconfigure_pipeline_model(&mut self) { self.run.is_running = false; - self.pipeline.reset_stages(self.run.cpu.pc); + let __rpc = self.run.cpu().pc; self.run.pipeline_mut().reset_stages(__rpc); for (idx, hart) in self.harts.iter_mut().enumerate() { if idx == self.selected_core { continue; } if let Some(p) = hart.pipeline.as_mut() { - Self::copy_pipeline_config_to_hart(&self.pipeline, p); + Self::copy_pipeline_config_to_hart(&self.run.pipeline(), p); p.reset_stages(hart.cpu.pc); } } @@ -324,11 +324,11 @@ impl App { self.harts.clear(); for core in 0..self.max_cores { let mut runtime = HartCoreRuntime::free(self.run.base_pc, self.run.mem_size); - runtime.cpu.heap_break = self.run.cpu.heap_break; + runtime.cpu.heap_break = self.run.cpu().heap_break; if core == 0 { runtime.hart_id = Some(0); runtime.lifecycle = HartLifecycle::Running; - runtime.cpu = self.run.cpu.clone(); + runtime.cpu = self.run.cpu().clone(); runtime.cpu.hart_id = 0; runtime.prev_x = self.run.prev_x; runtime.prev_f = self.run.prev_f; @@ -344,7 +344,7 @@ impl App { runtime.mem_access_log = self.run.mem_access_log.clone(); runtime.pipeline = None; } else if let Some(p) = runtime.pipeline.as_mut() { - Self::copy_pipeline_config_to_hart(&self.pipeline, p); + Self::copy_pipeline_config_to_hart(&self.run.pipeline(), p); p.reset_stages(runtime.cpu.pc); } self.harts.push(runtime); @@ -359,7 +359,7 @@ impl App { let selected = self.selected_core; let replacement = crate::ui::pipeline::PipelineSimState::new(); if let Some(runtime) = self.harts.get_mut(selected) { - runtime.cpu = self.run.cpu.clone(); + runtime.cpu = self.run.cpu().clone(); runtime.prev_x = self.run.prev_x; runtime.prev_f = self.run.prev_f; runtime.prev_pc = self.run.prev_pc; @@ -372,7 +372,7 @@ impl App { runtime.exec_trace = self.run.exec_trace.clone(); runtime.dyn_mem_access = self.run.dyn_mem_access; runtime.mem_access_log = self.run.mem_access_log.clone(); - runtime.pipeline = Some(std::mem::replace(&mut self.pipeline, replacement)); + runtime.pipeline = Some(std::mem::replace(&mut self.run.pipeline_mut(), replacement)); } } @@ -383,7 +383,7 @@ impl App { pub(super) fn sync_runtime_to_selected_core(&mut self) { let selected = self.selected_core; if let Some(runtime) = self.harts.get_mut(selected) { - self.run.cpu = runtime.cpu.clone(); + *self.run.machine.cpu_mut_unjournaled() = runtime.cpu.clone(); self.run.prev_x = runtime.prev_x; self.run.prev_f = runtime.prev_f; self.run.prev_pc = runtime.prev_pc; @@ -400,11 +400,11 @@ impl App { .pipeline .take() .unwrap_or_else(crate::ui::pipeline::PipelineSimState::new); - Self::copy_pipeline_config_to_hart(&self.pipeline, &mut pipeline); + Self::copy_pipeline_config_to_hart(&self.run.pipeline(), &mut pipeline); if pipeline.fetch_pc == 0 && pipeline.cycle_count == 0 { - pipeline.reset_stages(self.run.cpu.pc); + pipeline.reset_stages(self.run.cpu().pc); } - self.pipeline = pipeline; + *self.run.pipeline_mut() = pipeline; } } @@ -456,7 +456,7 @@ impl App { } pub(super) fn process_pending_hart_start_for_selected(&mut self) { - let Some(request) = self.run.cpu.pending_hart_start.take() else { + let Some(request) = self.run.machine.cpu_mut_unjournaled().pending_hart_start.take() else { return; }; @@ -468,7 +468,7 @@ impl App { ) }); let Some(free_core) = free_core else { - self.run.cpu.write(10, (-1i32) as u32); + self.run.machine.cpu_mut_unjournaled().write(10, (-1i32) as u32); self.console.push_colored( format!( "[C{}:H{}] hart start failed: no free core available (max_cores={})", @@ -481,7 +481,7 @@ impl App { return; }; if !self.is_pc_in_program(request.entry_pc) { - self.run.cpu.write(10, (-2i32) as u32); + self.run.machine.cpu_mut_unjournaled().write(10, (-2i32) as u32); self.console.push_colored( format!( "[C{}:H{}] hart start failed: entry PC 0x{:08X} is outside any executable region", @@ -498,7 +498,7 @@ impl App { || request.stack_ptr > self.run.mem_size as u32 || request.stack_ptr & 0xF != 0 { - self.run.cpu.write(10, (-3i32) as u32); + self.run.machine.cpu_mut_unjournaled().write(10, (-3i32) as u32); self.console.push_colored( format!( "[C{}:H{}] hart start failed: stack 0x{:08X} invalid (must be non-zero, 16-byte aligned, within memory [0..0x{:08X}])", @@ -522,15 +522,15 @@ impl App { child.cpu.pc = request.entry_pc; child.cpu.write(2, request.stack_ptr); child.cpu.write(10, request.arg); - child.cpu.heap_break = self.run.cpu.heap_break; + child.cpu.heap_break = self.run.cpu().heap_break; child.prev_pc = child.cpu.pc; if let Some(p) = child.pipeline.as_mut() { - Self::copy_pipeline_config_to_hart(&self.pipeline, p); + Self::copy_pipeline_config_to_hart(&self.run.pipeline(), p); p.reset_stages(child.cpu.pc); } self.harts[free_core] = child; - self.run.cpu.write(10, hart_id); + self.run.machine.cpu_mut_unjournaled().write(10, hart_id); self.console.push_colored( format!( "[C{}:H{}] hart start -> core {} pc=0x{:08X}", @@ -545,7 +545,7 @@ impl App { /// Handle a hart-spawn request issued by a non-selected (background) hart. /// Equivalent to `process_pending_hart_start_for_selected` but reads from - /// and writes to `self.harts[core_idx].cpu` instead of `self.run.cpu`. + /// and writes to `self.harts[core_idx].cpu` instead of `self.run.cpu()`. pub(super) fn process_pending_hart_start_for_bg(&mut self, core_idx: usize) { let Some(request) = self.harts[core_idx].cpu.pending_hart_start.take() else { return; @@ -616,7 +616,7 @@ impl App { child.cpu.heap_break = self.harts[core_idx].cpu.heap_break; child.prev_pc = child.cpu.pc; if let Some(p) = child.pipeline.as_mut() { - Self::copy_pipeline_config_to_hart(&self.pipeline, p); + Self::copy_pipeline_config_to_hart(&self.run.pipeline(), p); p.reset_stages(child.cpu.pc); } @@ -635,7 +635,7 @@ impl App { } pub(super) fn propagate_heap_break(&mut self, heap_break: u32) { - self.run.cpu.heap_break = heap_break; + self.run.machine.cpu_mut_unjournaled().heap_break = heap_break; for (idx, hart) in self.harts.iter_mut().enumerate() { if idx != self.selected_core { hart.cpu.heap_break = heap_break; diff --git a/src/ui/debug_hitboxes.rs b/src/ui/debug_hitboxes.rs index 7e6920d..2c7e985 100644 --- a/src/ui/debug_hitboxes.rs +++ b/src/ui/debug_hitboxes.rs @@ -128,6 +128,7 @@ pub fn debug_run_controls_dump(opts: DebugRunControlsOptions) -> String { RunButton::Speed => "Speed", RunButton::ExecCount => "ExecCount", RunButton::InstrType => "InstrType", + RunButton::Stepback => "Stepback", RunButton::Reset => "Reset", }; out.push_str(&format!(" {:<10} cols [{}..{})\n", name, start, end)); diff --git a/src/ui/input/keyboard/config_keys.rs b/src/ui/input/keyboard/config_keys.rs index b085a87..2f0c666 100644 --- a/src/ui/input/keyboard/config_keys.rs +++ b/src/ui/input/keyboard/config_keys.rs @@ -48,7 +48,7 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { } else if app.settings.selected == SETTINGS_ROW_RUN_SCOPE { app.run_scope = app.run_scope.cycle(); } else if app.settings.selected == SETTINGS_ROW_PIPELINE_ENABLED { - app.set_pipeline_enabled(!app.pipeline.enabled); + app.set_pipeline_enabled(!app.run.pipeline().enabled); } else if app.settings.selected == SETTINGS_ROW_VM_ENABLED { app.set_vm_mode(app.vm_mode().cycle()); } else if app.settings.selected == SETTINGS_ROW_TLB_ENABLED { diff --git a/src/ui/input/keyboard/intercepts.rs b/src/ui/input/keyboard/intercepts.rs index df3513d..0387263 100644 --- a/src/ui/input/keyboard/intercepts.rs +++ b/src/ui/input/keyboard/intercepts.rs @@ -1,7 +1,7 @@ use crate::ui::app::{App, EditorMode, Tab}; use crossterm::event::{KeyCode, KeyEvent, KeyModifiers}; -use super::paste::{paste_imem_search, paste_mem_search}; +use super::paste::{paste_imem_search, paste_mem_search, paste_run_edit}; use super::serialization::{ apply_imem_search, apply_mem_search, dispatch_path_input, refresh_path_completions, }; @@ -188,6 +188,46 @@ pub(super) fn handle_pre_find_intercepts(app: &mut App, key: KeyEvent) -> Option } pub(super) fn handle_post_find_intercepts(app: &mut App, key: KeyEvent) -> Option { + // An open Run inline editor owns every keystroke: Esc cancels, Enter + // commits, Ctrl+C copies the buffer, Ctrl+V pastes, and characters valid for + // the active format extend it. Handled here (rather than in `run_keys`) so + // key modifiers are visible and the editor pre-empts every Run shortcut. + if matches!(app.tab, Tab::Run) && app.run.run_edit.is_some() { + match key.code { + KeyCode::Esc => app.cancel_run_edit(), + KeyCode::Enter => app.commit_run_edit(), + KeyCode::Backspace => { + app.run.run_edit_buf.pop(); + app.run.run_edit_error = None; + } + KeyCode::Char('c') if key.modifiers.contains(KeyModifiers::CONTROL) => { + if let Some(clip) = app.clipboard.as_mut() { + let _ = clip.set_text(app.run.run_edit_buf.clone()); + } + } + KeyCode::Char('v') if key.modifiers.contains(KeyModifiers::CONTROL) => { + let recent_bracketed = app + .last_bracketed_paste + .is_some_and(|t| t.elapsed().as_millis() < 100); + if !recent_bracketed { + let text = app.clipboard.as_mut().and_then(|clip| clip.get_text().ok()); + if let Some(text) = text { + paste_run_edit(app, &text); + } + } + } + KeyCode::Char(c) + if !key.modifiers.contains(KeyModifiers::CONTROL) + && super::run_keys::edit_char_allowed(app, c) => + { + app.run.run_edit_buf.push(c); + app.run.run_edit_error = None; + } + _ => {} + } + return Some(false); + } + if matches!(app.tab, Tab::Run) && app.run.imem_search_open { match key.code { KeyCode::Esc => { @@ -322,7 +362,7 @@ pub(super) fn handle_global_shortcuts(app: &mut App, key: KeyEvent, ctrl: bool) } if key.code == KeyCode::F(9) && matches!(app.tab, Tab::Run) { - let addr = app.run.hover_imem_addr.unwrap_or(app.run.cpu.pc); + let addr = app.run.hover_imem_addr.unwrap_or(app.run.cpu().pc); if app.run.breakpoints.contains(&addr) { app.run.breakpoints.remove(&addr); } else { diff --git a/src/ui/input/keyboard/paste.rs b/src/ui/input/keyboard/paste.rs index 69815c2..9c3d8d9 100644 --- a/src/ui/input/keyboard/paste.rs +++ b/src/ui/input/keyboard/paste.rs @@ -6,6 +6,10 @@ use super::editor_shared::mark_editor_edited; use super::serialization::{apply_imem_search, apply_mem_search}; pub fn paste_from_terminal(app: &mut App, text: &str) { + if matches!(app.tab, Tab::Run) && app.run.run_edit.is_some() { + paste_run_edit(app, text); + return; + } if matches!(app.tab, Tab::Run) && app.run.imem_search_open { paste_imem_search(app, text); return; @@ -25,6 +29,21 @@ pub(super) fn paste_editor(app: &mut App, text: &str) { mark_editor_edited(app); } +/// Append pasted text to the open Run value editor, keeping only characters +/// legal for the active format (so a trailing newline or stray spaces are +/// dropped). Out-of-range values are still caught on commit. +pub(crate) fn paste_run_edit(app: &mut App, text: &str) { + let sanitized: String = text + .chars() + .filter(|&c| super::run_keys::edit_char_allowed(app, c)) + .collect(); + if sanitized.is_empty() { + return; + } + app.run.run_edit_buf.push_str(&sanitized); + app.run.run_edit_error = None; +} + pub(crate) fn paste_imem_search(app: &mut App, text: &str) { let sanitized: String = text.chars().filter(|&c| c != '\r' && c != '\n').collect(); if sanitized.is_empty() { diff --git a/src/ui/input/keyboard/pipeline_keys.rs b/src/ui/input/keyboard/pipeline_keys.rs index 5f77dc7..e7c1362 100644 --- a/src/ui/input/keyboard/pipeline_keys.rs +++ b/src/ui/input/keyboard/pipeline_keys.rs @@ -7,33 +7,33 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { match key.code { KeyCode::Tab => { use crate::ui::pipeline::PipelineSubtab; - app.pipeline.clear_hover_state(); - app.pipeline.subtab = match app.pipeline.subtab { + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().subtab = match app.run.pipeline_mut().subtab { PipelineSubtab::Main => PipelineSubtab::Config, PipelineSubtab::Config => PipelineSubtab::Main, }; true } KeyCode::Char('e') => { - app.pipeline.clear_hover_state(); - app.set_pipeline_enabled(!app.pipeline.enabled); + app.run.pipeline_mut().clear_hover_state(); + app.set_pipeline_enabled(!app.run.pipeline().enabled); true } KeyCode::Char('r') | KeyCode::Char('R') => { - app.pipeline.clear_hover_state(); + app.run.pipeline_mut().clear_hover_state(); app.restart_simulation(); true } KeyCode::Char('f') => { - app.pipeline.clear_hover_state(); - app.pipeline.speed = app.pipeline.speed.next(); - app.pipeline.last_tick = Instant::now(); + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().speed = app.run.pipeline_mut().speed.next(); + app.run.pipeline_mut().last_tick = Instant::now(); true } KeyCode::Char('b') => { use crate::ui::pipeline::BranchResolve; - app.pipeline.clear_hover_state(); - app.pipeline.branch_resolve = match app.pipeline.branch_resolve { + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().branch_resolve = match app.run.pipeline_mut().branch_resolve { BranchResolve::Id => BranchResolve::Ex, BranchResolve::Ex => BranchResolve::Mem, BranchResolve::Mem => BranchResolve::Id, @@ -42,21 +42,21 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { true } KeyCode::Char('s') => { - app.pipeline.clear_hover_state(); - if (app.pipeline.enabled || app.pipeline.sequential_mode) && !app.pipeline.faulted { + app.run.pipeline_mut().clear_hover_state(); + if (app.run.pipeline().enabled || app.run.pipeline().sequential_mode) && !app.run.pipeline().faulted { app.single_step(); } true } KeyCode::Char('p') | KeyCode::Char(' ') if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Main ) => { - app.pipeline.clear_hover_state(); - if (app.pipeline.enabled || app.pipeline.sequential_mode) && !app.pipeline.faulted { - if app.pipeline.halted { + app.run.pipeline_mut().clear_hover_state(); + if (app.run.pipeline().enabled || app.run.pipeline().sequential_mode) && !app.run.pipeline().faulted { + if app.run.pipeline().halted { app.restart_simulation(); if app.can_start_run() { app.run.is_running = true; @@ -74,85 +74,85 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { } KeyCode::Enter if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Config ) => { use crate::ui::pipeline::{BranchPredict, BranchResolve, PipelineMode}; - app.pipeline.clear_hover_state(); - match app.pipeline.config_cursor { - 0 => app.pipeline.bypass.ex_to_ex = !app.pipeline.bypass.ex_to_ex, - 1 => app.pipeline.bypass.mem_to_ex = !app.pipeline.bypass.mem_to_ex, - 2 => app.pipeline.bypass.wb_to_id = !app.pipeline.bypass.wb_to_id, - 3 => app.pipeline.bypass.store_to_load = !app.pipeline.bypass.store_to_load, + app.run.pipeline_mut().clear_hover_state(); + match app.run.pipeline().config_cursor { + 0 => app.run.pipeline_mut().bypass.ex_to_ex = !app.run.pipeline_mut().bypass.ex_to_ex, + 1 => app.run.pipeline_mut().bypass.mem_to_ex = !app.run.pipeline_mut().bypass.mem_to_ex, + 2 => app.run.pipeline_mut().bypass.wb_to_id = !app.run.pipeline_mut().bypass.wb_to_id, + 3 => app.run.pipeline_mut().bypass.store_to_load = !app.run.pipeline_mut().bypass.store_to_load, 4 => { - app.pipeline.mode = match app.pipeline.mode { + app.run.pipeline_mut().mode = match app.run.pipeline_mut().mode { PipelineMode::SingleCycle => PipelineMode::FunctionalUnits, PipelineMode::FunctionalUnits => PipelineMode::SingleCycle, }; } 5 => { - app.pipeline.branch_resolve = match app.pipeline.branch_resolve { + app.run.pipeline_mut().branch_resolve = match app.run.pipeline_mut().branch_resolve { BranchResolve::Id => BranchResolve::Ex, BranchResolve::Ex => BranchResolve::Mem, BranchResolve::Mem => BranchResolve::Id, }; } 6 => { - let next = match app.pipeline.predict { + let next = match app.run.pipeline().predict { BranchPredict::NotTaken => BranchPredict::Taken, BranchPredict::Taken => BranchPredict::Btfnt, BranchPredict::Btfnt => BranchPredict::TwoBit, BranchPredict::TwoBit => BranchPredict::NotTaken, }; - app.pipeline.set_predict(next); + app.run.pipeline_mut().set_predict(next); } 7 => { let idx = crate::ui::pipeline::FuKind::Alu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 8 => { let idx = crate::ui::pipeline::FuKind::Mul.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 9 => { let idx = crate::ui::pipeline::FuKind::Div.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 10 => { let idx = crate::ui::pipeline::FuKind::Fpu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 11 => { let idx = crate::ui::pipeline::FuKind::Lsu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 12 => { let idx = crate::ui::pipeline::FuKind::Sys.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } _ => {} @@ -162,67 +162,90 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { } KeyCode::Up if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Config ) => { - app.pipeline.clear_hover_state(); - app.pipeline.config_cursor = app.pipeline.config_cursor.saturating_sub(1); + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().config_cursor = app.run.pipeline_mut().config_cursor.saturating_sub(1); true } KeyCode::Down if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Config ) => { - app.pipeline.clear_hover_state(); - app.pipeline.config_cursor = - (app.pipeline.config_cursor + 1).min(PipelineBypassConfig::CONFIG_ROWS - 1); + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().config_cursor = + (app.run.pipeline_mut().config_cursor + 1).min(PipelineBypassConfig::CONFIG_ROWS - 1); true } + // Gantt scroll is bottom-anchored: 0 = follow the newest row, so Up + // moves *into* scrollback (larger offset) and Down back toward follow. KeyCode::Up if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Main ) => { - app.pipeline.clear_hover_state(); - app.pipeline.gantt_scroll = app.pipeline.gantt_scroll.saturating_sub(1); + app.run.pipeline_mut().clear_hover_state(); + let max = app.run.pipeline().gantt_max_scroll_cache.get(); + app.run.pipeline_mut().gantt_scroll = (app.run.pipeline_mut().gantt_scroll + 1).min(max); true } KeyCode::Down if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Main ) => { - app.pipeline.clear_hover_state(); - let max = app.pipeline.gantt_max_scroll_cache.get(); - app.pipeline.gantt_scroll = (app.pipeline.gantt_scroll + 1).min(max); + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().gantt_scroll = app.run.pipeline_mut().gantt_scroll.saturating_sub(1); true } KeyCode::PageUp if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Main ) => { - app.pipeline.clear_hover_state(); - let page = app.pipeline.gantt_visible_rows_cache.get().max(1); - app.pipeline.gantt_scroll = app.pipeline.gantt_scroll.saturating_sub(page); + app.run.pipeline_mut().clear_hover_state(); + let page = app.run.pipeline().gantt_visible_rows_cache.get().max(1); + let max = app.run.pipeline().gantt_max_scroll_cache.get(); + app.run.pipeline_mut().gantt_scroll = app.run.pipeline_mut().gantt_scroll.saturating_add(page).min(max); true } KeyCode::PageDown if matches!( - app.pipeline.subtab, + app.run.pipeline().subtab, crate::ui::pipeline::PipelineSubtab::Main ) => { - app.pipeline.clear_hover_state(); - let page = app.pipeline.gantt_visible_rows_cache.get().max(1); - let max = app.pipeline.gantt_max_scroll_cache.get(); - app.pipeline.gantt_scroll = app.pipeline.gantt_scroll.saturating_add(page).min(max); + app.run.pipeline_mut().clear_hover_state(); + let page = app.run.pipeline().gantt_visible_rows_cache.get().max(1); + app.run.pipeline_mut().gantt_scroll = app.run.pipeline_mut().gantt_scroll.saturating_sub(page); + true + } + KeyCode::End | KeyCode::Char('G') + if matches!( + app.run.pipeline().subtab, + crate::ui::pipeline::PipelineSubtab::Main + ) => + { + app.run.pipeline_mut().clear_hover_state(); + app.run.pipeline_mut().gantt_scroll = 0; + true + } + KeyCode::Home | KeyCode::Char('g') + if matches!( + app.run.pipeline().subtab, + crate::ui::pipeline::PipelineSubtab::Main + ) => + { + app.run.pipeline_mut().clear_hover_state(); + let max = app.run.pipeline().gantt_max_scroll_cache.get(); + app.run.pipeline_mut().gantt_scroll = max; true } _ => false, diff --git a/src/ui/input/keyboard/run_keys.rs b/src/ui/input/keyboard/run_keys.rs index 5b14eb2..a47e68f 100644 --- a/src/ui/input/keyboard/run_keys.rs +++ b/src/ui/input/keyboard/run_keys.rs @@ -13,6 +13,9 @@ pub(super) fn handle_execution_key(app: &mut App, code: KeyCode) -> bool { return false; } + // An open inline editor claims keystrokes earlier, in + // `intercepts::handle_post_find_intercepts` (which sees key modifiers, so + // Ctrl+C / Ctrl+V work); nothing to do here while one is open. match code { KeyCode::Char('s') => { if !app.run.faulted { @@ -20,6 +23,10 @@ pub(super) fn handle_execution_key(app: &mut App, code: KeyCode) -> bool { } true } + KeyCode::Char('b') => { + app.stepback_one(); + true + } KeyCode::Char('r') => { app.restart_simulation(); true @@ -41,6 +48,23 @@ pub(super) fn handle_execution_key(app: &mut App, code: KeyCode) -> bool { } } +/// Whether `c` is a legal character for the cell currently being edited. Floats +/// accept a free decimal/scientific form; integer and memory cells follow the +/// Run tab's display format (hex / decimal / binary / raw string). Shared by the +/// editor's key handler and clipboard paste (both in `intercepts`). +pub(super) fn edit_char_allowed(app: &App, c: char) -> bool { + use crate::ui::app::{FormatMode, RunEditTarget}; + if matches!(app.run.run_edit, Some(RunEditTarget::FReg(_))) { + return c.is_ascii_alphanumeric() || matches!(c, '.' | '-' | '+'); + } + match app.run.fmt_mode { + FormatMode::Hex => c.is_ascii_hexdigit() || matches!(c, 'x' | 'X' | '_'), + FormatMode::Dec => c.is_ascii_digit() || matches!(c, '-' | '_'), + FormatMode::Bin => matches!(c, '0' | '1' | 'b' | 'B' | '_'), + FormatMode::Str => !c.is_control(), + } +} + pub(super) fn handle(app: &mut App, key: KeyEvent, ctrl: bool) -> bool { match key.code { KeyCode::Char('v') => { @@ -193,7 +217,7 @@ pub(super) fn cycle_memory_region(app: &mut App) { match app.run.mem_region { MemRegion::Data | MemRegion::Custom => { app.run.mem_region = MemRegion::Stack; - let sp = app.run.cpu.x[2]; + let sp = app.run.cpu().x[2]; app.run.mem_view_addr = sp & !(app.run.mem_view_bytes - 1); } MemRegion::Stack => { @@ -201,7 +225,7 @@ pub(super) fn cycle_memory_region(app: &mut App) { } MemRegion::Access => { app.run.mem_region = MemRegion::Heap; - let hb = app.run.cpu.heap_break; + let hb = app.run.cpu().heap_break; app.run.mem_view_addr = hb & !(app.run.mem_view_bytes - 1); } MemRegion::Heap => { diff --git a/src/ui/input/keyboard/serialization.rs b/src/ui/input/keyboard/serialization.rs index 7e8bc33..2c0dd3b 100644 --- a/src/ui/input/keyboard/serialization.rs +++ b/src/ui/input/keyboard/serialization.rs @@ -478,7 +478,7 @@ pub(super) fn make_level_snapshot( } pub(super) fn capture_snapshot(app: &App) -> CacheResultsSnapshot { - let mem = &app.run.mem; + let mem = app.run.mem(); let pipeline = capture_pipeline_snapshot(app); let i_amat = mem.icache_amat(); let d_amat = mem.dcache_amat(); @@ -609,7 +609,7 @@ pub(crate) fn apply_rcfg_text(app: &mut App, text: &str) -> Result<(), String> { /// Apply a raw .pcfg text to the app (parse + apply, no file I/O). pub(crate) fn apply_pcfg_text(app: &mut App, text: &str) -> Result<(), String> { let cfg = parse_pcfg(text)?; - cfg.apply_to_state(&mut app.pipeline); + cfg.apply_to_state(&mut app.run.pipeline_mut()); Ok(()) } @@ -620,10 +620,11 @@ pub(crate) fn apply_fcache_text(app: &mut App, text: &str) -> Result<(), String> app.cache.pending_dcache = dcfg; let n_extra = extra.len(); app.cache.extra_pending = extra; - app.run.mem.extra_levels.clear(); + app.run.machine.mem_mut_unjournaled().extra_levels.clear(); for cfg in &app.cache.extra_pending { app.run - .mem + .machine + .mem_mut_unjournaled() .extra_levels .push(crate::falcon::cache::Cache::new(cfg.clone())); } @@ -631,7 +632,7 @@ pub(crate) fn apply_fcache_text(app: &mut App, text: &str) -> Result<(), String> app.cache.selected_level = n_extra; } app.tlb.pending = tlb.clone(); - app.run.mem.mmu_mut().tlb.reconfigure(tlb); + app.run.machine.mem_mut_unjournaled().mmu_mut().tlb.reconfigure(tlb); Ok(()) } @@ -677,10 +678,11 @@ pub(crate) fn do_import_cfg(app: &mut App) { app.cache.pending_dcache = dcfg; let n_extra = extra.len(); app.cache.extra_pending = extra; - app.run.mem.extra_levels.clear(); + app.run.machine.mem_mut_unjournaled().extra_levels.clear(); for cfg in &app.cache.extra_pending { app.run - .mem + .machine + .mem_mut_unjournaled() .extra_levels .push(crate::falcon::cache::Cache::new(cfg.clone())); } @@ -688,7 +690,7 @@ pub(crate) fn do_import_cfg(app: &mut App) { app.cache.selected_level = n_extra; } app.tlb.pending = tlb.clone(); - app.run.mem.mmu_mut().tlb.reconfigure(tlb); + app.run.machine.mem_mut_unjournaled().mmu_mut().tlb.reconfigure(tlb); app.cache.config_error = None; app.cache.config_status = Some(format!( "Imported from {}", @@ -714,7 +716,7 @@ pub(crate) fn do_export_rcfg(app: &mut App) { let text = serialize_rcfg( &app.run.cpi_config, app.run.cache_enabled, - app.pipeline.enabled, + app.run.pipeline().enabled, app.vm_mode(), &app.active_scheme(), app.run.trace_syscalls, @@ -777,7 +779,7 @@ pub(crate) fn do_import_rcfg(app: &mut App) { } pub(crate) fn do_export_pcfg(app: &mut App) { - let text = serialize_pcfg(&app.pipeline); + let text = serialize_pcfg(&app.run.pipeline()); if let Some(path) = OSFileDialog::new() .add_filter("Raven Pipeline Config", &["pcfg"]) .set_file_name("pipeline.pcfg") @@ -786,15 +788,15 @@ pub(crate) fn do_export_pcfg(app: &mut App) { let path = ensure_extension(path, "pcfg"); match std::fs::write(&path, &text) { Ok(()) => { - app.pipeline.status_error = None; - app.pipeline.status_msg = Some(format!( + app.run.pipeline_mut().status_error = None; + app.run.pipeline_mut().status_msg = Some(format!( "Pipeline config exported to {}", path.file_name().unwrap_or_default().to_string_lossy() )); } Err(e) => { - app.pipeline.status_msg = None; - app.pipeline.status_error = Some(format!("Export failed: {e}")); + app.run.pipeline_mut().status_msg = None; + app.run.pipeline_mut().status_error = Some(format!("Export failed: {e}")); } } } else { @@ -810,21 +812,21 @@ pub(crate) fn do_import_pcfg(app: &mut App) { match std::fs::read_to_string(&path) { Ok(text) => match parse_pcfg(&text) { Ok(cfg) => { - cfg.apply_to_state(&mut app.pipeline); - app.pipeline.status_error = None; - app.pipeline.status_msg = Some(format!( + cfg.apply_to_state(&mut app.run.pipeline_mut()); + app.run.pipeline_mut().status_error = None; + app.run.pipeline_mut().status_msg = Some(format!( "Pipeline config imported from {}", path.file_name().unwrap_or_default().to_string_lossy() )); } Err(msg) => { - app.pipeline.status_msg = None; - app.pipeline.status_error = Some(format!("Import failed: {msg}")); + app.run.pipeline_mut().status_msg = None; + app.run.pipeline_mut().status_error = Some(format!("Import failed: {msg}")); } }, Err(e) => { - app.pipeline.status_msg = None; - app.pipeline.status_error = Some(format!("Import failed: {e}")); + app.run.pipeline_mut().status_msg = None; + app.run.pipeline_mut().status_error = Some(format!("Import failed: {e}")); } } } else { @@ -899,15 +901,15 @@ pub(crate) fn do_export_pipeline_results(app: &mut App) { }; match std::fs::write(&path, &text) { Ok(()) => { - app.pipeline.status_msg = Some(format!( + app.run.pipeline_mut().status_msg = Some(format!( "Pipeline results exported to {}", path.file_name().unwrap_or_default().to_string_lossy() )); - app.pipeline.status_error = None; + app.run.pipeline_mut().status_error = None; } Err(e) => { - app.pipeline.status_error = Some(format!("Export failed: {e}")); - app.pipeline.status_msg = None; + app.run.pipeline_mut().status_error = Some(format!("Export failed: {e}")); + app.run.pipeline_mut().status_msg = None; } } } else { @@ -1582,10 +1584,11 @@ pub(super) fn dispatch_path_input( app.cache.pending_icache = icfg; app.cache.pending_dcache = dcfg; app.cache.extra_pending = extra; - app.run.mem.extra_levels.clear(); + app.run.machine.mem_mut_unjournaled().extra_levels.clear(); for cfg in &app.cache.extra_pending { app.run - .mem + .machine + .mem_mut_unjournaled() .extra_levels .push(crate::falcon::cache::Cache::new(cfg.clone())); } @@ -1593,7 +1596,7 @@ pub(super) fn dispatch_path_input( app.cache.selected_level = n_extra; } app.tlb.pending = tlb.clone(); - app.run.mem.mmu_mut().tlb.reconfigure(tlb); + app.run.machine.mem_mut_unjournaled().mmu_mut().tlb.reconfigure(tlb); app.cache.config_error = None; app.cache.config_status = Some(format!( "Imported from {}", @@ -1655,7 +1658,7 @@ pub(super) fn dispatch_path_input( let text = serialize_rcfg( &app.run.cpi_config, app.run.cache_enabled, - app.pipeline.enabled, + app.run.pipeline().enabled, app.vm_mode(), &app.active_scheme(), app.run.trace_syscalls, @@ -1681,7 +1684,7 @@ pub(super) fn dispatch_path_input( PathInputAction::OpenPcfg => match std::fs::read_to_string(&path) { Ok(text) => match parse_pcfg(&text) { Ok(cfg) => { - cfg.apply_to_state(&mut app.pipeline); + cfg.apply_to_state(&mut app.run.pipeline_mut()); app.cache.config_error = None; app.cache.config_status = Some(format!( "Pipeline config imported from {}", @@ -1700,7 +1703,7 @@ pub(super) fn dispatch_path_input( }, PathInputAction::SavePcfg => { let path = ensure_extension(path, "pcfg"); - let text = serialize_pcfg(&app.pipeline); + let text = serialize_pcfg(&app.run.pipeline()); match std::fs::write(&path, &text) { Ok(()) => { app.cache.config_error = None; @@ -1766,15 +1769,15 @@ pub(super) fn dispatch_path_input( }; match std::fs::write(&path, &text) { Ok(()) => { - app.pipeline.status_msg = Some(format!( + app.run.pipeline_mut().status_msg = Some(format!( "Pipeline results exported to {}", path.file_name().unwrap_or_default().to_string_lossy() )); - app.pipeline.status_error = None; + app.run.pipeline_mut().status_error = None; } Err(e) => { - app.pipeline.status_error = Some(format!("Export failed: {e}")); - app.pipeline.status_msg = None; + app.run.pipeline_mut().status_error = Some(format!("Export failed: {e}")); + app.run.pipeline_mut().status_msg = None; } } } diff --git a/src/ui/input/keyboard/tlb_keys.rs b/src/ui/input/keyboard/tlb_keys.rs index 684f56d..e71fd4b 100644 --- a/src/ui/input/keyboard/tlb_keys.rs +++ b/src/ui/input/keyboard/tlb_keys.rs @@ -42,7 +42,7 @@ pub(super) fn handle(app: &mut App, key: KeyEvent) -> bool { true } KeyCode::Down if in_entries(app) => { - let total = app.run.mem.mmu().tlb.entries.len(); + let total = app.run.mem().mmu().tlb.entries.len(); let next = app.tlb.entries_scroll.saturating_add(1); app.tlb.entries_scroll = next.min(total.saturating_sub(1)); true @@ -122,7 +122,7 @@ pub(crate) fn select(app: &mut App, vm: VmSubtab, sub: Option) { // Entering either Settings panel snapshots the live TLB config so the // editor starts from the current geometry. if matches!(vm, VmSubtab::Settings) { - app.tlb.pending = app.run.mem.mmu().tlb.config.clone(); + app.tlb.pending = app.run.mem().mmu().tlb.config.clone(); app.tlb.vm_edit_field = None; app.tlb.vm_edit_buf.clear(); app.tlb.map_status = None; @@ -130,7 +130,7 @@ pub(crate) fn select(app: &mut App, vm: VmSubtab, sub: Option) { if let Some(t) = sub { app.tlb.subtab = t; if matches!(t, TlbSubtab::Settings) { - app.tlb.pending = app.run.mem.mmu().tlb.config.clone(); + app.tlb.pending = app.run.mem().mmu().tlb.config.clone(); } } } diff --git a/src/ui/input/mouse.rs b/src/ui/input/mouse.rs index 12fbe1b..8676f4c 100644 --- a/src/ui/input/mouse.rs +++ b/src/ui/input/mouse.rs @@ -1,3 +1,4 @@ +use crate::falcon::machine::types::{FRegId, MemWidth, RegId, RegTarget}; use crate::ui::input::keyboard::{ do_export_cfg, do_export_pipeline_results, do_export_rcfg, do_export_results, do_import_cfg, do_import_rcfg, @@ -13,7 +14,9 @@ use crate::ui::platform::OSFileDialog; use crate::ui::{ app::{ App, CacheHoverTarget, CacheScope, CacheSubtab, CacheViewFocus, ConfigField, DocsPage, - EditorMode, FormatMode, MemRegion, PathInputAction, RunButton, SETTINGS_ROW_CACHE_ENABLED, + EditorMode, FormatMode, InstrFieldKind, MemRegion, PathInputAction, RunButton, + RunEditTarget, + SETTINGS_ROW_CACHE_ENABLED, SETTINGS_ROW_CPI_START, SETTINGS_ROW_JIT_MODE, SETTINGS_ROW_MAX_CORES, SETTINGS_ROW_MEM_SIZE, SETTINGS_ROW_PIPELINE_ENABLED, SETTINGS_ROW_RUN_SCOPE, SETTINGS_ROW_TLB_ENABLED, SETTINGS_ROW_TRACE_SYSCALLS, SETTINGS_ROW_VM_ENABLED, Tab, @@ -133,8 +136,10 @@ pub fn handle_mouse(app: &mut App, me: MouseEvent, area: Rect) { _ => {} }, Tab::Pipeline => { - if point_in_rect(me.column, me.row, app.pipeline.gantt_area_rect.get()) { - app.pipeline.gantt_scroll = app.pipeline.gantt_scroll.saturating_sub(1); + // Bottom-anchored scroll: wheel-up moves into scrollback. + if point_in_rect(me.column, me.row, app.run.pipeline().gantt_area_rect.get()) { + let max = app.run.pipeline().gantt_max_scroll_cache.get(); + app.run.pipeline_mut().gantt_scroll = (app.run.pipeline_mut().gantt_scroll + 1).min(max); } } Tab::Docs => { @@ -191,9 +196,9 @@ pub fn handle_mouse(app: &mut App, me: MouseEvent, area: Rect) { _ => {} }, Tab::Pipeline => { - if point_in_rect(me.column, me.row, app.pipeline.gantt_area_rect.get()) { - let max = app.pipeline.gantt_max_scroll_cache.get(); - app.pipeline.gantt_scroll = (app.pipeline.gantt_scroll + 1).min(max); + // Bottom-anchored scroll: wheel-down moves back toward follow (0). + if point_in_rect(me.column, me.row, app.run.pipeline().gantt_area_rect.get()) { + app.run.pipeline_mut().gantt_scroll = app.run.pipeline_mut().gantt_scroll.saturating_sub(1); } } Tab::Docs => { @@ -214,7 +219,7 @@ pub fn handle_mouse(app: &mut App, me: MouseEvent, area: Rect) { app.tlb.vm_settings_scroll.saturating_add(1).min(max); } VmSubtab::Tlb if matches!(app.tlb.subtab, TlbSubtab::Entries) => { - let total = app.run.mem.mmu().tlb.entries.len(); + let total = app.run.mem().mmu().tlb.entries.len(); let next = app.tlb.entries_scroll.saturating_add(1); app.tlb.entries_scroll = next.min(total.saturating_sub(1)); } @@ -458,9 +463,11 @@ pub fn handle_mouse(app: &mut App, me: MouseEvent, area: Rect) { start_imem_drag(app, me, area); handle_imem_bp_click(app, me, area); handle_imem_click(app, me, area); + handle_details_click(app, me, area); handle_console_clear(app, me, area); start_console_drag(app, me, area); handle_register_click(app, me, area); + handle_memory_click(app, me, area); } MouseEventKind::Drag(MouseButton::Left) => { if app.run.sidebar_drag { @@ -478,6 +485,9 @@ pub fn handle_mouse(app: &mut App, me: MouseEvent, area: Rect) { app.run.imem_drag = false; app.run.console_drag = false; } + MouseEventKind::Down(MouseButton::Right) => { + handle_imem_right_click(app, me, area); + } _ => {} } } @@ -506,7 +516,8 @@ fn apply_run_button(app: &mut App, btn: RunButton) { RunButton::Format => { app.run.fmt_mode = match app.run.fmt_mode { FormatMode::Hex => FormatMode::Dec, - FormatMode::Dec => FormatMode::Str, + FormatMode::Dec => FormatMode::Bin, + FormatMode::Bin => FormatMode::Str, FormatMode::Str => FormatMode::Hex, }; } @@ -530,7 +541,7 @@ fn apply_run_button(app: &mut App, btn: RunButton) { RunButton::Region => { match app.run.mem_region { MemRegion::Data | MemRegion::Custom => { - let sp = app.run.cpu.x[2]; + let sp = app.run.cpu().x[2]; app.run.mem_view_addr = sp & !(app.run.mem_view_bytes - 1); app.run.mem_region = MemRegion::Stack; } @@ -538,7 +549,7 @@ fn apply_run_button(app: &mut App, btn: RunButton) { app.run.mem_region = MemRegion::Access; } MemRegion::Access => { - let hb = app.run.cpu.heap_break; + let hb = app.run.cpu().heap_break; app.run.mem_view_addr = hb & !(app.run.mem_view_bytes - 1); app.run.mem_region = MemRegion::Heap; } @@ -580,6 +591,9 @@ fn apply_run_button(app: &mut App, btn: RunButton) { } } } + RunButton::Stepback => { + app.stepback_one(); + } RunButton::Reset => { app.restart_simulation(); } @@ -649,123 +663,10 @@ pub(crate) fn run_status_area(app: &App, area: Rect) -> Rect { } pub(crate) fn run_status_hit(app: &App, status: Rect, col: u16) -> Option { - let core_text = format!("{}/{}", app.selected_core, app.max_cores.saturating_sub(1)); - let view_text = if app.run.show_dyn { - "dyn" - } else if app.run.show_registers { - "regs" - } else { - "ram" - }; - let fmt_text = match app.run.fmt_mode { - FormatMode::Hex => "hex", - FormatMode::Dec => "dec", - FormatMode::Str => "str", - }; - let sign_text = if app.run.show_signed { "sgn" } else { "uns" }; - let bytes_text = match app.run.mem_view_bytes { - 4 => "4b", - 2 => "2b", - _ => "1b", - }; - let region_text = match app.run.mem_region { - MemRegion::Data | MemRegion::Custom => "data", - MemRegion::Stack => "stack", - MemRegion::Access => "r/w", - MemRegion::Heap => "heap", - }; - let state_text = crate::ui::view::run::state_text(app); - - let mut pos = status.x + 1; - let range = |start: &mut u16, label: &str| { - let s = *start; - *start += label.len() as u16; - (s, *start) - }; - let pair_range = |start: &mut u16, label: &str, value: &str| { - let s = *start; - *start += (label.len() + 1 + value.len()) as u16; - (s, *start) - }; - let skip = |start: &mut u16, s: &str| { - *start += s.len() as u16; - }; - - let (core_start, core_end) = pair_range(&mut pos, "core", &core_text); - skip(&mut pos, " "); - let (view_start, view_end) = pair_range(&mut pos, "view", view_text); - - let (region_start, region_end) = if app.run_sidebar_shows_memory() { - skip(&mut pos, " "); - pair_range(&mut pos, "region", region_text) - } else { - (0, 0) - }; - - skip(&mut pos, " "); - let (fmt_start, fmt_end) = pair_range(&mut pos, "fmt", fmt_text); - - skip(&mut pos, " "); - let (sign_start, sign_end) = pair_range(&mut pos, "sign", sign_text); - - let (bytes_start, bytes_end) = if app.run_sidebar_shows_memory() { - skip(&mut pos, " "); - pair_range(&mut pos, "bytes", bytes_text) - } else { - (0, 0) - }; - - let speed_text = if app.run.speed.label() == "GO" { - "go" - } else { - app.run.speed.label() - }; - skip(&mut pos, " "); - let (speed_start, speed_end) = pair_range(&mut pos, "speed", speed_text); - - skip(&mut pos, " "); - let (state_start, state_end) = pair_range(&mut pos, "state", &state_text); - - let count_text = if app.run.show_exec_count { "on" } else { "off" }; - skip(&mut pos, " "); - let (count_start, count_end) = pair_range(&mut pos, "count", count_text); - - let type_text = if app.run.show_instr_type { "on" } else { "off" }; - skip(&mut pos, " "); - let (type_start, type_end) = pair_range(&mut pos, "type", type_text); - - skip(&mut pos, " "); - let (reset_start, reset_end) = range(&mut pos, "reset"); - - if app.max_cores > 1 && col >= core_start && col < core_end { - Some(RunButton::Core) - } else if col >= view_start && col < view_end { - Some(RunButton::View) - } else if app.run_sidebar_shows_memory() && col >= region_start && col < region_end { - Some(RunButton::Region) - } else if col >= fmt_start && col < fmt_end { - Some(RunButton::Format) - } else if col >= sign_start && col < sign_end { - if matches!(app.run.fmt_mode, FormatMode::Dec) { - Some(RunButton::Sign) - } else { - None - } - } else if app.run_sidebar_shows_memory() && col >= bytes_start && col < bytes_end { - Some(RunButton::Bytes) - } else if col >= speed_start && col < speed_end { - Some(RunButton::Speed) - } else if col >= state_start && col < state_end { - Some(RunButton::State) - } else if col >= count_start && col < count_end { - Some(RunButton::ExecCount) - } else if col >= type_start && col < type_end { - Some(RunButton::InstrType) - } else if col >= reset_start && col < reset_end { - Some(RunButton::Reset) - } else { - None - } + // The run-controls bar is laid out once in `build_run_toolbar`; ask that same + // model which control the column falls in. The paragraph's block border eats + // one column, so content begins at `status.x + 1`. + crate::ui::view::run::build_run_toolbar(app).hit(col, status.x + 1) } fn run_main_area(app: &App, area: Rect) -> Rect { @@ -1532,13 +1433,94 @@ fn handle_imem_click(app: &mut App, me: MouseEvent, area: Rect) { && me.row < inner.y + inner.height { if let Some(addr) = app.run.hover_imem_addr { - app.run.prev_pc = app.run.cpu.pc; - app.run.cpu.pc = addr; - if app.pipeline.enabled { - app.pipeline.redirect_pc(addr); + // Single click pins the details panel to this row; a second click + // on the same row within the threshold redirects the PC there. + // The first click of the pair only selected, so a double-click + // never edits state the user didn't ask to change. + const DOUBLE_CLICK: std::time::Duration = std::time::Duration::from_millis(400); + app.run.details_addr = Some(addr); + if let Some((last_addr, at)) = app.run.last_imem_click { + if last_addr == addr && at.elapsed() <= DOUBLE_CLICK { + app.run.last_imem_click = None; + app.run.prev_pc = app.run.cpu().pc; + app.run.machine.cpu_mut_unjournaled().pc = addr; + if app.run.pipeline().enabled { + app.run.pipeline_mut().redirect_pc(addr); + } + return; + } } + app.run.last_imem_click = Some((addr, std::time::Instant::now())); + } + } +} + +/// Right-click on an imem row selects it for the details panel, same as a +/// single left click (the marker column keeps the breakpoint toggle). +fn handle_imem_right_click(app: &mut App, me: MouseEvent, area: Rect) { + if app.run.imem_collapsed { + return; + } + let cols = run_cols(app, area); + let imem = cols[1]; + let inner = Rect::new( + imem.x + 1, + imem.y + 1, + imem.width.saturating_sub(2), + imem.height.saturating_sub(2), + ); + if me.column >= inner.x + && me.column < inner.x + inner.width + && me.row >= inner.y + && me.row < inner.y + inner.height + { + if let Some(addr) = app.run.hover_imem_addr { + app.run.details_addr = Some(addr); + } + } +} + +/// Double-click on an editable field of the Instruction Details panel opens +/// the inline editor on it. Hitboxes are recorded by the details renderer +/// each frame; the address comes from what that frame actually showed. +fn handle_details_click(app: &mut App, me: MouseEvent, area: Rect) { + if app.run.details_collapsed || app.run.is_running { + return; + } + let cols = run_cols(app, area); + let details = cols[2]; + if me.column < details.x + || me.column >= details.x + details.width + || me.row < details.y + || me.row >= details.y + details.height + { + return; + } + let hit = app + .run + .details_field_hitboxes + .borrow() + .iter() + .find(|&&(_, y, x0, x1)| me.row == y && me.column >= x0 && me.column < x1) + .map(|&(field, ..)| field); + let Some(field) = hit else { + app.run.last_details_click = None; + return; + }; + const DOUBLE_CLICK: std::time::Duration = std::time::Duration::from_millis(400); + if let Some((last_field, at)) = app.run.last_details_click { + if last_field == field && at.elapsed() <= DOUBLE_CLICK { + app.run.last_details_click = None; + let addr = app.run.details_rendered_addr.get(); + let target = match field { + InstrFieldKind::Word => RunEditTarget::Instr { addr }, + _ => RunEditTarget::InstrField { addr, field }, + }; + app.begin_run_edit(target); + return; } } + app.run.last_details_click = Some((field, std::time::Instant::now())); } fn handle_imem_bp_click(app: &mut App, me: MouseEvent, area: Rect) { @@ -1590,29 +1572,54 @@ fn update_sidebar_hover(app: &mut App, me: MouseEvent, area: Rect) { } } -fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { - if !app.run_sidebar_shows_registers() || app.run.show_float_regs { - return; +/// Label-column width of the integer / float register tables (see the +/// `Constraint::Length` in `sidebar.rs`). A click at or past this offset lands +/// on the value column and opens the inline editor; a click on the label keeps +/// its existing meaning (pin / unpin). +const INT_LABEL_W: u16 = 16; +const FLOAT_LABEL_W: u16 = 13; + +/// The edit target for a register list row, where `0 = PC` and `1..=32 = x0..x31`. +fn reg_target_for_row(reg_idx: usize) -> Option { + match reg_idx { + 0 => Some(RegTarget::Pc), + 1..=32 => RegId::new((reg_idx - 1) as u8).map(RegTarget::X), + _ => None, } - let cols = run_cols(app, area); - let sidebar = cols[0]; +} + +/// The sidebar's inner content rect (inside the panel border), or `None` when +/// the click misses it or the sidebar is collapsed. +fn sidebar_inner_hit(app: &App, me: MouseEvent, area: Rect) -> Option { if app.run.sidebar_collapsed { - return; + return None; } + let sidebar = run_cols(app, area)[0]; let inner = Rect::new( sidebar.x + 1, sidebar.y + 1, sidebar.width.saturating_sub(2), sidebar.height.saturating_sub(2), ); - if me.column < inner.x || me.column >= inner.x + inner.width { + let in_x = me.column >= inner.x && me.column < inner.x + inner.width; + let in_y = me.row >= inner.y && me.row < inner.y + inner.height; + (in_x && in_y).then_some(inner) +} + +fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { + if !app.run_sidebar_shows_registers() { return; } - if me.row < inner.y || me.row >= inner.y + inner.height { + if app.run.show_float_regs { + handle_float_register_click(app, me, area); return; } + let Some(inner) = sidebar_inner_hit(app, me, area) else { + return; + }; let visual_row = (me.row - inner.y) as usize; + let is_value = me.column >= inner.x + INT_LABEL_W; let pinned = &app.run.pinned_regs; let sep_row = if pinned.is_empty() { usize::MAX @@ -1620,10 +1627,16 @@ fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { pinned.len() }; - // Click on a pinned register row → unpin it + // Pinned section: value column edits the register, label column unpins it. if visual_row < pinned.len() { let reg = pinned[visual_row]; - app.run.pinned_regs.retain(|&r| r != reg); + if is_value { + if let Some(target) = reg_target_for_row(reg as usize + 1) { + app.begin_run_edit(RunEditTarget::Reg(target)); + } + } else { + app.run.pinned_regs.retain(|&r| r != reg); + } return; } // Click on separator → ignore @@ -1631,7 +1644,7 @@ fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { return; } - // Click on a regular (scrolled) register row → pin/unpin + // Regular (scrolled) section. let offset = if pinned.is_empty() { 0 } else { @@ -1643,11 +1656,19 @@ fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { let max_scroll = total.saturating_sub(visible_rows.saturating_sub(offset)); let start = app.run.regs_scroll.min(max_scroll); let reg_idx = start + row_in_scroll; // 0 = PC, 1..=32 = x0..x31 + if reg_idx > 32 { + return; + } - if reg_idx == 0 { + // Value column → open the editor (PC included). + if is_value { + if let Some(target) = reg_target_for_row(reg_idx) { + app.begin_run_edit(RunEditTarget::Reg(target)); + } return; - } // PC can't be pinned - if reg_idx > 32 { + } + // Label column → pin / unpin (the PC row cannot be pinned). + if reg_idx == 0 { return; } let reg = (reg_idx - 1) as u8; @@ -1658,6 +1679,65 @@ fn handle_register_click(app: &mut App, me: MouseEvent, area: Rect) { } } +/// A click on the float register table's value column opens its editor. The row +/// → `f`-index mapping mirrors `render_float_register_table` (scroll + visible). +fn handle_float_register_click(app: &mut App, me: MouseEvent, area: Rect) { + let Some(inner) = sidebar_inner_hit(app, me, area) else { + return; + }; + if me.column < inner.x + FLOAT_LABEL_W { + return; + } + let visible = inner.height.saturating_sub(2) as usize; + if visible == 0 { + return; + } + let scroll = app.run.regs_scroll.min(32usize.saturating_sub(visible)); + let visual_row = (me.row - inner.y) as usize; + if visual_row >= visible { + return; + } + let f_index = scroll + visual_row; + if let Some(freg) = FRegId::new(f_index as u8) { + app.begin_run_edit(RunEditTarget::FReg(freg)); + } +} + +/// A click on a memory row opens that cell's editor (width follows the byte +/// view). The row → address mapping mirrors `memory_items`, including the +/// one-line offset the search bar takes when open. +fn handle_memory_click(app: &mut App, me: MouseEvent, area: Rect) { + if !app.run_sidebar_shows_memory() { + return; + } + let Some(inner) = sidebar_inner_hit(app, me, area) else { + return; + }; + // The search bar, when open, occupies the first inner row (see + // `render_memory_view`); the memory list starts below it. + let search_offset = if app.run.mem_search_open && inner.height > 2 { + 1 + } else { + 0 + }; + if me.row < inner.y + search_offset { + return; + } + let list_height = inner.height.saturating_sub(search_offset) as u32; + let row = (me.row - inner.y - search_offset) as u32; + let bytes = app.run.mem_view_bytes; + let base = app.run.visible_memory_base_addr(Some(list_height)); + let addr = base.wrapping_add(row * bytes); + let max = app.run.mem_size.saturating_sub(bytes as usize) as u32; + if addr > max { + return; + } + app.begin_run_edit(RunEditTarget::Mem { + addr, + width: MemWidth::from_view_bytes(bytes), + }); +} + fn handle_help_popup_mouse(app: &mut App, me: MouseEvent, area: Rect) { if matches!(me.kind, MouseEventKind::Down(MouseButton::Left)) { let popup = help_popup_rect(area, app); @@ -2244,16 +2324,16 @@ fn apply_l1_config(app: &mut App, keep_history: bool) { let extra = app.cache.extra_pending.clone(); if keep_history { app.cache.config_status = Some("Settings applied (history kept).".to_string()); - let old_istats = std::mem::take(&mut app.run.mem.icache.stats); - let old_dstats = std::mem::take(&mut app.run.mem.dcache.stats); - app.run.mem.apply_config(icfg, dcfg, extra); - app.run.mem.icache.stats.history = old_istats.history; - app.run.mem.dcache.stats.history = old_dstats.history; + let old_istats = std::mem::take(&mut app.run.machine.mem_mut_unjournaled().icache.stats); + let old_dstats = std::mem::take(&mut app.run.machine.mem_mut_unjournaled().dcache.stats); + app.run.machine.mem_mut_unjournaled().apply_config(icfg, dcfg, extra); + app.run.machine.mem_mut_unjournaled().icache.stats.history = old_istats.history; + app.run.machine.mem_mut_unjournaled().dcache.stats.history = old_dstats.history; } else { app.cache.config_status = Some("Settings applied (stats reset).".to_string()); - app.run.mem.apply_config(icfg, dcfg, extra); + app.run.machine.mem_mut_unjournaled().apply_config(icfg, dcfg, extra); } - app.run.mem.bypass = !app.run.cache_enabled; + app.run.machine.mem_mut_unjournaled().bypass = !app.run.cache_enabled; app.cache.view_scroll = 0; app.cache.view_scroll_d = 0; app.cache.stats_scroll = 0; @@ -2319,23 +2399,26 @@ fn apply_extra_config(app: &mut App, extra_idx: usize, keep_history: bool) { app.cache.config_error = None; if keep_history { app.cache.config_status = Some("Settings applied (history kept).".to_string()); - let old_stats = if extra_idx < app.run.mem.extra_levels.len() { + let old_stats = if extra_idx < app.run.mem().extra_levels.len() { Some(std::mem::take( - &mut app.run.mem.extra_levels[extra_idx].stats, + &mut app.run.machine.mem_mut_unjournaled().extra_levels[extra_idx].stats, )) } else { None }; - if extra_idx < app.run.mem.extra_levels.len() { - app.run.mem.extra_levels[extra_idx] = crate::falcon::cache::Cache::new(cfg); + if extra_idx < app.run.mem().extra_levels.len() { + app.run.machine.mem_mut_unjournaled().extra_levels[extra_idx] = + crate::falcon::cache::Cache::new(cfg); if let Some(s) = old_stats { - app.run.mem.extra_levels[extra_idx].stats.history = s.history; + app.run.machine.mem_mut_unjournaled().extra_levels[extra_idx].stats.history = + s.history; } } } else { app.cache.config_status = Some("Settings applied (stats reset).".to_string()); - if extra_idx < app.run.mem.extra_levels.len() { - app.run.mem.extra_levels[extra_idx] = crate::falcon::cache::Cache::new(cfg); + if extra_idx < app.run.mem().extra_levels.len() { + app.run.machine.mem_mut_unjournaled().extra_levels[extra_idx] = + crate::falcon::cache::Cache::new(cfg); } } app.cache.view_scroll = 0; @@ -2477,7 +2560,7 @@ fn handle_settings_click(app: &mut App, me: MouseEvent) { let (pipe_y, _, _) = app.settings.bool_btn_pipeline_rect.get(); if me.row == pipe_y { - app.set_pipeline_enabled(!app.pipeline.enabled); + app.set_pipeline_enabled(!app.run.pipeline().enabled); app.settings.selected = SETTINGS_ROW_PIPELINE_ENABLED; return; } @@ -2531,7 +2614,7 @@ fn handle_settings_click(app: &mut App, me: MouseEvent) { // ── Pipeline tab mouse ──────────────────────────────────────────────────────── fn update_pipeline_hover(app: &mut App, me: MouseEvent) { - let p = &mut app.pipeline; + let p = app.run.pipeline_mut(); let state_clickable = !p.faulted; p.hover_subtab_main = false; p.hover_subtab_config = false; @@ -2608,36 +2691,36 @@ fn handle_pipeline_click(app: &mut App, me: MouseEvent) { BranchPredict, BranchResolve, PipelineBypassConfig, PipelineMode, PipelineSubtab, }; - let (main_y, main_x0, main_x1) = app.pipeline.btn_subtab_main_rect.get(); + let (main_y, main_x0, main_x1) = app.run.pipeline().btn_subtab_main_rect.get(); if me.row == main_y && me.column >= main_x0 && me.column < main_x1 { - app.pipeline.subtab = PipelineSubtab::Main; + app.run.pipeline_mut().subtab = PipelineSubtab::Main; return; } - let (cfg_y, cfg_x0, cfg_x1) = app.pipeline.btn_subtab_config_rect.get(); + let (cfg_y, cfg_x0, cfg_x1) = app.run.pipeline().btn_subtab_config_rect.get(); if me.row == cfg_y && me.column >= cfg_x0 && me.column < cfg_x1 { - app.pipeline.subtab = PipelineSubtab::Config; + app.run.pipeline_mut().subtab = PipelineSubtab::Config; return; } - let (core_y, core_x0, core_x1) = app.pipeline.btn_core_rect.get(); + let (core_y, core_x0, core_x1) = app.run.pipeline().btn_core_rect.get(); if me.row == core_y && me.column >= core_x0 && me.column < core_x1 { app.cycle_selected_core(1); return; } - let (rst_y, rst_x0, rst_x1) = app.pipeline.btn_reset_rect.get(); + let (rst_y, rst_x0, rst_x1) = app.run.pipeline().btn_reset_rect.get(); if me.row == rst_y && me.column >= rst_x0 && me.column < rst_x1 { app.restart_simulation(); return; } - let (spd_y, spd_x0, spd_x1) = app.pipeline.btn_speed_rect.get(); + let (spd_y, spd_x0, spd_x1) = app.run.pipeline().btn_speed_rect.get(); if me.row == spd_y && me.column >= spd_x0 && me.column < spd_x1 { - app.pipeline.speed = app.pipeline.speed.next(); - app.pipeline.last_tick = std::time::Instant::now(); + app.run.pipeline_mut().speed = app.run.pipeline_mut().speed.next(); + app.run.pipeline_mut().last_tick = std::time::Instant::now(); return; } - let (st_y, st_x0, st_x1) = app.pipeline.btn_state_rect.get(); + let (st_y, st_x0, st_x1) = app.run.pipeline().btn_state_rect.get(); if me.row == st_y && me.column >= st_x0 && me.column < st_x1 { - if app.pipeline.enabled && !app.pipeline.faulted { - if app.pipeline.halted { + if app.run.pipeline().enabled && !app.run.pipeline().faulted { + if app.run.pipeline().halted { app.restart_simulation(); if app.can_start_run() { app.run.is_running = true; @@ -2653,107 +2736,107 @@ fn handle_pipeline_click(app: &mut App, me: MouseEvent) { } return; } - let (res_y, res_x0, res_x1) = app.pipeline.btn_export_results_rect.get(); + let (res_y, res_x0, res_x1) = app.run.pipeline().btn_export_results_rect.get(); if me.row == res_y && me.column >= res_x0 && me.column < res_x1 { do_export_pipeline_results(app); return; } - let (in_y, in_x0, in_x1) = app.pipeline.btn_import_cfg_rect.get(); + let (in_y, in_x0, in_x1) = app.run.pipeline().btn_import_cfg_rect.get(); if me.row == in_y && me.column >= in_x0 && me.column < in_x1 { crate::ui::input::keyboard::do_import_pcfg(app); return; } - let (out_y, out_x0, out_x1) = app.pipeline.btn_export_cfg_rect.get(); + let (out_y, out_x0, out_x1) = app.run.pipeline().btn_export_cfg_rect.get(); if me.row == out_y && me.column >= out_x0 && me.column < out_x1 { crate::ui::input::keyboard::do_export_pcfg(app); return; } // Config row clicks — toggle on click like Cache tab - if matches!(app.pipeline.subtab, PipelineSubtab::Config) { - let rects = app.pipeline.config_row_rects.get(); + if matches!(app.run.pipeline().subtab, PipelineSubtab::Config) { + let rects = app.run.pipeline().config_row_rects.get(); for i in 0..PipelineBypassConfig::CONFIG_ROWS { let (ry, rx0, rx1) = rects[i]; if ry > 0 && me.row == ry && me.column >= rx0 && me.column < rx1 { match i { - 0 => app.pipeline.bypass.ex_to_ex = !app.pipeline.bypass.ex_to_ex, - 1 => app.pipeline.bypass.mem_to_ex = !app.pipeline.bypass.mem_to_ex, - 2 => app.pipeline.bypass.wb_to_id = !app.pipeline.bypass.wb_to_id, - 3 => app.pipeline.bypass.store_to_load = !app.pipeline.bypass.store_to_load, + 0 => app.run.pipeline_mut().bypass.ex_to_ex = !app.run.pipeline_mut().bypass.ex_to_ex, + 1 => app.run.pipeline_mut().bypass.mem_to_ex = !app.run.pipeline_mut().bypass.mem_to_ex, + 2 => app.run.pipeline_mut().bypass.wb_to_id = !app.run.pipeline_mut().bypass.wb_to_id, + 3 => app.run.pipeline_mut().bypass.store_to_load = !app.run.pipeline_mut().bypass.store_to_load, 4 => { - app.pipeline.mode = match app.pipeline.mode { + app.run.pipeline_mut().mode = match app.run.pipeline_mut().mode { PipelineMode::SingleCycle => PipelineMode::FunctionalUnits, PipelineMode::FunctionalUnits => PipelineMode::SingleCycle, } } 5 => { - app.pipeline.branch_resolve = match app.pipeline.branch_resolve { + app.run.pipeline_mut().branch_resolve = match app.run.pipeline_mut().branch_resolve { BranchResolve::Id => BranchResolve::Ex, BranchResolve::Ex => BranchResolve::Mem, BranchResolve::Mem => BranchResolve::Id, } } 6 => { - let next = match app.pipeline.predict { + let next = match app.run.pipeline().predict { BranchPredict::NotTaken => BranchPredict::Taken, BranchPredict::Taken => BranchPredict::Btfnt, BranchPredict::Btfnt => BranchPredict::TwoBit, BranchPredict::TwoBit => BranchPredict::NotTaken, }; - app.pipeline.set_predict(next); + app.run.pipeline_mut().set_predict(next); } 7 => { let idx = crate::ui::pipeline::FuKind::Alu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 8 => { let idx = crate::ui::pipeline::FuKind::Mul.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 9 => { let idx = crate::ui::pipeline::FuKind::Div.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 10 => { let idx = crate::ui::pipeline::FuKind::Fpu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 11 => { let idx = crate::ui::pipeline::FuKind::Lsu.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } 12 => { let idx = crate::ui::pipeline::FuKind::Sys.index(); - app.pipeline.fu_capacity[idx] = if app.pipeline.fu_capacity[idx] >= 8 { + app.run.pipeline_mut().fu_capacity[idx] = if app.run.pipeline_mut().fu_capacity[idx] >= 8 { 1 } else { - app.pipeline.fu_capacity[idx] + 1 + app.run.pipeline_mut().fu_capacity[idx] + 1 }; } _ => {} } app.reconfigure_pipeline_model(); - app.pipeline.config_cursor = i; + app.run.pipeline_mut().config_cursor = i; return; } } diff --git a/src/ui/pipeline/mod.rs b/src/ui/pipeline/mod.rs index 9f16c70..4efc692 100644 --- a/src/ui/pipeline/mod.rs +++ b/src/ui/pipeline/mod.rs @@ -758,7 +758,11 @@ pub(crate) fn gantt_view_rows<'a>( scroll: usize, visible_rows: usize, ) -> Vec<&'a GanttRow> { - rows.iter().skip(scroll).take(visible_rows.max(1)).collect() + let visible = visible_rows.max(1); + let scroll = scroll.min(gantt_max_scroll_for_len(rows.len(), visible)); + let end = rows.len().saturating_sub(scroll); + let start = end.saturating_sub(visible); + rows.iter().skip(start).take(end - start).collect() } pub(crate) fn gantt_visible_rows(gantt_area_height: u16) -> usize { @@ -774,24 +778,6 @@ pub(crate) fn gantt_max_scroll(state: &PipelineSimState, gantt_area_height: u16) gantt_max_scroll_for_len(state.gantt.len(), gantt_visible_rows(gantt_area_height)) } -pub(crate) fn maybe_follow_gantt_tail( - current_scroll: usize, - visible_rows: usize, - prev_len: usize, -) -> usize { - if visible_rows == 0 { - return current_scroll; - } - - let prev_max_scroll = gantt_max_scroll_for_len(prev_len, visible_rows); - let was_showing_newest = current_scroll >= prev_max_scroll; - if was_showing_newest { - gantt_max_scroll_for_len(prev_len.saturating_add(1), visible_rows) - } else { - current_scroll - } -} - // ── Subtabs ─────────────────────────────────────────────────────────────────── #[derive(Clone, Copy, PartialEq, Eq)] @@ -1069,6 +1055,7 @@ pub struct PipelineSimState { // ── UI state ── pub subtab: PipelineSubtab, pub config_cursor: usize, + /// Scroll offset from the bottom (0 = pinned to the newest row, i.e. follow). pub gantt_scroll: usize, pub gantt_visible_rows_cache: Cell, pub gantt_max_scroll_cache: Cell, @@ -1119,6 +1106,97 @@ pub struct PipelineSimState { pub gantt_area_rect: Cell<(u16, u16, u16, u16)>, } +/// The reversible slice of [`PipelineSimState`] — everything `pipeline_tick` +/// mutates as the clock advances, and nothing else. Captured before each cycle +/// by [`crate::falcon::machine::Machine::step_pipeline`] and restored on +/// step-back, so undoing a cycle rewinds the stages, functional units, branch +/// predictor, hazard read-outs, and the Gantt history exactly. +/// +/// Excludes pure config (`bypass`/`mode`/`branch_resolve`/`exec_regions`/ +/// `fu_capacity`/`sequential_mode`) and pure view state (hover flags, button +/// rects, render caches) — those are not part of a clock cycle and must survive +/// an undo. +pub struct PipelineExecSnapshot { + fetch_pc: u32, + halted: bool, + faulted: bool, + stages: [Option; 5], + fu_bank: FuBank, + fu_busy: [u8; 7], + predictor: predictor::PredictorState, + pending_fetch_trap: Option<(u32, u32, u32, u32)>, + cycle_count: u64, + instr_committed: u64, + stall_count: u64, + stall_by_type: [u64; HazardType::STALL_TYPE_COUNT], + flush_count: u64, + branches_executed: u64, + class_counts: [u64; InstrClass::COUNT], + last_cycle_cache_only: bool, + hazard_msgs: Vec<(HazardType, String)>, + hazard_traces: Vec, + gantt: VecDeque, + gantt_scroll: usize, + next_gantt_id: u64, + next_seq: u64, +} + +impl crate::falcon::machine::JournaledPipeline for PipelineSimState { + type Snapshot = PipelineExecSnapshot; + + fn exec_snapshot(&self) -> PipelineExecSnapshot { + PipelineExecSnapshot { + fetch_pc: self.fetch_pc, + halted: self.halted, + faulted: self.faulted, + stages: self.stages.clone(), + fu_bank: self.fu_bank.clone(), + fu_busy: self.fu_busy, + predictor: self.predictor.clone(), + pending_fetch_trap: self.pending_fetch_trap, + cycle_count: self.cycle_count, + instr_committed: self.instr_committed, + stall_count: self.stall_count, + stall_by_type: self.stall_by_type, + flush_count: self.flush_count, + branches_executed: self.branches_executed, + class_counts: self.class_counts, + last_cycle_cache_only: self.last_cycle_cache_only, + hazard_msgs: self.hazard_msgs.clone(), + hazard_traces: self.hazard_traces.clone(), + gantt: self.gantt.clone(), + gantt_scroll: self.gantt_scroll, + next_gantt_id: self.next_gantt_id, + next_seq: self.next_seq, + } + } + + fn restore_exec(&mut self, s: PipelineExecSnapshot) { + self.fetch_pc = s.fetch_pc; + self.halted = s.halted; + self.faulted = s.faulted; + self.stages = s.stages; + self.fu_bank = s.fu_bank; + self.fu_busy = s.fu_busy; + self.predictor = s.predictor; + self.pending_fetch_trap = s.pending_fetch_trap; + self.cycle_count = s.cycle_count; + self.instr_committed = s.instr_committed; + self.stall_count = s.stall_count; + self.stall_by_type = s.stall_by_type; + self.flush_count = s.flush_count; + self.branches_executed = s.branches_executed; + self.class_counts = s.class_counts; + self.last_cycle_cache_only = s.last_cycle_cache_only; + self.hazard_msgs = s.hazard_msgs; + self.hazard_traces = s.hazard_traces; + self.gantt = s.gantt; + self.gantt_scroll = s.gantt_scroll; + self.next_gantt_id = s.next_gantt_id; + self.next_seq = s.next_seq; + } +} + impl PipelineSimState { pub fn clear_hover_state(&mut self) { self.hover_subtab_main = false; diff --git a/src/ui/pipeline/sim.rs b/src/ui/pipeline/sim.rs index 6b31308..928382a 100644 --- a/src/ui/pipeline/sim.rs +++ b/src/ui/pipeline/sim.rs @@ -3592,16 +3592,9 @@ fn update_gantt(state: &mut PipelineSimState) { last_stage: initial_stage, }; new_row.cells.push_back(cell); - let prev_len = state.gantt.len(); state.gantt.push_back(new_row); - state.gantt_scroll = super::maybe_follow_gantt_tail( - state.gantt_scroll, - state.gantt_visible_rows_cache.get(), - prev_len, - ); while state.gantt.len() > MAX_GANTT_ROWS + 4 { state.gantt.pop_front(); - state.gantt_scroll = state.gantt_scroll.saturating_sub(1); } } } @@ -3677,16 +3670,9 @@ fn update_gantt(state: &mut PipelineSimState) { last_stage: cell_track(cell), }; new_row.cells.push_back(cell); - let prev_len = state.gantt.len(); state.gantt.push_back(new_row); - state.gantt_scroll = super::maybe_follow_gantt_tail( - state.gantt_scroll, - state.gantt_visible_rows_cache.get(), - prev_len, - ); while state.gantt.len() > MAX_GANTT_ROWS + 4 { state.gantt.pop_front(); - state.gantt_scroll = state.gantt_scroll.saturating_sub(1); } } } @@ -3726,7 +3712,6 @@ fn update_gantt(state: &mut PipelineSimState) { while state.gantt.len() > MAX_GANTT_ROWS { state.gantt.pop_front(); - state.gantt_scroll = state.gantt_scroll.saturating_sub(1); } } diff --git a/src/ui/theme.rs b/src/ui/theme.rs index b0ccac4..45925c0 100644 --- a/src/ui/theme.rs +++ b/src/ui/theme.rs @@ -43,6 +43,8 @@ pub const RUNNING: Color = Color::Rgb(88, 200, 120); pub const PAUSED: Color = Color::Rgb(220, 170, 55); /// Danger / error / reset — clean red pub const DANGER: Color = Color::Rgb(210, 72, 68); +/// Speculative execution (unresolved branch) — burnt orange +pub const SPECULATIVE: Color = Color::Rgb(220, 140, 40); // ── General UI ─────────────────────────────────────────────────────────────── /// Accent = violet, for active tabs / titles / interactive highlights diff --git a/src/ui/tutorial/steps/pipeline.rs b/src/ui/tutorial/steps/pipeline.rs index b4a0473..993a24f 100644 --- a/src/ui/tutorial/steps/pipeline.rs +++ b/src/ui/tutorial/steps/pipeline.rs @@ -13,8 +13,7 @@ fn content_area(term: Rect) -> Rect { } struct PipelineLayout { - subtab: Rect, - controls: Rect, + header: Rect, content: Rect, } @@ -22,108 +21,72 @@ fn pipeline_layout(term: Rect) -> PipelineLayout { let c = content_area(term); let chunks = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Length(4), - Constraint::Length(4), - Constraint::Min(0), - Constraint::Length(3), - ]) + .constraints([Constraint::Length(2), Constraint::Min(0)]) .split(c); PipelineLayout { - subtab: chunks[0], - controls: chunks[1], - content: chunks[2], + header: chunks[0], + content: chunks[1], } } fn target_subtab(term: Rect, _app: &App) -> Option { - Some(pipeline_layout(term).subtab) + Some(pipeline_layout(term).header) } fn target_controls(term: Rect, _app: &App) -> Option { - Some(pipeline_layout(term).controls) + Some(pipeline_layout(term).header) +} + +fn main_plan(content: Rect, app: &App) -> crate::ui::view::pipeline::MainLayoutPlan { + crate::ui::view::pipeline::plan_main_layout( + content.height, + content.width, + app.run.pipeline().hazard_traces.len(), + ) } fn target_stages(term: Rect, app: &App) -> Option { let content = pipeline_layout(term).content; - let _ = app; - let stages_h = 9; + let plan = main_plan(content, app); + // Include the FU strip: it belongs to the stage/EX story. + let h = plan.stages_h + plan.fu_h; Some(Rect { x: content.x, y: content.y, width: content.width, - height: stages_h.min(content.height), + height: h.min(content.height), }) } fn target_hazards(term: Rect, app: &App) -> Option { let content = pipeline_layout(term).content; - let stages_h = 9; - let max_trace_rows = content - .height - .saturating_sub(stages_h) - .saturating_sub(5) - .clamp(3, 8); - let trace_rows = app - .pipeline - .hazard_traces - .len() - .min(max_trace_rows as usize) as u16; - let legend_rows = if app.pipeline.hazard_traces.is_empty() { - 0 - } else { - 1 - }; - let msg_rows = if app.pipeline.hazard_msgs.is_empty() { - 1 - } else { - app.pipeline.hazard_msgs.len().min(2) as u16 - }; - let hazards_h = (2 + trace_rows + legend_rows + msg_rows) - .min(content.height.saturating_sub(stages_h).saturating_sub(3)) - .clamp(4, 13); + let plan = main_plan(content, app); + if plan.collapsed { + return target_stages(term, app); + } + let top = (plan.stages_h + plan.fu_h).min(content.height); Some(Rect { x: content.x, - y: content.y + stages_h.min(content.height), + y: content.y + top, width: content.width, - height: hazards_h.min(content.height.saturating_sub(stages_h.min(content.height))), + height: plan.hazards_h.min(content.height.saturating_sub(top)), }) } fn target_gantt(term: Rect, app: &App) -> Option { let content = pipeline_layout(term).content; - let stages_h = 9; - let max_trace_rows = content - .height - .saturating_sub(stages_h) - .saturating_sub(5) - .clamp(3, 8); - let trace_rows = app - .pipeline - .hazard_traces - .len() - .min(max_trace_rows as usize) as u16; - let legend_rows = if app.pipeline.hazard_traces.is_empty() { - 0 - } else { - 1 - }; - let msg_rows = if app.pipeline.hazard_msgs.is_empty() { - 1 + let plan = main_plan(content, app); + let top = if plan.collapsed { + plan.stages_h } else { - app.pipeline.hazard_msgs.len().min(2) as u16 - }; - let hazards_h = (2 + trace_rows + legend_rows + msg_rows) - .min(content.height.saturating_sub(stages_h).saturating_sub(3)) - .clamp(4, 13); + plan.stages_h + plan.fu_h + plan.hazards_h + } + .min(content.height); Some(Rect { x: content.x, - y: content.y + stages_h.min(content.height) + hazards_h, + y: content.y + top, width: content.width, - height: content - .height - .saturating_sub(stages_h.min(content.height)) - .saturating_sub(hazards_h), + height: content.height.saturating_sub(top), }) } @@ -132,11 +95,11 @@ fn target_config(term: Rect, _app: &App) -> Option { } fn setup_main(app: &mut App) { - app.pipeline.subtab = PipelineSubtab::Main; + app.run.pipeline_mut().subtab = PipelineSubtab::Main; } fn setup_config(app: &mut App) { - app.pipeline.subtab = PipelineSubtab::Config; + app.run.pipeline_mut().subtab = PipelineSubtab::Config; } pub static STEPS: &[TutorialStep] = &[ @@ -156,6 +119,8 @@ pub static STEPS: &[TutorialStep] = &[ \nMain:\ \n[↑/↓] :: scroll gantt and history\ \n[PageUp/PageDown] :: page through gantt and history\ +\n[End/G] :: follow the newest instructions again\ +\n[Home/g] :: jump to the oldest recorded instruction\ \n\ \nSettings and files:\ \n[↑/↓] :: move the settings cursor\ @@ -176,6 +141,8 @@ pub static STEPS: &[TutorialStep] = &[ \nMain:\ \n[↑/↓] :: rolam o gantt e o histórico\ \n[PageUp/PageDown] :: avançam páginas no gantt e histórico\ +\n[End/G] :: volta a seguir as instruções mais novas\ +\n[Home/g] :: pula para a instrução mais antiga registrada\ \n\ \nSettings e arquivos:\ \n[↑/↓] :: movem o cursor de configuração\ @@ -257,12 +224,12 @@ pub static STEPS: &[TutorialStep] = &[ \n\nCtrl+e — export the current pipeline configuration as a .pcfg file.\ \nCtrl+l — import a .pcfg file and apply it immediately.\ \nCtrl+r — export simulation results (stage timings, hazard counts) as .pstats or .csv.\ -\n\nThese are also available as buttons in the controls bar at the bottom of the Pipeline tab.", +\n\nThese are also available as buttons in the header at the top of the Pipeline tab.", body_pt: "Três atalhos gerenciam os dados do pipeline fora da sessão:\ \n\nCtrl+e — exporta a configuração atual do pipeline como arquivo .pcfg.\ \nCtrl+l — importa um arquivo .pcfg e aplica imediatamente.\ \nCtrl+r — exporta os resultados da simulação (timings de estágios, contagem de hazards) em .pstats ou .csv.\ -\n\nEstes também estão disponíveis como botões na barra de controles na parte inferior da aba Pipeline.", +\n\nEstes também estão disponíveis como botões no cabeçalho no topo da aba Pipeline.", target: target_controls, setup: Some(setup_config), }, diff --git a/src/ui/tutorial/steps/tlb.rs b/src/ui/tutorial/steps/tlb.rs index 3a4ffee..3a6dd03 100644 --- a/src/ui/tutorial/steps/tlb.rs +++ b/src/ui/tutorial/steps/tlb.rs @@ -13,7 +13,7 @@ fn setup_stats(app: &mut App) { fn setup_config(app: &mut App) { app.tlb.vm_subtab = VmSubtab::Tlb; app.tlb.subtab = TlbSubtab::Settings; - app.tlb.pending = app.run.mem.mmu().tlb.config.clone(); + app.tlb.pending = app.run.mem().mmu().tlb.config.clone(); } fn setup_entries(app: &mut App) { app.tlb.vm_subtab = VmSubtab::Tlb; @@ -209,12 +209,12 @@ A árvore é somente-leitura — edite o mapa e o esquema na subaba settings. \ by setting the matching bit in medeleg (bit 13 = load fault, 15 = store) and pointing stvec at your handler. \ When U-mode touches an unmapped page, the CPU vectors to stvec in S-mode (saving sepc/scause/stval), \ the handler installs the missing PTE, runs sfence.vma, and `sret` returns to retry the faulting access — now it succeeds. \ -The full runnable kernel lives in docs/virtual-memory.md and is exercised by tests/mmu_traps.rs.", +The full runnable kernel is laid out step by step in the virtual-memory guide.", body_pt: "O modo Manual libera o padrão clássico de SO. Delegue page faults de load/store ao modo supervisor \ setando o bit correspondente em medeleg (bit 13 = load fault, 15 = store) e apontando stvec para seu handler. \ Quando o U-mode toca uma página não-mapeada, a CPU vetoriza para stvec em S-mode (salvando sepc/scause/stval), \ o handler instala a PTE faltante, roda sfence.vma, e `sret` retorna para repetir o acesso — agora ele funciona. \ -O kernel completo executável está em docs/virtual-memory.md e é testado em tests/mmu_traps.rs.", +O kernel completo executável está detalhado passo a passo no guia de memória virtual.", target: whole, setup: Some(setup_page_tree), }, diff --git a/src/ui/view/cache/config.rs b/src/ui/view/cache/config.rs index 34a8913..2d30b4d 100644 --- a/src/ui/view/cache/config.rs +++ b/src/ui/view/cache/config.rs @@ -47,9 +47,9 @@ fn render_cache_config_panel(f: &mut Frame, area: Rect, app: &App, icache: bool) "D-Cache Settings" }; let current = if icache { - &app.run.mem.icache.config + &app.run.mem().icache.config } else { - &app.run.mem.dcache.config + &app.run.mem().dcache.config }; // Determine which field (if any) is being edited in this panel @@ -114,8 +114,8 @@ fn render_unified_config(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) } else { return; }; - let current = if extra_idx < app.run.mem.extra_levels.len() { - &app.run.mem.extra_levels[extra_idx].config + let current = if extra_idx < app.run.mem().extra_levels.len() { + &app.run.mem().extra_levels[extra_idx].config } else { pending }; diff --git a/src/ui/view/cache/mod.rs b/src/ui/view/cache/mod.rs index 3b702bc..c4a244b 100644 --- a/src/ui/view/cache/mod.rs +++ b/src/ui/view/cache/mod.rs @@ -92,9 +92,9 @@ fn render_cache_exec_controls(f: &mut Frame, area: Rect, app: &App) { (cycles, cpi, committed) } else { ( - app.run.mem.total_program_cycles(), - app.run.mem.overall_cpi(), - app.run.mem.instruction_count, + app.run.mem().total_program_cycles(), + app.run.mem().overall_cpi(), + app.run.mem().instruction_count, ) }; diff --git a/src/ui/view/cache/stats.rs b/src/ui/view/cache/stats.rs index 238f77e..c8af25e 100644 --- a/src/ui/view/cache/stats.rs +++ b/src/ui/view/cache/stats.rs @@ -18,7 +18,7 @@ pub(super) fn render_stats(f: &mut Frame, area: Rect, app: &App) { render_l1_stats(f, area, app); } else { let idx = app.cache.selected_level - 1; - if idx < app.run.mem.extra_levels.len() { + if idx < app.run.mem().extra_levels.len() { render_unified_stats(f, area, app, idx); } } @@ -81,14 +81,14 @@ fn render_program_summary(f: &mut Frame, area: Rect, app: &App) { (cycles, cpi, ipc, committed) } else { ( - app.run.mem.total_program_cycles(), - app.run.mem.overall_cpi(), - app.run.mem.ipc(), - app.run.mem.instruction_count, + app.run.mem().total_program_cycles(), + app.run.mem().overall_cpi(), + app.run.mem().ipc(), + app.run.mem().instruction_count, ) }; - let i_cyc = app.run.mem.icache.stats.total_cycles; - let d_cyc = app.run.mem.dcache.stats.total_cycles; + let i_cyc = app.run.mem().icache.stats.total_cycles; + let d_cyc = app.run.mem().dcache.stats.total_cycles; let mut spans = vec![ Span::styled( @@ -130,7 +130,7 @@ fn render_program_summary(f: &mut Frame, area: Rect, app: &App) { ), ]; - for (i, lvl) in app.run.mem.extra_levels.iter().enumerate() { + for (i, lvl) in app.run.mem().extra_levels.iter().enumerate() { let name = crate::falcon::cache::CacheController::extra_level_name(i); spans.push(Span::raw(" + ")); spans.push(Span::styled( @@ -166,14 +166,14 @@ fn render_cache_metrics(f: &mut Frame, area: Rect, app: &App, icache: bool) { let (label, cache, instructions) = if icache { ( "I-Cache", - &app.run.mem.icache, - app.run.mem.instruction_count, + &app.run.mem().icache, + app.run.mem().instruction_count, ) } else { ( "D-Cache", - &app.run.mem.dcache, - app.run.mem.instruction_count, + &app.run.mem().dcache, + app.run.mem().instruction_count, ) }; let stats = &cache.stats; @@ -325,9 +325,9 @@ fn render_cache_metrics(f: &mut Frame, area: Rect, app: &App, icache: bool) { // Line 8: AMAT let amat = if icache { - app.run.mem.icache_amat() + app.run.mem().icache_amat() } else { - app.run.mem.dcache_amat() + app.run.mem().dcache_amat() }; let line8 = format!("Avarage Memory Access Time: {amat:.2} cyc"); f.render_widget( @@ -436,8 +436,8 @@ fn render_chart(f: &mut Frame, area: Rect, app: &App) { } let scope = app.cache.scope; - let i_data: Vec<(f64, f64)> = app.run.mem.icache.stats.history.iter().cloned().collect(); - let d_data: Vec<(f64, f64)> = app.run.mem.dcache.stats.history.iter().cloned().collect(); + let i_data: Vec<(f64, f64)> = app.run.mem().icache.stats.history.iter().cloned().collect(); + let d_data: Vec<(f64, f64)> = app.run.mem().dcache.stats.history.iter().cloned().collect(); if i_data.is_empty() && d_data.is_empty() { let msg = Paragraph::new("No data yet — run the program to collect cache statistics.") @@ -518,12 +518,12 @@ fn render_chart(f: &mut Frame, area: Rect, app: &App) { } fn render_unified_metrics(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) { - let cache = &app.run.mem.extra_levels[extra_idx]; + let cache = &app.run.mem().extra_levels[extra_idx]; let level_name = crate::falcon::cache::CacheController::extra_level_name(extra_idx); let label = format!("{level_name} (Unified)"); let stats = &cache.stats; let cfg = &cache.config; - let instructions = app.run.mem.instruction_count; + let instructions = app.run.mem().instruction_count; let hit_rate = stats.hit_rate(); let hit_color = if hit_rate >= 90.0 { @@ -650,7 +650,7 @@ fn render_unified_metrics(f: &mut Frame, area: Rect, app: &App, extra_idx: usize return; } - let amat = app.run.mem.extra_level_amat(extra_idx); + let amat = app.run.mem().extra_level_amat(extra_idx); f.render_widget( Paragraph::new(Span::styled( format!("Avarage Memory Access Time: {amat:.2} cyc"), @@ -673,7 +673,7 @@ fn render_unified_chart(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) return; } - let data: Vec<(f64, f64)> = app.run.mem.extra_levels[extra_idx] + let data: Vec<(f64, f64)> = app.run.mem().extra_levels[extra_idx] .stats .history .iter() diff --git a/src/ui/view/cache/view.rs b/src/ui/view/cache/view.rs index 2b70434..8446597 100644 --- a/src/ui/view/cache/view.rs +++ b/src/ui/view/cache/view.rs @@ -83,7 +83,7 @@ pub(super) fn render_view(f: &mut Frame, area: Rect, app: &App) { render_l1_view(f, area, app); } else { let idx = app.cache.selected_level - 1; - if idx < app.run.mem.extra_levels.len() { + if idx < app.run.mem().extra_levels.len() { render_unified_view(f, area, app, idx); } } @@ -127,7 +127,7 @@ fn render_unified_view(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) { // ── Legend bar (outside the matrix block, 1 line, no border) ───────────────── fn render_unified_legend_bar(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) { - let cfg = &app.run.mem.extra_levels[extra_idx].config; + let cfg = &app.run.mem().extra_levels[extra_idx].config; let scroll_hint = vertical_scroll_hint(app); let policy_hint = policy_hint_str(cfg.replacement); @@ -168,8 +168,8 @@ fn render_unified_legend_bar(f: &mut Frame, area: Rect, app: &App, extra_idx: us fn render_legend_bar(f: &mut Frame, area: Rect, app: &App) { let scope = app.cache.scope; - let icfg = &app.run.mem.icache.config; - let dcfg = &app.run.mem.dcache.config; + let icfg = &app.run.mem().icache.config; + let dcfg = &app.run.mem().dcache.config; // Policy-specific hint — if both caches have the same policy, show once let policy_hint: String = match scope { @@ -370,7 +370,7 @@ fn policy_hint_short(p: ReplacementPolicy) -> &'static str { // ── Cache matrix ────────────────────────────────────────────────────────────── fn render_extra_cache_matrix(f: &mut Frame, area: Rect, app: &App, extra_idx: usize) { - let cache = &app.run.mem.extra_levels[extra_idx]; + let cache = &app.run.mem().extra_levels[extra_idx]; let level_name = CacheController::extra_level_name(extra_idx); let cfg = &cache.config; @@ -595,9 +595,9 @@ fn render_extra_cache_matrix(f: &mut Frame, area: Rect, app: &App, extra_idx: us fn render_cache_matrix(f: &mut Frame, area: Rect, app: &App, icache: bool) { let cache = if icache { - &app.run.mem.icache + &app.run.mem().icache } else { - &app.run.mem.dcache + &app.run.mem().dcache }; let label = if icache { "I-Cache" } else { "D-Cache" }; let cfg = &cache.config; diff --git a/src/ui/view/components/controls.rs b/src/ui/view/components/controls.rs index a6d02da..dc600ff 100644 --- a/src/ui/view/components/controls.rs +++ b/src/ui/view/components/controls.rs @@ -1,7 +1,54 @@ +use std::cell::Cell; + use ratatui::prelude::*; +use unicode_width::UnicodeWidthStr; use crate::ui::theme; +/// Span row that tracks the terminal x-cursor as spans are pushed, so button +/// hitboxes can be recorded from real rendered widths instead of hand-counted +/// character offsets. +pub(crate) struct SpanRow { + spans: Vec>, + x: u16, + y: u16, +} + +impl SpanRow { + pub(crate) fn new(x: u16, y: u16) -> Self { + Self { + spans: Vec::new(), + x, + y, + } + } + + pub(crate) fn push(&mut self, span: Span<'static>) { + self.x = self + .x + .saturating_add(UnicodeWidthStr::width(span.content.as_ref()) as u16); + self.spans.push(span); + } + + pub(crate) fn gap(&mut self, n: u16) { + self.push(Span::raw(" ".repeat(n as usize))); + } + + /// Current x-cursor; capture before pushing a button's spans and pass to + /// [`SpanRow::record_hitbox`] afterwards. + pub(crate) fn cursor(&self) -> u16 { + self.x + } + + pub(crate) fn record_hitbox(&self, start: u16, rect: &Cell<(u16, u16, u16)>) { + rect.set((self.y, start, self.x)); + } + + pub(crate) fn into_line(self) -> Line<'static> { + Line::from(self.spans) + } +} + pub(crate) fn push_dense_pair( spans: &mut Vec>, label: &str, diff --git a/src/ui/view/components/mod.rs b/src/ui/view/components/mod.rs index e7a45e6..f62a622 100644 --- a/src/ui/view/components/mod.rs +++ b/src/ui/view/components/mod.rs @@ -1,7 +1,9 @@ pub(super) mod build; pub(super) mod console; pub(super) mod controls; +pub(super) mod toolbar; // Re-export selected widgets for use by sibling modules under `view` pub(super) use console::render_console; -pub(crate) use controls::{dense_action, dense_value, push_dense_pair}; +pub(crate) use controls::{SpanRow, dense_action, dense_value, push_dense_pair}; +pub(crate) use toolbar::Toolbar; diff --git a/src/ui/view/components/toolbar.rs b/src/ui/view/components/toolbar.rs new file mode 100644 index 0000000..48cd10c --- /dev/null +++ b/src/ui/view/components/toolbar.rs @@ -0,0 +1,185 @@ +//! A dense horizontal toolbar: a row of small labeled controls that is the +//! **single source of truth** for both rendering and mouse hit-testing. +//! +//! The status bars used to compute their layout twice — once to emit the spans +//! in the view, and again, by hand with parallel column arithmetic, to map a +//! click column back to a button. The two drifted apart every time a control +//! was added or reordered. [`Toolbar`] closes that gap: build the row once from +//! your button-id type, then ask it for [`spans`](Toolbar::spans) *or* for the +//! control under a column ([`hit`](Toolbar::hit)). Both read the same per-cell +//! geometry, so a new button appears in the view and becomes clickable from one +//! edit. +//! +//! ```ignore +//! let mut bar = Toolbar::new(); +//! bar.pair(Btn::Fmt, "fmt", "hex", hovered, /*active*/ true, /*enabled*/ true, theme::TEXT) +//! .action(Btn::Reset, "reset", reset_hovered, /*enabled*/ true, theme::DANGER); +//! let spans = bar.spans(); // view +//! let hit = bar.hit(col, origin); // input +//! ``` + +use ratatui::prelude::*; + +use crate::ui::theme; + +/// Columns of blank space rendered between adjacent cells. +const GAP: u16 = 3; + +/// One control in a [`Toolbar`]: either a `label value` pair or a standalone +/// action word. `start..end` are columns relative to the toolbar origin, filled +/// in as the cell is pushed. +struct Cell { + id: Id, + /// `Some` for a `label value` pair, `None` for a bare action word. + label: Option, + value: String, + hovered: bool, + /// Drives the lit (vs dimmed) style when not hovered. + active: bool, + /// When `false` the cell renders dimmed and is transparent to clicks. + enabled: bool, + color: Color, + start: u16, + end: u16, +} + +/// A row of [`Cell`]s laid out left-to-right. Generic over the caller's button +/// id (a small `Copy` enum). +pub(crate) struct Toolbar { + cells: Vec>, + /// Running column where the next cell starts (relative to the origin). + cursor: u16, +} + +impl Default for Toolbar { + fn default() -> Self { + Self::new() + } +} + +impl Toolbar { + pub(crate) fn new() -> Self { + Self { + cells: Vec::new(), + cursor: 0, + } + } + + /// Append a `label value` control (e.g. `fmt hex`). The whole `label value` + /// span — label included — is hit-testable, matching how a user aims at the + /// word they read. + #[allow(clippy::too_many_arguments)] + pub(crate) fn pair( + &mut self, + id: Id, + label: &str, + value: &str, + hovered: bool, + active: bool, + enabled: bool, + color: Color, + ) -> &mut Self { + self.push( + id, + Some(label.to_string()), + value.to_string(), + hovered, + active, + enabled, + color, + ) + } + + /// Append a standalone action word (e.g. `reset`). A disabled action renders + /// dimmed and is not clickable. + pub(crate) fn action( + &mut self, + id: Id, + text: &str, + hovered: bool, + enabled: bool, + color: Color, + ) -> &mut Self { + // An action is lit whenever it is enabled (there is no separate on/off). + self.push(id, None, text.to_string(), hovered, enabled, enabled, color) + } + + #[allow(clippy::too_many_arguments)] + fn push( + &mut self, + id: Id, + label: Option, + value: String, + hovered: bool, + active: bool, + enabled: bool, + color: Color, + ) -> &mut Self { + if !self.cells.is_empty() { + self.cursor += GAP; + } + let width = match &label { + // label + one space + value + Some(l) => l.chars().count() + 1 + value.chars().count(), + None => value.chars().count(), + } as u16; + let start = self.cursor; + self.cursor += width; + self.cells.push(Cell { + id, + label, + value, + hovered, + active, + enabled, + color, + start, + end: self.cursor, + }); + self + } + + /// Render every cell, in order, to styled spans for a single `Line`. + pub(crate) fn spans(&self) -> Vec> { + let mut spans = Vec::with_capacity(self.cells.len() * 4); + for (i, cell) in self.cells.iter().enumerate() { + if i > 0 { + spans.push(Span::raw(" ".repeat(GAP as usize))); + } + if let Some(label) = &cell.label { + spans.push(Span::styled( + label.clone(), + Style::default().fg(theme::IDLE), + )); + spans.push(Span::raw(" ")); + } + spans.push(value_span(&cell.value, cell.hovered, cell.active, cell.color)); + } + spans + } + + /// The control under `col`, where `origin` is the toolbar's first rendered + /// column. Disabled cells are transparent to clicks. + pub(crate) fn hit(&self, col: u16, origin: u16) -> Option { + self.cells + .iter() + .find(|c| c.enabled && col >= origin + c.start && col < origin + c.end) + .map(|c| c.id) + } +} + +/// Style a control value: bold-bright when hovered, lit in its colour when +/// active, dim otherwise. Shared so a hovered/active control looks identical +/// across every toolbar. +fn value_span(text: &str, hovered: bool, active: bool, color: Color) -> Span<'static> { + let style = if hovered { + Style::default() + .fg(theme::TEXT) + .add_modifier(Modifier::BOLD) + } else if active { + Style::default().fg(color).add_modifier(Modifier::BOLD) + } else { + Style::default().fg(theme::IDLE) + }; + Span::styled(text.to_string(), style) +} diff --git a/src/ui/view/docs/content/fcache_ref.rs b/src/ui/view/docs/content/fcache_ref.rs index d417422..b16c523 100644 --- a/src/ui/view/docs/content/fcache_ref.rs +++ b/src/ui/view/docs/content/fcache_ref.rs @@ -419,6 +419,18 @@ fn fcache_ref_lines_en() -> Vec> { ), ]), blank(), + h2("Unified TLB Keys"), + blank(), + kv("tlb.entry_count", "integer, power of 2, >= tlb.associativity (default 32)"), + kv("tlb.associativity", "integer >= 1 (default 4)"), + kv("tlb.replacement", "lru fifo random lfu clock mru (lowercase!)"), + kv("tlb.hit_latency", "integer >= 1 cycles (default 1)"), + kv("tlb.miss_penalty", "integer >= 0 cycles (default 20)"), + note("TLB replacement is lowercase, unlike the cache .replacement key (PascalCase)."), + blank(), + note("The VM mode lives in the .rcfg, not here: vm_mode = off|sv32|custom|manual,"), + note("plus vm_offset_bits and vm_level_bits (comma list) for Custom mode."), + blank(), h2("Annotated Example File"), blank(), mono(" # Raven Cache Config v2"), @@ -470,6 +482,13 @@ fn fcache_ref_lines_en() -> Vec> { mono(" cpi.system=10"), mono(" cpi.fp=5"), blank(), + mono(" # --- Unified TLB ---"), + mono(" tlb.entry_count=32"), + mono(" tlb.associativity=4"), + mono(" tlb.replacement=lru"), + mono(" tlb.hit_latency=1"), + mono(" tlb.miss_penalty=20"), + blank(), ] } @@ -895,6 +914,18 @@ fn fcache_ref_lines_ptbr() -> Vec> { Span::styled("Instruções float RV32F", Style::default().fg(Color::White)), ]), blank(), + h2("Chaves da TLB Unificada"), + blank(), + kv("tlb.entry_count", "inteiro, potência de 2, >= tlb.associativity (padrão 32)"), + kv("tlb.associativity", "inteiro >= 1 (padrão 4)"), + kv("tlb.replacement", "lru fifo random lfu clock mru (minúsculas!)"), + kv("tlb.hit_latency", "inteiro >= 1 ciclos (padrão 1)"), + kv("tlb.miss_penalty", "inteiro >= 0 ciclos (padrão 20)"), + note("A política da TLB é em minúsculas, diferente da chave .replacement do cache (PascalCase)."), + blank(), + note("O modo de VM vive no .rcfg, não aqui: vm_mode = off|sv32|custom|manual,"), + note("mais vm_offset_bits e vm_level_bits (lista por vírgula) no modo Custom."), + blank(), h2("Exemplo Anotado de Arquivo"), blank(), mono(" # Raven Cache Config v2"), @@ -946,5 +977,12 @@ fn fcache_ref_lines_ptbr() -> Vec> { mono(" cpi.system=10"), mono(" cpi.fp=5"), blank(), + mono(" # --- TLB Unificada ---"), + mono(" tlb.entry_count=32"), + mono(" tlb.associativity=4"), + mono(" tlb.replacement=lru"), + mono(" tlb.hit_latency=1"), + mono(" tlb.miss_penalty=20"), + blank(), ] } diff --git a/src/ui/view/editor.rs b/src/ui/view/editor.rs index 2b22772..f915a84 100644 --- a/src/ui/view/editor.rs +++ b/src/ui/view/editor.rs @@ -1336,7 +1336,7 @@ fn render_encoding_bar(f: &mut Frame, area: Rect, app: &App) { let cursor_row = app.editor.buf.cursor_row; let line = if let Some(&addr) = app.editor.line_to_addr.get(&cursor_row) { - if let Ok(word) = app.run.mem.peek32(addr) { + if let Ok(word) = app.run.mem().peek32(addr) { // Format as: 0x00b50533 funct7 rs2 rs1 f3 rd opcode // (hex) [31:25] [24:20][19:15][14:12][11:7] [6:0] let f7 = (word >> 25) & 0x7F; diff --git a/src/ui/view/mod.rs b/src/ui/view/mod.rs index 093f986..42898c0 100644 --- a/src/ui/view/mod.rs +++ b/src/ui/view/mod.rs @@ -14,7 +14,7 @@ pub mod disasm; pub mod docs; mod editor; mod path_input_overlay; -mod pipeline; +pub(crate) mod pipeline; pub(crate) mod run; mod settings; mod splash; @@ -138,9 +138,9 @@ pub fn ui(f: &mut Frame, app: &App) { Style::default().fg(theme::LABEL), ), Tab::Pipeline => { - if let Some(ref err) = app.pipeline.status_error { + if let Some(ref err) = app.run.pipeline().status_error { (format!("✗ {err}"), Style::default().fg(theme::DANGER)) - } else if let Some(ref ok) = app.pipeline.status_msg { + } else if let Some(ref ok) = app.run.pipeline().status_msg { (format!("✓ {ok}"), Style::default().fg(theme::RUNNING)) } else { ( @@ -507,6 +507,7 @@ fn help_pages(tab: Tab) -> Vec> { ("[Ctrl+↑/↓]", "scroll console"), ("[↑/↓]", "scroll memory or registers"), ("[click]", "select instruction / register"), + ("[2×click/right]", "edit instruction word in place"), ("[drag]", "resize sidebar / instruction panels"), ], vec![ diff --git a/src/ui/view/pipeline/config_view.rs b/src/ui/view/pipeline/config_view.rs index 05e2623..7d0109d 100644 --- a/src/ui/view/pipeline/config_view.rs +++ b/src/ui/view/pipeline/config_view.rs @@ -17,7 +17,7 @@ const CONFIG_LABEL_W: usize = 18; const LATENCY_LABEL_W: usize = 8; pub fn render_pipeline_config(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; + let p = &app.run.pipeline(); let block = Block::default() .borders(Borders::ALL) @@ -192,7 +192,7 @@ pub fn render_pipeline_config(f: &mut Frame, area: Rect, app: &App) { } } } - app.pipeline.config_row_rects.set(rects); + app.run.pipeline().config_row_rects.set(rects); let desc_row = p.config_cursor; let desc_lines = config_description_lines(desc_row); diff --git a/src/ui/view/pipeline/main_view.rs b/src/ui/view/pipeline/main_view.rs index b5f076c..69ab540 100644 --- a/src/ui/view/pipeline/main_view.rs +++ b/src/ui/view/pipeline/main_view.rs @@ -34,75 +34,120 @@ fn is_atomic_instr(slot: &crate::ui::pipeline::PipeSlot) -> bool { ) } -pub fn render_pipeline_main(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; - - // The main EX view always shows the functional units so students can - // reason about potential parallelism even when execution is serialized. - let stages_h: u16 = 9; - let max_trace_rows = area - .height - .saturating_sub(stages_h) - .saturating_sub(5) - .clamp(3, 8); - let trace_rows = p.hazard_traces.len().min(max_trace_rows as usize) as u16; - let legend_rows = if p.hazard_traces.is_empty() { 0 } else { 1 }; - let msg_rows = if p.hazard_msgs.is_empty() { - 1 - } else { - p.hazard_msgs.len().min(2) as u16 - }; - let remaining_after_stages = area.height.saturating_sub(stages_h); - let hazards_cap = remaining_after_stages.saturating_sub(3); - let hazards_h: u16 = if hazards_cap == 0 { - 0 +/// Vertical budget for the Main subtab: compact strips on top, HISTORY gets +/// the remainder. Shared with the tutorial so highlight rects stay in sync. +pub(crate) struct MainLayoutPlan { + pub(crate) stages_h: u16, + pub(crate) fu_h: u16, + pub(crate) hazards_h: u16, + pub(crate) collapsed: bool, +} + +/// Height ladder (h = content height below the header): +/// h ≥ 18 — stage strip 5, FU strip 1, hazards up to 4 +/// 12 ≤ h < 18 — FU detail folds into the EX column title, hazards ≤ 3 +/// 9 ≤ h < 12 — stage columns drop to 2 inner lines, hazards ≤ 2 +/// h < 9 — stages+hazards collapse to one status line +/// Widths under 72 also drop the third stage line (class+regs). +pub(crate) fn plan_main_layout(h: u16, w: u16, n_traces: usize) -> MainLayoutPlan { + if h < 9 { + return MainLayoutPlan { + stages_h: 1, + fu_h: 0, + hazards_h: 0, + collapsed: true, + }; + } + let rows_cap: u16 = if h >= 18 { + 3 + } else if h >= 12 { + 2 } else { - (2 + trace_rows + legend_rows + msg_rows) - .min(hazards_cap) - .clamp(1, 13) + 1 }; + let rows = (n_traces.max(1) as u16).min(rows_cap); + MainLayoutPlan { + stages_h: if h >= 12 && w >= 72 { 5 } else { 4 }, + fu_h: if h >= 18 { 1 } else { 0 }, + hazards_h: 1 + rows, + collapsed: false, + } +} + +pub fn render_pipeline_main(f: &mut Frame, area: Rect, app: &App) { + let p = &app.run.pipeline(); + let plan = plan_main_layout(area.height, area.width, p.hazard_traces.len()); + + if plan.collapsed { + let chunks = Layout::default() + .direction(Direction::Vertical) + .constraints([Constraint::Length(1), Constraint::Min(1)]) + .split(area); + render_collapsed_strip(f, chunks[0], app); + render_gantt(f, chunks[1], app); + return; + } - // Layout: stages | hazards (3) | gantt (rest) let chunks = Layout::default() .direction(Direction::Vertical) .constraints([ - Constraint::Length(stages_h), - Constraint::Length(hazards_h), + Constraint::Length(plan.stages_h), + Constraint::Length(plan.fu_h), + Constraint::Length(plan.hazards_h), Constraint::Min(1), ]) .split(area); - render_stages(f, chunks[0], app); - render_hazards(f, chunks[1], app); - render_gantt(f, chunks[2], app); + render_stages(f, chunks[0], app, plan.fu_h == 0); + if plan.fu_h > 0 { + render_fu_strip(f, chunks[1], app); + } + render_hazards(f, chunks[2], app); + render_gantt(f, chunks[3], app); +} + +/// Sub-9-line fallback: one line summarizing the stages and hazard count so +/// the HISTORY gantt can keep the rest of a very short terminal. +fn render_collapsed_strip(f: &mut Frame, area: Rect, app: &App) { + let p = &app.run.pipeline(); + let stage_labels = ["IF", "ID", "EX", "MEM", "WB"]; + let mut spans: Vec> = vec![Span::raw(" ")]; + for (i, label) in stage_labels.iter().enumerate() { + if i > 0 { + spans.push(Span::styled(" │ ", Style::default().fg(theme::BORDER))); + } + spans.push(Span::styled( + format!("{label} "), + Style::default().fg(theme::LABEL_Y).add_modifier(Modifier::BOLD), + )); + let text = match p.stages[i].as_ref() { + None => "—".to_string(), + Some(s) if s.is_bubble => "◦".to_string(), + Some(s) => s.disasm.split_whitespace().next().unwrap_or("?").to_string(), + }; + spans.push(Span::styled(text, Style::default().fg(theme::TEXT))); + } + if !p.hazard_traces.is_empty() { + spans.push(Span::styled( + format!(" · {} hazards", p.hazard_traces.len()), + Style::default().fg(theme::PAUSED), + )); + } + f.render_widget(Paragraph::new(Line::from(spans)), area); } // ── 5-stage boxes ───────────────────────────────────────────────────────────── -fn render_stages(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; +fn render_stages(f: &mut Frame, area: Rect, app: &App, fu_in_ex_title: bool) { + let p = &app.run.pipeline(); let stage_labels = ["IF", "ID", "EX", "MEM", "WB"]; - // EX always gets the expanded functional-unit panel. - let constraints: Vec = vec![ - Constraint::Ratio(1, 7), - Constraint::Ratio(1, 7), - Constraint::Ratio(3, 7), - Constraint::Ratio(1, 7), - Constraint::Ratio(1, 7), - ]; - let cols = Layout::default() .direction(Direction::Horizontal) - .constraints(constraints) + .constraints([Constraint::Ratio(1, 5); 5]) .split(area); for (i, _stage) in Stage::all().iter().enumerate() { - if i == Stage::EX as usize { - render_fu_box(f, cols[i], app); - continue; - } - let slot = p.stages[i].as_ref(); let mut stage_badges = stage_status_badges(p, i, slot); @@ -115,13 +160,25 @@ fn render_stages(f: &mut Frame, area: Rect, app: &App) { None => Style::default().fg(theme::BORDER), }; - let title_label = match slot { + let mut title_label = match slot { Some(s) if s.is_speculative && !s.is_bubble => format!("{} ⟪P⟫", stage_labels[i]), Some(s) if s.hazard == Some(HazardType::BranchFlush) => { format!("{} ⟪X⟫", stage_labels[i]) } _ => stage_labels[i].to_string(), }; + // With the FU strip folded away, surface overall FU occupancy here so + // functional-unit pressure stays visible. + if i == Stage::EX as usize && fu_in_ex_title { + let busy = p + .fu_bank + .iter() + .flatten() + .filter(|fu| fu.slot.as_ref().is_some_and(|s| !s.is_bubble)) + .count(); + let cap: u8 = p.fu_capacity.iter().sum(); + title_label = format!("{title_label} {busy}/{cap}"); + } let block = Block::default() .borders(Borders::ALL) @@ -133,7 +190,9 @@ fn render_stages(f: &mut Frame, area: Rect, app: &App) { let inner = block.inner(cols[i]); f.render_widget(block, cols[i]); - let content_area = inner; + if inner.height == 0 || inner.width == 0 { + continue; + } let lines = match slot { None => vec![Line::from(Span::styled( @@ -152,7 +211,7 @@ fn render_stages(f: &mut Frame, area: Rect, app: &App) { let dim = Style::default() .fg(Color::DarkGray) .add_modifier(Modifier::DIM); - let mut lines = vec![ + vec![ Line::from(Span::styled(format!("0x{:04X}", s.pc), dim)), Line::from(Span::styled( "⊘ invalid", @@ -161,98 +220,118 @@ fn render_stages(f: &mut Frame, area: Rect, app: &App) { .add_modifier(Modifier::DIM), )), Line::from(Span::styled(format!(".word 0x{:08x}", s.word), dim)), - ]; - if content_area.height >= 4 { - lines.push(Line::from(Span::styled("(ignored)", dim))); - } - lines + ] } - Some(s) => { - let w = inner.width as usize; - let pc_str = format!("0x{:04X}", s.pc); - let class_color = s.class.color(); - let hazard_indicator = s.hazard.map(|h| { - Span::styled( - format!(" ⚠{}", compact_stage_hazard_label(i, Some(s), h)), - Style::default().fg(h.color()), - ) - }); - let pred_badge = speculative_compact_badge(s, w); - let pred_w = pred_badge - .as_ref() - .map_or(0, |(label, _)| UnicodeWidthStr::width(label.as_str())); - trim_stage_badges_to_fit(&mut stage_badges, w, pred_w); - let badge_w: usize = stage_badges - .iter() - .map(|(label, _)| UnicodeWidthStr::width(label.as_str())) - .sum(); - let disasm_w = w - .saturating_sub(badge_w) - .saturating_sub(pred_w) - .saturating_sub(1) - .max(4); - let (disasm_trunc, _) = s.disasm.unicode_truncate(disasm_w); - let mut disasm_spans = vec![Span::styled( - disasm_trunc.to_string(), - Style::default().fg(theme::TEXT), - )]; - if let Some(h) = hazard_indicator.filter(|_| stage_badges.is_empty()) { - disasm_spans.push(h); - } - disasm_spans.extend( - stage_badges - .iter() - .map(|(label, style)| Span::styled(label.clone(), *style)), - ); - if let Some((label, style)) = pred_badge { - disasm_spans.push(Span::styled(label, style)); - } + Some(s) => stage_slot_lines(i, s, inner, &mut stage_badges), + }; - // rd/rs1/rs2 - let reg_str = { - let mut parts = Vec::new(); - if let Some(rd) = s.rd { - parts.push(format!("rd={}", reg_name(rd))); - } - if let Some(rs1) = s.rs1 { - parts.push(format!("rs1={}", reg_name(rs1))); - } - if let Some(rs2) = s.rs2 { - parts.push(format!("rs2={}", reg_name(rs2))); - } - parts.join(" ") - }; + let visible_lines: Vec<_> = lines.into_iter().take(inner.height as usize).collect(); + f.render_widget(Paragraph::new(visible_lines), inner); + } +} - let mut lines = vec![ - Line::from(Span::styled(pc_str, Style::default().fg(theme::LABEL))), - Line::from(disasm_spans), - Line::from(Span::styled( - format!("[{}]", s.class.label()), - Style::default().fg(class_color).add_modifier(Modifier::DIM), - )), - ]; - if content_area.height >= 5 { - if let Some(pred_line) = speculative_detail_line(s, w) { - lines.push(pred_line); - } - } - if content_area.height >= 4 && !reg_str.is_empty() { - lines.push(Line::from(Span::styled( - reg_str, - Style::default().fg(theme::BORDER), - ))); - } - lines - } - }; +/// Compact stage-column body. Three inner lines when there is room +/// (PC + badges / disasm / class + regs), two when squeezed +/// (disasm + badges / PC + regs). +fn stage_slot_lines( + stage_idx: usize, + s: &crate::ui::pipeline::PipeSlot, + inner: Rect, + stage_badges: &mut Vec<(String, Style)>, +) -> Vec> { + let w = inner.width as usize; + let pc_str = format!("0x{:04X}", s.pc); + let hazard_indicator = s.hazard.map(|h| { + Span::styled( + format!(" ⚠{}", compact_stage_hazard_label(stage_idx, Some(s), h)), + Style::default().fg(h.color()), + ) + }); + let pred_badge = speculative_compact_badge(s, w); + let pred_w = pred_badge + .as_ref() + .map_or(0, |(label, _)| UnicodeWidthStr::width(label.as_str())); + trim_stage_badges_to_fit(stage_badges, w, pred_w); + + let mut badge_spans: Vec> = Vec::new(); + if let Some(h) = hazard_indicator.filter(|_| stage_badges.is_empty()) { + badge_spans.push(h); + } + badge_spans.extend( + stage_badges + .iter() + .map(|(label, style)| Span::styled(label.clone(), *style)), + ); + if let Some((label, style)) = pred_badge { + badge_spans.push(Span::styled(label, style)); + } - if content_area.height > 0 { - let visible_lines: Vec<_> = lines - .into_iter() - .take(content_area.height as usize) - .collect(); - f.render_widget(Paragraph::new(visible_lines), content_area); + let reg_str = compact_reg_summary(s); + + if inner.height >= 3 { + // PC + badges / disasm / class + regs + let mut pc_spans = vec![Span::styled(pc_str, Style::default().fg(theme::LABEL))]; + pc_spans.extend(badge_spans); + + let (disasm_trunc, _) = s.disasm.unicode_truncate(w.max(4)); + let mut class_spans = vec![Span::styled( + format!("[{}]", s.class.label()), + Style::default() + .fg(s.class.color()) + .add_modifier(Modifier::DIM), + )]; + if !reg_str.is_empty() { + class_spans.push(Span::styled( + format!(" {reg_str}"), + Style::default().fg(theme::BORDER), + )); } + vec![ + Line::from(pc_spans), + Line::from(Span::styled( + disasm_trunc.to_string(), + Style::default().fg(theme::TEXT), + )), + Line::from(class_spans), + ] + } else { + // disasm + badges / PC + regs + let badge_w: usize = stage_badges + .iter() + .map(|(label, _)| UnicodeWidthStr::width(label.as_str())) + .sum(); + let disasm_w = w + .saturating_sub(badge_w) + .saturating_sub(pred_w) + .saturating_sub(1) + .max(4); + let (disasm_trunc, _) = s.disasm.unicode_truncate(disasm_w); + let mut disasm_spans = vec![Span::styled( + disasm_trunc.to_string(), + Style::default().fg(theme::TEXT), + )]; + disasm_spans.extend(badge_spans); + + let mut pc_spans = vec![Span::styled(pc_str, Style::default().fg(theme::LABEL))]; + if !reg_str.is_empty() { + pc_spans.push(Span::styled( + format!(" {reg_str}"), + Style::default().fg(theme::BORDER), + )); + } + vec![Line::from(disasm_spans), Line::from(pc_spans)] + } +} + +/// "a0←a1,a2"-style register summary for a stage column. +fn compact_reg_summary(s: &crate::ui::pipeline::PipeSlot) -> String { + let srcs: Vec<&str> = [s.rs1, s.rs2].iter().flatten().map(|r| reg_name(*r)).collect(); + let srcs = srcs.join(","); + match (s.rd, srcs.is_empty()) { + (Some(rd), false) => format!("{}←{}", reg_name(rd), srcs), + (Some(rd), true) => reg_name(rd).to_string(), + (None, false) => srcs, + (None, true) => String::new(), } } @@ -378,46 +457,33 @@ fn trim_stage_badges_to_fit(stage_badges: &mut Vec<(String, Style)>, width: usiz // ── EX box expandido (Functional Units mode) ────────────────────────────────── -fn render_fu_box(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; - let ex_slot = p.stages[Stage::EX as usize].as_ref(); - - // Borda colorida baseada no estado do EX slot - let border_style = match ex_slot { - Some(s) if s.hazard.is_some() => Style::default().fg(s.hazard.unwrap().color()), - Some(s) if s.is_bubble => Style::default().fg(theme::PAUSED), - Some(_) => Style::default().fg(theme::ACCENT), - None => Style::default().fg(theme::BORDER), - }; - - let block = Block::default() - .borders(Borders::ALL) - .border_style(border_style) - .title(Span::styled( - "EX (UFs)", - border_style.add_modifier(Modifier::BOLD), - )); - - let inner = block.inner(area); - f.render_widget(block, area); - let fu_area = inner; +/// One-line functional-unit strip: every FU stays visible as `LABEL busy/cap` +/// (with a mini progress bar on wide terminals), and the first active FU's +/// instruction is echoed at the end of the line. +/// Below this width the FU strip drops the mini bars and trailing disasm note, +/// keeping only `LABEL busy/cap` per unit. +pub(crate) fn fu_strip_is_compact(w: u16) -> bool { + w < 90 +} - // Lista de UFs: (nome, classe que a ocupa, latência via CPI config) +fn render_fu_strip(f: &mut Frame, area: Rect, app: &App) { + let p = &app.run.pipeline(); let cpi = &app.run.cpi_config; - let fu_defs = FuKind::all(); + let ex_slot = p.stages[Stage::EX as usize].as_ref(); + let wide = !fu_strip_is_compact(area.width); - // Cada FU ocupa uma linha dentro do box - let row_constraints: Vec = fu_defs.iter().map(|_| Constraint::Length(1)).collect(); - let rows = Layout::default() - .direction(Direction::Vertical) - .constraints(row_constraints) - .split(fu_area); + let mut spans: Vec> = vec![Span::styled( + " FU ", + Style::default() + .fg(theme::LABEL_Y) + .add_modifier(Modifier::BOLD), + )]; + let mut active_note: Option = None; - for (i, fu_kind) in fu_defs.iter().copied().enumerate() { - if i >= rows.len() { - break; + for (idx, fu_kind) in FuKind::all().iter().copied().enumerate() { + if idx > 0 { + spans.push(Span::styled(" · ", Style::default().fg(theme::BORDER))); } - let fu_states = &p.fu_bank[fu_kind.index()]; let (parallel_slots, mirrored_ex_slot) = split_fu_activity(fu_kind, fu_states, ex_slot); let active_slots: Vec<_> = parallel_slots @@ -425,14 +491,9 @@ fn render_fu_box(f: &mut Frame, area: Rect, app: &App) { .copied() .chain(mirrored_ex_slot.into_iter()) .collect(); - let active_slot = active_slots.first().copied(); - let is_active = active_slot.is_some(); let capacity = p.fu_capacity[fu_kind.index()].max(1); - let parallel_count = parallel_slots.len(); - let shows_ex_mirror = mirrored_ex_slot.is_some(); - let line = if is_active { - let s = active_slot.unwrap(); + if let Some(s) = active_slots.first().copied() { let latency_class = match fu_kind { FuKind::Alu => s.class, FuKind::Mul => InstrClass::Mul, @@ -444,91 +505,58 @@ fn render_fu_box(f: &mut Frame, area: Rect, app: &App) { }, FuKind::Sys => InstrClass::System, }; - let total = fu_latency_for_class(latency_class, cpi); + let total = fu_latency_for_class(latency_class, cpi).max(1); let done = total.saturating_sub(s.fu_cycles_left); - // Barra de progresso - let bar_w = (rows[i].width as usize).saturating_sub(26).min(12).max(4); - let filled = if total > 1 { - ((done as usize) * bar_w / (total as usize)).min(bar_w) - } else { - bar_w - }; - let bar: String = (0..bar_w) - .map(|j| if j < filled { '█' } else { '░' }) - .collect(); - - let w = rows[i].width as usize; - let disasm_w = w.saturating_sub(26); - let summary = if active_slots.len() > 1 { - format!("{} (+{} more)", s.disasm, active_slots.len() - 1) - } else { - s.disasm.clone() - }; - let (disasm_trunc, _) = summary.unicode_truncate(disasm_w); - let occupancy_label = if shows_ex_mirror && parallel_count == 0 { - format!(" EX {}/{} ", parallel_count, capacity) - } else { - format!(" {}/{} {}/{} ", done + 1, total, parallel_count, capacity) - }; - - Line::from(vec![ - Span::styled( - format!("{:<3} ", fu_kind.label()), - Style::default() - .fg(s.class.color()) - .add_modifier(Modifier::BOLD), - ), - Span::styled( - format!("{: 1 { + ((done as usize) * 3 / (total as usize)).min(3) + } else { + 3 + }; + let bar: String = (0..3).map(|j| if j < filled { '▰' } else { '▱' }).collect(); + spans.push(Span::styled( + format!(" {bar}"), + Style::default().fg(theme::RUNNING), + )); + } + if active_note.is_none() { + let summary = if active_slots.len() > 1 { + format!("{} (+{} more)", s.disasm, active_slots.len() - 1) + } else { + s.disasm.clone() + }; + active_note = Some(format!("{summary} ({}/{total})", done + 1)); + } } else { - Line::from(vec![ - Span::styled( - format!("{:<3} ", fu_kind.label()), - Style::default().fg(theme::BORDER), - ), - Span::styled( - format!("— 0/{capacity}"), - Style::default() - .fg(theme::BORDER) - .add_modifier(Modifier::DIM), - ), - ]) - }; + spans.push(Span::styled( + fu_kind.label().to_string(), + Style::default().fg(theme::BORDER), + )); + spans.push(Span::styled( + format!(" 0/{capacity}"), + Style::default() + .fg(theme::BORDER) + .add_modifier(Modifier::DIM), + )); + } + } - f.render_widget(Paragraph::new(line), rows[i]); + if let Some(note) = active_note.filter(|_| wide) { + spans.push(Span::raw(" ")); + spans.push(Span::styled(note, Style::default().fg(theme::TEXT))); } + + f.render_widget(Paragraph::new(Line::from(spans)), area); } fn slot_belongs_to_fu_kind(slot: &crate::ui::pipeline::PipeSlot, fu_kind: FuKind) -> bool { @@ -579,304 +607,88 @@ fn split_fu_activity<'a>( // ── Hazard messages ──────────────────────────────────────────────────────────── fn render_hazards(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; + let p = &app.run.pipeline(); let block = Block::default() .borders(Borders::TOP) .border_style(Style::default().fg(theme::BORDER)) .title(Span::styled( - " Hazard / Forwarding Map ", + " Hazards / Forwarding ", Style::default().fg(theme::LABEL_Y), )); let inner = block.inner(area); f.render_widget(block, area); + if inner.height == 0 || inner.width == 0 { + return; + } - let compact = hazard_map_is_compact(inner); - let cols = if compact { - Layout::default() - .direction(Direction::Horizontal) - .constraints([Constraint::Length(7), Constraint::Min(24)]) - .split(inner) - } else { - Layout::default() - .direction(Direction::Horizontal) - .constraints([ - Constraint::Length(7), - Constraint::Percentage(50), - Constraint::Percentage(50), - ]) - .split(inner) - }; - - let kind_area = cols[0]; - let map_area = cols[1]; - let info_area = if compact { cols[1] } else { cols[2] }; - - let row_cap = map_area.height.saturating_sub(1) as usize; - let mut kind_lines: Vec> = vec![trace_kind_header(kind_area.width as usize)]; - let mut map_lines: Vec> = vec![if compact { - compact_trace_header(map_area.width as usize) - } else { - stage_trace_header(map_area.width as usize) - }]; - let mut info_lines: Vec> = if compact { - Vec::new() - } else { - vec![trace_detail_header(info_area.width as usize)] - }; + let rows_avail = inner.height as usize; + let w = inner.width as usize; + let dim = Style::default() + .fg(theme::LABEL) + .add_modifier(Modifier::DIM); + let mut lines: Vec> = Vec::new(); if p.hazard_traces.is_empty() { - kind_lines.push(Line::from(Span::styled( - " —", - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - ))); - map_lines.push(Line::from(Span::styled( - if p.halted { - " ✓ Halted" - } else if p.faulted { - " ✗ Fault" - } else { - " No active links" - }, - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - ))); - let fallback = if p.hazard_msgs.is_empty() { - " No textual hazard notes this cycle".to_string() + let status = if p.halted { + " ✓ Halted" + } else if p.faulted { + " ✗ Fault" } else { - p.hazard_msgs[0].1.clone() + " No active links" }; - if compact { - let (trunc, _) = fallback.unicode_truncate(map_area.width.saturating_sub(2) as usize); - map_lines.push(Line::from(Span::styled( - format!(" {}", trunc), - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - ))); - } else { - let (trunc, _) = fallback.unicode_truncate(info_area.width.saturating_sub(2) as usize); - info_lines.push(Line::from(Span::styled( - format!(" {}", trunc), - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - ))); + let mut text = status.to_string(); + if let Some((_, msg)) = p.hazard_msgs.first() { + text.push_str(" · "); + text.push_str(msg); } + let (trunc, _) = text.unicode_truncate(w); + lines.push(Line::from(Span::styled(trunc.to_string(), dim))); } else { - let mut rendered = 0usize; - for (i, trace) in p.hazard_traces.iter().enumerate() { - if rendered >= row_cap { - break; - } + let n = p.hazard_traces.len(); + let shown = if n > rows_avail { + rows_avail.saturating_sub(1) + } else { + n + }; + for trace in p.hazard_traces.iter().take(shown) { let detail = trace_detail_for(trace, &p.hazard_msgs); - kind_lines.push(render_trace_kind_line(trace, kind_area.width as usize)); - map_lines.push(if compact { - render_compact_trace_line(trace, &detail, map_area.width as usize) - } else { - render_trace_map_line(trace, map_area.width as usize) - }); - if !compact { - info_lines.push(render_trace_detail_line( - trace, - &detail, - info_area.width as usize, - )); - } - rendered += 1; - if i + 1 == p.hazard_traces.len() && rendered < row_cap { - kind_lines.push(Line::from(Span::styled( - " i", - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - ))); - map_lines.push(if compact { - render_compact_trace_legend(map_area.width as usize) - } else { - Line::from(Span::styled(" ", Style::default())) - }); - if !compact { - info_lines.push(render_trace_legend(info_area.width as usize)); - } - } + lines.push(render_hazard_row(trace, &detail, w)); } - } - - f.render_widget(Paragraph::new(kind_lines), kind_area); - f.render_widget(Paragraph::new(map_lines), map_area); - if !compact { - f.render_widget(Paragraph::new(info_lines), info_area); - } -} - -fn hazard_map_is_compact(area: Rect) -> bool { - area.width < 72 -} - -fn trace_kind_header(width: usize) -> Line<'static> { - let label = format!("{: Line<'static> { - let label = format!("{: Line<'static> { - let text = format!("[{}]", trace.kind.short_label()); - let mut padded = format!("{: width { - padded.truncate(width); - } - Line::from(Span::styled(padded, trace_badge_style(trace.kind))) -} - -fn stage_trace_header(width: usize) -> Line<'static> { - let stage_cols = trace_stage_cols(width); - let mut chars = vec![' '; width.max(1)]; - for &col in &stage_cols { - if col < chars.len() { - chars[col] = '│'; - } - } - for (label, col) in [ - ("IF", stage_cols[0]), - ("ID", stage_cols[1]), - ("EX", stage_cols[2]), - ("MEM", stage_cols[3]), - ("WB", stage_cols[4]), - ] { - for (i, ch) in label.chars().enumerate() { - if col + i < chars.len() { - chars[col + i] = ch; - } - } - } - Line::from(Span::styled( - chars.into_iter().collect::(), - Style::default() - .fg(theme::LABEL_Y) - .add_modifier(Modifier::BOLD), - )) -} - -fn compact_trace_header(width: usize) -> Line<'static> { - let label = format!("{: [usize; 5] { - let usable = width.saturating_sub(34).max(16); - let start = 2usize; - [ - start, - start + usable / 4, - start + usable / 2, - start + usable * 3 / 4, - start + usable, - ] -} - -fn render_trace_map_line(trace: &HazardTrace, width: usize) -> Line<'static> { - let width = width.max(20); - let cols = trace_stage_cols(width); - let from_col = cols[trace.from_stage.min(4)]; - let to_col = cols[trace.to_stage.min(4)]; - let mut chars = vec![' '; width]; - - for &col in &cols { - if col < width { - chars[col] = '┆'; - } - } - - if from_col == to_col { - if from_col < width { - chars[from_col] = '●'; - } - } else if from_col < to_col { - let (src_char, line_char, dst_char) = trace_glyphs(trace.kind, true); - if from_col < width { - chars[from_col] = src_char; - } - for ch in chars.iter_mut().take(to_col).skip(from_col + 1) { - *ch = line_char; - } - if to_col < width { - chars[to_col] = dst_char; - } - } else { - let (src_char, line_char, dst_char) = trace_glyphs(trace.kind, false); - if from_col < width { - chars[from_col] = src_char; - } - for ch in chars.iter_mut().take(from_col).skip(to_col + 1) { - *ch = line_char; - } - if to_col < width { - chars[to_col] = dst_char; + if n > shown { + let hidden = &p.hazard_traces[shown..]; + let fwd = hidden + .iter() + .filter(|t| matches!(t.kind, TraceKind::Forward)) + .count(); + let hzd = hidden.len() - fwd; + lines.push(Line::from(Span::styled( + format!(" +{} more ({fwd} FWD · {hzd} HZD)", hidden.len()), + dim, + ))); } } - for &col in &cols { - if col < width && chars[col] == ' ' { - chars[col] = '┆'; - } - } - let graphic = chars.into_iter().collect::(); - Line::from(Span::styled( - graphic, - Style::default() - .fg(trace.kind.color()) - .add_modifier(Modifier::BOLD), - )) -} - -fn render_compact_trace_line(trace: &HazardTrace, detail: &str, width: usize) -> Line<'static> { - let text = format!("{} {}", trace_stage_summary(trace), detail); - let (trunc, _) = text.unicode_truncate(width.max(12)); - Line::from(Span::styled( - trunc.to_string(), - Style::default() - .fg(trace.kind.color()) - .add_modifier(Modifier::BOLD), - )) + f.render_widget(Paragraph::new(lines), inner); } -fn render_trace_detail_line(trace: &HazardTrace, detail: &str, width: usize) -> Line<'static> { - let prefix = match trace.kind { - TraceKind::Forward => "bypass ", - TraceKind::Hazard(HazardType::LoadUse) => "stall ", - TraceKind::Hazard(HazardType::BranchFlush) => "squash ", - TraceKind::Hazard(HazardType::MemLatency) => "wait ", - _ => "hazard ", - }; - let text = format!("{}{}", prefix, detail); - let (trunc, _) = text.unicode_truncate(width.max(8)); - Line::from(Span::styled( - trunc.to_string(), - Style::default().fg(theme::TEXT), - )) +/// One hazard/forwarding link per line: `[FWD] MEM -> EX detail…`. +fn render_hazard_row(trace: &HazardTrace, detail: &str, width: usize) -> Line<'static> { + let badge = format!("[{}]", trace.kind.short_label()); + let route = format!(" {:<10}", trace_stage_summary(trace)); + let used = 1 + UnicodeWidthStr::width(badge.as_str()) + UnicodeWidthStr::width(route.as_str()); + let (detail_trunc, _) = detail.unicode_truncate(width.saturating_sub(used).max(8)); + Line::from(vec![ + Span::raw(" "), + Span::styled(badge, trace_badge_style(trace.kind)), + Span::styled( + route, + Style::default() + .fg(trace.kind.color()) + .add_modifier(Modifier::BOLD), + ), + Span::styled(detail_trunc.to_string(), Style::default().fg(theme::TEXT)), + ]) } fn trace_detail_for(trace: &HazardTrace, hazard_msgs: &[(HazardType, String)]) -> String { @@ -896,17 +708,6 @@ fn trace_detail_for(trace: &HazardTrace, hazard_msgs: &[(HazardType, String)]) - } } -fn render_compact_trace_legend(width: usize) -> Line<'static> { - let text = " path producer -> consumer"; - let (trunc, _) = text.unicode_truncate(width.max(8)); - Line::from(Span::styled( - trunc.to_string(), - Style::default() - .fg(theme::LABEL) - .add_modifier(Modifier::DIM), - )) -} - fn trace_stage_summary(trace: &HazardTrace) -> String { format!( "{} -> {}", @@ -925,126 +726,6 @@ fn stage_name_from_idx(idx: usize) -> &'static str { } } -#[cfg(test)] -mod tests { - use super::{ - bubble_label_for_stage, cell_to_span, compact_stage_hazard_label, hazard_map_is_compact, - split_fu_activity, trace_stage_summary, - }; - use crate::ui::pipeline::{ - FuKind, FuState, GanttCell, HazardTrace, HazardType, PipeSlot, Stage, TraceKind, - }; - use ratatui::layout::Rect; - - #[test] - fn mem_latency_bubble_in_id_reads_as_waiting_for_if() { - let mut slot = PipeSlot::bubble(); - slot.hazard = Some(HazardType::MemLatency); - - assert_eq!( - bubble_label_for_stage(Stage::ID as usize, &slot), - "waiting for IF" - ); - assert_eq!( - compact_stage_hazard_label(Stage::ID as usize, Some(&slot), HazardType::MemLatency), - "UP" - ); - } - - #[test] - fn mem_latency_on_if_and_mem_uses_stage_specific_badges() { - let mut if_slot = PipeSlot::from_word(0, 0x0000_0013); - if_slot.hazard = Some(HazardType::MemLatency); - - let mut mem_slot = PipeSlot::from_word(4, 0x0000_0013); - mem_slot.hazard = Some(HazardType::MemLatency); - - assert_eq!( - compact_stage_hazard_label(Stage::IF as usize, Some(&if_slot), HazardType::MemLatency), - "IFWT" - ); - assert_eq!( - compact_stage_hazard_label( - Stage::MEM as usize, - Some(&mem_slot), - HazardType::MemLatency - ), - "MEMWT" - ); - } - - #[test] - fn hazard_map_switches_to_compact_layout_when_width_is_tight() { - assert!(hazard_map_is_compact(Rect::new(0, 0, 71, 5))); - assert!(!hazard_map_is_compact(Rect::new(0, 0, 72, 5))); - } - - #[test] - fn compact_trace_summary_uses_stage_route_labels() { - let trace = HazardTrace { - kind: TraceKind::Forward, - from_stage: Stage::MEM as usize, - to_stage: Stage::EX as usize, - detail: "BYPASS".to_string(), - }; - - assert_eq!(trace_stage_summary(&trace), "MEM -> EX"); - } - - #[test] - fn gantt_fu_cells_render_as_ex_in_history() { - assert_eq!(cell_to_span(GanttCell::InFu(FuKind::Mul)).0, "EX"); - assert_eq!(cell_to_span(GanttCell::SpeculativeFu(FuKind::Lsu)).0, "EX"); - assert_eq!(cell_to_span(GanttCell::Stall).0, "──"); - } - - #[test] - fn split_fu_activity_excludes_serial_ex_mirror_from_parallel_count() { - let mut ex_slot = PipeSlot::from_word(0, 0x0000_0013); - ex_slot.seq = 7; - - let fu_states = vec![FuState { - kind: Some(FuKind::Alu), - slot: Some(ex_slot.clone()), - busy_cycles_left: 0, - }]; - - let (parallel, mirror) = split_fu_activity(FuKind::Alu, &fu_states, Some(&ex_slot)); - assert!(parallel.is_empty()); - assert!(mirror.is_some()); - } -} - -fn render_trace_legend(width: usize) -> Line<'static> { - let compact = width < 72; - Line::from(vec![ - Span::styled(" ", Style::default()), - Span::styled("[FWD]", trace_badge_style(TraceKind::Forward)), - Span::styled( - if compact { - " bypass path " - } else { - " producer bypass path into consumer " - }, - Style::default().fg(theme::TEXT), - ), - Span::styled( - "[HZD]", - Style::default() - .fg(theme::PAUSED) - .add_modifier(Modifier::BOLD), - ), - Span::styled( - if compact { - " dependency / squash" - } else { - " dependency, stall, or squash path" - }, - Style::default().fg(theme::TEXT), - ), - ]) -} - fn speculative_compact_badge( slot: &crate::ui::pipeline::PipeSlot, width: usize, @@ -1068,28 +749,6 @@ fn speculative_compact_badge( )) } -fn speculative_detail_line( - slot: &crate::ui::pipeline::PipeSlot, - width: usize, -) -> Option> { - if !slot.is_speculative { - return None; - } - let target = slot.predicted_target.unwrap_or(slot.pc.wrapping_add(4)); - let summary = if slot.predicted_taken { - format!("⟪pred taken -> 0x{:04X}⟫", target) - } else { - format!("⟪pred not-taken -> 0x{:04X}⟫", target) - }; - let (trunc, _) = summary.unicode_truncate(width.max(8)); - Some(Line::from(Span::styled( - trunc.to_string(), - Style::default() - .fg(theme::PAUSED) - .add_modifier(Modifier::DIM), - ))) -} - fn trace_badge_style(kind: TraceKind) -> Style { Style::default() .fg(kind.color()) @@ -1097,36 +756,10 @@ fn trace_badge_style(kind: TraceKind) -> Style { .add_modifier(Modifier::BOLD) } -fn trace_glyphs(kind: TraceKind, forward_dir: bool) -> (char, char, char) { - match kind { - TraceKind::Forward => { - if forward_dir { - ('◆', '┈', '▶') - } else { - ('◆', '┈', '◀') - } - } - TraceKind::Hazard(HazardType::BranchFlush) => { - if forward_dir { - ('●', '═', '✕') - } else { - ('●', '═', '✕') - } - } - TraceKind::Hazard(_) => { - if forward_dir { - ('●', '━', '▶') - } else { - ('●', '━', '◀') - } - } - } -} - // ── Gantt diagram ───────────────────────────────────────────────────────────── fn render_gantt(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; + let p = &app.run.pipeline(); const LABEL_W: usize = 12; const CELL_W: usize = 4; @@ -1134,13 +767,15 @@ fn render_gantt(f: &mut Frame, area: Rect, app: &App) { let preview_cols = ((preview_inner_w.saturating_sub(LABEL_W)) / CELL_W).max(1); let visible_cols = preview_cols.min(crate::ui::pipeline::MAX_GANTT_COLS); + let scroll_hint = if p.gantt_scroll == 0 { + format!(" HISTORY up to {} cycles · following ", visible_cols) + } else { + format!(" HISTORY scrollback ↑{} · End=follow ", p.gantt_scroll) + }; let block = Block::default() .borders(Borders::ALL) .border_style(Style::default().fg(theme::BORDER)) - .title(Span::styled( - format!(" HISTORY up to {} cycles ", visible_cols), - Style::default().fg(theme::LABEL), - )); + .title(Span::styled(scroll_hint, Style::default().fg(theme::LABEL))); let inner = block.inner(area); p.gantt_area_rect @@ -1253,7 +888,7 @@ fn render_gantt(f: &mut Frame, area: Rect, app: &App) { fn cell_to_span(cell: GanttCell) -> (&'static str, Style) { // Speculative stages: same label as InStage but in orange — shows instruction // was fetched/decoded speculatively while a branch was unresolved. - let spec_style = Style::default().fg(Color::Rgb(220, 140, 40)); + let spec_style = Style::default().fg(theme::SPECULATIVE); match cell { GanttCell::Empty => ("·", Style::default().fg(theme::BORDER)), GanttCell::InStage(Stage::IF) => ("IF", Style::default().fg(theme::ACCENT)), @@ -1288,3 +923,180 @@ fn cell_to_span(cell: GanttCell) -> (&'static str, Style) { GanttCell::Flush => ("◀FL", Style::default().fg(theme::DANGER)), } } + +#[cfg(test)] +mod tests { + use super::{ + bubble_label_for_stage, cell_to_span, compact_stage_hazard_label, plan_main_layout, + split_fu_activity, trace_stage_summary, + }; + use crate::ui::pipeline::{ + FuKind, FuState, GanttCell, HazardTrace, HazardType, PipeSlot, Stage, TraceKind, + }; + + #[test] + fn mem_latency_bubble_in_id_reads_as_waiting_for_if() { + let mut slot = PipeSlot::bubble(); + slot.hazard = Some(HazardType::MemLatency); + + assert_eq!( + bubble_label_for_stage(Stage::ID as usize, &slot), + "waiting for IF" + ); + assert_eq!( + compact_stage_hazard_label(Stage::ID as usize, Some(&slot), HazardType::MemLatency), + "UP" + ); + } + + #[test] + fn mem_latency_on_if_and_mem_uses_stage_specific_badges() { + let mut if_slot = PipeSlot::from_word(0, 0x0000_0013); + if_slot.hazard = Some(HazardType::MemLatency); + + let mut mem_slot = PipeSlot::from_word(4, 0x0000_0013); + mem_slot.hazard = Some(HazardType::MemLatency); + + assert_eq!( + compact_stage_hazard_label(Stage::IF as usize, Some(&if_slot), HazardType::MemLatency), + "IFWT" + ); + assert_eq!( + compact_stage_hazard_label( + Stage::MEM as usize, + Some(&mem_slot), + HazardType::MemLatency + ), + "MEMWT" + ); + } + + #[test] + fn main_layout_gives_history_the_remainder_at_full_height() { + let plan = plan_main_layout(30, 120, 2); + assert!(!plan.collapsed); + assert_eq!(plan.stages_h, 5); + assert_eq!(plan.fu_h, 1); + assert_eq!(plan.hazards_h, 1 + 2); + // HISTORY inherits 30 - 9 = 21 lines (~70%). + assert!(30 - plan.stages_h - plan.fu_h - plan.hazards_h >= 18); + } + + #[test] + fn main_layout_folds_fu_strip_then_stage_lines_as_height_shrinks() { + let mid = plan_main_layout(14, 120, 5); + assert_eq!(mid.fu_h, 0); + assert_eq!(mid.stages_h, 5); + assert_eq!(mid.hazards_h, 1 + 2); + + let short = plan_main_layout(10, 120, 5); + assert_eq!(short.stages_h, 4); + assert_eq!(short.hazards_h, 1 + 1); + + let tiny = plan_main_layout(8, 120, 5); + assert!(tiny.collapsed); + } + + #[test] + fn main_layout_drops_third_stage_line_on_narrow_widths() { + assert_eq!(plan_main_layout(30, 71, 0).stages_h, 4); + assert_eq!(plan_main_layout(30, 72, 0).stages_h, 5); + } + + #[test] + fn fu_strip_goes_compact_below_ninety_columns() { + assert!(super::fu_strip_is_compact(89)); + assert!(!super::fu_strip_is_compact(90)); + } + + #[test] + fn pipeline_tab_renders_at_every_compression_breakpoint_without_panicking() { + use ratatui::{Terminal, backend::TestBackend}; + use std::collections::VecDeque; + + let mut app = crate::ui::app::App::new(None); + app.editor.last_ok_text = Some(vec![0x0000_0013]); + { + let p = app.run.pipeline_mut(); + p.stages[0] = Some(PipeSlot::from_word(0, 0x0000_0013)); + p.stages[2] = Some(PipeSlot::from_word(8, 0x0000_0013)); + p.hazard_traces = (0..5) + .map(|i| HazardTrace { + kind: if i % 2 == 0 { + TraceKind::Forward + } else { + TraceKind::Hazard(HazardType::LoadUse) + }, + from_stage: Stage::MEM as usize, + to_stage: Stage::EX as usize, + detail: format!("trace {i}"), + }) + .collect(); + p.gantt = (0..12) + .map(|i| crate::ui::pipeline::GanttRow { + gantt_id: i + 1, + pc: (i * 4) as u32, + disasm: format!("addi x{i}, x{i}, 1"), + class: crate::ui::pipeline::InstrClass::Alu, + cells: VecDeque::from(vec![GanttCell::InStage(Stage::IF); 4]), + first_cycle: i, + done: false, + last_stage: None, + }) + .collect(); + } + + for (w, h) in [ + (120, 40), + (100, 30), + (80, 24), + (70, 20), + (60, 16), + (40, 10), + (20, 6), + ] { + let backend = TestBackend::new(w, h); + let mut terminal = Terminal::new(backend).expect("terminal"); + terminal + .draw(|f| { + crate::ui::view::pipeline::render_pipeline(f, f.area(), &app); + }) + .unwrap_or_else(|e| panic!("render at {w}x{h} failed: {e}")); + } + } + + #[test] + fn compact_trace_summary_uses_stage_route_labels() { + let trace = HazardTrace { + kind: TraceKind::Forward, + from_stage: Stage::MEM as usize, + to_stage: Stage::EX as usize, + detail: "BYPASS".to_string(), + }; + + assert_eq!(trace_stage_summary(&trace), "MEM -> EX"); + } + + #[test] + fn gantt_fu_cells_render_as_ex_in_history() { + assert_eq!(cell_to_span(GanttCell::InFu(FuKind::Mul)).0, "EX"); + assert_eq!(cell_to_span(GanttCell::SpeculativeFu(FuKind::Lsu)).0, "EX"); + assert_eq!(cell_to_span(GanttCell::Stall).0, "──"); + } + + #[test] + fn split_fu_activity_excludes_serial_ex_mirror_from_parallel_count() { + let mut ex_slot = PipeSlot::from_word(0, 0x0000_0013); + ex_slot.seq = 7; + + let fu_states = vec![FuState { + kind: Some(FuKind::Alu), + slot: Some(ex_slot.clone()), + busy_cycles_left: 0, + }]; + + let (parallel, mirror) = split_fu_activity(FuKind::Alu, &fu_states, Some(&ex_slot)); + assert!(parallel.is_empty()); + assert!(mirror.is_some()); + } +} diff --git a/src/ui/view/pipeline/mod.rs b/src/ui/view/pipeline/mod.rs index d6009bc..ba97ae8 100644 --- a/src/ui/view/pipeline/mod.rs +++ b/src/ui/view/pipeline/mod.rs @@ -1,39 +1,34 @@ mod config_view; mod main_view; +pub(crate) use main_view::{MainLayoutPlan, plan_main_layout}; + use crate::ui::app::App; use crate::ui::pipeline::PipelineSubtab; use crate::ui::theme; -use crate::ui::view::components::{dense_action, push_dense_pair}; +use crate::ui::view::components::{SpanRow, dense_action, dense_value}; use ratatui::{ Frame, layout::{Constraint, Direction, Layout, Rect}, prelude::*, - widgets::{Block, BorderType, Borders, Paragraph}, + widgets::Paragraph, }; pub fn render_pipeline(f: &mut Frame, area: Rect, app: &App) { - app.pipeline.gantt_area_rect.set((0, 0, 0, 0)); - if !matches!(app.pipeline.subtab, PipelineSubtab::Config) { - app.pipeline + app.run.pipeline().gantt_area_rect.set((0, 0, 0, 0)); + if !matches!(app.run.pipeline().subtab, PipelineSubtab::Config) { + app.run.pipeline() .config_row_rects .set([(0, 0, 0); crate::ui::pipeline::PipelineBypassConfig::CONFIG_ROWS]); } - // Layout: subtab_header (3) | exec_controls (4) | content (min) | controls (3) + // Layout: merged header (2) | content (min) let layout = Layout::default() .direction(Direction::Vertical) - .constraints([ - Constraint::Length(4), - Constraint::Length(5), - Constraint::Min(0), - Constraint::Length(3), - ]) + .constraints([Constraint::Length(2), Constraint::Min(0)]) .split(area); - render_subtab_header(f, layout[0], app); - render_exec_controls(f, layout[1], app); - render_controls_bar(f, layout[3], app); + render_header(f, layout[0], app); // When pipeline is disabled the sequential visualization is available; // fall through to the normal rendering path. @@ -49,91 +44,25 @@ pub fn render_pipeline(f: &mut Frame, area: Rect, app: &App) { Style::default().fg(theme::LABEL), )), ]); - f.render_widget(p, layout[2]); + f.render_widget(p, layout[1]); return; } - match app.pipeline.subtab { - PipelineSubtab::Main => main_view::render_pipeline_main(f, layout[2], app), - PipelineSubtab::Config => config_view::render_pipeline_config(f, layout[2], app), + match app.run.pipeline().subtab { + PipelineSubtab::Main => main_view::render_pipeline_main(f, layout[1], app), + PipelineSubtab::Config => config_view::render_pipeline_config(f, layout[1], app), } } -// ── Subtab header ───────────────────────────────────────────────────────────── +// ── Merged header ───────────────────────────────────────────────────────────── +// +// Two borderless lines replacing the old subtab / Execution / bottom-bar boxes: +// L1: title, subtab buttons, core/hart/status, speed/state/reset, file actions +// L2: cycle metrics + stall breakdown (+ sequential note / key hints) -fn render_subtab_header(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; +fn render_header(f: &mut Frame, area: Rect, app: &App) { + let p = &app.run.pipeline(); let single_core = app.max_cores <= 1; - - let main_style = subtab_style(p.subtab == PipelineSubtab::Main, p.hover_subtab_main); - let config_style = subtab_style(p.subtab == PipelineSubtab::Config, p.hover_subtab_config); - let core_text = format!("{}/{}", app.selected_core, app.max_cores.saturating_sub(1)); - let core_style = if single_core { - Style::default().fg(theme::LABEL) - } else if p.hover_core { - Style::default().fg(theme::ACTIVE).bold() - } else { - Style::default().fg(theme::TEXT).bold() - }; - - let line1 = Line::from(vec![ - Span::raw(" "), - Span::styled("main", main_style), - Span::raw(" "), - Span::styled("settings", config_style), - Span::styled(" core ", Style::default().fg(theme::LABEL)), - Span::styled(core_text.clone(), core_style), - Span::styled( - format!( - " / Hart {} / {}", - app.core_hart_id(app.selected_core) - .map(|id| id.to_string()) - .unwrap_or_else(|| "-".to_string()), - app.core_status(app.selected_core).label() - ), - Style::default().fg(theme::LABEL), - ), - ]); - let line2 = Line::from(vec![ - Span::raw(" "), - Span::styled("Tab to switch", Style::default().fg(theme::LABEL)), - ]); - - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme::BORDER)) - .title(Span::styled( - " Pipeline Simulator ", - Style::default().fg(theme::ACCENT).bold(), - )); - let inner = block.inner(area); - f.render_widget(block, area); - f.render_widget(Paragraph::new(vec![line1, line2]), inner); - - // Record button geometry for mouse: y=inner.y, x ranges - // "main" starts at x = inner.x + 1, "settings" starts at +8 - app.pipeline - .btn_subtab_main_rect - .set((inner.y, inner.x + 1, inner.x + 5)); - app.pipeline - .btn_subtab_config_rect - .set((inner.y, inner.x + 8, inner.x + 14)); - if single_core { - app.pipeline.btn_core_rect.set((0, 0, 0)); - } else { - let core_x = inner.x + 17; - let core_w = ("core ".len() + core_text.len()) as u16; - app.pipeline - .btn_core_rect - .set((inner.y, core_x, core_x + core_w)); - } -} - -// ── Exec controls ───────────────────────────────────────────────────────────── - -fn render_exec_controls(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; let state_clickable = !p.faulted; let (state_label, state_color) = if p.faulted { @@ -146,93 +75,157 @@ fn render_exec_controls(f: &mut Frame, area: Rect, app: &App) { ("pause", theme::PAUSED) }; - let mut spans = Vec::new(); - push_dense_pair( - &mut spans, - "speed", - p.speed.label(), - p.hover_speed, - true, - theme::TEXT, - ); - push_dense_pair( - &mut spans, - "state", + // ── Line 1: buttons ── + let mut row = SpanRow::new(area.x, area.y); + row.push(Span::styled( + " Pipeline ", + Style::default().fg(theme::ACCENT).bold(), + )); + row.gap(1); + + let start = row.cursor(); + row.push(Span::styled( + "main", + subtab_style(p.subtab == PipelineSubtab::Main, p.hover_subtab_main), + )); + row.record_hitbox(start, &p.btn_subtab_main_rect); + row.gap(2); + let start = row.cursor(); + row.push(Span::styled( + "settings", + subtab_style(p.subtab == PipelineSubtab::Config, p.hover_subtab_config), + )); + row.record_hitbox(start, &p.btn_subtab_config_rect); + + row.gap(3); + let core_style = if single_core { + Style::default().fg(theme::LABEL) + } else if p.hover_core { + Style::default().fg(theme::ACTIVE).bold() + } else { + Style::default().fg(theme::TEXT).bold() + }; + let start = row.cursor(); + row.push(Span::styled("core ", Style::default().fg(theme::LABEL))); + row.push(Span::styled( + format!("{}/{}", app.selected_core, app.max_cores.saturating_sub(1)), + core_style, + )); + if single_core { + p.btn_core_rect.set((0, 0, 0)); + } else { + row.record_hitbox(start, &p.btn_core_rect); + } + row.push(Span::styled( + format!( + " · hart {} · {}", + app.core_hart_id(app.selected_core) + .map(|id| id.to_string()) + .unwrap_or_else(|| "-".to_string()), + app.core_status(app.selected_core).label() + ), + Style::default().fg(theme::LABEL), + )); + + row.gap(3); + let start = row.cursor(); + row.push(Span::styled("speed ", Style::default().fg(theme::IDLE))); + row.push(dense_value(p.speed.label(), p.hover_speed, true, theme::TEXT)); + row.record_hitbox(start, &p.btn_speed_rect); + + row.gap(3); + let start = row.cursor(); + row.push(Span::styled("state ", Style::default().fg(theme::IDLE))); + row.push(dense_value( state_label, p.hover_state && state_clickable, state_clickable, state_color, - ); - spans.push(Span::raw(" ")); - spans.push(dense_action("reset", theme::DANGER, p.hover_reset)); - if p.sequential_mode { + )); + row.record_hitbox(start, &p.btn_state_rect); + + row.gap(3); + let start = row.cursor(); + row.push(dense_action("reset", theme::DANGER, p.hover_reset)); + row.record_hitbox(start, &p.btn_reset_rect); + + row.gap(3); + let start = row.cursor(); + row.push(dense_action("results", theme::ACCENT, p.hover_export_results)); + row.record_hitbox(start, &p.btn_export_results_rect); + + if matches!(p.subtab, PipelineSubtab::Config) { + row.gap(3); + let start = row.cursor(); + row.push(dense_action("import cfg", theme::METRIC_CYC, p.hover_import_cfg)); + row.record_hitbox(start, &p.btn_import_cfg_rect); + row.gap(3); + let start = row.cursor(); + row.push(dense_action("export cfg", theme::METRIC_CYC, p.hover_export_cfg)); + row.record_hitbox(start, &p.btn_export_cfg_rect); + } else { + p.btn_import_cfg_rect.set((0, 0, 0)); + p.btn_export_cfg_rect.set((0, 0, 0)); + } + let line1 = row.into_line(); + + // ── Line 2: metrics ── + let mut spans: Vec> = vec![Span::styled( + format!(" cyc {}", p.cycle_count), + Style::default().fg(theme::METRIC_CYC), + )]; + if p.instr_committed > 0 { + let cpi = p.cycle_count as f64 / p.instr_committed as f64; spans.push(Span::styled( - " Sequential (pipeline off) — one instruction at a time", - Style::default().fg(theme::PAUSED), + format!(" CPI {cpi:.2}"), + Style::default().fg(theme::METRIC_CPI), )); + let stalls = if header_drops_stall_breakdown(area.width) { + format!(" instr {} stalls {}", p.instr_committed, p.stall_count) + } else { + let [raw, lu, br, fu, mem] = p.stall_by_type; + format!( + " instr {} stalls {} (RAW {raw} · LD {lu} · BR {br} · FU {fu} · MEM {mem})", + p.instr_committed, p.stall_count + ) + }; + spans.push(Span::styled(stalls, Style::default().fg(theme::LABEL))); + if p.branches_executed > 0 { + let mispredict_pct = p.flush_count as f64 / p.branches_executed as f64 * 100.0; + spans.push(Span::styled( + format!( + " br {} · mispred {} ({mispredict_pct:.0}%)", + p.branches_executed, p.flush_count + ), + Style::default().fg(theme::LABEL), + )); + } } else { spans.push(Span::styled( - " r=reset f=speed s=step p/Space=run", + " (no instructions committed)", Style::default().fg(theme::LABEL), )); } - let line1 = Line::from(spans); - - let (cpi_str, stall_str) = if p.instr_committed > 0 { - let cpi = p.cycle_count as f64 / p.instr_committed as f64; - let branch_str = if p.branches_executed > 0 { - let mispredict_pct = p.flush_count as f64 / p.branches_executed as f64 * 100.0; - format!( - " control:{} mispred:{} ({:.0}%)", - p.branches_executed, p.flush_count, mispredict_pct - ) - } else { - String::new() - }; - let main = format!( - " Cycle:{} CPI:{cpi:.2} instrs:{} stalls:{}{}", - p.cycle_count, p.instr_committed, p.stall_count, branch_str, - ); - let [raw, lu, br, fu, mem] = p.stall_by_type; - let detail = - format!(" Stall tags — RAW:{raw} Load-Use:{lu} Branch:{br} FU:{fu} Mem:{mem}"); - (main, detail) + if p.sequential_mode { + spans.push(Span::styled( + " · Sequential (pipeline off)", + Style::default().fg(theme::PAUSED), + )); } else { - ( - format!(" Cycle:{} (no instructions committed)", p.cycle_count), - String::new(), - ) - }; - - let line2 = Line::from(Span::styled(cpi_str, Style::default().fg(theme::LABEL))); - let line3 = Line::from(Span::styled(stall_str, Style::default().fg(theme::LABEL))); + spans.push(Span::styled( + " s=step · p=run · r=reset · f=speed", + Style::default().fg(theme::LABEL).add_modifier(Modifier::DIM), + )); + } + let line2 = Line::from(spans); - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme::BORDER)) - .title(Span::styled("Execution", Style::default().fg(theme::LABEL))); - let inner = block.inner(area); - f.render_widget(block, area); - f.render_widget(Paragraph::new(vec![line1, line2, line3]), inner); + f.render_widget(Paragraph::new(vec![line1, line2]), area); +} - // Record button geometry for mouse - // Layout: "speed state reset" - // Offsets: speed_label at +6, state_label at +15+speed_w, reset at +18+speed_w+state_w - let speed_x = inner.x + 1; - let speed_label_w = ("speed ".len() + p.speed.label().len()) as u16; - app.pipeline - .btn_speed_rect - .set((inner.y, speed_x, speed_x + speed_label_w)); - let state_x = inner.x + 4 + speed_label_w; - let state_label_w = ("state ".len() + state_label.len()) as u16; - app.pipeline - .btn_state_rect - .set((inner.y, state_x, state_x + state_label_w)); - let reset_x = state_x + state_label_w + 3; - app.pipeline - .btn_reset_rect - .set((inner.y, reset_x, reset_x + 5)); +/// Below this width header line 2 shows only the stall total, without the +/// per-type breakdown. +fn header_drops_stall_breakdown(w: u16) -> bool { + w < 90 } fn subtab_style(active: bool, hovered: bool) -> Style { @@ -244,54 +237,3 @@ fn subtab_style(active: bool, hovered: bool) -> Style { Style::default().fg(theme::IDLE) } } - -fn render_controls_bar(f: &mut Frame, area: Rect, app: &App) { - let p = &app.pipeline; - let mut spans = vec![ - Span::raw(" "), - dense_action("results", theme::ACCENT, p.hover_export_results), - ]; - - if matches!(p.subtab, PipelineSubtab::Config) { - spans.push(Span::raw(" ")); - spans.push(dense_action( - "import cfg", - theme::METRIC_CYC, - p.hover_import_cfg, - )); - spans.push(Span::raw(" ")); - spans.push(dense_action( - "export cfg", - theme::METRIC_CYC, - p.hover_export_cfg, - )); - } - - let block = Block::default() - .borders(Borders::ALL) - .border_type(BorderType::Rounded) - .border_style(Style::default().fg(theme::BORDER)); - let inner = block.inner(area); - f.render_widget(block, area); - f.render_widget(Paragraph::new(Line::from(spans)), inner); - - let y = inner.y; - let x = inner.x + 1; - app.pipeline - .btn_export_results_rect - .set((y, x, x + "results".len() as u16)); - - if matches!(p.subtab, PipelineSubtab::Config) { - let import_x = x + "results".len() as u16 + 3; - let export_x = import_x + "import cfg".len() as u16 + 3; - app.pipeline - .btn_import_cfg_rect - .set((y, import_x, import_x + "import cfg".len() as u16)); - app.pipeline - .btn_export_cfg_rect - .set((y, export_x, export_x + "export cfg".len() as u16)); - } else { - app.pipeline.btn_import_cfg_rect.set((0, 0, 0)); - app.pipeline.btn_export_cfg_rect.set((0, 0, 0)); - } -} diff --git a/src/ui/view/run/formatting.rs b/src/ui/view/run/formatting.rs index 2053c6e..65e4e20 100755 --- a/src/ui/view/run/formatting.rs +++ b/src/ui/view/run/formatting.rs @@ -5,17 +5,17 @@ pub(super) fn format_memory_value(app: &App, addr: u32) -> String { // so write-back stores are visible in the RUN tab memory view. match app.run.mem_view_bytes { 4 => format_u32_value( - app.run.mem.effective_read32(addr).unwrap_or(0), + app.run.mem().effective_read32(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), 2 => format_u16_value( - app.run.mem.effective_read16(addr).unwrap_or(0), + app.run.mem().effective_read16(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), _ => format_u8_value( - app.run.mem.effective_read8(addr).unwrap_or(0), + app.run.mem().effective_read8(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), @@ -26,17 +26,17 @@ pub(super) fn format_memory_value(app: &App, addr: u32) -> String { pub(super) fn format_stale_value(app: &App, addr: u32) -> String { match app.run.mem_view_bytes { 4 => format_u32_value( - app.run.mem.peek32(addr).unwrap_or(0), + app.run.mem().peek32(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), 2 => format_u16_value( - app.run.mem.peek16(addr).unwrap_or(0), + app.run.mem().peek16(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), _ => format_u8_value( - app.run.mem.peek8(addr).unwrap_or(0), + app.run.mem().peek8(addr).unwrap_or(0), app.run.fmt_mode, app.run.show_signed, ), @@ -50,6 +50,7 @@ pub(super) fn format_u32_value(value: u32, fmt: FormatMode, show_signed: bool) - true => format!("{}", value as i32), false => format!("{value}"), }, + FormatMode::Bin => format!("0b{value:032b}"), FormatMode::Str => ascii_bytes(&value.to_le_bytes()), } } @@ -61,6 +62,7 @@ pub(super) fn format_u16_value(value: u16, fmt: FormatMode, show_signed: bool) - true => format!("{}", value as i16), false => format!("{value}"), }, + FormatMode::Bin => format!("0b{value:016b}"), FormatMode::Str => ascii_bytes(&value.to_le_bytes()), } } @@ -72,6 +74,7 @@ pub(super) fn format_u8_value(value: u8, fmt: FormatMode, show_signed: bool) -> true => format!("{}", value as i8), false => format!("{value}"), }, + FormatMode::Bin => format!("0b{value:08b}"), FormatMode::Str => ascii_bytes(&[value]), } } diff --git a/src/ui/view/run/instruction_details.rs b/src/ui/view/run/instruction_details.rs index 5e644af..8769cc9 100644 --- a/src/ui/view/run/instruction_details.rs +++ b/src/ui/view/run/instruction_details.rs @@ -1,5 +1,7 @@ use crate::falcon; -use crate::ui::app::cpi_class_label; +use crate::ui::app::{ + EncFormat, InstrFieldKind, RunEditTarget, Seg, cpi_class_label, detect_format, +}; use crate::ui::theme; use ratatui::Frame; use ratatui::prelude::*; @@ -12,10 +14,12 @@ use super::registers::reg_name; // ── Public entry point ─────────────────────────────────────────────────────── pub(super) fn render_instruction_details(f: &mut Frame, area: Rect, app: &App) { + app.run.details_field_hitboxes.borrow_mut().clear(); if area.width < 4 || area.height < 4 { return; } let ctx = detail_context(app); + app.run.details_rendered_addr.set(ctx.addr); // Split into 3 sections: header (3 lines + border), field map (4 lines + border), rest let header_h = 5u16; @@ -31,7 +35,7 @@ pub(super) fn render_instruction_details(f: &mut Frame, area: Rect, app: &App) { .split(area); render_header(f, chunks[0], &ctx, app); - render_field_map(f, chunks[1], ctx.word, ctx.format); + render_field_map(f, chunks[1], ctx.word, ctx.format, app); render_decoded( f, chunks[2], @@ -39,10 +43,47 @@ pub(super) fn render_instruction_details(f: &mut Frame, area: Rect, app: &App) { ctx.format, &ctx.disasm, ctx.comment.as_deref(), - Some(&app.run.cpu), + Some(app.run.cpu()), + app, + &ctx, ); } +/// The field of `addr` the inline editor is currently open on, if any. +/// `Word` stands for the full hex word (`RunEditTarget::Instr`). +fn editing_field(app: &App, addr: u32) -> Option { + match app.run.run_edit { + Some(RunEditTarget::Instr { addr: a }) if a == addr => Some(InstrFieldKind::Word), + Some(RunEditTarget::InstrField { addr: a, field }) if a == addr => Some(field), + _ => None, + } +} + +/// Record a clickable field span, clipped to the section's inner rect so a +/// partially hidden value never produces a hitbox past the border. +fn push_hitbox(app: &App, inner: Rect, field: InstrFieldKind, y: u16, x0: u16, len: usize) { + if y >= inner.y + inner.height || y < inner.y { + return; + } + let right = inner.x + inner.width; + if x0 >= right { + return; + } + let x1 = (x0 + len as u16).min(right); + app.run + .details_field_hitboxes + .borrow_mut() + .push((field, y, x0, x1)); +} + +/// The buffer + pseudo-cursor span of the open inline editor. +fn edit_buf_span(app: &App) -> Span<'static> { + Span::styled( + format!("{}█", app.run.run_edit_buf), + Style::default().fg(theme::ACCENT).bold(), + ) +} + pub(super) fn disasm_word(word: u32) -> String { match falcon::decoder::decode(word) { Ok(instruction) => pretty_instr(&instruction), @@ -65,7 +106,7 @@ struct DetailContext { fn compute_jump_target(word: u32, addr: u32, app: &App) -> Option<(bool, u32, Option)> { use crate::falcon::decoder::decode; use crate::falcon::instruction::Instruction::*; - let cpu = &app.run.cpu; + let cpu = app.run.cpu(); let (taken, target) = match decode(word) { Ok(Beq { rs1, rs2, imm }) => ( cpu.x[rs1 as usize] == cpu.x[rs2 as usize], @@ -100,15 +141,19 @@ fn compute_jump_target(word: u32, addr: u32, app: &App) -> Option<(bool, u32, Op } fn detail_context(app: &App) -> DetailContext { - let (addr, word, origin) = if let Some(addr) = app.run.hover_imem_addr { - let word = app.run.mem.peek32(addr).unwrap_or(0); - (addr, word, "hover") - } else if exec_address_in_range(app, app.run.cpu.pc) { - let word = app.run.mem.peek32(app.run.cpu.pc).unwrap_or(0); - (app.run.cpu.pc, word, "PC") + // A click-selected row pins the panel; otherwise it follows the PC. + let selected = app + .run + .details_addr + .and_then(|addr| app.run.mem().peek32(addr).ok().map(|word| (addr, word))); + let (addr, word, origin) = if let Some((addr, word)) = selected { + (addr, word, "selected") + } else if exec_address_in_range(app, app.run.cpu().pc) { + let word = app.run.mem().peek32(app.run.cpu().pc).unwrap_or(0); + (app.run.cpu().pc, word, "PC") } else { return DetailContext { - addr: app.run.cpu.pc, + addr: app.run.cpu().pc, word: 0, disasm: "".into(), origin: "PC", @@ -147,44 +192,101 @@ fn render_header(f: &mut Frame, area: Rect, ctx: &DetailContext, app: &App) { let inner = block.inner(area); f.render_widget(block, area); + let editing = editing_field(app, ctx.addr); let origin_span = Span::styled( format!(" @ 0x{:08x} ({})", ctx.addr, ctx.origin), Style::default().fg(theme::LABEL), ); - let word_span = Span::styled( - format!("0x{:08x}", ctx.word), - Style::default().fg(theme::IMM_COLOR), - ); - let disasm_span = Span::styled( - ctx.disasm.clone(), - Style::default().fg(Color::Yellow).bold(), - ); + + // Line 0 — mnemonic, editable as one line of assembly. While editing, + // the typed buffer replaces the disasm and a dim preview shows what it + // assembles to before Enter commits it. + let mut mnemonic_line = vec![Span::styled("▶ ", Style::default().fg(Color::Green))]; + if editing == Some(InstrFieldKind::Asm) { + mnemonic_line.push(edit_buf_span(app)); + let preview = match falcon::asm::assemble(&app.run.run_edit_buf, ctx.addr) { + Ok(prog) if prog.text.len() == 1 => disasm_word(prog.text[0]), + _ => "?".to_string(), + }; + mnemonic_line.push(Span::styled( + format!(" → {preview}"), + Style::default().fg(Color::DarkGray), + )); + } else { + push_hitbox( + app, + inner, + InstrFieldKind::Asm, + inner.y, + inner.x + 2, + ctx.disasm.chars().count(), + ); + mnemonic_line.push(Span::styled( + ctx.disasm.clone(), + Style::default().fg(Color::Yellow).bold(), + )); + mnemonic_line.push(origin_span); + } + + // Line 1 — the word in hex (edits through the full-word editor) and in + // binary (edits as a 32-bit binary value). + let mut word_line = vec![Span::styled(" word ", Style::default().fg(theme::LABEL))]; + match editing { + Some(InstrFieldKind::Word) => { + word_line.push(edit_buf_span(app)); + let preview = match crate::falcon::machine::parse::parse_cell( + &app.run.run_edit_buf, + crate::falcon::machine::types::MemWidth::B4, + app.cell_format(), + app.run.show_signed, + ) { + Ok(value) => disasm_word(value as u32), + Err(_) => "?".to_string(), + }; + word_line.push(Span::styled( + format!(" → {preview}"), + Style::default().fg(Color::DarkGray), + )); + } + Some(InstrFieldKind::Bin) => { + word_line.push(edit_buf_span(app)); + } + _ => { + let word_x = inner.x + 8; + push_hitbox(app, inner, InstrFieldKind::Word, inner.y + 1, word_x, 10); + push_hitbox( + app, + inner, + InstrFieldKind::Bin, + inner.y + 1, + word_x + 10 + 3, + 32, + ); + word_line.push(Span::styled( + format!("0x{:08x}", ctx.word), + Style::default().fg(theme::IMM_COLOR), + )); + word_line.push(Span::styled( + format!(" ({:032b})", ctx.word), + Style::default().fg(Color::Rgb(80, 80, 100)), + )); + } + } // Compute base CPI cycles for current instruction let cpi = &app.run.cpi_config; let base_cycles = crate::ui::app::classify_cpi_for_display( ctx.word, ctx.addr, - &app.run.cpu, + app.run.cpu(), cpi, - app.pipeline.enabled, + app.run.pipeline().enabled, ); let class_label = cpi_class_label(ctx.word); let mut lines = vec![ - Line::from(vec![ - Span::styled("▶ ", Style::default().fg(Color::Green)), - disasm_span, - origin_span, - ]), - Line::from(vec![ - Span::styled(" word ", Style::default().fg(theme::LABEL)), - word_span, - Span::styled( - format!(" ({:032b})", ctx.word), - Style::default().fg(Color::Rgb(80, 80, 100)), - ), - ]), + Line::from(mnemonic_line), + Line::from(word_line), Line::from(vec![ Span::styled(" cycles ", Style::default().fg(theme::LABEL)), Span::styled( @@ -257,7 +359,7 @@ fn render_header(f: &mut Frame, area: Rect, ctx: &DetailContext, app: &App) { // ── Section 2 : Field map ──────────────────────────────────────────────────── -fn render_field_map(f: &mut Frame, area: Rect, word: u32, format: EncFormat) { +fn render_field_map(f: &mut Frame, area: Rect, word: u32, format: EncFormat, app: &App) { let segs = format.segments(); let block = Block::default() .borders(Borders::ALL) @@ -268,6 +370,18 @@ fn render_field_map(f: &mut Frame, area: Rect, word: u32, format: EncFormat) { let inner = block.inner(area); f.render_widget(block, area); + // Each segment's bits row doubles as a click target for editing that + // field (the editor itself renders in the header/Decoded sections). + let bits_y = inner.y + 2; + let mut x = inner.x; + for seg in &segs { + let w = display_width(seg); + if let Some(field) = seg_field(seg.label) { + push_hitbox(app, inner, field, bits_y, x, w); + } + x = x.saturating_add(w as u16 + 1); + } + // Row 1 — bit position markers let pos_line = bit_position_line(&segs); // Row 2 — colored label blocks (▮▮… label) @@ -279,6 +393,21 @@ fn render_field_map(f: &mut Frame, area: Rect, word: u32, format: EncFormat) { f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: false }), inner); } +/// Map a field-map segment label to its editable field. All immediate pieces +/// (`imm[...]`, `i12`, `i10:5`, …) edit the one logical immediate. +fn seg_field(label: &str) -> Option { + match label { + "funct7" => Some(InstrFieldKind::Funct7), + "rs2" => Some(InstrFieldKind::Rs2), + "rs1" => Some(InstrFieldKind::Rs1), + "fn3" => Some(InstrFieldKind::Funct3), + "rd" => Some(InstrFieldKind::Rd), + "opcode" => Some(InstrFieldKind::Opcode), + l if l.starts_with("imm") || l.starts_with('i') => Some(InstrFieldKind::Imm), + _ => None, + } +} + fn bit_position_line(segs: &[Seg]) -> Line<'static> { let mut spans = Vec::new(); let mut bit = 31i32; @@ -355,6 +484,7 @@ fn display_width(seg: &Seg) -> usize { // ── Section 3 : Decoded fields + description ───────────────────────────────── +#[allow(clippy::too_many_arguments)] fn render_decoded( f: &mut Frame, area: Rect, @@ -363,6 +493,8 @@ fn render_decoded( disasm: &str, comment: Option<&str>, cpu: Option<&crate::falcon::Cpu>, + app: &App, + ctx: &DetailContext, ) { let block = Block::default() .borders(Borders::ALL) @@ -375,21 +507,33 @@ fn render_decoded( let mut lines: Vec> = Vec::new(); if let Some(c) = comment { + // Truncated to one row: a wrapped comment would shift every kv row + // below it and break the recorded hitbox positions. + let max = (inner.width as usize).saturating_sub(3); + let c_fit: String = c.chars().take(max).collect(); lines.push(Line::from(vec![ Span::styled("#! ", Style::default().fg(Color::Rgb(100, 200, 100))), - Span::styled( - c.to_string(), - Style::default().fg(Color::Rgb(180, 220, 130)), - ), + Span::styled(c_fit, Style::default().fg(Color::Rgb(180, 220, 130))), ])); lines.push(Line::from("")); } - push_fields(&mut lines, word, format, cpu); + let mut rows = DecodedRows { + lines: &mut lines, + hits: Vec::new(), + editing: editing_field(app, ctx.addr), + buf: &app.run.run_edit_buf, + }; + push_fields(&mut rows, word, format, cpu); + let hits = rows.hits; // blank separator lines.push(Line::from("")); // Semantic description push_description(&mut lines, word, format, disasm); + for (field, idx, len) in hits { + push_hitbox(app, inner, field, inner.y + idx as u16, inner.x + 10, len); + } + f.render_widget(Paragraph::new(lines).wrap(Wrap { trim: true }), inner); } @@ -400,24 +544,55 @@ fn kv(key: &'static str, val: String, val_color: Color) -> Line<'static> { ]) } -fn reg_kv(key: &'static str, reg: u8) -> Line<'static> { - kv( - key, - format!("x{reg} ({})", reg_name(reg)), - Color::LightGreen, - ) +/// Collects the Decoded section's kv rows, remembering which row holds which +/// editable field (for click hitboxes) and substituting the open editor's +/// buffer into the row it is editing. +struct DecodedRows<'a> { + lines: &'a mut Vec>, + /// `(field, line index, value length)` for every editable row pushed. + hits: Vec<(InstrFieldKind, usize, usize)>, + editing: Option, + buf: &'a str, } -fn imm_kv(key: &'static str, v: i32) -> Line<'static> { - kv(key, format!("{v} (0x{v:x})"), theme::IMM_COLOR) +impl DecodedRows<'_> { + fn field(&mut self, field: InstrFieldKind, key: &'static str, val: String, color: Color) { + if self.editing == Some(field) { + self.lines.push(Line::from(vec![ + Span::styled(format!("{key:<10}"), Style::default().fg(theme::LABEL)), + Span::styled( + format!("{}█", self.buf), + Style::default().fg(theme::ACCENT).bold(), + ), + ])); + } else { + self.hits.push((field, self.lines.len(), val.chars().count())); + self.lines.push(kv(key, val, color)); + } + } + fn plain(&mut self, key: &'static str, val: String, color: Color) { + self.lines.push(kv(key, val, color)); + } + fn reg(&mut self, field: InstrFieldKind, key: &'static str, reg: u8) { + self.field( + field, + key, + format!("x{reg} ({})", reg_name(reg)), + Color::LightGreen, + ); + } + fn imm(&mut self, field: InstrFieldKind, key: &'static str, v: i32) { + self.field(field, key, format!("{v} (0x{v:x})"), theme::IMM_COLOR); + } } fn push_fields( - lines: &mut Vec>, + rows: &mut DecodedRows<'_>, word: u32, format: EncFormat, cpu: Option<&crate::falcon::Cpu>, ) { + use InstrFieldKind::*; let opcode = word & 0x7f; match format { EncFormat::R => { @@ -426,36 +601,32 @@ fn push_fields( let rs1 = ((word >> 15) & 0x1f) as u8; let funct3 = (word >> 12) & 0x7; let rd = ((word >> 7) & 0x1f) as u8; - lines.push(reg_kv("rd", rd)); - lines.push(reg_kv("rs1", rs1)); - lines.push(reg_kv("rs2", rs2)); - lines.push(kv("funct3", format!("0x{funct3:01x}"), Color::Yellow)); - lines.push(kv("funct7", format!("0x{funct7:02x}"), Color::Red)); + rows.reg(Rd, "rd", rd); + rows.reg(Rs1, "rs1", rs1); + rows.reg(Rs2, "rs2", rs2); + rows.field(Funct3, "funct3", format!("0x{funct3:01x}"), Color::Yellow); + rows.field(Funct7, "funct7", format!("0x{funct7:02x}"), Color::Red); } EncFormat::I => { let imm = (((word >> 20) as i32) << 20) >> 20; let rs1 = ((word >> 15) & 0x1f) as u8; let funct3 = (word >> 12) & 0x7; let rd = ((word >> 7) & 0x1f) as u8; - lines.push(reg_kv("rd", rd)); - lines.push(reg_kv("rs1", rs1)); - lines.push(imm_kv("imm", imm)); - lines.push(kv("funct3", format!("0x{funct3:01x}"), Color::Yellow)); + rows.reg(Rd, "rd", rd); + rows.reg(Rs1, "rs1", rs1); + rows.imm(Imm, "imm", imm); + rows.field(Funct3, "funct3", format!("0x{funct3:01x}"), Color::Yellow); if matches!(funct3, 0x1 | 0x5) { let shamt = (word >> 20) & 0x1f; let funct7 = (word >> 25) & 0x7f; - lines.push(kv("shamt", format!("{shamt}"), Color::LightRed)); - lines.push(kv("funct7", format!("0x{funct7:02x}"), Color::Red)); + rows.field(Shamt, "shamt", format!("{shamt}"), Color::LightRed); + rows.field(Funct7, "funct7", format!("0x{funct7:02x}"), Color::Red); } // Feature 5: effective address for loads (opcode 0x03) if opcode == 0x03 { if let Some(cpu) = cpu { let ea = cpu.x[rs1 as usize].wrapping_add(imm as u32); - lines.push(kv( - "\u{2192} addr", - format!("0x{ea:08x}"), - Color::Rgb(255, 180, 80), - )); + rows.plain("\u{2192} addr", format!("0x{ea:08x}"), Color::Rgb(255, 180, 80)); } } } @@ -466,18 +637,14 @@ fn push_fields( let rs2 = ((word >> 20) & 0x1f) as u8; let imm_hi = (word >> 25) & 0x7f; let imm = (((((imm_hi << 5) | imm_lo) as i32) << 20) >> 20) as i32; - lines.push(reg_kv("rs1 (base)", rs1)); - lines.push(reg_kv("rs2 (src)", rs2)); - lines.push(imm_kv("offset", imm)); - lines.push(kv("funct3", format!("0x{funct3:01x}"), Color::Yellow)); + rows.reg(Rs1, "rs1 (base)", rs1); + rows.reg(Rs2, "rs2 (src)", rs2); + rows.imm(Imm, "offset", imm); + rows.field(Funct3, "funct3", format!("0x{funct3:01x}"), Color::Yellow); // Feature 5: effective address for stores if let Some(cpu) = cpu { let ea = cpu.x[rs1 as usize].wrapping_add(imm as u32); - lines.push(kv( - "\u{2192} addr", - format!("0x{ea:08x}"), - Color::Rgb(255, 180, 80), - )); + rows.plain("\u{2192} addr", format!("0x{ea:08x}"), Color::Rgb(255, 180, 80)); } } EncFormat::B => { @@ -490,16 +657,16 @@ fn push_fields( let b11 = (word >> 7) & 1; let imm = (((((b12 << 12) | (b11 << 11) | (b10_5 << 5) | (b4_1 << 1)) as i32) << 19) >> 19) as i32; - lines.push(reg_kv("rs1", rs1)); - lines.push(reg_kv("rs2", rs2)); - lines.push(imm_kv("offset", imm)); - lines.push(kv("funct3", format!("0x{funct3:01x}"), Color::Yellow)); + rows.reg(Rs1, "rs1", rs1); + rows.reg(Rs2, "rs2", rs2); + rows.imm(Imm, "offset", imm); + rows.field(Funct3, "funct3", format!("0x{funct3:01x}"), Color::Yellow); } EncFormat::U => { let rd = ((word >> 7) & 0x1f) as u8; let imm = ((word & 0xfffff000) as i32) >> 12; - lines.push(reg_kv("rd", rd)); - lines.push(imm_kv("imm[31:12]", imm)); + rows.reg(Rd, "rd", rd); + rows.imm(Imm, "imm[31:12]", imm); } EncFormat::J => { let b20 = (word >> 31) & 1; @@ -509,10 +676,13 @@ fn push_fields( let rd = ((word >> 7) & 0x1f) as u8; let imm = (((((b20 << 20) | (b19_12 << 12) | (b11 << 11) | (b10_1 << 1)) as i32) << 11) >> 11) as i32; - lines.push(reg_kv("rd", rd)); - lines.push(imm_kv("offset", imm)); + rows.reg(Rd, "rd", rd); + rows.imm(Imm, "offset", imm); } } + // The opcode selects the format itself; editing it morphs the whole row + // layout above, which is exactly the didactic point. + rows.field(Opcode, "opcode", format!("0x{opcode:02x}"), Color::Cyan); } fn push_description(lines: &mut Vec>, word: u32, _format: EncFormat, disasm: &str) { @@ -603,112 +773,6 @@ fn push_description(lines: &mut Vec>, word: u32, _format: EncForma } } -// ── Format detection + segments ────────────────────────────────────────────── - -#[derive(Copy, Clone)] -enum EncFormat { - R, - I, - S, - B, - U, - J, -} - -impl EncFormat { - fn name(self) -> &'static str { - match self { - EncFormat::R => "R-type", - EncFormat::I => "I-type", - EncFormat::S => "S-type", - EncFormat::B => "B-type", - EncFormat::U => "U-type", - EncFormat::J => "J-type", - } - } - fn segments(self) -> Vec { - seg_list(self) - } -} - -fn detect_format(word: u32) -> EncFormat { - match word & 0x7f { - 0x03 | 0x13 | 0x1b | 0x67 | 0x73 => EncFormat::I, - 0x23 => EncFormat::S, - 0x63 => EncFormat::B, - 0x37 | 0x17 => EncFormat::U, - 0x6f => EncFormat::J, - _ => EncFormat::R, - } -} - -struct Seg { - label: &'static str, - width: u8, - color: Color, -} - -fn seg_list(format: EncFormat) -> Vec { - macro_rules! s { - ($l:expr, $w:expr, $c:expr) => { - Seg { - label: $l, - width: $w, - color: $c, - } - }; - } - use Color::*; - match format { - EncFormat::R => vec![ - s!("funct7", 7, Red), - s!("rs2", 5, LightRed), - s!("rs1", 5, LightMagenta), - s!("fn3", 3, Yellow), - s!("rd", 5, LightGreen), - s!("opcode", 7, Cyan), - ], - EncFormat::I => vec![ - s!("imm[11:0]", 12, Blue), - s!("rs1", 5, LightMagenta), - s!("fn3", 3, Yellow), - s!("rd", 5, LightGreen), - s!("opcode", 7, Cyan), - ], - EncFormat::S => vec![ - s!("imm[11:5]", 7, Blue), - s!("rs2", 5, LightRed), - s!("rs1", 5, LightMagenta), - s!("fn3", 3, Yellow), - s!("imm[4:0]", 5, Blue), - s!("opcode", 7, Cyan), - ], - EncFormat::B => vec![ - s!("i12", 1, Blue), - s!("i10:5", 6, Blue), - s!("rs2", 5, LightRed), - s!("rs1", 5, LightMagenta), - s!("fn3", 3, Yellow), - s!("i4:1", 4, Blue), - s!("i11", 1, Blue), - s!("opcode", 7, Cyan), - ], - EncFormat::U => vec![ - s!("imm[31:12]", 20, Blue), - s!("rd", 5, LightGreen), - s!("opcode", 7, Cyan), - ], - EncFormat::J => vec![ - s!("i20", 1, Blue), - s!("i10:1", 10, Blue), - s!("i11", 1, Blue), - s!("i19:12", 8, Blue), - s!("rd", 5, LightGreen), - s!("opcode", 7, Cyan), - ], - } -} - // ── Disassembly pretty-printer ──────────────────────────────────────────────── fn pretty_instr(instruction: &falcon::instruction::Instruction) -> String { diff --git a/src/ui/view/run/instruction_list.rs b/src/ui/view/run/instruction_list.rs index 2f3572c..d44b051 100644 --- a/src/ui/view/run/instruction_list.rs +++ b/src/ui/view/run/instruction_list.rs @@ -250,11 +250,15 @@ fn heat_color(n: u64) -> Color { } const HOVER_BG: Color = theme::BG_HOVER; +/// The row the details panel is pinned to (click-selected). Stronger than the +/// hover wash so the pin survives the mouse moving away. +const SELECTED_BG: Color = theme::BG_RAISED; fn instruction_item(app: &App, addr: u32) -> ListItem<'static> { - let word = app.run.mem.peek32(addr).unwrap_or(0); + let word = app.run.mem().peek32(addr).unwrap_or(0); let is_bp = app.run.breakpoints.contains(&addr); - let is_pc = addr == app.run.cpu.pc; + let is_pc = addr == app.run.cpu().pc; + let is_selected = !is_pc && app.run.details_addr == Some(addr); let is_hover = !is_pc && app.run.hover_imem_addr == Some(addr); // Collect non-selected harts that are currently at this address. @@ -269,6 +273,7 @@ fn instruction_item(app: &App, addr: u32) -> ListItem<'static> { } else { " " }; + let disasm = disasm_word(word); let exec_count = app.run.exec_counts.get(&addr).copied().unwrap_or(0); @@ -319,7 +324,7 @@ fn instruction_item(app: &App, addr: u32) -> ListItem<'static> { // Branch/jump indicator on current PC instruction if is_pc { - if let Some((taken, target)) = branch_outcome(word, addr, &app.run.cpu) { + if let Some((taken, target)) = branch_outcome(word, addr, app.run.cpu()) { let label = app .run .labels @@ -352,6 +357,9 @@ fn instruction_item(app: &App, addr: u32) -> ListItem<'static> { let line = Line::from(spans); let mut style = Style::default(); + if is_selected { + style = style.bg(SELECTED_BG); + } if is_hover { style = style.bg(HOVER_BG); } diff --git a/src/ui/view/run/mod.rs b/src/ui/view/run/mod.rs index 54befc4..d393278 100644 --- a/src/ui/view/run/mod.rs +++ b/src/ui/view/run/mod.rs @@ -19,7 +19,7 @@ mod status; use instruction_details::render_instruction_details; use instruction_list::{render_exec_trace, render_instruction_memory}; use sidebar::render_sidebar; -pub(crate) use status::{render_run_status, run_controls_plain_text, state_text}; +pub(crate) use status::{build_run_toolbar, render_run_status, run_controls_plain_text}; pub(crate) const RUN_COLLAPSED_RAIL_W: u16 = 2; pub(crate) const RUN_SIDEBAR_MIN_W: u16 = 20; diff --git a/src/ui/view/run/sidebar.rs b/src/ui/view/run/sidebar.rs index fbdba49..6840385 100644 --- a/src/ui/view/run/sidebar.rs +++ b/src/ui/view/run/sidebar.rs @@ -5,8 +5,39 @@ use ratatui::widgets::{Block, BorderType, Borders, Cell, List, ListItem, Paragra use super::formatting::{format_memory_value, format_stale_value, format_u32_value}; use super::registers::reg_name; use super::{App, MemRegion}; +use crate::falcon::machine::types::{RegId, RegTarget}; +use crate::ui::app::RunEditTarget; use crate::ui::theme; +/// The cursor-suffixed edit buffer to paint in a cell, when it is the target of +/// the open inline editor. `None` means render the cell's value normally. +fn reg_edit_overlay(app: &App, target: RegTarget) -> Option { + match app.run.run_edit { + Some(RunEditTarget::Reg(t)) if t == target => Some(format!("{}█", app.run.run_edit_buf)), + _ => None, + } +} + +/// Like [`reg_edit_overlay`] for the float register `index`. +fn freg_edit_overlay(app: &App, index: u8) -> Option { + match app.run.run_edit { + Some(RunEditTarget::FReg(f)) if f.index() == index => { + Some(format!("{}█", app.run.run_edit_buf)) + } + _ => None, + } +} + +/// Like [`reg_edit_overlay`] for the memory cell at `addr`. +fn mem_edit_overlay(app: &App, addr: u32) -> Option { + match app.run.run_edit { + Some(RunEditTarget::Mem { addr: a, .. }) if a == addr => { + Some(format!("{}█", app.run.run_edit_buf)) + } + _ => None, + } +} + pub(super) fn render_sidebar(f: &mut Frame, area: Rect, app: &App) { if app.run.show_dyn { // STORE → show where data was written; LOAD/ALU/branch → show registers @@ -78,9 +109,14 @@ fn build_register_rows(inner: Rect, app: &App) -> Vec> { } else { base }; + let target = RegId::new(reg_idx).map(RegTarget::X); + let (val, value_style) = match target.and_then(|t| reg_edit_overlay(app, t)) { + Some(overlay) => (overlay, edit_value_style()), + None => (val, style), + }; rows.push(Row::new(vec![ Cell::from(pin_label).style(style), - Cell::from(val).style(style), + Cell::from(val).style(value_style), ])); } @@ -124,15 +160,35 @@ fn build_register_rows(inner: Rect, app: &App) -> Vec> { } else { base_style }; + let target = reg_target_for_row(index); + let (val, value_style) = match target.and_then(|t| reg_edit_overlay(app, t)) { + Some(overlay) => (overlay, edit_value_style()), + None => (val, row_style), + }; rows.push(Row::new(vec![ Cell::from(full_label).style(row_style), - Cell::from(val).style(row_style), + Cell::from(val).style(value_style), ])); } rows } +/// Row index `0 = PC`, `1..=32 = x0..x31` to its edit target (mirrors the click +/// hit-test in `mouse.rs`). +fn reg_target_for_row(reg_idx: usize) -> Option { + match reg_idx { + 0 => Some(RegTarget::Pc), + 1..=32 => RegId::new((reg_idx - 1) as u8).map(RegTarget::X), + _ => None, + } +} + +/// Accent style for a cell currently being inline-edited. +fn edit_value_style() -> Style { + Style::default().fg(theme::ACCENT).bold() +} + /// Style based on register age (0 = just changed → bright yellow, fades over steps). fn age_style(age: u8) -> Style { match age { @@ -147,17 +203,17 @@ fn age_style(age: u8) -> Style { /// Returns (label, value, age). fn register_entry(index: usize, app: &App) -> (String, String, u8) { if index == 0 { - let age = if app.run.cpu.pc != app.run.prev_pc { + let age = if app.run.cpu().pc != app.run.prev_pc { 0 } else { 255 }; - let val = format_u32_value(app.run.cpu.pc, app.run.fmt_mode, app.run.show_signed); + let val = format_u32_value(app.run.cpu().pc, app.run.fmt_mode, app.run.show_signed); ("PC".to_string(), val, age) } else { let reg_index = (index - 1) as u8; let val = format_u32_value( - app.run.cpu.x[reg_index as usize], + app.run.cpu().x[reg_index as usize], app.run.fmt_mode, app.run.show_signed, ); @@ -172,7 +228,7 @@ fn register_entry(index: usize, app: &App) -> (String, String, u8) { /// Returns (label, value, age) for pinned register. fn register_entry_reg(reg_idx: u8, app: &App) -> (String, String, u8) { let val = format_u32_value( - app.run.cpu.x[reg_idx as usize], + app.run.cpu().x[reg_idx as usize], app.run.fmt_mode, app.run.show_signed, ); @@ -201,7 +257,7 @@ fn render_float_register_table(f: &mut Frame, area: Rect, app: &App) { .take(visible) .map(|i| { let age = app.run.f_age[i as usize]; - let bits = app.run.cpu.f[i as usize]; + let bits = app.run.cpu().f[i as usize]; let val = f32::from_bits(bits); let label = format!("f{i:02} ({}) ", freg_name_short(i)); let value = if val.is_nan() { @@ -216,9 +272,13 @@ fn render_float_register_table(f: &mut Frame, area: Rect, app: &App) { format!("{val:.6}") }; let style = age_style(age); + let (value, value_style) = match freg_edit_overlay(app, i) { + Some(overlay) => (overlay, edit_value_style()), + None => (value, style), + }; Row::new(vec![ Cell::from(label).style(style), - Cell::from(value).style(style), + Cell::from(value).style(value_style), ]) }) .collect(); @@ -333,7 +393,7 @@ fn render_mem_search_bar(f: &mut Frame, area: Rect, app: &App) { } fn memory_block(app: &App) -> Block<'static> { - let base_addr = visible_memory_base_addr(app, None); + let base_addr = app.run.visible_memory_base_addr(None); let section = memory_title_section(app, base_addr); let accent = memory_accent_color(app, section); let title = Line::from(vec![ @@ -352,7 +412,7 @@ fn memory_block(app: &App) -> Block<'static> { } fn memory_items(inner: Rect, app: &App) -> Vec> { - let base = visible_memory_base_addr(app, Some(inner.height as u32)); + let base = app.run.visible_memory_base_addr(Some(inner.height as u32)); let bytes = app.run.mem_view_bytes; let lines = inner.height as u32; let max = app.run.mem_size.saturating_sub(bytes as usize) as u32; @@ -365,29 +425,12 @@ fn memory_items(inner: Rect, app: &App) -> Vec> { .collect() } -fn visible_memory_base_addr(app: &App, lines_override: Option) -> u32 { - let bytes = app.run.mem_view_bytes.max(1); - let lines = lines_override.unwrap_or(0); - let center = app.run.mem_region == MemRegion::Stack - || app.run.mem_region == MemRegion::Access - || app.run.mem_region == MemRegion::Heap - || (app.run.show_dyn && matches!(app.run.dyn_mem_access, Some((_, _, true)))); - let base = if center { - let half = lines / 2; - app.run.mem_view_addr.saturating_sub(half * bytes) - } else { - app.run.mem_view_addr - }; - let align_mask = !(bytes - 1); - base & align_mask -} - fn memory_title_section<'a>(app: &'a App, addr: u32) -> &'a str { classify_memory_section(app, addr) } fn classify_memory_section<'a>(app: &'a App, addr: u32) -> &'a str { - let sp_aligned = app.run.cpu.x[2] & !(app.run.mem_view_bytes.saturating_sub(1)); + let sp_aligned = app.run.cpu().x[2] & !(app.run.mem_view_bytes.saturating_sub(1)); if addr >= sp_aligned && (addr as usize) < app.run.mem_size { return "stack"; } @@ -419,7 +462,7 @@ fn classify_memory_section<'a>(app: &'a App, addr: u32) -> &'a str { if addr >= data_end && addr < bss_end { return ".bss"; } - if addr >= app.run.heap_start && addr < app.run.cpu.heap_break { + if addr >= app.run.heap_start && addr < app.run.cpu().heap_break { return "heap"; } @@ -457,28 +500,34 @@ fn mem_age_style(age: u8) -> Option