High Resolution without gb.display

By aoneill, 5 years ago

Controls: D-Pad: [Arrows / WASD] - A: [J] - B: [K] - Menu: [U] - Home: [I]
Enjoy games at full speed with sound and lights on the Gamebuino META!
Emulator by aoneill

Disclaimer: This code may not be perfect, and there may be better ways to accomplish the same thing. However, I hope that it inspires some people to try pushing the capabilities of the Gamebuino. 

The emulator doesn't display this example correctly, so you may want to try it on your Gamebuino.

Here be dragons.

I have been working on some games that use the full 160px × 128px resolution with full 16-bit color. The Gamebuino doesn't have enough RAM to buffer the whole screen at that resolution and color depth. That means using gb.display isn't an option. Instead of trying to buffer the whole screen, we can split the screen into multiple slices. These could be horizontal, vertical, or any other arrangements that are ideally rectangular. Each slice is small enough to fit in RAM. As long as we send all of these slices to the screen fast enough, we will have a full display.

Taking things a step further, we can use the DMA controller to send one slice to the screen while we start buffering the next slice in RAM. Luckily we can piggyback off of a lot of the logic in gb.tft that handles the DMA. To squeeze a bit more performance we will write directly to the buffers we want to send to the screen. To summarize, the idea is double buffers of small slices of the screen, filling one while the previous one is being sent. 

Enough with words. Let's see some code! Here is a minimal program that illustrates the above. It is able to eke out a pattern of colors, 160px × 128px, 16-bit color depth, at 40 frames per second! (At least my Gamebuino is showing CPU utilization hovering at 100%.) This is meant as more of a jumping off point rather than a strict pattern. I've used something similar for a tile-based platformer and a simple raycaster. I hope it inspires you!

#include <Gamebuino-Meta.h>

#define SCREEN_WIDTH 160 #define SCREEN_HEIGHT 128 #define SLICE_WIDTH 10

// Magic to get access to wait_for_transfers_done namespace Gamebuino_Meta { #define DMA_DESC_COUNT (3) extern volatile uint32_t dma_desc_free_count;

static inline void wait_for_transfers_done(void) { while (dma_desc_free_count < DMA_DESC_COUNT); }

static SPISettings tftSPISettings = SPISettings(24000000, MSBFIRST, SPI_MODE0); };

// Double buffers for screen data. Fill one while the other is being sent to the screen. uint16_t buffer1[SCREEN_HEIGHT * SLICE_WIDTH]; uint16_t buffer2[SCREEN_HEIGHT * SLICE_WIDTH];

void setup() { gb.begin(); // We aren't using the normal screen buffer, so initialize it to 0px × 0px. gb.display.init(0, 0, ColorMode::rgb565);

// Just to push things to the limit for this example, increase to 40fps. gb.setFrameRate(40);

SerialUSB.begin(9600); }

void loop() { while (!gb.update());

// Use the serial monitor to observe the CPU utilization. if (gb.frameCount % 25 == 0) SerialUSB.printf("CPU: %i\n", gb.getCpuLoad());

// Loop over each 8px × 128px slice of the screen. for (int sliceIndex = 0; sliceIndex < SCREEN_WIDTH / SLICE_WIDTH; sliceIndex++) { // Alternate between buffers. While one is being sent to the screen with the DMA controller, // the other can be used for buffering the next slice of the screen. uint16_t *buffer = sliceIndex % 2 == 0 ? buffer1 : buffer2;

// Feel free and skip past this since it will be completely different based on what you are trying to do.
uint16_t initRed = sliceIndex * SLICE_WIDTH + gb.frameCount;
uint16_t blue = gb.frameCount % 32;
for (int y = 0; y &lt; SCREEN_HEIGHT; y++) {
  uint16_t red = initRed;
  blue = (blue + 1) %32;
  for (int x = 0; x &lt; SLICE_WIDTH; x++) {
    red = (red + 1) % 32;
    uint16_t green = x + y + initRed;
    // Note that the processor and tft use different endianness. The color data needs to be:
    // g2 g1 g0 b4 b3 b2 b1 b0   r4 r3 r2 r1 r0 g5 g4 g3
    buffer[x + y * SLICE_WIDTH] = (blue &lt;&lt; 8) | (red &lt;&lt; 3) | (green &lt;&lt; 13) | ((0b111000 &amp; green) &gt;&gt; 3);

// As long as this isn&#39;t the first time through the loop, make sure the previous
// write to the screen is done.
if (sliceIndex != 0) waitForPreviousDraw();
// And finally send the current buffer slice to the screen!
customDrawBuffer(sliceIndex * SLICE_WIDTH, 0, buffer, SLICE_WIDTH, SCREEN_HEIGHT); 

} // Wait for the final slice to complete before leaving the function. waitForPreviousDraw(); }

// Use gb.tft calls to communicate with the screen. void customDrawBuffer(int16_t x, int16_t y, uint16_t buffer, uint16_t w, uint16_t h) { gb.tft.setAddrWindow(x, y, x + w - 1, y + h - 1); SPI.beginTransaction(Gamebuino_Meta::tftSPISettings); gb.tft.dataMode(); gb.tft.sendBuffer(buffer, wh); }

void waitForPreviousDraw() { Gamebuino_Meta::wait_for_transfers_done(); gb.tft.idleMode(); SPI.endTransaction(); }