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

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

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); },
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();
@@ -235,11 +235,10 @@ public:
refreshRomList();
mode = Mode::Browse;
browserDirty = true;
scheduleNextTick(0);
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,8 +1113,8 @@ public:
cardboy::sdk::IFilesystem* filesystem = nullptr;
cardboy::sdk::IHighResClock* highResClock = nullptr;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
std::optional<std::uint32_t> nextTimeoutMs;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse;
@@ -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;
@@ -75,15 +70,16 @@ public:
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const cardboy::sdk::AppEvent& event) override {
event.visit(cardboy::sdk::overload(
[this](const AppButtonEvent& button) { handleButtonEvent(button); },
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:
@@ -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 {
@@ -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;
@@ -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;
@@ -531,8 +526,7 @@ 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);
const int maxTimeY = std::max(minTimeY, framebuffer.height() - font16x8::kGlyphHeight * scaleTime - 48);
timeY = std::clamp(timeY, minTimeY, maxTimeY);
char hoursMinutes[6];
@@ -570,8 +564,7 @@ private:
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 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); },
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,13 +38,11 @@ public:
renderIfNeeded();
}
void handleEvent(const cardboy::sdk::AppEvent& event) override {
const auto* buttonEvent = event.button();
if (!buttonEvent)
return;
const auto& current = buttonEvent->current;
const auto& previous = buttonEvent->previous;
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 bool previousAvailable = buzzerAvailable;
syncBuzzerState();
@@ -74,6 +72,10 @@ public:
dirty = true;
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;
@@ -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:
@@ -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;
@@ -408,7 +408,7 @@ public:
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
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:
@@ -636,7 +637,7 @@ public:
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
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>
@@ -98,7 +99,7 @@ public:
virtual ~IEventBus() = default;
virtual void post(const AppEvent& event) = 0;
virtual AppEvent pop() = 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>
@@ -78,7 +79,7 @@ public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
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;