EC · ENRIQUE CHYRNIA / EMURV.PRT ← Home
EMURV.PRTSHEET 01/09 · WHY IT EXISTSGREENBAR · 132 COL

01The 8086 in the room

In the Microprocessors course you learn assembly on emu8086: a 16-bit Intel 8086 emulator, Windows-only, from the DOS era. It does the job — until you try to write something that reacts to a keypress while it runs. My own experience with that wall is where this thesis starts.

RISC-V is the obvious replacement: an open specification, current, and the one the industry is actually adopting. But the RISC-V tools built for teaching are aging or closed — RARS is Java, Ripes is Qt, CPUlator is a closed web app. None of them is a modern native desktop application with real-time I/O. That gap is the whole reason EmuRV exists.

What it does, in one line Edit → assemble → run or step → watch registers and memory. Errors by line and column, in Spanish. A built-in 10-lesson course, 15 commented examples, and a graphics screen you can actually play a game on.
Read this page as backend Almost nothing here is about the interface. What follows is an assembler I had to write from scratch, an execution loop that has to stay interruptible, a memory policy — and above all the only question that really matters for an emulator: how do you know it's correct?
Thesis prototype · v0.1.0
EMURV.PRTSHEET 02/09 · RUN IT HEREGREENBAR · 132 COL

02Try it right here

Below is a working EmuRV, running in your browser. It is not a video and not a mockup: press Ensamblar, then Paso, and watch the registers light up as they change. Click the editor's left margin to drop a breakpoint. Run «Suma interactiva» and type into the console when it asks, or open the «Sprites» tab to watch the 320×240 screen get drawn. Switch the theme, open the lessons, read the help.

Honest about what this is: a faithful in-browser reconstruction of the interface, with a JavaScript interpreter underneath that genuinely assembles and executes the three programs on the tabs — here the editor is read-only, and the rest of the catalogue is labelled «solo en la app». The desktop application runs on Unicorn Engine with the full RV32IMAFDC repertoire, your own programs, ELF import and export, a GDB stub and detachable windows. Everything on the sheets that follow describes the real one.

EmuRV — interactive demo · in-browser build Open full screen ↗

The demo needs a window at least 1000 px wide to fit the editor and the panels side by side. On a narrow screen it will say so — open it full screen, or come back from a laptop.

EMURV.PRTSHEET 03/09 · THE MACHINEGREENBAR · 132 COL

03What happens when you press Step

A Tauri 2 shell, a Rust core, and the CPU itself borrowed from Unicorn Engine — the emulation engine carved out of QEMU. The emulation lives in its own thread and never talks to the UI directly: the interface sends commands and receives events (paused, output, registers_update). Between the two sits the piece everything else depends on.

Frontend · TypeScript Core · Rust · own thread MMIO devices Editor CodeMirror 6 · breakpoints Own assembler errors at line:column Machine · Unicorn adaptive slice ~40 ms Screen 320×240 · 0x9000_0000 Keyboard · 0xFFFF_0000 Console · ecall (a7) source_map · line ↔ address ↔ pc ties the editor's breakpoint to the program counter The UI never touches the machine: it sends commands and receives events. That is what keeps «Stop» responsive while a program is running. Conceptual, simplified diagram — the real core also holds the ELF importer, the disassembler, the trap handler and the GDB stub.
One step of execution, from the editor line to the emulated device.

The source_map, first

Assembling doesn't just produce bytes: it produces a bidirectional table between source line and address. Every 32-bit word emitted knows which line it came from — including the ones expanded out of a pseudo-instruction — and every line knows its first word. That table is what lets a click on the editor's margin become a real breakpoint, and what highlights the line that is about to execute. It had to be designed first, because everything else hangs off it.

AddressWordSource · hola-mundo.asm
0040000010010537lui   a0, 0x10010 ← «la a0, saludo» expands into two
0040000400050513addi  a0, a0, 0
0040000800400893addi  a7, zero, 4 ← service 4: print string
0040000c00000073ecall
0040001000a00893addi  a7, zero, 10 ← breakpoint on the editor line
0040001400000073ecall
Assembled: 24 bytes of code · ready to run.text base 0x00400000

