Running JavaScript on the GameBoy Advance

The GameBoy is a really interesting handheld console that I often enjoy hacking around on. During one of my internships, I got the chance to work on an IoT JavaScript engine named JerryScript, which happens to be a former Samsung project. So putting the two pieces together I thought: is it possible I could get JavaScript running on it? Here's how I did it.

GBATek is the documentation resource I used for hardware specifications and software interrupts.

You can find the game's repository here: https://github.com/AnthonyCalandra/gba-js

Installing devkitARM

devkitPro is the organization that provides devkitARM, the toolchain we'll be using to build GameBoy Advance ROMs. Installing devkitARM is a required step and the instructions to do so can be found here: https://devkitpro.org/wiki/Getting_Started.

Building JerryScript

I cloned the latest version of JerryScript from their GitHub page (as of May 25th) and followed the instructions for building the engine.

I needed to provide a toolchain for Jerry so that it knew to build using devkitARM's compiler. To do this, create a new CMake script in the cmake directory named toolchain_gba_armv7tdmi.cmake with the following content:

1
2
3
4
5
6
7
8
9
10
11
12
set(DEVKITARM $ENV{DEVKITARM})
 
if(NOT DEVKITARM)
    message(FATAL_ERROR "DEVKITARM environment variable not set")
endif()
 
set(CMAKE_SYSTEM_NAME GBA)
set(CMAKE_SYSTEM_PROCESSOR arm7tdmi)
 
set(CMAKE_C_COMPILER_WORKS TRUE)
set(CMAKE_C_COMPILER ${DEVKITARM}/bin/arm-none-eabi-gcc)
set(FLAGS_COMMON_ARCH -mcpu=arm7tdmi -mtune=arm7tdmi -mthumb -fomit-frame-pointer)

This sets the ARM compiler and provides the necessary flags so it's compiled for the GameBoy Advance's CPU architecture.

Once that was completed, I found that the following build command worked for me:

1
python tools/build.py --toolchain=cmake/toolchain_gba_armv7tdmi.cmake --lto=off --jerry-cmdline=off --clean --mem-heap=64 --compile-flag=-Os --profile=es2015-subset --strip=on --jerry-cmdline-snapshot=off

A couple things about these flags:

  • The engine's heap is set to 64K, which can probably be set higher since the GBA has 288K of RAM to work with (specifically, internal working RAM (IWRAM) which is 32K fast on-chip memory, and external working RAM (EWRAM) which is the remaining 256K.
  • LTO (link-time optimization) is turned off because I'm building on OSX. See this issue for details.

Once this step is completed, cd to the build directory and run make install. You can find where the static libraries and header files are installed to by running the following command:

1
2
> pkg-config --cflags --libs libjerry-core libjerry-port-default libjerry-libm
-I/usr/local/include -I/usr/local/include/jerry-libm -L/usr/local/lib -ljerry-core -ljerry-port-default -ljerry-libm

Nothing more needs to be done as the Makefile in the GBA project will place the appropriate compiler/linker flags to pull these libraries in.

Building the game

So my objective is to write a very simple pong game, with the game logic written in full JavaScript. The game would sit on top of the collection of GBA modules for IO, graphics, etc. and the JerryScript engine, both of which sit on top of the hardware.

The first step is to properly link in the JavaScript engine. Here's some starter code to initialize, parse and run some code, and cleanup:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
#include <jerryscript.h>
 
static const jerry_char_t script[] = " \
 const foo = 'hello world!'; \
";
static const jerry_length_t script_size = sizeof(script) - 1;
 
int main()
{
    jerry_init(JERRY_INIT_EMPTY);
 
    jerry_value_t eval_value = jerry_eval(script, script_size, JERRY_PARSE_NO_OPTS);
 
    while (1);
 
    jerry_release_value(eval_value);
    jerry_cleanup();
    return 0;
}

