summaryrefslogtreecommitdiffstats
path: root/debug/risc-v-gsp-rm-vm/docs/51-HOST-TRAFFIC-LOOP.md
blob: e56094c471a1afc4bf5a5651923e43ab69639571 (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
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
# 51 — Host-traffic message loop: doorbell + msgq transport + replay (in development)

> **Status: in development (2026-10-06, B4/B5 transport wave).** This page
> documents `sim/host_traffic.py`, the transport half of boot-ladder rungs
> B4/B5 (docs/35-GREENEYE-SIMULATOR-DESIGN.md §3.3): a modelled host
> doorbell, a command/status message-queue pair laid out with the real queue
> arithmetic, queue-element framing with the real checksum, a stub dispatcher
> and deterministic replay of an `rm_capture` stimulus (docs/45). The **real
> 570.144 firmware is not run by this module**: there is no real message loop,
> no RM semantics and no firmware-behaviour claim.
>
> Development-only, offline: no GPU, kernel module, mdev, guest, network,
> QEMU, `/dev` access or reboot. Nothing here is a product dependency and
> nothing is injected into a guest.
>
> **See also:** `35-GREENEYE-SIMULATOR-DESIGN.md` §3.3 (ladder), `36` (device
> model; the falcon window this doorbell overlays), `45` (capture decode and
> stimulus), `49` (surface map; B4/B5 is queued build #1), `06-ACCURACY.md`
> (L0/L1 only), `open-iov/docs/eng-ops.md` §2 (admission rule).

## 1. What this is and why

The campaign keeps hitting one wall class: *what does the server do with the
traffic we send it, and what does it read/write/wait on for this control*.
The strategic tool for that class is B4/B5: replay a captured real RPC
sequence into the emulated GSP through an emulated transport and record the
replies and memory writes as evidence (handoff-simulator §27, §30, §31 item
B1; docs/49 §6 build order #1).

This wave lands the **transport and framing** so the replay plumbing exists
and is tested before the firmware loop is connected:

* a doorbell register model at the real hardware offset;
* command/status msgq rings in simulated memory, laid out with the arithmetic
  the real host queue code uses (including the `MSGQ_FLAGS_SWAP_RX`
  cross-link of the two rx headers);
* queue-element and RPC framing with the real header layouts and checksum;
* a 2-handler stub dispatcher plus `NV_ERR_NOT_SUPPORTED` for real RM
  functions, so a message can travel host → command ring → dispatch → status
  ring → host and be recorded;
* replay of a `greeneye-rm-rpc-stimulus/1` document (or a capture document
  converted by `harness/rm_capture.py`), message by message, in order.

The dispatcher is deliberately a **stub**: answers are labelled
`stub:echo`, `stub:status_query`, `stub:unsupported` in the transcript. A
stub verdict is never presented as firmware behaviour.

## 2. Admission record (draft — pending maintainer filing)

Copied from the queued execution item (private live contract §31 item B1),
expanded to the `open-iov/docs/eng-ops.md` §2 five-field format; a maintainer
files the final record. One scenario, one axis (axis 4, bug-analysis time).

```text
TOOL ADMISSION RECORD                                    id: TOOL-2026-XX (draft)
tool:           sim/host_traffic.py (new; B4/B5 transport + replay wave)
loop:           dynamic-RE (campaign traffic-interaction walls)
axis:           4 — bug-analysis time (supervised cycles avoided / TTFA)
owner/decider:  <maintainer>; executor: agent
scenario:       a campaign traffic-interaction wall (e.g. the V6 launch
                silence) asks what the server does with a given control or
                reply traffic; replay the captured stimulus into the modelled
                transport and read the server-side evidence offline.
measurable:     wall resolved from emulator evidence alone or with < 1
                supervised cycle; per wall: a session/transcript pair with
                every submitted message, dispatch handler and reply recorded.
baseline:       today = static decompiler dig + one or more supervised
                hardware cycles per hypothesis (unrecorded as a single
                number; docs/49 §4).
threshold:      keep if 2 of the next 3 wall-class instances are answered
                offline; N >= 3 wall-class instances; environment: this tool
                checkout, simulator only.
failure signal: replies diverge from captured server behaviour, or the tool
                cannot represent the control/queue shape -> record the
                fidelity gap and stop (no emulation edit from simulator-only
                evidence).
evidence:       tests/test_host_traffic.py (36 tests) + `demo`/`replay`
                session directories under the `--out` directory.
decision:       admit-pilot
review-by:      one quarter / five sessions      trials-dir: dumps/
```

Queued-item exit criterion (handoff §31 B1): *one real 512-byte RPC control
replayed end-to-end with the reply captured in a session*. This wave's demo
replays a **real-shaped** 512-byte `GSP_RM_CONTROL` (built to the documented
control shape, from no capture) end-to-end through the transport; its reply
is the stub dispatcher's framed `NV_ERR_NOT_SUPPORTED`, captured in a session.
`replay` consumes a real stimulus document the moment a supervised capture
provides one — the capture itself is still pending, so no captured-sequence
claim is made yet.

## 3. What the transport is based on (real offsets and structures)

All facts below come from public OpenRM register/queue header material (the
same class of evidence the tracked device model already uses). The capture
tool exposes the overlapping facts as constants (`RPC_HEADER_BYTES`, the
`GSP_RM_*` ids, the payload offsets), not as struct types, so this module
mirrors them and `test_rpc_constants_match_capture_tool` fails if they drift.
The models are L0/L1 framing and ordering only; no timing claim.

| Fact | Value | Use in the model |
|---|---|---|
| GSP queue-head register | `NV_PGSP_QUEUE_HEAD(i) = 0x110c00 + i*8`, 32-bit RW, i < 8 | doorbell window `PRIV+0x110c00`, 0x40 bytes; reached by the firmware through the PRIV aperture |
| Host kick | the 570.144 submit path writes the queue head after queueing (observed value 0) | `HostTraffic.submit` rings queue 0 with 0; the recorded value is the firmware-visible read-back |
| Falcon mailbox | `NV_PGSP_FALCON_MAILBOX0/1` 0x110040/0x110044 | already modelled by `sim/devices/falcon.py`; not duplicated here |
| `msgqTxHeader` | 8 x u32: version, size, msgSize, msgCount, writePtr, flags, rxHdrOff, entryOff | `MsgqTxHeader` / `MsgqQueue` |
| `msgqRxHeader` | 1 x u32: readPtr | `MsgqQueue.set_read` / `.read_ptr` |
| msgq arithmetic | `rxHdrOff = align32(16) = 32`, `entryOff = align36(16) = 48`, `msgCount = (size - entryOff) / msgSize` | `MsgqQueue.format` reproduces it exactly |
| Queue flag | `MSGQ_FLAGS_SWAP_RX = 1`; the real command queue sets it and `msgqRxLink` cross-links the rx headers between the command and status buffers | `SharedQueues` places the command rx header in the status buffer and vice versa |
| `GSP_MSG_QUEUE_ELEMENT` | 16 B authTag + 16 B aad + checksum + seqNum + elemCount + 4 B reserved + 32 B `rpc_message_header_v` (payload at 0x50) | `build_queue_element` / `parse_queue_element` |
| Element/queue sizes | element min 0x1000, max 0x10000; default command and status queue 0x40000 each; header size 0x1000; header align 4, element align 12 | defaults in the module; a message stages contiguously and splits across ring slots exactly as the real producer copies it |
| Multi-element | elemCount in the first element; the contiguous staged stream is split into 0x1000-byte slots (tail zero-padded); checksum covers the staged stream | `split_element_stream` + `SharedQueues._place`/`_take` (wrap-safe); max 16 elements |
| Checksum | XOR of 64-bit words over `0x30 + rpc.length` bytes, then hi32 ^ lo32; the stored field makes the receiver's recomputation zero | `compute_message_checksum` / `build_queue_element` (verified) |
| RPC header | 32 bytes: header_version (0x03000000), signature ("VRPC" 0x43505256), length (= 32 + payload), function, rpc_result, rpc_result_private, sequence, spare | `build_rpc_message` / `parse_rpc_message` |
| RM ids | `GSP_RM_CONTROL = 76`, `GSP_RM_ALLOC = 103`, `GSP_GET_GSP_STATIC_INFO = 65` (same as the capture tool) | replay mapping for `control`/`alloc` records |
| NV status | `NV_OK = 0`, `NV_ERR_NOT_SUPPORTED = 0x56` | stub replies |

Modelled rather than proven, and therefore labelled:

* the **shared memory address and queue sizes** are fixture defaults; the real
  driver learns them from the `GSP_ARGUMENTS_CACHED` / message-queue init
  arguments. Nothing here reads a real `MESSAGE_QUEUE_INIT_ARGUMENTS`.
* the **doorbell read-back** is storage of the last written value (the header
  says RW); the firmware's read use of this register has **not** been observed.
* the **command/status direction** is the documented one (host writes the
  command queue, GSP writes the status queue), but no real GSP has yet been
  observed consuming or producing through this model.
* the **MSI-X/event path** by which the real GSP signals a status-queue write
  is not modelled; replies are read by polling (`read_reply`).

## 4. Files and API

| File | Contents |
|---|---|
| `sim/host_traffic.py` | framing (`build_rpc_message`, `parse_rpc_message`, `build_queue_element`, `parse_queue_element`, `compute_message_checksum`), `MsgqQueue`, `SharedQueues`, `DoorbellModel`, `DoorbellBlock` (a `sim.devices` overlay), `HostTraffic`, `load_stimulus`, `replay_stimulus`, `run_demo`, CLI |
| `tests/test_host_traffic.py` | 36 offline tests: framing/checksum, queue arithmetic + cross-link, doorbell model + guest instruction access, submit/dispatch/reply, determinism, replay (stimulus + capture doc), recorder events, demo + CLI |
| `docs/51-HOST-TRAFFIC-LOOP.md` | this page |

`HostTraffic` surface:

* `submit(message)` / `submit_rpc(function, params, sequence=None)` — frame,
  write one command element, advance `writePtr`, ring the doorbell (recorded).
* `poll()` — GSP-side drain of the command ring through the handler table,
  writing framed replies to the status ring; returns the dispatched count.
  Bounded by construction (each iteration consumes one element).
* `read_reply()` — host-side consumption of one status element.
* `run_cycles(budget)` — run the attached `sim.machine.Machine` for a bounded
  budget (cap 2M) and then `poll`; without a machine it only polls.
* `transcript()` / `state()` / `close()` — deterministic transcript, queue and
  doorbell state; `close()` writes `blobs/host-traffic.json` when a recorder
  is attached.
* `doorbell_block()` — a `DoorbellBlock` sharing the same `DoorbellModel`, so
  the guest-visible register and the host model are one state. Attach it with
  `machine.devices.add(host.doorbell_block())`; the narrow window overrides
  the wider falcon window only for `0x110c00..0x110c3f`.

Records: `greeneye-host-traffic-{demo,replay,log,reply}/1`. The transcript has
no host timestamps and is byte-identical across runs; the optional
`greeneye-session/1` recorder events (`host_traffic_config`, `doorbell`,
`host_submit`, `host_dispatch`, `host_reply_read`, `host_replay_skip`,
`host_transcript`) follow the standard event shape, where `t` is host-timed
and non-semantic.

## 5. How to run it

```bash
cd open-iov/debug/risc-v-gsp-rm-vm

# Deterministic end-to-end demo (echo + status query + real-shaped 512-byte
# GSP_RM_CONTROL; stub replies), with a recorded session:
python3 -m sim.host_traffic demo --out dumps/host-traffic-demo

# Replay a stimulus document (or a capture document / raw capture log):
python3 -m sim.host_traffic replay --stimulus dumps/rm-stimulus.json --out dumps/host-traffic-replay
python3 -m sim.host_traffic replay --stimulus capture.json --allow-truncated

# Tests:
python3 -m unittest tests.test_host_traffic -v
```

`replay` maps stimulus records as follows: `send` carries the wire function
and payload directly; `control` and `alloc` records carry a named id and are
mapped with the capture tool's constants (76 / 103) using the captured
parameter block as the payload. That identity is an **assumption** until a
capture with full send-side payloads exists (the current runbook helper also
truncates payloads above 192 bytes; truncated messages are refused by default
and skipped with `--allow-truncated`).

## 6. Real vs stubbed — the honest table

| Piece | State |
|---|---|
| RPC header + queue element framing, checksum | **real** (mirrored; drift-tested against `harness/rm_capture.py` constants) |
| msgq header arithmetic + `SWAP_RX` cross-link | **real** (mirrored from the public queue header and `msgqTxCreate`/`msgqRxLink`) |
| Doorbell register offset/width/access | **real** (public register header); read-back is storage of the last write, firmware read use unobserved |
| Guest access to the doorbell window through `Machine` | **real** (device window + recorded store PC); exercised by instruction-level tests |
| Command/status rings in simulated memory | **real layout**, fixture addresses and sizes |
| Multi-element messages | **real split** (contiguous staged stream across ring slots, wrap-safe, checksum over the stream) |
| Dispatcher (`echo`, `status_query`) | **stub** (ids 0x40/0x41 are not RM functions) |
| Reply to real RM functions | **stub** `NV_ERR_NOT_SUPPORTED` — framed and transported exactly; not firmware behaviour |
| Real firmware message loop | **not run** (B5 remains open) |
| RM semantics (controls, allocs, objects, events) | **absent** |
| MSI-X / GSP→host interrupt | **not modelled** (polling) |
| Timing/cycles | **not claimed** (L2 absent; `run_cycles` is an instruction budget) |

## 7. What this does NOT do yet (and the next step to B5)

Not done: booting the real image and serving its message-loop poll; reading
the real `GSP_ARGUMENTS_CACHED`/queue-init parameters; event/MSI-X delivery;
any RM handler.

Next step for B5, concretely (each point is bounded and session-recorded):

1. Boot the real 570.144 image to B3 (`sim.gsp_boot`, 474k instructions,
   stop at the message-loop port wait, `ecall` at `0x4018038`).
2. Attach `host.doorbell_block()` to the boot registry before the run, so the
   firmware's first queue-head access is served by the model and recorded,
   and place the command/status rings at addresses the firmware is told
   (stub: the fixture block; real: the addresses from a captured
   `GSP_ARGUMENTS_CACHED`).
3. Ring the doorbell from the host model and continue the machine with
   `run_cycles` until it polls the rings; classify the first poll: which
   address does it read (queue header? Tx/Rx header? entry?) and what
   read-back semantics does it need.
4. Compare the firmware's observed ring arithmetic against this module's
   (field-by-field); where the real init used `MSGQ_FLAGS_SWAP_RX`, the
   cross-link must already match; fix only with a recorded session pair
   (docs/06 accuracy loop), never from a decompiler reading alone.
5. Then replay one captured stimulus message with the real image and record
   the reply/writes as the B5 session. Until a supervised capture exists,
   this stops at step 3–4 with the stub dispatcher.

The seam for step 3 already exists: `HostTraffic(machine=..., shared_base=...)`
uses the machine's RAM for the rings and `run_cycles` steps the core; the
doorbell block overlays the falcon window without touching
`sim/devices/falcon.py` or the profile.

## 8. Limits, deferrals and verification

Limits: L0/L1 only; no capture has been consumed yet (no supervised runbook
execution); control/alloc payload identity is an assumption; truncated
payloads are refused or skipped, never padded; a stub reply is never a
firmware claim; no real queue-init parameters.

Deferred: real-firmware B4/B5 wiring (above), event/MSI-X delivery, RM-object
state seeding (docs/49 build #2), golden matrices (#3).

Verification of this wave: `python3 -m unittest tests.test_host_traffic -v`
→ 36 tests green; full suite `make test` green; `demo` `ok: true` with all
checks (`echo` payload round-trip, status magic, 512-byte control replay,
framed stub verdict, one doorbell ring per submit, all replies read);
`ruff check` clean on the new files. Two-run determinism is asserted by
tests (transcript and replies byte-equal).