The bug that shaped the execution loop

The first version called Unicorn once and let it run to the end, with a timeout to interrupt it. Then a student program of the form loop: j loop left the «Stop» button dead: Unicorn's timeout does not cut a tight loop with no syscalls in it. The instruction count does cut it, so execution was rewritten into bounded slices, checking for the stop signal between them.

A fixed slice of one million instructions cost speed. The current version makes the slice adaptive: it aims at roughly 40 ms of wall time per check, and drops back to the floor when the graphics screen is on, so frames keep their cadence. That single change moved the engine from 129–309 to 160–718 M instructions per second without losing any perceptible responsiveness. The register refresh was decoupled too — emitting one event per slice would have been about 500 events a second at the UI.

RegisterHexadecimalDecimalLast step
pc0x004000084194312
a0 x100x10010000268500992◀ changed
sp x20x7ffffff02147483632
a7 x170x000000000
zero x00x000000000
After two steps: «la a0, saludo» has landed. The changed register is highlighted — that flash is the whole didactic point.
EMURV.PRTSHEET 04/09 · THE ASSEMBLERGREENBAR · 132 COL

04The assembler I did not plan to write

The thesis brief closed the stack up front, and it said Keystone for assembly. Keystone's RISC-V support turned out not to be incomplete — it is nonexistent. I checked the 0.9.2 headers directly: the ks_arch enum has no KS_ARCH_RISCV entry at all, in any official release. That is a risk that fires on day one of a project, and the plan B was to write the assembler myself.

Which turned out to be the better outcome anyway, for two reasons the brief could not have anticipated: total control of the source_map, and diagnostics written for a beginner rather than for a toolchain. A third-party assembler would have handed me an English error with a byte offset.

stderr · emurv-cli asm hola.s
hola.s: línea 7, columna 9: instrucción desconocida: «sumar». EmuRV soporta RV32IM
        (enteros y multiplicación/división de 32 bits) y Zicsr (csrr/csrw/…)
hola.s: línea 12, columna 14: «li» carga constantes numéricas; para direcciones
        de etiquetas usá «la»
emurv-cli: el ensamblado falló con 2 error(es)

Real output, not an illustration. Every diagnostic carries line, column and — where the mistake has a common cause — the fix. In the app the same errors appear underlined in the editor.

How I know the encodings are right

An assembler that emits a wrong word produces a program that runs and gives the wrong answer — the worst possible failure for a teaching tool. So the encodings are not checked by hand: an oracle test suite assembles the same source with GNU as and compares the emitted words one by one. That oracle covers floating point (F and D), the atomics (A), the compressed 2-byte instructions (C) and the RV64IM repertoire against as -march=rv64im.

  • 001Full repertoire, switchable widthRV32IMAFDC — integers, multiply/divide, single and double floating point with the f0f31 file, atomics, and hand-written compressed instructions. A selector in the toolbar switches the machine to RV64: ld/sd, lwu, the «W» instructions and 6-bit shift amounts. RV32 stays untouched, and a 64-bit instruction inside an RV32 program produces a teaching error rather than a wrong result.
  • 002Directives a real project needs.include resolved relative to the including file, .macro with named parameters, .eqv/.equ constants, and %hi()/%lo() so a student can see what la actually expands to. Multi-file programs work in the app and from the CLI.
  • 003Standard ELF32, in and outExport produces a real ELF32 RISC-V that other tools load — Ripes, objdump. Import is the other direction: open a binary and the editor switches to read-only disassembly mode, with run, step and breakpoints working on the listing. A C program compiled with riscv64-elf-gcc loads too; an ELF with the wrong base address is rejected with a message that names the exact flag that is missing.
  • 004A relocatable variant, because someone will askasm --reubicable emits symbol references PC-relative (auipc) instead of absolute and marks the ELF ET_DYN, for a loader that places it somewhere else. The default stays absolute: that is the one that matches EmuRV's memory map when you inspect it elsewhere.