A few things to unpack here:

  • Our JavaScript code lives in an array of chars (for simplicity, think of jerry_char_t as just a char). Why? The GameBoy Advance doesn't have a filesystem for us to be able to simply call fopen on and read from -- there's no OS! So we need a way to embed our JavaScript code into the ROM and read from without having a filesystem. Placing the code in a char array will place that string into the .rodata section of our binary, which belongs in the ROM. The ROM is an optimal place for this, because that's where the linker places our executable code, and this section is also the biggest. According to GBATek, there's 32MB of ROM space to work with.
  • We initialize the engine using jerry_init and do a parse and evaluate in jerry_eval. Cleanup is done by calling jerry_cleanup.
  • For every value that lives in the engine which we get access to outside of the engine's execution environment, we must "release" the value using jerry_release_value so that it can get garbage collected by the engine later. This is necessary because when we access a value outside the engine, we don't want it GC'd if we're still using it.

Compiling this simple program doesn't work however:

1
2
3
4
5
6
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124a4 of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.data' is not within region `iwram'
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124a8 of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.init_array' is not within region `iwram'
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124ac of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.fini_array' is not within region `iwram'
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124a4 of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.data' is not within region `iwram'
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124a8 of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.init_array' is not within region `iwram'
/opt/devkitpro/devkitARM/bin/../lib/gcc/arm-none-eabi/9.1.0/../../../../arm-none-eabi/bin/ld: address 0x30124ac of /Users/anthonycalandra/Projects/gba-js/bin/gba_js.elf section `.fini_array' is not within region `iwram'

This is a cryptic way of saying we don't have enough room in IWRAM for our code. The .data segment contains any global or static variables which have a pre-defined value and can be modified. The .init_array and .fini_array segments have also overflowed outside of IWRAM. We can see this by digging into the mapfile. Recall that IWRAM has a size of 32K and begins at physical address 0x3000000, thus IWRAM should end at 0x3008000. Looking at the mapfile:

1
2
0x0000000003000000                __iwram_start = ORIGIN (iwram)
0x0000000003008000                __iwram_top = (ORIGIN (iwram) + LENGTH (iwram))
1
2
3
4
5
COMMON         0x00000000030000a8    0x10930 /usr/local/lib/libjerry-core.a(jcontext.c.obj)
               0x00000000030000a8                jerry_global_context
               0x00000000030009d8                jerry_global_heap
               0x00000000030109d8                . = ALIGN (0x4)
               0x00000000030109d8                __bss_end__ = ABSOLUTE (.)
1
0x00000000030124ac                __data_end__ = ABSOLUTE (.)

These three snippets tell the story. In the first snippet, IWRAM starts and ends exactly where we were expecting. In the second snippet, we can see where the culprit is, when jerry_global_heap swells past the top of IWRAM. Recall from earlier we set the heap size to 64K, and that's where this memory is being used up. The third snippet shows that the data segment ends way past the top of IWRAM, and thus explains the error message we receive from the linker.

So there are two solutions: either shrink the heap size when we build JerryScript to something small like 16K, or figure out where else the heap can be placed. The latter is ideal because we probably do not want all our fast RAM to belong to the engine, and on top of that, if our heap space is too small, the engine will constantly be garbage collecting which impacts performance. Heap allocations should be minimized as much as possible, especially on resource constrained hardware. So we'll make the tradeoff of having slower heap allocations but more memory to work with. If our heap isn't going to be placed in IWRAM, it should instead be placed in EWRAM, which has way more space (256K), but is also off-chip and thus a bit slower.

Using some special linker syntax, I made all data (non-code) segments belonging to static libraries (including JerryScript) be placed in EWRAM specifically; 32K of fast RAM is available outside the engine for us to use if we wish, while our EWRAM gets a bit smaller. Tuning the engine's heap to an appropriate size can be done here, but I'll keep it at 64K. The modified linker script is in the game repository named gba_cart.ld. At this time I don't know how to force the linker to use that linker script instead of the built-in one from devkitARM, so I simply replaced the default one with the new one.

Creating a JavaScript API layer

Now that the engine is able to execute a simple script, the next step is adding an API layer. The goal is to provide an API like the following:

1
2
3
4
5
6
7
8
9
10
11
12
13
const gba = require('gba');
 
// Graphics routines
gba.print(x, y, 'foo');
gba.drawRect(x0, y0, x1, y1, gba.colors.BLACK);
 
