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}