Verified against GNU as
EMURV.PRTSHEET 05/09 · DEVICES & REAL TIMEGREENBAR · 132 COL

05Devices, and the game that justifies them

The whole memory map is compatible with RARS — the same base addresses, the same keyboard semantics. That is not nostalgia: it means the Spanish-language teaching material that already exists for RARS runs on EmuRV unchanged.

AddressRegionNotes
0x0040_0000.textCode. Mapped R+X — not writable, on purpose.
0x1001_0000.dataData. Not executable.
0x1004_0000heapGrows upward from sbrk (syscall 9). No free.
0x7FFF_FFF0stack (sp)Initial stack pointer, 8 MB mapped below it.
0x9000_0000framebuffer320×240, one byte per pixel — VGA mode 13h.
0x9002_0000palette256 RGB888 colours; the program can rewrite them.
0x9003_0000vsyncWriting 1 means «frame ready».
0xFFFF_0000keyboard · controlBit 0 set means a key is waiting. Same address as RARS/MARS.
0xFFFF_0004keyboard · dataThe oldest key code — reading it consumes the key. Queue of 64.

Polling, and why it isn't a shortcut

All the I/O is polling, and that is a documented structural limitation, not a simplification: Unicorn's own FAQ states that it does not expose hardware interrupts. Building a CLINT and a PLIC on top of a CPU-only engine that officially does not support them is real architecture work, and it is written down as future work rather than quietly implied. What does exist is a keyboard the program can poll without ever blocking, plus syscalls 30 (system time in ms) and 32 (sleep, interruptible by «Stop»).

The .text region being read-only is a deliberate teaching policy — W^X. Jumping into data or patching your own code stops with a message that explains what happened rather than corrupting silently. Keep that decision in mind: it comes back with a cost on the next sheet.

Tetris as an acceptance test

The example that closes the lesson series is a port to RV32IM of an 8086 Tetris from the same Microprocessors course — the exact program that was painful on emu8086. Board in memory, bitmaps, collisions, line clears, scoring, drawn as coloured blocks on the 320×240 screen and played with the keyboard. Its game logic and the resulting pixels are checked by a test oracle, not by me looking at it.

Full-repaint frame rate (Rust side)811 FPS
What the UI actually consumes~30 Hz
Headroom over what the screen needs~27×
Hardware interrupts (CLINT / PLIC)[FUTURE]
Worst case, on purpose That 811 is the median of five runs (range 799–838), measured on the production path: a complete repaint of the framebuffer every frame plus the frame-close syscall. Nothing is skipped to make the number look better.
EMURV.PRTSHEET 06/09 · HOW I KNOW IT'S CORRECTGREENBAR · 132 COL

06Two verdicts I did not write myself

My own tests can only tell me the emulator agrees with what I believed when I wrote it. So the correctness argument rests on two suites that come from outside the project: the official riscv-tests, and riscv-arch-test through RISCOF, which compares my machine's output against Sail — the formal golden model of RISC-V International. Every number below is reproducible from a script in the repository.

Verdict A · riscv-tests, official self-checking suites · commit 34e6b6d1 · 2026-07-22
Suite · -marchTestsPassExcludedResult
rv32ui · rv32i_zicsr_zifencei42411[ PASS ]
rv32um · rv32im_zicsr88[ PASS ]
rv32ua · rv32ia_zicsr1010[ PASS ]
rv32uc · rv32ic_zicsr101[ EXCL ]
rv32uf · rv32if_zicsr11101[ PASS ]
rv32ud · rv32ifd_zicsr1091[ PASS ]
Total — 78/78 of the executable tests827840 FAIL
Verdict B · RISCOF 1.25.3 vs Sail 0.13 · signature compared word for word · 2026-07-22
Suite rv32i_m/TestsMatchMismatchResult
I — base integer3838[ MATCH ]
M — multiply / divide88[ MATCH ]
C — compressed27261[ 1 BY DESIGN ]
Zifencei101[ 1 BY DESIGN ]
Total — 72/72 of the compatible tests, identical signature74722100 %

