GameboyCore in the Web!

Happy new year!

I’ve been pretty busy the last few months, but there is a project I’ve had in mind for a long time now.

That would be creating an emulator that runs in the browser. Specifically I wanted to take GameboyCore and cross compile it to Webassembly and run it in a simple web app.

And well.. I’ve managed to get it working (In a not so nice looking React app)

Image not found!

It runs a lot faster than I expect as well.

In this post I’ll be going over the steps take to get this far.

Emscripten

Emscripten SDK is a LLVM to Javascript compiler. Specifically it can take an existing C/C++ code base and compile it to Javascript or Webassembly.

For this application I used Emscripten’s CMake toolchain to cross compile GameboyCore and Emscripten wrapper to Webassembly.

Setting up Emscripten

Download Emscripten:

git clone https://github.com/juj/emsdk.git

Activate latest Emscripten SDK:

cd emsdk

./emsdk install latest
./emsdk activate latest

This adds the Emscripten tools to your path.

Building Embind Wrapper

Next I created a CMake project that contains embind code for GameboyCore. Embind is used to interface C++ code with Javascript/Webassembly like Boost.Python, pybind11, luabind, etc.

Project structure:

gameboycore-web/
    src/
        external/
            gameboycore/
            CMakeLists.txt
        gameboycore_web.cpp
    CMakeLists.txt

Where src/external/gameboycore is a submodule pointing to GameboyCore.

Below in the CMakeList.txt for gameboycore-web.

CMakeLists.txt:

########################################################################################################################
### This is a Emscripten build for gameboycore-web
### Must invoke with CMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake
########################################################################################################################

cmake_minimum_required(VERSION 3.0.0)

project(gameboycore-web)

set(CMAKE_CXX_STANDARD 14)

set(EMSDK_ROOT $ENV{EMSCRIPTEN})
set(CMAKE_MODULE_PATH ${CMAKE_MODULE_PATH} "${EMSDK_ROOT}/cmake/Modules")
set(CMAKE_PREFIX_PATH ${CMAKE_PREFIX_PATH} "${EMSDK_ROOT}/cmake/Modules")

########################################################################################################################
### External Dependencies                                                                                            ###
########################################################################################################################

add_subdirectory(src/external)

########################################################################################################################
### GameboyCore Web                                                                                                  ###
########################################################################################################################

add_executable(${PROJECT_NAME}
    src/gameboycore_web.cpp
)

target_link_libraries(${PROJECT_NAME}
    # Link to GameboyCore
    gameboycore::gameboycore

    # Emscripten compiler options
    "-o ${CMAKE_CURRENT_SOURCE_DIR}/dist/gameboycore.js"
    "--bind"
    "-s WASM=1"
    "-s MODULARIZE=1"
    "-s EXPORT_ES6=1"
    "--post-js ${CMAKE_CURRENT_SOURCE_DIR}/src/js/post.js"
    "-s DISABLE_EXCEPTION_CATCHING=0"
    "-s ALLOW_MEMORY_GROWTH=1"
)

target_compile_definitions(${PROJECT_NAME} PRIVATE GAMEBOYCORE_STATIC=1)

src/external/CMakeLists.txt:

set(BUILD_TESTS OFF CACHE BOOL "Disable tests")
set(BUILD_TOOLS OFF CACHE BOOL "Disable tools")

add_subdirectory(gameboycore)

So a few things.

First, I append ${EMSDK_ROOT}/cmake/Modules to CMAKE_MODULE_PATH and CMAKE_PREFIX_PATH. This is necessary to build GameboyCore as Emscripten provides patches to make TestBigEndian and CheckTypeSize cmake modules work.

Next I included gameboycore via add_subdirectory(src/external) and added the gameboycore emscripten wrapper.

The call to target_link_libraries links GameboyCore to the Emscripten wrapper and sets some emcc flags. MODULARIZE and EXPORT_ES6 are import to make the output Javascript an imported module (I import it into Typescript later).

Emscripten Wrapper

To create an Emscripten binding for GameboyCore I needed to make a class that adapts the Emscripten library to the GameboyCore API.

The first thing to do in allow ROM file loading. The ROM file will be loading from disk by Javascript and passed to C++ code. The pointer is actaully passed as an int and needs to be casted to a pointer GameboyCore can work with.

Here is the loadROM function:

