- An Arduous Endeavor (Part 1): Background and Yak Shaving
- An Arduous Endeavor (Part 2): Display Emulation
- An Arduous Endeavor (Part 3): CPU Emulation
- An Arduous Endeavor (Part 4): Input Handling
- An Arduous Endeavor (Part 5): Buzzes and Beeps
- An Arduous Endeavor (Part 6): Save States and Rewind
- An Arduous Endeavor (Part 7): Automated Builds
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:
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)); }
1 Comment