The four exclusions and the two mismatches, named

A result with an asterisk is worth nothing if the asterisk is not spelled out, so here they are, each traced to root cause rather than waved away.

fence_i · rvc · Fencei — my W^X policy These tests write instructions into .text and jump into data — self-modifying code is precisely what they are testing. The official environment links everything into one RWX segment. EmuRV maps .text read-only by the teaching decision on the previous sheet, and stops with «the program tried to write to an address that doesn't belong to .data». Incompatible by design; the compressed decoding itself is covered by my own oracle against GNU as.
cebreak-01 — ebreak belongs to the debugger The test expects c.ebreak to be delegated to the guest's trap handler. In EmuRV ebreak is the software breakpoint mechanism of the UI, so it never reaches the guest. Same shape of conflict: a deliberate decision, not a semantics defect.
fmin — a defect in the engine underneath This one is not mine and it is the finding I am most pleased with. An isolated probe against the machine showed fmax.s(1.0, sNaN) returning a canonical qNaN instead of 1.0. That is the IEEE 754-2008 semantics, superseded by the RISC-V F v2.2 spec: QEMU fixed it in 6.0, but Unicorn 2.x is still based on QEMU 5.0.1. Subtest 20 of fmin.S detects exactly that case. Documented as an inherited limitation — with the version chain that explains it.

And underneath, the ordinary discipline

Rust core, against the real machine271
CLI50
Interface119
Total, zero failures · clippy with no warnings440
CI jobs green (macOS build + bundle weight gate)4/4
Counting them properly That total used to be reported as «271 + 10 + 119». The 10 was wrong: it counted a single test binary and silently dropped three others. I found it doing a documentation reconciliation pass, re-ran the whole suite and corrected every document that carried the figure. A number nobody had questioned was wrong for weeks — which is exactly why the reconciliation pass exists.
78/78 · 72/72 · 0 fail
EMURV.PRTSHEET 07/09 · PERFORMANCEGREENBAR · 132 COL

07Measured against the reference, not against itself

Four RV32IM kernels, the same algorithm on four emulators, ~500 M instructions per run, median of five, and equivalence verified first — all four produce the same numeric result per kernel. Comparing an emulator only against its own previous version proves nothing.

QEMU system-mode, TCG 1 091 – 4 806 Spike the ecosystem's reference ISS 882 – 992 EmuRV with the didactic hooks on 160 – 718 RARS its own category: teaching 5,6 – 6,0 1 10 100 1 000 10 000 Millions of instructions per second — LOGARITHMIC SCALE Band across the four kernels · median of 5 runs each · Apple M4 Pro
Each bar spans the slowest and fastest of the four kernels for that emulator. A logarithmic axis is the only honest way to hold 5.6 and 4 806 in the same picture — the exact numbers are in the table below.
Kernel · RV32IMEmuRVQEMUSpikeRARS
arithmetic — pure ALU718,43 289,5881,86,0
memory — lw/sw over 4 KiB257,14 342,1992,05,6
calls — call/ret + stack299,41 090,5982,15,8
mixed — LCG + memory + mul160,24 805,8992,05,7

Millions of instructions per second, median of 5. EmuRV 0.1.0 (Unicorn 2.1.5) · QEMU 11.0.2 · Spike 1.1.1-dev · RARS 1.6 on OpenJDK 25, with its 0.133 s JVM startup discounted.

Reading it honestly

Against RARS — its actual category, the same didactic job — EmuRV is 28 to 120× faster. Against Spike, the reference instruction-set simulator of the ecosystem, it lands in the same order of magnitude: 81 % of Spike on pure ALU. Against plain QEMU it is 0.7 to 1.5 orders behind.