// IO routines
gba.isKeyDown(gba.keys.KEY_UP);
 
// Game loop
gba.onTick(() => {
  ...
});

This part is essentially just using the JerryScript API and calling into lower-level C code. Here's an example of how print was written:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
static jerry_value_t print_handler(
    const jerry_value_t function_obj,
    const jerry_value_t this_val,
    const jerry_value_t arguments[],
    const jerry_length_t argument_count)
{
    jerry_value_t err = jerry_create_boolean(false);
    if (argument_count < 3 ||
        !jerry_value_is_number(arguments[0]) ||
        !jerry_value_is_number(arguments[1]))
    {
        return err;
    }
 
    // Default to index 1 (black).
    uint32_t color_index = 1;
    // The message is either given as the third or fourth argument depending on if the optional
    // color argument was given.
    jerry_size_t str_index = 2;
    // Optional color argument given.
    if (jerry_value_is_number(arguments[2]))
    {
        color_index = (uint32_t) jerry_get_number_value(arguments[2]);
        if (color_index > MAX_PALETTES)
        {
            color_index = COLOR_BLACK;
        }
 
        str_index = 3;
    }
 
    if (argument_count <= str_index ||
        !jerry_value_is_string(arguments[str_index]))
    {
        return err;
    }
 
    int32_t x = (int32_t) jerry_get_number_value(arguments[0]);
    int32_t y = (int32_t) jerry_get_number_value(arguments[1]);
 
    jerry_char_t str_buf[256] = {0};
    JERRY_STRING_TO_CHAR_ARRAY(arguments[str_index], str_buf, sizeof(str_buf), err);
 
    gba_printf(x, y, color_index, (const char*) str_buf);
    jerry_release_value(err);
    return jerry_create_boolean(true);
}
 
static jerry_value_t create_gba_module()
{
    jerry_value_t module = jerry_create_object();
    // ...
    ADD_OBJECT_PROP_FN(module, "print", print_handler);
    // ...
    return module;
}

The full code can be found in the game's GitHub repository.

Writing the JavaScript

While the char array works for holding JS code, it makes writing the code quite tedious. There's no filesystem to load scripts from, so the script has to be embedded in the binary. One way to help make development easier is to embed the JS code into a C char array as part of the make process. I wrote a simple Python script that takes a JS script, and embeds its contents in a C array that lives in a header file, so the script can be #included when the engine needs to read the script in.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#!/usr/bin/env python3
import os, sys
from pathlib import Path
 
def generate_header(filename, js_path, js_header_dir):
    with open(os.path.join(js_path, filename), 'r') as js_file:
        with open(os.path.join(js_header_dir, 'jsapp.h'), 'w') as header_file:
            header_file.write('#pragma once\n')
            header_file.write('static const jerry_char_t script[] = ""\n')
            for line in js_file:
                header_file.write('"{}"\n'.format(line.strip('\n').replace('"', '\\"')))
 
            header_file.write(';\n')
            header_file.write('static const jerry_length_t script_size = sizeof(script) - 1;\n')
 
js_dir = sys.argv[1]
js_header_dir = sys.argv[2]
print('Converting Javascript to C headers ...')
for filename in os.listdir(js_dir):
    if filename.endswith('.js'):
        generate_header(filename, js_dir, js_header_dir)
 
print('Converting Javascript to C headers ... COMPLETE')

This isn't perfect, but it does the job.

Making this better could involve making bundling and minification part of this process: game code doesn't just live in one file, it can live in many files and get bundled into one JS file; and minifying code helps save space when it's embedded in the binary.

Conclusion

Thanks for sticking around! You can find the game's repository here: https://github.com/AnthonyCalandra/gba-js

Here's a picture of the game running on actual GameBoy Advance hardware:

Pong screen

There are opportunities for lots of optimizations here. I've intentionally slowed down the ball speed because my collision detection is awful; if the ball moves too fast, it often goes through paddles! So while the game is slow, it's possible with a bit of effort it can run much more smoothly.

Tags: gba, gameboy, javascript, gaming, embedded, programming