diff --git a/Firmware/main/include/app_framework.hpp b/Firmware/main/include/app_framework.hpp index 1ba4717..2532959 100644 --- a/Firmware/main/include/app_framework.hpp +++ b/Firmware/main/include/app_framework.hpp @@ -1,5 +1,8 @@ #pragma once +#include "app_platform.hpp" +#include "input_state.hpp" + #include #include #include @@ -8,46 +11,18 @@ class AppSystem; -struct InputState { - bool up = false; - bool left = false; - bool right = false; - bool down = false; - bool a = false; - bool b = false; - bool select = false; - bool start = false; -}; +template +struct BasicAppContext { + using Framebuffer = FramebufferT; + using Input = InputT; + using Clock = ClockT; -class IFramebuffer { -public: - virtual ~IFramebuffer() = default; - virtual void drawPixel(int x, int y, bool on) = 0; // on=true => black - virtual void clear(bool on) = 0; // fill full screen to on/off - virtual int width() const = 0; - virtual int height() const = 0; -}; + BasicAppContext() = delete; + BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {} -class IInput { -public: - virtual ~IInput() = default; - virtual InputState readState() = 0; -}; - -class IClock { -public: - virtual ~IClock() = default; - virtual uint32_t millis() = 0; - virtual void sleep_ms(uint32_t ms) = 0; -}; - -struct AppContext { - AppContext() = delete; - AppContext(IFramebuffer& fb, IInput& in, IClock& clk) : framebuffer(fb), input(in), clock(clk) {} - - IFramebuffer& framebuffer; - IInput& input; - IClock& clock; + FramebufferT& framebuffer; + InputT& input; + ClockT& clock; AppSystem* system = nullptr; void requestAppSwitchByIndex(std::size_t index) { @@ -73,6 +48,8 @@ private: std::string pendingAppName; }; +using AppContext = BasicAppContext; + struct AppSleepPlan { uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh uint32_t normal_ms = 0; // short sleep for responsiveness on input wake diff --git a/Firmware/main/include/app_platform.hpp b/Firmware/main/include/app_platform.hpp new file mode 100644 index 0000000..93902be --- /dev/null +++ b/Firmware/main/include/app_platform.hpp @@ -0,0 +1,64 @@ +#pragma once + +#include "config.hpp" +#include "input_state.hpp" + +#include +#include +#include + +#include "freertos/FreeRTOS.h" +#include "freertos/task.h" + +class PlatformFramebuffer { +public: + int width() const { return DISP_WIDTH; } + int height() const { return DISP_HEIGHT; } + + void drawPixel(int x, int y, bool on) { + if (x < 0 || y < 0 || x >= width() || y >= height()) + return; + DispTools::set_pixel(x, y, on); + } + + void clear(bool on) { + for (int y = 0; y < height(); ++y) + for (int x = 0; x < width(); ++x) + DispTools::set_pixel(x, y, on); + } +}; + +class PlatformInput { +public: + InputState readState() { + InputState state{}; + const uint8_t pressed = Buttons::get().get_pressed(); + if (pressed & BTN_UP) + state.up = true; + if (pressed & BTN_LEFT) + state.left = true; + if (pressed & BTN_RIGHT) + state.right = true; + if (pressed & BTN_DOWN) + state.down = true; + if (pressed & BTN_A) + state.a = true; + if (pressed & BTN_B) + state.b = true; + if (pressed & BTN_SELECT) + state.select = true; + if (pressed & BTN_START) + state.start = true; + return state; + } +}; + +class PlatformClock { +public: + uint32_t millis() { + TickType_t ticks = xTaskGetTickCount(); + return static_cast((static_cast(ticks) * 1000ULL) / configTICK_RATE_HZ); + } + + void sleep_ms(uint32_t ms) { PowerHelper::get().delay(static_cast(ms), static_cast(ms)); } +}; diff --git a/Firmware/main/include/font16x8.hpp b/Firmware/main/include/font16x8.hpp index 8307a43..fba4a29 100644 --- a/Firmware/main/include/font16x8.hpp +++ b/Firmware/main/include/font16x8.hpp @@ -9,8 +9,8 @@ namespace font16x8 { -constexpr int kGlyphWidth = 8; -constexpr int kGlyphHeight = 16; +constexpr int kGlyphWidth = 8; +constexpr int kGlyphHeight = 16; constexpr unsigned char kFallbackChar = '?'; inline unsigned char normalizeChar(char ch) { @@ -27,7 +27,8 @@ inline const std::array& glyphBitmap(char ch) { return fonts_Terminess_Powerline[uc]; } -inline void drawGlyph(IFramebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) { +template +inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) { const auto& rows = glyphBitmap(ch); for (int row = 0; row < kGlyphHeight; ++row) { const uint8_t rowBits = rows[row]; @@ -49,7 +50,8 @@ inline int measureText(std::string_view text, int scale = 1, int letterSpacing = return static_cast(text.size()) * advance - letterSpacing * scale; } -inline void drawText(IFramebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true, +template +inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true, int letterSpacing = 1) { int cursor = x; for (char ch: text) { diff --git a/Firmware/main/include/input_state.hpp b/Firmware/main/include/input_state.hpp new file mode 100644 index 0000000..4842f20 --- /dev/null +++ b/Firmware/main/include/input_state.hpp @@ -0,0 +1,12 @@ +#pragma once + +struct InputState { + bool up = false; + bool left = false; + bool right = false; + bool down = false; + bool a = false; + bool b = false; + bool select = false; + bool start = false; +}; diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 8ef2c55..1336037 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -14,8 +14,8 @@ #include #include #include -#include #include +#include #include #include #include @@ -29,63 +29,6 @@ #include "esp_sleep.h" #include "sdkconfig.h" -namespace { - -class PlatformFramebuffer final : public IFramebuffer { -public: - int width() const override { return DISP_WIDTH; } - int height() const override { return DISP_HEIGHT; } - - void drawPixel(int x, int y, bool on) override { - if (x < 0 || y < 0 || x >= width() || y >= height()) - return; - DispTools::set_pixel(x, y, on); - } - - void clear(bool on) override { - for (int y = 0; y < height(); ++y) - for (int x = 0; x < width(); ++x) - DispTools::set_pixel(x, y, on); - } -}; - -class PlatformInput final : public IInput { -public: - InputState readState() override { - InputState state{}; - const uint8_t pressed = Buttons::get().get_pressed(); - if (pressed & BTN_UP) - state.up = true; - if (pressed & BTN_LEFT) - state.left = true; - if (pressed & BTN_RIGHT) - state.right = true; - if (pressed & BTN_DOWN) - state.down = true; - if (pressed & BTN_A) - state.a = true; - if (pressed & BTN_B) - state.b = true; - if (pressed & BTN_SELECT) - state.select = true; - if (pressed & BTN_START) - state.start = true; - return state; - } -}; - -class PlatformClock final : public IClock { -public: - uint32_t millis() override { - TickType_t ticks = xTaskGetTickCount(); - return static_cast((static_cast(ticks) * 1000ULL) / configTICK_RATE_HZ); - } - - void sleep_ms(uint32_t ms) override { PowerHelper::get().delay(static_cast(ms), static_cast(ms)); } -}; - -} // namespace - extern "C" void app_main() { #ifdef CONFIG_PM_ENABLE // const esp_pm_config_t pm_config = { diff --git a/Firmware/main/src/apps/clock_app.cpp b/Firmware/main/src/apps/clock_app.cpp index a4593c3..9392f50 100644 --- a/Firmware/main/src/apps/clock_app.cpp +++ b/Firmware/main/src/apps/clock_app.cpp @@ -19,6 +19,9 @@ namespace { constexpr const char* kClockAppName = "Clock"; +using Framebuffer = typename AppContext::Framebuffer; +using Clock = typename AppContext::Clock; + struct TimeSnapshot { bool hasWallTime = false; int hour24 = 0; @@ -76,9 +79,9 @@ public: } private: - AppContext& context; - IFramebuffer& framebuffer; - IClock& clock; + AppContext& context; + Framebuffer& framebuffer; + Clock& clock; bool use24Hour = true; bool dirty = false; @@ -119,7 +122,7 @@ private: return snap; } - static void drawCenteredText(IFramebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) { + static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) { const int width = font16x8::measureText(text, scale, letterSpacing); const int x = (fb.width() - width) / 2; font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); diff --git a/Firmware/main/src/apps/gameboy_app.cpp b/Firmware/main/src/apps/gameboy_app.cpp index 2f79aa8..eecd495 100644 --- a/Firmware/main/src/apps/gameboy_app.cpp +++ b/Firmware/main/src/apps/gameboy_app.cpp @@ -32,6 +32,8 @@ namespace { constexpr int kMenuStartY = 48; constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; +using Framebuffer = typename AppContext::Framebuffer; + constexpr std::array kRomExtensions = {".gb", ".gbc"}; extern "C" { @@ -116,7 +118,7 @@ int measureVerticalText(std::string_view text, int scale = 1, int letterSpacing return static_cast(text.size()) * advance - letterSpacing * scale; } -void drawGlyphRotated(IFramebuffer& fb, int x, int y, char ch, bool clockwise, int scale = 1, bool on = true) { +void drawGlyphRotated(Framebuffer& fb, int x, int y, char ch, bool clockwise, int scale = 1, bool on = true) { const auto& rows = font16x8::glyphBitmap(ch); for (int row = 0; row < font16x8::kGlyphHeight; ++row) { const uint8_t rowBits = rows[row]; @@ -142,7 +144,7 @@ void drawGlyphRotated(IFramebuffer& fb, int x, int y, char ch, bool clockwise, i } } -void drawTextRotated(IFramebuffer& fb, int x, int y, std::string_view text, bool clockwise, int scale = 1, +void drawTextRotated(Framebuffer& fb, int x, int y, std::string_view text, bool clockwise, int scale = 1, bool on = true, int letterSpacing = 1) { int cursor = y; const int advance = (font16x8::kGlyphWidth + letterSpacing) * scale; @@ -482,9 +484,9 @@ private: std::array colXEnd{}; }; - AppContext& context; - IFramebuffer& framebuffer; - PerfTracker perf{}; + AppContext& context; + Framebuffer& framebuffer; + PerfTracker perf{}; Mode mode = Mode::Browse; ScaleMode scaleMode = ScaleMode::Original; @@ -1129,7 +1131,7 @@ private: auto* self = fromGb(gb); if (!self) return 0xFF; - ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead); + // ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead); if (!self->romDataView || addr >= self->romDataViewSize) return 0xFF; return self->romDataView[static_cast(addr)]; @@ -1186,7 +1188,7 @@ private: self->frameDirty = true; - IFramebuffer& fb = self->framebuffer; + Framebuffer& fb = self->framebuffer; if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) { const int dstY = yStart; diff --git a/Firmware/main/src/apps/menu_app.cpp b/Firmware/main/src/apps/menu_app.cpp index 733bf4b..b35f054 100644 --- a/Firmware/main/src/apps/menu_app.cpp +++ b/Firmware/main/src/apps/menu_app.cpp @@ -15,6 +15,8 @@ namespace apps { namespace { +using Framebuffer = typename AppContext::Framebuffer; + struct MenuEntry { std::string name; std::size_t index = 0; @@ -60,7 +62,7 @@ public: private: AppContext& context; - IFramebuffer& framebuffer; + Framebuffer& framebuffer; std::vector entries; std::size_t selected = 0; @@ -110,7 +112,7 @@ private: dirty = true; } - static void drawCenteredText(IFramebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) { + static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 0) { const int width = font16x8::measureText(text, scale, letterSpacing); const int x = (fb.width() - width) / 2; font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); diff --git a/Firmware/main/src/apps/tetris_app.cpp b/Firmware/main/src/apps/tetris_app.cpp index f00baef..deb8011 100644 --- a/Firmware/main/src/apps/tetris_app.cpp +++ b/Firmware/main/src/apps/tetris_app.cpp @@ -103,6 +103,10 @@ using RotGrid = std::array; using PieceR = std::array; constexpr char _ = '.', X = '#'; +using Framebuffer = typename AppContext::Framebuffer; +using InputDevice = typename AppContext::Input; +using Clock = typename AppContext::Clock; + static const std::array TET = {{ PieceR{{RotGrid{_, _, _, _, X, X, X, X, _, _, _, _, _, _, _, _}, RotGrid{_, _, X, _, _, _, X, _, _, _, X, _, _, _, X, _}, @@ -201,7 +205,7 @@ constexpr int kHudLabelGap = 2; constexpr int kHudBlockGap = 8; constexpr int kHudLineGapBattery = 4; -inline void drawHudText(IFramebuffer& fb, int x, int y, std::string_view text) { +inline void drawHudText(Framebuffer& fb, int x, int y, std::string_view text) { font16x8::drawText(fb, x, y, text, kHudFontScale, true, kHudLetterSpacing); } @@ -211,7 +215,7 @@ inline int hudFontHeight() { return font16x8::kGlyphHeight * kHudFontScale; } // Renderer (centered board + HUD) class Renderer { public: - Renderer(IFramebuffer& fb) : fb(fb) { + Renderer(Framebuffer& fb) : fb(fb) { bw = cfg::BoardW * cfg::CellPx; // 10 * 11 = 110 bh = cfg::BoardH * cfg::CellPx; // 20 * 11 = 220 (leaves 10px margins vertically) ox = (fb.width() - bw) / 2; // centered horizontally @@ -375,8 +379,8 @@ public: } private: - IFramebuffer& fb; - int ox = 0, oy = 0, bw = 0, bh = 0; + Framebuffer& fb; + int ox = 0, oy = 0, bw = 0, bh = 0; // Pattern helper: returns true if pixel (xx,yy) inside w x h interior should be filled for piece type bool patternOn(int type, int w, int h, int xx, int yy) const { @@ -926,15 +930,15 @@ private: static constexpr uint32_t idleActivePollMs = 40; // normal_ms provided to PowerHelper for responsiveness static constexpr uint32_t activeNormalCapMs = 40; // cap for normal_ms during active play - AppContext& appContext; - IFramebuffer& fb; - IInput& input; - IClock& clock; - Renderer renderer; - Board board; - Bag bag; - ScoreState score; - bool running = true, paused = false, touchingGround = false; + AppContext& appContext; + Framebuffer& fb; + InputDevice& input; + Clock& clock; + Renderer renderer; + Board board; + Bag bag; + ScoreState score; + bool running = true, paused = false, touchingGround = false; bool rotPrev = false, lHeld = false, rHeld = false, backPrev = false, selectPrev = false, exitComboPrev = false; uint32_t lHoldStart = 0, rHoldStart = 0, lLastRep = 0, rLastRep = 0, lastFall = 0, touchTime = 0; int current = 0, nextPiece = 0, px = 3, py = -2, rot = 0;