That remaining gap is bought, not lost: an interruptibility check every ~40 ms, the observation hooks that make the keyboard, the screen and the syscalls visible, and a wall time that includes process start and assembling the source. The engine's own ceiling with no slicing, on the same machine, was 1 293 M instr/s when it was last remeasured. The requirement in the spec was 10 — it clears it by 16 to 72× in the worst kernel, and the author's decision was to stop chasing speed there.

Three numbers for one metric My own documentation carried three different frame rates for the same benchmark — 835, 797 and 832 — and I spent a while looking for the regression between them. There wasn't one. All three were single runs of a bench with ±2.5 % spread. Five control runs gave 799, 804, 811, 814, 838: the three historical figures all sit inside the normal band. The fix wasn't to the code, it was to the method — every figure now ships with its median, its range and its n.

Limitations I am declaring before you find them

  • 001Nominal instruction countDeterministic loops counted over the listing, not retired by a hardware counter. The same criterion is applied to all four emulators, so the ratios hold even if an absolute is off.
  • 002Synthetic microkernels, not CoreMarkThe bare-metal CoreMark port per target was deferred by an explicit effort gate. It does not change the order-of-magnitude conclusion, and the harness is ready for it.
  • 003One machine, and QEMU in system modeEverything ran on a single Apple M4 Pro, so the ratios between emulators are the citable part, not the absolutes. QEMU had to run in system mode because macOS Homebrew ships no user-mode build; Spike and QEMU execute bare-metal ELF while EmuRV and RARS execute the same assembly source.
  • 004emu8086 is a documentary comparisonThe emulator EmuRV replaces is a step-by-step interpreter that redraws its UI per instruction, publishes no performance figures, and needs a Windows machine to run. I am not going to invent a number for it.
EMURV.PRTSHEET 08/09 · THE INTERFACEGREENBAR · 132 COL

08The part the student actually touches

Everything themeable lives in CSS design tokens, so a theme is one block of variable overrides and no duplicated CSS. Five themes ship with the app plus one the user builds, and the contrast of all five is verified by an automated test — body text and editor syntax both, against WCAG AA. A theme that fails the test does not ship.

EmuRV — tema Educativo (default)
EmuRV main window: toolbar, file tabs, editor with the executing line highlighted and a breakpoint, and the Screen, Registers, Memory and Debug panels, console and status bar
Main view. Editor on the left, machine on the right. Registers are always visible and come first; Memory folds; Debug starts collapsed and reveals itself the moment you set your first breakpoint. Each of those panels can be torn off into its own OS window with the ⧉ button, and stays synchronised with the running program.
Tema Retro-CRT
EmuRV with the Retro-CRT theme: amber phosphor on black with scanlines
Retro-CRT. Amber phosphor and scanlines. The editor's syntax colours follow the theme tokens too — CodeMirror reads the same variables.
Tema Pergamino
EmuRV with the Pergamino theme: warm light paper
Pergamino. The light theme, for a projector in a lit classroom — the case where a dark IDE stops being readable from the back row.
Tema Retro Kawaii
EmuRV with the Retro Kawaii theme: pastel vaporwave palette
Retro Kawaii. Pastel vaporwave. It passes the same AA test as the rest — that constraint is what kept the pastels from going unreadable.
Tema Disquete 90s
EmuRV with the Disquete 90s theme: nineties desktop beige with coloured title bars
Disquete 90s. Nineties desktop beige, coloured title bars. Same tokens, a different decade.
Modal · Editar tema
EmuRV theme editor: a colour control per design token, with a live preview and a WCAG AA contrast warning
Build your own. One colour control per token — surfaces, text, accent, states and editor syntax — with live preview, export/import as JSON, and a contrast warning that informs without blocking: it is your theme.
Modal · Lecciones
EmuRV lessons modal: two-column curriculum with the lesson list and progress bar on the left and the selected lesson's objectives on the right
Ten lessons. Registers → arithmetic → memory (endianness in front of you) → conditionals → loops → functions and the stack → syscalls → multi-file → MMIO and real time → traps, your first mini-kernel. Opening one loads its code into the editor. All ten also run from the terminal, and the CI keeps watch on them.
Modal · Ayuda
EmuRV help modal: two-column reference with a scroll-spy index covering syscalls, memory map, MMIO keyboard, directives and debugging
The reference, inside the app. Syscalls, memory map, MMIO keyboard, directives, debugging — with a scroll-spy index, so nobody has to leave the tool to look up an a7 value.
Ventana angosta
EmuRV in a narrow window: toolbar buttons collapse to icon-only and the controls rewrap
Any window width. The toolbar buttons drop to icon-only and the controls rewrap instead of being cut off — a laptop in a classroom is not a 27-inch display.
Tema personalizado
EmuRV showing a user-built custom theme
A user's theme. Unlike the built-ins, its values never enter tokens.css: they are CSS variables applied live on :root and saved on the machine.

