A gameboy emulator in several different languages

Related tags

Game rosettaboy
Overview

RosettaBoy

Trying to implement a gameboy emulator in a bunch of languages for my own amusement and education; also giving people an opportunity to compare the same code written in different languages, similar to Rosetta Code but with a non-trivial codebase :)

The main goals are:

  • Readability of the code
  • Consistency across langauges
  • Idiomatic use of language features
  • Basic playability

Notably, 100% accuracy is not a goal - if Tetris works perfectly then I'm happy, if other games require more obscure hardware features, then I'll weigh up whether or not the feature is worth the complexity.

Also yes, "consistent across languages" and "idiomatic" can be at odds - there are subjective compromises to be made, but for the most part that doesn't seem to be a huge problem.

So far all the implementations follow a fairly standard layout, with each module teaching me how to do a new thing. In fact they're all so similar, I wrote one copy of the documentation for all the implementations:

  • main: argument parsing
  • cpu: CPU emulation
  • gpu: graphical processing
  • apu: audio processing
  • buttons: user input
  • cart: binary file I/O and parsing
  • clock: timing / sleeping
  • consts: lists of constant values
  • ram: array access where some array values are special

Pull requests to translate into new languages, or fleshing out existing languages, are very welcome :)

Completeness

Feature Python C++ Rust Go
gblargh's CPU test suite
silent / headless
scaled output
CPU logging
keyboard input
gamepad input
turbo button
audio off-key glitchy
memory mapping
scanline rendering
bank swapping ? ? ?
CPU interrupts
GPU interrupts

Benchmarks

Warning: These implementations aren't fully in-sync, so take numbers with a large grain of salt. For example: the Python implementation uses native code to blit whole 8x8 sprites in one go, while the other languages do one pixel at a time (which is more correct, and necessary for things like parallax effects), which means that the python version is unfairly fast.

Measurements are FPS, done with --turbo --profile 3600 opus5.gb (measuring how much physical time it takes to emulate 60 seconds of game time), on a Macbook Pro 2019 (2.4 GHz 8-Core Intel Core i9). If somebody knows how to measure CPU instructions instead of clock time, that seems fairer; especially if we can get the measurement included automatically via github actions. Pull requests welcome :)

I also wonder if there's something happening in SDL which is capping frame rates to 60fps when a window is displayed...

