From 535b0078e5df3b472d68c6e634b334d013408a93 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sat, 11 Oct 2025 11:15:39 +0200 Subject: [PATCH] some cleanup --- Firmware/CMakeLists.txt | 2 + Firmware/components/sdk-esp/CMakeLists.txt | 18 ++- Firmware/main/CMakeLists.txt | 7 - Firmware/main/include/app_framework.hpp | 3 - Firmware/main/include/app_platform.hpp | 32 ++--- .../include/cardboy/backend/esp_backend.hpp | 14 ++ Firmware/main/include/disp_tools.hpp | 53 -------- Firmware/main/include/disp_tty.hpp | 42 ------ Firmware/main/include/display.hpp | 14 +- Firmware/main/include/power_helper.hpp | 1 + Firmware/main/src/app_main.cpp | 3 - Firmware/main/src/disp_tools.cpp | 10 -- Firmware/main/src/disp_tty.cpp | 62 --------- Firmware/main/src/display.cpp | 24 +++- Firmware/sdk/CMakeLists.txt | 41 ++++-- Firmware/sdk/apps/clock_app.cpp | 5 +- Firmware/sdk/apps/gameboy_app.cpp | 51 +++----- Firmware/sdk/apps/menu_app.cpp | 5 +- Firmware/sdk/apps/tetris_app.cpp | 5 +- .../cardboy/backend/desktop_backend.hpp | 63 +++++++++ Firmware/sdk/hosts/sfml_main.cpp | 97 ++++++-------- .../sdk/include/cardboy/sdk/app_framework.hpp | 28 ++-- .../sdk/include/cardboy/sdk/app_system.hpp | 14 +- Firmware/sdk/include/cardboy/sdk/backend.hpp | 10 ++ Firmware/sdk/include/cardboy/sdk/platform.hpp | 123 ++++++++++++++---- 25 files changed, 359 insertions(+), 368 deletions(-) create mode 100644 Firmware/main/include/cardboy/backend/esp_backend.hpp delete mode 100644 Firmware/main/include/disp_tools.hpp delete mode 100644 Firmware/main/include/disp_tty.hpp delete mode 100644 Firmware/main/src/disp_tools.cpp delete mode 100644 Firmware/main/src/disp_tty.cpp create mode 100644 Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp create mode 100644 Firmware/sdk/include/cardboy/sdk/backend.hpp diff --git a/Firmware/CMakeLists.txt b/Firmware/CMakeLists.txt index 0a454d0..b18619b 100644 --- a/Firmware/CMakeLists.txt +++ b/Firmware/CMakeLists.txt @@ -2,5 +2,7 @@ # CMakeLists in this exact order for cmake to work correctly cmake_minimum_required(VERSION 3.16) +add_compile_options(-Ofast) + include($ENV{IDF_PATH}/tools/cmake/project.cmake) project(hello_world) diff --git a/Firmware/components/sdk-esp/CMakeLists.txt b/Firmware/components/sdk-esp/CMakeLists.txt index 26172ac..b8343a2 100644 --- a/Firmware/components/sdk-esp/CMakeLists.txt +++ b/Firmware/components/sdk-esp/CMakeLists.txt @@ -2,4 +2,20 @@ idf_component_register() add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build) -target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk) \ No newline at end of file +target_link_libraries(${COMPONENT_LIB} + INTERFACE + cardboy_sdk + cardboy_apps +) + +target_compile_definitions(${COMPONENT_LIB} + INTERFACE + CARDBOY_SDK_BACKEND_HEADER=\"cardboy/backend/esp_backend.hpp\" + CARDBOY_SDK_ACTIVE_BACKEND_TYPE=cardboy::backend::EspBackend +) + +target_include_directories(${COMPONENT_LIB} + INTERFACE + ${CMAKE_CURRENT_LIST_DIR}/../../main/include +) +target_compile_options(${COMPONENT_LIB} INTERFACE -fjump-tables -ftree-switch-conversion) diff --git a/Firmware/main/CMakeLists.txt b/Firmware/main/CMakeLists.txt index aeffcfd..f9b0008 100644 --- a/Firmware/main/CMakeLists.txt +++ b/Firmware/main/CMakeLists.txt @@ -1,21 +1,14 @@ idf_component_register(SRCS src/app_main.cpp - ../sdk/apps/menu_app.cpp - ../sdk/apps/clock_app.cpp - ../sdk/apps/tetris_app.cpp - ../sdk/apps/gameboy_app.cpp src/display.cpp src/bat_mon.cpp src/spi_global.cpp src/i2c_global.cpp - src/disp_tools.cpp - src/disp_tty.cpp src/shutdowner.cpp src/buttons.cpp src/power_helper.cpp src/buzzer.cpp src/fs_helper.cpp - ../sdk/src/app_system.cpp PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs INCLUDE_DIRS "include" "../sdk/include" EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb") diff --git a/Firmware/main/include/app_framework.hpp b/Firmware/main/include/app_framework.hpp index 5177034..5c0d1ac 100644 --- a/Firmware/main/include/app_framework.hpp +++ b/Firmware/main/include/app_framework.hpp @@ -11,9 +11,6 @@ using AppButtonEvent = cardboy::sdk::AppButtonEvent; using AppTimerEvent = cardboy::sdk::AppTimerEvent; using AppEvent = cardboy::sdk::AppEvent; -template -using BasicAppContext = cardboy::sdk::BasicAppContext; - using AppContext = cardboy::sdk::AppContext; using IApp = cardboy::sdk::IApp; diff --git a/Firmware/main/include/app_platform.hpp b/Firmware/main/include/app_platform.hpp index 65b265f..8b1bd78 100644 --- a/Firmware/main/include/app_platform.hpp +++ b/Firmware/main/include/app_platform.hpp @@ -5,37 +5,37 @@ #include "config.hpp" #include -#include +#include #include #include "freertos/FreeRTOS.h" #include "freertos/task.h" -class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer { +class PlatformFramebuffer final : public cardboy::sdk::FramebufferFacade { public: - int width() const override { return cardboy::sdk::kDisplayWidth; } - int height() const override { return cardboy::sdk::kDisplayHeight; } + [[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; } + [[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; } - void drawPixel(int x, int y, bool on) override { + __attribute__((always_inline)) void drawPixel_impl(int x, int y, bool on) { if (x < 0 || y < 0 || x >= width() || y >= height()) return; - DispTools::set_pixel(x, y, on); + SMD::set_pixel(x, y, on); } - void clear(bool on) override { + void clear_impl(bool on) { for (int y = 0; y < height(); ++y) for (int x = 0; x < width(); ++x) - DispTools::set_pixel(x, y, on); + SMD::set_pixel(x, y, on); } - void beginFrame() override { DispTools::draw_to_display_async_wait(); } - void endFrame() override { DispTools::draw_to_display_async_start(); } - bool isFrameInFlight() const override { return DispTools::draw_to_display_async_busy(); } + __attribute__((always_inline)) void frameReady_impl() { SMD::frame_ready(); } + __attribute__((always_inline)) void sendFrame_impl(bool clear) { SMD::send_frame(clear); } + __attribute__((always_inline)) [[nodiscard]] bool frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); } }; -class PlatformInput final : public cardboy::sdk::IInput { +class PlatformInput final : public cardboy::sdk::InputFacade { public: - cardboy::sdk::InputState readState() override { + cardboy::sdk::InputState readState_impl() { cardboy::sdk::InputState state{}; const uint8_t pressed = Buttons::get().get_pressed(); if (pressed & BTN_UP) @@ -58,14 +58,14 @@ public: } }; -class PlatformClock final : public cardboy::sdk::IClock { +class PlatformClock final : public cardboy::sdk::ClockFacade { public: - std::uint32_t millis() override { + std::uint32_t millis_impl() { TickType_t ticks = xTaskGetTickCount(); return static_cast((static_cast(ticks) * 1000ULL) / configTICK_RATE_HZ); } - void sleep_ms(std::uint32_t ms) override { + void sleep_ms_impl(std::uint32_t ms) { if (ms == 0) return; PowerHelper::get().delay(static_cast(ms), static_cast(ms)); diff --git a/Firmware/main/include/cardboy/backend/esp_backend.hpp b/Firmware/main/include/cardboy/backend/esp_backend.hpp new file mode 100644 index 0000000..5a43107 --- /dev/null +++ b/Firmware/main/include/cardboy/backend/esp_backend.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include "app_platform.hpp" +#include "cardboy/sdk/platform.hpp" + +namespace cardboy::backend { + +struct EspBackend { + using Framebuffer = PlatformFramebuffer; + using Input = PlatformInput; + using Clock = PlatformClock; +}; + +} // namespace cardboy::backend diff --git a/Firmware/main/include/disp_tools.hpp b/Firmware/main/include/disp_tools.hpp deleted file mode 100644 index 3a01c66..0000000 --- a/Firmware/main/include/disp_tools.hpp +++ /dev/null @@ -1,53 +0,0 @@ -// -// Created by Stepan Usatiuk on 02.03.2025. -// - -#ifndef CB_DISP_TOOLS_HPP -#define CB_DISP_TOOLS_HPP - -#include - -namespace DispTools { - static void clear() { - for (int y = 0; y < DISP_HEIGHT; y++) { - for (int x = 0; x < DISP_WIDTH; x++) { - SMD::set_pixel(x, y, true); - } - } - } - static bool get_pixel(int x, int y) { - if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT) - assert(false); - assert(false); // Not implemented - return true; - // return disp_frame[y][x]; - } - static void reset_pixel(int x, int y) { - if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT) - assert(false); - SMD::set_pixel(x, y, false); - } - static void set_pixel(int x, int y) { - if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT) - assert(false); - - SMD::set_pixel(x, y, true); - } - static void set_pixel(int x, int y, bool on) { - if (on) { - set_pixel(x, y); - } else { - reset_pixel(x, y); - } - } - // New simplified async pipeline wrappers - static void async_frame_start() { SMD::async_draw_wait(); } // call at frame start - static void async_frame_end() { SMD::async_draw_start(); } // call after rendering - // Legacy names (temporary) mapped to new API in case of straggling calls - static void draw_to_display_async_start() { SMD::async_draw_start(); } - static void draw_to_display_async_wait() { SMD::async_draw_wait(); } - static bool draw_to_display_async_busy() { return SMD::async_draw_busy(); } -}; - - -#endif // DISP_TOOLS_HPP diff --git a/Firmware/main/include/disp_tty.hpp b/Firmware/main/include/disp_tty.hpp deleted file mode 100644 index 790318a..0000000 --- a/Firmware/main/include/disp_tty.hpp +++ /dev/null @@ -1,42 +0,0 @@ -// -// Created by Stepan Usatiuk on 02.03.2025. -// - -#ifndef DISP_TTY_HPP -#define DISP_TTY_HPP - -#include -#include - -#include "config.hpp" - -#include - -class FbTty { -public: - void putchar(char c); - void putstr(const char* str); - void reset(); - - template - auto fmt(std::format_string fmt, Args&&... args) { - auto str = std::format(fmt, std::forward(args)...); - putstr(str.c_str()); - } -private: - void draw_char(int col, int row); - - int _cur_col = 0; - int _cur_row = 0; - - static constexpr size_t _max_col = DISP_WIDTH / 8; - static constexpr size_t _max_row = DISP_HEIGHT / 16; - - std::array, _max_col> _buf = {}; - - void next_col(); - void next_row(); -}; - - -#endif // DISP_TTY_HPP diff --git a/Firmware/main/include/display.hpp b/Firmware/main/include/display.hpp index 8eca365..1e36b50 100644 --- a/Firmware/main/include/display.hpp +++ b/Firmware/main/include/display.hpp @@ -24,15 +24,15 @@ extern uint8_t* dma_buf; void init(); // Double-buffered asynchronous frame pipeline: // Usage pattern each frame: -// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced +// SMD::frame_ready(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced // ... write pixels into dma_buf via set_pixel / surface ... -// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer -// // is asynchronously cleared so the alternate buffer is ready for the next frame -void async_draw_start(); -void async_draw_wait(); -bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight? +// SMD::send_frame(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer +// // is optionally cleared so the alternate buffer is ready for the next frame +void send_frame(bool clear_after_send = true); +void frame_ready(); +bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight? -static void set_pixel(int x, int y, bool value) { +__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) { assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT); unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8); diff --git a/Firmware/main/include/power_helper.hpp b/Firmware/main/include/power_helper.hpp index 302d0dd..8fe8a06 100644 --- a/Firmware/main/include/power_helper.hpp +++ b/Firmware/main/include/power_helper.hpp @@ -6,6 +6,7 @@ #define POWER_HELPER_HPP #include "freertos/FreeRTOS.h" +#include "freertos/event_groups.h" class PowerHelper { public: diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 278a7ed..1b615d6 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -14,7 +14,6 @@ #include #include #include -#include #include #include #include @@ -299,8 +298,6 @@ extern "C" void app_main() { BatMon::get(); SpiGlobal::get(); SMD::init(); - - DispTools::clear(); Buzzer::get().init(); FsHelper::get().mount(); diff --git a/Firmware/main/src/disp_tools.cpp b/Firmware/main/src/disp_tools.cpp deleted file mode 100644 index e552d66..0000000 --- a/Firmware/main/src/disp_tools.cpp +++ /dev/null @@ -1,10 +0,0 @@ -// -// Created by Stepan Usatiuk on 02.03.2025. -// - -#include "disp_tools.hpp" - -#include - -#include - diff --git a/Firmware/main/src/disp_tty.cpp b/Firmware/main/src/disp_tty.cpp deleted file mode 100644 index 03dd8ae..0000000 --- a/Firmware/main/src/disp_tty.cpp +++ /dev/null @@ -1,62 +0,0 @@ -// -// Created by Stepan Usatiuk on 26.04.2024. -// - -#include "disp_tty.hpp" - -#include - -#include "cardboy/gfx/Fonts.hpp" - -void FbTty::draw_char(int col, int row) { - for (int x = 0; x < 8; x++) { - for (int y = 0; y < 16; y++) { - bool color = fonts_Terminess_Powerline[_buf[col][row]][y] & (1 << (8 - x)); - if (color) - DispTools::set_pixel(col * 8 + x, row * 16 + y); - else - DispTools::reset_pixel(col * 8 + x, row * 16 + y); - } - } -} -void FbTty::reset() { - _cur_col = 0; - _cur_row = 0; -} -void FbTty::putchar(char c) { - if (c == '\n') { - next_row(); - return; - } - - _buf[_cur_col][_cur_row] = c; - - draw_char(_cur_col, _cur_row); - - next_col(); -} -void FbTty::putstr(const char* str) { - while (*str != 0) { - putchar(*str); - str++; - } -} -void FbTty::next_col() { - _cur_col++; - _cur_col = _cur_col % _max_col; - if (_cur_col == 0) { - next_row(); - } else { - _buf[_cur_col][_cur_row] = ' '; - draw_char(_cur_col, _cur_row); - } -} -void FbTty::next_row() { - _cur_col = 0; - _cur_row++; - _cur_row = _cur_row % _max_row; - for (int i = 0; i < _max_col; i++) { - _buf[i][_cur_row] = ' '; - draw_char(i, _cur_row); - } -} diff --git a/Firmware/main/src/display.cpp b/Firmware/main/src/display.cpp index 22a8f60..78dd1d5 100644 --- a/Firmware/main/src/display.cpp +++ b/Firmware/main/src/display.cpp @@ -4,7 +4,6 @@ #include #include #include -#include "disp_tools.hpp" #include "driver/spi_master.h" #include "esp_async_memcpy.h" #include "esp_timer.h" @@ -27,6 +26,7 @@ static bool _vcom = false; static TaskHandle_t s_clearTaskHandle = nullptr; static SemaphoreHandle_t s_clearReqSem = nullptr; static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr}; +static bool s_clearPending[2] = {true, true}; static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG(); // update the maximum data stream supported by underlying DMA engine @@ -58,8 +58,17 @@ static void clear_task(void*) { ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0)); int bufIdx = (int) r->user; xSemaphoreGive(_txSem); - ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U, - my_async_memcpy_cb, static_cast(s_bufferSem[bufIdx]))); + const bool shouldClear = s_clearPending[bufIdx]; + s_clearPending[bufIdx] = true; + if (shouldClear) { + constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4); + static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway + ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize, + my_async_memcpy_cb, static_cast(s_bufferSem[bufIdx]))); + } else { + if (!xSemaphoreGive(s_bufferSem[bufIdx])) + assert(false); + } } } } @@ -101,9 +110,9 @@ void SMD::init() { xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle); } -bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; } +bool SMD::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; } -void SMD::async_draw_start() { +void SMD::send_frame(bool clear_after_send) { assert(driver != nullptr); if (!xSemaphoreTake(_txSem, portMAX_DELAY)) assert(false); @@ -115,7 +124,8 @@ void SMD::async_draw_start() { if (!xSemaphoreTake(sem, 0)) assert(false); - const int nextDrawIdx = sendIdx ^ 1; + const int nextDrawIdx = sendIdx ^ 1; + s_clearPending[sendIdx] = clear_after_send; _vcom = !_vcom; _tx = {}; @@ -129,7 +139,7 @@ void SMD::async_draw_start() { dma_buf = s_dma_buffers[nextDrawIdx]; } -void SMD::async_draw_wait() { +void SMD::frame_ready() { SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx]; // uint64_t waitedUs = 0; if (!uxSemaphoreGetCount(sem)) { diff --git a/Firmware/sdk/CMakeLists.txt b/Firmware/sdk/CMakeLists.txt index ce603d7..d55955e 100644 --- a/Firmware/sdk/CMakeLists.txt +++ b/Firmware/sdk/CMakeLists.txt @@ -5,35 +5,39 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED YES) set(CMAKE_CXX_EXTENSIONS NO) -add_library(cardboy_sdk STATIC - src/app_system.cpp -) +add_library(cardboy_sdk INTERFACE) target_include_directories(cardboy_sdk - PUBLIC + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) -target_compile_features(cardboy_sdk PUBLIC cxx_std_20) +target_compile_features(cardboy_sdk INTERFACE cxx_std_20) -add_library(cardboy_apps STATIC - apps/menu_app.cpp - apps/clock_app.cpp - apps/tetris_app.cpp - apps/gameboy_app.cpp +target_sources(cardboy_sdk + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp ) +add_library(cardboy_apps INTERFACE) + target_include_directories(cardboy_apps - PUBLIC + INTERFACE ${CMAKE_CURRENT_SOURCE_DIR}/include ) target_link_libraries(cardboy_apps - PUBLIC + INTERFACE cardboy_sdk ) -target_compile_features(cardboy_apps PUBLIC cxx_std_20) +target_sources(cardboy_apps + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/apps/menu_app.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/apps/clock_app.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/apps/tetris_app.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/apps/gameboy_app.cpp +) option(CARDBOY_BUILD_SFML "Build SFML harness" OFF) @@ -66,5 +70,16 @@ if (CARDBOY_BUILD_SFML) SFML::System ) + target_include_directories(cardboy_desktop + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/hosts/include + ) + + target_compile_definitions(cardboy_desktop + PRIVATE + CARDBOY_SDK_BACKEND_HEADER=\"cardboy/backend/desktop_backend.hpp\" + CARDBOY_SDK_ACTIVE_BACKEND_TYPE=cardboy::backend::DesktopBackend + ) + target_compile_features(cardboy_desktop PRIVATE cxx_std_20) endif () diff --git a/Firmware/sdk/apps/clock_app.cpp b/Firmware/sdk/apps/clock_app.cpp index 31c6a56..90e81de 100644 --- a/Firmware/sdk/apps/clock_app.cpp +++ b/Firmware/sdk/apps/clock_app.cpp @@ -160,8 +160,7 @@ private: return; dirty = false; - framebuffer.beginFrame(); - framebuffer.clear(false); + framebuffer.frameReady(); const int scaleLarge = 3; const int scaleSeconds = 2; @@ -220,7 +219,7 @@ private: drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1); drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1); - framebuffer.endFrame(); + framebuffer.sendFrame(); } }; diff --git a/Firmware/sdk/apps/gameboy_app.cpp b/Firmware/sdk/apps/gameboy_app.cpp index 2c0e942..e9adfb0 100644 --- a/Firmware/sdk/apps/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy_app.cpp @@ -2,9 +2,9 @@ #include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/peanut_gb.h" +#include "cardboy/gfx/font16x8.hpp" #include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_system.hpp" -#include "cardboy/gfx/font16x8.hpp" #include "cardboy/sdk/services.hpp" #include @@ -13,6 +13,7 @@ #include #include #include +#include #include #include #include @@ -21,8 +22,7 @@ #include #include #include -#include - +#define ESP_PLATFORM 1 #define GAMEBOY_PERF_METRICS 0 #ifndef GAMEBOY_PERF_METRICS @@ -41,6 +41,7 @@ namespace { constexpr int kMenuStartY = 48; constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; + using cardboy::sdk::AppContext; using cardboy::sdk::AppEvent; using cardboy::sdk::AppEventType; @@ -250,8 +251,6 @@ public: break; } - framebuffer.beginFrame(); - framebuffer.clear(false); GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();) ensureRenderGeometry(); @@ -264,7 +263,6 @@ public: GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();) renderGameFrame(); GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;) - framebuffer.endFrame(); break; } } @@ -536,14 +534,14 @@ private: std::array colXEnd{}; }; - AppContext& context; - Framebuffer& framebuffer; - cardboy::sdk::IFilesystem* filesystem = nullptr; + AppContext& context; + Framebuffer& framebuffer; + cardboy::sdk::IFilesystem* filesystem = nullptr; cardboy::sdk::IHighResClock* highResClock = nullptr; - PerfTracker perf{}; - AppTimerHandle tickTimer = kInvalidAppTimer; - int64_t frameDelayCarryUs = 0; - static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms + PerfTracker perf{}; + AppTimerHandle tickTimer = kInvalidAppTimer; + int64_t frameDelayCarryUs = 0; + static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms Mode mode = Mode::Browse; ScaleMode scaleMode = ScaleMode::Original; @@ -849,8 +847,7 @@ private: return; browserDirty = false; - framebuffer.beginFrame(); - framebuffer.clear(false); + framebuffer.frameReady(); const std::string_view title = "GAME BOY"; const int titleWidth = font16x8::measureText(title, 2, 1); @@ -893,7 +890,7 @@ private: font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1); } - framebuffer.endFrame(); + framebuffer.sendFrame(); } bool loadRom(std::size_t index) { @@ -1184,7 +1181,7 @@ private: font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1); } } - + framebuffer.sendFrame(); } void maybeSaveRam() { @@ -1259,10 +1256,7 @@ private: return static_cast(gb->direct.priv); } - static bool shouldPixelBeOn(int value, int dstX, int dstY, bool useDither) { - value &= 0x03; - if (!useDither) - return value >= 2; + __attribute__((always_inline)) static bool shouldPixelBeOn(int value, int dstX, int dstY) { if (value >= 3) return true; if (value <= 0) @@ -1335,16 +1329,13 @@ private: self->frameDirty = true; Framebuffer& fb = self->framebuffer; - - const bool useDither = (self->scaleMode == ScaleMode::FullHeight) && - (geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT); + 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 int val = (pixels[x] & 0x03u); - const bool on = shouldPixelBeOn(val, dstXBase + x, dstY, useDither); + const bool on = shouldPixelBeOn(pixels[x], dstXBase + x, dstY); fb.drawPixel(dstXBase + x, dstY, on); } return; @@ -1359,7 +1350,7 @@ private: continue; for (int dstY = yStart; dstY < yEnd; ++dstY) for (int dstX = drawStart; dstX < drawEnd; ++dstX) { - const bool on = shouldPixelBeOn(pixels[x], dstX, dstY, useDither); + const bool on = shouldPixelBeOn(pixels[x], dstX, dstY); fb.drawPixel(dstX, dstY, on); } } @@ -1398,7 +1389,7 @@ private: class GameboyAppFactory final : public cardboy::sdk::IAppFactory { public: - const char* name() const override { return kGameboyAppName; } + const char* name() const override { return kGameboyAppName; } std::unique_ptr create(cardboy::sdk::AppContext& context) override { return std::make_unique(context); } @@ -1406,8 +1397,6 @@ public: } // namespace -std::unique_ptr createGameboyAppFactory() { - return std::make_unique(); -} +std::unique_ptr createGameboyAppFactory() { return std::make_unique(); } } // namespace apps diff --git a/Firmware/sdk/apps/menu_app.cpp b/Firmware/sdk/apps/menu_app.cpp index 4cf57e1..1cbe999 100644 --- a/Firmware/sdk/apps/menu_app.cpp +++ b/Firmware/sdk/apps/menu_app.cpp @@ -135,8 +135,7 @@ private: return; dirty = false; - framebuffer.beginFrame(); - framebuffer.clear(false); + framebuffer.frameReady(); drawCenteredText(framebuffer, 24, "APPS", 1, 1); @@ -158,7 +157,7 @@ private: drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1); } - framebuffer.endFrame(); + framebuffer.sendFrame(); } }; diff --git a/Firmware/sdk/apps/tetris_app.cpp b/Firmware/sdk/apps/tetris_app.cpp index 47fdecc..7d16203 100644 --- a/Firmware/sdk/apps/tetris_app.cpp +++ b/Firmware/sdk/apps/tetris_app.cpp @@ -467,15 +467,14 @@ private: return; dirty = false; - framebuffer.beginFrame(); - framebuffer.clear(false); + framebuffer.frameReady(); drawBoard(); drawActivePiece(); drawNextPreview(); drawHUD(); - framebuffer.endFrame(); + framebuffer.sendFrame(); } void drawBoard() { diff --git a/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp new file mode 100644 index 0000000..e296383 --- /dev/null +++ b/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include "cardboy/sdk/platform.hpp" + +#include +#include +#include + +namespace cardboy::backend::desktop { + +class DesktopRuntime; + +class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { +public: + explicit DesktopFramebuffer(DesktopRuntime& runtime); + + [[nodiscard]] int width_impl() const; + [[nodiscard]] int height_impl() const; + void drawPixel_impl(int x, int y, bool on); + void clear_impl(bool on); + void frameReady_impl(); + void sendFrame_impl(bool clearAfterSend); + [[nodiscard]] bool frameInFlight_impl() const { return false; } + +private: + DesktopRuntime& runtime; +}; + +class DesktopInput final : public cardboy::sdk::InputFacade { +public: + explicit DesktopInput(DesktopRuntime& runtime); + + cardboy::sdk::InputState readState_impl(); + void handleKey(sf::Keyboard::Key key, bool pressed); + +private: + DesktopRuntime& runtime; + cardboy::sdk::InputState state{}; +}; + +class DesktopClock final : public cardboy::sdk::ClockFacade { +public: + explicit DesktopClock(DesktopRuntime& runtime); + + std::uint32_t millis_impl(); + void sleep_ms_impl(std::uint32_t ms); + +private: + DesktopRuntime& runtime; + const std::chrono::steady_clock::time_point start; +}; + +struct Backend { + using Framebuffer = DesktopFramebuffer; + using Input = DesktopInput; + using Clock = DesktopClock; +}; + +} // namespace cardboy::backend::desktop + +namespace cardboy::backend { +using DesktopBackend = desktop::Backend; +} // namespace cardboy::backend diff --git a/Firmware/sdk/hosts/sfml_main.cpp b/Firmware/sdk/hosts/sfml_main.cpp index 69adb21..52f25e4 100644 --- a/Firmware/sdk/hosts/sfml_main.cpp +++ b/Firmware/sdk/hosts/sfml_main.cpp @@ -2,6 +2,7 @@ #include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/menu_app.hpp" #include "cardboy/apps/tetris_app.hpp" +#include "cardboy/backend/desktop_backend.hpp" #include "cardboy/sdk/app_system.hpp" #include "cardboy/sdk/display_spec.hpp" #include "cardboy/sdk/services.hpp" @@ -26,7 +27,7 @@ #include #include -namespace { +namespace cardboy::backend::desktop { constexpr int kPixelScale = 2; @@ -135,54 +136,6 @@ private: bool mounted = false; }; -class DesktopRuntime; - -class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer { -public: - explicit DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {} - - int width() const override; - int height() const override; - void drawPixel(int x, int y, bool on) override; - void clear(bool on) override; - void beginFrame() override {} - void endFrame() override; - -private: - DesktopRuntime& runtime; -}; - -class DesktopInput final : public cardboy::sdk::IInput { -public: - explicit DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {} - - cardboy::sdk::InputState readState() override { return state; } - - void handleKey(sf::Keyboard::Key key, bool pressed); - -private: - DesktopRuntime& runtime; - cardboy::sdk::InputState state{}; -}; - -class DesktopClock final : public cardboy::sdk::IClock { -public: - explicit DesktopClock(DesktopRuntime& runtime) - : runtime(runtime), start(std::chrono::steady_clock::now()) {} - - std::uint32_t millis() override { - const auto now = std::chrono::steady_clock::now(); - return static_cast( - std::chrono::duration_cast(now - start).count()); - } - - void sleep_ms(std::uint32_t ms) override; - -private: - DesktopRuntime& runtime; - const std::chrono::steady_clock::time_point start; -}; - class DesktopRuntime { private: friend class DesktopFramebuffer; @@ -193,8 +146,9 @@ private: sf::Texture texture; sf::Sprite sprite; std::vector pixels; // RGBA buffer - bool dirty = true; - bool running = true; + bool dirty = true; + bool running = true; + bool clearNextFrame = true; DesktopBuzzer buzzerService; DesktopBattery batteryService; @@ -326,16 +280,32 @@ void DesktopRuntime::sleepFor(std::uint32_t ms) { std::this_thread::yield(); } while (true); } + +DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {} -int DesktopFramebuffer::width() const { return cardboy::sdk::kDisplayWidth; } +int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; } -int DesktopFramebuffer::height() const { return cardboy::sdk::kDisplayHeight; } +int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; } -void DesktopFramebuffer::drawPixel(int x, int y, bool on) { runtime.setPixel(x, y, on); } +void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); } -void DesktopFramebuffer::clear(bool on) { runtime.clearPixels(on); } +void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); } -void DesktopFramebuffer::endFrame() { runtime.dirty = true; } +void DesktopFramebuffer::frameReady_impl() { + if (runtime.clearNextFrame) { + runtime.clearPixels(false); + runtime.clearNextFrame = false; + } +} + +void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) { + runtime.clearNextFrame = clearAfterSend; + runtime.dirty = true; +} + +DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {} + +cardboy::sdk::InputState DesktopInput::readState_impl() { return state; } void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) { switch (key) { @@ -370,9 +340,20 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) { } } -void DesktopClock::sleep_ms(std::uint32_t ms) { runtime.sleepFor(ms); } +DesktopClock::DesktopClock(DesktopRuntime& runtime) + : runtime(runtime), start(std::chrono::steady_clock::now()) {} -} // namespace +std::uint32_t DesktopClock::millis_impl() { + const auto now = std::chrono::steady_clock::now(); + return static_cast( + std::chrono::duration_cast(now - start).count()); +} + +void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); } + +} // namespace cardboy::backend::desktop + +using cardboy::backend::desktop::DesktopRuntime; int main() { try { diff --git a/Firmware/sdk/include/cardboy/sdk/app_framework.hpp b/Firmware/sdk/include/cardboy/sdk/app_framework.hpp index 2f96d18..17107ac 100644 --- a/Firmware/sdk/include/cardboy/sdk/app_framework.hpp +++ b/Firmware/sdk/include/cardboy/sdk/app_framework.hpp @@ -1,5 +1,6 @@ #pragma once +#include "backend.hpp" #include "platform.hpp" #include "services.hpp" @@ -37,18 +38,23 @@ struct AppEvent { AppTimerEvent timer{}; }; -template -struct BasicAppContext { - using Framebuffer = FramebufferT; - using Input = InputT; - using Clock = ClockT; +#ifdef CARDBOY_SDK_ACTIVE_BACKEND_TYPE +using ActiveBackend = CARDBOY_SDK_ACTIVE_BACKEND_TYPE; +#else +#error "CARDBOY_SDK_ACTIVE_BACKEND_TYPE must name the active backend type" +#endif - BasicAppContext() = delete; - BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {} +struct AppContext { + using Framebuffer = typename ActiveBackend::Framebuffer; + using Input = typename ActiveBackend::Input; + using Clock = typename ActiveBackend::Clock; - FramebufferT& framebuffer; - InputT& input; - ClockT& clock; + AppContext() = delete; + AppContext(Framebuffer& fb, Input& in, Clock& clk) : framebuffer(fb), input(in), clock(clk) {} + + Framebuffer& framebuffer; + Input& input; + Clock& clock; AppSystem* system = nullptr; Services* services = nullptr; @@ -113,8 +119,6 @@ private: void cancelAllTimersInternal(); }; -using AppContext = BasicAppContext; - class IApp { public: virtual ~IApp() = default; diff --git a/Firmware/sdk/include/cardboy/sdk/app_system.hpp b/Firmware/sdk/include/cardboy/sdk/app_system.hpp index 1459ab0..8dc5e62 100644 --- a/Firmware/sdk/include/cardboy/sdk/app_system.hpp +++ b/Firmware/sdk/include/cardboy/sdk/app_system.hpp @@ -28,8 +28,7 @@ public: [[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; } private: - template - friend struct BasicAppContext; + friend struct AppContext; struct TimerRecord { AppTimerHandle id = kInvalidAppTimer; @@ -63,23 +62,18 @@ private: InputState lastInputState{}; }; -template -AppTimerHandle BasicAppContext::scheduleTimerInternal(std::uint32_t delay_ms, - bool repeat) { +inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) { return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer; } -template -void BasicAppContext::cancelTimerInternal(AppTimerHandle handle) { +inline void AppContext::cancelTimerInternal(AppTimerHandle handle) { if (system) system->cancelTimer(handle); } -template -void BasicAppContext::cancelAllTimersInternal() { +inline void AppContext::cancelAllTimersInternal() { if (system) system->cancelAllTimers(); } } // namespace cardboy::sdk - diff --git a/Firmware/sdk/include/cardboy/sdk/backend.hpp b/Firmware/sdk/include/cardboy/sdk/backend.hpp new file mode 100644 index 0000000..f6ce78a --- /dev/null +++ b/Firmware/sdk/include/cardboy/sdk/backend.hpp @@ -0,0 +1,10 @@ +#pragma once + +#include "platform.hpp" + +#ifndef CARDBOY_SDK_BACKEND_HEADER +#error "CARDBOY_SDK_BACKEND_HEADER must expand to the active backend header path" +#endif + +#include CARDBOY_SDK_BACKEND_HEADER + diff --git a/Firmware/sdk/include/cardboy/sdk/platform.hpp b/Firmware/sdk/include/cardboy/sdk/platform.hpp index d3c1fca..dcd2fa7 100644 --- a/Firmware/sdk/include/cardboy/sdk/platform.hpp +++ b/Firmware/sdk/include/cardboy/sdk/platform.hpp @@ -2,39 +2,114 @@ #include "input_state.hpp" +#include #include namespace cardboy::sdk { -class IFramebuffer { -public: - virtual ~IFramebuffer() = default; - - virtual int width() const = 0; - virtual int height() const = 0; - virtual void drawPixel(int x, int y, bool on) = 0; - virtual void clear(bool on) = 0; - - // Optional hooks for async display pipelines; default to no-ops. - virtual void beginFrame() {} - virtual void endFrame() {} - virtual bool isFrameInFlight() const { return false; } +namespace detail { +template +concept HasClearImpl = requires(Impl& impl, bool value) { + { impl.clear_impl(value) }; }; -class IInput { -public: - virtual ~IInput() = default; - - virtual InputState readState() = 0; +template +concept HasFrameReadyImpl = requires(Impl& impl) { + { impl.frameReady_impl() }; }; -class IClock { -public: - virtual ~IClock() = default; +template +concept HasSendFrameImpl = requires(Impl& impl, bool flag) { + { impl.sendFrame_impl(flag) }; +}; - virtual std::uint32_t millis() = 0; - virtual void sleep_ms(std::uint32_t ms) = 0; +template +concept HasFrameInFlightImpl = requires(const Impl& impl) { + { impl.frameInFlight_impl() } -> std::convertible_to; +}; + +template +concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) { + { impl.sleep_ms_impl(value) }; +}; +} // namespace detail + +template +class FramebufferFacade { +public: + [[nodiscard]] int width() const { return impl().width_impl(); } + [[nodiscard]] int height() const { return impl().height_impl(); } + + __attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); } + + void clear(bool on) { + if constexpr (detail::HasClearImpl) { + impl().clear_impl(on); + } else { + defaultClear(on); + } + } + + __attribute__((always_inline)) void frameReady() { + if constexpr (detail::HasFrameReadyImpl) + impl().frameReady_impl(); + } + + __attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) { + if constexpr (detail::HasSendFrameImpl) + impl().sendFrame_impl(clearDrawBuffer); + } + + __attribute__((always_inline)) [[nodiscard]] bool isFrameInFlight() const { + if constexpr (detail::HasFrameInFlightImpl) + return impl().frameInFlight_impl(); + return false; + } + +protected: + FramebufferFacade() = default; + ~FramebufferFacade() = default; + +private: + __attribute__((always_inline)) [[nodiscard]] Impl& impl() { return static_cast(*this); } + __attribute__((always_inline)) [[nodiscard]] const Impl& impl() const { return static_cast(*this); } + + void defaultClear(bool on) { + for (int y = 0; y < height(); ++y) + for (int x = 0; x < width(); ++x) + drawPixel(x, y, on); + } +}; + +template +class InputFacade { +public: + InputState readState() { return impl().readState_impl(); } + +protected: + InputFacade() = default; + ~InputFacade() = default; + +private: + [[nodiscard]] Impl& impl() { return static_cast(*this); } +}; + +template +class ClockFacade { +public: + std::uint32_t millis() { return impl().millis_impl(); } + + void sleep_ms(std::uint32_t ms) { + if constexpr (detail::HasSleepMsImpl) + impl().sleep_ms_impl(ms); + } + +protected: + ClockFacade() = default; + ~ClockFacade() = default; + +private: + [[nodiscard]] Impl& impl() { return static_cast(*this); } }; } // namespace cardboy::sdk -