ROVM - A Virtual Computer & OS Implemented Entirely in Roblox

ROVM - A Virtual Computer & OS Implemented Entirely in Roblox


What is this?

ROVM is a complete virtual computer running inside Roblox. This project started as an experiment to see how far Roblox’s runtime and Luau could be pushed, but it has since evolved into a full-fledged computer. It has its own CPU, a built in C compiler, assembly language, framebuffer, terminal, filesystem, and even virtual memory with copy-on-write. You can write programs for it in C or assembly and they actually run.

This runs entirely within Roblox’s sandbox, with no exploits, no native code, and no external execution.


What can it do?

  • Custom 32-bit CPU with 16 registers and 26+ instructions
  • Full assembly language with labels, macros, and directives
  • Built in C Compiler supporting standard libraries
  • Pixel-addressable framebuffer for drawing graphics
  • Memory management with virtual memory and page tables
  • Persistent filesystem that saves to DataStore
  • Included Demos: DOOM, Neofetch, Bad Apple, and more!
  • Interactive terminal with shell commands
  • Text display with an 8x8 bitmap font
  • Kernel/user mode separation
  • Round-robin process scheduler

The Framebuffer

One of the most unique parts of this project is the framebuffer. Programs can write directly to a memory-mapped pixel buffer and draw whatever they want. Each pixel is just a memory address; write an RGB value to it and it shows up on screen. It is powered by EditableImage for extremely fast O(1) rendering, allowing for consistently hitting high FPS while using the shell. For more intensive programs, like DOOM, the screen is not the bottleneck.

The framebuffer maps every pixel to a memory address. Writing 0xFF0000 to address 0x100000 sets the first pixel to red. Simple but powerful.

-- Each pixel is memory-mapped starting at PIX_BASE
mem:mapDevice(PIX_BASE, pixelCount,
    function(addr) -- reading a pixel
        local i = addr - PIX_BASE
        local x = i % w
        local y = math.floor(i / w)
        return color3ToInt(screen.pixels[y * w + x + 1])
    end,
    function(addr, value) -- writing a pixel
        local i = addr - PIX_BASE
        local x = i % w
        local y = math.floor(i / w)
        screen:setPixel(x, y, intToColor3(value))
    end
)

There’s also a control device for things like clearing the screen and triggering reboot:

-- Control registers for screen operations  
local CTRL_CLEAR  = 0  -- write color to clear screen
local CTRL_FLUSH  = 1  -- write anything to flush display
local CTRL_REBOOT = 2  -- write 1 to trigger reboot
local CTRL_WIDTH  = 3  -- read screen width
local CTRL_HEIGHT = 4  -- read screen height

Hardware-Accelerated GPU

Originally, the system required you to write pixels one by one to the framebuffer. Now, the OS has a built in virtual GPU accessed via syscalls (SC_GPU_DRAW_RECT, SC_GPU_DRAW_LINE, etc.). This hardware acceleration natively interfaces with Roblox’s EditableImage API under the hood, allowing C programs to draw filled rectangles, lines, and complex 3D raycasted scenes.


// Example of drawing a red square in C

void draw_square(int x, int y, int size) {

syscall(3, x, y, size, size, RGB(255, 0, 0), 0); // SC_GPU_DRAW_RECT

}

There’s also a control device for things like clearing the screen, syncing frames, and triggering reboot:


syscall(2, 0, 0, 0, 0, 0, 0); // FLUSH display

syscall(5, 0, 0, 0, 0, 0, 0); // CLEAR screen


The CPU

The CPU runs in a fetch-decode-execute loop like a real processor. Each instruction is 32 bits packed into one word, opcode in the top 8 bits, then register operands in the rest.

OPCODES[0x04] = function(self, d, a, b) -- ADD rd, ra, rb
    self.reg[d+1] = bit32.band(self.reg[a+1] + self.reg[b+1], 0xFFFFFFFF)
    self.pc += 1
end

OPCODES[0x07] = function(self, d) -- JZ rd, target
    local tgt = memRead(self, self.pc + 1)
    if self.reg[d+1] == 0 then
        self.pc = tgt
    else
        self.pc += 2
    end
end

OPCODES[0x30] = function(self, _, _, b) -- SYSCALL imm8
    self.trap = { kind = "syscall", n = b, pc = self.pc }
    self.pc += 1
end

There’s kernel mode and user mode. User programs can’t run privileged instructions like HALT or FLUSH - they’ll get killed if they try:

OPCODES[0x01] = function(self) -- HALT
    if self.mode == "user" then
        trap(self, "fault", { 
            type = "privileged_instruction", 
            instruction = "HALT"
        })
        return
    end
    trap(self, "halt", "HALT")
end

Assembly Language

