From 0ae6e9c77b73aca3017bacb8126fb8df8aefd670 Mon Sep 17 00:00:00 2001 From: gaok1 Date: Mon, 1 Jun 2026 17:04:56 -0300 Subject: [PATCH 01/13] docs(vm): finalize VM/TLB docs (internal + external) - Rewrite EN virtual-memory.md to full parity with the PT guide (21 sections) - PT: add the four VM modes (Off/Sv32/Custom/Manual) + Custom parametric scheme; rewrite the tab section to the real structure (Status/Tree/Settings/TLB; TLB Stats/Entries/Settings); fix stale UI refs - READMEs (en+pt): add the Virtual Memory/TLB tab to the feature list and link virtual-memory.md; fix tab numbering - cache-config.md (en+pt): document the tlb.* block (.fcache) and the vm_mode/vm_offset_bits/vm_level_bits fields (.rcfg) - platform doc: add VM/TLB subsection + doc-index entry - in-app: drop the .rs citation from the TLB tutorial; add the TLB/VM block to the in-app .fcache reference --- docs/en/README.md | 12 +- docs/en/cache-config.md | 43 ++ docs/en/virtual-memory.md | 697 ++++++++++++++++++++----- docs/pt-BR/README.md | 14 +- docs/pt-BR/cache-config.md | 43 ++ docs/pt-BR/virtual-memory.md | 106 ++-- src/ui/tutorial/steps/tlb.rs | 4 +- src/ui/view/docs/content/fcache_ref.rs | 38 ++ 8 files changed, 788 insertions(+), 169 deletions(-) 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/ui/tutorial/steps/tlb.rs b/src/ui/tutorial/steps/tlb.rs index 3a4ffee..cbeb26d 100644 --- a/src/ui/tutorial/steps/tlb.rs +++ b/src/ui/tutorial/steps/tlb.rs @@ -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/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(), ] } From 6b21ae2b89abfd664d5fbdb0830a19172371aef0 Mon Sep 17 00:00:00 2001 From: gaok1 Date: Thu, 4 Jun 2026 12:05:32 -0300 Subject: [PATCH 02/13] =?UTF-8?q?feat(machine):=20Phase=201=20=E2=80=94=20?= =?UTF-8?q?Machine=20module=20+=20step=20journal=20(standalone)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduce src/falcon/machine/ as the single sanctioned owner of mutable simulator state, with a reversible step journal. Standalone in this phase (#[allow(dead_code)]); wired into RunState in Phase 2. - types/parse: typed edit targets (RegId/FRegId/MemWidth/RegTarget) and width-bounded parsing that rejects overflow (never truncates); accepts `_` digit separators. - journal: bounded stack of ChangeSets with a Rewind enum (CpuOnly / Delta / Full). - RAM capture at the real chokepoint Ram::store8 (write_log pre-images), correct under write-back caches; CacheSnapshot clones the cache subsystem (sans RAM). mem_mut_unjournaled preserves a Full checkpoint. - 11 unit tests: step/edit round-trip, overflow rejection, ring bound. Co-Authored-By: Claude Opus 4.8 --- src/falcon/cache/cache.rs | 2 + src/falcon/cache/controller.rs | 49 ++++++ src/falcon/cache/mod.rs | 2 +- src/falcon/cache/stats.rs | 2 +- src/falcon/machine/journal.rs | 99 ++++++++++++ src/falcon/machine/mod.rs | 260 ++++++++++++++++++++++++++++++++ src/falcon/machine/parse.rs | 104 +++++++++++++ src/falcon/machine/types.rs | 139 +++++++++++++++++ src/falcon/memory.rs | 34 +++++ src/falcon/mmu/mod.rs | 1 + src/falcon/mmu/tlb.rs | 3 +- src/falcon/mod.rs | 1 + tests/support/falcon_machine.rs | 237 +++++++++++++++++++++++++++++ 13 files changed, 930 insertions(+), 3 deletions(-) create mode 100644 src/falcon/machine/journal.rs create mode 100644 src/falcon/machine/mod.rs create mode 100644 src/falcon/machine/parse.rs create mode 100644 src/falcon/machine/types.rs create mode 100644 tests/support/falcon_machine.rs 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..e22d920 --- /dev/null +++ b/src/falcon/machine/journal.rs @@ -0,0 +1,99 @@ +//! 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, + }, +} + +/// One reversible unit of history: the CPU as it was, plus how to undo the +/// memory effects. +pub(super) struct ChangeSet { + /// Clock value at which this change was recorded (stack key, for display). + pub clock: u64, + pub cpu_before: Cpu, + 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..462c85b --- /dev/null +++ b/src/falcon/machine/mod.rs @@ -0,0 +1,260 @@ +//! `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}; +use types::{EditError, FRegId, MemWidth, RegTarget}; + +/// Default journal depth: how many steps/edits a user can rewind. +const JOURNAL_CAPACITY: usize = 1024; + +/// Owns the CPU, the memory hierarchy, and the step journal, and is the sole +/// gateway for mutating them. See the module docs for the design rationale. +pub struct Machine { + cpu: Cpu, + mem: CacheController, + 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) -> Self { + Self { + cpu, + mem, + 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 + } + + // ── 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, Rewind::Delta { cache_before, ram_log }); + outcome + } + + // ── 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, 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, 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, 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 `false` when the journal is empty. + pub fn stepback(&mut self) -> bool { + let Some(change) = self.journal.pop() else { + return false; + }; + self.cpu = change.cpu_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); + true + } + + /// 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, 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 + } + + // ── Internal ── + + /// Advance the clock and push one change-set. + fn record(&mut self, cpu_before: Cpu, rewind: Rewind) { + self.clock += 1; + self.journal.push(ChangeSet { + clock: self.clock, + cpu_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..80018fa --- /dev/null +++ b/src/falcon/machine/parse.rs @@ -0,0 +1,104 @@ +//! 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, + /// 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::Str => parse_str(input, width), + } +} + +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/tests/support/falcon_machine.rs b/tests/support/falcon_machine.rs new file mode 100644 index 0000000..aa9c1d2 --- /dev/null +++ b/tests/support/falcon_machine.rs @@ -0,0 +1,237 @@ +//! Unit tests for `falcon::machine` — included from `machine/mod.rs`. +//! +//! These prove the journal round-trips (step-back is the exact inverse of a +//! step or edit) and that overflowing edits are rejected, all before any of +//! this is wired into the UI. + +use super::parse::{parse_cell, CellFormat}; +use super::types::{EditError, FRegId, MemWidth, RegId, RegTarget}; +use super::Machine; + +use crate::falcon::cache::{CacheConfig, CacheController}; +use crate::falcon::encoder::encode; +use crate::falcon::instruction::Instruction; +use crate::falcon::memory::Bus; +use crate::falcon::registers::Cpu; +use crate::ui::Console; + +const RAM_SIZE: usize = 256; +/// Data scratch address used by the load/store programs below. +const DATA_ADDR: u32 = 0x40; + +/// Build a machine with `program` loaded at PC 0 and the given D-cache config. +/// Instructions are written straight to RAM (as a loader would), so the +/// I-cache fetches them cleanly. +fn machine_with(program: &[Instruction], dcfg: CacheConfig) -> Machine { + let mut mem = CacheController::new(CacheConfig::default(), dcfg, vec![], RAM_SIZE); + for (i, inst) in program.iter().enumerate() { + let word = encode(*inst).expect("encodable instruction"); + mem.ram_mut() + .store32(i as u32 * 4, word) + .expect("in-bounds program word"); + } + Machine::new(Cpu::default(), mem) +} + +/// A small program: set a base pointer, compute a value, store it, load it back. +fn load_store_program() -> Vec { + vec![ + Instruction::Addi { rd: 2, rs1: 0, imm: DATA_ADDR as i32 }, // x2 = 0x40 + Instruction::Addi { rd: 1, rs1: 0, imm: 5 }, // x1 = 5 + Instruction::Sw { rs2: 1, rs1: 2, imm: 0 }, // mem[x2] = x1 + Instruction::Lw { rd: 3, rs1: 2, imm: 0 }, // x3 = mem[x2] + ] +} + +/// A comparable view of architectural CPU state. +fn fingerprint(cpu: &Cpu) -> (u32, [u32; 32], u64) { + let mut regs = [0u32; 32]; + for (i, slot) in regs.iter_mut().enumerate() { + *slot = cpu.read(i as u8); + } + (cpu.pc, regs, cpu.instr_count) +} + +fn write_through_dcache() -> CacheConfig { + CacheConfig { + write_policy: crate::falcon::cache::WritePolicy::WriteThrough, + ..CacheConfig::default() + } +} + +#[test] +fn step_then_stepback_is_identity() { + let program = load_store_program(); + let mut m = machine_with(&program, CacheConfig::default()); + let mut console = Console::default(); + + let cpu0 = fingerprint(m.cpu()); + let mem0 = m.mem().effective_read32(DATA_ADDR).unwrap(); + + for _ in 0..program.len() { + m.step_interpreted(&mut console).unwrap(); + } + // The program actually changed something. + assert_eq!(m.cpu().read(3), 5, "lw should have loaded the stored value"); + assert_ne!(fingerprint(m.cpu()), cpu0); + + while m.can_stepback() { + assert!(m.stepback()); + } + assert_eq!(fingerprint(m.cpu()), cpu0, "CPU must round-trip to its start"); + assert_eq!( + m.mem().effective_read32(DATA_ADDR).unwrap(), + mem0, + "effective memory must round-trip to its start" + ); +} + +#[test] +fn stepback_restores_memory_store() { + // Write-through so the store reaches RAM and `peek32` (raw RAM) observes it + // — this exercises the byte-level pre-image log specifically. + let program = load_store_program(); + let mut m = machine_with(&program, write_through_dcache()); + let mut console = Console::default(); + + for _ in 0..program.len() { + m.step_interpreted(&mut console).unwrap(); + } + assert_eq!(m.mem().peek32(DATA_ADDR).unwrap(), 5, "store reached RAM"); + + while m.can_stepback() { + m.stepback(); + } + assert_eq!(m.mem().peek32(DATA_ADDR).unwrap(), 0, "RAM reverted to zero"); +} + +#[test] +fn write_reg_journaled_undo() { + let mut m = machine_with(&[], CacheConfig::default()); + let x5 = RegTarget::X(RegId::new(5).unwrap()); + + m.write_reg(x5, 0xDEAD).unwrap(); + assert_eq!(m.cpu().read(5), 0xDEAD); + + assert!(m.stepback()); + assert_eq!(m.cpu().read(5), 0, "register reverted"); + assert!(!m.can_stepback()); +} + +#[test] +fn write_pc_and_freg_journaled_undo() { + let mut m = machine_with(&[], CacheConfig::default()); + + m.write_reg(RegTarget::Pc, 0x80).unwrap(); + m.write_freg(FRegId::new(3).unwrap(), 0x4048_F5C3); // 3.14f bits + assert_eq!(m.cpu().pc, 0x80); + assert_eq!(m.cpu().fread_bits(3), 0x4048_F5C3); + + m.stepback(); // undo freg + assert_eq!(m.cpu().fread_bits(3), 0); + m.stepback(); // undo pc + assert_eq!(m.cpu().pc, 0); +} + +#[test] +fn write_reg_x0_rejected() { + let mut m = machine_with(&[], CacheConfig::default()); + let x0 = RegTarget::X(RegId::new(0).unwrap()); + + assert_eq!(m.write_reg(x0, 0xFFFF), Err(EditError::X0Immutable)); + assert_eq!(m.cpu().read(0), 0, "x0 stays zero"); + assert!(!m.can_stepback(), "a rejected edit journals nothing"); +} + +#[test] +fn write_mem_edit_journaled_undo() { + let mut m = machine_with(&[], write_through_dcache()); + + m.write_mem(DATA_ADDR, MemWidth::B4, 0xCAFE_BABE).unwrap(); + assert_eq!(m.mem().effective_read32(DATA_ADDR).unwrap(), 0xCAFE_BABE); + + m.stepback(); + assert_eq!(m.mem().effective_read32(DATA_ADDR).unwrap(), 0, "edit undone"); +} + +#[test] +fn instr_word_edit_visible_to_fetch() { + // Editing the instruction word in memory changes what a later fetch decodes. + let original = encode(Instruction::Addi { rd: 1, rs1: 0, imm: 5 }).unwrap(); + let replacement = encode(Instruction::Addi { rd: 1, rs1: 0, imm: 9 }).unwrap(); + let mut m = machine_with(&[Instruction::Addi { rd: 1, rs1: 0, imm: 5 }], write_through_dcache()); + + assert_eq!(m.mem().peek32(0).unwrap(), original); + m.write_mem(0, MemWidth::B4, replacement as u64).unwrap(); + assert_eq!(m.mem().effective_read32(0).unwrap(), replacement, "fetch sees new word"); + + let mut console = Console::default(); + m.step_interpreted(&mut console).unwrap(); + assert_eq!(m.cpu().read(1), 9, "edited immediate took effect"); +} + +#[test] +fn checkpoint_restores_full_state() { + // A checkpoint is the only way to rewind writes that bypass the byte log. + let mut m = machine_with(&[], write_through_dcache()); + m.write_mem(DATA_ADDR, MemWidth::B4, 0x1111_1111).unwrap(); + + m.checkpoint(); + // Mutate via the unjournaled hatch (as a GO burst would). + m.mem_mut_unjournaled() + .ram_mut() + .store32(DATA_ADDR, 0x2222_2222) + .unwrap(); + assert_eq!(m.mem().peek32(DATA_ADDR).unwrap(), 0x2222_2222); + + assert!(m.stepback(), "step back to the checkpoint"); + assert_eq!(m.mem().peek32(DATA_ADDR).unwrap(), 0x1111_1111); +} + +#[test] +fn journal_ring_bounded() { + let mut m = machine_with(&[], CacheConfig::default()); + let x5 = RegTarget::X(RegId::new(5).unwrap()); + for i in 0..2000u32 { + m.write_reg(x5, i).unwrap(); + } + assert_eq!(m.journal_depth(), 1024, "oldest entries evicted at capacity"); +} + +#[test] +fn parse_cell_overflow_matrix() { + use CellFormat::{Dec, Hex}; + use MemWidth::{B1, B4}; + + assert_eq!( + parse_cell("0x1FF", B1, Hex, false), + Err(EditError::OutOfRange { width: B1, signed: false }) + ); + assert_eq!( + parse_cell("256", B1, Dec, false), + Err(EditError::OutOfRange { width: B1, signed: false }) + ); + assert_eq!( + parse_cell("-129", B1, Dec, true), + Err(EditError::OutOfRange { width: B1, signed: true }) + ); + assert_eq!(parse_cell("-128", B1, Dec, true), Ok(0x80)); + assert_eq!(parse_cell("0xFF", B1, Hex, false), Ok(0xFF)); + // `_` digit-group separators are accepted in numeric input. + assert_eq!(parse_cell("1_000", B4, Dec, false), Ok(1000)); + assert_eq!(parse_cell("0xFFFFFFFF", B4, Hex, false), Ok(0xFFFF_FFFF)); + assert!(matches!( + parse_cell("0x1_0000_0000", B4, Hex, false), + Err(EditError::OutOfRange { .. }) + )); + assert!(matches!(parse_cell("xyz", B4, Hex, false), Err(EditError::ParseFailed { .. }))); +} + +#[test] +fn parse_cell_str_packs_little_endian() { + assert_eq!(parse_cell("AB", MemWidth::B2, CellFormat::Str, false), Ok(0x4241)); + assert!(matches!( + parse_cell("ABC", MemWidth::B2, CellFormat::Str, false), + Err(EditError::OutOfRange { .. }) + )); +} From 5813b1981fedc9fffe65a0a4fdd0f657e81e1717 Mon Sep 17 00:00:00 2001 From: Luis Phillip Lemos Martins <107713925+Gaok1@users.noreply.github.com> Date: Thu, 4 Jun 2026 23:19:22 -0300 Subject: [PATCH 03/13] =?UTF-8?q?feat(machine):=20Phase=202=20=E2=80=94=20?= =?UTF-8?q?wire=20Machine=20into=20RunState=20(mechanical=20rename)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace RunState's `pub(crate) cpu` / `pub(crate) mem` fields with a single `machine: Machine`, routing the ~262 read sites through thin `run.cpu()` / `run.mem()` accessors and every mutation through the journaling gateway's `*_unjournaled()` escape hatches. No behavior change: the journal stays empty until Phase 3 wires execution through `step_interpreted`, so the unjournaled clears are no-ops here. - RunState.machine is pub(crate) (not private): the type-safety that matters is Machine's own private cpu/mem fields (mutation only via named methods). A private field would break the disjoint `self.run.machine` vs `self.run.backend` borrows used in GO / background-hart stepping. - Add Machine::cpu_mem_mut_unjournaled() -> (&mut Cpu, &mut CacheController) for the four sites needing both &mut at once (single-step ExecCtx, pipeline_tick, and the two sync_mmu_to_cpu calls) — two separate &mut self accessors can't stack. - Reads renamed across view/input/app; reset/load/config mutations go through cpu_mut_unjournaled()/mem_mut_unjournaled(); test support helpers migrated too. cargo build (default + --features jit) + 357 tests + clippy --all-targets green. Co-Authored-By: Claude Opus 4.8 --- src/falcon/machine/mod.rs | 13 ++ src/ui/app/mod.rs | 302 +++++++++++++------------ src/ui/app/run_state.rs | 18 +- src/ui/app/runtime.rs | 42 ++-- src/ui/input/keyboard/intercepts.rs | 2 +- src/ui/input/keyboard/run_keys.rs | 4 +- src/ui/input/keyboard/serialization.rs | 23 +- src/ui/input/keyboard/tlb_keys.rs | 6 +- src/ui/input/mouse.rs | 41 ++-- src/ui/tutorial/steps/tlb.rs | 2 +- src/ui/view/cache/config.rs | 8 +- src/ui/view/cache/mod.rs | 6 +- src/ui/view/cache/stats.rs | 40 ++-- src/ui/view/cache/view.rs | 14 +- src/ui/view/editor.rs | 2 +- src/ui/view/run/formatting.rs | 12 +- src/ui/view/run/instruction_details.rs | 16 +- src/ui/view/run/instruction_list.rs | 6 +- src/ui/view/run/sidebar.rs | 32 +-- src/ui/view/run/status.rs | 12 +- src/ui/view/tlb/config.rs | 2 +- src/ui/view/tlb/entries.rs | 2 +- src/ui/view/tlb/mod.rs | 2 +- src/ui/view/tlb/page_tree.rs | 4 +- src/ui/view/tlb/stats.rs | 4 +- src/ui/view/tlb/status.rs | 2 +- tests/support/ui_app_internal.rs | 192 ++++++++-------- tests/support/ui_input_keyboard.rs | 10 +- tests/support/ui_input_mouse.rs | 6 +- 29 files changed, 441 insertions(+), 384 deletions(-) diff --git a/src/falcon/machine/mod.rs b/src/falcon/machine/mod.rs index 462c85b..77fe782 100644 --- a/src/falcon/machine/mod.rs +++ b/src/falcon/machine/mod.rs @@ -242,6 +242,19 @@ impl Machine { &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. diff --git a/src/ui/app/mod.rs b/src/ui/app/mod.rs index e24fae0..1695cb6 100644 --- a/src/ui/app/mod.rs +++ b/src/ui/app/mod.rs @@ -357,16 +357,18 @@ impl App { show_encoding: false, }, run: RunState { - cpu, + machine: crate::falcon::machine::Machine::new( + cpu, + CacheController::new( + CacheConfig::default(), + CacheConfig::default(), + vec![], + mem_size, + ), + ), 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, @@ -582,46 +584,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); + self.pipeline.reset_stages(self.run.cpu().pc); } 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 +638,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; @@ -675,7 +682,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 +690,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); + self.pipeline.reset_stages(self.run.cpu().pc); self.editor.last_assemble_msg = Some(format!( "Assembled {} instructions, {} data bytes, {} bss bytes.", @@ -786,27 +793,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 +827,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 +840,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 +851,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]; @@ -871,7 +883,7 @@ impl App { 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); + self.pipeline.reset_stages(self.run.cpu().pc); self.rebuild_harts(); } } @@ -879,7 +891,7 @@ impl App { pub(super) fn restart_simulation(&mut self) { self.run.is_running = false; self.run.faulted = false; - self.run.cpu.ebreak_hit = 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 +902,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); + self.pipeline.reset_stages(self.run.cpu().pc); 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 +919,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 +948,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 +967,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 +1027,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 +1041,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 +1060,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) { @@ -1096,9 +1113,9 @@ impl App { 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); + self.pipeline.reset_stages(self.run.cpu().pc); self.rebuild_harts(); - self.pipeline.reset_stages(self.run.cpu.pc); + self.pipeline.reset_stages(self.run.cpu().pc); } /// Convert the currently-loaded ELF into an editable assembly source and load @@ -1301,7 +1318,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 +1434,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 +1591,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 +1620,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 +1649,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 +1690,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 +1729,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 +1797,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 +1831,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); @@ -1969,11 +1986,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,14 +2010,14 @@ 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 { @@ -2008,7 +2025,7 @@ impl App { } } else if self.run.faulted || self.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.pipeline.halted { HartLifecycle::Exited } else { HartLifecycle::Running @@ -2028,11 +2045,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,7 +2063,7 @@ 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; } } @@ -2105,8 +2122,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 +2132,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 +2141,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,7 +2174,7 @@ 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; @@ -2177,11 +2194,11 @@ impl App { if self.pipeline.sequential_mode { let all_clear = self.pipeline.stages.iter().all(|s| s.is_none()); if all_clear - && self.pipeline.fetch_pc != self.run.cpu.pc + && self.pipeline.fetch_pc != self.run.cpu().pc && !self.pipeline.halted && !self.pipeline.faulted { - self.pipeline.reset_stages(self.run.cpu.pc); + self.pipeline.reset_stages(self.run.cpu().pc); } } @@ -2191,26 +2208,32 @@ impl App { // 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); + { + let (cpu, mem) = self.run.machine.cpu_mem_mut_unjournaled(); + crate::ui::app::hart::sync_mmu_to_cpu(mem, 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, - ); + let commit = { + let (cpu, mem) = self.run.machine.cpu_mem_mut_unjournaled(); + crate::ui::pipeline::sim::pipeline_tick( + &mut self.pipeline, + cpu, + mem, + &cpi, + &mut self.console, + ) + }; 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 +2246,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,19 +2254,20 @@ 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.mem_mut_unjournaled().instruction_count = + self.run.mem().instruction_count.saturating_add(1); + self.run.machine.mem_mut_unjournaled().snapshot_stats(); !is_transparent_single_step_word(word) } else { false @@ -2252,7 +2276,7 @@ impl App { if self.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(); @@ -2295,11 +2319,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 +2357,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 +2368,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; @@ -2492,16 +2516,19 @@ 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); + { + let (cpu, mem) = self.run.machine.cpu_mem_mut_unjournaled(); + crate::ui::app::hart::sync_mmu_to_cpu(mem, cpu); + } 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,20 +2539,17 @@ 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, - ); + let (cpu, mem) = self.run.machine.cpu_mem_mut_unjournaled(); + let mut ctx = crate::falcon::jit::ExecCtx::new(cpu, mem, &mut self.console); if go_mode { // Run mode: usa o backend JIT completo (blocos compilados). self.run.backend.run_until_yield(&mut ctx) @@ -2568,8 +2592,8 @@ impl App { (false, 1) } }; - self.run.mem.add_instruction_cycles(cpi_cycles); - self.run.mem.snapshot_stats(); + self.run.machine.mem_mut_unjournaled().add_instruction_cycles(cpi_cycles); + self.run.machine.mem_mut_unjournaled().snapshot_stats(); // Track every instruction the JIT block executed (not just block entry). // For the interpreter jit_instr_count == 1, so this is equivalent. @@ -2597,7 +2621,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 +2629,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 +2652,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..39ee98c 100644 --- a/src/ui/app/run_state.rs +++ b/src/ui/app/run_state.rs @@ -1,5 +1,6 @@ use super::CpiConfig; use crate::falcon::jit::ExecutionBackend; +use crate::falcon::machine::Machine; use crate::falcon::{CacheController, Cpu, registers::ExecRegion}; use crate::ui::editor::Editor; use std::time::{Duration, Instant}; @@ -139,13 +140,26 @@ impl RunState { pub(crate) fn vm_enabled(&self) -> bool { self.vm_mode != crate::falcon::mmu::VmMode::Off } + + /// 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() + } } 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, diff --git a/src/ui/app/runtime.rs b/src/ui/app/runtime.rs index d3ca464..790f3ea 100644 --- a/src/ui/app/runtime.rs +++ b/src/ui/app/runtime.rs @@ -162,8 +162,8 @@ 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(); } @@ -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,7 +280,7 @@ impl App { pub(crate) fn reconfigure_pipeline_model(&mut self) { self.run.is_running = false; - self.pipeline.reset_stages(self.run.cpu.pc); + self.pipeline.reset_stages(self.run.cpu().pc); for (idx, hart) in self.harts.iter_mut().enumerate() { if idx == self.selected_core { @@ -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; @@ -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; @@ -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; @@ -402,7 +402,7 @@ impl App { .unwrap_or_else(crate::ui::pipeline::PipelineSimState::new); Self::copy_pipeline_config_to_hart(&self.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; } @@ -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,7 +522,7 @@ 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); @@ -530,7 +530,7 @@ impl App { } 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; @@ -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/input/keyboard/intercepts.rs b/src/ui/input/keyboard/intercepts.rs index df3513d..c5e6c34 100644 --- a/src/ui/input/keyboard/intercepts.rs +++ b/src/ui/input/keyboard/intercepts.rs @@ -322,7 +322,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/run_keys.rs b/src/ui/input/keyboard/run_keys.rs index 5b14eb2..d360589 100644 --- a/src/ui/input/keyboard/run_keys.rs +++ b/src/ui/input/keyboard/run_keys.rs @@ -193,7 +193,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 +201,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..2413245 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(); @@ -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 {}", @@ -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 {}", 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..f1ef64a 100644 --- a/src/ui/input/mouse.rs +++ b/src/ui/input/mouse.rs @@ -214,7 +214,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)); } @@ -530,7 +530,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 +538,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; } @@ -1532,8 +1532,8 @@ 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; + app.run.prev_pc = app.run.cpu().pc; + app.run.machine.cpu_mut_unjournaled().pc = addr; if app.pipeline.enabled { app.pipeline.redirect_pc(addr); } @@ -2244,16 +2244,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 +2319,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; diff --git a/src/ui/tutorial/steps/tlb.rs b/src/ui/tutorial/steps/tlb.rs index cbeb26d..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; 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/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/run/formatting.rs b/src/ui/view/run/formatting.rs index 2053c6e..0456788 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, ), diff --git a/src/ui/view/run/instruction_details.rs b/src/ui/view/run/instruction_details.rs index 5e644af..b4706a5 100644 --- a/src/ui/view/run/instruction_details.rs +++ b/src/ui/view/run/instruction_details.rs @@ -39,7 +39,7 @@ 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()), ); } @@ -65,7 +65,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], @@ -101,14 +101,14 @@ 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); + 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") + } 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", @@ -165,7 +165,7 @@ fn render_header(f: &mut Frame, area: Rect, ctx: &DetailContext, app: &App) { let base_cycles = crate::ui::app::classify_cpi_for_display( ctx.word, ctx.addr, - &app.run.cpu, + app.run.cpu(), cpi, app.pipeline.enabled, ); diff --git a/src/ui/view/run/instruction_list.rs b/src/ui/view/run/instruction_list.rs index 2f3572c..6b59257 100644 --- a/src/ui/view/run/instruction_list.rs +++ b/src/ui/view/run/instruction_list.rs @@ -252,9 +252,9 @@ fn heat_color(n: u64) -> Color { const HOVER_BG: Color = theme::BG_HOVER; 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_hover = !is_pc && app.run.hover_imem_addr == Some(addr); // Collect non-selected harts that are currently at this address. @@ -319,7 +319,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 diff --git a/src/ui/view/run/sidebar.rs b/src/ui/view/run/sidebar.rs index fbdba49..cbcc08c 100644 --- a/src/ui/view/run/sidebar.rs +++ b/src/ui/view/run/sidebar.rs @@ -147,17 +147,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 +172,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 +201,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() { @@ -387,7 +387,7 @@ fn memory_title_section<'a>(app: &'a App, addr: u32) -> &'a str { } 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 +419,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 +457,28 @@ fn mem_age_style(age: u8) -> Option