code wiki / _hdl_build / rv64im_min_sim.nx
rv64im_min_sim.nx source
↩ module page · 1276 lines · 61942 B
1// rv64im_min_sim.nx -- single-cycle RV64IM-min behavioural simulator.
2//
3// First runnable thing in nishi-silicon. Ties the previously-shipped
4// modules together so RV64IM programs execute end-to-end in pure
5// NishiLang:
6//
7// decoder (rv64im_min_decoder) -> opcode kind + operand fields
8// ALU (rv64im_min_alu) -> arithmetic compute
9// regfile (rv64im_min_regfile) -> 32 GPRs storage
10// CSR (rv64im_min_csr) -> M-mode CSRs
11// CLINT (rv64im_min_clint) -> MTIP interrupt source
12// UART (rv64im_min_uart) -> debug console
13//
14// Why "single-cycle" not "pipelined": a single-cycle model is the
15// canonical "first simulator" -- one instruction per simulator step,
16// no pipeline hazards, no fetch/decode/execute overlap. Verifies
17// the combinational + sequential modules without the complexity of
18// IF/ID/EX latches or hazard detection. Pipelined model lands in a
19// follow-on commit; Tier A FPGA bitstream gets the pipelined version
20// (no point fabbing a single-cycle core when the pipelined one is
21// also single-issue in-order and only marginally larger).
22//
23// What this enables:
24// - Run RV64IM .text segments emitted by the SOVEREIGN toolchain (nx_cc
25// -> nxasm rv64 backend); gcc/llvm are a diff-lane reference ONLY, never on
26// the chain (this sim IS the genesis ISA spec + the sovereign-cycle yardstick)
27// - Validate the decoder + ALU against any RV64IM disassembler
28// - Boot the nishi-os kernel ELF (after the load/store unit + memory
29// bus land in a follow-on commit) before fabbing FPGA bitstream
30//
31// Status: SEED. 2026-05-26. Single-cycle non-interrupt loop.
32// Interrupt handling, load/store, mret/ecall flow land per the TODO
33// markers below.
34
35import "nx_syscalls.nx"
36import "nishi_hdl_primitives.nx"
37import "rv64im_min_decoder.nx"
38import "rv64im_min_alu.nx"
39import "rv64im_min_regfile.nx"
40import "rv64im_min_csr.nx"
41import "rv64im_min_clint.nx"
42import "rv64im_min_uart.nx"
43import "rv64im_min_virtio.nx"
44import "rv64im_min_nvme.nx"
45import "rv64im_min_nndev.nx"
46import "rv64im_min_mmu.nx"
47import "rv64im_min_gpu.nx"
48import "nx_rvc_expand.nx" // RV64 C (compressed) extension: expand 16-bit -> 32-bit at fetch
49
50// ===== Sim state =================================================
51//
52// The simulator owns instances of every device and a PC register.
53// Memory is a flat byte buffer with the CLINT + UART carve-outs
54// dispatched by the load/store path.
55
56struct NxRv64imSim {
57 pc: i64
58 rf: *NxRv64imRegfile
59 csr: *NxRv64imCsrFile
60 clint: *NxClint
61 uart: *NxUart
62 virtio: *NxVirtioMmio // legacy virtio-MMIO transport, blk @0x10001000 (K-R2-001a)
63 virtio_net:*NxVirtioMmio // second legacy virtio-MMIO transport, net @0x10002000 (K-R2-001c1)
64 nvme: *NxNvmeCtrl // NVMe-class controller, admin SQ/CQ @0x10003000 (X-DRV-W1 stage B)
65 nndev: *NxNnDev // Nishi-native device protocol (NNDP) @0x10005000 (sovereign-device-protocol exceed)
66 mmu: *NxMmu // Sv39 page-table-walk device @0x10006000 (virtual-memory-paging-mmu)
67 xlate_fault: i64 // set by nx_rv64im_xlate on a data-access page fault; step loop traps on it
68 gpu: *NxGpu // GPU-class command-submission controller @0x10007000 (driver-from-spec D)
69 mem_base: i64 // physical address of mem_buf[0] (e.g., 0x80000000)
70 mem_buf: *u8 // byte-addressable RAM
71 mem_size: i64 // bytes
72 valid: i64
73 halted: i64
74 halt_code: i64 // exit code on halt
75 steps: i64 // instructions executed
76 priv: i64 // current privilege: 3=M, 1=S, 0=U (reset M). Sv39 translation
77 // applies only in S/U; M-mode is always Bare (riscv-priv-spec).
78 xlate_store: i64 // 1 if the in-flight data access is a store (page-fault cause select)
79 // ----- Per-PC cycle attribution (M2 silicon-feedback foundation) -----
80 // pc_cycles[i] = total cycles attributable to PC = mem_base + (i << 2).
81 // Indexed by word (4-byte stride) since RV64IM instructions are 32-bit.
82 // Caller allocates pc_cycles_buf with at least (mem_size >> 2) i64s.
83 // Without these the substrate has no per-instruction perf data; with
84 // them the silicon-feedback loop (M3) can pick out top-N hot PCs.
85 pc_cycles_buf: *i64
86 pc_cycles_cap: i64 // capacity in i64 slots
87 pc_cycles_on: i64 // 1 = attribution active; 0 = skip (cheaper hot loop)
88 // ----- A-extension (atomics) LR/SC reservation (F107h, 2026-07-18) -----
89 // Single-address reservation set: lr.w/d records (resv_addr, resv_valid); sc.w/d succeeds
90 // iff still valid AND matches; any sc or amo* clears it. Single-hart model -> an lr/sc pair
91 // with no intervening reservation-breaker succeeds (matches QEMU on the canonical CAS loop);
92 // multi-hart contention is the F107g follow-on.
93 resv_addr: i64
94 resv_valid: i64
95}
96
97// Allocation size for ONE NxRv64imSim, in bytes. EVERY site that mmaps a sim MUST use this --
98// never a literal.
99//
100// ★ROOT FIX 2026-08-07. Five sites hardcoded `sys_mmap(128)` for this struct. 128 bytes is 16
101// i64 slots; the struct has been grown to 26 fields (208 bytes) by the virtio-net, NVMe, NNDP,
102// Sv39-MMU, GPU, privilege, per-PC-cycle and A-extension work. So `halted` (offset 128),
103// `halt_code` (136) and `steps` (144) -- THE THREE FIELDS EVERY VERDICT IS COMPUTED FROM -- were
104// written and read OUT OF BOUNDS.
105//
106// It stayed invisible for months because sys_mmap served each small request from its own 4 KiB
107// page, so the 80-byte overrun landed in page slack and read back correctly BY LUCK. When
108// NXA_SMALL_MAX went 64 -> 256 (2026-08-07) a 128-byte request began coming from the shared small
109// arena, the overrun started landing on a live neighbour, and nx_boot_run_sov reported
110// steps=1953655343 -- which is 0x74726976, the ASCII "virt" of the virtio magic the driver under
111// test had just verified. The arena's own ring detector named it: ARENA-OVERRUN prev_alloc_size=128.
112//
113// AN OUT-OF-BOUNDS WRITE ABSORBED BY PAGE SLACK IS A BUG YOUR ALLOCATOR PAYS 4KB APIECE TO HIDE.
114//
115// 256 is chosen deliberately, not rounded up thoughtlessly: it covers 208 with room for 6 more
116// fields, AND it is <= NXA_SMALL_MAX, so this allocation KEEPS coming from the instrumented small
117// arena. Sizing it above NXA_SMALL_MAX would hand it a private VMA and restore exactly the page
118// slack that hid this bug for months. Growing the struct past 32 fields must raise this constant;
119// _simsize_gate.nx enforces that mechanically so it cannot drift again.
120const NX_RV64IM_SIM_BYTES: i64 = 256
121
122// ===== Sim init =================================================
123//
124// Caller provides allocated regfile/csr/clint/uart + memory buffer.
125// Sim init initialises PC to mem_base + entry_offset (typically 0
126// for kernel ELFs which start at the base of mem).
127
128func nx_rv64im_sim_init(s: *NxRv64imSim,
129 rf: *NxRv64imRegfile,
130 csr: *NxRv64imCsrFile,
131 clint: *NxClint,
132 uart: *NxUart,
133 mem_base: i64,
134 mem_buf: *u8,
135 mem_size: i64,
136 entry_offset: i64) -> i64 {
137 if (s as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
138 if (mem_buf as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
139 if mem_size <= 0 { return 0 - NX_HDL_BAD_KIND }
140 s.pc = mem_base + entry_offset
141 s.rf = rf
142 s.csr = csr
143 s.clint = clint
144 s.uart = uart
145 // virtio attaches separately via nx_rv64im_sim_attach_virtio (keeps the init
146 // signature stable -- API contract stability). Null until the runner wires it.
147 s.virtio = (0 as i64) as *NxVirtioMmio
148 s.virtio_net = (0 as i64) as *NxVirtioMmio
149 s.nvme = (0 as i64) as *NxNvmeCtrl
150 s.nndev = (0 as i64) as *NxNnDev
151 s.mmu = (0 as i64) as *NxMmu
152 s.gpu = (0 as i64) as *NxGpu
153 s.xlate_fault = 0
154 s.priv = 3 // reset to M-mode
155 s.xlate_store = 0
156 s.mem_base = mem_base
157 s.mem_buf = mem_buf
158 s.mem_size = mem_size
159 s.valid = 1
160 s.halted = 0
161 s.halt_code = 0
162 s.steps = 0
163 // Cycle attribution opt-in: caller wires nx_rv64im_sim_enable_pc_cycles
164 // after init. Default off so the hot step loop stays branch-free for
165 // bench runs that don't need profiling.
166 s.pc_cycles_buf = (0 as i64) as *i64
167 s.pc_cycles_cap = 0
168 s.pc_cycles_on = 0
169 // A-extension reservation starts clear (no lr outstanding).
170 s.resv_addr = 0
171 s.resv_valid = 0
172 return NX_HDL_OK
173}
174
175// ===== virtio-MMIO attach (K-R2-001a) =================================================
176//
177// Wires a (already-init'd) virtio-MMIO transport into the sim so load32/store32 route
178// the 0x10001000..NX_VIRTIO_END window to it. Kept separate from sim_init so the init
179// signature stays stable. Null virtio = no device (load32/store32 fall through to 0).
180func nx_rv64im_sim_attach_virtio(s: *NxRv64imSim, virtio: *NxVirtioMmio) -> i64 {
181 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
182 s.virtio = virtio
183 return NX_HDL_OK
184}
185
186// ===== second virtio-MMIO attach: virtio-net (K-R2-001c1) =================================
187// Wires the net device (DeviceID=1) so load32/store32 route the 0x10002000..NX_VIRTIO_NET_END
188// window to it. Independent of the blk device -- the blk window (0x10001000) is untouched.
189func nx_rv64im_sim_attach_virtio_net(s: *NxRv64imSim, virtio_net: *NxVirtioMmio) -> i64 {
190 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
191 s.virtio_net = virtio_net
192 return NX_HDL_OK
193}
194
195// ===== NVMe-class controller attach (X-DRV-W1 stage B) =================================
196// Wires the NVMe controller (admin SQ/CQ @0x10003000) so load32/store32 route the
197// 0x10003000..NX_NVME_END window to it. Independent of the virtio devices -- the blk + net
198// windows are untouched (additive). Null nvme = no device (the window falls through to 0).
199func nx_rv64im_sim_attach_nvme(s: *NxRv64imSim, nvme: *NxNvmeCtrl) -> i64 {
200 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
201 s.nvme = nvme
202 return NX_HDL_OK
203}
204
205// ===== Nishi-native device protocol attach (sovereign-device-protocol exceed) =================
206// Wires the NNDP device (NND1 @0x10004000) so load32/store32 route the 0x10004000..NX_NNDEV_END
207// window to it. Independent of virtio + nvme (their windows are untouched -- additive). Null
208// nndev = no device (the window falls through to 0).
209func nx_rv64im_sim_attach_nndev(s: *NxRv64imSim, nndev: *NxNnDev) -> i64 {
210 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
211 s.nndev = nndev
212 return NX_HDL_OK
213}
214
215// ===== GPU-class controller attach (driver-from-spec instance D, GPU trajectory) =================
216// Wires the GPU controller (command ring @0x10007000) so load32/store32 route the 0x10007000..
217// NX_GPU_END window to it. Independent of the other devices -- their windows are untouched (additive).
218func nx_rv64im_sim_attach_gpu(s: *NxRv64imSim, gpu: *NxGpu) -> i64 {
219 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
220 s.gpu = gpu
221 return NX_HDL_OK
222}
223
224// ===== Sv39 MMU walk device attach (virtual-memory-paging-mmu) =================================
225// Wires the Sv39 page-table-walk device (@0x10006000) so load32/store32 route the 0x10006000..
226// NX_MMU_END window to it. Independent of all other devices (additive). Null mmu = no device.
227func nx_rv64im_sim_attach_mmu(s: *NxRv64imSim, mmu: *NxMmu) -> i64 {
228 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
229 s.mmu = mmu
230 return NX_HDL_OK
231}
232
233// ===== Cycle attribution opt-in =================================================
234//
235// Hand in a pre-allocated buffer of (mem_size >> 2) i64s (one slot per
236// 4-byte instruction word). Once enabled, every step() bumps the slot
237// for the executing PC. Disabled paths skip the bump in 1 branch.
238
239func nx_rv64im_sim_enable_pc_cycles(s: *NxRv64imSim,
240 buf: *i64, cap: i64) -> i64 {
241 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
242 if (buf as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
243 let need: i64 = s.mem_size >> 2
244 if cap < need { return 0 - NX_HDL_GRAPH_FULL }
245 s.pc_cycles_buf = buf
246 s.pc_cycles_cap = cap
247 s.pc_cycles_on = 1
248 var i: i64 = 0
249 while i < cap { buf[i] = 0; i = i + 1 }
250 return NX_HDL_OK
251}
252
253// Read cycles attributable to a specific PC (returns 0 if out of range
254// or attribution disabled).
255func nx_rv64im_sim_pc_cycles_at(s: *NxRv64imSim, pc: i64) -> i64 {
256 if s.pc_cycles_on != 1 { return 0 }
257 if pc < s.mem_base { return 0 }
258 let off: i64 = (pc - s.mem_base) >> 2
259 if off < 0 { return 0 }
260 if off >= s.pc_cycles_cap { return 0 }
261 return s.pc_cycles_buf[off]
262}
263
264// Pick the top-N hottest PCs. out_pcs[i] / out_cycles[i] filled with
265// the i-th hottest (descending). Returns count actually filled (<= n).
266// V1 is O(cap * n) -- fine for n <= ~50; better algorithm when N gets
267// large.
268func nx_rv64im_sim_pc_cycles_topn(s: *NxRv64imSim, n: i64,
269 out_pcs: *i64, out_cycles: *i64) -> i64 {
270 if s.pc_cycles_on != 1 { return 0 }
271 if (out_pcs as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
272 if (out_cycles as i64) == 0 { return 0 - NX_HDL_BAD_KIND }
273 var filled: i64 = 0
274 while filled < n {
275 var best_off: i64 = 0 - 1
276 var best_val: i64 = 0
277 var i: i64 = 0
278 while i < s.pc_cycles_cap {
279 let v: i64 = s.pc_cycles_buf[i]
280 // Skip slots already picked (encoded by negating after pick).
281 if v > best_val {
282 best_val = v
283 best_off = i
284 }
285 i = i + 1
286 }
287 if best_off < 0 { return filled }
288 if best_val == 0 { return filled }
289 out_pcs[filled] = s.mem_base + (best_off << 2)
290 out_cycles[filled] = best_val
291 // Mark picked by zeroing the slot. Destructive but acceptable
292 // for a one-shot top-N read; caller can re-enable + re-run if
293 // they need fresh data.
294 s.pc_cycles_buf[best_off] = 0
295 filled = filled + 1
296 }
297 return filled
298}
299
300// ===== Memory access (load/store) =================================================
301//
302// Routes physical addresses to the right device:
303// - RAM (mem_base..mem_base+mem_size) -> mem_buf
304// - CLINT (0x02000000..0x0200C000) -> clint MMIO
305// - UART (0x10000000..0x10000008) -> uart MMIO
306// - Anything else -> halt (illegal access)
307
308func nx_rv64im_sim_load8(s: *NxRv64imSim, addr: i64) -> i64 {
309 if addr >= s.mem_base {
310 let off: i64 = addr - s.mem_base
311 if off < s.mem_size {
312 return s.mem_buf[off] as i64
313 }
314 }
315 // UART path: LSR polling reads via lbu. Returns the byte in
316 // the low 8 bits; sign-extension done by the caller per funct3.
317 if addr >= NX_UART_BASE { if addr < NX_UART_END {
318 let out: *i64 = (sys_mmap(8)) as *i64
319 out[0] = 0
320 nx_uart_read8(s.uart, addr, out)
321 return out[0] & 0xff
322 }}
323 // CLINT byte reads not used by kernel; return 0.
324 return 0
325}
326
327func nx_rv64im_sim_load16(s: *NxRv64imSim, addr: i64) -> i64 {
328 if addr >= s.mem_base {
329 let off: i64 = addr - s.mem_base
330 if off < s.mem_size - 1 {
331 let b0: i64 = s.mem_buf[off] as i64
332 let b1: i64 = s.mem_buf[off + 1] as i64
333 return b0 | (b1 << 8)
334 }
335 }
336 return 0
337}
338
339func nx_rv64im_sim_load32(s: *NxRv64imSim, addr: i64) -> i64 {
340 if addr >= s.mem_base {
341 let off: i64 = addr - s.mem_base
342 if off < s.mem_size - 3 {
343 let b0: i64 = s.mem_buf[off] as i64
344 let b1: i64 = s.mem_buf[off + 1] as i64
345 let b2: i64 = s.mem_buf[off + 2] as i64
346 let b3: i64 = s.mem_buf[off + 3] as i64
347 return b0 | (b1 << 8) | (b2 << 16) | (b3 << 24)
348 }
349 }
350 // virtio-MMIO transport path (K-R2-001a): lw from the device register window
351 // returns the device-modelled 32-bit register value (identity / features / status).
352 if addr >= NX_VIRTIO_BASE { if addr < NX_VIRTIO_END {
353 if (s.virtio as i64) != 0 {
354 let vout: *i64 = (sys_mmap(8)) as *i64
355 vout[0] = 0
356 nx_virtio_read32(s.virtio, addr, vout)
357 return vout[0] & 0xffffffff
358 }
359 }}
360 // virtio-net device register window (K-R2-001c1): lw from the second transport block
361 // at 0x10002000 returns DeviceID=1 + the same identity/feature/status surface as blk.
362 if addr >= NX_VIRTIO_NET_BASE { if addr < NX_VIRTIO_NET_END {
363 if (s.virtio_net as i64) != 0 {
364 let nout: *i64 = (sys_mmap(8)) as *i64
365 nout[0] = 0
366 nx_virtio_read32(s.virtio_net, addr, nout)
367 return nout[0] & 0xffffffff
368 }
369 }}
370 // NVMe controller register window (X-DRV-W1 stage B): lw from 0x10003000.. returns CAP/VS,
371 // the latched CC/AQA/ASQ/ACQ regs, CSTS (RDY flop the driver polls), and the CQPEEK binding-
372 // proof instrument. An INDEPENDENT device instance -- the virtio windows are untouched.
373 if addr >= NX_NVME_BASE { if addr < NX_NVME_END {
374 if (s.nvme as i64) != 0 {
375 let mout: *i64 = (sys_mmap(8)) as *i64
376 mout[0] = 0
377 nx_nvme_read32(s.nvme, addr, mout)
378 return mout[0] & 0xffffffff
379 }
380 }}
381 // Nishi-native device window (sovereign-device-protocol): lw from 0x10004000.. returns MAGIC/
382 // DEVCLASS identity, the latched ENABLE/CMD_ADDR, and the STATUS/RESULT completion the driver
383 // reads back after a doorbell. An INDEPENDENT instance -- virtio + nvme windows untouched.
384 if addr >= NX_NNDEV_BASE { if addr < NX_NNDEV_END {
385 if (s.nndev as i64) != 0 {
386 let dout: *i64 = (sys_mmap(8)) as *i64
387 dout[0] = 0
388 nx_nndev_read32(s.nndev, addr, dout)
389 return dout[0] & 0xffffffff
390 }
391 }}
392 // Sv39 MMU walk device window (virtual-memory-paging-mmu): lw from 0x10006000.. returns the
393 // translated PADDR + FAULT the device latched on the last doorbell walk. Independent instance.
394 if addr >= NX_MMU_BASE { if addr < NX_MMU_END {
395 if (s.mmu as i64) != 0 {
396 let uout: *i64 = (sys_mmap(8)) as *i64
397 uout[0] = 0
398 nx_mmu_read32(s.mmu, addr, uout)
399 return uout[0] & 0xffffffff
400 }
401 }}
402 // GPU-class controller register window (driver-from-spec D): lw from 0x10007000.. returns ID,
403 // CTRL/STATUS, RING base, the FENCE seqno + the RESULTPEEK binding-proof word. Independent instance.
404 if addr >= NX_GPU_BASE { if addr < NX_GPU_END {
405 if (s.gpu as i64) != 0 {
406 let gout: *i64 = (sys_mmap(8)) as *i64
407 gout[0] = 0
408 nx_gpu_read32(s.gpu, addr, gout)
409 return gout[0] & 0xffffffff
410 }
411 }}
412 return 0
413}
414
415// Sign-extend an 8/16/32-bit value to 64 bits.
416func nx_rv64im_sext8(v: i64) -> i64 {
417 let low: i64 = v & 0xff
418 if (low & 0x80) != 0 { return low | (0 - 256) }
419 return low
420}
421func nx_rv64im_sext16(v: i64) -> i64 {
422 let low: i64 = v & 0xffff
423 if (low & 0x8000) != 0 { return low | (0 - 65536) }
424 return low
425}
426
427func nx_rv64im_sim_load64(s: *NxRv64imSim, addr: i64) -> i64 {
428 if addr >= s.mem_base {
429 let off: i64 = addr - s.mem_base
430 if off < s.mem_size - 7 {
431 var v: i64 = 0
432 var i: i64 = 0
433 while i < 8 {
434 let b: i64 = s.mem_buf[off + i] as i64
435 v = v | (b << (i * 8))
436 i = i + 1
437 }
438 return v
439 }
440 }
441 // CLINT MMIO path
442 if addr >= NX_CLINT_BASE { if addr < NX_CLINT_END {
443 let out: *i64 = (sys_mmap(8)) as *i64
444 out[0] = 0
445 nx_clint_read64(s.clint, addr, out)
446 return out[0]
447 }}
448 return 0
449}
450
451func nx_rv64im_sim_store64(s: *NxRv64imSim, addr: i64, value: i64) -> i64 {
452 if addr >= s.mem_base {
453 let off: i64 = addr - s.mem_base
454 if off < s.mem_size - 7 {
455 var i: i64 = 0
456 while i < 8 {
457 s.mem_buf[off + i] = ((value >> (i * 8)) & 0xff) as u8
458 i = i + 1
459 }
460 return NX_HDL_OK
461 }
462 }
463 if addr >= NX_CLINT_BASE { if addr < NX_CLINT_END {
464 return nx_clint_write64(s.clint, addr, value)
465 }}
466 // UART path (8-bit writes only at THR; 64-bit writes get low byte)
467 if addr >= NX_UART_BASE { if addr < NX_UART_END {
468 return nx_uart_write8(s.uart, addr, value & 0xff)
469 }}
470 return 0 - NX_HDL_BAD_KIND
471}
472
473func nx_rv64im_sim_store8(s: *NxRv64imSim, addr: i64, value: i64) -> i64 {
474 if addr >= s.mem_base {
475 let off: i64 = addr - s.mem_base
476 if off < s.mem_size {
477 s.mem_buf[off] = (value & 0xff) as u8
478 return NX_HDL_OK
479 }
480 }
481 if addr >= NX_UART_BASE { if addr < NX_UART_END {
482 return nx_uart_write8(s.uart, addr, value & 0xff)
483 }}
484 return 0 - NX_HDL_BAD_KIND
485}
486
487func nx_rv64im_sim_store16(s: *NxRv64imSim, addr: i64, value: i64) -> i64 {
488 if addr >= s.mem_base {
489 let off: i64 = addr - s.mem_base
490 if off < s.mem_size - 1 {
491 s.mem_buf[off] = (value & 0xff) as u8
492 s.mem_buf[off + 1] = ((value >> 8) & 0xff) as u8
493 return NX_HDL_OK
494 }
495 }
496 return 0 - NX_HDL_BAD_KIND
497}
498
499func nx_rv64im_sim_store32(s: *NxRv64imSim, addr: i64, value: i64) -> i64 {
500 if addr >= s.mem_base {
501 let off: i64 = addr - s.mem_base
502 if off < s.mem_size - 3 {
503 s.mem_buf[off] = (value & 0xff) as u8
504 s.mem_buf[off + 1] = ((value >> 8) & 0xff) as u8
505 s.mem_buf[off + 2] = ((value >> 16) & 0xff) as u8
506 s.mem_buf[off + 3] = ((value >> 24) & 0xff) as u8
507 return NX_HDL_OK
508 }
509 }
510 // virtio-MMIO transport path (K-R2-001a): sw into the device register window
511 // latches GuestFeatures / drives the Status handshake (ACK/DRIVER/FEATURES_OK/
512 // DRIVER_OK) or resets the device (Status write of 0). On a QueueNotify kick
513 // (K-R2-001b2a) the device DMA-reads descriptor 0's first field from the QueuePFN-
514 // bound ring page in guest RAM (the sim owns mem_buf, so we hand it in) and latches
515 // it into QueueDescPeek -- proving the device walked the descriptor the driver laid.
516 if addr >= NX_VIRTIO_BASE { if addr < NX_VIRTIO_END {
517 if (s.virtio as i64) != 0 {
518 let wr: i64 = nx_virtio_write32(s.virtio, addr, value)
519 if addr == (NX_VIRTIO_BASE + NX_VIRTIO_OFF_QUEUENOTIFY) {
520 nx_virtio_notify_dma(s.virtio, s.mem_buf, s.mem_base, s.mem_size)
521 }
522 return wr
523 }
524 }}
525 // virtio-net device register window (K-R2-001c1/c2/c3): sw into the second transport block
526 // at 0x10002000 latches GuestFeatures / drives the Status handshake (c1) + the legacy
527 // virtqueue-config registers (c2). On a QueueNotify kick (K-R2-001c3) the net device DMA-
528 // walks the driver's tx ring EXACTLY like the blk branch above: nx_virtio_notify_dma is
529 // device-instance-generic (it walks v.storage[QPFN] -> desc-read + avail/used + the data
530 // round-trip), so handing it s.virtio_net makes the net FRAME word the driver laid in the
531 // tx descriptor's data buffer round-trip into QueueSectPeek with NO new DMA code. The blk
532 // device at 0x10001000 is untouched -- this is the net block's own QueueNotify trigger.
533 if addr >= NX_VIRTIO_NET_BASE { if addr < NX_VIRTIO_NET_END {
534 if (s.virtio_net as i64) != 0 {
535 let wrn: i64 = nx_virtio_write32(s.virtio_net, addr, value)
536 if addr == (NX_VIRTIO_NET_BASE + NX_VIRTIO_OFF_QUEUENOTIFY) {
537 nx_virtio_notify_dma(s.virtio_net, s.mem_buf, s.mem_base, s.mem_size)
538 }
539 return wrn
540 }
541 }}
542 // NVMe controller register window (X-DRV-W1 stage B): sw into 0x10003000.. latches CC (and
543 // sets/clears the CSTS.RDY flop on CC.EN), AQA, the ASQ/ACQ base-address registers, and the
544 // doorbells. On the SQ-Tail doorbell (off_sq0tdbl) the device runs the doorbell DMA: it
545 // fetches the 64-byte SQE from ASQ_base (reads opcode+CID), posts a phase-tagged 16-byte CQE
546 // into ACQ_base, and latches NvmeCqPeek = (CID<<16)|status|phase. The sim owns mem_buf, so
547 // it hands guest RAM in -- mirroring the virtio QueueNotify branch. The virtio devices are
548 // untouched -- this is the NVMe instance's own doorbell trigger.
549 if addr >= NX_NVME_BASE { if addr < NX_NVME_END {
550 if (s.nvme as i64) != 0 {
551 let wrm: i64 = nx_nvme_write32(s.nvme, addr, value)
552 if addr == (NX_NVME_BASE + NX_NVME_OFF_SQ0TDBL) {
553 nx_nvme_doorbell_dma(s.nvme, s.mem_buf, s.mem_base, s.mem_size)
554 }
555 return wrm
556 }
557 }}
558 // Nishi-native device window (sovereign-device-protocol): sw into 0x10004000.. latches ENABLE
559 // (single-step bring-up) / CMD_ADDR. On the DOORBELL write the device runs the inline command
560 // at CMD_ADDR (DMA-reads the descriptor + the data buffer in guest RAM, the sim owns mem_buf so
561 // we hand it in) and latches STATUS + RESULT inline -- ONE descriptor, ONE round-trip, NO rings.
562 // virtio + nvme devices are untouched -- this is the NNDP instance's own doorbell trigger.
563 if addr >= NX_NNDEV_BASE { if addr < NX_NNDEV_END {
564 if (s.nndev as i64) != 0 {
565 let wrd: i64 = nx_nndev_write32(s.nndev, addr, value)
566 if addr == (NX_NNDEV_BASE + NX_NNDEV_OFF_DOORBELL) {
567 nx_nndev_doorbell_dma(s.nndev, s.mem_buf, s.mem_base, s.mem_size)
568 }
569 return wrd
570 }
571 }}
572 // Sv39 MMU walk device window (virtual-memory-paging-mmu): sw into 0x10006000.. latches satp /
573 // vaddr. On the DOORBELL write the device WALKS the 3-level Sv39 page table in guest RAM (the
574 // sim owns mem_buf, so we hand it in) and latches PADDR + FAULT. Other devices untouched.
575 if addr >= NX_MMU_BASE { if addr < NX_MMU_END {
576 if (s.mmu as i64) != 0 {
577 let wru: i64 = nx_mmu_write32(s.mmu, addr, value)
578 if addr == (NX_MMU_BASE + NX_MMU_OFF_DOORBELL) {
579 nx_mmu_doorbell_walk(s.mmu, s.mem_buf, s.mem_base, s.mem_size)
580 }
581 return wru
582 }
583 }}
584 // GPU-class controller window (driver-from-spec D): sw into 0x10007000.. latches CTRL (sets/clears
585 // STATUS.READY on CTRL.EN) + the RING base. On the DOORBELL write the device runs the dispatch DMA:
586 // it fetches the command packet from RING base, validates the SPIR-V magic, signals the FENCE + latches
587 // RESULTPEEK. The sim owns mem_buf, so it hands guest RAM in. Other devices untouched.
588 if addr >= NX_GPU_BASE { if addr < NX_GPU_END {
589 if (s.gpu as i64) != 0 {
590 let wrg: i64 = nx_gpu_write32(s.gpu, addr, value)
591 if addr == (NX_GPU_BASE + NX_GPU_OFF_DOORBELL) {
592 nx_gpu_doorbell_dma(s.gpu, s.mem_buf, s.mem_base, s.mem_size)
593 }
594 return wrg
595 }
596 }}
597 // SiFive test/finisher at 0x100000: 32-bit write of 0x5555 = clean
598 // poweroff per kernel tasks.nx:203-204.
599 if addr == 0x100000 {
600 if (value & 0xffff) == 0x5555 {
601 s.halted = 1
602 s.halt_code = 0 // clean exit
603 return NX_HDL_OK
604 }
605 }
606 return 0 - NX_HDL_BAD_KIND
607}
608
609// ===== CPU-datapath Sv39 translation (virtual-memory-paging-mmu, integration rung) ============
610//
611// When satp.MODE == Sv39 (8), DATA loads/stores translate their address through the page table in
612// guest RAM (the SAME nx_sv39_walk the MMU walk device uses). satp.MODE == 0 (Bare, the reset +
613// every existing test) -> identity, so there is ZERO change to all prior gates. Device MMIO
614// windows stay physical (identity) even under Sv39 in this slice so a paging test can still drive
615// the UART/finisher. Instruction FETCH is NOT translated here (the image is physically addressed);
616// fetch-translation + the page-fault TRAP on a bad data walk are the documented follow-on rungs --
617// on a walk fault this returns the vaddr unchanged (a tampered mapping then reads out-of-range -> 0,
618// which the gate's verify catches).
619func nx_rv64im_is_device(addr: i64) -> i64 {
620 if addr >= 0x100000 { if addr < 0x100008 { return 1 } } // SiFive finisher
621 if addr >= 0x02000000 { if addr < 0x0200C000 { return 1 } } // CLINT
622 if addr >= 0x10000000 { if addr < 0x10000008 { return 1 } } // UART
623 if addr >= 0x10001000 { if addr < 0x10007000 { return 1 } } // virtio/nvme/nndev/mmu windows
624 return 0
625}
626const NX_ACC_FETCH: i64 = 0
627const NX_ACC_LOAD: i64 = 1
628const NX_ACC_STORE: i64 = 2
629// Translate `vaddr` for an access of type `access` (FETCH/LOAD/STORE) and ENFORCE the page's permission
630// bits (riscv-priv-spec): a fetch needs X (the NX-bit / W^X foundation -- a data page is not executable),
631// a load needs R, a store needs W. A valid leaf lacking the required bit signals xlate_fault, which the
632// step loop routes to the SAME page-fault cause it already uses per access (instr=12/load=13/store=15).
633// Identity in M-mode and under Bare, and device MMIO stays physical -> every physically-addressed path
634// is unchanged (perm enforcement applies only to S/U-mode Sv39 RAM accesses).
635func nx_rv64im_xlate(s: *NxRv64imSim, vaddr: i64, access: i64) -> i64 {
636 if s.priv == 3 { return vaddr } // M-mode: always Bare (riscv-priv-spec)
637 let satp: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_SATP)
638 if ((satp >> 60) & 0xf) != 8 { return vaddr } // Bare / non-Sv39: identity
639 if nx_rv64im_is_device(vaddr) == 1 { return vaddr } // device MMIO stays physical
640 let fb: *i64 = sys_mmap(16) as *i64
641 let pb: *i64 = sys_mmap(16) as *i64 // leaf PTE perm bits (V|R|W|X) from the walk
642 let pab: *i64 = sys_mmap(16) as *i64 // leaf PTE physical address (A/D write-back target)
643 let pa: i64 = nx_sv39_walk(satp, s.mem_buf, s.mem_base, s.mem_size, vaddr, fb, pb, pab)
644 if fb[0] == 1 { s.xlate_fault = 1; return vaddr } // walk fault: signal the step loop to trap
645 var need: i64 = 8 // FETCH needs X (bit 3)
646 if access == NX_ACC_LOAD { need = 2 } // LOAD needs R (bit 1)
647 if access == NX_ACC_STORE { need = 4 } // STORE needs W (bit 2)
648 if (pb[0] & need) == 0 { s.xlate_fault = 1; return vaddr } // R/W/X permission violation -> page fault
649 // USER/SUPERVISOR isolation (riscv-priv-spec, the U-bit = bit 4): a U-mode access REQUIRES a
650 // user page; an S-mode access to a USER page is denied unless mstatus.SUM (Supervisor User Memory).
651 let is_user_page: i64 = pb[0] & 0x10
652 if s.priv == 0 { if is_user_page == 0 { s.xlate_fault = 1; return vaddr } } // U-mode: page must be U=1
653 if s.priv == 1 { if is_user_page != 0 { // S-mode touching a U=1 page
654 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
655 if (mstatus & 0x40000) == 0 { s.xlate_fault = 1; return vaddr } // SUM=0 -> deny (kernel can't deref user memory by default)
656 } }
657 // ACCESSED/DIRTY bits (hardware-managed, riscv-priv-spec Svadu): a SUCCESSFUL translation sets A on
658 // any access + D on a store, in the leaf PTE. Additive -- A/D (bits 6/7) don't affect the PA/perm
659 // decode, so every prior test is unchanged; they let the OS drive page replacement + dirty write-back.
660 let pte_off: i64 = pab[0] - s.mem_base
661 if nx_mmu_inrange(pte_off, 8, s.mem_size) == 1 {
662 let cur: i64 = nx_mmu_rd64(s.mem_buf, pte_off)
663 var npte: i64 = cur | 0x40 // A (bit 6)
664 if access == NX_ACC_STORE { npte = npte | 0x80 } // D (bit 7) on a store
665 if npte != cur { nx_mmu_wr64(s.mem_buf, pte_off, npte) }
666 }
667 return pa
668}
669
670// ===== Map decoded {opcode, funct3, funct7} -> ALU op =================================================
671//
672// The decoder gave us NX_RV64IM_OP_OP / OP_IMM / OP_32 / etc.; we
673// need the finer-grained ALU op for the compute call.
674
675func nx_rv64im_sim_alu_select(op_kind: i64, funct3: i64, funct7: i64) -> i64 {
676 if op_kind == NX_RV64IM_OP_OP {
677 if funct7 == 0x01 {
678 // M extension
679 if funct3 == 0 { return NX_RV64IM_ALU_MUL }
680 if funct3 == 1 { return NX_RV64IM_ALU_MULH }
681 if funct3 == 2 { return NX_RV64IM_ALU_MULHSU }
682 if funct3 == 3 { return NX_RV64IM_ALU_MULHU }
683 if funct3 == 4 { return NX_RV64IM_ALU_DIV }
684 if funct3 == 5 { return NX_RV64IM_ALU_DIVU }
685 if funct3 == 6 { return NX_RV64IM_ALU_REM }
686 if funct3 == 7 { return NX_RV64IM_ALU_REMU }
687 }
688 if funct3 == 0 {
689 if funct7 == 0x20 { return NX_RV64IM_ALU_SUB }
690 return NX_RV64IM_ALU_ADD
691 }
692 if funct3 == 1 { return NX_RV64IM_ALU_SLL }
693 if funct3 == 2 { return NX_RV64IM_ALU_SLT }
694 if funct3 == 3 { return NX_RV64IM_ALU_SLTU }
695 if funct3 == 4 { return NX_RV64IM_ALU_XOR }
696 if funct3 == 5 {
697 if funct7 == 0x20 { return NX_RV64IM_ALU_SRA }
698 return NX_RV64IM_ALU_SRL
699 }
700 if funct3 == 6 { return NX_RV64IM_ALU_OR }
701 if funct3 == 7 { return NX_RV64IM_ALU_AND }
702 }
703 if op_kind == NX_RV64IM_OP_OP_IMM {
704 // Same as OP except immediates; ALU is the same compute layer
705 // (the executor swaps operand b for the immediate value).
706 if funct3 == 0 { return NX_RV64IM_ALU_ADD }
707 if funct3 == 1 { return NX_RV64IM_ALU_SLL }
708 if funct3 == 2 { return NX_RV64IM_ALU_SLT }
709 if funct3 == 3 { return NX_RV64IM_ALU_SLTU }
710 if funct3 == 4 { return NX_RV64IM_ALU_XOR }
711 if funct3 == 5 {
712 // SRLI vs SRAI distinguished by inst[30] (funct7 bit 5). FIXED 2026-08-23 (nishios NO3,
713 // riscv-arch-test 2.7.4 srai-01 + c.srai-01 FAILED while register sra-01 PASSED): on RV64
714 // the I-type shamt is SIX bits, so shamt[5] lives in inst[25] = funct7 bit 0. A shift by
715 // 32..63 therefore carries funct7 = 0x21, the old `funct7 == 0x20` test missed it and the
716 // instruction fell through to the LOGICAL shift -- sign bits silently dropped. Test the
717 // marker bit alone; the register form (funct7 exactly 0x20/0x00) is unchanged by this.
718 if (funct7 & 0x20) == 0x20 { return NX_RV64IM_ALU_SRA }
719 return NX_RV64IM_ALU_SRL
720 }
721 if funct3 == 6 { return NX_RV64IM_ALU_OR }
722 if funct3 == 7 { return NX_RV64IM_ALU_AND }
723 }
724 if op_kind == NX_RV64IM_OP_OP_32 {
725 if funct7 == 0x01 {
726 if funct3 == 0 { return NX_RV64IM_ALU_MULW }
727 if funct3 == 4 { return NX_RV64IM_ALU_DIVW }
728 if funct3 == 5 { return NX_RV64IM_ALU_DIVUW }
729 if funct3 == 6 { return NX_RV64IM_ALU_REMW }
730 if funct3 == 7 { return NX_RV64IM_ALU_REMUW }
731 }
732 if funct3 == 0 {
733 if funct7 == 0x20 { return NX_RV64IM_ALU_SUBW }
734 return NX_RV64IM_ALU_ADDW
735 }
736 if funct3 == 1 { return NX_RV64IM_ALU_SLLW }
737 if funct3 == 5 {
738 if funct7 == 0x20 { return NX_RV64IM_ALU_SRAW }
739 return NX_RV64IM_ALU_SRLW
740 }
741 }
742 if op_kind == NX_RV64IM_OP_OP_IMM_32 {
743 if funct3 == 0 { return NX_RV64IM_ALU_ADDW }
744 if funct3 == 1 { return NX_RV64IM_ALU_SLLW }
745 if funct3 == 5 {
746 if funct7 == 0x20 { return NX_RV64IM_ALU_SRAW }
747 return NX_RV64IM_ALU_SRLW
748 }
749 }
750 return NX_RV64IM_ALU_INVALID
751}
752
753// ===== Trap entry =================================================
754//
755// Common path for all exceptions + interrupts. Saves mepc, sets
756// mcause, transitions mstatus per riscv-privileged-spec §3.1.6.2:
757// mstatus.MPIE <- mstatus.MIE (save the interrupt-enable bit)
758// mstatus.MIE <- 0 (disable interrupts in handler)
759// mstatus.MPP <- current privilege (M-mode in Tier A)
760// mepc <- pc-of-faulting-instruction
761// pc <- mtvec (direct mode; vectored mode unused)
762
763const NX_MCAUSE_INT_MACHINE_TIMER: i64 = 0 - 9223372036854775801 // (1<<63)|7
764const NX_MCAUSE_EXC_ECALL_U: i64 = 8 // environment call from U-mode
765const NX_MCAUSE_EXC_ECALL_S: i64 = 9 // environment call from S-mode
766const NX_MCAUSE_EXC_ECALL_M: i64 = 11
767const NX_MCAUSE_EXC_ILLEGAL_INSTR: i64 = 2
768const NX_MCAUSE_EXC_INSTR_PAGE_FAULT: i64 = 12 // Sv39 instruction-FETCH walk fault
769const NX_MCAUSE_EXC_LOAD_PAGE_FAULT: i64 = 13 // Sv39 data-load walk fault
770const NX_MCAUSE_EXC_STORE_PAGE_FAULT: i64 = 15 // Sv39 data-store walk fault
771
772const NX_MSTATUS_MIE_BIT: i64 = 0x08 // bit 3
773const NX_MSTATUS_MPIE_BIT: i64 = 0x80 // bit 7
774const NX_MSTATUS_MPP_MASK: i64 = 0x1800 // bits 11..12
775const NX_MIE_MTIE_BIT: i64 = 0x80 // bit 7
776
777func nx_rv64im_sim_take_trap(s: *NxRv64imSim, cause: i64, mepc_val: i64) -> i64 {
778 // S-MODE DELEGATION (the real OS trap path): an EXCEPTION (cause>=0, not an interrupt) taken from
779 // S/U-mode (priv<3) whose medeleg bit is set traps to S-mode -- stvec/sepc/scause/sstatus.SPP -- so
780 // the kernel handles its own page-faults/syscalls in S-mode and sret's back. medeleg resets to 0
781 // (mmap-zeroed) so with no delegation every trap still goes to M-mode exactly as before (no regression).
782 if cause >= 0 { if s.priv < 3 {
783 let medeleg: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MEDELEG)
784 if ((medeleg >> cause) & 1) == 1 {
785 let sst: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
786 var nsst: i64 = sst & (0 - 1 ^ 0x100) // clear SPP (mstatus bit 8)
787 nsst = nsst | ((s.priv & 1) << 8) // SPP <- the privilege we trap FROM (S=1 / U=0)
788 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, nsst)
789 nx_rv64im_csr_write(s.csr, NX_CSR_SEPC, mepc_val)
790 nx_rv64im_csr_write(s.csr, NX_CSR_SCAUSE, cause)
791 s.priv = 1 // enter S-mode (the kernel)
792 let stvec: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_STVEC)
793 s.pc = stvec & (0 - 4)
794 return NX_HDL_OK
795 }
796 } }
797 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
798 var new_mstatus: i64 = mstatus
799 // Save MIE into MPIE.
800 if (mstatus & NX_MSTATUS_MIE_BIT) != 0 {
801 new_mstatus = new_mstatus | NX_MSTATUS_MPIE_BIT
802 }
803 if (mstatus & NX_MSTATUS_MIE_BIT) == 0 {
804 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MPIE_BIT)
805 }
806 // Disable interrupts.
807 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MIE_BIT)
808 // MPP <- the privilege we are trapping FROM (so mret can restore it); then enter M-mode.
809 new_mstatus = (new_mstatus & (0 - 1 ^ NX_MSTATUS_MPP_MASK)) | ((s.priv & 0x3) << 11)
810 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, new_mstatus)
811 nx_rv64im_csr_write(s.csr, NX_CSR_MEPC, mepc_val)
812 nx_rv64im_csr_write(s.csr, NX_CSR_MCAUSE, cause)
813 s.priv = 3 // the handler runs in M-mode
814 // Jump to handler. Direct mode: low 2 bits of mtvec are 00.
815 let mtvec: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MTVEC)
816 s.pc = mtvec & (0 - 4) // clear low 2 bits
817 return NX_HDL_OK
818}
819
820func nx_rv64im_sim_mret(s: *NxRv64imSim) -> i64 {
821 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
822 var new_mstatus: i64 = mstatus
823 // MIE <- MPIE
824 if (mstatus & NX_MSTATUS_MPIE_BIT) != 0 {
825 new_mstatus = new_mstatus | NX_MSTATUS_MIE_BIT
826 }
827 if (mstatus & NX_MSTATUS_MPIE_BIT) == 0 {
828 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MIE_BIT)
829 }
830 // MPIE <- 1
831 new_mstatus = new_mstatus | NX_MSTATUS_MPIE_BIT
832 // privilege <- MPP (the saved mode); then MPP <- U (0), per riscv-priv-spec mret.
833 let mpp: i64 = (mstatus >> 11) & 0x3
834 new_mstatus = new_mstatus & (0 - 1 ^ NX_MSTATUS_MPP_MASK)
835 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, new_mstatus)
836 s.priv = mpp
837 s.pc = nx_rv64im_csr_read(s.csr, NX_CSR_MEPC)
838 return NX_HDL_OK
839}
840
841// S-mode trap return -- the S-mode twin of mret (riscv-priv-spec): privilege <- sstatus.SPP (mstatus
842// bit 8: 1=S / 0=U); pc <- sepc; then SPP <- U. Lets an S-mode kernel return to the trapped context.
843func nx_rv64im_sim_sret(s: *NxRv64imSim) -> i64 {
844 let sst: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
845 let spp: i64 = (sst >> 8) & 1
846 let nsst: i64 = sst & (0 - 1 ^ 0x100) // SPP <- U(0) after sret
847 nx_rv64im_csr_write(s.csr, NX_CSR_MSTATUS, nsst)
848 s.priv = spp
849 s.pc = nx_rv64im_csr_read(s.csr, NX_CSR_SEPC)
850 return NX_HDL_OK
851}
852
853// ===== CSR address extraction =================================================
854//
855// SYSTEM-op CSR address is inst[31:20] treated as UNSIGNED 12-bit.
856// Note this is the same bit slice as imm_i but NOT sign-extended.
857
858func nx_rv64im_csr_addr(inst: i64) -> i64 {
859 return (inst >> 20) & 0xfff
860}
861
862// ===== Single-cycle step =================================================
863//
864// Executes ONE instruction at PC, advances PC + steps counter, ticks
865// CLINT once. Returns NX_HDL_OK on success, 0 - <verdict> on halt.
866
867func nx_rv64im_sim_step(s: *NxRv64imSim) -> i64 {
868 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
869 if s.halted == 1 { return 0 - NX_HDL_BAD_KIND }
870
871 // Tick CLINT first so MTIME advances per cycle (kernel polls it).
872 nx_clint_tick(s.clint)
873 nx_rv64im_csr_tick_mcycle(s.csr)
874
875 // Per-PC cycle attribution (M2 silicon-feedback foundation). Bumps
876 // the slot indexed by the executing PC; single branch when disabled.
877 if s.pc_cycles_on == 1 {
878 if s.pc >= s.mem_base {
879 let _attr_off: i64 = (s.pc - s.mem_base) >> 2
880 if _attr_off < s.pc_cycles_cap {
881 s.pc_cycles_buf[_attr_off] = s.pc_cycles_buf[_attr_off] + 1
882 }
883 }
884 }
885
886 // Interrupt check. CLINT asserts MTIP via nx_clint_tick; CPU
887 // takes the timer trap if mstatus.MIE && mie.MTIE. This is the
888 // path that fires kernel preemption every TICK_CYCLES.
889 if nx_clint_mtip_get(s.clint) == 1 {
890 let mstatus: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MSTATUS)
891 let mie: i64 = nx_rv64im_csr_read(s.csr, NX_CSR_MIE)
892 if (mstatus & NX_MSTATUS_MIE_BIT) != 0 {
893 if (mie & NX_MIE_MTIE_BIT) != 0 {
894 nx_rv64im_sim_take_trap(s, NX_MCAUSE_INT_MACHINE_TIMER, s.pc)
895 s.steps = s.steps + 1
896 return NX_HDL_OK
897 }
898 }
899 }
900
901 // Fetch -- the PC is a VIRTUAL address in S/U-mode under Sv39, so the instruction fetch translates
902 // through the page table just like a data access (nx_rv64im_xlate returns identity in M-mode and
903 // under Bare, so every physically-addressed test -- M-mode boot, handlers, Bare programs -- is
904 // unchanged). A walk fault on the code page raises an instruction-page-fault (mcause=12) and
905 // aborts the fetch, vectoring to mtvec (where the M-mode handler is fetched physically).
906 s.xlate_fault = 0
907 let fetch_pa: i64 = nx_rv64im_xlate(s, s.pc, NX_ACC_FETCH) // fetch enforces the X bit
908 if s.xlate_fault == 1 { // code-page walk fault -> instruction-page-fault trap
909 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, s.pc)
910 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_INSTR_PAGE_FAULT, s.pc)
911 s.steps = s.steps + 1
912 return NX_HDL_OK
913 }
914 // C extension: read a halfword; if compressed (low 2 bits != 11) expand it to the 32-bit equivalent + advance pc by
915 // 2, else read the full 32-bit word + advance by 4. TRANSPARENT to 32-bit code (low bits 11 -> not compressed).
916 // Fetch the code word ONCE and reuse it. Was two load32 calls per 32-bit instruction (the low
917 // 16 bits drive the compressed-check; the full 32 bits are the instruction) -- a redundant
918 // memory read every step. JIT-ladder rung 0: eliminate it. Behaviour-identical.
919 let fetch_word: i64 = nx_rv64im_sim_load32(s, fetch_pa) & 0xffffffff
920 let half: i64 = fetch_word & 0xffff
921 var inst: i64 = 0
922 var instr_len: i64 = 4
923 if nx_rvc_is_compressed(half) == 1 { inst = nx_rvc_expand(half); instr_len = 2 }
924 else { inst = fetch_word }
925 if inst == 0 {
926 // All-zero instruction: treat as halt sentinel. Kernel never
927 // emits these; real RV64 would raise illegal-instr. V1 sim
928 // uses this as a clean test-end signal.
929 s.halted = 1
930 s.halt_code = nx_rv64im_rf_read(s.rf, 10) // exit code in a0
931 return 0
932 }
933
934 // Decode
935 let op_kind: i64 = nx_rv64im_decode_kind(inst)
936 let rd: i64 = nx_rv64im_rd(inst)
937 let rs1: i64 = nx_rv64im_rs1(inst)
938 let rs2: i64 = nx_rv64im_rs2(inst)
939 let funct3: i64 = nx_rv64im_funct3(inst)
940 let funct7: i64 = nx_rv64im_funct7(inst)
941
942 let rs1_val: i64 = nx_rv64im_rf_read(s.rf, rs1)
943 let rs2_val: i64 = nx_rv64im_rf_read(s.rf, rs2)
944
945 var next_pc: i64 = s.pc + instr_len // C ext: +2 for compressed, +4 for 32-bit
946
947 if op_kind == NX_RV64IM_OP_LUI {
948 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_imm_u(inst))
949 }
950 if op_kind == NX_RV64IM_OP_AUIPC {
951 nx_rv64im_rf_write(s.rf, rd, s.pc + nx_rv64im_imm_u(inst))
952 }
953 if op_kind == NX_RV64IM_OP_JAL {
954 nx_rv64im_rf_write(s.rf, rd, s.pc + instr_len)
955 next_pc = s.pc + nx_rv64im_imm_j(inst)
956 }
957 if op_kind == NX_RV64IM_OP_JALR {
958 let tgt: i64 = (rs1_val + nx_rv64im_imm_i(inst)) & (0 - 2)
959 nx_rv64im_rf_write(s.rf, rd, s.pc + instr_len)
960 next_pc = tgt
961 }
962 if op_kind == NX_RV64IM_OP_BRANCH {
963 var take: i64 = 0
964 if funct3 == 0 { if rs1_val == rs2_val { take = 1 } } // beq
965 if funct3 == 1 { if rs1_val != rs2_val { take = 1 } } // bne
966 if funct3 == 4 { if rs1_val < rs2_val { take = 1 } } // blt
967 if funct3 == 5 { if rs1_val >= rs2_val { take = 1 } } // bge
968 if funct3 == 6 { take = nx_rv64im_ltu(rs1_val, rs2_val) } // bltu
969 if funct3 == 7 {
970 let lt: i64 = nx_rv64im_ltu(rs1_val, rs2_val)
971 if lt == 0 { take = 1 } // bgeu
972 }
973 if take == 1 { next_pc = s.pc + nx_rv64im_imm_b(inst) }
974 }
975 if op_kind == NX_RV64IM_OP_LOAD {
976 let vaddr: i64 = rs1_val + nx_rv64im_imm_i(inst)
977 s.xlate_fault = 0
978 let addr: i64 = nx_rv64im_xlate(s, vaddr, NX_ACC_LOAD) // Sv39 data translation + R enforcement
979 if s.xlate_fault == 1 { // walk/perm fault -> load-page-fault trap (instr aborted)
980 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, vaddr)
981 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_LOAD_PAGE_FAULT, s.pc)
982 s.steps = s.steps + 1
983 return NX_HDL_OK
984 }
985 if funct3 == 0 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext8(nx_rv64im_sim_load8(s, addr))) } // lb
986 if funct3 == 1 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext16(nx_rv64im_sim_load16(s, addr))) } // lh
987 if funct3 == 2 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sext32(nx_rv64im_sim_load32(s, addr))) } // lw
988 if funct3 == 3 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load64(s, addr)) } // ld
989 if funct3 == 4 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load8(s, addr) & 0xff) } // lbu
990 if funct3 == 5 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load16(s, addr) & 0xffff) } // lhu
991 if funct3 == 6 { nx_rv64im_rf_write(s.rf, rd, nx_rv64im_sim_load32(s, addr) & 0xffffffff) } // lwu
992 }
993 if op_kind == NX_RV64IM_OP_STORE {
994 let vaddr: i64 = rs1_val + nx_rv64im_imm_s(inst)
995 s.xlate_fault = 0
996 let addr: i64 = nx_rv64im_xlate(s, vaddr, NX_ACC_STORE) // Sv39 data translation + W enforcement
997 if s.xlate_fault == 1 { // walk/perm fault -> store-page-fault trap (instr aborted)
998 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, vaddr)
999 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_STORE_PAGE_FAULT, s.pc)
1000 s.steps = s.steps + 1
1001 return NX_HDL_OK
1002 }
1003 if funct3 == 0 { nx_rv64im_sim_store8(s, addr, rs2_val) } // sb (UART THR + general)
1004 if funct3 == 1 { nx_rv64im_sim_store16(s, addr, rs2_val) } // sh
1005 if funct3 == 2 { nx_rv64im_sim_store32(s, addr, rs2_val) } // sw (+ finisher 0x100000)
1006 if funct3 == 3 { nx_rv64im_sim_store64(s, addr, rs2_val) } // sd
1007 }
1008 if op_kind == NX_RV64IM_OP_OP {
1009 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7)
1010 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1011 }
1012 if op_kind == NX_RV64IM_OP_OP_IMM {
1013 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7)
1014 let imm: i64 = nx_rv64im_imm_i(inst)
1015 // SLLI/SRLI/SRAI shamt is encoded in the immediate's low 6 bits.
1016 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, imm))
1017 }
1018 if op_kind == NX_RV64IM_OP_OP_32 {
1019 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7)
1020 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1021 }
1022 // M-extension R-type ops (mul/div/rem family). The decoder gives
1023 // them their own op_kind (OP_M_MUL / OP_M_DIV / *_32); alu_select
1024 // keys on the base OP / OP_32 kind + funct7=0x01, so route through
1025 // those. Without these four cases the sim silently skipped every
1026 // multiply/divide/remainder and left rd unwritten.
1027 if op_kind == NX_RV64IM_OP_M_MUL {
1028 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7)
1029 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1030 }
1031 if op_kind == NX_RV64IM_OP_M_DIV {
1032 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP, funct3, funct7)
1033 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1034 }
1035 if op_kind == NX_RV64IM_OP_M_MUL_32 {
1036 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7)
1037 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1038 }
1039 if op_kind == NX_RV64IM_OP_M_DIV_32 {
1040 let alu_op: i64 = nx_rv64im_sim_alu_select(NX_RV64IM_OP_OP_32, funct3, funct7)
1041 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, rs2_val))
1042 }
1043 if op_kind == NX_RV64IM_OP_OP_IMM_32 {
1044 let alu_op: i64 = nx_rv64im_sim_alu_select(op_kind, funct3, funct7)
1045 nx_rv64im_rf_write(s.rf, rd, nx_rv64im_alu_compute(alu_op, rs1_val, nx_rv64im_imm_i(inst)))
1046 }
1047 if op_kind == NX_RV64IM_OP_FENCE {
1048 // No-op in single-cycle in-order sim (no memory reordering).
1049 }
1050 if op_kind == NX_RV64IM_OP_SYSTEM {
1051 let csr_addr: i64 = nx_rv64im_csr_addr(inst)
1052
1053 if funct3 == 0 {
1054 // ecall / ebreak / mret -- distinguished by inst[31:20].
1055 // Per spec table 3.4 / 9.1:
1056 // ecall -> imm[11:0] = 000000000000
1057 // ebreak -> imm[11:0] = 000000000001
1058 // mret -> imm[11:0] = 001100000010 (0x302)
1059 if csr_addr == 0x000 {
1060 // ecall -- the cause depends on the originating privilege (8=U, 9=S, 11=M); the
1061 // handler at mtvec is the kernel's syscall dispatcher.
1062 var ecause: i64 = NX_MCAUSE_EXC_ECALL_M
1063 if s.priv == 0 { ecause = NX_MCAUSE_EXC_ECALL_U }
1064 if s.priv == 1 { ecause = NX_MCAUSE_EXC_ECALL_S }
1065 nx_rv64im_sim_take_trap(s, ecause, s.pc)
1066 s.steps = s.steps + 1
1067 return NX_HDL_OK
1068 }
1069 if csr_addr == 0x001 {
1070 // ebreak -- treat as halt for the V1 sim (kernel
1071 // never executes ebreak in normal flow).
1072 s.halted = 1
1073 s.halt_code = nx_rv64im_rf_read(s.rf, 10)
1074 return 0
1075 }
1076 if csr_addr == 0x302 {
1077 // mret -- pc <- mepc, restore MIE from MPIE.
1078 nx_rv64im_sim_mret(s)
1079 s.steps = s.steps + 1
1080 return NX_HDL_OK
1081 }
1082 if csr_addr == 0x102 {
1083 // sret -- S-mode trap return: pc <- sepc, privilege <- sstatus.SPP.
1084 nx_rv64im_sim_sret(s)
1085 s.steps = s.steps + 1
1086 return NX_HDL_OK
1087 }
1088 // Unrecognised SYSTEM/funct3=0 op -- illegal.
1089 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_ILLEGAL_INSTR, s.pc)
1090 s.steps = s.steps + 1
1091 return NX_HDL_OK
1092 }
1093
1094 // csrr* family. Build the "operand" -- register value for
1095 // funct3 = 1/2/3; zero-extended 5-bit immediate for 5/6/7.
1096 var operand: i64 = 0
1097 if funct3 == 1 { operand = rs1_val }
1098 if funct3 == 2 { operand = rs1_val }
1099 if funct3 == 3 { operand = rs1_val }
1100 if funct3 == 5 { operand = rs1 & 0x1f } // csrrwi uimm = rs1 field
1101 if funct3 == 6 { operand = rs1 & 0x1f }
1102 if funct3 == 7 { operand = rs1 & 0x1f }
1103
1104 let old_out: *i64 = (sys_mmap(8)) as *i64
1105 old_out[0] = 0
1106 let cur: i64 = nx_rv64im_csr_read(s.csr, csr_addr)
1107
1108 if funct3 == 1 { // csrrw
1109 nx_rv64im_csr_swap(s.csr, csr_addr, operand, old_out)
1110 nx_rv64im_rf_write(s.rf, rd, old_out[0])
1111 }
1112 if funct3 == 5 { // csrrwi
1113 nx_rv64im_csr_swap(s.csr, csr_addr, operand, old_out)
1114 nx_rv64im_rf_write(s.rf, rd, old_out[0])
1115 }
1116 if funct3 == 2 { // csrrs -- set bits indicated by operand
1117 nx_rv64im_csr_write(s.csr, csr_addr, cur | operand)
1118 nx_rv64im_rf_write(s.rf, rd, cur)
1119 }
1120 if funct3 == 6 { // csrrsi
1121 nx_rv64im_csr_write(s.csr, csr_addr, cur | operand)
1122 nx_rv64im_rf_write(s.rf, rd, cur)
1123 }
1124 if funct3 == 3 { // csrrc -- clear bits indicated by operand
1125 nx_rv64im_csr_write(s.csr, csr_addr, cur & (0 - 1 ^ operand))
1126 nx_rv64im_rf_write(s.rf, rd, cur)
1127 }
1128 if funct3 == 7 { // csrrci
1129 nx_rv64im_csr_write(s.csr, csr_addr, cur & (0 - 1 ^ operand))
1130 nx_rv64im_rf_write(s.rf, rd, cur)
1131 }
1132 }
1133 if op_kind == NX_RV64IM_OP_AMO {
1134 // RV64A atomics: lr/sc + amo* (F107h). funct5 = inst[31:27] selects the op; funct3 = 2 (.w,
1135 // 32-bit) or 3 (.d, 64-bit). The address is rs1 EXACTLY (no immediate offset, per spec).
1136 let funct5: i64 = (inst >> 27) & 0x1f
1137 var is_d: i64 = 0
1138 if funct3 == 3 { is_d = 1 }
1139 let amo_va: i64 = rs1_val
1140 s.xlate_fault = 0
1141 let amo_pa: i64 = nx_rv64im_xlate(s, amo_va, NX_ACC_STORE) // amo needs R+W; enforce W (identity in M/Bare)
1142 if s.xlate_fault == 1 {
1143 nx_rv64im_csr_write(s.csr, NX_CSR_MTVAL, amo_va)
1144 nx_rv64im_sim_take_trap(s, NX_MCAUSE_EXC_STORE_PAGE_FAULT, s.pc)
1145 s.steps = s.steps + 1
1146 return NX_HDL_OK
1147 }
1148 // old value: sign-extended 32-bit for .w (so rd + signed compares are correct); full 64 for .d.
1149 var amo_old: i64 = 0
1150 if is_d == 1 { amo_old = nx_rv64im_sim_load64(s, amo_pa) }
1151 if is_d == 0 { amo_old = nx_rv64im_sext32(nx_rv64im_sim_load32(s, amo_pa)) }
1152
1153 if funct5 == 0x02 {
1154 // lr.w/d: record the reservation; rd <- old.
1155 s.resv_addr = amo_pa
1156 s.resv_valid = 1
1157 nx_rv64im_rf_write(s.rf, rd, amo_old)
1158 }
1159 if funct5 == 0x03 {
1160 // sc.w/d: success iff the reservation is still valid AND matches this address.
1161 var sc_ok: i64 = 0
1162 if s.resv_valid == 1 { if s.resv_addr == amo_pa { sc_ok = 1 } }
1163 if sc_ok == 1 {
1164 if is_d == 1 { nx_rv64im_sim_store64(s, amo_pa, rs2_val) }
1165 if is_d == 0 { nx_rv64im_sim_store32(s, amo_pa, rs2_val & 0xffffffff) }
1166 nx_rv64im_rf_write(s.rf, rd, 0) // 0 = success
1167 }
1168 if sc_ok == 0 { nx_rv64im_rf_write(s.rf, rd, 1) } // 1 = failure
1169 s.resv_valid = 0 // sc always clears the reservation
1170 }
1171 // amo* arithmetic/logical/min-max family (every funct5 except lr=0x02 / sc=0x03).
1172 if funct5 != 0x02 { if funct5 != 0x03 {
1173 // second operand: full 64 for .d; sign-extended low-32 for .w.
1174 var amo_src: i64 = rs2_val
1175 if is_d == 0 { amo_src = nx_rv64im_sext32(rs2_val & 0xffffffff) }
1176 var amo_new: i64 = amo_old
1177 if funct5 == 0x01 { amo_new = amo_src } // amoswap
1178 if funct5 == 0x00 { amo_new = amo_old + amo_src } // amoadd
1179 if funct5 == 0x04 { amo_new = amo_old ^ amo_src } // amoxor
1180 if funct5 == 0x0c { amo_new = amo_old & amo_src } // amoand
1181 if funct5 == 0x08 { amo_new = amo_old | amo_src } // amoor
1182 if funct5 == 0x10 { if amo_old < amo_src { amo_new = amo_old } if amo_old >= amo_src { amo_new = amo_src } } // amomin (signed)
1183 if funct5 == 0x14 { if amo_old < amo_src { amo_new = amo_src } if amo_old >= amo_src { amo_new = amo_old } } // amomax (signed)
1184 if funct5 == 0x18 { // amominu (unsigned)
1185 var uo: i64 = amo_old
1186 var us: i64 = amo_src
1187 if is_d == 0 { uo = amo_old & 0xffffffff; us = amo_src & 0xffffffff }
1188 if nx_rv64im_ltu(uo, us) == 1 { amo_new = amo_old }
1189 if nx_rv64im_ltu(uo, us) == 0 { amo_new = amo_src }
1190 }
1191 if funct5 == 0x1c { // amomaxu (unsigned)
1192 var uo2: i64 = amo_old
1193 var us2: i64 = amo_src
1194 if is_d == 0 { uo2 = amo_old & 0xffffffff; us2 = amo_src & 0xffffffff }
1195 if nx_rv64im_ltu(uo2, us2) == 1 { amo_new = amo_src }
1196 if nx_rv64im_ltu(uo2, us2) == 0 { amo_new = amo_old }
1197 }
1198 s.resv_valid = 0 // an amo writes the line -> breaks any reservation
1199 if is_d == 1 { nx_rv64im_sim_store64(s, amo_pa, amo_new) }
1200 if is_d == 0 { nx_rv64im_sim_store32(s, amo_pa, amo_new & 0xffffffff) }
1201 nx_rv64im_rf_write(s.rf, rd, amo_old)
1202 } }
1203 }
1204 if op_kind == NX_RV64IM_OP_INVALID {
1205 // Illegal instruction. Real kernel: trap with mcause=2.
1206 // V1 sim: halt loud.
1207 s.halted = 1
1208 s.halt_code = 0 - 2
1209 return 0 - NX_HDL_BAD_KIND
1210 }
1211
1212 s.pc = next_pc
1213 s.steps = s.steps + 1
1214 return NX_HDL_OK
1215}
1216
1217// ===== Run loop =================================================
1218//
1219// Steps until halted or max_steps reached (guard against infinite
1220// loops in misbehaving test programs). Returns the final exit code
1221// from the halt path.
1222
1223func nx_rv64im_sim_run(s: *NxRv64imSim, max_steps: i64) -> i64 {
1224 var i: i64 = 0
1225 while i < max_steps {
1226 if s.halted == 1 { return s.halt_code }
1227 nx_rv64im_sim_step(s)
1228 i = i + 1
1229 }
1230 return 0 - 1 // exceeded max_steps; not halted cleanly
1231}
1232
1233// ===== Deterministic snapshot / restore (F107i, 2026-07-19) =================================================
1234//
1235// Save + restore the ARCHITECTURAL machine state (32 GPRs + pc/priv/reservation/step-counters + all of RAM)
1236// to/from a caller buffer. Because the sovereign emulator is bit-DETERMINISTIC (no floating point, no host
1237// nondeterminism -- the determinism exceed bet), a restore is EXACT: continuing from a restored snapshot is
1238// byte-identical to never having stopped. This is what QEMU needs icount + savevm to approximate; here it
1239// is free. (CSR/CLINT/UART device state is the documented follow-on rung; this covers register+memory-
1240// defined behaviour -- the common case.) Buffer must be >= NX_RV64IM_SNAP_HDR + mem_size bytes.
1241const NX_RV64IM_SNAP_HDR: i64 = 312 // (7 scalars + 32 GPRs) * 8 bytes
1242
1243func nx_snap_wr64(b: *u8, off: i64, v: i64) -> i64 { var i: i64=0; while i<8 { b[off+i]=((v>>(i*8))&0xff) as u8; i=i+1 } return 0 }
1244func nx_snap_rd64(b: *u8, off: i64) -> i64 { var v: i64=0; var i: i64=0; while i<8 { v=v|((b[off+i] as i64)<<(i*8)); i=i+1 } return v }
1245
1246func nx_rv64im_sim_snapshot(s: *NxRv64imSim, buf: *u8) -> i64 {
1247 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
1248 nx_snap_wr64(buf, 0, s.pc)
1249 nx_snap_wr64(buf, 8, s.priv)
1250 nx_snap_wr64(buf, 16, s.halted)
1251 nx_snap_wr64(buf, 24, s.halt_code)
1252 nx_snap_wr64(buf, 32, s.steps)
1253 nx_snap_wr64(buf, 40, s.resv_addr)
1254 nx_snap_wr64(buf, 48, s.resv_valid)
1255 var i: i64 = 0
1256 while i < 32 { nx_snap_wr64(buf, 56 + i*8, nx_rv64im_rf_read(s.rf, i)); i = i + 1 }
1257 var m: i64 = 0
1258 while m < s.mem_size { buf[NX_RV64IM_SNAP_HDR + m] = s.mem_buf[m]; m = m + 1 }
1259 return NX_RV64IM_SNAP_HDR + s.mem_size
1260}
1261
1262func nx_rv64im_sim_restore(s: *NxRv64imSim, buf: *u8) -> i64 {
1263 if s.valid != 1 { return 0 - NX_HDL_BAD_KIND }
1264 s.pc = nx_snap_rd64(buf, 0)
1265 s.priv = nx_snap_rd64(buf, 8)
1266 s.halted = nx_snap_rd64(buf, 16)
1267 s.halt_code = nx_snap_rd64(buf, 24)
1268 s.steps = nx_snap_rd64(buf, 32)
1269 s.resv_addr = nx_snap_rd64(buf, 40)
1270 s.resv_valid = nx_snap_rd64(buf, 48)
1271 var i: i64 = 0
1272 while i < 32 { nx_rv64im_rf_write(s.rf, i, nx_snap_rd64(buf, 56 + i*8)); i = i + 1 }
1273 var m: i64 = 0
1274 while m < s.mem_size { s.mem_buf[m] = buf[NX_RV64IM_SNAP_HDR + m]; m = m + 1 }
1275 return NX_HDL_OK
1276}