The assembler takes human-readable assembly and produces machine code. It handles labels, wide immediates (32-bit values embedded in the instruction stream), and some useful macros.

Here’s the full instruction set:

local OPCODES = {
    NOP = 0x00, HALT = 0x01, LOAD = 0x02, STORE = 0x03,
    ADD = 0x04, SUB = 0x05, JMP = 0x06, JZ = 0x07,
    LOADI = 0x08, MUL = 0x09, DIV = 0x0A, MOD = 0x0B,
    AND = 0x0C, OR = 0x0D, XOR = 0x0E, NOT = 0x0F,
    SHL = 0x10, SHR = 0x11, MOV = 0x12, CMPEQ = 0x13,
    CMPLT = 0x14, CMPGT = 0x15, FLUSH = 0x20, SLEEP = 0x21,
    CALL = 0x22, RET = 0x23, PUSH = 0x24, POP = 0x25,
    SYSCALL = 0x30,
    -- Immediate variants
    ADDI = 0x16, SUBI = 0x17, MULI = 0x18,
    ANDI = 0x19, ORI = 0x1A, XORI = 0x1B,
}

Built in constants make it easier to work with memory-mapped devices:

local STDLIB_LABELS = {
    SYS_TEXT_WRITE = 0x400004,  -- write char here to display it
    SYS_TEXT_FG = 0x400002,     -- foreground color
    SYS_TEXT_BG = 0x400003,     -- background color
    SYS_NL = 10, SYS_CR = 13, SYS_BS = 8, SYS_SP = 32,
}

Byte-Addressable Memory

A major recent upgrade to the VM architecture: memory is now fully standard, byte-addressable RAM powered by Luau buffer objects. This allows standard C functionality, like char* string manipulation, memcpy, pointers, and malloc, to work exactly as they do natively without awkward 32-bit word alignments.


The Terminal

The operating system shell is written fully in C. The system boots up, compiles shell.c, and launches it inside the VM just like a real Linux machine booting standard binaries.

Commands: help, clear, reboot, echo, ls, mkdir, cat, write, rm

Because it’s written in standard C, the main input loop looks familiar:

int main() {
    print_banner();
    
    char buf[256];
    int pos = 0;
    
    while (1) {
        print("\nuser:/");
        print(cwd);
        print("> ");
        while (1) {
            char c = syscall(1, 0, 0, 0, 0, 0, 0); // read char
            if (c == 13 || c == 10) break; // enter
            if (c == 8 && pos > 0) { // backspace
                pos--;
                syscall(0, 8, 0, 0, 0, 0, 0);
            } else if (c >= 32 && pos < 255) {
                buf[pos++] = c;
                syscall(0, c, 0, 0, 0, 0, 0);
            }
        }
        buf[pos] = 0;
        
        if (pos > 0) {
            execute_cmd(buf); // dispatch it to commands
        }
        pos = 0;
    }
    return 0;
}

Syscalls are how C programs talk to the kernel (instead of standard libc wrappers):

// Syscall numbers:
//   0 = write char     1 = read char      2 = flush screen
//   3 = gpu rect       4 = gpu line       5 = clear screen
//   32 = open file     33 = read file     34 = write file
//   35 = close file    37 = unlink        38 = mkdir
//   40 = listdir       42 = sbrk (alloc)  73 = sysinfo

All file I/O, parsing, and command dispatch is handled completely inside the virtual machine with no host side UI shortcuts!


Virtual Memory

The MMU translates virtual addresses to physical ones. Each process gets its own page table so they can’t mess with each other’s memory.

function MMU:translate(virtualAddr, accessType)
    if self.kernelMode then
        return virtualAddr  -- kernel uses physical addresses directly
    end
    
    local virtualPage = self.currentPageTable:addressToPage(virtualAddr)
    local pageOffset = self.currentPageTable:getPageOffset(virtualAddr)
    
    local physicalPage, err = self.currentPageTable:translate(virtualPage)
    if not physicalPage then
        return nil, "page_fault"
    end
    
    -- Copy-on-write check
    if accessType == "write" and self.currentPageTable:isCow(virtualPage) then
        return nil, "cow_fault"
    end
    
    return (physicalPage * self.pageSize) + pageOffset
end

Copy-on-write means forked processes share memory until one of them writes to it. Then we copy the page:

function MMU:handleCowFault(virtualAddr, physicalMemory)
    local virtualPage = self.currentPageTable:addressToPage(virtualAddr)
    local oldPhysicalPage = self.currentPageTable:translate(virtualPage)
    
    -- Allocate new page
    local newPhysicalPage = self.physicalAllocator:allocatePage()
    
    -- Copy the data
    for i = 0, self.pageSize - 1 do
        physicalMemory.data[newAddr + i] = physicalMemory.data[oldAddr + i]
    end
    
    -- Update page table to point to new page
    entry.physicalPage = newPhysicalPage
    entry.permissions = bit32.band(entry.permissions, bit32.bnot(PERM_COW))