One language decision, stated rather than hidden

The interface has an ES/EN selector. The assembler and runtime diagnostics, and the lesson content, stay in Spanish even in English mode. That is a declared scope decision — the Spanish material is the didactic contribution of the thesis, and translating it would have meant maintaining two versions of the thing being evaluated. It is written down as a decision, not left as an oversight for someone to discover.

EMURV.PRTSHEET 09/09 · STACK & HONEST STATUSGREENBAR · 132 COL

09Inventory, and what is still missing

Core

  • Rust · Tauri 2
  • Unicorn Engine 2.1.5
  • Own RV32IMAFDC / RV64IM assembler
  • Own disassembler
  • ELF32 export and import (crate object)
  • Emulation on a dedicated thread

Devices

  • Framebuffer VGA 13h · 320×240
  • MMIO keyboard, RARS-compatible
  • Tone generator (off by default)
  • Syscalls in the RARS convention
  • Zicsr · user trap handler

Interface

  • TypeScript · Vite
  • CodeMirror 6 (own RISC-V mode)
  • CSS design tokens · 5 themes + user theme
  • IBM Plex Sans · JetBrains Mono (self-hosted, OFL)
  • Detachable panel windows
  • ES / EN with persistence

Tooling

  • emurv-cli — headless assemble / run / disasm / debug
  • GDB remote stub (gdbserver protocol)
  • Semantic exit codes, for automatic grading
  • GitHub Actions · 4 jobs · bundle weight gate

Verification

  • 440 automated tests, zero failures
  • Encoding oracle against GNU as
  • riscv-tests · 78/78 executable
  • RISCOF vs Sail · 72/72 signatures
  • Automated WCAG AA contrast test
  • Reproducible benchmark (script + CSV)

Written record

  • 32 roadmap points, each with its plan
  • Engineering log with the decisions and their why
  • Documentary reconciliation against evidence

Where it really stands

The thesis is in progress. Separating what is verified from what is merely built is the whole point of the exercise.

[x] Built and verified
  • · RV32IMAFDC + switchable RV64IM
  • · Official compliance, twice over
  • · Screen, keyboard, sound, sprites, traps
  • · CLI, GDB stub, CI in green
  • · Benchmark against QEMU, Spike and RARS
  • · 10 lessons and 15 examples, watched by tests
[~] Built, not verified
  • · Windows: the NSIS installer cross-compiles and runs in CI, but it has never been opened on a real Windows machine — so I do not claim it works
  • · Linux: the code is portable and the build leg exists, marked experimental; not validated
  • · macOS on Apple Silicon is the only platform I have actually tested
[ ] Missing, and named
  • · The usability study with students — protocol and questionnaires ready; it needs the supervisor's sign-off and participants. It is what the committee actually grades
  • · Hardware interrupts: CLINT and PLIC over an engine that does not expose them
  • · CoreMark, code signing and notarisation, versioned releases
Thesis in progress