faster timeout for games

This commit is contained in:
2025-10-25 13:52:50 +02:00
parent 844cf86d8d
commit 278e822600
13 changed files with 197 additions and 196 deletions

View File

@@ -144,11 +144,14 @@ public:
}
~EventBus() override { vQueueDelete(_queueHandle); }
void post(const sdk::AppEvent& event) override { xQueueSendToBack(_queueHandle, &event, portMAX_DELAY); }
sdk::AppEvent pop() override {
void post(const sdk::AppEvent& event) override { xQueueSendToBack(_queueHandle, &event, portMAX_DELAY); }
std::optional<sdk::AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) override {
sdk::AppEvent out;
xQueueReceive(_queueHandle, &out, portMAX_DELAY);
return out;
TickType_t ticks = timeout_ms ? pdMS_TO_TICKS(*timeout_ms) : portMAX_DELAY;
if (xQueueReceive(_queueHandle, &out, ticks) == pdTRUE) {
return out;
}
return std::nullopt;
}
private:

View File

@@ -18,6 +18,7 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppTimerEvent;
@@ -55,13 +56,14 @@ public:
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer)
updateDisplay();
}));
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer)
updateDisplay();
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:

View File

@@ -154,6 +154,7 @@ class GameboyApp;
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppTimerEvent;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
@@ -224,7 +225,6 @@ public:
::gAudioWriteThunk = &GameboyApp::audioWriteThunk;
apu.attach(this);
apu.reset();
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.resetAll();)
prevInput = context.input.readState();
@@ -233,13 +233,12 @@ public:
scaleMode = ScaleMode::FullHeightWide;
ensureFilesystemReady();
refreshRomList();
mode = Mode::Browse;
browserDirty = true;
scheduleNextTick(0);
mode = Mode::Browse;
browserDirty = true;
nextTimeoutMs = 0;
}
void onStop() override {
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
unloadRom();
@@ -252,14 +251,9 @@ public:
}
}
void handleEvent(const AppEvent& event) override {
bool handled = false;
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this, &handled](const AppTimerEvent& timer) {
if (timer.handle != tickTimer)
return;
handled = true;
tickTimer = kInvalidAppTimer;
[this](const AppTimeoutEvent&) {
const uint64_t frameStartUs = nowMicros();
performStep();
const uint64_t frameEndUs = nowMicros();
@@ -269,10 +263,10 @@ public:
},
[this](const AppButtonEvent&) {
frameDelayCarryUs = 0;
scheduleNextTick(0);
}));
if (handled)
return;
nextTimeoutMs = 0;
},
[](const AppTimerEvent&) { /* ignore */ }));
return nextTimeoutMs;
}
void performStep() {
@@ -1119,9 +1113,9 @@ public:
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
std::optional<std::uint32_t> nextTimeoutMs;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::FullHeightWide;
@@ -1155,20 +1149,6 @@ public:
uint8_t lastLoud = 0;
uint32_t stableFrames = 0;
void cancelTick() {
if (tickTimer == kInvalidAppTimer)
return;
if (auto* timer = context.timer())
timer->cancelTimer(tickTimer);
tickTimer = kInvalidAppTimer;
}
void scheduleNextTick(uint32_t delayMs) {
cancelTick();
if (auto* timer = context.timer())
tickTimer = timer->scheduleTimer(delayMs, false);
}
uint32_t idleDelayMs() const { return browserDirty ? 50 : 140; }
void scheduleAfterFrame(uint64_t elapsedUs) {
@@ -1177,17 +1157,17 @@ public:
desiredUs += frameDelayCarryUs;
if (desiredUs <= 0) {
frameDelayCarryUs = desiredUs;
scheduleNextTick(0);
nextTimeoutMs = 0;
return;
}
frameDelayCarryUs = desiredUs % 1000;
desiredUs -= frameDelayCarryUs;
uint32_t delayMs = static_cast<uint32_t>(desiredUs / 1000);
scheduleNextTick(delayMs);
nextTimeoutMs = delayMs;
return;
}
frameDelayCarryUs = 0;
scheduleNextTick(idleDelayMs());
nextTimeoutMs = idleDelayMs();
}
bool ensureFilesystemReady() {
@@ -1838,7 +1818,7 @@ public:
promptDirty = true;
mode = Mode::Prompt;
gb.direct.joypad = 0xFF;
scheduleNextTick(0);
// scheduleNextTick(0);
}
void exitPrompt(Mode nextMode) {

View File

@@ -5,8 +5,8 @@
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include <array>
#include <algorithm>
#include <array>
#include <cstdio>
#include <ctime>
#include <string>
@@ -18,6 +18,7 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppTimerEvent;
@@ -27,19 +28,13 @@ constexpr std::uint32_t kUnlockHoldMs = 1500;
using Framebuffer = typename AppContext::Framebuffer;
using Clock = typename AppContext::Clock;
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{0b00011000, 0b00111100, 0b01111110,
0b11111111, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00000000,
0b00000000};
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowUpGlyph{
0b00011000, 0b00111100, 0b01111110, 0b11111111, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00000000, 0b00000000};
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{0b00000000, 0b00000000, 0b00011000,
0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b11111111,
0b01111110, 0b00111100, 0b00011000,
0b00000000};
constexpr std::array<std::uint8_t, font16x8::kGlyphHeight> kArrowDownGlyph{
0b00000000, 0b00000000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000, 0b00011000,
0b00011000, 0b00011000, 0b00011000, 0b11111111, 0b01111110, 0b00111100, 0b00011000, 0b00000000};
struct TimeSnapshot {
bool hasWallTime = false;
@@ -60,10 +55,10 @@ public:
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
holdActive = false;
holdProgressMs = 0;
dirty = true;
lastSnapshot = {};
holdActive = false;
holdProgressMs = 0;
dirty = true;
lastNotificationInteractionMs = clock.millis();
refreshNotifications();
const auto snap = captureTime();
@@ -75,33 +70,34 @@ public:
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer) {
advanceHoldProgress();
updateDisplay();
}
}));
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == refreshTimer) {
advanceHoldProgress();
updateDisplay();
}
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:
static constexpr std::size_t kMaxDisplayedNotifications = 5;
static constexpr std::uint32_t kNotificationHideMs = 8000;
AppContext& context;
Framebuffer& framebuffer;
Clock& clock;
cardboy::sdk::INotificationCenter* notificationCenter = nullptr;
std::uint32_t lastNotificationRevision = 0;
static constexpr std::size_t kMaxDisplayedNotifications = 5;
static constexpr std::uint32_t kNotificationHideMs = 8000;
AppContext& context;
Framebuffer& framebuffer;
Clock& clock;
cardboy::sdk::INotificationCenter* notificationCenter = nullptr;
std::uint32_t lastNotificationRevision = 0;
std::vector<cardboy::sdk::INotificationCenter::Notification> notifications;
std::size_t selectedNotification = 0;
std::size_t selectedNotification = 0;
bool dirty = false;
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
bool holdActive = false;
std::uint32_t holdProgressMs = 0;
bool holdActive = false;
std::uint32_t holdProgressMs = 0;
std::uint32_t lastNotificationInteractionMs = 0;
void cancelRefreshTimer() {
@@ -120,7 +116,7 @@ private:
bool navPressed = false;
if (!notifications.empty() && (upPressed || downPressed)) {
const std::size_t count = notifications.size();
const std::size_t count = notifications.size();
lastNotificationInteractionMs = clock.millis();
navPressed = true;
if (count > 1) {
@@ -133,8 +129,8 @@ private:
const bool deletePressed = button.current.b && !button.previous.b;
if (deletePressed && notificationCenter && !notifications.empty()) {
const std::size_t index = std::min<std::size_t>(selectedNotification, notifications.size() - 1);
const auto& note = notifications[index];
const std::size_t index = std::min<std::size_t>(selectedNotification, notifications.size() - 1);
const auto& note = notifications[index];
std::size_t preferredIndex = index;
if (index + 1 < notifications.size())
preferredIndex = index + 1;
@@ -144,9 +140,9 @@ private:
notificationCenter->removeByExternalId(note.externalId);
else
notificationCenter->removeById(note.id);
selectedNotification = preferredIndex;
selectedNotification = preferredIndex;
lastNotificationInteractionMs = clock.millis();
dirty = true;
dirty = true;
refreshNotifications();
}
@@ -161,9 +157,9 @@ private:
if (!notificationCenter) {
if (!notifications.empty() || lastNotificationRevision != 0) {
notifications.clear();
selectedNotification = 0;
lastNotificationRevision = 0;
dirty = true;
selectedNotification = 0;
lastNotificationRevision = 0;
dirty = true;
}
return;
}
@@ -175,7 +171,7 @@ private:
const std::uint64_t previousId =
(selectedNotification < notifications.size()) ? notifications[selectedNotification].id : 0;
auto latest = notificationCenter->recent(kMaxDisplayedNotifications);
auto latest = notificationCenter->recent(kMaxDisplayedNotifications);
notifications = std::move(latest);
if (notifications.empty()) {
@@ -193,7 +189,7 @@ private:
}
lastNotificationInteractionMs = clock.millis();
dirty = true;
dirty = true;
}
void updateHoldState(bool comboNow) {
@@ -213,8 +209,7 @@ private:
void advanceHoldProgress() {
if (holdActive) {
const std::uint32_t next =
std::min<std::uint32_t>(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs);
const std::uint32_t next = std::min<std::uint32_t>(holdProgressMs + kRefreshIntervalMs, kUnlockHoldMs);
if (next != holdProgressMs) {
holdProgressMs = next;
dirty = true;
@@ -239,8 +234,8 @@ private:
}
static bool sameSnapshot(const TimeSnapshot& a, const TimeSnapshot& b) {
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute &&
a.second == b.second && a.day == b.day && a.month == b.month && a.year == b.year;
return a.hasWallTime == b.hasWallTime && a.hour24 == b.hour24 && a.minute == b.minute && a.second == b.second &&
a.day == b.day && a.month == b.month && a.year == b.year;
}
TimeSnapshot captureTime() const {
@@ -327,7 +322,7 @@ private:
if (font16x8::measureText(text, scale, letterSpacing) <= maxWidth)
return std::string(text);
std::string result(text.begin(), text.end());
std::string result(text.begin(), text.end());
const std::string ellipsis = "...";
while (!result.empty()) {
result.pop_back();
@@ -369,8 +364,7 @@ private:
if (!word.empty()) {
std::string candidate = current.empty() ? word : current + " " + word;
if (!current.empty() &&
font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
if (!current.empty() && font16x8::measureText(candidate, scale, letterSpacing) > maxWidth) {
flushCurrent();
if (lines.size() >= static_cast<std::size_t>(maxLines)) {
truncated = true;
@@ -432,20 +426,20 @@ private:
framebuffer.frameReady();
const int scaleTime = 4;
const int scaleSeconds = 2;
const int scaleSmall = 1;
const int scaleTime = 4;
const int scaleSeconds = 2;
const int scaleSmall = 1;
const int textLineHeight = font16x8::kGlyphHeight * scaleSmall;
const int cardMarginTop = 4;
const int cardMarginSide = 8;
const int cardPadding = 6;
const int cardMarginTop = 4;
const int cardMarginSide = 8;
const int cardPadding = 6;
const int cardLineSpacing = 4;
int cardHeight = 0;
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
int cardHeight = 0;
const int cardWidth = framebuffer.width() - cardMarginSide * 2;
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const std::uint32_t nowMs = clock.millis();
const bool hasNotifications = !notifications.empty();
const bool showNotificationDetails =
hasNotifications && (nowMs - lastNotificationInteractionMs <= kNotificationHideMs);
@@ -481,7 +475,8 @@ private:
std::snprintf(counter, sizeof(counter), "%zu/%zu", selectedNotification + 1, notifications.size());
const int counterWidth = font16x8::measureText(counter, scaleSmall, 1);
const int counterX = cardMarginSide + cardWidth - cardPadding - counterWidth;
font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true, 1);
font16x8::drawText(framebuffer, counterX, cardMarginTop + cardPadding, counter, scaleSmall, true,
1);
const int arrowWidth = font16x8::kGlyphWidth * scaleSmall;
const int arrowSpacing = std::max(1, scaleSmall);
const int arrowsTotalWide = arrowWidth * 2 + arrowSpacing;
@@ -495,7 +490,7 @@ private:
if (!bodyLines.empty()) {
int bodyY = cardMarginTop + cardPadding + textLineHeight + cardLineSpacing;
for (const auto& line : bodyLines) {
for (const auto& line: bodyLines) {
font16x8::drawText(framebuffer, cardMarginSide + cardPadding, bodyY, line, scaleSmall, true, 1);
bodyY += textLineHeight + cardLineSpacing;
}
@@ -531,9 +526,8 @@ private:
if (cardHeight > 0)
timeY = cardMarginTop + cardHeight + 16;
const int minTimeY = (cardHeight > 0) ? (cardMarginTop + cardHeight + 12) : 16;
const int maxTimeY =
std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
timeY = std::clamp(timeY, minTimeY, maxTimeY);
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
timeY = std::clamp(timeY, minTimeY, maxTimeY);
char hoursMinutes[6];
std::snprintf(hoursMinutes, sizeof(hoursMinutes), "%02d:%02d", snap.hour24, snap.minute);
@@ -541,7 +535,7 @@ private:
const int timeX = (framebuffer.width() - mainW) / 2;
const int secX = timeX + mainW + 12;
const int secY = timeY + font16x8::kGlyphHeight * scaleTime - font16x8::kGlyphHeight * scaleSeconds;
char secs[3];
char secs[3];
std::snprintf(secs, sizeof(secs), "%02d", snap.second);
font16x8::drawText(framebuffer, timeX, timeY, hoursMinutes, scaleTime, true, 0);
@@ -550,7 +544,7 @@ private:
const std::string dateLine = formatDate(snap);
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleTime + 16, dateLine, scaleSmall, 1);
const std::string instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT";
const std::string instruction = holdActive ? "KEEP HOLDING A+SELECT" : "HOLD A+SELECT";
const int instructionWidth = font16x8::measureText(instruction, scaleSmall, 1);
const int barHeight = 14;
const int barY = framebuffer.height() - 24;
@@ -568,11 +562,10 @@ private:
drawRectOutline(framebuffer, barX, barY, barWidth, barHeight);
if (holdActive || holdProgressMs > 0) {
const int innerWidth = barWidth - 2;
const int innerHeight = barHeight - 2;
const float ratio =
std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
const int fillWidth = static_cast<int>(ratio * innerWidth + 0.5f);
const int innerWidth = barWidth - 2;
const int innerHeight = barHeight - 2;
const float ratio = std::clamp(holdProgressMs / static_cast<float>(kUnlockHoldMs), 0.0f, 1.0f);
const int fillWidth = static_cast<int>(ratio * innerWidth + 0.5f);
if (fillWidth > 0)
fillRect(framebuffer, barX + 1, barY + 1, fillWidth, innerHeight);
}

View File

@@ -18,6 +18,7 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppTimerEvent;
@@ -44,15 +45,16 @@ public:
void onStop() override { cancelInactivityTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == inactivityTimer) {
cancelInactivityTimer();
context.requestAppSwitchByName(kLockscreenAppName);
}
}));
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtonEvent(button); },
[this](const AppTimerEvent& timer) {
if (timer.handle == inactivityTimer) {
cancelInactivityTimer();
context.requestAppSwitchByName(kLockscreenAppName);
}
},
[](const AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:

View File

@@ -38,42 +38,44 @@ public:
renderIfNeeded();
}
void handleEvent(const cardboy::sdk::AppEvent& event) override {
const auto* buttonEvent = event.button();
if (!buttonEvent)
return;
std::optional<std::uint32_t> handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const cardboy::sdk::AppButtonEvent& button) {
const auto& current = button.current;
const auto& previous = button.previous;
const auto& current = buttonEvent->current;
const auto& previous = buttonEvent->previous;
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (current.b && !previous.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
bool moved = false;
if (current.down && !previous.down) {
moveSelection(+1);
moved = true;
} else if (current.up && !previous.up) {
moveSelection(-1);
moved = true;
}
bool moved = false;
if (current.down && !previous.down) {
moveSelection(+1);
moved = true;
} else if (current.up && !previous.up) {
moveSelection(-1);
moved = true;
}
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
if (moved)
dirty = true;
if (moved)
dirty = true;
renderIfNeeded();
renderIfNeeded();
},
[](const cardboy::sdk::AppTimerEvent&) { /* ignore */ },
[](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ }));
return std::nullopt;
}
private:

View File

@@ -20,6 +20,7 @@ namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppTimeoutEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppTimerEvent;
@@ -28,13 +29,13 @@ using cardboy::sdk::InputState;
constexpr char kSnakeAppName[] = "Snake";
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 20;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;
constexpr int kMinMoveIntervalMs = 80;
constexpr int kBaseMoveIntervalMs = 220;
constexpr int kBoardWidth = 32;
constexpr int kBoardHeight = 20;
constexpr int kCellSize = 10;
constexpr int kInitialSnakeLength = 5;
constexpr int kScorePerFood = 10;
constexpr int kMinMoveIntervalMs = 80;
constexpr int kBaseMoveIntervalMs = 220;
constexpr int kIntervalSpeedupPerSegment = 4;
struct Point {
@@ -69,11 +70,12 @@ public:
void onStop() { cancelMoveTimer(); }
void handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); }));
std::optional<std::uint32_t> handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); },
[](const AppTimeoutEvent&) { /* ignore */ }));
renderIfNeeded();
return std::nullopt;
}
private:
@@ -89,8 +91,8 @@ private:
bool dirty = false;
int score = 0;
int highScore = 0;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
AppTimerHandle moveTimer = cardboy::sdk::kInvalidAppTimer;
std::mt19937 rng;
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
@@ -158,7 +160,7 @@ private:
}
void advance() {
direction = queuedDirection;
direction = queuedDirection;
Point nextHead = snake.front();
switch (direction) {
case Direction::Up:
@@ -299,9 +301,7 @@ private:
framebuffer.sendFrame();
}
[[nodiscard]] int boardOriginX() const {
return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
}
[[nodiscard]] int boardOriginX() const { return (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2; }
[[nodiscard]] int boardOriginY() const {
const int centered = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
@@ -406,9 +406,9 @@ class SnakeApp final : public cardboy::sdk::IApp {
public:
explicit SnakeApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override { return game.handleEvent(event); }
private:
SnakeGame game;

View File

@@ -148,11 +148,12 @@ public:
void onStop() { cancelTimers(); }
void handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); }));
std::optional<std::uint32_t> handleEvent(const AppEvent& event) {
event.visit(cardboy::sdk::overload([this](const AppButtonEvent& button) { handleButtons(button); },
[this](const AppTimerEvent& timer) { handleTimer(timer.handle); },
[](const cardboy::sdk::AppTimeoutEvent&) { /* ignore */ }));
renderIfNeeded();
return std::nullopt;
}
private:
@@ -634,9 +635,9 @@ class TetrisApp final : public cardboy::sdk::IApp {
public:
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
std::optional<std::uint32_t> handleEvent(const AppEvent& event) override { return game.handleEvent(event); }
private:
TetrisGame game;

View File

@@ -21,14 +21,17 @@ struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppTimeoutEvent {};
struct AppEvent {
using Data = std::variant<AppButtonEvent, AppTimerEvent>;
using Data = std::variant<AppButtonEvent, AppTimerEvent, AppTimeoutEvent>;
std::uint32_t timestamp_ms = 0;
Data data{AppButtonEvent{}};
[[nodiscard]] bool isButton() const { return std::holds_alternative<AppButtonEvent>(data); }
[[nodiscard]] bool isTimer() const { return std::holds_alternative<AppTimerEvent>(data); }
[[nodiscard]] bool isTimeout() const { return std::holds_alternative<AppTimeoutEvent>(data); }
[[nodiscard]] const AppButtonEvent* button() const { return std::get_if<AppButtonEvent>(&data); }
[[nodiscard]] AppButtonEvent* button() { return std::get_if<AppButtonEvent>(&data); }
@@ -36,6 +39,9 @@ struct AppEvent {
[[nodiscard]] const AppTimerEvent* timer() const { return std::get_if<AppTimerEvent>(&data); }
[[nodiscard]] AppTimerEvent* timer() { return std::get_if<AppTimerEvent>(&data); }
[[nodiscard]] const AppTimeoutEvent* timeout() const { return std::get_if<AppTimeoutEvent>(&data); }
[[nodiscard]] AppTimeoutEvent* timeout() { return std::get_if<AppTimeoutEvent>(&data); }
template<typename Visitor>
decltype(auto) visit(Visitor&& visitor) {
return std::visit(std::forward<Visitor>(visitor), data);

View File

@@ -5,6 +5,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -97,8 +98,8 @@ class IEventBus {
public:
virtual ~IEventBus() = default;
virtual void post(const AppEvent& event) = 0;
virtual AppEvent pop() = 0;
virtual void post(const AppEvent& event) = 0;
virtual std::optional<AppEvent> pop(std::optional<std::uint32_t> timeout_ms = std::nullopt) = 0;
};
struct AppScopedServices {

View File

@@ -7,6 +7,7 @@
#include <cstdint>
#include <memory>
#include <optional>
#include <string>
#include <string_view>
#include <vector>
@@ -76,9 +77,9 @@ private:
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
virtual void onStart() {}
virtual void onStop() {}
virtual std::optional<std::uint32_t> handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {

View File

@@ -39,6 +39,7 @@ private:
std::size_t _activeIndex = static_cast<std::size_t>(-1);
std::unique_ptr<AppScopedServices> _scopedServices;
std::uint64_t _nextScopedGeneration = 1;
std::optional<std::uint32_t> _currentTimeout;
};
} // namespace cardboy::sdk

View File

@@ -3,6 +3,7 @@
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <optional>
#include <utility>
namespace cardboy::sdk {
@@ -66,6 +67,7 @@ void AppSystem::startAppByIndex(std::size_t index) {
_context._pendingSwitchByName = false;
_context._pendingAppName.clear();
_current = std::move(app);
_currentTimeout = std::nullopt;
StatusBar::instance().setServices(_context.services);
StatusBar::instance().setCurrentAppName(_activeFactory ? _activeFactory->name() : "");
_current->onStart();
@@ -81,14 +83,21 @@ void AppSystem::run() {
if (auto* hooks = _context.loopHooks())
hooks->onLoopIteration();
auto event = _context.eventBus()->pop();
AppEvent event;
auto event_opt = _context.eventBus()->pop(_currentTimeout);
if (!event_opt) {
event = AppEvent{_context.clock.millis(), AppTimeoutEvent{}};
} else {
event = *event_opt;
}
if (const auto* btn = event.button()) {
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(btn->current, btn->previous);
if (consumedByStatusToggle) {
continue;
}
}
_current->handleEvent(event);
_currentTimeout = _current->handleEvent(event);
if (_context._pendingSwitch) {
handlePendingSwitchRequest();
continue;