diff --git a/platformio.ini b/platformio.ini index bacd867..aff0502 100644 --- a/platformio.ini +++ b/platformio.ini @@ -30,6 +30,8 @@ build_flags = -DENABLE_SDCARD=1 -DENABLE_LCD_DMA=1 -DENABLE_LCD_FRAMEBUFFER=1 + -DENABLE_FRAMEBUFFER_FLIP_X_Y=1 + -DENABLE_DOUBLE_BUFFERING=1 -DENABLE_USB_STORAGE_DEVICE=0 -DPEANUT_FULL_GBC_SUPPORT=1 -DUSE_TINYUSB diff --git a/src/lcd_core.cpp b/src/lcd_core.cpp index acc85d4..1815dbd 100644 --- a/src/lcd_core.cpp +++ b/src/lcd_core.cpp @@ -19,9 +19,24 @@ TFT_eSPI tft = TFT_eSPI(); #if ENABLE_LCD_FRAMEBUFFER -static TFT_eSprite framebuffer = TFT_eSprite(&tft); +#if ENABLE_FRAMEBUFFER_FLIP_X_Y +#define FRAMEBUFFER_WIDTH DISPLAY_HEIGHT +#define FRAMEBUFFER_HEIGHT DISPLAY_WIDTH #else -static TFT_eSPI& framebuffer = tft; +#define FRAMEBUFFER_WIDTH DISPLAY_WIDTH +#define FRAMEBUFFER_HEIGHT DISPLAY_HEIGHT +#endif +#define FRAMEBUFFER_PIXELS (FRAMEBUFFER_WIDTH * FRAMEBUFFER_HEIGHT) +#if ENABLE_LCD_DMA && ENABLE_DOUBLE_BUFFERING +#define BUFFER_COUNT 2 +#else // !ENABLE_LCD_DMA +#define BUFFER_COUNT 1 // no use in double buffering without DMA +#endif +static uint16_t framebuffers[BUFFER_COUNT][FRAMEBUFFER_PIXELS]; +static int8_t activeFramebufferId = 0; +#else // !ENABLE_LCD_FRAMEBUFFER +// Note DMA mode does not have a measurable effect without a framebuffer +static uint16_t linebuffer[DISPLAY_WIDTH]; #endif static uint8_t scaledLineOffsetTable[LCD_HEIGHT]; // scaled to 240 lines @@ -45,13 +60,24 @@ static void calcExtraLineTable() { void lcd_init(bool isCore1) { tft.init(); + #if ENABLE_LCD_DMA // do not enable DMA on core0 as it fails on core1 if it is already enabled if (isCore1 && !tft.initDMA(/*ctrl_cs not supported in RP2040 implementation*/)) { error("Failed to initialize TFT DMA"); } #endif - tft.setRotation(1); + + bool rotate = true; +#if ENABLE_FRAMEBUFFER_FLIP_X_Y + // the rotation only flips x and y in GRAM but does not change the LCD screen refresh direction. + // If the display is a native portrait (and not landscape) lcd, rotating the lcd to landscape results in ugly + // diagonal update lines. Keeping it in portrait mode and Flipping x and y in the framebuffer results in nicer + // horizontal update lines. Even better would be VSync or TE line handling (which is not present in most cheap displays) + rotate = !isCore1; // rotate in start menu but not in-game +#endif + tft.setRotation(rotate ? 1 : 0); + tft.fillScreen(TFT_BLACK); } @@ -73,37 +99,39 @@ void lcd_draw_line(struct gb_s* gb, const uint8_t pixels[LCD_WIDTH], const uint_ } #if ENABLE_LCD_FRAMEBUFFER -void lcd_pushColors(size_t offset, const uint16_t* pixels, uint_fast16_t count) { - uint16_t* image = (uint16_t*) framebuffer.getPointer(); - memcpy(&image[offset], pixels, count * sizeof(uint16_t)); -} +void lcd_pushLine(uint16_t screenColOffset, uint16_t screenLineOffset, uint16_t line, const uint16_t* pixels, uint_fast16_t width) { + uint16_t* framebuffer = framebuffers[activeFramebufferId]; +#if ENABLE_FRAMEBUFFER_FLIP_X_Y + uint_fast16_t pos = (screenColOffset * DISPLAY_HEIGHT) + DISPLAY_HEIGHT - (screenLineOffset + line) - 1; + for (uint_fast16_t i = 0; i < width; ++i) { + framebuffer[pos] = pixels[i]; + pos += DISPLAY_HEIGHT; + } #else -void lcd_pushColors(uint16_t colOffset, uint16_t lineOffset, const uint16_t* pixels, uint_fast16_t count) { - framebuffer.setAddrWindow(colOffset, lineOffset, count, 1); + uint32_t offset = screenColOffset + (uint32_t)(screenLineOffset + line) * DISPLAY_WIDTH; + memcpy(&framebuffer[offset], pixels, width * sizeof(uint16_t)); +#endif +} +#else // !ENABLE_LCD_FRAMEBUFFER +void lcd_pushLine(uint16_t screenColOffset, uint16_t screenLineOffset, uint16_t line, const uint16_t* pixels, uint_fast16_t width) { + tft.setAddrWindow(screenColOffset, screenLineOffset + line, width, 1); #if ENABLE_LCD_DMA - // DMA mode does not have a measurable effect without a framebuffer - static uint16_t dmaBuffer[DISPLAY_WIDTH]; tft.dmaWait(); - memcpy(dmaBuffer, pixels, count * sizeof(uint16_t)); + memcpy(linebuffer, pixels, width * sizeof(uint16_t)); tft.setSwapBytes(true); tft.startWrite(); // manual start required as DMA transfer is asynchronous - tft.pushPixelsDMA((uint16_t*) dmaBuffer, count); + tft.pushPixelsDMA((uint16_t*) linebuffer, width); //tft.endWrite(); // do not call endWrite(), as it will wait for the DMA transfer to finish, which results in no performance gain #else - tft.pushColors((uint16_t*) pixels, count, true); + tft.pushColors((uint16_t*) pixels, width, true); #endif } #endif void lcd_write_pixels_normal(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { - const uint16_t colOffset = (DISPLAY_WIDTH - count) / 2; + const uint16_t colOffset = (DISPLAY_WIDTH - LCD_WIDTH) / 2; const uint16_t screenLineOffset = (DISPLAY_HEIGHT - LCD_HEIGHT) / 2; - const uint16_t lineOffset = screenLineOffset + line; -#if ENABLE_LCD_FRAMEBUFFER - lcd_pushColors(lineOffset * DISPLAY_WIDTH + colOffset, pixels, count); -#else - lcd_pushColors(colOffset, lineOffset, pixels, count); -#endif + lcd_pushLine(colOffset, screenLineOffset, line, pixels, count); } void lcd_write_pixels_stretched(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { @@ -117,18 +145,10 @@ void lcd_write_pixels_stretched(const uint16_t* pixels, uint8_t line, uint_fast1 const uint8_t lineRepeated = IS_REPEATED(line); const uint16_t lineOffset = scaledLineOffsetTable[line]; -#if ENABLE_LCD_FRAMEBUFFER - size_t offset = lineOffset * DISPLAY_WIDTH; - lcd_pushColors(offset, doubledPixels, stretchedWidth); + lcd_pushLine(0, 0, lineOffset, doubledPixels, stretchedWidth); if (lineRepeated) { - lcd_pushColors(offset + DISPLAY_WIDTH, doubledPixels, stretchedWidth); + lcd_pushLine(0, 0, lineOffset + 1, doubledPixels, stretchedWidth); } -#else - lcd_pushColors(0, lineOffset, doubledPixels, stretchedWidth); - if (lineRepeated) { - lcd_pushColors(0, lineOffset + 1, doubledPixels, stretchedWidth); - } -#endif } void lcd_write_pixels_stretched_keep_aspect(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { @@ -144,58 +164,65 @@ void lcd_write_pixels_stretched_keep_aspect(const uint16_t* pixels, uint8_t line const uint16_t colOffset = (DISPLAY_WIDTH - stretchedWidth) / 2; - uint8_t lineRepeated = IS_REPEATED(line); + const uint8_t lineRepeated = IS_REPEATED(line); const uint16_t lineOffset = scaledLineOffsetTable[line]; -#if ENABLE_LCD_FRAMEBUFFER - size_t offset = lineOffset * DISPLAY_WIDTH + colOffset; - lcd_pushColors(offset, doubledPixels, stretchedWidth); + lcd_pushLine(colOffset, 0, lineOffset, doubledPixels, stretchedWidth); if (lineRepeated) { - lcd_pushColors(offset + DISPLAY_WIDTH, doubledPixels, stretchedWidth); + lcd_pushLine(colOffset, 0, lineOffset + 1, doubledPixels, stretchedWidth); } -#else - lcd_pushColors(colOffset, lineOffset, doubledPixels, stretchedWidth); - if (lineRepeated) { - lcd_pushColors(colOffset, lineOffset + 1, doubledPixels, stretchedWidth); - } -#endif } // Writes pixels to screen or framebuffer -void lcd_write_pixels(const uint16_t* pixels, uint8_t line, uint_fast16_t nmemb) { +void lcd_write_pixels(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { switch (scalingMode) { case ScalingMode::STRETCH: - lcd_write_pixels_stretched(pixels, line, nmemb); + lcd_write_pixels_stretched(pixels, line, count); break; case ScalingMode::STRETCH_KEEP_ASPECT: - lcd_write_pixels_stretched_keep_aspect(pixels, line, nmemb); + lcd_write_pixels_stretched_keep_aspect(pixels, line, count); break; case ScalingMode::NORMAL: default: - lcd_write_pixels_normal(pixels, line, nmemb); + lcd_write_pixels_normal(pixels, line, count); break; } } + #if ENABLE_LCD_FRAMEBUFFER -// Writes framebuffer to screen -void lcd_write_framebuffer_to_screen() { - tft.setSwapBytes(true); -#ifdef ENABLE_LCD_DMA - tft.startWrite(); // manual start required as DMA transfer is asynchronous - tft.pushImageDMA(0, 0, framebuffer.width(), framebuffer.height(), (uint16_t *) framebuffer.getPointer()); - //tft.endWrite(); // do not call endWrite(), as it will wait for the DMA transfer to finish, which results in no performance gain -#else - tft.pushImage(0, 0, framebuffer.width(), framebuffer.height(), (uint16_t *) framebuffer.getPointer()); + +void lcd_swap_buffers() { +#if BUFFER_COUNT == 2 + activeFramebufferId = (activeFramebufferId == 0) ? 1 : 0; #endif } + +// Writes framebuffer to screen +void lcd_write_framebuffer_to_screen() { + uint16_t* framebuffer = framebuffers[activeFramebufferId]; + tft.setSwapBytes(true); +#if ENABLE_LCD_DMA + tft.startWrite(); // manual start required as DMA transfer is asynchronous + tft.pushImageDMA(0, 0, FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT, framebuffer); + //tft.endWrite(); // do not call endWrite(), as it will wait for the DMA transfer to finish, which results in no performance gain + lcd_swap_buffers(); +#else + tft.pushImage(0, 0, FRAMEBUFFER_WIDTH, FRAMEBUFFER_HEIGHT, framebuffer); +#endif +} + #endif -void lcd_fill(uint16_t color) { + +void lcd_clear() { #if ENABLE_LCD_FRAMEBUFFER - framebuffer.fillSprite(color); + memset(framebuffers[0], 0, FRAMEBUFFER_PIXELS * sizeof(uint16_t)); +#if BUFFER_COUNT == 2 + memset(framebuffers[1], 0, FRAMEBUFFER_PIXELS * sizeof(uint16_t)); +#endif #else - tft.fillScreen(color); + tft.fillScreen(TFT_BLACK); #endif } @@ -226,7 +253,7 @@ void core1_lcd_draw_line(const uint_fast8_t line) { void core1DispatchLoop() { union core_cmd cmd; - /* Handle commands coming from core0. */ + // Handle commands coming from core0 cmd.full = multicore_fifo_pop_blocking(); switch (cmd.cmd) { case CORE_CMD_LCD_LINE: @@ -234,7 +261,7 @@ void core1DispatchLoop() { break; case CORE_CMD_IDLE_SET: - lcd_fill(TFT_BLACK); + lcd_clear(); break; case CORE_CMD_NOP: @@ -244,16 +271,10 @@ void core1DispatchLoop() { } void core1_init() { - /* Initialise and control LCD on core 1. */ + // Initialise and control LCD on core 1 lcd_init(true); -#if ENABLE_LCD_FRAMEBUFFER - framebuffer.setColorDepth(16); - framebuffer.createSprite(tft.width(), tft.height()); -#endif - - /* Clear LCD screen. */ - lcd_fill(TFT_BLACK); + lcd_clear(); calcExtraLineTable();