bool loadROM(const uintptr_t handle, size_t size)
{
    try
    {
        const auto buffer = reinterpret_cast<const uint8_t*>(handle);
        core_->loadROM(buffer, size);
    }
    catch(const std::runtime_error& e)
    {
        return false;
    }

    return true;
}

The code I used to load the ROM file into C++:

function loadFromArrayBuffer(core, buffer, length) {
    var ptr = Module._malloc(length);
    Module.HEAPU8.set(new Uint8Array(buffer), ptr);

    var result = core.loadROM(ptr, length);
    Module._free(ptr);

    return result;
}

One silly thing about embind is that arrays must be explictly defined include EACH index. For example, defining a simple 2 element array might look like the following.

    value_arrray<std::array<int, 2>>("array_int_2")
        .element(index<0>())
        .element(index<1>());

Which is fine for a two element array. In my case, GPU::Scanline is an array of pixels of size 160. You can’t call element in a for loop because the integer is a template argument. However I used a simple meta-programming quick to get it to work. A recurvice struct.

template<typename ArrayT, size_t N>
struct ArrayInitializer : public ArrayInitializer<ArrayT, N-1>
{
    explicit ArrayInitializer(emscripten::value_array<ArrayT>& arr) : ArrayInitializer<ArrayT, N-1>{arr}
    {
        arr.element(emscripten::index<N-1>());
    }
};

template<typename ArrayT>
struct ArrayInitializer<ArrayT, 0>
{
    explicit ArrayInitializer(emscripten::value_array<ArrayT>& arr)
    {
    }
};

...

// Register array of Pixels as a Scanline
value_array<GPU::Scanline> scanline_value_array("Scanline");
ArrayInitializer<GPU::Scanline, std::tuple_size<GPU::Scanline>::value>{scanline_value_array};

Building Emscripten Project

Configure the CMake project by setting CMAKE_TOOLCHAIN_FILE to $EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake

mkdir build && build
cmake .. -DCMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake

I did this on Windows, so I needed to specify a different build system to work with the emcc compiler (The default is Visual Studio on Windows). I used Ninja.

cmake .. -GNinja -DCMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN/cmake/Modules/Platform/Emscripten.cmake

Then build:

cmake --build .

This produces dist/gameboycore.js and dist/gameboycore.wasm.

React and Typescript

At this point the library is ready to use. However, to learn more about web development I decided to use Typescript and React.

Typescript is a superset of Javascript and can import any javascript code, assuming you have the necessary declaration file.

For GameboyCore that is:

declare module "gameboycore" {
    export interface Pixel {
        r: number;
        g: number;
        b: number;
    }

    export interface GameboyCore {
        new(): GameboyCore;
        loadROM(handle: number, length: number): void;
        setScanlineCallback(callback: (scanline: Pixel[], line: number) => void): void;
        setVBlankCallback(callback: () => void): void;
        emulateFrame(): void;
        release(): void;
    }

    export interface GameboyCoreJS {
        GameboyCore: GameboyCore;
        Pixel: Pixel,

        loadFromArrayBuffer(core: GameboyCore, buffer: ArrayBuffer, length: number): boolean;
    }

    export default function Module(emscriptenArgs: any): GameboyCoreJS;
}

GameboyCoreJS is an instance of Module generated by Emscripten.

And using the library

import gameboy_wasm from 'gameboycore/dist/gameboycore.wasm';

private initializeGameboyCoreRuntime() {
    import('gameboycore').then(gb => {
        if (gb != null) {
            const runtime = gb.default({
                locateFile: (filename: string, dir: string): string => {
                    if (filename === 'gameboycore.wasm') {
                        return gameboy_wasm;
                    }
                    else {
                        return "";
                    }
                },
                onRuntimeInitialized: () => {
                    console.log('GameboyCoreJS runtime has been initialized');
                    this.initializeCore(runtime);
                }
            });

            this.setState({gbjs: runtime});
        }
    });
}

private initializeCore(runtime: GameboyCoreJS) {
    const core = new runtime.GameboyCore();
    if (core != null) {
        console.log('GameboyCore has been instantiated');
    }
    else {
        console.log('Failed to instanitate GameboyCore object');
    }
    this.setState({core});
}

The module is dynamically loaded and the default function is called. The default function is defined in the gameboycore declaration file and is a reference to the Emscripten Module function.

Importing gameboycore.wasm is important as it will allow webpack to bundle the file. Notice how it is used in the locateFile function.

Future Work