diff --git a/Firmware/main/src/apps/gameboy_app.cpp b/Firmware/main/src/apps/gameboy_app.cpp index 7460a99..d8691d5 100644 --- a/Firmware/main/src/apps/gameboy_app.cpp +++ b/Firmware/main/src/apps/gameboy_app.cpp @@ -40,6 +40,7 @@ public: void onStart() override { prevInput = {}; statusMessage.clear(); + resetFpsStats(); ensureFilesystemReady(); refreshRomList(); mode = Mode::Browse; @@ -63,6 +64,7 @@ public: browserDirty = true; break; } + DispTools::draw_to_display_async_wait(); gb_run_frame(&gb); renderGameFrame(); break; @@ -94,14 +96,16 @@ private: InputState prevInput{}; // Emulator state - struct gb_s gb{}; - bool gbReady = false; - std::vector romData; - std::vector cartRam; - std::array frameBuffer{}; - bool frameDirty = false; - std::string activeRomName; - std::string activeRomSavePath; + struct gb_s gb{}; + bool gbReady = false; + std::vector romData; + std::vector cartRam; + bool frameDirty = false; + uint32_t fpsLastSampleMs = 0; + uint32_t fpsFrameCounter = 0; + uint32_t fpsCurrent = 0; + std::string activeRomName; + std::string activeRomSavePath; bool ensureFilesystemReady() { esp_err_t err = FsHelper::get().mount(); @@ -233,7 +237,6 @@ private: browserDirty = false; DispTools::draw_to_display_async_wait(); - framebuffer.clear(false); const std::string_view title = "GAME BOY"; const int titleWidth = font16x8::measureText(title, 2, 1); @@ -334,6 +337,9 @@ private: activeRomSavePath = buildSavePath(rom.fullPath); loadSaveFile(); + resetFpsStats(); + fpsLastSampleMs = context.clock.millis(); + gbReady = true; mode = Mode::Running; frameDirty = true; @@ -345,6 +351,7 @@ private: void unloadRom() { if (!gbReady) { + resetFpsStats(); romData.clear(); cartRam.clear(); activeRomName.clear(); @@ -353,6 +360,7 @@ private: } maybeSaveRam(); + resetFpsStats(); gbReady = false; romData.clear(); @@ -401,27 +409,27 @@ private: return; frameDirty = false; - DispTools::draw_to_display_async_wait(); - framebuffer.clear(false); - - const int offsetX = (framebuffer.width() - LCD_WIDTH) / 2; - const int offsetY = (framebuffer.height() - LCD_HEIGHT) / 2; - - for (int y = 0; y < LCD_HEIGHT; ++y) { - const int dstY = offsetY + y; - if (dstY < 0 || dstY >= framebuffer.height()) - continue; - for (int x = 0; x < LCD_WIDTH; ++x) { - const int dstX = offsetX + x; - if (dstX < 0 || dstX >= framebuffer.width()) - continue; - const bool on = frameBuffer[static_cast(y) * LCD_WIDTH + x] != 0; - framebuffer.drawPixel(dstX, dstY, on); - } + ++fpsFrameCounter; + const uint32_t nowMs = context.clock.millis(); + if (fpsLastSampleMs == 0) + fpsLastSampleMs = nowMs; + const uint32_t elapsed = nowMs - fpsLastSampleMs; + if (elapsed >= 1000U) { + const uint64_t scaledFrames = static_cast(fpsFrameCounter) * 1000ULL; + fpsCurrent = static_cast(scaledFrames / elapsed); + fpsFrameCounter = 0; + fpsLastSampleMs = nowMs; } + char fpsBuf[16]; + std::snprintf(fpsBuf, sizeof(fpsBuf), "%u FPS", static_cast(fpsCurrent)); + const std::string fpsText(fpsBuf); + const int fpsWidth = font16x8::measureText(fpsText, 1, 1); + const int fpsX = std::max(16, framebuffer.width() - fpsWidth - 16); + if (!activeRomName.empty()) font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1); + font16x8::drawText(framebuffer, fpsX, 16, fpsText, 1, true, 1); font16x8::drawText(framebuffer, 16, framebuffer.height() - 24, "START+SELECT BACK", 1, true, 1); DispTools::draw_to_display_async_start(); @@ -464,6 +472,12 @@ private: browserDirty = true; } + void resetFpsStats() { + fpsLastSampleMs = 0; + fpsFrameCounter = 0; + fpsCurrent = 0; + } + static std::string buildSavePath(const std::string& romPath) { std::string result = romPath; const auto dot = result.find_last_of('.'); @@ -516,11 +530,20 @@ private: if (!self || line >= LCD_HEIGHT) return; - const std::size_t offset = static_cast(line) * LCD_WIDTH; + const int offsetX = (self->framebuffer.width() - LCD_WIDTH) / 2; + const int offsetY = (self->framebuffer.height() - LCD_HEIGHT) / 2; + const int dstY = offsetY + static_cast(line); + if (dstY < 0 || dstY >= self->framebuffer.height()) + return; + for (int x = 0; x < LCD_WIDTH; ++x) { // Collapse 2-bit colour into monochrome. - const uint8_t shade = pixels[x] & 0x03u; - self->frameBuffer[offset + static_cast(x)] = (shade >= 2) ? 1 : 0; + const uint8_t shade = pixels[x] & 0x03u; + const bool on = (shade >= 2); + const int dstX = offsetX + x; + if (dstX < 0 || dstX >= self->framebuffer.width()) + continue; + self->framebuffer.drawPixel(dstX, dstY, on); } self->frameDirty = true; } diff --git a/Firmware/main/src/display.cpp b/Firmware/main/src/display.cpp index 568e8e3..4958b70 100644 --- a/Firmware/main/src/display.cpp +++ b/Firmware/main/src/display.cpp @@ -102,13 +102,13 @@ void SMD::async_draw_start() { _tx.tx_buffer = dma_buf; _tx.length = SMD::kLineDataBytes * 8; dma_buf[0] = 0b10000000 | (_vcom << 6); - _inFlight = true; + _inFlight = true; ESP_ERROR_CHECK(spi_device_queue_trans(_spi, &_tx, 0)); } void SMD::async_draw_wait() { - if (uxSemaphoreGetCount(s_clearSem) || !_inFlight) { - assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight); + if (!_inFlight || uxSemaphoreGetCount(s_clearSem)) { + // assert((uxSemaphoreGetCount(s_clearSem) == 0) == _inFlight); return; } if (!xSemaphoreTake(s_clearSem, portMAX_DELAY))