High Resolution without gb.display

Creations

aoneill

2 months ago

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;

    // BEGIN DRAWING TO BUFFER
    // 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 < SCREEN_HEIGHT; y++) {
      uint16_t red = initRed;
      blue = (blue + 1) %32;
      for (int x = 0; x < 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 << 8) | (red << 3) | (green << 13) | ((0b111000 & green) >> 3);
      }
    }
    // END DRAWING TO BUFFER

    // As long as this isn'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, w*h);
}

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

View full creation

BobDeFortuna

NEW 2 months ago

Thanks for sharing, interresting and impressive stuff!

jicehel

NEW 2 months ago

Like don't works on my forum, but anyway its not a like that i would put on your post. It's one "j'adore" (more than a like). Great tips, nice tuto, very usefull. I'll not be able to test it before some weeks, but when i'll remake my Parachute game i'll make it in Hi Rez to test. Many thanks Aoneill and may the force be with you for DN  ;)

STUDIOCRAFTapps

NEW 2 months ago

<3

steph

NEW 1 month ago

We owe you all a huge THANK YOU Andy for this tip!
Your technique is very clever and opens the door to high resolution.
I am completely new to C++ (I still have a good foundation in other languages) but I think I understand how you do it, including in DnGame!
It's impressive and we are very lucky to have you with us!

void loop() {
    while (!gb.update());
    gb.display.setColor(1 + gb.frameCount%15);
    gb.display.setCursor(2, 2 + 6 * (gb.frameCount%10));
    gb.display.println("Thank you!");
}

jicehel

NEW 1 month ago

Yes, this works is amazing.

It's works very well, you maybe could add some functions to your example to let people use them more easyly to draw sprites.

You could maybe make another easy example like your first one with a load of a background and some moving sprites. There will have  advantages: First more will be able to understand it, second: the functions used could be the same so its could make it more easy to reuse codes / parts of codes.

If you have time, it's could be cool to make example of a fixed background, another with some tiles and show how to write text on it (for score, lives, ...)

As i would try to begin the conversion of one of my game into Hi Res version, i have works a few on your demo example and i have make a horizontal block version of it that i post below (just a change of implementation by horizontal bloc as for my games it's will be more easy)


#include <Gamebuino-Meta.h>

#define SCREEN_WIDTH 160
#define SCREEN_HEIGHT 128
#define SLICE_HEIGHT 16

// 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_WIDTH * SLICE_HEIGHT];
uint16_t buffer2[SCREEN_WIDTH * SLICE_HEIGHT];  

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(38);

  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_HEIGHT / SLICE_HEIGHT; 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;

    // BEGIN DRAWING TO BUFFER
    // 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_HEIGHT + gb.frameCount;
    uint16_t blue = gb.frameCount % 32;
    for (int x = 0; x < SCREEN_WIDTH; x++) {
      uint16_t red = initRed;
      blue = (blue + 1) %32;
      for (int y = 0; y < SLICE_HEIGHT; y++) {
        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 * SCREEN_WIDTH ] = (blue << 8) | (red << 3) | (green << 13) | ((0b111000 & green) >> 3);
      }
    }
    // END DRAWING TO BUFFER

    // As long as this isn'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(0 , sliceIndex * SLICE_HEIGHT, buffer, SCREEN_WIDTH, SLICE_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, w*h);
}

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

steph

NEW 2 weeks ago

Hello Andy,

I don't know if you saw the notification pass, but I recently published a very complete tutorial on how to implement a shading effect on a high-resolution game scene. I based myself on your very useful article High Resolution without gb.display, which I tried to demystify for as many people as possible. So I hope I didn't misrepresent the technique you so kindly explained to us. If you have time to take a look at my tutorial, I'd love to. This time, I also worked on a complete English translation, so that it would be accessible to as many people as possible. I hope the translation is not too bad....

You must be logged in in order to post a message on the forum

Log in