end

Text Rendering

The text device draws characters using an embedded 8x8 bitmap font. Each character is 8 bytes, one per row, with bits representing pixels.

local function drawChar(screen, cellX, cellY, ch, fg, bg)
    local glyph = FONT[ch] or FONT[63]  -- '?' for unknown chars
    local px0 = cellX * 8
    local py0 = cellY * 8

    for row = 0, 7 do
        local bits = glyph[row + 1]
        for col = 0, 7 do
            local on = bit32.band(bits, bit32.lshift(1, col)) ~= 0
            if on then
                screen:setPixel(px0 + col, py0 + row, fg)
            elseif bg then
                screen:setPixel(px0 + col, py0 + row, bg)
            end
        end
    end
end

-- The font table
local FONT = {
    [65] = {56,108,198,254,198,198,198,0},   -- A
    [66] = {126,198,198,126,198,198,126,0},  -- B
    -- ... 95 printable ASCII characters
}

Filesystem

The filesystem is inode-based like Unix. Files and directories get inode numbers, directories store child inodes by name. Everything persists to DataStore so your files survive between sessions.

function Filesystem:createFile(path, data)
    local dirPath, filename = self:splitPath(path)
    local parentInode = self:resolvePath(dirPath)
    
    local inode = self:allocateInode()
    self.inodes[inode] = {
        type = Filesystem.TYPE_FILE,
        size = #data,
        data = data,
        parent = parentInode,
        permissions = PERM_READ + PERM_WRITE,
    }
    
    self.inodes[parentInode].children[filename] = inode
    return inode
end

Process Scheduler

There’s a round-robin scheduler that can run multiple processes. Each process has its own CPU state, page table, and file descriptors.

function Scheduler:scheduleNext()
    -- Put current process back in queue if still running
    if self.currentPid then
        local current = self.processes[self.currentPid]
        if current and current.state == "ready" then
            table.insert(self.processQueue, self.currentPid)
        end
    end
    
    -- Pop next ready process
    local nextPid = table.remove(self.processQueue, 1)
    self.currentPid = nextPid
    
    if nextPid then
        local proc = self.processes[nextPid]
        proc.state = "running"
        proc:restoreState()
    end
end

The C Compiler

While the system was originally built just for assembly (and OS written fully in assembly), I’ve since created a full custom C compiler for BloxOS. You can write C code using the built in edit command, compile it directly within the virtual shell using cc, and run it instantly.

The compiler supports #include directives, pointers, arrays, loops, and hooks into kernel syscalls via rovm.h. You can even render graphics in C!


What’s Included

When you boot the VM, the BloxOS shell launches automatically. I’ve pre-compiled several impressive demos that push the system to its absolute limits:

  • doom: Fully playable classic 1993 shooter.

  • neofetch: A dynamic system tool that queries the kernel for uptime, used memory, active processes, and screen resolution.

  • bad_apple: A playback of the famous music video.

  • cube: A smoothly rotating 3D wireframe render using <math.h> sine/cosine functions.

  • benchmark: A CPU and Memory stress test that measures virtual instructions-per-second.



What’s Next

Still working on a few things:

  • Parallel Luau for multithreading.

This project took a LOT of time and coffee, but it was, in my opinion, worth it to push what is possible in Roblox. You don’t need to understand all of this to play with it, but if you like low-level systems, there’s a lot to dig into. If you have questions about how any of this works, feel free to ask.

Thanks for reading!

TL;DR: I built a full virtual computer and operating system inside Roblox. It has a custom CPU, C compiler, assembler, persistent filesystem, and shell. I even managed to port a fully playable DOOM clone to run entirely within the VM.

46 Likes

Looks very cool! im interested to try this out! if its released ofc…

2 Likes

Thanks! Not released just yet, but that’s definitely the plan. I want to clean a few things up and make it more user-friendly first. I’ll try to keep this thread updated with what I add, and when it is playable I will be sure to post.

1 Like

Im excited to see jow this goes! It’ll be interesting to try out!

1 Like

While you’re at it, add Bad Apple to the list

1 Like

Haha yes!! How could I forget?

1 Like

wish i could use editablemesh and editableimage api, of course they make it 13+
and i cant wait until the day i cant use devforum too

i was right u haven’t verified, sorry to see u go :confused:

:sob:

Progress update 2/9/2026

Hello! This is the first update on ROVM and will cover:

  • compiler
  • release date
  • open source?

