History & Culture

From Assembler to Hell’s Gates: How a RISC-V Emulator Brought DOOM to Life

Featured visual

In a quiet corner of the internet, where code meets creativity, a developer has achieved something that sounds like digital alchemy: running the legendary 1993 first-person shooter DOOM on a custom-built RISC-V emulator. This isn’t just a nostalgic tech demo—it’s a masterclass in systems programming, low-level architecture, and the sheer tenacity required to make software dance on bare metal. The journey from a simple “Hello, World!” in assembly to a fully functional game rendering pixel-perfect demons in a simulated environment is a story of perseverance, ingenuity, and a deep understanding of how computers truly work.

What makes this project extraordinary isn’t just that it runs DOOM—it’s that it does so on an emulator built from scratch, implementing only a subset of the RISC-V instruction set. The creator, who shared their work on Hacker News, didn’t rely on existing emulation frameworks like QEMU or Spike. Instead, they crafted a lean, focused emulator capable of executing RV32IM instructions—a foundational 32-bit RISC-V standard with support for integer operations and multiplication—and paired it with a minimal system call interface. The result? A self-contained virtual machine that boots, loads an ELF binary, and runs one of the most iconic games in computing history.

🏛️Historical Fact
DOOM was originally written in C and compiled to run on x86-based DOS systems. To run it on a custom emulator, the developer used doomgeneric, a portable version of the game’s engine that abstracts away platform-specific code. This allowed the game logic to be compiled for RISC-V with minimal changes—proving that even decades-old software can be resurrected on entirely new architectures.

The Genesis: A “Hello, World!” That Sparked a Vision

Every great technical achievement begins with a simple proof of concept. For this project, that moment came when the developer successfully executed a basic assembly program that printed “Hello, World!” to the screen. It may sound trivial, but in the context of building a CPU emulator from scratch, this milestone is monumental. It means the core fetch-decode-execute loop is working, registers are being managed correctly, and the emulator can interface with some form of output.

From there, the challenge escalated rapidly. Running a single assembly instruction is one thing; running a complex C program with dynamic memory allocation, function calls, and system interactions is another beast entirely. The developer turned to newlib, a lightweight C library designed for embedded systems. Newlib provides standard C functions like `printf()` and `malloc()`, but it relies on the underlying system to implement low-level “stubs”—functions like `write()` or `sbrk()` that handle I/O and memory management. This modular approach allowed the developer to incrementally build up the system’s capabilities, one stub at a time.

Quick Tip
The RV32IM instruction set includes 47 base instructions and adds integer multiplication and division.

Newlib reduces the need to reimplement standard C libraries by allowing custom system call implementations.

ELF (Executable and Linkable Format) is the standard binary format used by Linux and many other systems.

DOOM requires approximately 1 MB of memory to run, including video RAM and game data.

The emulator uses a fixed memory layout with VRAM starting at address `0x705FDD`.


Building the Virtual Machine: RV32IM and the Art of Emulation

At the heart of this project lies a custom RISC-V emulator that interprets and executes machine code instructions in software. RISC-V, an open-source instruction set architecture (ISA), was chosen for its simplicity, modularity, and growing popularity in academia and industry. The RV32IM variant used here supports 32-bit addressing, basic integer operations, and multiplication—sufficient for running DOOM, but deliberately limited to keep the emulator lean and manageable.

Emulating a CPU is like recreating the nervous system of a computer. The emulator must simulate registers, memory, and the program counter, fetching instructions one by one and executing them in sequence. For example, when the emulator encounters an `ADD` instruction, it reads the values from two registers, performs the addition, and stores the result in a third. When it sees a `LW` (load word) instruction, it accesses simulated memory at a calculated address and pulls data into a register. Every operation must be precise, because even a single misinterpreted instruction can crash the entire system.

One of the most challenging aspects was implementing the memory model. The emulator maps a contiguous block of virtual memory, starting at address `0x000000`, where the program code, data, heap, and stack reside. The stack grows downward from `0x705FDD`, while the heap expands upward from the program’s data section. This layout mirrors how real operating systems manage memory, but in this case, it’s all simulated in software.

💡Did You Know?
The entire DOOM game, including graphics, sound, and level data, fits into just 2.39 MB of data. Despite its small size, it pushed the limits of what was possible on 1990s hardware—and now, on a hand-rolled RISC-V emulator.

Loading the Game: ELF Binaries and the Art of Portability

To run DOOM, the emulator needed to load an executable file. On modern systems, this is typically an ELF (Executable and Linkable Format) binary—a structured file format that contains machine code, data, and metadata like entry points and memory layout. The developer implemented a minimal ELF loader that parses the binary header and loads the program’s code and data into memory.

However, the loader currently supports only a single `PT_LOAD` segment—a simplification that reduces complexity but limits flexibility. In a full-featured system, ELF files can contain multiple segments for code, data, and dynamic linking. By restricting support to one segment, the developer avoided the intricacies of memory protection and segment alignment, focusing instead on getting the core functionality working.

This approach reflects a common strategy in systems programming: start simple, validate the concept, then iterate. The ELF loader reads the program’s entry point and jumps to it, beginning execution at the `_start` symbol—a convention in C programs that initializes the runtime environment before calling `main()`.

🤯Amazing Fact
Historical Fact

The ELF format was introduced by Unix System V in the late 1980s and became the standard for Linux and BSD systems. Its design allows for efficient loading and dynamic linking, making it ideal for complex software like games and applications.


The DOOM Port: Bridging Eras with doomgeneric

