summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs
diff options
context:
space:
mode:
authorArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
committerArthur Hanson Rasmusson <arthur@vgpu.io>2026-10-04 18:42:36 -0400
commite214609c2f60e35254c243a806a82a0f1d4ec3ae (patch)
tree1731b30a65f1cd45ebea9155d1bf882017b17dc3 /debug/risc-v-gsp-rm-vm/docs
parent09b50bfee94fe21088fa1449e16153b9a413b44b (diff)
downloadopen-iov-e214609c2f60e35254c243a806a82a0f1d4ec3ae.tar.gz
open-iov-e214609c2f60e35254c243a806a82a0f1d4ec3ae.zip
greeneye: GMMU/FBIF observability, PTE decoder and oracle, MPU/libos simulator wave
Simulator development wave (docs 42-44): GMMU page-table entry decode with its oracle/workflow harness, FBIF observability, and MPU/LIBOS device surfaces in the device model, plus refreshed boot/machine/profile data (tu104-570.144), the qemu-line results, and the new unit tests. Docs 00/06/35-41 updated to match; harness helpers and .gitignore included.
Diffstat (limited to 'debug/risc-v-gsp-rm-vm/docs')
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md12
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md8
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md20
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md127
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md47
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md2
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md8
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md45
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/42-GMMU-FBIF-OBSERVABILITY.md203
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/43-GMMU-PTE-DECODER.md206
-rw-r--r--debug/risc-v-gsp-rm-vm/docs/44-GMMU-ORACLE-AND-WORKFLOW.md254
11 files changed, 862 insertions, 70 deletions
diff --git a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
index 50b4428..a5f14ab 100644
--- a/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
+++ b/debug/risc-v-gsp-rm-vm/docs/00-OVERVIEW.md
@@ -44,12 +44,12 @@ whole stack scriptable, and QEMU is just the first execution backend.
| Plugin loader + example plugin | working |
| Native RV64IMAC simulator (M1, `--backend native`) | working; QEMU-parity differential test |
| Native exec/access trace (M1.5, R4): `--trace`, analyzer coverage | working; trace blob + determinism test |
-| Test suite | full suite green on the 2026-10-03 review host (298 tests incl. live QEMU + native differential; count varies with local prerequisites) |
+| Test suite | full suite green on the 2026-10-03 review host (417 tests incl. live QEMU + native differential; count varies with local prerequisites) |
| Ghidra bridge | scripts + docs |
| Real GSP `.fwimage` probe (first-fault report) | working (first fault + lenient continuation) |
-| GSP device model + versioned profiles (S1) | in development: `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`; B0–B2 on the real 570.144 image with 0 unknown MMIO (36) |
+| GSP device model + versioned profiles (S1) | in development: `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`, XMPU model `sim/mpu.py`, libos service model `sim/libos.py`; B0–B3 on the real 570.144 image with 0 unknown MMIO (36) |
| Modeled cycle accounting | increment 0: `sim/cycles.py`; modeled defaults, not silicon timing (32) |
-| Simulator-vs-QEMU A/B + new-version triage | working: frozen thresholds, verdict `above` on the S1 path per docs/37 §4.3; `harness/triage.py` (37, 40) |
+| Simulator-vs-QEMU A/B + new-version triage | working: frozen thresholds, verdict `above` on the S1 path per docs/37 §4.4; `harness/triage.py` (37, 40) |
| GPGPU-Sim overlay | Phase 0 scaffold + CUDA-12 build port record; Phase G gated (30) |
| TCG full-memory tracing plugin | source + optional build (verified live) |
| Automation harness (mutation/portability/version diff) + value demos | working (18/19/21/25) |
@@ -63,7 +63,7 @@ The in-development notices were removed on 2026-10-02 after the tool audit
end-to-end s01–s10 value demonstrations
([25-VALUE-VALIDATION](25-VALUE-VALIDATION.md)). Open items stay listed as
open: docs 28/29 are unadmitted pilots, the device model is partial (S1,
-B0–B2; docs/36) and custom-simulator M2+ remains roadmap, and nothing here is
+B0–B3; docs/36) and custom-simulator M2+ remains roadmap, and nothing here is
a stability guarantee beyond the audited v0.1 surface.
## Quick start
@@ -127,6 +127,10 @@ Interactively:
| [38-GSP-EM-API](38-GSP-EM-API.md) | GSP-emulator/system-simulator TLV boundary (design) |
| [39-RE-WORKFLOW-REVIEW](39-RE-WORKFLOW-REVIEW.md) | reverse-engineering workflow review |
| [40-NEW-VERSION-TRIAGE](40-NEW-VERSION-TRIAGE.md) | one-command new-version triage |
+| [41-STABILITY-AND-TIME-TO-VALUE](41-STABILITY-AND-TIME-TO-VALUE.md) | formal stability and value report |
+| [42-GMMU-FBIF-OBSERVABILITY](42-GMMU-FBIF-OBSERVABILITY.md) | observe-only GMMU/FBIF windows and trace (in development) |
+| [43-GMMU-PTE-DECODER](43-GMMU-PTE-DECODER.md) | TU104 GMMU PTE/PDE decoder (in development) |
+| [44-GMMU-ORACLE-AND-WORKFLOW](44-GMMU-ORACLE-AND-WORKFLOW.md) | GMMU/FBIF oracle comparison and static<->dynamic join (in development) |
| [RISC-V-GSP-RM-DEBUGGER-ROADMAP](../RISC-V-GSP-RM-DEBUGGER-ROADMAP.md) | long-term program H1–H7 |
## Where this fits in the Open-IOV workflow
diff --git a/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md
index a9a91aa..1a85ec5 100644
--- a/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md
+++ b/debug/risc-v-gsp-rm-vm/docs/06-ACCURACY.md
@@ -124,9 +124,11 @@ Before trusting a risc-v-gsp-rm-debugger session:
## 8. Known accuracy gaps (living list)
1. Partial TU104 device model (S1, docs/36): the real 570.144 image is now
- served past the first unmapped access to B2 (relocation copy, 0 unknown
- MMIO) with a recorded boot shim; B3/B4 (CSB poll, message loop) are not
- reached, and the model is uncalibrated and partial.
+ served past the first unmapped access to B3 (libos init complete; the
+ message-loop task blocks in the port wait, 0 unknown MMIO) with the XMPU
+ model, a recorded boot shim and the libos service model; B4/B5
+ (host-traffic-triggered CSB poll, scripted message) are not reached, and
+ the model is uncalibrated and partial.
2. No time/concurrency model; multi-core and partition-switch behaviour is
approximated. Cycles are modeled defaults, not measured (docs/32).
3. PMU/SEC2/ACR/Booter behaviour is message-level only.
diff --git a/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md b/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md
index 83954d4..d5bf4a4 100644
--- a/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md
+++ b/debug/risc-v-gsp-rm-vm/docs/35-GREENEYE-SIMULATOR-DESIGN.md
@@ -129,11 +129,20 @@ reproducible twice identically and have its device requirements listed.
|---|---|---|
| B0 | flat image loads; first MMIO observed at `0x2000000000000A00` | done (native, lenient) |
| B1 | device model serves the whole boot path: 0 unknown MMIO, 0 unhandled in windows; deterministic 2M-instruction run stopping at the boot-halt/report loop (`0x4010928`) | done (S1 device model; sessions `dumps/gsp-boot-b1-run1/2`) |
-| B2 | boot stage progress: vector table/TCM/MPU/trap handling exercised with the shim; passes the bootargs consumer and reaches the relocation copy, then stops on the known DMEM-runspace unknown (`fault @ 0x400EA0C`; docs/36 §4) | done (shim) |
-| B3 | first CSB/TRANSCOM mailbox poll with correct read-back semantics | target |
-| B4 | message loop reached (`appMain`/equivalent); waits on host message deterministically | target |
+| B2 | boot stage progress: vector table/TCM/MPU/trap handling exercised with the shim; passes the bootargs consumer and reaches the relocation copy | done (S1 device model + XMPU model + bootstrap-args shim; reaches B3) |
+| B3 | libos init completes (TCM registration, pagetable registration, DMA flush, external-interrupt arming) and the message-loop task blocks in the port wait | done (sessions `dumps/gsp-boot-b3-run1/2`: 474 043 insns, stop `ecall` at `0x4018038`, two-run identical) |
+| B4 | first CSB/TRANSCOM mailbox poll served with correct read-back semantics (host-traffic-triggered doorbell) | target |
| B5 | one scripted host-side RM message served end-to-end with a recorded reply | target (gated by S3 injection) |
+**Ladder revision (2026-10-03, B3).** The original B3 wording assumed the
+firmware polls CSB/TRANSCOM during a silent boot. The 570.144 image does not:
+after arming interrupts and completing its libos services it blocks in the
+message-loop port wait, and the CSB/TRANSCOM poll is *triggered by host
+traffic*. B3 is therefore anchored on the observed wait (a real, reproducible
+milestone) and the CSB poll moved to B4, where a scripted doorbell will
+produce it. The pre-revision text is kept in history; this note is the
+operative definition.
+
The ladder is deliberately versioned-data driven: a new firmware version is a
new profile + shim record, not a code fork.
@@ -211,7 +220,7 @@ flags; every call attributable to a session.
| Probe/record/plugins | yes | — | backend peer | yes |
| Ghidra symbol/control bridges | yes | watch/heat-map/nav | protocol-compatible | full (S2/S3) |
| Injection SDK | demo | RM-core interception H1.3 | demo backend | services vs GSP model (S3) |
-| Device model / real boot | no (faults) | H2.2/H2.3/H2.4 planned | ladder B0–B2 (S1, device model + shim) | B4/B5 (S1/S3) |
+| Device model / real boot | no (faults) | H2.2/H2.3/H2.4 planned | ladder B0–B3 (S1, device model + XMPU + libos services) | B4/B5 (S1/S3) |
| Determinism/replay | wall-clock | H2.4 budgets | instruction-count | event replay (S2) |
| MCP | yes (QEMU) | — | native sim tools + backend selection (20) | wider GSP-model tools (S3) |
| Tiering | — | — | modeled cycles | calibrated bands (S4) |
@@ -277,12 +286,13 @@ The comparison and decision are recorded in the private handoff.
| Wave | Date | Result |
|---|---|---|
-| Recon + master handoff | 2026-10-03 | `handoff-simulator.md`, local sources register |
+| Recon + master handoff | 2026-10-03 | master handoff; source register |
| W1 GPGPU-Sim build | 2026-10-03 | CUDA 12 port patches; `vectorAdd` functional+perf green |
| W1 TU104 config/overlay | 2026-10-03 | config scaffold + sanity 26/26 + admissions draft |
| W1 GSP cycle-0 | 2026-10-03 | modeled cycles; real image 200k instr deterministic; 298 tests green (2026-10-03 suite run; count varies with prerequisites) |
| W1 dossier | 2026-10-03 | Turing/Ampere/Blackwell parameter docs + cross-generation survey |
| W2 Phase S (S1 + S4) | 2026-10-03 | `sim/devices` + `sim/profiles` device model: B0–B2 on the real image (0 unknown MMIO), 2-run determinism; A/B harness discovers the profile and records verdict `above` on the frozen checks (docs/36, docs/37 §4.3, docs/40) |
+| W2 Phase S (4c: XMPU + libos) | 2026-10-03 | `sim/mpu.py` + `sim/libos.py` + bootstrap-args shim: B3 on the real image (474 043 instr, message-loop port wait, 0 unknown MMIO, 2-run identical); A/B re-run at `--instructions 2000000` verdict `above` (docs/37 §4.4); 417 tests green |
| W2 Phase S | launched | S2 parity, S3 MCP/injection, S4 validation |
Backlog (recorded, not started): Ampere/Blackwell models; fabric (PCIe,
diff --git a/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md b/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md
index 9516fd5..2741748 100644
--- a/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md
+++ b/debug/risc-v-gsp-rm-vm/docs/36-GSP-DEVICE-MODEL.md
@@ -7,7 +7,7 @@
> `06-ACCURACY.md`. Development-only: nothing here authorises GPU hardware,
> kernel modules, mdev, guest access or reboots.
>
-> **See also:** `37-SIM-QEMU-LINE.md` (the B0–B2 device model on the frozen
+> **See also:** `37-SIM-QEMU-LINE.md` (the B0–B3 device model on the frozen
> simulator-vs-QEMU checks) and `40-NEW-VERSION-TRIAGE.md` (one-command triage
> of a new image through this ladder).
@@ -47,13 +47,15 @@ sim/devices/__init__.py registry_from_profile(profile, machine)
│ FBIF control/transcfg
├─ priscv.py RISC-V-local block: IRQ mask, trace, error regs
├─ timer.py PTIMER (modeled virtual time; opt-in)
- ├─ csb.py CSB/TRANSCOM message window seam (B3 landing)
+ ├─ csb.py CSB/TRANSCOM message window seam (B4 landing)
├─ fwsec.py WPR2 address pair (FWSEC state unmodeled)
├─ pmu.py PMU stub: observes, never fakes
├─ dma.py aperture → RAM translation seam (FBGPA/SYSGPA)
└─ irq.py falcon + RISC-V-local IRQ aggregation
▼
sim/machine.py Machine(devices=..., translator=..., reset_regs=...)
+ │ + sim/mpu.py: XMPU entry model (fetch/load/store)
+ │ + sim/libos.py: libos syscall service model
▼
sim/gsp_boot.py bounded real-firmware run + session + rung
```
@@ -80,7 +82,7 @@ noted; every unlisted offset is unhandled and logged.
| RISC-V local | `+0x111000`, alias `0x300001000` | `IRQMSET` 0x2ac, `IRQMCLR` 0x2b0, `IRQMASK` 0x2b4 (read), `IRQDEST` 0x2b8, `IRQTYPE` 0x2bc, `TRACECTL` 0x344, `TRACE_RDIDX` 0x348, `TRACE_WTIDX` 0x34c, `PRIV_ERR_STAT` 0x360, `PRIV_ERR_INFO` 0x364 (read), `PRIV_ERR_ADDR` 0x368 (read), `HUB_ERR_STAT` 0x36c | header + observed |
| PTIMER | `+0x9000` | `TIME_0` aperture 0x9400 / block `+0x400` (NSEC 31:5), `TIME_1` aperture 0x9410 / block `+0x410` (NSEC 28:0); value = instruction count × `ns_per_insn` (**modeled**, not calibrated) | header + **modeled** time |
| WPR2 | `+0x1FA000` | `WPR2_ADDR_LO` 0x1FA824, `WPR2_ADDR_HI` 0x1FA828 (4 KiB alignment; HI points at the last 128 KiB block) | header + firmware use |
-| CSB/TRANSCOM | profile-declared | none by default; a profile may declare individual registers with width/value and `rw`/`ro`/`wo`/`w1c`/`set`/`clear` semantics | none yet (B3 landing point) |
+| CSB/TRANSCOM | profile-declared | none by default; a profile may declare individual registers with width/value and `rw`/`ro`/`wo`/`w1c`/`set`/`clear` semantics | none yet (B4 landing point) |
| PMU | profile-declared | none; the stub records accesses and returns unhandled | none yet |
| DMA seam | FBGPA/SYSGPA (profile) | GPU-physical → RAM windows for falcon DMA and aperture loads/stores; unmapped transfers are refused and recorded | address-map header + firmware use |
@@ -99,10 +101,15 @@ and records each as an assumption in `session.json.boot_shim.assumptions`:
* `NV_RISCV_BOOT_PARAMS` at a RAM address (bootType `NVRM`, size 120,
version 1, core-dump buffer, one `WPRM` libos init arg);
* a `GspFwWprMeta` (magic/revision/verified and a self-consistent FB layout);
-* raw-image mirror at the DMEM aperture base so the app's own relocation copy
- is self-consistent (it is not: see §6);
-* `a0`–`a5` register arguments (bootargs PA/VA, ELF PA/size, load base, WPR
- id).
+* a `libos_bootstrap_args_t` — six `u64` fields (IMEM and DMEM
+ `base`/`size`/`bs`) the real bootloader builds from its TCM configuration
+ and passes in `a0`; the M-mode TCM configuration is declared in the
+ profile (offsets 0, 64 KiB windows). This replaces the earlier
+ raw-image mirror at the DMEM aperture: with the XMPU model (§3.2) the
+ resident-section copy reads its real backing and no image mirror is needed;
+* `a0`–`a5` register arguments (a0 = bootstrap-args pointer; the rest are
+ declared assumptions the boot path only uses where its own computation is
+ not self-sufficient).
The DEBUGINFO **host-consume** behaviour is also a shim: the firmware writes a
boot-stage marker and polls bits 22:20 until they clear, and no self-clearing
@@ -110,28 +117,86 @@ path exists in the firmware, so the host side is modelled as consuming the
stage marker on read. The device never changes a read value except for this
documented shim, and the shim is profile-enabled and session-recorded.
+### 3.2 CPU protection unit (XMPU) — core model, `sim/mpu.py`
+
+The GSP core boots in bare mode and then enables a small protection unit
+through M-mode CSRs; the firmware programs entries for its code/data ranges
+and, during init, for a scratch window it copies resident sections through.
+Evidence kind: **hardware CSR/field header + firmware use + observed access
+sequence** (the 570.144 bootstrap writes are visible in any run's device/CSR
+trace). Modelled CSRs:
+
+| CSR | value | semantics |
+|---|---|---|
+| `MMPUIDX` (0x7C8) | entry index | INDEX field 5:0 (64 entries) |
+| `MMPUVA` (0x7C9) | virtual base | BASE 63:12, `VLD` bit 0 |
+| `MMPUPA` (0x7CA) | physical base | BASE 63:12 |
+| `MMPURNG` (0x7CB) | range (a size) | RANGE 63:12 |
+| `MMPUATTR` (0x7CC) | attributes | recorded, not enforced |
+
+Translation is active only when `mstatus.VM == NVMPU` (`0x1E`; field bits
+28:24); lookup is lowest-index-first (the firmware's own bootstrap comment
+documents the precedence). An entry maps `addr` to `pa + (addr - va_base)`
+when `va_base <= addr < va_base + range`. The model applies to instruction
+fetch, loads, stores and atomics; permissions/cache attributes are recorded
+but not enforced, and **no match is treated as identity** (more lenient than
+silicon) because the simulator keeps local memory flat and the boot path's
+mappings are self-consistent. `read_mem`/`write_mem` (debugger surface)
+remain raw and untranslated.
+
+Evidence in the session: `session.json.mpu` carries the enabled flag, entry
+table and `mstatus`. The model removes the B2 ceiling: with the correct
+bootstrap args and the scratch mapping, the resident-section copy reads the
+in-place image and the run continues (no DMEM image mirror, no faked copy).
+
+### 3.3 libos syscall surface — service model, `sim/libos.py`
+
+The application is a libos task that requests kernel services with `ecall`
+(the syscall number in `t0`). The native machine stops on `ecall` unless a
+handler is installed, so the boot runner attaches a deterministic service
+model that:
+
+* records every request (number, name, arguments, pc, instruction count) in
+ `session.json.libos`;
+* acknowledges only no-ops and modelled services — task yield, cache
+ invalidation, external-interrupt arming, profiler enable/query, the
+ modeled timer read, DMA bounce-buffer registration (`TCM` region),
+ inverted-pagetable registration, and DMA copy/flush (an in-RAM copy in the
+ flat model: the real kernel drives the falcon DMA through TCM, the copy
+ effect is identical and is not faked — both sides must resolve or the run
+ stops);
+* **never fakes waits**: `processor_suspend`, `task_port_send_recv_wait`,
+ `task_panic` and every unimplemented syscall stop the run with `R_ECALL`
+ and are reported in `session.json.libos.stopped`.
+
+Task switching, interrupt delivery, pagination, partition switch and memory
+queries are not modelled (force-stopped, not invented). The B3 result is
+exactly this stop: the message-loop task blocks in `task_port_send_recv_wait`
+with `armed_mask=0xFF`.
+
## 4. Boot ladder results (observed, 2026-10-03)
| Rung | Claim | Evidence |
|---|---|---|
| **B0** | flat image loads; first MMIO is the unmodelled PMC `BOOT_42` read at `0x2000_0000_0000_0A00` | `tests/test_sim_devices.py` (B0 classification) |
| **B1** | device model serves the whole boot path: **0 unknown MMIO, 0 unhandled in windows**, deterministic 2M-instruction run stopping at the boot-halt/report loop (`0x4010928`) | `run_boot(use_shim=False)`; sessions `dumps/gsp-boot-b1-run1`, `dumps/gsp-boot-b1-run2`; test `test_no_shim_b1_device_model_serves_boot` |
-| **B2** | with the shim, the firmware passes the chip gate, MPU/vector setup, the bootargs consumer (`0x4007A30`) and reaches its **relocation copy** (`0x4007B7C`), then stops at a known unknown (DMEM runspace) | sessions `dumps/gsp-boot-b2-run1`, `dumps/gsp-boot-b2-run2`; test `test_shim_reaches_relocation_b2` |
+| **B2** | with the shim, the firmware passes the chip gate, XMPU setup, the bootstrap-args consumer and its **relocation copy** (`0x4007B7C`); the XMPU model resolves the copy's real backing and the run continues into libos init | sessions `dumps/gsp-boot-b2-run1`, `dumps/gsp-boot-b2-run2`; test `test_shim_reaches_relocation_plus_mmu` |
+| **B3** | libos init completes (TCM registration `0x5B22000`/`0x1000`, pagetable registration, DMA flush, external-interrupt arming `0xFF`) and the message-loop task blocks in the port wait | sessions `dumps/gsp-boot-b3-run1`, `dumps/gsp-boot-b3-run2`; test `test_shim_reaches_message_loop_b3` |
Reproduction (read-only; no hardware):
```bash
cd open-iov/debug/risc-v-gsp-rm-vm
-python3 -m sim.gsp_boot --instructions 2000000 --record dumps/gsp-boot-b2-run1
-python3 -m unittest tests.test_sim_devices -v
+python3 -m sim.gsp_boot --instructions 2000000 --record dumps/gsp-boot-b3-run1
+python3 -m unittest tests.test_sim_devices tests.test_mpu -v
```
Two-run determinism is asserted by `test_two_runs_identical` and was checked
-at the recorded B2 sessions: identical instruction count (14 682), stop
-reason/PC (`fault @ 0x400EA0C`), device-state SHA-256
-(`3fa264b9…`) and register file. The B1 no-shim run is likewise identical
-across runs (2 000 000 instructions, budget stop at `0x4010928`, device-state
-SHA-256 `88eb07b7…`).
+at the recorded B3 sessions: identical instruction count (474 043), stop
+reason/PC (`ecall @ 0x4018038`, syscall
+`task_port_send_recv_wait`), device-state SHA-256 (`5b3cc7e4…`) and register
+file. The B1 no-shim run is likewise identical across runs (2 000 000
+instructions, budget stop at `0x4010928`, device-state SHA-256 `88eb07b7…`).
## 5. Session evidence
@@ -139,21 +204,24 @@ Each `run_boot` invocation writes a `greeneye-session/1` directory with:
* `session.json` — firmware SHA-256, profile name, load layout, rung,
milestones, stop reason/PC, unknown-MMIO counts, device-state SHA-256,
- device-access counts and the full boot-shim assumption list;
+ device-access counts, the full boot-shim assumption list, the XMPU state
+ (`mpu`: enabled, entries, mstatus) and the libos service log (`libos`:
+ calls, counts, TCM/pagetable registrations, the stopping request);
* `blobs/device-state.json` — the `greeneye-sim-devices/1` snapshot of every
device (registers, IRQ state, DMA windows/counters, shim flags);
* `events.jsonl` — the normal session event stream (`gsp_boot` note).
## 6. Unknowns (explicitly not modeled / not verified)
-1. **DMEM runspace content.** The app's relocation copy reads the DMEM
- aperture (`0x1_0000_0000+`) and writes it to the `0x400E000` runtime
- region; the bootloader-provided content is not reconstructed, so the copy
- writes zeros and the run stops at `0x400EA0C` (illegal instruction). This
- is the B2 ceiling today.
-2. **CSB/TRANSCOM register semantics.** No message-window poll has been
- observed yet, so no register is modeled. B3 waits for the first observed
- poll; the `csb.py` seam and profile register declarations are in place.
+1. **CSB/TRANSCOM register semantics.** No message-window poll has been
+ observed yet, so no register is modeled. The 570.144 firmware blocks in
+ the message-loop port wait before any CSB access; B4 waits for the first
+ host-traffic-triggered poll; the `csb.py` seam and profile register
+ declarations are in place.
+2. **Libos services.** Task switching, interrupt delivery, pagination,
+ partition switch and memory queries are not modelled: the service model
+ records and force-stops on them (never invents progress). DMA copies are
+ modelled as in-RAM copies in the flat memory model.
3. **PMU.** No PMU register access observed; stub only.
4. **FWSEC state machine.** Only the WPR2 address pair is modeled; FWSEC
execution is not.
@@ -161,14 +229,15 @@ Each `run_boot` invocation writes a `greeneye-session/1` directory with:
not proven; only the profile's DMA windows decide what an address means.
6. **Interrupt delivery.** Falcon/RISC-V IRQ state is aggregated and
serialisable, but mstatus/mie/mip/mtvec trap entry and interrupt injection
- are not modelled.
+ are not modelled (the firmware arms external interrupts with mask `0xFF`
+ and then waits forever in the flat model).
7. **PTIMER calibration.** Values are modeled from the instruction count with
a placeholder `ns_per_insn`; no firmware PTIMER read has been observed and
no timing claim is made.
-8. **MPU translation.** The firmware programs its MPU CSRs (observed), but
- the machine does not enforce translations; the libos private mapping is
- served by explicit device aliases and the FBGPA/SYSGPA seam, not by an MPU
- model.
+8. **XMPU enforcement.** Translation, VLD gating, mstatus enable and
+ lowest-index precedence are modelled; permissions, cache/coherence
+ attributes and WPR IDs are recorded but not enforced, and an unmatched
+ access passes through as identity (lenient by design, listed here).
Nothing in this page is a silicon-accuracy claim; `06-ACCURACY.md` fidelity
levels apply (L0/L1 modelled, L2 modelled-not-calibrated, L3 partial and
diff --git a/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md b/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md
index 42c76cc..06fa943 100644
--- a/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md
+++ b/debug/risc-v-gsp-rm-vm/docs/37-SIM-QEMU-LINE.md
@@ -29,11 +29,12 @@ and analyze/compare, probes/plugins, Ghidra bridges, trace, injection SDK and
`Session.call`/`run_until`, plus the documented first fault at firmware vaddr
`0x4009014` (4-byte read of `0x2000000000000A00`).
-The boot ladder (docs/35 §3.3) is `pre-B0` (image loads, first MMIO observed
-*as a fault*) < `B0` (first MMIO observed; lenient) < `B1` (first fault past
-the QEMU point, explained by device semantics) < `B2` (boot-stage progress) <
-`B3` (first mailbox poll) < `B4` (message loop) < `B5` (one scripted message
-served).
+The boot ladder (docs/35 §3.3, revised 2026-10-03) is `pre-B0` (image loads,
+first MMIO observed *as a fault*) < `B0` (first MMIO observed; lenient) <
+`B1` (first fault past the QEMU point, explained by device semantics) <
+`B2` (boot-stage progress: relocation copy) < `B3` (libos init complete;
+message-loop task blocks in the port wait) < `B4` (first CSB/TRANSCOM poll
+served, host-traffic-triggered) < `B5` (one scripted message served).
**"Above the QEMU line"** requires *all* of the frozen checks in §3,
including the docs/35 §1.1 requirement that the milestone be reached "with
@@ -196,6 +197,33 @@ MMIO. Frozen thresholds unchanged. Measured run on the same image
`harness/qemu_line/sim-qemu-line.json|.md`). Wall figures are host-load
dependent.
+### 4.4 Re-measurement after the XMPU/libos wave (2026-10-03, session 4c)
+
+The native side now runs the real image with the XMPU (MPU) translation model
+and the libos service model (docs/36 §3.2/§3.3), so the DMEM-runspace unknown
+is gone. Fresh run, frozen thresholds unchanged, same image
+(sha256 `2d1bb5c60b208cfd…`), `--instructions 2000000`:
+
+| side | ran | rung | instructions | cycles (modeled) | stop | wall s | evidence |
+|---|---|---|---:|---:|---|---:|---|
+| QEMU probe | yes | `pre-B0` | – | – | `fault_load` fw `0x4009014` -> `0x2000000000000A00` | 0.98 | `harness/qemu_line/sessions/qemu-probe` |
+| native run 1 | yes | `B3` | 474,043 | 871,904 | `ecall@0x4018038` (`task_port_send_recv_wait`) | 0.65 | `harness/qemu_line/sessions/native-1` |
+| native run 2 | yes | `B3` | 474,043 | 871,904 | `ecall@0x4018038` | 0.66 | `harness/qemu_line/sessions/native-2` |
+
+* all five frozen checks pass: `qemu_first_fault_reproduced`,
+ `native_above_qemu`, `identical_runs` (device-state sha256
+ `5b3cc7e4af933e01…` on both runs), `native_pair_wall_s`, and
+ `device_model_documented`; **verdict `above`**.
+* milestones observed: `bootargs_consumer`, `relocation_copy`;
+ `unknown MMIO = 0`, `device_unhandled = 0`; `armed_mask=0xFF`.
+* What this does **not** claim: B4/B5 are not reached (the message-loop task
+ blocks before any CSB access without host traffic); the shim values are
+ declared assumptions; cycles are modeled (docs/32); no timing or
+ device-environment claim.
+* Command: `python3 harness/sim_qemu_line.py --instructions 2000000`
+ (regenerates `harness/qemu_line/sim-qemu-line.json|.md`; the tracked report
+ is this run). Wall figures are host-load dependent.
+
## 5. Failure signals (when to disable, per U48/U56)
* Any frozen check false **with QEMU available and the firmware present**:
@@ -227,10 +255,11 @@ report is emitted with `unverified_qemu_absent`.
## 7. Honest limits
-* **S1 partial** — the device model reaches B2 (relocation copy) with the
- recorded boot shim; B3/B4 (CSB poll, message loop) are not reached and the
- DMEM-runspace unknown is the documented ceiling (docs/36 §6). The §4.1
- lenient record predates S1; §4.3 is the post-S1 measurement.
+* **S1 partial** — the device model reaches B3 (libos init complete;
+ message-loop port wait) with the XMPU model, the recorded boot shim and the
+ libos service model (docs/36); B4/B5 (host-traffic-triggered CSB poll and a
+ scripted message) are not reached. §4.1/§4.3 are the pre-XMPU records;
+ §4.4 is the current measurement.
* **L2/L3 partial** — cycles are modeled (`rv64imac-modeled/1`, docs/32), not
measured; the device model is partial and carries per-register evidence
(docs/36). No timing or full device-environment claim is made.
diff --git a/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md b/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md
index 2b94746..d21957f 100644
--- a/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md
+++ b/debug/risc-v-gsp-rm-vm/docs/39-RE-WORKFLOW-REVIEW.md
@@ -83,7 +83,7 @@ but unfinished; docs 30–38 and the untracked `sim/devices`/`gpgpu-sim` trees),
| 4 | Native RV64IMAC backend (M1) | `sim/decode.py`, `sim/machine.py`, `greeneye/sim_backend.py` | deterministic in-process backend; QEMU parity | tests, demos, new sim work | differential test (`docs/16` §1.1); `sim_diff.py` `PARITY: OK` | verified useful (reference core) |
| 5 | Native exec/access trace (M1.5/R4) | `sim/trace.py` | full instruction/access trace as session blob | record/analyze, value demos | `docs/16` §1.1a; trace tests | verified useful |
| 6 | Modeled cycle counter | `sim/cycles.py` | table-driven modeled cycles (explicitly not silicon timing) | 21 offline tests | `docs/32`; `results.json` of `test_sim_cycles` | in development (increment 0) |
-| 7 | GSP device model + profiles + bounded boot runner | `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`, `sim/gsp_profile.py` | real 570.144 image past the QEMU first fault (B0–B2) | S1 workstream | `dumps/gsp-boot-b1-run1/2`, `b2-run1/2`; 2-run determinism asserted (`docs/36` §4) | in development (S1) |
+| 7 | GSP device model + profiles + bounded boot runner | `sim/devices/`, `sim/profiles/`, `sim/gsp_boot.py`, `sim/gsp_profile.py`, `sim/mpu.py`, `sim/libos.py` | real 570.144 image past the QEMU first fault (B0–B3) | S1 workstream | `dumps/gsp-boot-b1-run1/2`, `b3-run1/2`; 2-run determinism asserted (`docs/36` §4) | in development (S1) |
| 8 | Declarative probe specs | `greeneye/probes.py`, `examples/probes/` | JSON hooks/conditions without scripts | audit smoke; documented pattern | `docs/27` §3 (3 probes fired) | verified useful |
| 9 | Plugin loader + example plugin | `greeneye/plugins.py`, `plugins/example_plugin.py` | extension point | audit smoke | `plugins` CLI lists all three | verified useful |
| 10 | MCP server | `plugins/mcp_server.py` | agent/CI machine surface (JSON-RPC over stdio/loopback) | no live agent consumer found | 55 tests (`docs/20` §9); tool catalogue 31 (`docs/20` §5) | implemented; **unused in real workflow** (F1 `partially used`) |
diff --git a/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md b/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md
index 613e966..aadae37 100644
--- a/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md
+++ b/debug/risc-v-gsp-rm-vm/docs/40-NEW-VERSION-TRIAGE.md
@@ -194,6 +194,14 @@ the QEMU line" next-action line no longer appears. Wall ≈2.7 s
(host-load dependent). The pre-S1 trace above is kept as the recorded
before/after evidence.
+Post-XMPU/libos wave (2026-10-03, session 4c; same command): with the default
+budget the `native_boot` step reports rung **B2** and a budget stop at
+200,000 instructions (no fault anymore — the former `0x400EA0C` wall is
+gone); with `--budget 2000000` it reports rung **B3** (474,043 instructions,
+`ecall` port wait, 0 unknown MMIO) and `ab_eval` verdict `above` (measured
+wall ≈4.1 s, 434× the frozen 1800 s manual baseline). The numbers in the
+example and the post-S1 paragraph above are the pre-wave record.
+
Baseline variant (same image both sides, an unchanged-version rehearsal with
the docs/21 path):
diff --git a/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md b/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md
index 77e1298..57ee33b 100644
--- a/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md
+++ b/debug/risc-v-gsp-rm-vm/docs/41-STABILITY-AND-TIME-TO-VALUE.md
@@ -3,15 +3,16 @@
> **Date:** 2026-10-03. **Scope:** the simulator/debugger workstream inside
> `debug/risc-v-gsp-rm-vm/` only. **Verdict:** the minimum bar for marking the
> *tool and standalone GSP simulator* as **stable (conditional)** is passed:
-> 298 tests green twice, deterministic real-firmware runs, an independently
+> 417 tests green (298 at the first review; the XMPU/libos wave added the
+> rest), deterministic real-firmware runs, an independently
> reproduced A/B verdict "above the QEMU line", and reproducible value
> measurements. Conditions are listed in §7 — chiefly that the device model
-> currently reaches boot rung B2 (not the message loop) and cycle numbers are
+> currently reaches boot rung B3 (message-loop port wait; the CSB poll awaits
+> host traffic) and cycle numbers are
> modelled, not silicon-calibrated.
>
-> Local notes: `../../../../handoff-simulator.md` (directive ledger, top work),
-> `../LOCAL-REVIEW-NOTES.md` (local provenance). Public text describes
-> components and behaviour only.
+> Companion: `../../../../handoff-simulator.md` (directive ledger, top work).
+> Public text describes components and behaviour only.
## 1. What we were doing before (the QEMU baseline)
@@ -29,8 +30,8 @@
| Phase | Improvement | Status |
|---|---|---|
-| P1 | **GSP device model** (`sim/devices/`, `sim/profiles/`) with PMC/FALCON/PRISCV/timer/CSB/FWSEC/PMU/DMA/IRQ windows, versioned profile, boot shims | B2: 14,682 instr, fault `0x400EA0C`, 0 unknown MMIO, two-run identical |
-| P2 | **`sim.gsp_boot` boot-ladder runner** with rung classification and serialisable device state | B0–B2 (B3/B4 targets) |
+| P1 | **GSP device model** (`sim/devices/`, `sim/profiles/`) with PMC/FALCON/PRISCV/timer/CSB/FWSEC/PMU/DMA/IRQ windows, versioned profile, boot shims, **XMPU model** (`sim/mpu.py`) and **libos service model** (`sim/libos.py`) | B3: 474,043 instr, `ecall` port wait, 0 unknown MMIO, two-run identical |
+| P2 | **`sim.gsp_boot` boot-ladder runner** with rung classification and serialisable device state | B0–B3 (B4/B5 targets) |
| P3 | **One-command release triage** (`./risc-v-gsp-rm-debugger.sh triage`) orchestrating identity/QEMU probe/native boot/A-B eval/version diff/readiness | 570.144 in ~2.7 s; baseline pair ~11.4 s |
| P4 | **QEMU-line A/B evaluation harness** (`harness/sim_qemu_line.py`, frozen checks) | verdict `above`, all threshold checks computed from live runs |
| P5 | **Real RE demonstration** (`harness/re_demo.py`) answering a concrete bootstrap question with simulator tools | PASS 5/5, 0.13 s vs 601 s baseline floor |
@@ -41,7 +42,7 @@
| Scenario | Before | Now | Evidence |
|---|---|---|---|
-| What happens in GSP boot before the first wall? | QEMU: fault at `0x4009014`, stop | Device model serves the path; deterministic B2 fault `0x400EA0C`; unknown accesses 0 | `python3 -m sim.gsp_boot`; sessions `dumps/gsp-boot-b2-run1/2` |
+| What happens in GSP boot before the first wall? | QEMU: fault at `0x4009014`, stop | Device model serves the path; deterministic B3 port wait (`ecall @ 0x4018038`); unknown accesses 0 | `python3 -m sim.gsp_boot`; sessions `dumps/gsp-boot-b3-run1/2` |
| Triage a new firmware image | manual probe + reading, ~1800 s estimated | one command, **2.68 s**, JSON+MD report with hashes | `./risc-v-gsp-rm-debugger.sh triage --firmware … --version 570.144` |
| Is the emulator better than QEMU? | no mechanism | validators run both, compute rung/feature deltas; verdict `above` | `python3 harness/sim_qemu_line.py` |
| Answer a bootstrap RE question | static reading only | 5/5 recorded answers in 0.13 s | `python3 harness/re_demo.py` |
@@ -54,10 +55,10 @@ one-line command in the report/doc that produced it.
## 4. Stability evidence
-* `python3 -m unittest discover -s tests -t .` → **298 tests, OK**, twice
- (rounded identity across runs); MCP 55, triage 14, device model 34 new
- checks included.
-* Determinism: B1/B2 device-state SHA identical across runs; triage sessions
+* `python3 -m unittest discover -s tests -t .` → **417 tests, OK** (298 at
+ the first review; the XMPU/libos wave added 13 boot/MPU checks plus WPR2
+ and trace-number updates); MCP 55, triage 14, device model 35 included.
+* Determinism: B1/B3 device-state SHA identical across runs; triage sessions
hashed; A/B harness repeats native runs and compares.
* Back-compat: `Session`, `RspClient`, `SimClient` and CLI keep prior
behaviour; additions are keyword/default forms; QEMU path untouched
@@ -79,15 +80,20 @@ one-line command in the report/doc that produced it.
derivation, DMA refusal counting, PMC chip-ID duplication, overlapping
window fallback, stale doc counts, `session_launch` documentation, docs/40
measurement labels). One latent item recorded: WPR2 window offsets versus
- profile base (firmware never touches it in B0–B2).
+ profile base — left unserved because the old B2 run never reached the read;
+ the XMPU/libos wave reached it and the offset defect was fixed (relative and
+ absolute forms both accepted, docs/36 §3).
* Verdict after fixes: conditional stability as stated in §7; no unverified
"beats silicon" or "boots RM" statement anywhere.
## 6. Top work for the next session (time-to-value first)
-1. **Push the device model to B3/B4** (CSB/TRANSCOM mailbox semantics,
- DMEM runspace source, PMU/FWSEC stubs) — this is the gate to serving a
- real RM message and the largest remaining time-to-value lever.
+1. **Push the device model to B4/B5** (host-traffic-triggered CSB/TRANSCOM
+ poll semantics, one scripted message served end-to-end) — this is the gate
+ to serving a real RM message and the largest remaining time-to-value
+ lever. *Update 2026-10-03, session 4c: the XMPU model, bootstrap-args shim
+ and libos service model landed; the ladder is now B3 (was B2), with the
+ former DMEM-runspace unknown resolved.*
2. **Surgical rehost of the live GR engine-context promote/commit wall**
(docs/39 top-3): convert the manual, multi-cycle physical campaign into
device-free hook/watch/compare sessions.
@@ -99,7 +105,8 @@ one-line command in the report/doc that produced it.
## 7. Conditions on the stability claim
-1. Device model ceiling is B2; B3/B4 pending profile semantics.
+1. Device model ceiling is B3 (libos init complete; message-loop port wait);
+ B4/B5 (host-traffic-triggered CSB poll, scripted message) pending.
2. Cycle counts are modelled defaults, not calibrated bands.
3. Manual-baseline speedups use labelled estimates, not timed human studies.
4. Cross-version triage was exercised as a self-diff rehearsal (no second
@@ -111,8 +118,8 @@ one-line command in the report/doc that produced it.
```bash
cd open-iov/debug/risc-v-gsp-rm-vm
-python3 -m unittest discover -s tests -t . # 298 tests
-python3 -m sim.gsp_boot # B2
+python3 -m unittest discover -s tests -t . # 417 tests
+python3 -m sim.gsp_boot # B3 (port wait)
python3 -m sim.gsp_boot --no-shim # B1 budget stop
python3 harness/sim_qemu_line.py # verdict "above"
python3 harness/re_demo.py # PASS 5/5
diff --git a/debug/risc-v-gsp-rm-vm/docs/42-GMMU-FBIF-OBSERVABILITY.md b/debug/risc-v-gsp-rm-vm/docs/42-GMMU-FBIF-OBSERVABILITY.md
new file mode 100644
index 0000000..4a2d6ee
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/42-GMMU-FBIF-OBSERVABILITY.md
@@ -0,0 +1,203 @@
+# 42 — GMMU/FBIF observability (in development)
+
+> **Status: in development (2026-10-03, G1 work package).** This page
+> documents the observe-only GMMU and FBIF/FBPA register windows added to the
+> device model, the `greeneye-gmmu-fbif-trace/1` document and the boot-runner
+> flags that produce it. Development-only: nothing here authorises GPU
+> hardware, kernel modules, mdev, guest access or reboots, and nothing here is
+> a silicon-accuracy claim.
+>
+> **See also:** `36-GSP-DEVICE-MODEL.md` (the device model this plugs into),
+> `06-ACCURACY.md` (fidelity levels), `24-DEVICE-MAP.md` (harvested access
+> maps), `35-GREENEYE-SIMULATOR-DESIGN.md` §4.1 (the Phase G inventory this
+> observability de-risks).
+
+## 1. What this is
+
+The Phase G board model needs the GMMU and the FB interface eventually. Before
+modelling either, we need to know **what the firmware actually touches** in
+those register windows, with which width and direction, from which PC, and
+without pretending to know register behaviour we have not established.
+
+`sim/devices/observe.py` adds **observer windows**: device-model windows that
+record every access and then **decline** it, so execution follows the existing
+device-map / lenient / unknown path exactly as it did before the window
+existed. They never invent a read value.
+
+## 2. What is observed
+
+The TU104 profile (`sim/profiles/tu104-570.144.json`) declares 23 observer
+windows, all `observe-only`. Bases are runtime addresses in the GSP RISC-V
+PRIV aperture (`0x2000_0000_0000_0000` + BAR0 offset); offsets/names come from
+the hardware register/field headers (evidence kind: **header**). The B0–B2
+runs did not touch any of them; the B3 wave reaches the WPR2 read pair
+(`0x1FA824/0x1FA828`), which falls inside the `gmmu-wpr-lock` window and is
+recorded observe-only (2 accesses, 0 served; the fwsec device answers).
+
+| Window label | Runtime base | Size | Covers |
+|---|---|---|---|
+| `gmmu-pri-mmu` | `0x2000000000100C80` | `0x280` | `NV_PFB_PRI_MMU_*` cluster (`NV_PFB_PRI_MMU_CTRL` first) |
+| `gmmu-wpr-lock` | `0x20000000001FA7D8` | `0x5C` | MMU per-engine PRIV_LEVEL masks + VPR/WPR/lock address pair |
+| `gmmu-hsmmu` | `0x20000000001FAC00` | `0x400` | `NV_PFB_HSMMU_PRI_MMU_*` (secondary MMU block) |
+| `fbpa` | `0x20000000009A0000` | `0x4000` | main FBPA block (`NV_PFB_FBPA_*`) |
+| `fbpa-partition-0..15` | `0x2000000000900000` + i·`0x4000` | `0x4000` | per-partition FBPA instances |
+| `fbpa-mc-0..2` | `0x2000000000980000` + i·`0x4000` | `0x4000` | FBPA MC instances |
+
+Register names (and, for a few control/status registers, header field names
+with bit positions) are declared per window so traces are readable; offsets
+inside a window that the profile does not name are still recorded and land
+under `unclassified`. `gmmu-pri-mmu` spans the `NV_PFB_PRI_MMU_*` cluster
+`0x100C80–0x100EFC`; the few non-MMU PFB registers interleaved in that span
+(e.g. the PRI_FB clock/power-gating registers) are recorded as unclassified,
+never renamed.
+
+## 3. Mechanism: record, then decline
+
+Each `ObservedWindow` is a normal device-model window and is consulted by the
+registry like any other. Its `read`/`write` always record:
+
+```text
+addr, offset, size, rw, value-if-known, pc, vt, window, register, served
+```
+
+and then return `None`/`False` unless the window is declared `storage` **and**
+the accessed offset is a declared plain read/write register **and** (for
+reads) that register has been written before. Declining means the registry
+falls through to the next matching window and the machine's existing path
+applies unchanged:
+
+* lenient mode: the access is logged/counted as before and reads 0;
+* fault mode: the same `Fault(cause, tval)` as without the window;
+* a modelled window underneath (e.g. the PMC block) is consulted exactly as
+ before and serves the access if it models it, and the observer records that
+ it happened. (The `gmmu-wpr-lock` window overlaps the fwsec WPR2 window, so
+ it exercises this fall-through; it asserts nothing about whether the
+ underlying WPR2 path serves — that path is unchanged by the observer and
+ currently serves nothing through the registry, a pre-existing condition.)
+
+**No-fake-read guarantee.** An observe-only read records `"value": null` and
+returns `None`; a storage read before any write is declined the same way; no
+reset value is synthesised. The only read value a storage window can return
+is one this same run wrote to that register. All profile windows for
+GMMU/FBIF are `observe-only`; the `storage` mode exists for surfaces whose
+header proves plain read/write behaviour and is exercised by tests.
+
+`pc` is the faulting instruction address (the registry forwards it only to
+devices that opt in). `vt` is the machine instruction counter at the access;
+it advances at `Machine.run` boundaries, so accesses inside one bounded run
+share the run-start value — a documented granularity, not a timing claim
+(fidelity L2 is not modelled).
+
+**Size limit.** Each window's access log is capped (`max_access_log`, default
+65536 entries); accesses beyond the cap are counted, not written. The trace
+document reports `dropped_accesses` per window and in the summary and
+separates `accesses` (total observed) from `retained_accesses` (present in
+the document), so truncation is never silent.
+
+## 4. Trace document: `greeneye-gmmu-fbif-trace/1`
+
+`--trace-out <path>` writes a trace file: a path that is an existing
+directory (or ends in `/`, which makes that directory) gets the canonical
+filename `gmmu-fbif-trace.json` inside; anything else is used as the file
+name. Accesses are grouped per window and per declared register; undeclared
+offsets go to `unclassified`. Shape:
+
+```json
+{
+ "schema": "greeneye-gmmu-fbif-trace/1",
+ "image_sha256": "…",
+ "profile": "tu104-570.144.json",
+ "profile_name": "tu104-570.144",
+ "symbols": "symbols.json",
+ "summary": {"windows": 23, "windows_touched": 0, "accesses": 0,
+ "retained_accesses": 0, "dropped_accesses": 0,
+ "reads": 0, "writes": 0, "served": 0, "registers_touched": 0},
+ "windows": [
+ {"label": "gmmu-pri-mmu", "base": "0x2000000000100c80", "size": "0x280",
+ "mode": "observe-only", "evidence": "…",
+ "counts": {"r": 0, "w": 0, "served": 0, "declined": 0},
+ "retained_accesses": 0, "dropped_accesses": 0,
+ "registers": [
+ {"offset": "0x0", "name": "NV_PFB_PRI_MMU_CTRL", "access": "rw",
+ "counts": {"r": 0, "w": 1, "served": 0},
+ "accesses": [
+ {"addr": "0x2000000000100c80", "offset": "0x0", "size": 4,
+ "rw": "w", "value": "0x1", "pc": "0x4001234",
+ "pc_symbol": "rm_init+0x20", "vt": 0,
+ "window": "gmmu-pri-mmu", "register": "NV_PFB_PRI_MMU_CTRL",
+ "served": false, "decoded": {"VM_PG_SIZE": 1}}
+ ]}
+ ],
+ "unclassified": {"counts": {"r": 0, "w": 0, "served": 0},
+ "accesses": []}}
+ ]
+}
+```
+
+`pc_symbol` is `null` unless `--symbols <greeneye-symbols/1 JSON>` is given;
+`decoded` is present only when a value is known and the register declares
+fields (bit positions from the header). `value`/`addr`/`offset`/`pc` are hex
+strings; `vt` is an integer. The document is deterministic for a fixed image,
+profile, flags and stimulus.
+
+The **count summary is always in the boot report** (and session metadata under
+`gmmu_fbif`) even when `--trace-out` is off; the trace blob is added to the
+recorded session only when the flag is on.
+
+## 5. Commands
+
+```bash
+cd open-iov/debug/risc-v-gsp-rm-vm
+
+# B2 run with the observer summary, trace written to a directory:
+python3 -m sim.gsp_boot --instructions 20000 --record dumps/gsp-boot-b2-observe \
+ --trace-out dumps/gsp-boot-b2-observe
+
+# Resolve recorded PCs through a symbol database:
+python3 -m sim.gsp_boot --trace-out /tmp/trace.json --symbols symbols.json
+
+# Ad-hoc window whose base is not established yet (repeatable, no guessing):
+python3 -m sim.gsp_boot --observe-window 0x20000000009A0000:0x4000:fbpa \
+ --observe-window 0x2000000000100C80:0x280:gmmu
+```
+
+`--observe-window BASE:SIZE[:LABEL[:MODE]]` is the escape hatch: the trace
+accepts a runtime window list, so an unknown surface can be observed before
+any profile entry exists. A runtime window is `observe-only` unless
+`MODE=storage` is given (storage without declared registers still records and
+declines).
+
+## 6. What is NOT modelled
+
+* **No GMMU page walk, no PTE/PDE decode, no TLB, no fault handling, no ATS.**
+ Window accesses are recorded; their semantics are not interpreted.
+* **No FBIF transaction semantics, compression, ECC or partition hashing.**
+ FBPA register offsets/names are labels, not behaviour.
+* **No interrupt or timing (L2) claims.** `vt` is the modeled instruction
+ counter, not cycles.
+* **No device-environment (L3) claims.** The windows exist because the
+ simulator serves them; silicon behaviour is not asserted.
+* **Storage is not semantics.** A storage window only echoes values written in
+ the same run; it is not evidence that the real register stores them.
+* **No product or hardware action.** Nothing in this workstream touches
+ `vgsp`, a GPU, a module, mdev, or a guest.
+
+## 7. Evidence, limits and tests
+
+* Evidence kind for every window/offset is the hardware register/field header
+ (block ranges and register offsets); observed firmware use in B0–B2 was
+ **zero accesses** (measured at the B2 stop: 14 682 instructions, 0 unknown
+ MMIO, 1 device access — the PMC chip-ID read). The B3 wave's WPR2 reads are
+ the first observed accesses (2 reads inside `gmmu-wpr-lock`, observe-only,
+ 0 served). The mechanism is still tested end-to-end with runtime windows.
+* Boot numbers are unchanged by the observer windows (instructions, stop
+ PC/reason, unknown MMIO, unhandled-in-window, served accesses); the only
+ session difference is the added device-state entries for the observers.
+* Tests: `tests/test_gmmu_fbif_trace.py` — no-semantic-change (lenient and
+ fault paths match a registry without the window), storage read-back,
+ field decode, profile expansion, trace schema, access-log-cap reporting,
+ WPR2-overlap pass-through, B2 invariance, symbol resolution, runtime
+ windows.
+* Public artifacts carry only component register names/offsets and behaviour;
+ the window/register tables come from the tracked analysis and recorded
+ sessions.
diff --git a/debug/risc-v-gsp-rm-vm/docs/43-GMMU-PTE-DECODER.md b/debug/risc-v-gsp-rm-vm/docs/43-GMMU-PTE-DECODER.md
new file mode 100644
index 0000000..ea6d3d4
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/43-GMMU-PTE-DECODER.md
@@ -0,0 +1,206 @@
+# 43 — GMMU PTE/PDE decoder for TU104 (in development)
+
+> **Status: in development (2026-10-03, G2 work package).** This page
+> documents `sim/gmmu_pte.py`: a clean-room decoder for captured Turing
+> (TU104) GMMU page-table entries. It is decode-only tooling — no hardware,
+> no kernel module, no mdev, no guest, no RM 610. Development-only, never
+> shipped.
+>
+> **Rule: decode captured data only; validate against a capture before
+> trusting.** Every field position below is a reverse-engineering result;
+> none of it is a silicon-accuracy or timing claim (`06-ACCURACY.md`
+> fidelity levels apply).
+
+
+## 1. What this is — and is not
+
+`sim/gmmu_pte.py` turns a captured page-table value or blob into named
+fields with exact bit positions. It is **not** a page-table walker: it never
+dereferences a decoded address, never reads a target table, and never fills
+a gap with a guess. Unsupported encodings and levels raise `E_UNSUPPORTED`;
+set bits that no named field covers are reported (`reserved_bits` /
+`reserved_mask`) and never dropped.
+
+The evidence base is the Turing GMMU entry-format definitions (field names,
+bit ranges, aperture/KIND numeric IDs) together with the driver's own
+page-table encoder/decoder behaviour, which is what proves which of the
+several described encodings Turing actually uses (VER2) and how the
+page-tree geometry maps to virtual-address bits.
+
+## 2. Encodings shipped
+
+| Encoding | Size | Used for |
+|---|---|---|
+| `ver2` | 8 bytes | VER2 leaf PTE and directory PDE — the encoding the Turing driver's page-tree path programs |
+| `ver2_dual` | 16 bytes | VER2 dual PDE0 slot for mixed 64K/4K page tables: BIG half (low 64 bits) + SMALL half (high 64 bits) |
+
+Known-but-unsupported encodings (explicit `E_UNSUPPORTED`): the older
+non-VER2 PTE/PDE encoding and the "NEW"/VER1 encodings also described by the
+same register definitions. They are not shipped because the Turing driver's
+page-tree path uses VER2; adding them without a capture to validate against
+would be unverified surface. See §8.
+
+## 3. VER2 leaf PTE (8 bytes)
+
+| Field (our name) | Bits | Meaning |
+|---|---|---|
+| `valid` | 0 | entry is present |
+| `aperture` / `aperture_name` | 2:1 | 0 video, 1 peer, 2 system coherent, 3 system non-coherent |
+| `volatile` | 3 | cache/volatility bit: 0 = cached, 1 = volatile (the format's cache-related field) |
+| `encrypted` | 4 | encrypted memory |
+| `privilege` | 5 | privileged access |
+| `read_only` (`writable`) | 6 | write protection |
+| `atomic_disable` | 7 | atomics disabled |
+| `address_sys` | 53:8 | system-aperture address bits (PA 53:12) |
+| `address_vid` | 32:8 | video/peer address bits (PA 36:12) |
+| `peer_id` | 35:33 | peer/fabric id (peer aperture) |
+| `comptagline` | 55:36 | compression tag; overloaded as high address bits for the documented 47-bit peer case |
+| `kind` / `kind_name` | 63:56 | memory kind id (see §6); unknown ids are reported, not guessed |
+
+## 4. VER2 directory PDE (8 bytes)
+
+| Field (our name) | Bits | Meaning |
+|---|---|---|
+| `is_pde_pointer` / `is_leaf` | 0 | 0 = pointer to a lower table, 1 = 2 MB leaf PTE stored at PDE0 |
+| `aperture` / `aperture_name` | 2:1 | 0 invalid, 1 video, 2 system coherent, 3 system non-coherent |
+| `volatile` | 3 | as PTE |
+| `no_ats` | 5 | skip the ATS lookup for the covered range (driver sets it at depth 2) |
+| `address_sys` | 53:8 | next-table address bits (system aperture) |
+| `address_vid` | 32:8 | next-table address bits (video aperture) |
+| `peer_id` | 35:33 | peer/fabric id |
+
+Note the aperture-id difference: for a leaf PTE aperture 0 is video memory;
+for a directory PDE aperture 0 is invalid.
+
+## 5. VER2 dual PDE0 (16 bytes)
+
+The BIG half is the low 64 bits, the SMALL half the high 64 bits. Bit 0
+applies to the whole slot.
+
+| Field (our name) | Bits | Meaning |
+|---|---|---|
+| `is_pde_pointer` / `is_leaf` | 0 | whole-slot discriminator |
+| `aperture_big` | 2:1 | 0 invalid, 1 video, 2 system coherent, 3 system non-coherent |
+| `volatile_big` | 3 | as PTE |
+| `no_ats` | 5 | ATS skip; lies inside the BIG address field (see §7) |
+| `address_big_sys` | 53:4 | BIG target address; stores address >> 8 (PA 57:8) |
+| `address_big_vid` | 32:4 | BIG target address, video aperture |
+| `peer_id_big` | 35:33 | BIG peer/fabric id |
+| `aperture_small` | 66:65 | as `aperture_big` |
+| `volatile_small` | 67 | as PTE |
+| `address_small_sys` | 117:72 | SMALL target address; stores address >> 12 (PA 57:12) |
+| `address_small_vid` | 96:72 | SMALL target address, video aperture |
+| `peer_id_small` | 99:97 | SMALL peer/fabric id |
+
+Bits 54–64, 68–71 and 118–127 are unnamed by the format definitions and are
+reported as reserved when set.
+
+## 6. Page-tree geometry and KIND ids
+
+Turing's page-tree path inherits the Pascal/Volta geometry and uses the VER2
+entry encoding. Depths are root (0) to leaf (4):
+
+| Depth | HW level | VA bits | Index bits | Entry size | Role |
+|---|---|---|---|---|---|
+| 0 | PDE3 | 48:47 | 2 | 8 | pointer PDE |
+| 1 | PDE2 | 46:38 | 9 | 8 | pointer PDE |
+| 2 | PDE1 | 37:29 | 9 | 8 | pointer PDE (NO_ATS here) |
+| 3 | PDE0 | 28:21 | 8 | 16 (dual) / 8 (2 MB leaf) | dual 64K/4K PDE, or a 2 MB leaf PTE |
+| 4 | PTE | 20:12 (4 KB) / 20:16 (64 KB) | 9 (4 KB) / 5 (64 KB) | 8 | leaf PTE |
+
+Supported page sizes: 2 MB (depth 3), 64 KB and 4 KB (depth 4); VA width 49
+bits. A leaf entry alone does not tell 4 KB from 64 KB — the table's depth
+and page-size context do, so the decoder reports `page_size` as
+`4k_or_64k` when the depth is pinned to 4.
+
+The PTE `kind` field carries numeric ids (243 named values re-derived from
+the format definitions). Common compute values: `0x00 PITCH`,
+`0x06 GENERIC_MEMORY`, `0x07 INVALID`, `0x0F SMSKED_MESSAGE`,
+`0xFE GENERIC_16BX2`. Unknown ids decode as `kind_name: null`,
+`kind_known: false` with the raw value preserved.
+
+## 7. Address decode and uncertainty
+
+* System apertures (`system_coherent_memory`, `system_non_coherent_memory`):
+ the reconstructed `address` is complete (`address_complete: true`).
+* Video/peer apertures: only the low address bits are unambiguous
+ (`address_complete: false`). `comptagline` is a compression tag in the
+ normal case and is documented as overloaded for 47-bit peer addresses; the
+ decoder therefore does not claim a complete video address.
+* Dual BIG address: the field stores `address >> 8`, so `address` is
+ reconstructed as `field << 8`. Bit 5 of the slot is named `no_ats` and also
+ lies inside the BIG address field (address bit 9). The decoder reports the
+ flag from bit 5 and excludes that bit from the reconstructed BIG address;
+ the raw `address_big_sys` / `address_big_vid` fields keep the bit visible.
+ This assumes the target table is 4 KB aligned, which is the normal GPU
+ allocation granularity — if a capture shows a non-aligned target, treat
+ that entry as an unexplained case, not as proof of a different format.
+* Reserved-bit policy: `reserved_bits` lists every set bit not covered by a
+ named field; `reserved_mask` is the same set as a mask. Nothing is
+ silently dropped.
+* The VER2 PTE has no separate cache field: `volatile` (bit 3) is the only
+ cache-related bit the format defines, so no cache encoding is invented.
+* `decode_table` is little-endian and never follows pointers; a blob whose
+ length is not a multiple of `entry_size` raises `E_TRUNCATED`, an empty
+ blob decodes to `[]`.
+
+## 8. Supported levels and explicit gaps
+
+`levels` may pin the page-tree depth (0–4) or a sequence of acceptable
+depths. `None` (default) uses the full supported set and lets each entry's
+bit-0 discriminator choose between the leaf-PTE and directory-PDE views,
+reporting `level: null` plus `level_candidates` instead of guessing a single
+depth. Any depth outside 0–4 raises `E_UNSUPPORTED`.
+
+Explicit gaps (never guessed):
+
+1. Pre-VER2 and "NEW"/VER1 encodings are not decoded (`E_UNSUPPORTED`).
+2. The decoder does not walk tables, resolve virtual addresses, or infer a
+ 4 KB vs 64 KB leaf page size from the entry alone.
+3. No capture has been validated yet: the golden vectors are constructed
+ field-by-field from the format definitions, not sampled from silicon.
+ **Validate against a real capture before trusting a decode.**
+4. No timing, device-environment, isolation or security claim is made; the
+ simulator fidelity levels in `06-ACCURACY.md` apply.
+
+## 9. Usage
+
+Python:
+
+```python
+from sim.gmmu_pte import decode_pte, decode_table
+
+pte = decode_pte(0x0600000000123456, pte_kind="ver2")
+entries = decode_table(blob, base_pa=0x2000, entry_size=8, levels=4)
+dual = decode_table(blob16, base_pa=0x2000, entry_size=16, kind="ver2_dual",
+ levels=3)
+```
+
+CLI:
+
+```bash
+# one entry (default role "auto": bit 0 picks leaf vs directory)
+python3 -m sim.gmmu_pte --value 0x0600000000123456 --role pte
+python3 -m sim.gmmu_pte --value 0x0600000000123456 --json
+
+# a captured table blob, with the blob's GPU physical base address
+python3 -m sim.gmmu_pte --blob table.bin --base-pa 0x2000
+python3 -m sim.gmmu_pte --blob table.bin --base-pa 0x2000 --levels 4 --json
+
+# a dual PDE0 slot
+python3 -m sim.gmmu_pte --blob dual.bin --base-pa 0x2000 \
+ --kind ver2_dual --entry-size 16 --levels 3 --json
+```
+
+Exit code 0 on success; 2 with an `E_*` code on stderr for unsupported
+kinds/levels, truncated blobs or bad values. JSON output is sorted-key and
+deterministic.
+
+## 10. Tests
+
+`tests/test_gmmu_pte.py` builds every vector field-by-field from the tables
+above, asserts field round-trips, reserved-bit reporting (including the
+unnamed dual bit 64 and the unnamed PDE bits 4, 6, 7, 54–63), the NO_ATS
+overlap behaviour, unknown-kind reporting, unsupported kind/level/entry-size
+errors, empty and truncated blobs, deterministic output, and CLI success and
+error paths. 44 constructed golden entry vectors across 43 tests.
diff --git a/debug/risc-v-gsp-rm-vm/docs/44-GMMU-ORACLE-AND-WORKFLOW.md b/debug/risc-v-gsp-rm-vm/docs/44-GMMU-ORACLE-AND-WORKFLOW.md
new file mode 100644
index 0000000..affd350
--- /dev/null
+++ b/debug/risc-v-gsp-rm-vm/docs/44-GMMU-ORACLE-AND-WORKFLOW.md
@@ -0,0 +1,254 @@
+# 44 — GMMU/FBIF oracle and workflow (in development)
+
+> **Status: in development (2026-10-03, G3 work package).** This page
+> documents two stdlib-only harness tools:
+>
+> * `harness/gmmu_oracle.py` — offline comparison of a human oracle capture
+> against a simulator GMMU/FBIF trace (per-register agreement, coverage,
+> unmatched lists, fidelity labels);
+> * `harness/gmmu_debug.py` — static↔dynamic join: a trace plus a symbol
+> map becomes a per-function register-write report and a
+> `greeneye_hits.tsv` the existing Ghidra annotator consumes.
+>
+> Development-only: these tools read files and write reports. They never
+> touch a GPU, kernel module, mdev, guest or network, and they make no
+> cycle/timing claim. Physical work (the oracle capture) is supervised-only
+> per RUNBOOK P1/P7 and is never run by an agent.
+>
+> **See also:** `06-ACCURACY.md` (fidelity levels, M1–M6),
+> `05-GHIDRA-BRIDGE.md` (address spaces, hit annotation),
+> `17-RM-HARNESS.md` (read-only RM oracle policy),
+> `25-VALUE-VALIDATION.md` (measured-value discipline),
+> `36-GSP-DEVICE-MODEL.md` (the simulator device model whose traces feed
+> these tools).
+
+## 1. What this is and why
+
+The device model (`sim/devices/`, docs/36) records every modelled MMIO
+access with its PC, width, direction and value. Two questions follow:
+
+1. **Accuracy** — does the value the model serves agree with what a human
+ can observe on the host (read-only RM/sysfs readbacks, supervised
+ captures)? (`gmmu_oracle.py`)
+2. **Static analysis** — which firmware function programs which
+ GMMU/FBIF register, in what order, and where is it in Ghidra?
+ (`gmmu_debug.py`)
+
+Both are offline. The oracle file is produced by a human; the tools ingest
+it and compare semantically. Nothing here executes a physical command.
+
+## 2. The accuracy loop (oracle vs simulator)
+
+```
+human (supervised, read-only) simulator (agent-safe)
+ rmtool / rmtop / sysfs / debugfs greeneye-gmmu-fbif-trace/1
+ -> greeneye-gmmu-oracle/1 (from sim/ devices + trace)
+ \ /
+ \ harness/gmmu_oracle.py compare /
+ \____________________________________/
+ |
+ gmmu-oracle-report.{json,md}
+ agreement %, coverage %, unmatched lists,
+ fidelity label per row
+```
+
+Rules:
+
+* The oracle is **observed** (human capture). The simulator value is
+ **modelled** (device model / recorded trace). Every row carries that
+ distinction; when a value is missing on either side the row's fidelity
+ label says the value is **unknown** instead of guessing.
+* Agreement is computed **per register on the matched intersection** — it
+ is never a single global "accuracy %". Coverage is reported separately.
+* An oracle-only register means the simulator did not touch it under this
+ stimulus; a simulator-only register means no oracle counterpart exists
+ yet. Neither is proof of a gap.
+* No timing, ordering or silicon claim. Fidelity is L0/L1 (docs/06).
+
+### 2.1 Oracle schema `greeneye-gmmu-oracle/1`
+
+An object with a `captures` list (a bare JSON list is accepted and reported
+as `unlabeled-list`):
+
+```json
+{
+ "format": "greeneye-gmmu-oracle/1",
+ "captures": [
+ {"name": "FBIF_CTL", "window": "falcon", "offset": "0x624",
+ "value": "0x1", "source": "rm-readback", "captured_at": "2026-10-03"}
+ ]
+}
+```
+
+`name`, `window`, `offset`, `value`, `source`, `captured_at` are the
+documented fields. `offset`/`value`/`addr` accept ints or `0x…` strings.
+A different explicit `format` is a clean error.
+
+### 2.2 Simulator schema `greeneye-gmmu-fbif-trace/1`
+
+The producer (`sim/devices/observe.py:build_trace`, docs/36) emits a
+nested document: accesses are grouped per window and per declared register,
+with an `unclassified` bucket for observed offsets the profile does not
+name. The `schema` key carries the format tag:
+
+```json
+{
+ "schema": "greeneye-gmmu-fbif-trace/1",
+ "image_sha256": "…", "profile": "tu104-570.144",
+ "summary": {"windows": 1, "accesses": 2},
+ "windows": [
+ {
+ "label": "falcon-observe", "base": "0x300110000", "size": "0x1000",
+ "mode": "observe-only", "evidence": "header",
+ "registers": [
+ {"offset": "0x624", "name": "FBIF_CTL", "access": "rw",
+ "counts": {"r": 0, "w": 1, "served": 0},
+ "accesses": [
+ {"addr": "0x300110624", "offset": "0x624", "size": 4, "rw": "w",
+ "value": "0x1", "pc": "0x4002040", "vt": 123,
+ "window": "falcon-observe", "register": "FBIF_CTL",
+ "served": false, "pc_symbol": "fbif_setup"}
+ ]}
+ ],
+ "unclassified": {"counts": {"r": 1, "w": 0, "served": 0},
+ "accesses": [{"addr": "0x300110008", "offset": "0x8",
+ "rw": "r", "value": null, "pc": "0x4002050"}]}
+ }
+ ]
+}
+```
+
+`load_sim_trace` flattens this shape (and accepts a flat `events` list, a
+bare list, JSONL, and aliases) so the harness survives producer naming
+choices: `kind`/`rw`/`direction`/`type`/`t`; `name`/`register`/`reg`;
+`window`/`block`/`device`; `offset`/`reg_offset`; `addr`/`address`/`pa`;
+`value`/`data`; `pc`; `function`/`symbol`/`pc_symbol`; `handled`/`served`.
+Nested documents are flattened per window/register: per-register access
+order is preserved, but global interleaving across registers is not
+recoverable from that grouping. The producer's top-level `summary` —
+including `retained_accesses` / `dropped_accesses` — is carried into
+`inputs.trace.source.summary`, so a capped (truncated) access log stays
+visible downstream. A session directory is accepted too:
+`session.json` pointers (`gmmu_fbif_trace`, `gmmu_fbif_trace_blob`,
+`trace_blob`) or a `blobs/*.json[l]` file carrying the format tag. A raw
+`read`/`write` JSONL trace without register names still parses (registers
+are then reported by address).
+
+## 3. Static-analysis debugging loop (`gmmu_debug.py`)
+
+```
+greeneye-gmmu-fbif-trace/1 + symbols (greeneye-symbols/1 | addr->name map | TSV)
+ |
+ v
+per-function register-write report gmmu-debug-report.{json,md}
+ function, register, window, offset,
+ sequence of written values,
+ first/last pc, write/read counts
+ |
+ v
+greeneye_hits.tsv (addr hits symbol annotation)
+ |
+ v
+analyzeHeadless ... -postScript MarkGreenEyeHits.java <greeneye_hits.tsv>
+```
+
+`greeneye_hits.tsv` is byte-compatible with
+`greeneye/ghidra.py:export_hits` and `ghidra/MarkGreenEyeHits.java`: header
+`addr\thits\tsymbol\tannotation`, hex address, one row per function, `hits`
+= register accesses attributed to the function. Function attribution uses
+the symbol DB (containing symbol when sizes exist, the toolchain's
+nearest-preceding fallback otherwise); a pc outside every symbol becomes
+`sub_<pc>`. An explicit `function` field in the trace wins over resolution.
+
+## 4. Exact commands
+
+All commands run from `open-iov/debug/risc-v-gsp-rm-vm`. They are offline
+and safe for an agent.
+
+```bash
+# Accuracy loop — compare a human oracle with a simulator trace/session.
+python3 harness/gmmu_oracle.py compare \
+ --oracle oracle.json --sim dumps/<session-or-trace.json> \
+ --out dumps/gmmu-oracle-report
+
+# Print (do NOT execute) the supervised, read-only capture instructions.
+python3 harness/gmmu_oracle.py --print-capture-hints
+
+# Static↔dynamic join — report + Ghidra-ready hits TSV.
+python3 harness/gmmu_debug.py report \
+ --trace dumps/<session-or-trace.json> --symbols symbols.json \
+ --out dumps/gmmu-debug-report
+
+# Synthetic end-to-end fixture (no firmware, no hardware).
+python3 harness/gmmu_debug.py demo --out harness/gmmu_debug_fixture
+
+# Tests (synthetic only; no live oracle capture, no hardware).
+python3 -m unittest tests.test_gmmu_oracle tests.test_gmmu_debug -v
+```
+
+`--print-capture-hints` only prints. It names the read-only RM wrapper
+commands (`./rmharness/rmtool.sh gpu list/engine list/mem stats`,
+`rmtop --once`), sysfs/proc/debugfs readbacks, and the optional
+first-touch step, with the hard warning that physical work is
+supervised-only and an agent must never run those commands. The safe
+simulator observation (`python3 -m sim.gsp_boot … --record …`) is the only
+capture an agent may produce.
+
+Generated fixture/report directories under `harness/gmmu_*` are gitignored.
+
+## 5. Worked example (synthetic, measured)
+
+`python3 harness/gmmu_debug.py demo` builds a 13-event synthetic trace with
+three named functions plus one unattributed pc, runs the whole loop, and
+prints a `RESULT_JSON` line. On 2026-10-03 (this host, CPython 3.12) the
+loop measured **≈1 ms**, against a manual static-reading baseline frozen
+in `harness/gmmu_debug.py` **before** the first measurement:
+
+| field | value |
+|---|---|
+| manual baseline (frozen) | `1200 s` — a **conservative estimate**, not measured on a human |
+| threshold fraction (frozen) | `0.05` → `60 s` |
+| measured (fresh run required to quote) | ≈0.001 s |
+| verdict | pass |
+
+The baseline and fraction are constants in `harness/gmmu_debug.py`; moving
+them requires a new recorded run and a note here. The demo proves the
+**shape** of the loop (trace → per-function report → hits TSV →
+Ghidra-ready output), never a hardware fact.
+
+Synthetic oracle comparison check (three oracle items, three trace events):
+one agree, one mismatch, one oracle-only, one simulator-only → agreement
+50 % on the two-item intersection, coverage 66.67 %. These are fixture
+numbers, not an accuracy claim about any firmware.
+
+## 6. Limits and not-verified list
+
+* **No live oracle capture was taken.** The tools were exercised only with
+ synthetic fixtures. A real oracle requires a supervised human session
+ per §4 and docs/17 §6.
+* **No real firmware trace was compared.** The loader is exercised against
+ the real `sim/devices/observe.py` producer (read-only integration test)
+ and against synthetic documents; no captured firmware run was compared,
+ and no live oracle capture was taken.
+* **L0/L1 only.** The trace records execution and register accesses, not
+ time; no L2/L3 claim is made. Absence of a register in a trace is a fact
+ about that run's stimulus, not proof of dead code.
+* **Agreement is per-register on the intersection**, with coverage and both
+ unmatched lists reported; never a single global percentage.
+* **Function attribution depends on the symbol map.** Zero-size symbols use
+ the toolchain's nearest-preceding fallback; supply sized symbols
+ (`greeneye-symbols/1`) for strict boundaries.
+* **Product changes are not implied.** Any emulation change still needs the
+ accuracy loop plus physical reproduction per docs/06 §6.
+* The demo baseline is an **estimate**; no human was timed.
+
+## 7. Tests
+
+`tests/test_gmmu_oracle.py` and `tests/test_gmmu_debug.py` cover: oracle
+ingest (object/list/aliases/bad format), comparison (agreement, mismatch,
+oracle-only, simulator-only, name fallback, no-value, empty), session
+directory ingest, report shape and sequence order, explicit-function
+override, unknown-pc handling, hits-TSV compatibility with
+`MarkGreenEyeHits.java`, empty traces, the frozen-threshold demo, and the
+CLI error paths (exit 2, no traceback). All fixtures are synthetic; no
+hardware, module, guest or network is touched.