summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/24-DEVICE-MAP.md
blob: 0dde6a8dd573b1c11457513d18619e1bc454293f (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
# 24 — Device map: first-touch MMIO and lenient native execution

> **# TOOL IN DEVELOPMENT - NOT READY YET!** See 00 for status.
> This is the H2.1 data layer and the first H2.2 building block, not a device
> model. It carries the same fidelity rules as `06-ACCURACY.md`: L3 remains
> **absent**; nothing here is a hardware, timing or security claim.

## 1. Purpose and status

`greeneye-device-map/1` records which MMIO addresses a firmware image touched
first, at which width and direction, with observed values, hit counts and
provenance. It is JSON data, versioned with the firmware that produced it, and
consumed by the native simulator's lenient MMIO mode. It exists so the R1
device work (H2.2) starts from measured addresses instead of guesses, and so
the real GSP image can be stepped past its first unmapped access to collect
the *next* ones.

| Piece | Where |
|---|---|
| map model, harvest, query, CLI | `greeneye/device_map.py` |
| lenient / fault MMIO policy | `sim/machine.py` (`Machine(device_map=..., unknown_mmio=...)`) |
| backend plumbing | `greeneye/sim_backend.py` (`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)`) |
| tests | `tests/test_device_map.py` |

## 2. The model

An entry is one `(address, width, direction)` observation:

| field | meaning |
|---|---|
| `address` | the MMIO address as accessed (64-bit; 61-bit GSP addresses fit without allocating memory) |
| `width` | access size in bytes (1/2/4/8) |
| `kind` | `"r"` or `"w"` |
| `values` | observed values (first-seen order, deduplicated); empty for probe-harvested reads because the QEMU log records the fault, not the value |
| `hits` | number of times observed at this key |
| `first_epc` | the first observing PC as executed (relocated) |
| `first_epc_fw` | `first_epc - bias`: the firmware's own virtual address; a PC of 0 (trap-vector fetch) stays 0 |
| `source` | `probe` (harvested), `manual` (hand-written), `trace` (learned at runtime) |
| `notes` | fault/exception provenance, e.g. `fault_load reason=rejected` |

The file carries `meta` with the schema version, harvest/learn source, the
relocation `bias`, `entry`, the firmware path, its `sha256` (when the file is
readable) and `gsp_version.txt` (sanitised), plus `qemu_fault_count` when
harvested. Entries are always sorted by `(address, width, kind)` and the file
is written with `sort_keys=True`, so harvesting the same log twice is
byte-identical; there are no timestamps.

`DeviceMap.lookup(addr, size, write)` returns a behaviour spec:

* `value:0x...` — a read observed to return this value;
* `zero` — the address is known (or the direction is unknown) and reads back 0;
* `unimplemented` — no evidence at this address.

Exact `(address, width, kind)` wins; a different width at the same address is
still evidence for the register. A write to a read-observed address is
absorbed; a read of a write-only register reads back zero.

## 3. Harvesting from `greeneye probe`

`greeneye probe` (unchanged) records `blobs/qemu.log` and the parsed
`qemu_first_fault` in `session.json`. Harvest reads either:

```bash
# 1) run a bounded probe against the real image (QEMU path, existing command)
./risc-v-gsp-rm-debugger.sh probe                      # writes dumps/<stamp>-probe-<image>/

# 2) turn that session into a device map
python3 -m greeneye.device_map harvest dumps/<stamp>-probe-<image> \
    --out dumps/570.144-device-map.json

# 3) inspect / merge maps later, e.g. across firmware versions
python3 -m greeneye.device_map show dumps/570.144-device-map.json
python3 -m greeneye.device_map merge a.json b.json --out merged.json
```

Harvesting the recorded TU104 570.144 probe session
(`dumps/20261002-091222-probe-gsp-570.144.fwimage`) gives two entries — the
first-touch MMIO read and the `mtvec=0` fetch loop QEMU falls into afterwards:

```json
{"schema": "greeneye-device-map/1", "entries": [
  {"address": 0, "width": 2, "kind": "r", "hits": 367551, "first_epc": 0,
   "first_epc_fw": 0, "source": "probe", "notes": "fault_fetch reason=rejected"},
  {"address": 2305843009213696512, "width": 4, "kind": "r", "hits": 1,
   "first_epc": 2214629396, "first_epc_fw": 67145748, "source": "probe",
   "notes": "fault_load reason=rejected"}]}
```

`2305843009213696512 == 0x2000000000000A00`, `67145748 == 0x4009014`; `meta`
also carries the firmware sha256
(`2d1bb5c6…`), `firmware_version: 570.144W` and `qemu_fault_count: 367552`.
Programmatic forms: `harvest_probe_session(session_dir)` and
`harvest_from_text(log_text, bias=...)` for arbitrary logs; `DeviceMap.save`,
`DeviceMap.load`, `merge`, `lookup`.

## 4. Lenient MMIO in the native simulator

`sim.machine.Machine` gained three knobs (all default to today's behaviour):

```python
Machine(memory_mb=256, ram_base=0x80000000,
        device_map=None, unknown_mmio="fault", learn_mmio=False, bias=0)
```

* `unknown_mmio="fault"` (default) — **byte-for-byte M1 parity**: an access
  that is neither RAM nor UART faults exactly as before, and nothing is logged
  or counted.
* `device_map=...` — mapped entries are served in *either* mode; a mapped
  address that is unknown to the map is still `unimplemented`.
* `unknown_mmio="lenient"` — an unmodelled access returns the mapped
  behaviour, else zero; it is appended to `machine.mmio_log` as
  `{pc, addr, size, rw}` and counted in `machine.mmio_unknown`. No logging
  occurs when lenient mode is off. Debugger `read_mem`/`write_mem` stay
  strict.
* `learn_mmio=True` — lenient observations are written back into the map
  (`source: "trace"`, read value `0`, write value as written); with no map
  given, one is created lazily. Values recorded with `pc` and `bias`.

Through the backend:

```python
session = ge.launch(kernel=..., backend="native",
                    device_map="dumps/570.144-device-map.json",
                    unknown_mmio="lenient")
session.target.mmio_log       # [{pc, addr, size, rw}, ...]
session.target.mmio_unknown   # int
```

`launch_native(..., device_map=..., unknown_mmio=..., learn_mmio=..., bias=...)`
takes the same options; `device_map` may be a `DeviceMap` or a saved path, and
`bias` relocates images linked for GPU physical addresses. The lenient path is
only entered on a miss, so RAM/UART/trace throughput is unchanged; recording
is a plain list append and the map lookup is a dict hit.

**What lenient proves, honestly:** an unmodelled address can be served a
mapped/zero value and execution continues deterministically; a bounded run
lists every unmodelled access and their PCs. It does **not** prove what a real
device returns, does not model read-back, side effects, interrupts or timing,
and a zero that keeps the firmware running is a hypothesis to be checked
against the H2.2 device model (and eventually hardware), not a finding about
silicon.

## 5. Real-image demonstration (bounded, measured 2026-10-02)

```bash
python3 -m greeneye.device_map demo --instructions 200000
```

```text
image: /home/rmcore/Git/integdev_gpu_drv/open-iov/firmware/57X-GSP/570.144/gsp-570.144.fwimage.bin
sha256: 2d1bb5c60b208cfd115f015b17dd04a6fece05fd2c0dcba3625d9d48cce3ceea
elf: entry=0x4009000 vaddr=[0x0,0x5bb9000)
bias: 0x80000000  relocated entry: 0x84009000
budget: 200000 instructions
stopped: reason=budget pc=0x84009070 (fw 0x4009070)
unknown mmio accesses: 1
  first unknown: pc=0x84009014 (fw 0x4009014) addr=0x2000000000000a00 size=4 rw=r
[device-map] qemu probe first fault: fw vaddr 0x4009014 reading 0x2000000000000a00 (load access fault)
[device-map] verdict: first-touch address reproduced; lenient execution continued for 200000 instructions (199994 past the qemu trap)
```

Comparison against the QEMU probe (`docs/06 §3`, still current): QEMU executes
six instructions and traps at firmware vaddr `0x4009014` on the 4-byte read of
`0x2000000000000A00`, then loops on `mtvec=0` fetch faults. The lenient native
run reproduces the same first access at the same PC (the `pc` in `mmio_log` is
the accessing instruction, not the run start), returns 0, and continues
through the entire 200 000-instruction budget, ending at firmware vaddr
`0x4009070` with **exactly one** unmodelled access logged. It does not fault.

**61-bit addresses.** `0x2000000000000A00` is a valid 64-bit value in the
interpreter's address computation; no memory is allocated for it. RAM is
bounds-checked by `addr - ram_base`, the UART window by range, and everything
else goes to the device map / lenient handler. (`1 << 61` is well inside the
RV64 address width; the only care needed is that no map or device code ever
treats MMIO addresses as host indices.) `--out map.json` saves the learned
map; `python3 -m greeneye.device_map show map.json` renders it.

## 6. How this feeds H2.2

* The harvested entries are the first test vectors for the `falcon`/`CSB`/
  `pmu_stub` windows in the docs/16 profile DSL: group entries by page
  (`address & ~0xFFF`), take `lookup()` behaviour as the initial read-back,
  and replace `zero` with real semantics one window at a time.
* `learn_mmio=True` is the bring-up loop: run lenient, inspect the learned
  map + `mmio_log`, promote hand-verified entries to `source: "manual"`, and
  re-run strict to see the next unmapped wall. Each promotion should carry a
  note and the firmware hash, so the map is a reviewable artifact.
* The map's `bias`, firmware sha256/version and schema make it profile data
  keyed by `(device, firmware-family, profile-version)`, as required by
  docs/16 §7.
* The same maps are the YAML/JSON seed for the QEMU `-device` model, keeping
  the two backends on one source of truth.

## 7. API and commands

| Call | Purpose |
|---|---|
| `harvest_probe_session(dir)` | session dir -> `DeviceMap` |
| `harvest_from_text(text, bias=0)` | arbitrary QEMU log -> `DeviceMap` |
| `DeviceMap.lookup(addr, size, write)` | `zero` / `value:0x...` / `unimplemented` |
| `DeviceMap.record(...)`, `merge(...)`, `save(path)`, `load(path)` | authoring/persistence |
| `python3 -m greeneye.device_map harvest\|show\|merge\|demo` | CLI |
| `python3 -m pytest tests/test_device_map.py` | 20 unit/parity/real-image tests |

Testing honesty: the unit tests are synthetic (no QEMU); the real-image test
is bounded to 200 000 instructions and skips when the fwimage is absent.
`tests/test_sim_backend.py` remains the M1 parity guard and passes unchanged.