diff --git a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp index 89f46ad..3fbb417 100644 --- a/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp @@ -4,6 +4,7 @@ #include "cardboy/gfx/font16x8.hpp" #include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_system.hpp" +#include "cardboy/sdk/display_spec.hpp" #include "cardboy/sdk/services.hpp" #include "cardboy/utils/utils.hpp" @@ -115,8 +116,7 @@ public: prevInput = {}; statusMessage.clear(); resetFpsStats(); - scaleMode = ScaleMode::Original; - geometryDirty = true; + scaleMode = ScaleMode::Original; ensureFilesystemReady(); refreshRomList(); mode = Mode::Browse; @@ -180,9 +180,7 @@ public: } - GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();) - ensureRenderGeometry(); - GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;) + GB_PERF_ONLY(perf.geometryUs = 0;) GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();) gb_run_frame(&gb); @@ -447,20 +445,34 @@ private: #endif }; - struct RenderGeometry { - float scaleX = 1.0f; - float scaleY = 1.0f; - int scaledWidth = LCD_WIDTH; - int scaledHeight = LCD_HEIGHT; - int offsetX = 0; - int offsetY = 0; - int leftMargin = 0; - int rightMargin = 0; - std::array lineYStart{}; - std::array lineYEnd{}; - std::array colXStart{}; - std::array colXEnd{}; - }; + static constexpr int kOriginalOffsetX = (cardboy::sdk::kDisplayWidth - LCD_WIDTH) / 2; + static constexpr int kOriginalOffsetY = (cardboy::sdk::kDisplayHeight - LCD_HEIGHT) / 2; + static constexpr int kFullHeightScaledWidth = + (((LCD_WIDTH * cardboy::sdk::kDisplayHeight + LCD_HEIGHT / 2) / LCD_HEIGHT) + 7) & ~7; + static constexpr int kFullHeightOffsetX = (((cardboy::sdk::kDisplayWidth - kFullHeightScaledWidth) / 2) / 8) * 8; + + static_assert(kFullHeightScaledWidth % 8 == 0); + static_assert(kFullHeightOffsetX % 8 == 0); + static_assert(kOriginalOffsetX % 8 == 0); + static_assert(kFullHeightOffsetX + kFullHeightScaledWidth <= cardboy::sdk::kDisplayWidth); + + inline static constexpr std::array kFullHeightColumnBounds = []() constexpr { + std::array bounds{}; + for (int x = 0; x <= LCD_WIDTH; ++x) + bounds[static_cast(x)] = kFullHeightOffsetX + (kFullHeightScaledWidth * x) / LCD_WIDTH; + return bounds; + }(); + + inline static constexpr std::array kFullHeightRowBounds = []() constexpr { + std::array bounds{}; + for (int y = 0; y <= LCD_HEIGHT; ++y) + bounds[static_cast(y)] = (cardboy::sdk::kDisplayHeight * y) / LCD_HEIGHT; + return bounds; + }(); + + static_assert(kFullHeightColumnBounds[0] % 8 == 0); + static_assert(kFullHeightColumnBounds[LCD_WIDTH] - kFullHeightColumnBounds[0] == kFullHeightScaledWidth); + static_assert(kFullHeightRowBounds[LCD_HEIGHT] == cardboy::sdk::kDisplayHeight); AppContext& context; Framebuffer& framebuffer; @@ -471,10 +483,8 @@ private: int64_t frameDelayCarryUs = 0; static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms - Mode mode = Mode::Browse; - ScaleMode scaleMode = ScaleMode::Original; - bool geometryDirty = true; - RenderGeometry geometry{}; + Mode mode = Mode::Browse; + ScaleMode scaleMode = ScaleMode::Original; std::vector roms; std::size_t selectedIndex = 0; bool browserDirty = true; @@ -657,86 +667,10 @@ private: scaleMode = ScaleMode::FullHeight; else scaleMode = ScaleMode::Original; - geometryDirty = true; - frameDirty = true; + frameDirty = true; setStatus(scaleMode == ScaleMode::FullHeight ? "Scale: Full height" : "Scale: Original"); } - void ensureRenderGeometry() { - if (!geometryDirty) - return; - geometryDirty = false; - - auto& geom = geometry; - const int fbWidth = framebuffer.width(); - const int fbHeight = framebuffer.height(); - const auto resetGeom = [&]() { - geom.scaleX = 1.0f; - geom.scaleY = 1.0f; - geom.scaledWidth = 0; - geom.scaledHeight = 0; - geom.offsetX = 0; - geom.offsetY = 0; - geom.leftMargin = 0; - geom.rightMargin = 0; - std::fill(geom.lineYStart.begin(), geom.lineYStart.end(), 0); - std::fill(geom.lineYEnd.begin(), geom.lineYEnd.end(), 0); - std::fill(geom.colXStart.begin(), geom.colXStart.end(), 0); - std::fill(geom.colXEnd.begin(), geom.colXEnd.end(), 0); - }; - - if (fbWidth <= 0 || fbHeight <= 0) { - resetGeom(); - return; - } - - int scaledWidth; - int scaledHeight; - if (scaleMode == ScaleMode::FullHeight) { - int targetHeight = fbHeight; - int targetWidth = static_cast((static_cast(LCD_WIDTH) * targetHeight + LCD_HEIGHT / 2) / - std::max(1, LCD_HEIGHT)); - if (targetWidth > fbWidth) { - targetWidth = fbWidth; - targetHeight = static_cast((static_cast(LCD_HEIGHT) * targetWidth + LCD_WIDTH / 2) / - std::max(1, LCD_WIDTH)); - } - scaledWidth = std::clamp(targetWidth, 1, fbWidth); - scaledHeight = std::clamp(targetHeight, 1, fbHeight); - } else { - scaledWidth = std::clamp(fbWidth, 1, LCD_WIDTH); - scaledHeight = std::clamp(fbHeight, 1, LCD_HEIGHT); - } - - geom.scaledWidth = scaledWidth; - geom.scaledHeight = scaledHeight; - geom.offsetX = std::max(0, (fbWidth - scaledWidth) / 2); - geom.offsetY = std::max(0, (fbHeight - scaledHeight) / 2); - geom.leftMargin = geom.offsetX; - geom.rightMargin = std::max(0, fbWidth - (geom.offsetX + scaledWidth)); - geom.scaleX = static_cast(scaledWidth) / static_cast(LCD_WIDTH); - geom.scaleY = static_cast(scaledHeight) / static_cast(LCD_HEIGHT); - - for (int srcLine = 0; srcLine < LCD_HEIGHT; ++srcLine) { - int start = geom.offsetY + static_cast((static_cast(scaledHeight) * srcLine) / LCD_HEIGHT); - int end = - geom.offsetY + static_cast((static_cast(scaledHeight) * (srcLine + 1)) / LCD_HEIGHT); - start = std::clamp(start, 0, fbHeight); - end = std::clamp(end, start, fbHeight); - geom.lineYStart[srcLine] = start; - geom.lineYEnd[srcLine] = end; - } - - for (int srcCol = 0; srcCol < LCD_WIDTH; ++srcCol) { - int start = geom.offsetX + static_cast((static_cast(scaledWidth) * srcCol) / LCD_WIDTH); - int end = geom.offsetX + static_cast((static_cast(scaledWidth) * (srcCol + 1)) / LCD_WIDTH); - start = std::clamp(start, 0, fbWidth); - end = std::clamp(end, start, fbWidth); - geom.colXStart[srcCol] = start; - geom.colXEnd[srcCol] = end; - } - } - void handleBrowserInput(const InputState& input) { if (input.select && !prevInput.select) { refreshRomList(); @@ -912,7 +846,6 @@ private: gbReady = true; mode = Mode::Running; frameDirty = true; - geometryDirty = true; activeRomName = rom.name.empty() ? "Game" : rom.name; std::string statusText = "Running " + activeRomName; @@ -931,7 +864,6 @@ private: cartRam.clear(); activeRomName.clear(); activeRomSavePath.clear(); - geometryDirty = true; return; } @@ -945,7 +877,6 @@ private: cartRam.clear(); activeRomName.clear(); activeRomSavePath.clear(); - geometryDirty = true; std::memset(&gb, 0, sizeof(gb)); mode = Mode::Browse; browserDirty = true; @@ -992,8 +923,6 @@ private: return; frameDirty = false; - ensureRenderGeometry(); - ++fpsFrameCounter; ++totalFrameCounter; const uint32_t nowMs = context.clock.millis(); @@ -1015,16 +944,9 @@ private: const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE"; if (scaleMode == ScaleMode::FullHeight) { - const auto& geom = geometry; - const int textScale = 1; - const int rotatedWidth = font16x8::kGlyphHeight * textScale; - const int screenHeight = framebuffer.height(); - const int screenWidth = framebuffer.width(); - const int leftMargin = std::max(0, geom.leftMargin); - const int rightMargin = std::max(0, geom.rightMargin); - const int maxLeftX = std::max(0, screenWidth - rotatedWidth); - const int maxRightXBase = std::max(0, screenWidth - rotatedWidth); - + const int textScale = 1; + const int screenHeight = framebuffer.height(); + const int screenWidth = framebuffer.width(); const int horizontalPadding = 8; const int fpsLineGap = 4; @@ -1202,6 +1124,54 @@ private: return value > threshold; } + static void drawLineOriginal(GameboyApp& self, const uint8_t pixels[160], int srcLine) { + Framebuffer& fb = self.framebuffer; + const int dstY = kOriginalOffsetY + srcLine; + const int baseX = kOriginalOffsetX; + CARDBOY_CHECK((baseX % 8) == 0); + int x = 0; + while (x < LCD_WIDTH) { + uint8_t pack = 0; + for (int i = 0; i < 8; ++i, ++x) { + const bool on = shouldPixelBeOn(pixels[x], baseX + x, dstY); + pack = static_cast((pack << 1) | (on ? 1 : 0)); + } + fb.drawBits8(baseX + x - 8, dstY, pack); + } + } + + static void drawLineFullHeight(GameboyApp& self, const uint8_t pixels[160], int srcLine) { + Framebuffer& fb = self.framebuffer; + int yStart = kFullHeightRowBounds[static_cast(srcLine)]; + int yEnd = kFullHeightRowBounds[static_cast(srcLine) + 1]; + + CARDBOY_CHECK(yEnd > yStart); + CARDBOY_CHECK((kFullHeightColumnBounds[0] % 8) == 0); + + for (int dstY = yStart; dstY < yEnd; ++dstY) { + uint8_t pack = 0; + int bitsCollected = 0; + for (int x = 0; x < LCD_WIDTH; ++x) { + const int colStart = kFullHeightColumnBounds[static_cast(x)]; + const int colEnd = kFullHeightColumnBounds[static_cast(x) + 1]; + CARDBOY_CHECK(colEnd > colStart); + for (int dstX = colStart; dstX < colEnd; ++dstX) { + const bool on = shouldPixelBeOn(pixels[x], dstX, dstY); + pack = static_cast((pack << 1) | (on ? 1 : 0)); + ++bitsCollected; + if (bitsCollected == 8) { + const int byteStart = dstX - 7; + CARDBOY_CHECK((byteStart % 8) == 0); + fb.drawBits8(byteStart, dstY, pack); + pack = 0; + bitsCollected = 0; + } + } + } + CARDBOY_CHECK(bitsCollected == 0); + } + } + static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) { auto* self = fromGb(gb); CARDBOY_CHECK_CODE(if (!self) return 0xFF; @@ -1246,49 +1216,19 @@ private: __attribute__((optimize("Ofast"))) static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160], const uint_fast8_t line) { auto* self = fromGb(gb); - if (!self || line >= LCD_HEIGHT) - return; + CARDBOY_CHECK(self && line < LCD_HEIGHT); ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::LcdDraw); - self->ensureRenderGeometry(); - const auto& geom = self->geometry; - if (geom.scaledWidth <= 0 || geom.scaledHeight <= 0) - return; - - const int yStart = geom.lineYStart[line]; - const int yEnd = geom.lineYEnd[line]; - if (yStart >= yEnd) - return; - self->frameDirty = true; Framebuffer& fb = self->framebuffer; fb.frameReady(); - if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) { - const int dstY = yStart; - const int dstXBase = geom.offsetX; - for (int x = 0; x < LCD_WIDTH; ++x) { - const bool on = shouldPixelBeOn(pixels[x], dstXBase + x, dstY); - fb.drawPixel(dstXBase + x, dstY, on); - } - return; - } - - const auto& colStart = geom.colXStart; - const auto& colEnd = geom.colXEnd; - for (int x = 0; x < LCD_WIDTH; ++x) { - const int drawStart = colStart[x]; - const int drawEnd = colEnd[x]; - if (drawStart >= drawEnd) - continue; - for (int dstY = yStart; dstY < yEnd; ++dstY) - for (int dstX = drawStart; dstX < drawEnd; ++dstX) { - const bool on = shouldPixelBeOn(pixels[x], dstX, dstY); - fb.drawPixel(dstX, dstY, on); - } - } + if (self->scaleMode == ScaleMode::FullHeight) + drawLineFullHeight(*self, pixels, static_cast(line)); + else + drawLineOriginal(*self, pixels, static_cast(line)); } static const char* initErrorToString(enum gb_init_error_e err) {