code wiki / _hdl_build / rv64im_min_virtio.nx
rv64im_min_virtio.nx source
↩ module page · 522 lines · 29892 B
1// rv64im_min_virtio.nx -- legacy virtio-MMIO transport device model (K-R2-001a).
2//
3// Carves the legacy virtio-MMIO register block at base 0x10001000 into the sovereign
4// sim's load32/store32 dispatch. This is the TRANSPORT layer: it exposes the device
5// identity (MagicValue/Version/DeviceID/VendorID), the feature-negotiation registers
6// (HostFeatures/GuestFeatures), and the Status register the driver uses to walk the
7// virtio device-initialization handshake (spec virtio v1.2 sec 3.1 / legacy MMIO
8// sec 4.2.2). The split-virtqueue + DMA + blk sector round-trip land in K-R2-001b;
9// virtio-net loopback in K-R2-001c. This rung is the handshake only.
10//
11// Memory map (matches qemu virt machine with virtio-mmio.force-legacy=true; the spec
12// knowledge/specs/virtio_blk_handshake_virt.spec is the single data source -- these
13// consts mirror it, and the gate proves the device, the driver, and the spec all agree
14// end-to-end on the SOVEREIGN emu):
15// 0x10001000 MagicValue (RO) 0x74726976 ("virt", little-endian)
16// 0x10001004 Version (RO) 1 (legacy)
17// 0x10001008 DeviceID (RO) 2 (block device)
18// 0x1000100C VendorID (RO) 0x554D4551 ("QEMU")
19// 0x10001010 HostFeatures (RO) device-offered feature bits
20// 0x10001020 GuestFeatures(WO) driver-accepted feature bits (latched)
21// 0x10001070 Status (RW) driver writes OR-in ACK/DRIVER/FEATURES_OK/DRIVER_OK;
22// a write of 0 = device reset (Status -> 0)
23//
24// Status: SEED. 2026-06-13. Register storage + identity reads + Status latch + reset.
25// Synth target (Tier B+ MPW): identity ROM + 2 feature flops + 1 status flop + decoder.
26
27import "nx_syscalls.nx"
28import "nishi_hdl_primitives.nx"
29
30// ===== MMIO addresses =================================================
31const NX_VIRTIO_BASE: i64 = 0x10001000
32const NX_VIRTIO_END: i64 = 0x10001100 // one device's 256-byte register window
33// ===== second device block: virtio-net (K-R2-001c1) ====================
34// A SECOND legacy virtio-MMIO transport at base 0x10002000 returning DeviceID=1
35// (virtio-net). The blk device at 0x10001000 is left byte-untouched -- the net block is
36// an INDEPENDENT NxVirtioMmio instance carrying its OWN base + device-id (the per-instance
37// v.base / v.deviceid fields below), so the identity reads + range check resolve against
38// the instance's base, not a single global const. c1 is the transport HANDSHAKE only
39// (identity + Status + feature regs); the net queue-config + tx/rx loopback land in c2/c3.
40const NX_VIRTIO_NET_BASE: i64 = 0x10002000
41const NX_VIRTIO_NET_END: i64 = 0x10002100 // the net device's 256-byte register window
42const NX_VIRTIO_NET_DEVICEID: i64 = 1 // virtio-net
43
44const NX_VIRTIO_OFF_MAGIC: i64 = 0x000
45const NX_VIRTIO_OFF_VERSION: i64 = 0x004
46const NX_VIRTIO_OFF_DEVICEID: i64 = 0x008
47const NX_VIRTIO_OFF_VENDORID: i64 = 0x00C
48const NX_VIRTIO_OFF_HOSTFEAT: i64 = 0x010
49const NX_VIRTIO_OFF_GUESTFEAT: i64 = 0x020
50// ===== legacy virtqueue-config register block (K-R2-001b1) ====================
51// 0x030 QueueSel (WO) selects the virtqueue the next config regs address
52// 0x034 QueueNumMax (RO) device-offered max ring size for the selected queue
53// 0x038 QueueNum (WO) driver latches the negotiated ring size
54// 0x03C QueueAlign (WO) used-ring alignment (legacy) -- latched, no semantics yet
55// 0x040 QueuePFN (RW) guest ring page-frame number; reads back what was written
56// 0x050 QueueNotify (WO) kick: a write increments the per-queue notify count
57const NX_VIRTIO_OFF_QUEUESEL: i64 = 0x030
58const NX_VIRTIO_OFF_QUEUENUMMAX: i64 = 0x034
59const NX_VIRTIO_OFF_QUEUENUM: i64 = 0x038
60const NX_VIRTIO_OFF_QUEUEALIGN: i64 = 0x03C
61const NX_VIRTIO_OFF_QUEUEPFN: i64 = 0x040
62const NX_VIRTIO_OFF_QUEUENOTIFY: i64 = 0x050
63// ===== b2a descriptor-DMA result register (K-R2-001b2a) ====================
64// 0x060 QueueDescPeek (RO) the device latches the first 32-bit field (desc.addr low
65// word) it DMA-read from the QueuePFN-bound ring page on the QueueNotify kick;
66// the driver reads it back to prove the device walked the descriptor it wrote.
67const NX_VIRTIO_OFF_QUEUEDESCPEEK: i64 = 0x060
68// ===== b2b avail-consume + used-ring writeback result register (K-R2-001b2b) ==========
69// 0x064 QueueUsedIdxPeek (RO) the device latches the used.idx it bumped into guest RAM
70// on the last QueueNotify kick (after consuming the avail ring head + writing the
71// used ring); the driver reads it back to prove the device completed the used-ring
72// writeback (the binding proof).
73const NX_VIRTIO_OFF_QUEUEUSEDIDXPEEK: i64 = 0x064
74// ===== b2c status-byte writeback result register (K-R2-001b2c) ==========
75// 0x068 QueueStatPeek (RO) the device latches the virtio-blk request status byte
76// (VIRTIO_BLK_S_OK) it DMA-WROTE into the status descriptor's guest buffer on the
77// last QueueNotify kick (after the used-ring writeback); the driver reads it back to
78// prove the device completed the request status writeback (the binding proof).
79const NX_VIRTIO_OFF_QUEUESTATPEEK: i64 = 0x068
80// ===== b3 sector-data round-trip result register (K-R2-001b3) ==========
81// 0x06C QueueSectPeek (RO) on the b2b kick's writeback path the device follows the
82// consumed descriptor's data pointer (desc[head].addr) into guest RAM and DMA-READS
83// the FIRST 32-bit word of the data buffer the driver placed the sector pattern into,
84// then latches it into the QueueSectPeek RO result register. The driver reads it back
85// and verifies it == sect_expected -- proving REAL sector DATA round-tripped through
86// the ring: the driver wrote the data buffer, the device read it via the descriptor's
87// addr binding, the driver read the device's latched copy back. This is the data-
88// plane binding proof (b2a..b2c proved the control plane: desc/used/status).
89const NX_VIRTIO_OFF_QUEUESECTPEEK: i64 = 0x06C
90const NX_VIRTIO_OFF_STATUS: i64 = 0x070
91
92// ===== Device identity (read-only register values) =================================================
93const NX_VIRTIO_MAGIC: i64 = 0x74726976 // "virt"
94const NX_VIRTIO_VERSION: i64 = 1 // legacy MMIO
95const NX_VIRTIO_DEVICEID: i64 = 2 // virtio-blk
96const NX_VIRTIO_VENDORID: i64 = 0x554D4551 // "QEMU"
97const NX_VIRTIO_HOSTFEAT: i64 = 0x00000020 // offered feature bits (legacy blk subset)
98const NX_VIRTIO_QUEUENUMMAX: i64 = 8 // device-offered max ring size (legacy blk q0)
99// ===== b2a descriptor layout (legacy 16-byte split-virtqueue descriptor) =============
100// The device DMA-reads the FIRST 32-bit field (desc.addr low word) at ring+0 on a notify.
101const NX_VIRTIO_DESC_OFF_ADDR: i64 = 0 // desc.addr (8 bytes); low word is DMA-read
102// ===== b2b legacy split-virtqueue avail/used ring byte offsets (within the ring page) ==
103// Legacy layout for queue_num=8 (must match virtio_blk_availused_virt.spec; the driver
104// lays the avail ring at these offsets, the device consumes it + writes the used ring):
105// avail.ring[0] @ +0x084 (the first available descriptor head index the driver publishes)
106// avail.idx @ +0x082 (the driver bumps it to 1 to publish one buffer)
107// used.ring[0] @ +0x104 (used element: id(4)+len(4); the device writes the consumed head)
108// used.idx @ +0x102 (the device bumps it after writing a used element)
109const NX_VIRTIO_AVAIL_RING_OFF: i64 = 0x084
110const NX_VIRTIO_AVAIL_IDX_OFF: i64 = 0x082
111const NX_VIRTIO_USED_RING_OFF: i64 = 0x104 // used.ring[0].id; .len at +4
112const NX_VIRTIO_USED_IDX_OFF: i64 = 0x102
113const NX_VIRTIO_QUEUE_NUM: i64 = 8 // ring size (queuenum); head mask = num-1
114// ===== b2c status-byte writeback (K-R2-001b2c) =====================================
115// On the b2b used-ring-writeback path the device DMA-WRITES the virtio-blk request status
116// byte VIRTIO_BLK_S_OK into the status descriptor's guest buffer (a fixed spec-declared
117// guest-physical address; the minimal-fixture approach b2a used for desc_addr_lo), then
118// latches it into the QueueStatPeek RO register. STAT_BUF_PHYS must match the spec's
119// status_buf_addr (page 4 of guest RAM, clear of the ring page + used struct + data buffer,
120// valid on BOTH lanes). S_OK=0 = the virtio-blk status byte for a completed-OK request.
121const NX_VIRTIO_STAT_BUF_PHYS: i64 = 0x80004000
122const NX_VIRTIO_BLK_S_OK: i64 = 0
123
124// ===== Status bits the driver ORs in =================================================
125const NX_VIRTIO_STAT_ACK: i64 = 1 // ACKNOWLEDGE
126const NX_VIRTIO_STAT_DRIVER: i64 = 2 // DRIVER
127const NX_VIRTIO_STAT_DRIVER_OK: i64 = 4 // DRIVER_OK
128const NX_VIRTIO_STAT_FEATURES_OK: i64 = 8 // FEATURES_OK
129
130// ===== Verdicts =================================================
131const NX_VIRTIO_OK: i64 = 0
132const NX_VIRTIO_ADDR_OUT_OF_RANGE: i64 = 1
133
134// ===== Storage =================================================
135//
136// Caller allocates a 6-i64 backing buffer:
137// slot 0: Status (latched OR of driver-written bits; reset to 0 on a Status write of 0)
138// slot 1: GuestFeatures (latched value of the driver's last GuestFeatures write)
139// slot 2: QueueSel (latched index of the currently-selected virtqueue)
140// slot 3: QueueNum (latched negotiated ring size for the selected queue)
141// slot 4: QueuePFN (latched guest ring page-frame number; read-back register)
142// slot 5: QueueNotify (count of kicks the driver wrote to QueueNotify)
143// slot 6: QueueDescPeek (b2a: the first 32-bit descriptor field the device DMA-read from
144// the QueuePFN-bound ring page on the last kick; RO result register)
145// (K-R2-001b1 models one virtqueue's config; K-R2-001b2a adds the descriptor-DMA read on
146// the notify kick (nx_virtio_notify_dma). All queue slots reset to 0 on a Status write 0.)
147const NX_VIRTIO_SLOT_STATUS: i64 = 0
148const NX_VIRTIO_SLOT_GUESTFEAT: i64 = 1
149const NX_VIRTIO_SLOT_QSEL: i64 = 2
150const NX_VIRTIO_SLOT_QNUM: i64 = 3
151const NX_VIRTIO_SLOT_QPFN: i64 = 4
152const NX_VIRTIO_SLOT_QNOTIFY: i64 = 5
153// b2a: the first 32-bit descriptor field (desc.addr low word) the device DMA-read from the
154// QueuePFN-bound ring page on the last QueueNotify kick; exposed RO at QueueDescPeek. 0
155// until a notify has DMA-walked a descriptor. Reset to 0 on a Status-write-of-0 reset.
156const NX_VIRTIO_SLOT_DESCPEEK: i64 = 6
157// b2b: the used.idx the device bumped into guest RAM on the last notify (after consuming the
158// avail ring head + writing the used ring); exposed RO at QueueUsedIdxPeek. 0 until a notify
159// has completed a used-ring writeback. Reset to 0 on a Status-write-of-0 reset.
160const NX_VIRTIO_SLOT_USEDIDX: i64 = 7
161// b2c: the virtio-blk request status byte (VIRTIO_BLK_S_OK) the device DMA-wrote into the
162// status descriptor's guest buffer on the last notify's used-ring-writeback path; exposed RO
163// at QueueStatPeek. 0 until a notify has completed a status writeback (0 also = S_OK, so the
164// driver's read-back-==-0 check distinguishes "writeback done, OK" only because the writeback
165// runs on the SAME used.idx-advancing path the b2b USED canary already proves ran). Reset to
166// 0 on a Status-write-of-0 reset.
167const NX_VIRTIO_SLOT_STATPEEK: i64 = 8
168// b3: the first 32-bit word of the descriptor's data buffer (desc[head].addr) the device
169// DMA-read on the last notify's writeback path; exposed RO at QueueSectPeek. 0 until a notify
170// has completed a sector-data round-trip. Reset to 0 on a Status-write-of-0 reset.
171const NX_VIRTIO_SLOT_SECTPEEK: i64 = 9
172const NX_VIRTIO_SLOT_N: i64 = 10
173
174struct NxVirtioMmio {
175 storage: *i64 // 6 i64s
176 valid: i64
177 base: i64 // this instance's MMIO base (blk=0x10001000, net=0x10002000)
178 devid: i64 // this instance's DeviceID (blk=2, net=1) -- the identity read returns it
179}
180
181// shared storage/state reset for every device instance (blk + net) -- keeps the two init
182// paths bit-identical on the register surface (DRY: one reset, two devices).
183func nx_virtio_reset_storage(storage: *i64) -> i64 {
184 storage[NX_VIRTIO_SLOT_STATUS] = 0
185 storage[NX_VIRTIO_SLOT_GUESTFEAT] = 0
186 storage[NX_VIRTIO_SLOT_QSEL] = 0
187 storage[NX_VIRTIO_SLOT_QNUM] = 0
188 storage[NX_VIRTIO_SLOT_QPFN] = 0
189 storage[NX_VIRTIO_SLOT_QNOTIFY] = 0
190 storage[NX_VIRTIO_SLOT_DESCPEEK] = 0
191 storage[NX_VIRTIO_SLOT_USEDIDX] = 0
192 storage[NX_VIRTIO_SLOT_STATPEEK] = 0
193 storage[NX_VIRTIO_SLOT_SECTPEEK] = 0
194 return NX_VIRTIO_OK
195}
196
197// init the BLK device instance (DeviceID=2 @ 0x10001000). Backward-compatible signature:
198// every existing caller keeps the same blk identity (the b-series gates stay byte-identical).
199func nx_virtio_init(v: *NxVirtioMmio, storage: *i64) -> i64 {
200 if (v as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
201 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
202 v.storage = storage
203 v.valid = 1
204 v.base = NX_VIRTIO_BASE
205 v.devid = NX_VIRTIO_DEVICEID
206 nx_virtio_reset_storage(storage)
207 return NX_VIRTIO_OK
208}
209
210// init the NET device instance (DeviceID=1 @ 0x10002000) -- the K-R2-001c1 second device.
211// Same register surface as blk (identity + Status + feature regs for the handshake); only
212// the base + device-id differ. The blk device is untouched by this -- a SEPARATE instance.
213func nx_virtio_init_net(v: *NxVirtioMmio, storage: *i64) -> i64 {
214 if (v as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
215 if (storage as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
216 v.storage = storage
217 v.valid = 1
218 v.base = NX_VIRTIO_NET_BASE
219 v.devid = NX_VIRTIO_NET_DEVICEID
220 nx_virtio_reset_storage(storage)
221 return NX_VIRTIO_OK
222}
223
224// ===== Range check =================================================
225// Resolved against THIS instance's base (per-device window), so the blk and net blocks
226// each own their own 256-byte MMIO window.
227func nx_virtio_addr_in_range(v: *NxVirtioMmio, addr: i64) -> i64 {
228 if addr < v.base { return 0 }
229 if addr >= v.base + 0x100 { return 0 }
230 return 1
231}
232
233// ===== MMIO read (32-bit) =================================================
234//
235// Identity registers return their spec constants; HostFeatures returns the offered
236// feature bits; Status returns the latched OR of driver-written bits; GuestFeatures
237// reads back the driver's last written value (read-back convenience); all other
238// offsets return 0 (sparse, like the real device). value_out[0] gets the word.
239func nx_virtio_read32(v: *NxVirtioMmio, addr: i64, value_out: *i64) -> i64 {
240 if v.valid != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
241 if (value_out as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
242 if nx_virtio_addr_in_range(v, addr) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
243 let off: i64 = addr - v.base
244 if off == NX_VIRTIO_OFF_MAGIC {
245 value_out[0] = NX_VIRTIO_MAGIC
246 return NX_VIRTIO_OK
247 }
248 if off == NX_VIRTIO_OFF_VERSION {
249 value_out[0] = NX_VIRTIO_VERSION
250 return NX_VIRTIO_OK
251 }
252 if off == NX_VIRTIO_OFF_DEVICEID {
253 // per-instance DeviceID: blk=2, net=1 -- the identity read the driver verifies.
254 value_out[0] = v.devid
255 return NX_VIRTIO_OK
256 }
257 if off == NX_VIRTIO_OFF_VENDORID {
258 value_out[0] = NX_VIRTIO_VENDORID
259 return NX_VIRTIO_OK
260 }
261 if off == NX_VIRTIO_OFF_HOSTFEAT {
262 value_out[0] = NX_VIRTIO_HOSTFEAT
263 return NX_VIRTIO_OK
264 }
265 if off == NX_VIRTIO_OFF_GUESTFEAT {
266 value_out[0] = v.storage[NX_VIRTIO_SLOT_GUESTFEAT]
267 return NX_VIRTIO_OK
268 }
269 if off == NX_VIRTIO_OFF_QUEUESEL {
270 value_out[0] = v.storage[NX_VIRTIO_SLOT_QSEL]
271 return NX_VIRTIO_OK
272 }
273 if off == NX_VIRTIO_OFF_QUEUENUMMAX {
274 // device-offered max ring size for the selected queue (RO constant).
275 value_out[0] = NX_VIRTIO_QUEUENUMMAX
276 return NX_VIRTIO_OK
277 }
278 if off == NX_VIRTIO_OFF_QUEUENUM {
279 value_out[0] = v.storage[NX_VIRTIO_SLOT_QNUM]
280 return NX_VIRTIO_OK
281 }
282 if off == NX_VIRTIO_OFF_QUEUEPFN {
283 // RW: reads back the guest-written ring page-frame number (binding proof).
284 value_out[0] = v.storage[NX_VIRTIO_SLOT_QPFN]
285 return NX_VIRTIO_OK
286 }
287 if off == NX_VIRTIO_OFF_QUEUEDESCPEEK {
288 // RO (b2a): the first 32-bit descriptor field the device DMA-read from the ring
289 // page on the last QueueNotify kick. The driver verifies it == the descriptor
290 // addr-low word it wrote -- the descriptor-DMA binding proof.
291 value_out[0] = v.storage[NX_VIRTIO_SLOT_DESCPEEK]
292 return NX_VIRTIO_OK
293 }
294 if off == NX_VIRTIO_OFF_QUEUEUSEDIDXPEEK {
295 // RO (b2b): the used.idx the device bumped into guest RAM on the last kick after
296 // consuming the avail ring head + writing the used ring. The driver verifies it ==
297 // used_idx_expected -- the used-ring writeback binding proof.
298 value_out[0] = v.storage[NX_VIRTIO_SLOT_USEDIDX]
299 return NX_VIRTIO_OK
300 }
301 if off == NX_VIRTIO_OFF_QUEUESTATPEEK {
302 // RO (b2c): the virtio-blk request status byte the device DMA-wrote into the status
303 // descriptor's guest buffer on the last kick after the used-ring writeback. The driver
304 // verifies it == stat_expected (0 = VIRTIO_BLK_S_OK) -- the status-byte writeback proof.
305 value_out[0] = v.storage[NX_VIRTIO_SLOT_STATPEEK]
306 return NX_VIRTIO_OK
307 }
308 if off == NX_VIRTIO_OFF_QUEUESECTPEEK {
309 // RO (b3): the first 32-bit word of the descriptor's data buffer the device DMA-read
310 // (following desc[head].addr) on the last kick's writeback path. The driver verifies it
311 // == sect_expected -- the sector-data round-trip binding proof (the data-plane proof).
312 value_out[0] = v.storage[NX_VIRTIO_SLOT_SECTPEEK]
313 return NX_VIRTIO_OK
314 }
315 if off == NX_VIRTIO_OFF_STATUS {
316 value_out[0] = v.storage[NX_VIRTIO_SLOT_STATUS]
317 return NX_VIRTIO_OK
318 }
319 value_out[0] = 0
320 return NX_VIRTIO_OK
321}
322
323// ===== MMIO write (32-bit) =================================================
324//
325// GuestFeatures latches the value. Status ORs in the written bits (the driver builds
326// the device-init handshake by progressively writing ACK / DRIVER / FEATURES_OK /
327// DRIVER_OK); a Status write of 0 resets the device (Status -> 0, per virtio reset).
328// Read-only identity registers ignore writes (like the real device).
329func nx_virtio_write32(v: *NxVirtioMmio, addr: i64, value: i64) -> i64 {
330 if v.valid != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
331 if nx_virtio_addr_in_range(v, addr) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
332 let off: i64 = addr - v.base
333 if off == NX_VIRTIO_OFF_GUESTFEAT {
334 v.storage[NX_VIRTIO_SLOT_GUESTFEAT] = value & 0xffffffff
335 return NX_VIRTIO_OK
336 }
337 if off == NX_VIRTIO_OFF_QUEUESEL {
338 // select the virtqueue the following config registers address (WO).
339 v.storage[NX_VIRTIO_SLOT_QSEL] = value & 0xffffffff
340 return NX_VIRTIO_OK
341 }
342 if off == NX_VIRTIO_OFF_QUEUENUM {
343 // latch the driver-negotiated ring size for the selected queue (WO).
344 v.storage[NX_VIRTIO_SLOT_QNUM] = value & 0xffffffff
345 return NX_VIRTIO_OK
346 }
347 if off == NX_VIRTIO_OFF_QUEUEALIGN {
348 // legacy used-ring alignment: accepted, no DMA semantics until K-R2-001b2 (WO).
349 return NX_VIRTIO_OK
350 }
351 if off == NX_VIRTIO_OFF_QUEUEPFN {
352 // bind the guest ring page-frame number (RW: read-back proves the binding).
353 v.storage[NX_VIRTIO_SLOT_QPFN] = value & 0xffffffff
354 return NX_VIRTIO_OK
355 }
356 if off == NX_VIRTIO_OFF_QUEUENOTIFY {
357 // kick: count the notify (the descriptor walk it triggers lands in K-R2-001b2).
358 v.storage[NX_VIRTIO_SLOT_QNOTIFY] = v.storage[NX_VIRTIO_SLOT_QNOTIFY] + 1
359 return NX_VIRTIO_OK
360 }
361 if off == NX_VIRTIO_OFF_STATUS {
362 let w: i64 = value & 0xff
363 if w == 0 {
364 // virtio device reset: Status -> 0, feature negotiation + queue config forgotten.
365 v.storage[NX_VIRTIO_SLOT_STATUS] = 0
366 v.storage[NX_VIRTIO_SLOT_GUESTFEAT] = 0
367 v.storage[NX_VIRTIO_SLOT_QSEL] = 0
368 v.storage[NX_VIRTIO_SLOT_QNUM] = 0
369 v.storage[NX_VIRTIO_SLOT_QPFN] = 0
370 v.storage[NX_VIRTIO_SLOT_QNOTIFY] = 0
371 v.storage[NX_VIRTIO_SLOT_DESCPEEK] = 0
372 v.storage[NX_VIRTIO_SLOT_USEDIDX] = 0
373 v.storage[NX_VIRTIO_SLOT_STATPEEK] = 0
374 v.storage[NX_VIRTIO_SLOT_SECTPEEK] = 0
375 return NX_VIRTIO_OK
376 }
377 // latch = OR-in the newly-written bits (driver accumulates handshake state).
378 v.storage[NX_VIRTIO_SLOT_STATUS] = v.storage[NX_VIRTIO_SLOT_STATUS] | w
379 return NX_VIRTIO_OK
380 }
381 // identity + host-features registers are read-only: writes silently dropped.
382 return NX_VIRTIO_OK
383}
384
385// ===== Descriptor-DMA on notify (K-R2-001b2a) =================================================
386//
387// On a QueueNotify kick the legacy device walks the driver's virtqueue. This first slice
388// DMA-reads the FIRST 32-bit field (desc.addr low word) of descriptor 0 from the QueuePFN-
389// bound guest ring page and latches it into the QueueDescPeek RO result register -- proving
390// the device genuinely read the descriptor the driver laid in guest RAM (the binding proof).
391//
392// The sim owns memory, so the store32 dispatch hands this function the sim's flat RAM buffer
393// + its physical base + size. The ring page guest-physical address = QueuePFN << 12; its
394// byte offset into mem_buf = (QueuePFN << 12) - mem_base. The read is bounds-checked: if the
395// ring page (or the 4-byte field) is outside [mem_base, mem_base+mem_size) the DMA is a no-op
396// (DESCPEEK stays 0) -- defensive at the device/memory boundary (no out-of-bounds RAM read).
397// Returns NX_VIRTIO_OK on a completed DMA, NX_VIRTIO_ADDR_OUT_OF_RANGE if the ring is unmapped.
398// little-endian DMA helpers over the sim's flat guest RAM (the device owns no memory of its
399// own). off is a byte offset into mem_buf already known to be in [0, mem_size). Each access
400// is bounds-checked by the caller (the notify-DMA walk) before it is invoked.
401func nx_virtio_dma_rd16(mem_buf: *u8, off: i64) -> i64 {
402 let b0: i64 = mem_buf[off] as i64
403 let b1: i64 = mem_buf[off + 1] as i64
404 return b0 | (b1 << 8)
405}
406func nx_virtio_dma_rd32(mem_buf: *u8, off: i64) -> i64 {
407 let b0: i64 = mem_buf[off] as i64
408 let b1: i64 = mem_buf[off + 1] as i64
409 let b2: i64 = mem_buf[off + 2] as i64
410 let b3: i64 = mem_buf[off + 3] as i64
411 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
412}
413func nx_virtio_dma_wr16(mem_buf: *u8, off: i64, val: i64) -> i64 {
414 mem_buf[off] = (val & 0xff) as u8
415 mem_buf[off + 1] = ((val >> 8) & 0xff) as u8
416 return 0
417}
418func nx_virtio_dma_wr32(mem_buf: *u8, off: i64, val: i64) -> i64 {
419 mem_buf[off] = (val & 0xff) as u8
420 mem_buf[off + 1] = ((val >> 8) & 0xff) as u8
421 mem_buf[off + 2] = ((val >> 16) & 0xff) as u8
422 mem_buf[off + 3] = ((val >> 24) & 0xff) as u8
423 return 0
424}
425// is the w-byte guest-RAM access at byte offset off fully in bounds? 1/0.
426func nx_virtio_dma_inrange(off: i64, w: i64, mem_size: i64) -> i64 {
427 if off < 0 { return 0 }
428 if off > mem_size - w { return 0 }
429 return 1
430}
431
432func nx_virtio_notify_dma(v: *NxVirtioMmio, mem_buf: *u8, mem_base: i64, mem_size: i64) -> i64 {
433 if v.valid != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
434 if (mem_buf as i64) == 0 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
435 let pfn: i64 = v.storage[NX_VIRTIO_SLOT_QPFN]
436 let ring_phys: i64 = pfn << 12
437 if ring_phys < mem_base { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
438 let ring_off: i64 = ring_phys - mem_base
439 if ring_off < 0 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
440 // ---- b2a: DMA-read descriptor 0's first 32-bit field (desc.addr low word) ----
441 let desc0_off: i64 = ring_off + NX_VIRTIO_DESC_OFF_ADDR
442 if nx_virtio_dma_inrange(desc0_off, 4, mem_size) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
443 let word: i64 = nx_virtio_dma_rd32(mem_buf, desc0_off)
444 v.storage[NX_VIRTIO_SLOT_DESCPEEK] = word & 0xffffffff
445 // ---- b2b: consume the AVAILABLE ring head, then write the USED ring + advance used.idx ----
446 // 1) read avail.idx (how many buffers the driver has published since queue init).
447 let availidx_off: i64 = ring_off + NX_VIRTIO_AVAIL_IDX_OFF
448 if nx_virtio_dma_inrange(availidx_off, 2, mem_size) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
449 let availidx: i64 = nx_virtio_dma_rd16(mem_buf, availidx_off)
450 // read the current used.idx (how many the device has already completed).
451 let usedidx_off: i64 = ring_off + NX_VIRTIO_USED_IDX_OFF
452 if nx_virtio_dma_inrange(usedidx_off, 2, mem_size) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
453 let prev_usedidx: i64 = nx_virtio_dma_rd16(mem_buf, usedidx_off)
454 // nothing new available? (avail.idx has not advanced past used.idx) -> no used writeback.
455 // This makes the FIRST notify (the b2a DESC kick, fired before the driver publishes the
456 // avail ring, avail.idx==0) a no-op for the used ring; only the b2b kick (after avail.idx
457 // is bumped to 1) writes the used ring -> used.idx advances 0 -> 1 exactly once.
458 if availidx == prev_usedidx { v.storage[NX_VIRTIO_SLOT_USEDIDX] = prev_usedidx; return NX_VIRTIO_OK }
459 // 2) the newest available descriptor head = avail.ring[(avail.idx - 1) & (num-1)].
460 let mask: i64 = NX_VIRTIO_QUEUE_NUM - 1
461 let ringslot: i64 = (availidx - 1) & mask
462 let availring_off: i64 = ring_off + NX_VIRTIO_AVAIL_RING_OFF + (ringslot * 2)
463 if nx_virtio_dma_inrange(availring_off, 2, mem_size) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
464 let desc_head: i64 = nx_virtio_dma_rd16(mem_buf, availring_off)
465 // 3) read the consumed descriptor's length field (desc[head].len at desc_table + head*16 + 8).
466 let desclen_off: i64 = ring_off + (desc_head * 16) + 8
467 var consumed_len: i64 = 0
468 if nx_virtio_dma_inrange(desclen_off, 4, mem_size) == 1 { consumed_len = nx_virtio_dma_rd32(mem_buf, desclen_off) }
469 // 4) write the USED ring element used.ring[used.idx & mask]: .id = desc_head, .len = len.
470 let used_slot: i64 = prev_usedidx & mask
471 let usedring_off: i64 = ring_off + NX_VIRTIO_USED_RING_OFF + (used_slot * 8)
472 if nx_virtio_dma_inrange(usedring_off, 8, mem_size) != 1 { return 0 - NX_VIRTIO_ADDR_OUT_OF_RANGE }
473 nx_virtio_dma_wr32(mem_buf, usedring_off, desc_head & 0xffffffff)
474 nx_virtio_dma_wr32(mem_buf, usedring_off + 4, consumed_len & 0xffffffff)
475 // 5) advance used.idx in guest RAM (publish the completion to the driver) + latch it RO.
476 let new_usedidx: i64 = (prev_usedidx + 1) & 0xffff
477 nx_virtio_dma_wr16(mem_buf, usedidx_off, new_usedidx)
478 v.storage[NX_VIRTIO_SLOT_USEDIDX] = new_usedidx
479 // ---- b2c: DMA-WRITE the virtio-blk request status byte VIRTIO_BLK_S_OK into the status
480 // descriptor's guest buffer (a fixed spec-declared guest-physical address) + latch RO. ----
481 // This runs ONLY on the used-ring-writeback path (avail.idx advanced past used.idx), so the
482 // status byte lands exactly once, on the b2b kick that completed the request. The status
483 // buffer is at NX_VIRTIO_STAT_BUF_PHYS; its byte offset into the sim's flat guest RAM =
484 // phys - mem_base. Bounds-checked at the device/memory boundary (matching the rd/wr
485 // helpers): if the status buffer is outside [mem_base, mem_base+mem_size) the write is a
486 // no-op (STATPEEK stays 0) -- no out-of-bounds RAM write.
487 let stat_off: i64 = NX_VIRTIO_STAT_BUF_PHYS - mem_base
488 if nx_virtio_dma_inrange(stat_off, 1, mem_size) == 1 {
489 mem_buf[stat_off] = (NX_VIRTIO_BLK_S_OK & 0xff) as u8
490 v.storage[NX_VIRTIO_SLOT_STATPEEK] = NX_VIRTIO_BLK_S_OK
491 }
492 // ---- b3 (K-R2-001b3): follow the consumed descriptor's DATA POINTER into guest RAM and
493 // DMA-READ the first 32-bit word of the data buffer the driver placed the sector pattern
494 // into, then latch it into QueueSectPeek. This is the DATA-plane round-trip: the driver
495 // wrote the sector word into the data buffer (guest RAM); the device follows desc[head].addr
496 // (the descriptor's own data binding, NOT a fixed const -- so it genuinely proves the addr
497 // field bound real data) to read it back; the driver verifies QueueSectPeek == sect_expected.
498 // The descriptor's addr low word is at desc_table + head*16 + NX_VIRTIO_DESC_OFF_ADDR; its
499 // value is a guest-physical address, so the data-buffer RAM offset = data_phys - mem_base.
500 // Bounds-checked at the device/memory boundary (rd helpers' contract): if the descriptor
501 // addr or the data word is outside guest RAM the read is a no-op (SECTPEEK stays 0) -- no
502 // out-of-bounds RAM read. Runs ONLY on the used-ring-writeback path (a real consumed kick),
503 // so the sector word lands exactly once, on the b2b kick that completed the request.
504 let dataptr_off: i64 = ring_off + (desc_head * 16) + NX_VIRTIO_DESC_OFF_ADDR
505 if nx_virtio_dma_inrange(dataptr_off, 4, mem_size) == 1 {
506 let data_phys: i64 = nx_virtio_dma_rd32(mem_buf, dataptr_off)
507 if data_phys >= mem_base {
508 let data_off: i64 = data_phys - mem_base
509 if nx_virtio_dma_inrange(data_off, 4, mem_size) == 1 {
510 let sect_word: i64 = nx_virtio_dma_rd32(mem_buf, data_off)
511 v.storage[NX_VIRTIO_SLOT_SECTPEEK] = sect_word & 0xffffffff
512 }
513 }
514 }
515 return NX_VIRTIO_OK
516}
517
518// ===== Status accessor (harness helper) =================================================
519func nx_virtio_status(v: *NxVirtioMmio) -> i64 {
520 if v.valid != 1 { return 0 }
521 return v.storage[NX_VIRTIO_SLOT_STATUS]
522}