An Arduous Endeavor (Part 2): Display Emulation

SSD 1306 OLED
This entry is part 2 of 7 in the series An Arduous Endeavor

There are a variety of components that need to be emulated to fully emulate the Arduboy. I decided to start with the display, since without that I wouldn’t be able to see much of anything.

The SSD1306 display is a standalone device with its own commands and data. I figured this would be a good warmup for writing the CPU emulator.

The SSD1306 supports at least three different ways of being addressed, but I decided to write my initial implementation from a higher level by focusing on the types of input it can receive: commands and data.

The commands allow for some pretty interesting stuff, including some effects that are handled at a “hardware” level. I’m unsure how many Arduboy games actually use these effects, though, as it appears that ProjectABE doesn’t support anything other than simple “horizontal” mode.

I wrote my implementation as a class with a bunch of private variables representing the various registers/state flags that seemed to be documented. I kept the public interface intentionally minimal, focusing on the methods pushCommand, pushData, tick (for an as yet unimplemented scrolling feature), getContrast, and getFrameBuffer. getFrameBuffer exposes the screen as a bitset, since that’s ultimately the only output the screen has.

I decided to make it the responsibility of libretro.cpp to convert that bitset into RGB pixels for actual display:

void update_video() {
    uint16_t fb[FRAME_WIDTH * FRAME_HEIGHT];
    memset(fb, BLACK, sizeof(uint16_t) * FRAME_WIDTH * FRAME_HEIGHT);
    auto bit_fb = arduous->getFrameBuffer();
    for (int y = 0; y < FRAME_HEIGHT; y++) {
        for (int x = 0; x < FRAME_WIDTH; x++) {
            fb[y * FRAME_WIDTH + x] = bit_fb[y * FRAME_WIDTH + x] ? WHITE : BLACK;
        }
    }
    video_cb((void*)fb, FRAME_WIDTH, FRAME_HEIGHT, FRAME_WIDTH * sizeof(uint16_t));
}

I took a look at the various commands listed in the datasheet document, and wrote methods that could process these commands, along with any additional data passed along. I think the code is pretty readable on its own.

To verify it worked, I hardcoded some simple commands and data to send as part of the retro_run calls.

std::vector<uint8_t> SCREEN_TEST_COMMANDS = {
    0x20, 0x00  // horizontal addressing mode
};

std::vector<std::vector<uint8_t>> SCREEN_TEST_DATA = {
    {0x01, 0x03, 0x07, 0x0F, 0x1F, 0x3F, 0x7F, 0xFF},
};

Arduous::Arduous() {
    cpu = Atcore();
    screen = SSD1306();
    for (uint8_t command : SCREEN_TEST_COMMANDS) {
        screen.pushCommand(command);
    }
    cpuTicksPerFrame = cpu.getDesc().clock / TIMING_FPS;
}

void Arduous::emulateFrame() {
    for (int i = 0; i < cpuTicksPerFrame; i++) {
        cpu.tick();
    }

    for (uint8_t data : SCREEN_TEST_DATA[SCREEN_TEST_DATA_PTR]) {
        screen.pushData(data);
    }
    SCREEN_TEST_DATA_PTR++;
    SCREEN_TEST_DATA_PTR %= SCREEN_TEST_DATA.size();

    screen.tick();
}

make and run with RetroArch, and I got some output:

Stair step

UPDATE

One oddity I want to document that is probably obvious to anyone with more experience in C/C++ programming: when pausing the emulator, I got some funky garbage data showing on the screen instead of my nice stairstep pattern:

It turns out this was due to my code allocating the fb array fresh inside each call of update_video. Aside from being pretty inefficient, this meant that RetroArch was hanging onto a reference to some deallocated memory. Moving the declaration of the array outside of the update_video function fixed the issue!

uint16_t fb[FRAME_WIDTH * FRAME_HEIGHT];

void update_video() {
    memset(fb, BLACK, sizeof(uint16_t) * FRAME_WIDTH * FRAME_HEIGHT);
    auto bit_fb = arduous->getFrameBuffer();
    for (int y = 0; y < FRAME_HEIGHT; y++) {
        for (int x = 0; x < FRAME_WIDTH; x++) {
            fb[y * FRAME_WIDTH + x] = bit_fb[y * FRAME_WIDTH + x] ? WHITE : BLACK;
        }
    }
    video_cb((void*)fb, FRAME_WIDTH, FRAME_HEIGHT, FRAME_WIDTH * sizeof(uint16_t));
}

Series Navigation<< An Arduous Endeavor (Part 1): Background and Yak ShavingAn Arduous Endeavor (Part 3): CPU Emulation >>

1 Comment

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.