For the past week I have been working hard on a C compiler for the ROVM architecture. It is being written fully in assembly and will come pre-installed in the release. The compiler outputs ROVM assembly into a temporary file, which is then assembled into machine code. All core logic is complete; what remains is debugging and fixing edge cases.

As for the release date… hopefully around mid February if all goes well. It will not be a full release by any means but just something to play around with.

Alright, now onto the elephant in the room: I DO plan on making it open source. The project has grown very organically and the codebase is currently… not pretty. Before open sourcing it, I want to do some organization and add documentation so it’s actually readable for anyone curious about how it works. Once that’s done, I’ll be posting it on my GitHub.

Thank you for all of the support, and I am looking forward to reading your feedback.

2 Likes

I’m just wondering, what made you decide to build a custom ASM separate from more standard ones like RISC-V?

1 Like

I thought about that a lot before building it. it mostly comes down to what i actually want this project to be. I’m not trying to make a cool demo people look at then forget, I want something people can actually mess with.

Using something like x86 or RISC-V would add a ton of complexity, and honestly the fun is designing my own thing from scratch. Also, decoding a clean, consistent 32 bit format in a script is way nicer than dealing with all the bit shuffling and weird stuff you would run into if trying to simulate a real world architecture.

That said, i’d still love to see someone go all in and make a crazy complex cpu in roblox.

then that’s great for your purpose.

i’ve seen people make RISC-V linux emulation in roblox.

Oh yeah, i remember seeing that. Wish the game was still up, as i’m pretty sure it’s down. Still though, extremely impressive.

2 Likes

Progress update 2/22/2026

Hello! This quick post will summarize what I’ve done since the last update.

  • Bad Apple
  • Release date?
  • C Compiler

After a whole lot of pain I finally got bad apple to run inside of my VM. For more info, go here.


Yes, yes, I know I said I would publish the game mid February, but I’ve been struggling.
Optimization has been a constant battle. I fix one bug, aand another appears. After going in circles for a while, I decided to stop chasing perfection and just publish.

ROVM will be released to the public before the end of the month.

It won’t include everything I originally planned (like a good compiler or games), but it will be fully usable and expandable. I will continue to update it.


The C compiler is now semi-working.
It can parse C and produce valid assembly for the VM. It’s extremely limited right now, but it works. Expanding it into something more complete is going to take a hell of a lot of time, as parsing and proper codegen are no joke.

Thank you all for sticking through this project.
As always, I’d love to hear suggestions or answer any questions.

3 Likes

Progress update 3/1/2026

Finally, a playable version! I mentioned everything in the main post update, but for some of you here are the main additions:

I decided to quit the assembly-written compiler, as that was unrealistic and would take too much time. I instead made the compiler in Lua, which was way easier. The compiler doesn’t spit out machine code, it first does assembly then sends that to the assembler. Pretty sweet!

I did a major refactor to make the system byte-addressable instead of word-addressable. This made the system much more realistic. If you want to know more about what this really means, this would explain it better than i could., but in short byte addressability means pointers now point to individual 8-bit bytes rather than 32-bit chunks, which is essential for standard C string and memory operations.

I decided to push myself and also created some little games, like basic DOOM, in C. I also rewrote all previous assembly programs in C to not hate myself in the future. This helped a lot, but sucked to do.

Here is the game link for anyone wanting to try:

Thank you all for the support, and I would love to hear feedback!

1 Like

Since this game is purely UI, there’s no need to run the PlayerModule or have a 3D environment.

The terminal font is incredibly hard to read. Also, the cd command seems to break after running a program. Rebooting the OS fixes the bug until you run another program.

9 FPS when playing doom..!

The highlighted character in the editor shows the wrong character after line 99. Also very bad UX that I can’t click where I want to type.

2 Likes

Thank you for the detailed bug report! I have fixed most of your issues, and as for the FPS in doom, I lowered graphics to reduce the number of rays, and performance increased quite a bit.

If you have used vim before, the point is that you don’t have to leave your keyboard while coding. I see how this may be jarring, which is why I added the command set mouse=a. This allows clicking in the editor. I would recommend to just use the keyboard, though!

Again, thank you for reporting all of this, it is highly appreciated.

“Hmm, theres a bug in the entry point, let’s go fix it!”

Couldn’t figure out how to actually edit the script, but it took 40 seconds to scroll to the bottom. You also often need to scroll up to see what functions, variables, and other logic exist. If mouse is implemented as an input option for the terminal, it should be the default! We’re Roblox users, not patient terminal experts.

Alright, alright I see your point. I made the ide a lot more user friendly. You can now scroll by clicking the side bar, highlight text, and ctrl z and y. Mouse control is also enabled by default.

If you notice anything else, feel free to report it here!

2 Likes