Simmerv is a RISC-V SoC emulator written in Rust and compilable to WebAssembly. It began as a fork of Takahiro's riscv-rust emulator, but has by now been extensively rewritten, making it far more complete and much faster. Ultimately, we expect it to become substantially faster, but this work is delayed until we are able to run standard benchmarks and off-the-shelf Linux distributions.
You can run Linux on the emulator in your browser: online demo is here
- Emulates RISC-V
RV64GC_Zba_Zbb_Zbc_Zbs_Zicond_Zfhmin_Svinval_Svade_Svpbmt_Sstc_Zicbom_Zicbop_Zicboz_Zihpm(RVA22) processor and peripheral devices (CLINT, PLIC, NS16550A UART, virtio block device, and VirtIO ethernet) - Optional RVA23 mode, enabled with
--rva23: theVvector extension (RVV 1.0,ELEN=64,VLEN=128 or 256 via--vlen) plus Zcb, Zimop, Zcmop, Zfa, Zawrs, Zacas, Zabha, Zvbb, Zvkt and Zihintntl. Boots the RVA23 port of Ubuntu 26.04. - Targets native and WASM
- Snapshots
- Speedometer
- RV64IMAC
- RV64FD
- RV64Zifencei
- RV64Zicsr
- Zba, Zbb, Zbc, Zbs ("B" extension)
- Zicond
- Zfhmin (half-precision float conversions)
- Zihpm (hardware performance counters)
- Zicbom, Zicbop, Zicboz (cache block operations)
- Svinval (fine-grained TLB invalidation)
- Svade (hardware A/D fault-on-access)
- Sstc (stimecmp/menvcfg timer compare)
- Sv39, Sv48, Sv57 (the wider two are opt-in: see
--satp-mode) - Svpbmt (page-based memory types; PTEs accepted, no caches to model)
- Privileged Spec 1.12 (mcounteren/scounteren, senvcfg, PMP stub with 0 entries)
- Svnapot
- [-] PMP enforcement (0 entries implemented; all accesses permitted)
The emulator supports all instructions listed above.
- Passes all riscof (RISC-V Architectural Tests) for RV64IMC
- Boots Buildroot, Debian Trixie, Ubuntu
- Linux OpenSBI and legacy BBL boot support
- V — the vector extension, RVV 1.0. All of
Zve64dplus the fullVinstruction set: configuration, unit-stride / strided / indexed / segment / whole-register / fault-only-first loads and stores, the integer, fixed-point, mask, permutation and floating-point operations, and thevstart/vl/vtype/vxrm/vxsat/vcsr/vlenbCSRs withmstatus.VS.VLENdefaults to 128 (Zvl128b, the profile minimum);--vlen 256selectsZvl256binstead. - Zvfh, Zvfhmin — vector half-precision (the full arithmetic set, which is
a superset of the
Zvfhminthe profile requires) - Zvkt, Zkt — constant-time execution; trivially satisfied by a functional model with no data-dependent timing
- Zicond, Zfhmin, Zicbom/z/p, Svinval, Svnapot, Svpbmt, Sstc, Svade (inherited from RVA22)
- Zcb — the newer compressed encodings. Each one abbreviates an instruction the emulator already had, so all twelve decode straight to that uop rather than to a parallel execution path.
- Zimop, Zcmop — may-be-operations.
mop.r.n/mop.rr.nwrite zero tord;c.mop.nchanges no architectural state at all. - Zfa —
fli(both constant tables),fminm/fmaxm, the quiet comparesfleq/fltq,fround/froundnxandfcvtmod.w.d. The half-precision members are omitted: they need fullZfh, which RVA23 does not mandate and this emulator does not implement. - Zvbb — vector basic bit manipulation (
vandn,vbrev,vbrev8,vrev8,vclz,vctz,vcpop,vrol,vror,vwsll), and with itZvkb, which is a strict subset - Zawrs — wait-on-reservation-set. Both forms may terminate the wait immediately and for any reason, so both retire as no-ops.
- Zacas —
amocas.b/h/w/d/q, including the quadword form over even/odd register pairs - Zabha — byte and halfword forms of every AMO
- Zihintntl — non-temporal locality hints. The four hints are
ADD x0, x0, x2..x5(and theC.ADD x0, x2..x5compressed forms), so any conforming hart already retires them as no-ops; a functional model has no cache hierarchy to hint at, which is the same reasonZktandZvktare satisfied for free. Verified to leavex0and the source registers untouched. - Sv48, Sv57 in
satp. Opt-in with--satp-mode sv48/sv57, because Linux keeps the widest modesatpaccepts: defaulting to Sv57 would move every existing guest onto a deeper page table. Exercised by the Tenstorrent suite'spaging_sv48andpaging_sv57groups. - Supm / Ssnpm / Smnpm — pointer masking. Deferred. Nothing depends
on it: masking is opt-in per process (
prctl(PR_SET_TAGGED_ADDR_CTRL)), so software that does not find it simply does not tag pointers, and the one real beneficiary -- HWASAN, which is why Android leans on Arm's equivalent TBI -- does not exist for RISC-V yet. Nor would an emulator gain what the feature is for: the point is that hardware ignores the tag bits for free, and here that is just "do not fault on these addresses". Revisit if LLVM gains RISC-V HWASAN, or a distro ships HWASAN-instrumented RISC-V packages. - Sscofpmf — count-overflow interrupts. A counter wrapping past all-ones
sets
OFon the 0->1 edge and raisesLCOFIP; the interrupt is delivered asCounterOverflowInterruptandscountovfreflects it. Advertised in both device trees. - Ssstateen / Smstateen — the state-enable CSRs.
mstateen0'sSE0andENVCFGbits gatesstateen0andsenvcfgbelow M-mode; every other bit, andmstateen1..3/sstateen0..3entire, reads zero, because the state those bits guard (AIA, IMSIC,Zcmt'sjvt,Zfinx'sfcsr, custom) does not exist here.mstateen0resets permissive rather than to the architectural zero: the only state it gates issenvcfg, so denying by default protects nothing while breaking any firmware that predates Smstateen and never opens the gate. - H — the hypervisor extension, required by RVA23S64. Deferred. Not
needed to run Linux, only to host it: a guest that finds no H simply
offers no KVM. It is also the largest item by a distance -- two-stage
translation, the VS-mode CSR bank, the hypervisor load/stores -- and it
lands on the MMU fast path. Tenstorrent's suite ships an
h_exttest tree that is skipped for now, so the tests are waiting whenever it is picked up.
So RVA23 is complete but for Supm and H, both deferred above for the
same reason: nothing depends on either, and neither blocks anything simmerv is
for.
Coverage is not self-reported: the user-mode set is exercised by Tenstorrent's
architectural tests (see tests/tenstorrent/run.sh) and the vector
implementation is diffed against QEMU instruction by instruction at both
VLEN widths (tests/vector/run.sh).
The device trees advertise what is implemented, so a guest can discover it.
That includes the default (non---rva23) machine, which used to name only
imafdc plus a handful of CSR extensions despite always having had the
bitmanip set, Zicond, Zfhmin and the cache-block operations.
V, Zcb, Zimop, Zcmop, Zfa, Zvbb, Zawrs, Zacas and Zabha are off by default and
enabled together by --rva23, so that a run without the flag still models a hart
that traps every one of those encodings. (The entries inherited from RVA22 are
always on.) RVA23 mandates V, so one switch covers the lot: it gates
instruction decoding, the misa V bit and the vector CSRs, and swaps in a
device tree whose cpu node advertises the profile.
$ cargo r -r -- --rva23 -n my-vector-program.elfTo boot the RVA23 port of Ubuntu, the guest kernel must also have been built
with CONFIG_RISCV_ISA_V=y — without it the kernel drops v from the ISA it
parses out of the device tree, and an RVA23 userland dies on the first vector
instruction in ld.so, because the profile lets glibc emit vector code with no
scalar fallback. riscv: base ISA extensions in the boot log is the thing to
check: it should read acdfimv.
$ cargo r -r -- --rva23 -f ubuntu-26.04-preinstalled-server-riscv64.img \
fw_payload.bin,0x80000000tests/vector/run.sh builds a bare-metal exerciser — 1994 instruction cases
across every SEW, LMUL, rounding mode and addressing form, covering the RVA23
additions as well as base RVV — and diffs simmerv's transcript against QEMU's
virt machine, which shares simmerv's memory map. All 1994 transcript lines
currently match exactly, with three traps on both sides: vwsll at SEW=64,
where the widened element exceeds ELEN and the encoding must be illegal.
One deliberate difference: simmerv enforces vill, register-group alignment,
EMUL range and the "masked instruction may not write v0" rule, but not the
finer source/destination overlap constraints (a narrowing vs1 inside the
double-width vs2 group, a segment-load destination overlapping the index
group). QEMU raises an illegal instruction for those; simmerv executes them.
No conforming assembler emits them.
VERY IMPORTANT: images are stored with git LFS. Install LFS (don't
forget git lfs install also) and recheckout if needed. Otherwise
the images will be small files with LFS pointers.
$ cargo r -r -- linux/fw_payload.bin,0x80000000 -f linux/rootfs.imgor
$ cargo r -r -- -c linux/opensbi/fw_jump.elf,0x80000000 linux/vmlinux,0x80200000 -f linux/rootfs.imgAllocate 2 GiB, use a device tree with initramfs at 0xa0000000 and load the initrd2+gdb.cpio binary at that address.
$ (cd linux;cargo r -r -- -m 2048 -d with-initrd.dtb fw_payload.bin,0x80000000 initrd2+gdb.cpio,0xa0000000)Simmerv emulates a VirtIO-net device (MAC 52:54:00:12:34:56) on the second
virtio-mmio window. The built-in device tree already declares it, so a guest
kernel with CONFIG_VIRTIO_NET probes it automatically — but the device stays
inert (packets are dropped) until you attach a host backend. There is no
built-in emulator NAT/DHCP other than what the backend provides.
On Linux, Simmerv connects the guest's NIC to a TAP device — a raw
layer-2 link with no DHCP or NAT of its own, so you configure the host side
and give the guest a static address (or bridge tap0 into a real network if
you prefer).
- Create a persistent TAP owned by your user (so Simmerv needs no root):
$ sudo ip tuntap add dev tap0 mode tap user $USER
$ sudo ip link set tap0 up
$ sudo ip addr add 172.16.0.1/24 dev tap0- For guest internet access, enable forwarding + NAT on the host. The
MASQUERADE rule must name your internet-facing interface — auto-detect
it from the default route rather than assuming
eth0:
$ UPLINK=$(ip route show default | awk '{print $5; exit}') # e.g. enp9s0, wlan0
$ echo "NAT via uplink: $UPLINK" # sanity-check it
$ sudo sysctl -w net.ipv4.ip_forward=1
$ sudo iptables -t nat -A POSTROUTING -s 172.16.0.0/24 -o "$UPLINK" -j MASQUERADE
$ sudo iptables -A FORWARD -i tap0 -j ACCEPT
$ sudo iptables -A FORWARD -o tap0 -m state --state RELATED,ESTABLISHED -j ACCEPTIf the guest reaches the host (172.16.0.1) but nothing beyond it, this
rule is almost always the culprit — check it with
sudo iptables -t nat -L POSTROUTING -n -v (wrong out interface, or a
pkts count stuck at 0 while the guest generates traffic).
- Run Simmerv attached to the TAP (
-T <ifname>):
$ cargo r -r -- -T tap0 linux/fw_payload.bin,0x80000000 -f linux/rootfs.img- Configure the interface inside the guest (the name may be
eth0,enp0s…, etc. — checkip link):
# in the guest
$ ip addr add 172.16.0.2/24 dev eth0
$ ip link set eth0 up
$ ip route add default via 172.16.0.1 # only needed for step 2
$ echo 'nameserver 1.1.1.1' > /etc/resolv.confHost (172.16.0.1) and guest (172.16.0.2) can now ping each other; with
step 2 the guest also reaches the internet. Remove the TAP afterwards with
sudo ip tuntap del dev tap0 mode tap.
On macOS, Simmerv uses Apple's vmnet.framework in shared mode, which
supplies DHCP and NAT automatically — the guest just needs to request an
address. vmnet shared mode requires elevated privileges, so run under
sudo. Build first so sudo doesn't rebuild the tree as root:
$ cargo build -r
$ sudo ./target/release/simmerv_cli --vmnet linux/fw_payload.bin,0x80000000 -f linux/rootfs.imgThe guest receives an address on vmnet's subnet (typically 192.168.x.x) with
NAT to the host's network. If your guest image doesn't bring the link up
automatically, run a DHCP client inside it:
# in the guest
$ udhcpc -i eth0 # busybox; or: dhclient eth0(-T/--tap is Linux-only and --vmnet is macOS-only; each errors out on the
other platform.)
$ ./run-riscv-tests.shSee wasm/web
See wasm/npm