/** * Copyright (C) 2022 by Mahyar Koshkouei * * Permission to use, copy, modify, and/or distribute this software for any * purpose with or without fee is hereby granted. * * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH * REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY * AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, * INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM * LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR * OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR * PERFORMANCE OF THIS SOFTWARE. */ #include #include "common.h" TFT_eSPI tft = TFT_eSPI(); #if ENABLE_LCD_FRAMEBUFFER #if ENABLE_FRAMEBUFFER_FLIP_X_Y #define FRAMEBUFFER_WIDTH DISPLAY_HEIGHT #define FRAMEBUFFER_HEIGHT DISPLAY_WIDTH #else #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 /* Pixel data is stored in here. */ static uint8_t pixels_buffer[LCD_WIDTH]; volatile ScalingMode scalingMode = ScalingMode::STRETCH_KEEP_ASPECT; static int lcd_line_busy = 0; #define IS_REPEATED(pos) ((pos % 2) || (pos % 6 == 0)) static void calcExtraLineTable() { uint8_t offset = 0; for (uint8_t line = 0; line < LCD_HEIGHT; ++line) { scaledLineOffsetTable[line] = offset; offset += 1 + IS_REPEATED(line); } } 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 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); } void lcd_draw_line(struct gb_s* gb, const uint8_t pixels[LCD_WIDTH], const uint_fast8_t line) { union core_cmd cmd; /* Wait until previous line is sent. */ while (__atomic_load_n(&lcd_line_busy, __ATOMIC_SEQ_CST)) tight_loop_contents(); memcpy(pixels_buffer, pixels, LCD_WIDTH); /* Populate command. */ cmd.cmd = CORE_CMD_LCD_LINE; cmd.data = line; __atomic_store_n(&lcd_line_busy, 1, __ATOMIC_SEQ_CST); multicore_fifo_push_blocking(cmd.full); } #if ENABLE_LCD_FRAMEBUFFER 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 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 tft.dmaWait(); memcpy(linebuffer, pixels, width * sizeof(uint16_t)); tft.setSwapBytes(true); tft.startWrite(); // manual start required as DMA transfer is asynchronous 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, 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 - LCD_WIDTH) / 2; const uint16_t screenLineOffset = (DISPLAY_HEIGHT - LCD_HEIGHT) / 2; lcd_pushLine(colOffset, screenLineOffset, line, pixels, count); } void lcd_write_pixels_stretched(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { static uint16_t doubledPixels[DISPLAY_WIDTH]; uint16_t pos = 0; for (int col = 0; col < count; ++col) { doubledPixels[pos++] = pixels[col]; doubledPixels[pos++] = pixels[col]; } const uint16_t stretchedWidth = pos; const uint8_t lineRepeated = IS_REPEATED(line); const uint16_t lineOffset = scaledLineOffsetTable[line]; lcd_pushLine(0, 0, lineOffset, doubledPixels, stretchedWidth); if (lineRepeated) { lcd_pushLine(0, 0, lineOffset + 1, doubledPixels, stretchedWidth); } } void lcd_write_pixels_stretched_keep_aspect(const uint16_t* pixels, uint8_t line, uint_fast16_t count) { static uint16_t doubledPixels[DISPLAY_WIDTH]; uint16_t pos = 0; for (int col = 0; col < count; ++col) { doubledPixels[pos++] = pixels[col]; if (IS_REPEATED(col)) { doubledPixels[pos++] = pixels[col]; } } const uint16_t stretchedWidth = pos; const uint16_t colOffset = (DISPLAY_WIDTH - stretchedWidth) / 2; const uint8_t lineRepeated = IS_REPEATED(line); const uint16_t lineOffset = scaledLineOffsetTable[line]; lcd_pushLine(colOffset, 0, lineOffset, doubledPixels, stretchedWidth); if (lineRepeated) { lcd_pushLine(colOffset, 0, lineOffset + 1, doubledPixels, stretchedWidth); } } // Writes pixels to screen or framebuffer 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, count); break; case ScalingMode::STRETCH_KEEP_ASPECT: lcd_write_pixels_stretched_keep_aspect(pixels, line, count); break; case ScalingMode::NORMAL: default: lcd_write_pixels_normal(pixels, line, count); break; } } #if ENABLE_LCD_FRAMEBUFFER 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_clear() { #if ENABLE_LCD_FRAMEBUFFER 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(TFT_BLACK); #endif } void core1_lcd_draw_line(const uint_fast8_t line) { static uint16_t fb[LCD_WIDTH]; if (gb.cgb.cgbMode) { for (unsigned int x = 0; x < LCD_WIDTH; x++) { fb[x] = gb.cgb.fixPalette[pixels_buffer[x]]; } } else { for (unsigned int x = 0; x < LCD_WIDTH; x++) { fb[x] = palette[(pixels_buffer[x] & LCD_PALETTE_ALL) >> 4] [pixels_buffer[x] & 3]; } } lcd_write_pixels(fb, line, LCD_WIDTH); __atomic_store_n(&lcd_line_busy, 0, __ATOMIC_SEQ_CST); #if ENABLE_LCD_FRAMEBUFFER if (line == LCD_HEIGHT - 1) { lcd_write_framebuffer_to_screen(); } #endif } void core1DispatchLoop() { union core_cmd cmd; // Handle commands coming from core0 cmd.full = multicore_fifo_pop_blocking(); switch (cmd.cmd) { case CORE_CMD_LCD_LINE: core1_lcd_draw_line(cmd.data); break; case CORE_CMD_IDLE_SET: lcd_clear(); break; case CORE_CMD_NOP: default: break; } } void core1_init() { // Initialise and control LCD on core 1 lcd_init(true); lcd_clear(); calcExtraLineTable(); while (true) { core1DispatchLoop(); } }