This commit is contained in:
2025-10-09 09:26:34 +02:00
parent 13cdcb01dd
commit ddf5a47c33
9 changed files with 135 additions and 126 deletions

View File

@@ -1,5 +1,8 @@
#pragma once #pragma once
#include "app_platform.hpp"
#include "input_state.hpp"
#include <cstdint> #include <cstdint>
#include <memory> #include <memory>
#include <string> #include <string>
@@ -8,46 +11,18 @@
class AppSystem; class AppSystem;
struct InputState { template<typename FramebufferT, typename InputT, typename ClockT>
bool up = false; struct BasicAppContext {
bool left = false; using Framebuffer = FramebufferT;
bool right = false; using Input = InputT;
bool down = false; using Clock = ClockT;
bool a = false;
bool b = false;
bool select = false;
bool start = false;
};
class IFramebuffer { BasicAppContext() = delete;
public: BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
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;
};
class IInput { FramebufferT& framebuffer;
public: InputT& input;
virtual ~IInput() = default; ClockT& clock;
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;
AppSystem* system = nullptr; AppSystem* system = nullptr;
void requestAppSwitchByIndex(std::size_t index) { void requestAppSwitchByIndex(std::size_t index) {
@@ -73,6 +48,8 @@ private:
std::string pendingAppName; std::string pendingAppName;
}; };
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
struct AppSleepPlan { struct AppSleepPlan {
uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh uint32_t slow_ms = 0; // long sleep allowing battery/UI periodic refresh
uint32_t normal_ms = 0; // short sleep for responsiveness on input wake uint32_t normal_ms = 0; // short sleep for responsiveness on input wake

View File

@@ -0,0 +1,64 @@
#pragma once
#include "config.hpp"
#include "input_state.hpp"
#include <buttons.hpp>
#include <disp_tools.hpp>
#include <power_helper.hpp>
#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<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
}
void sleep_ms(uint32_t ms) { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
};

View File

@@ -9,8 +9,8 @@
namespace font16x8 { namespace font16x8 {
constexpr int kGlyphWidth = 8; constexpr int kGlyphWidth = 8;
constexpr int kGlyphHeight = 16; constexpr int kGlyphHeight = 16;
constexpr unsigned char kFallbackChar = '?'; constexpr unsigned char kFallbackChar = '?';
inline unsigned char normalizeChar(char ch) { inline unsigned char normalizeChar(char ch) {
@@ -27,7 +27,8 @@ inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
return fonts_Terminess_Powerline[uc]; return fonts_Terminess_Powerline[uc];
} }
inline void drawGlyph(IFramebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) { template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
const auto& rows = glyphBitmap(ch); const auto& rows = glyphBitmap(ch);
for (int row = 0; row < kGlyphHeight; ++row) { for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[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<int>(text.size()) * advance - letterSpacing * scale; return static_cast<int>(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<typename Framebuffer>
inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true,
int letterSpacing = 1) { int letterSpacing = 1) {
int cursor = x; int cursor = x;
for (char ch: text) { for (char ch: text) {

View File

@@ -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;
};

View File

@@ -14,8 +14,8 @@
#include <buzzer.hpp> #include <buzzer.hpp>
#include <disp_tools.hpp> #include <disp_tools.hpp>
#include <display.hpp> #include <display.hpp>
#include <i2c_global.hpp>
#include <fs_helper.hpp> #include <fs_helper.hpp>
#include <i2c_global.hpp>
#include <power_helper.hpp> #include <power_helper.hpp>
#include <shutdowner.hpp> #include <shutdowner.hpp>
#include <spi_global.hpp> #include <spi_global.hpp>
@@ -29,63 +29,6 @@
#include "esp_sleep.h" #include "esp_sleep.h"
#include "sdkconfig.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<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
}
void sleep_ms(uint32_t ms) override { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
};
} // namespace
extern "C" void app_main() { extern "C" void app_main() {
#ifdef CONFIG_PM_ENABLE #ifdef CONFIG_PM_ENABLE
// const esp_pm_config_t pm_config = { // const esp_pm_config_t pm_config = {

View File

@@ -19,6 +19,9 @@ namespace {
constexpr const char* kClockAppName = "Clock"; constexpr const char* kClockAppName = "Clock";
using Framebuffer = typename AppContext::Framebuffer;
using Clock = typename AppContext::Clock;
struct TimeSnapshot { struct TimeSnapshot {
bool hasWallTime = false; bool hasWallTime = false;
int hour24 = 0; int hour24 = 0;
@@ -76,9 +79,9 @@ public:
} }
private: private:
AppContext& context; AppContext& context;
IFramebuffer& framebuffer; Framebuffer& framebuffer;
IClock& clock; Clock& clock;
bool use24Hour = true; bool use24Hour = true;
bool dirty = false; bool dirty = false;
@@ -119,7 +122,7 @@ private:
return snap; 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 width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2; const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);

View File

@@ -32,6 +32,8 @@ namespace {
constexpr int kMenuStartY = 48; constexpr int kMenuStartY = 48;
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
using Framebuffer = typename AppContext::Framebuffer;
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"}; constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
extern "C" { extern "C" {
@@ -116,7 +118,7 @@ int measureVerticalText(std::string_view text, int scale = 1, int letterSpacing
return static_cast<int>(text.size()) * advance - letterSpacing * scale; return static_cast<int>(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); const auto& rows = font16x8::glyphBitmap(ch);
for (int row = 0; row < font16x8::kGlyphHeight; ++row) { for (int row = 0; row < font16x8::kGlyphHeight; ++row) {
const uint8_t rowBits = rows[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) { bool on = true, int letterSpacing = 1) {
int cursor = y; int cursor = y;
const int advance = (font16x8::kGlyphWidth + letterSpacing) * scale; const int advance = (font16x8::kGlyphWidth + letterSpacing) * scale;
@@ -482,9 +484,9 @@ private:
std::array<int, LCD_WIDTH> colXEnd{}; std::array<int, LCD_WIDTH> colXEnd{};
}; };
AppContext& context; AppContext& context;
IFramebuffer& framebuffer; Framebuffer& framebuffer;
PerfTracker perf{}; PerfTracker perf{};
Mode mode = Mode::Browse; Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original; ScaleMode scaleMode = ScaleMode::Original;
@@ -1129,7 +1131,7 @@ private:
auto* self = fromGb(gb); auto* self = fromGb(gb);
if (!self) if (!self)
return 0xFF; return 0xFF;
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead); // ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead);
if (!self->romDataView || addr >= self->romDataViewSize) if (!self->romDataView || addr >= self->romDataViewSize)
return 0xFF; return 0xFF;
return self->romDataView[static_cast<std::size_t>(addr)]; return self->romDataView[static_cast<std::size_t>(addr)];
@@ -1186,7 +1188,7 @@ private:
self->frameDirty = true; self->frameDirty = true;
IFramebuffer& fb = self->framebuffer; Framebuffer& fb = self->framebuffer;
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) { if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
const int dstY = yStart; const int dstY = yStart;

View File

@@ -15,6 +15,8 @@ namespace apps {
namespace { namespace {
using Framebuffer = typename AppContext::Framebuffer;
struct MenuEntry { struct MenuEntry {
std::string name; std::string name;
std::size_t index = 0; std::size_t index = 0;
@@ -60,7 +62,7 @@ public:
private: private:
AppContext& context; AppContext& context;
IFramebuffer& framebuffer; Framebuffer& framebuffer;
std::vector<MenuEntry> entries; std::vector<MenuEntry> entries;
std::size_t selected = 0; std::size_t selected = 0;
@@ -110,7 +112,7 @@ private:
dirty = true; 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 width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2; const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing); font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);

View File

@@ -103,6 +103,10 @@ using RotGrid = std::array<char, 16>;
using PieceR = std::array<RotGrid, 4>; using PieceR = std::array<RotGrid, 4>;
constexpr char _ = '.', X = '#'; constexpr char _ = '.', X = '#';
using Framebuffer = typename AppContext::Framebuffer;
using InputDevice = typename AppContext::Input;
using Clock = typename AppContext::Clock;
static const std::array<PieceR, 7> TET = {{ static const std::array<PieceR, 7> TET = {{
PieceR{{RotGrid{_, _, _, _, X, X, X, X, _, _, _, _, _, _, _, _}, PieceR{{RotGrid{_, _, _, _, X, X, X, X, _, _, _, _, _, _, _, _},
RotGrid{_, _, X, _, _, _, X, _, _, _, X, _, _, _, X, _}, RotGrid{_, _, X, _, _, _, X, _, _, _, X, _, _, _, X, _},
@@ -201,7 +205,7 @@ constexpr int kHudLabelGap = 2;
constexpr int kHudBlockGap = 8; constexpr int kHudBlockGap = 8;
constexpr int kHudLineGapBattery = 4; 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); font16x8::drawText(fb, x, y, text, kHudFontScale, true, kHudLetterSpacing);
} }
@@ -211,7 +215,7 @@ inline int hudFontHeight() { return font16x8::kGlyphHeight * kHudFontScale; }
// Renderer (centered board + HUD) // Renderer (centered board + HUD)
class Renderer { class Renderer {
public: public:
Renderer(IFramebuffer& fb) : fb(fb) { Renderer(Framebuffer& fb) : fb(fb) {
bw = cfg::BoardW * cfg::CellPx; // 10 * 11 = 110 bw = cfg::BoardW * cfg::CellPx; // 10 * 11 = 110
bh = cfg::BoardH * cfg::CellPx; // 20 * 11 = 220 (leaves 10px margins vertically) bh = cfg::BoardH * cfg::CellPx; // 20 * 11 = 220 (leaves 10px margins vertically)
ox = (fb.width() - bw) / 2; // centered horizontally ox = (fb.width() - bw) / 2; // centered horizontally
@@ -375,8 +379,8 @@ public:
} }
private: private:
IFramebuffer& fb; Framebuffer& fb;
int ox = 0, oy = 0, bw = 0, bh = 0; 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 // 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 { 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 idleActivePollMs = 40; // normal_ms provided to PowerHelper for responsiveness
static constexpr uint32_t activeNormalCapMs = 40; // cap for normal_ms during active play static constexpr uint32_t activeNormalCapMs = 40; // cap for normal_ms during active play
AppContext& appContext; AppContext& appContext;
IFramebuffer& fb; Framebuffer& fb;
IInput& input; InputDevice& input;
IClock& clock; Clock& clock;
Renderer renderer; Renderer renderer;
Board board; Board board;
Bag bag; Bag bag;
ScoreState score; ScoreState score;
bool running = true, paused = false, touchingGround = false; bool running = true, paused = false, touchingGround = false;
bool rotPrev = false, lHeld = false, rHeld = false, backPrev = false, selectPrev = false, exitComboPrev = 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; 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; int current = 0, nextPiece = 0, px = 3, py = -2, rot = 0;