Feature Python C++ Rust Go
release, silent, headless 5 1000 1800
debug, silent, headless 5 220 80
release, silent 5 60 60
Comments
  • avoid generic exceptions

    avoid generic exceptions

    rather than raise Exception("Invalid write to address %04X" % addr) we should use a specific exception like raise InvalidWriteAddress(addr)

    • [x] c++
    • [x] python
    • [x] rust
    • [x] go
    • [x] php

    For languages which support inheritance, I'd do something like:

    • Exception
      • ControlledExit - emulation should stop for some reason (exit 0, except for UnitTestFailed, which should exit 2)
        • Quit - the user hit the X button on the window
        • Timeout - we reached as far as we wanted to reach with the --profile flag
        • UnitTestPassed - Opcode 0xFC was executed
        • UnitTestFailed - Opcode 0xFD was executed
      • GameException - bugs in the game itself (exit 3)
        • InvalidOpcode
        • InvalidRomBank
        • InvalidRomRead
        • InvalidRomWrite
        • InvalidRamBank
        • InvalidRamRead
        • InvalidRamWrite
      • UserException - the user gave us some sort of invalid input (exit 4)
        • RomMissing - the .gb file specified on the command line doesn't exit
        • HeaderChecksumFailed - the .gb file specified has an invalid checksum
        • LogoChecksumFailed - the .gb file specified has an invalid checksum
    rust good first issue c++ python go php 
    opened by shish 10
  • Add `shell.nix` files for developer environments.

    Add `shell.nix` files for developer environments.

    Implements #57.

    This enables two usage modes:

    1. If you run nix-shell in any of the implementation subdirectories, you get a shell with correct versions of everything needed to build and run for that language.
    2. If you run nix-shell in the root directory, you get a shell with everything needed for all implementations.

    I tested it on a clean Debian 11 Bullseye Stable container with Nix 2.3.7, and also on an Arch-based rolling distro with Nix 2.11.1. The format and build scripts work for all languages (though the Zig output doesn't run on Debian Stable, due to an unrelated(?) issue of GLIBC being too old).

    Since most of the languages are actually fairly mature and stable on their own, I only pinned Zig to a specific language interpreter/compiler version. Everything else just uses the latest versions from the users' <nixpkgs> channel, though with a major version qualifier if relevant (E.G. python310, php81). It is trivial to pin everything by changing the import argument to a specific commit in the NixPkgs repository, though, if that becomes a problem, but doing so might mean giving up the binary cache and requiring the user to build everything from source.


    Compare with @rrbutani's approach: https://github.com/shish/rosettaboy/issues/57#issuecomment-1332564900

    The above (@rrbutani's WIP) uses Nix Flakes. As I understand it, flakes are the more advanced and more reproducible, technically still-experimental way to specify code dependencies. I think the flake above not only fetches dependencies but also builds each implementation? My goal was only to define shells that allow the existing run.sh and format.sh scripts to be used.

    Personally I'm not convinced that the complexity, schema, whatever of Flakes is worth it compared to ad-hoc derivations and shells unless you're targeting a completely Nix-based platform. I'm still quite new to Nix though, and I'm sure @rrbutani can comment more on the merits of each approach.


    Things I did not do:

    • Github Workflow to test the Nix environments. I personally found the GH Workflow API annoying the last time I interacted with it. Plus, I can't test this locally. Should be easy, though— Can just do nix-shell --pure --run './utils/bench.py --default --frames 10 --threads 4' (or whatever the test script is).
    opened by will-ca 7
  • Use Nix for reproducible environments?

    Use Nix for reproducible environments?

    I got the below error trying to run the Zig version:

    ./build.zig:22:8: error: no member named 'addLibraryPath' in struct 'std.build.LibExeObjStep'
        exe.addLibraryPath("/Users/shish2k/homebrew/lib/");
           ^
    ./lib/sdl/Sdk.zig:139:63: error: expected type 'std.mem.Allocator', found 'std.zig.CrossTarget'
        const target = (std.zig.system.NativeTargetInfo.detect(exe.target) catch @panic("failed to detect native target info!")).target;
                                                                  ^
    /usr/lib/zig/std/mem/Allocator.zig:1:1: note: std.mem.Allocator declared here
    //! The standard memory allocation interface.
    ^
    /usr/lib/zig/std/zig/CrossTarget.zig:1:1: note: std.zig.CrossTarget declared here
    //! Contains all the same data as `Target`, additionally introducing the concept of "the native target".
    ^
    ./lib/sdl/Sdk.zig:86:9: error: no member named 'source' in struct 'std.build.Pkg'
            .source = .{ .path = sdkPath("/src/wrapper/sdl.zig") },
            ^
    

    Rather than endlessly trying to keep track of libraries, version incompatibilities, etc., I think Nix can be used to declaratively define reproducible, self-contained development environments as needed.

    For example, I have the below shell.nix file for the Python version:

    { pkgs ? import <nixpkgs> {} } : pkgs.mkShell {
    	buildInputs = with pkgs; [
    		(python310.withPackages (pypkgs: with pypkgs; [
    			pysdl2
    			setuptools
    			
    			mypy
    			black
    		])).out
    	];
    }
    

    (To read this if you haven't looked into Nix before: The language is functional. Colons are function definitions, with signature to the left and the return expression to the right. Calls are names followed by a space and then argument. With makes names from a mapping available in the subsequent scope. Lists are whitespace-separated, and curly braces are mappings. — The root expression is a function that takes the argument pkgs (or actually uses the default value import <nixpkgs> {}) and returns an environment, and the argument passed to .withPackages is a function that takes the argument pypkgs and returns a list of Python packages.)

    And then, anyone on any distro of Linux or MacOS can just run $ nix-shell in the terminal to get the exact right development environment. The packages are stored entirely self-contained in /nix, and symlinked in ~/.nix-profile or added to $PATH as requested.

    For perfect reproducibility, the packages used can be pinned to a specific version by downloading from a specific commit in the nixpkgs respository. So this way, if you specify a version of a library or language that works on your machine, anyone else running it will get the exact same version.

    { pkgs ? import (fetchTarball "https://github.com/NixOS/nixpkgs/archive/facfd565093c8f410be840dc427975a6d7368f91.tar.gz") {} } : pkgs.mkShell {
    	buildInputs = […];
    }
    

    More info:

    https://nixos.org/guides/declarative-and-reproducible-developer-environments.html

    I'm not trying to evangelize. I bring this up as somebody who is reluctant to modify my global computing environment for a local requirement— I.E. I'm not going to change my system Zig, set up venvs everywhere, or install new pip packages just to run this, so I was just thinking it'd be useful if a declarative ad-hoc environment were provided.

    I do see there's already something similar with Docker/Podman, which I guess fills the same purpose. But that seems heavier (pulling in a whole Debian, looks like?), and involves more permissions and system configuration to set up. — FWIW, Nix can also be used to generate Docker images, and it looks like there's a Nix image on Docker (though I haven't tried either).

    Anyway, it's just an idea.

    enhancement common 
    opened by will-ca 7
  • [nim] modern compile flags

    [nim] modern compile flags

    Running on my machine using daa54a6de67c6c7f21125372afb52c311ddd8954: Emulated 600 frames in 0.53s (1136fps)

    WIth this change: Emulated 600 frames in 0.38s (1568fps)

    1. --mm:arc
    2. "The benefit of --panics:on is that it produces smaller binary code and the compiler has more freedom to optimize the code."
    opened by n5m 6
  • Make docker work fully

    Make docker work fully

    [email protected]:~/Projects/rosettaboy$ ./utils/shell.sh
    [email protected]:/home/dev$ ./utils/bench.py 
      cpp / release: 
       go / release: Emulated 600 frames in  2.07s (290fps)
      nim / release: Emulated 600 frames in  0.30s (1969fps)
      php / release: Emulated 600 frames in 11.16s (54fps)
       py / release: Emulated 600 frames in 60.24s (10fps)
       py / mypyc  : Emulated 600 frames in 56.73s (11fps)
       rs / release: Emulated 600 frames in  0.26s (2302fps)
       rs / pgo    : 
      zig / release: 
    

    cpp crashes

    rust pgo is probably missing something

    zig compiler isn't installed

    rust 
    opened by shish 5
  • `run_mypyc.sh` does not really use `mypyc`?

    `run_mypyc.sh` does not really use `mypyc`?

    Since mypyc compiles modules, it probably needs to be run on all src/*.py files. Doing only mypyc main.py probably only compiles the entrypoint.

    E.G.:

    • If you delete one of the files like src/cpu.py, mypyc still compiles successfully but ModuleNotFoundErrors at run time. This presumably shows it's still trying to import the slow, uncompiled cpu.py.
    • Looks like you can open build/__native.c to read the generated code. It's not pretty, but it doesn't look like it's doing anything more than importing the uncompiled modules, running them, and exiting.
    • Docs show separately specifying all modules that you want to compile.

    No idea if it will even work if you try to compile everything— E.G. To get it running in Cython, I had to separate all the exec()-generated code in cpu.py out into actual code instead (....Which I see has been removed and made obsolete literally the day after I did that.... Gosh dangit!)— ~~If that's needed for MyPy too, I can post that as a starting point.~~

    python 
    opened by will-ca 3
  • In php github workflow not use sdl2 lib binding

    In php github workflow not use sdl2 lib binding

    Hi, I just saw your comment on top of ycombinator (congrats!), checking out this repo I notice that in all github workflow we are using sdl2 lib binding, but in php github workflow I didn't see it being use (It is still actively maintained at https://github.com/Ponup/php-sdl).

    That may yield why php is roughly the same speed with python (it is faster in my experience).

    I've never use SDL2 for php so I don't yet know how, creating this issue for tracking (as I might try SDL with php on weekend).

    php 
    opened by kocoten1992 3
  • Use operator overloading for nice RAM access

    Use operator overloading for nice RAM access

    Rather than

    u8 IE = this->ram->get(Mem::IE);
    

    it would be nice to treat RAM as if it was an array, like:

    u8 IE = this->ram[Mem::IE];
    
    • [x] Python
    • [ ] C++
    • [ ] Rust - https://doc.rust-lang.org/core/ops/trait.Index.html & https://doc.rust-lang.org/core/ops/trait.IndexMut.html

    (Do Go or PHP support this?)

    rust c++ 
    opened by shish 2
  • Use SDL Textures

    Use SDL Textures

    Right now most implementations use the Surface API (software rendering), except for rust which uses the Renderer API (hardware accelerated, except in our case that just means "additional overhead on every setpixel() call") - the docs say that for our use case (we are setting every pixel individually) the Texture API is best

    https://wiki.libsdl.org/MigrationGuide#if_your_game_just_wants_to_get_fully_rendered_frames_to_the_screen

    python 
    opened by shish 2
  • [pxd] Add Cython implementation.

    [pxd] Add Cython implementation.

    This adds a Cythonized pxd copy of the py implementation.

    The pxd implementation shoots Pythonic performance up past PHP to a bit below Go, but not quite yet to the level of any of the other native-targeting languages.

    I aimed for a mostly naïve use of Cython. Basically, all logic is exactly the same as the Python version, but type annotations and definitions are added to compile all costly code to C.

    Looking at the annotated HTML output produced during a build, essentially all of the emulation logic now runs as native. Only some of the __init__s, the errors, and debug stuff still do a lot of stuff in Python-space, which I didn't bother with as I assume they're usually run only a couple times at most in the lifetime of the emulator. SDL2 is also linked directly from the generated C code (based on the project sdl2_cython on PyPI), rather than used through pysdl2— This was surprisingly painless to drop in, and only required a couple minor changes for some struct types and decisions in Python-space on how to handle arrays.

    The individual commit messages say how much of a speedup each change brought on my machine.

    The Python sources still run fine in CPython as well. I don't know if it might be possible to unify this with the py implementation so there's only one code set to maintain that works with CPython, MyPyC, and also Cython. This implementation is logically basically identical, but Cython uses some special types that may or may not play nice with MyPy, and doesn't support match yet (although apparently match is nearly identical to elif chains in CPython bytecode anyway).

    I haven't tested this with Python 3.11.

    Cython also has a number of options that may affect the output. Most notably, Cython can output C++ instead of C, which enables access to a handful of extra features. This PR does not include variations for any of those options, although I don't suppose compiling largely the same code as C++ instead of C would really make much of a difference.

    When reviewing: It would make sense to ignore the first commit, as it's just a copy of py, as well as the SDL2 header wrapper that adds a lot of lines.

    It's not immediately clear to me exactly why this is still significantly slower than the other native binaries. For example, profiling points to RAM.get() being the largest single cost, but comparing that code with other implementations side-by-side, they look essentially identical. I originally thought Pythonic code might leave a lot of superfluous allocations around, but that doesn't really look like the case either. Cython seems to be using gcc -O3, so that should be fine too, unless the generated code is somehow much harder to optimize. Is the overall call graph significantly different?

    opened by will-ca 1
  • Small typing mistake in `gpu.py` I think.

    Small typing mistake in `gpu.py` I think.

    IIRC I was thrown off a bit by this.

    palette in GPU.paint_tile() and GPU.paint_tile_line() is typed to SDL_Color.

    https://github.com/shish/rosettaboy/blob/177f282c7b0e10eb5df0e0597d966a7ccc8e9ab9/py/src/gpu.py#L365-L372

    https://github.com/shish/rosettaboy/blob/177f282c7b0e10eb5df0e0597d966a7ccc8e9ab9/py/src/gpu.py#L388-L396

    However, in all invocations, it's set to something like self.bgp, self.obp1, or self.obp0, which are actually List[SDL_Color]:

    https://github.com/shish/rosettaboy/blob/177f282c7b0e10eb5df0e0597d966a7ccc8e9ab9/py/src/gpu.py#L325

    https://github.com/shish/rosettaboy/blob/177f282c7b0e10eb5df0e0597d966a7ccc8e9ab9/py/src/gpu.py#L344-L353

    https://github.com/shish/rosettaboy/blob/177f282c7b0e10eb5df0e0597d966a7ccc8e9ab9/py/src/gpu.py#L205-L228

    opened by will-ca 1
  • Frame time distribution reporting?

    Frame time distribution reporting?

    In soft real-time and interactive uses, performance consistency is arguably more important than mean performance.

    60FPS with constant frame times is way better than 200FPS with a lag spike every couple hundred frames. Apparently, even some quite large games and GUI applications can have quite bad issues with the entire process freezing every couple seconds when the garbage collector kicks in.

    Complication: Nim has multiple memory management strategies that can be used. I don't know if some of the other languages might too. I suppose these would probably be best exposed as run_arc.sh, run_boehm.sh, etc., similar to how there's already run_speed.sh, run_mypyc.sh, run_lto.sh, etc.

    Obviously results from this should be taken with a grain of salt the size of a boulder, as small implementation details could completely change them. But still, I could see it as an at-a-glance useful way to judge what you're likely going to be able to get out of each language if you jump in without taking special measures, for which I have not yet seen any previously existing empirical data or comparison.

    Possible components:

    • Output format? Maybe just dump a JSON array of frame times to disk without any additional processing.
    • Option to sample only after a warm-up period? So skip startup, and give JITs time to get going. Hm; I guess if it's implemented as something like --frametimes 1000 meaning "Keep only the last 1,000 frame times," then we can also avoid having to ever reallocate for the internal frame time array. (And then you could do, E.G. --frametimes 1000 --frames 1200 to get 1000 frame times after 200 warm-up frames.)
    • ./utils/frametimes.py <LANG>/frametimes.json to bin the times and show a MatPlotLib histogram?
    • Would it be worth it to make bench.py report on deviations/quantiles when available too?

    I considered PR'ing this directly or trying to start it myself, but I suppose there's enough different ways it could be done and reasons to do or not do it, and I'm not proficient enough, that it should probably be discussed more thoroughly.

    common 
    opened by will-ca 1
  • [rs] an attempt at benchmarking tick()

    [rs] an attempt at benchmarking tick()

    An experiment for making it easier to see if changes are helping or hindering

    exposes the code as a library, with args and gameboy marked as public

    run with cargo bench

    opened by shish 0
Owner
Shish
Shish
DMGPlus, upgraded internals for the original GameBoy

These are various files for the DMGPlus project. Note that I did this over the time span of about three years, so there's no plug-and-play recipe to b

Jeroen Domburg 14 Dec 19, 2021
Realtime strategy game for Gameboy Advance

Skyland Overview A Gameboy Advance game created, frantically, for a game jam, in about twenty days. A simple realtime strategy game where you build fl

Evan Bowman 37 Dec 27, 2022
GameBoy Advance dungeon crawling game

inheritors-of-the-oubliette GameBoy Advance dungeon crawling game Compilation instructions: have devkitpro installed with all the GBA dev stuff. Also

null 13 Dec 12, 2022
Gameboy Port of LazyDevs' Porklike

porklike.gb A port of the wonderful Porklike, created by Krystian Majewski, to the original Game Boy! Please see the original pico-8 game at https://k

Ben Smith 20 Dec 28, 2022
A Game Boy game that rewards you for playing it on several console models!

GB Corp. A Game Boy game for the Game Boy Competition 2021 by Dr. Ludos (2021) This is the source code, you can get a precompiled rom from here: https

Dr. Ludos 10 Sep 25, 2022
a game of packing problems - several thousand of them, to be exact

myriad myriad is a game of packing problems -- several thousand of them, to be exact. install you can compile the game using gcc: gcc -Wall -lncurses

Lux L. 5 Dec 21, 2021
Game Scripting Languages benchmarked

Scriptorium ?? Game Scripting Languages benchmarked. Using latest versions at the time of writing (Jul 2015) Total solutions evaluated: 50 Results Ran

null 482 Jan 7, 2023
This is a list of different open-source video games and commercial video games open-source remakes.

This is a list of different open-source video games and commercial video games open-source remakes.

Ivan Bobev 173 Jan 2, 2023
A Binary Clock. Written 3 different ways. C and SDL, Python and PyGame, Python and PyGame Zero.

Super Clock A Binary Clock. Written 3 different ways. Python with PyGame Zero, Python with PyGame and C with SDL2. Time is displayed in 3 groups of 8

null 3 Dec 8, 2021
A cycle-accurate Game Boy and Game Boy Color Emulator, with rewind feature.

Azayaka is a free and open-source Game Boy and Game Boy Color emulator written in C++. Features Cycle-Accurate emulation. Console based Debugg

Zach Collins 23 Dec 3, 2022
MVS64 -- A NeoGeo emulator for Nintendo 64

MVS64 -- A NeoGeo emulator for Nintendo 64 Status This emulator is in VERY EARLY STAGE. Only a handful of games boot or work. Performance on N64 is st

Giovanni Bajo 24 Nov 9, 2022
yuzu is an experimental open-source emulator for the Nintendo Switch from the creators of Citra.

yuzu (Nintendo Switch Emulator) yuzu is an experimental open-source emulator for the Nintendo Switch from the creators of Citra. It is written in C++

null 537 Dec 29, 2022
Experimental Nintendo 64 emulator in C11

shibumi | 渋味 "Shibui (adjective), shibumi (noun) or shibusa (noun) are Japanese words that refer to a particular aesthetic or beauty that is simple, s

Simone Coco 10 Dec 5, 2022
Modification of Marat Fayzullin's Z80 emulator to run on the Arduino Mega

Z80 Core for Arduino Mega This library only functions with the Arduino Mega or Mega 2560 due to memory constraints! This is a free, open source projec

Jack Kingsman 8 Sep 10, 2022
SameBoy DX is a Qt-based interface of SameBoy, a free, highly accurate Game Boy and Game Boy Color emulator.

SameBoy DX SameBoy DX is a Qt-based interface of SameBoy, a free, highly accurate Game Boy and Game Boy Color emulator. Build requirements: CMake Pyth

Snowy 7 Oct 2, 2022
A Game Boy emulator with serial data transfer (link cable) support over tcp.

gbmulator A Game Boy emulator with serial data transfer (link cable) support over tcp. Key bindings Key bindings are not configurable yet. Key Action

null 6 Dec 30, 2022
bsnes is a Super Nintendo (SNES) emulator focused on performance, features, and ease of use.

bsnes is a Super Nintendo (SNES) emulator focused on performance, features, and ease of use.

bsnes 1.3k Jan 4, 2023
Game Boy, Game Boy Color, and Game Boy Advanced Emulator

SkyEmu SkyEmu is low level cycle accurate GameBoy, GameBoy Color and Game Boy Advance emulator that I have been developing in my spare time. Its prima

Sky 321 Jan 4, 2023
NeoGB Printer an SD card-based standalone Game Boy Printer emulator.

An open-source and standalone Gameboy Printer emulator 100% compatible with all officially released games (110 in total) that support the accessory. Just print and save the images as BMP

Rafael Zenaro 85 Dec 22, 2022