Porting a 30-year-old game to a custom emulator is no small feat. The original DOOM was tightly coupled to MS-DOS and x86 hardware, relying on specific BIOS calls and VGA graphics modes. To make it portable, the developer used doomgeneric, a community-maintained version of the DOOM source code that strips away platform-specific dependencies.

doomgeneric provides a clean interface for rendering graphics, handling input, and managing timing—functions that the emulator must implement. For example, when DOOM wants to draw a frame, it writes pixel data to a fixed memory region starting at `0x705FDD`, which the emulator treats as video RAM (VRAM). The emulator then reads this data and displays it, completing the feedback loop.

Article visual

Input is handled through a circular queue in memory. The emulator writes keyboard events (like arrow key presses) into a 32-byte buffer, updating read and write indices. DOOM periodically checks this queue to process player actions. This producer-consumer model is a classic synchronization pattern, demonstrating how even simple data structures can enable complex interactions.

💡Did You Know?
VRAM size: 1,024,000 bytes (enough for a 320×200 resolution with 16-bit color)

Input queue: 32 bytes with separate read/write indices

Stack starts at `0x705FDD` and grows downward

Program entry point is always at virtual address `0`


The Memory Map: A Digital Landscape for Demons

The memory layout of the emulator is a carefully orchestrated digital landscape. At the top of memory, just below `0x7FFFFF`, lies a small control structure: a 32-byte input queue with read and write indices. This is where the emulator injects keyboard events, allowing the player to move, shoot, and open doors.

Below that, spanning from `0x705FDD` to `0x7FFFFF`, is the VRAM region—1,024,000 bytes reserved for the game’s framebuffer. Every time DOOM renders a frame, it writes pixel data here. The emulator reads this data and updates the display, creating the illusion of real-time graphics.

The stack begins at `0x705FDD` and grows downward, while the heap and program data grow upward from address `0`. This creates a classic “stack-heap collision” scenario, where both regions expand toward each other. In a real system, this could lead to memory exhaustion, but in this controlled environment, the limits are carefully managed.

🤯Amazing Fact
Health Fact

Running DOOM on a custom emulator can be mentally taxing—debugging a segmentation fault in a hand-rolled CPU simulator requires intense concentration. Developers often report needing frequent breaks to avoid “debugging fatigue,” a real cognitive drain from prolonged low-level problem-solving.


Why This Matters: Beyond Nostalgia

At first glance, running DOOM on a custom RISC-V emulator might seem like a quirky hobby project—a digital Easter egg for tech enthusiasts. But beneath the surface lies a profound demonstration of systems programming mastery. This project touches on CPU design, operating system concepts, binary formats, memory management, and software portability.

It also highlights the enduring relevance of RISC-V. As the open-source ISA gains traction in everything from microcontrollers to data centers, understanding how to build and run software on RISC-V platforms becomes increasingly valuable. Projects like this serve as educational tools, helping developers grasp the fundamentals of computer architecture without relying on black-box tools.

Moreover, the success of DOOM on such a minimal system underscores the efficiency of well-designed software. Despite running on an emulator with no GPU, no OS, and limited memory, the game remains playable—proof that clever engineering can overcome hardware constraints.

💡Did You Know?
The original DOOM engine uses raycasting, a rendering technique that simulates 3D environments using 2D maps. It’s computationally efficient but limited in realism—yet it created the illusion of depth so convincingly that it revolutionized gaming in the 1990s.

The Road Ahead: What’s Next for the Emulator?

While the current implementation is impressive, it’s far from complete. Future enhancements could include support for more ELF segments, dynamic linking, floating-point operations (via the RV32F extension), and even a basic operating system layer. Adding sound would be another major milestone—DOOM’s iconic soundtrack and sound effects are a key part of its experience.

There’s also potential for performance optimization. Currently, the emulator likely interprets instructions one by one, which is slow compared to just-in-time (JIT) compilation. A JIT compiler could translate RISC-V code into native machine code on the fly, dramatically speeding up execution.

But perhaps the most exciting possibility is education. This emulator could become a teaching tool for computer science students, offering a hands-on way to explore CPU design, assembly language, and systems programming. Imagine a classroom where students modify the emulator to add new instructions or port other classic games.

💡Did You Know?
The creator of DOOM, John Carmack, later worked on VR and AI, but he began his career writing assembly code for the IBM PC. His early work on optimized rendering engines laid the groundwork for modern game development—and inspired countless developers to push the limits of what’s possible.

Conclusion: A Testament to Human Ingenuity

Running DOOM on a custom RISC-V emulator is more than a technical stunt—it’s a celebration of human curiosity and problem-solving. It reminds us that computers are not magical boxes, but intricate systems built on layers of logic, abstraction, and creativity. From a single “Hello, World!” to a hellish landscape filled with demons, this project traces the arc of software development itself: simple beginnings, iterative progress, and the joy of making something work against the odds.

In an age of high-level frameworks and cloud-native applications, projects like this reconnect us with the fundamentals. They show that understanding how a computer truly works—down to the last register and memory address—is not just possible, but deeply rewarding. And who knows? The next great innovation in computing might begin with someone else’s emulator, running a game that shouldn’t work, on hardware that doesn’t exist.

This article was curated from Show HN: I built a RISC-V emulator that runs DOOM via Hacker News (Top)


Discover more from GTFyi.com

Subscribe to get the latest posts sent to your email.

Alex Hayes is the founder and lead editor of GTFyi.com. Believing that knowledge should be accessible to everyone, Alex created this site to serve as...

Leave a Reply

Your email address will not be published. Required fields are marked *