Compare commits

...

30 Commits

Author SHA1 Message Date
7a5d1c2819 dump 2025-10-13 20:13:32 +02:00
1bc5b75dba format 2025-10-13 00:39:34 +02:00
e37f8e3dc8 a little better sound 2025-10-13 00:39:26 +02:00
df7c4ff3b9 even better sound 2025-10-13 00:27:45 +02:00
07186b4b73 sound upgrade 2025-10-13 00:17:16 +02:00
6a1f7d48ce simple music 2025-10-13 00:05:56 +02:00
031ff1952b get rid of powerhelper 2025-10-12 22:54:18 +02:00
df55b8f2e1 hang fix fix 2025-10-12 20:43:13 +02:00
ed1cee82d2 fix hanging 2 2025-10-12 20:37:39 +02:00
7474f65aaa fix hanging 2025-10-12 20:37:30 +02:00
088c6e47bd better timers 2025-10-12 20:34:05 +02:00
d91b7540fc fix intr 2025-10-12 17:44:19 +02:00
d5506b9455 fix gameboy autostart 2025-10-12 17:32:33 +02:00
db88e16aaa better controls 2025-10-12 17:29:15 +02:00
a6713859b2 better statusbar 2025-10-12 17:16:07 +02:00
aaac0514c0 more settings 2025-10-12 17:11:22 +02:00
1b6e9a0f78 settings app 2025-10-12 16:57:40 +02:00
c64f03a09f status bar 2 2025-10-12 15:18:42 +02:00
5ab8662332 statusbar 2025-10-12 15:03:34 +02:00
6d8834d9b2 nicer tetris 2025-10-12 14:04:06 +02:00
83ba775971 some stuff 2025-10-12 13:51:50 +02:00
df57e55171 better default scale mode 2025-10-12 00:52:58 +02:00
a3b837f329 a bit more speedup 2025-10-12 00:33:48 +02:00
fc9e85aea0 fast wide scale 2025-10-12 00:20:22 +02:00
b55feb68f8 some opt 2025-10-12 00:03:01 +02:00
f04b026d46 a little faster gameboy 2025-10-11 22:21:31 +02:00
e18278e130 disable analyzer 2025-10-11 20:59:29 +02:00
9a392d6aec check macro 2025-10-11 20:36:43 +02:00
961453e28a 8bit draw 2025-10-11 20:03:00 +02:00
a4c2719077 text 2025-10-11 16:54:41 +02:00
54 changed files with 2993 additions and 654 deletions

4
Firmware/AGENTS.md Normal file
View File

@@ -0,0 +1,4 @@
To build:
(in zsh)
. "$HOME/esp/esp-idf/export.sh"
idf.py build

View File

@@ -4,10 +4,10 @@ idf_component_register(
"src/buttons.cpp"
"src/buzzer.cpp"
"src/esp_backend.cpp"
"src/event_bus.cpp"
"src/display.cpp"
"src/fs_helper.cpp"
"src/i2c_global.cpp"
"src/power_helper.cpp"
"src/shutdowner.cpp"
"src/spi_global.cpp"
INCLUDE_DIRS
@@ -21,6 +21,7 @@ idf_component_register(
nvs_flash
)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/utils" cardboy_utils_esp)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/backend_interface" backend_interface_from_backend_esp)
add_library(cardboy_backend_esp INTERFACE)

View File

@@ -5,9 +5,11 @@
#ifndef BUTTONS_HPP
#define BUTTONS_HPP
#include "cardboy/sdk/event_bus.hpp"
#include <cstdint>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include <cstdint>
typedef enum {
BTN_START = 1 << 1,
@@ -25,16 +27,18 @@ public:
static Buttons& get();
void pooler(); // FIXME:
uint8_t get_pressed();
void install_isr();
void register_listener(TaskHandle_t task);
void install_isr();
void register_listener(TaskHandle_t task);
void setEventBus(cardboy::sdk::IEventBus* bus);
TaskHandle_t _pooler_task;
TaskHandle_t _pooler_task;
private:
Buttons();
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
volatile uint8_t _current;
volatile TaskHandle_t _listener = nullptr;
cardboy::sdk::IEventBus* _eventBus = nullptr;
};

View File

@@ -6,7 +6,7 @@
class Buzzer {
public:
static Buzzer &get();
static Buzzer& get();
void init(); // call once from app_main
@@ -17,8 +17,8 @@ public:
void beepRotate();
void beepMove();
void beepLock();
void beepLines(int lines); // 1..4 lines
void beepLevelUp(int level); // after increment
void beepLines(int lines); // 1..4 lines
void beepLevelUp(int level); // after increment
void beepGameOver();
// Mute controls
@@ -26,29 +26,29 @@ public:
void toggleMuted();
bool isMuted() const { return _muted; }
// Persistence
void loadState();
void saveState();
private:
struct Step { uint32_t freq; uint32_t dur_ms; uint32_t gap_ms; };
struct Step {
uint32_t freq;
uint32_t dur_ms;
uint32_t gap_ms;
};
static constexpr int MAX_QUEUE = 16;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = false;
bool _in_gap = false;
void *_timer = nullptr; // esp_timer_handle_t (opaque here)
bool _muted = false;
Step _queue[MAX_QUEUE]{};
int _q_head = 0; // inclusive
int _q_tail = 0; // exclusive
bool _running = false;
bool _in_gap = false;
void* _timer = nullptr; // esp_timer_handle_t (opaque here)
bool _muted = false;
Buzzer() = default;
void enqueue(const Step &s);
bool empty() const { return _q_head == _q_tail; }
Step &front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void *arg);
void clearQueue() { _q_head = _q_tail = 0; }
void enqueue(const Step& s);
bool empty() const { return _q_head == _q_tail; }
Step& front() { return _queue[_q_head]; }
void popFront();
void startNext();
void schedule(uint32_t ms, bool gapPhase);
void applyFreq(uint32_t freq);
static void timerCb(void* arg);
void clearQueue() { _q_head = _q_tail = 0; }
};

View File

@@ -20,7 +20,7 @@
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
#define BUZZER_PIN GPIO_NUM_25
#define BUZZER_PIN GPIO_NUM_22
#define PWR_INT GPIO_NUM_10
#define PWR_KILL GPIO_NUM_12

View File

@@ -6,6 +6,7 @@
#define CB_DISPLAY_HPP
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/utils/utils.hpp"
#include "driver/spi_master.h"
// (Async memcpy removed for debugging simplification)
@@ -13,7 +14,7 @@
#include <array>
#include <bitset>
#include <cassert>
#include <cstdint>
namespace SMD {
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
@@ -34,7 +35,7 @@ void frame_ready();
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
@@ -46,6 +47,14 @@ __attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
}
}
__attribute__((always_inline)) static void set_pixel_8(int x, int y, std::uint8_t value) {
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK((x % 8) == 0);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
dma_buf[lineIdx] = ~value;
}
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
static inline spi_device_interface_config_t _devcfg = {

View File

@@ -0,0 +1,27 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
#include "freertos/timers.h"
namespace cardboy::backend::esp {
class EventBus final : public cardboy::sdk::IEventBus {
public:
EventBus();
~EventBus() override;
void signal(std::uint32_t bits) override;
void signalFromISR(std::uint32_t bits) override;
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
void scheduleTimerSignal(std::uint32_t delay_ms) override;
void cancelTimerSignal() override;
private:
EventGroupHandle_t group;
TimerHandle_t timer;
};
} // namespace cardboy::backend::esp

View File

@@ -22,6 +22,7 @@ private:
.scl_io_num = I2C_SCL,
.clk_source = I2C_CLK_SRC_DEFAULT,
.glitch_ignore_cnt = 7,
.intr_priority = 2,
.flags = {.enable_internal_pullup = true, .allow_pd = true},
};
i2c_master_bus_handle_t _bus_handle;

View File

@@ -1,30 +0,0 @@
//
// Created by Stepan Usatiuk on 03.03.2025.
//
#ifndef POWER_HELPER_HPP
#define POWER_HELPER_HPP
#include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
class PowerHelper {
public:
static PowerHelper& get();
bool is_slow() const;
void set_slow(bool slow);
BaseType_t reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken);
void delay(int slow_ms, int normal_ms);
void install_isr();
private:
PowerHelper();
bool _slow = false;
EventGroupHandle_t _event_group;
};
#endif // POWER_HELPER_HPP

View File

@@ -1,5 +1,8 @@
#pragma once
#include <cardboy/sdk/display_spec.hpp>
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -14,9 +17,12 @@ class EspFramebuffer final : public cardboy::sdk::FramebufferFacade<EspFramebuff
public:
EspFramebuffer() = default;
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
[[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
[[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
void __attribute__((always_inline)) drawPixel_impl(int x, int y, bool on) { SMD::set_pixel(x, y, on); }
void __attribute__((always_inline)) drawBits8_impl(int x, int y, std::uint8_t bits) {
SMD::set_pixel_8(x, y, bits);
}
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
@@ -53,16 +59,17 @@ private:
class StorageService;
class RandomService;
class HighResClockService;
class PowerService;
class FilesystemService;
class LoopHooksService;
std::unique_ptr<BuzzerService> buzzerService;
std::unique_ptr<BatteryService> batteryService;
std::unique_ptr<StorageService> storageService;
std::unique_ptr<RandomService> randomService;
std::unique_ptr<HighResClockService> highResClockService;
std::unique_ptr<PowerService> powerService;
std::unique_ptr<FilesystemService> filesystemService;
std::unique_ptr<EventBus> eventBus;
std::unique_ptr<LoopHooksService> loopHooksService;
cardboy::sdk::Services services{};
};

View File

@@ -4,8 +4,6 @@
#include "cardboy/backend/esp/bat_mon.hpp"
#include "cardboy/backend/esp/power_helper.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
@@ -105,7 +103,7 @@ void BatMon::pooler() {
_charge = capToMah(ReadRegister(0x05));
_current = regToCurrent(ReadRegister(0x0B));
_voltage = regToVoltage(ReadRegister(0x09));
PowerHelper::get().delay(10000, 1000);
vTaskDelay(pdMS_TO_TICKS(10000));
if (_voltage < 3.0f) {
Shutdowner::get().shutdown();
}

View File

@@ -6,7 +6,6 @@
#include <driver/gpio.h>
#include <esp_err.h>
#include "cardboy/backend/esp/power_helper.hpp"
#include <rom/ets_sys.h>
#include "freertos/FreeRTOS.h"
@@ -14,6 +13,7 @@
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/backend/esp/i2c_global.hpp"
#include "cardboy/sdk/event_bus.hpp"
static i2c_master_dev_handle_t dev_handle;
static inline i2c_device_config_t dev_cfg = {
@@ -37,12 +37,10 @@ static void wakeup(void* arg) {
ESP_ERROR_CHECK(gpio_wakeup_enable(EXP_INT, GPIO_INTR_HIGH_LEVEL));
is_on_low = false;
BaseType_t xResult = pdFAIL;
BaseType_t xHigherPriorityTaskWoken = pdFALSE;
xTaskNotifyFromISR(Buttons::get()._pooler_task, 0, eNoAction, &xHigherPriorityTaskWoken);
PowerHelper::get().reset_slow_isr(&xHigherPriorityTaskWoken);
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
} else {
ESP_ERROR_CHECK(gpio_set_intr_type(EXP_INT, GPIO_INTR_LOW_LEVEL));
@@ -92,9 +90,13 @@ void Buttons::pooler() {
i2c_master_transmit_receive(dev_handle, &reg, sizeof(reg), reinterpret_cast<uint8_t*>(&buffer), 1, -1));
if (_listener)
xTaskNotifyGive(_listener);
if (_eventBus)
_eventBus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
}
}
uint8_t Buttons::get_pressed() { return _current; }
void Buttons::install_isr() { gpio_isr_handler_add(EXP_INT, wakeup, nullptr); }
void Buttons::register_listener(TaskHandle_t task) { _listener = task; }
void Buttons::setEventBus(cardboy::sdk::IEventBus* bus) { _eventBus = bus; }

View File

@@ -5,30 +5,18 @@
#include <driver/ledc.h>
#include <esp_err.h>
#include <esp_timer.h>
#include <nvs_flash.h>
#include <nvs.h>
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
static constexpr ledc_mode_t LEDC_MODE = LEDC_LOW_SPEED_MODE; // low speed is fine
static constexpr ledc_timer_t LEDC_TIMER = LEDC_TIMER_0;
static constexpr ledc_channel_t LEDC_CH = LEDC_CHANNEL_0;
static constexpr ledc_timer_bit_t LEDC_BITS = LEDC_TIMER_10_BIT;
Buzzer &Buzzer::get() {
Buzzer& Buzzer::get() {
static Buzzer b;
return b;
}
void Buzzer::init() {
// Initialize NVS once (safe if already done)
static bool nvsInited = false;
if (!nvsInited) {
esp_err_t err = nvs_flash_init();
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
nvs_flash_erase();
nvs_flash_init();
}
nvsInited = true;
}
ledc_timer_config_t tcfg{};
tcfg.speed_mode = LEDC_MODE;
tcfg.timer_num = LEDC_TIMER;
@@ -52,7 +40,6 @@ void Buzzer::init() {
args.arg = this;
args.name = "buzz";
ESP_ERROR_CHECK(esp_timer_create(&args, reinterpret_cast<esp_timer_handle_t*>(&_timer)));
loadState();
}
void Buzzer::applyFreq(uint32_t freq) {
@@ -65,7 +52,7 @@ void Buzzer::applyFreq(uint32_t freq) {
ledc_update_duty(LEDC_MODE, LEDC_CH);
}
void Buzzer::enqueue(const Step &s) {
void Buzzer::enqueue(const Step& s) {
int nextTail = (_q_tail + 1) % MAX_QUEUE;
if (nextTail == _q_head) { // full, drop oldest
_q_head = (_q_head + 1) % MAX_QUEUE;
@@ -87,21 +74,23 @@ void Buzzer::startNext() {
}
_running = true;
_in_gap = false;
Step &s = front();
Step& s = front();
applyFreq(s.freq);
schedule(s.dur_ms, false);
}
void Buzzer::schedule(uint32_t ms, bool gapPhase) {
if (!_timer) return;
if (!_timer)
return;
_in_gap = gapPhase;
esp_timer_stop(reinterpret_cast<esp_timer_handle_t>(_timer));
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t)ms * 1000ULL);
esp_timer_start_once(reinterpret_cast<esp_timer_handle_t>(_timer), (uint64_t) ms * 1000ULL);
}
void Buzzer::timerCb(void *arg) {
auto *self = static_cast<Buzzer*>(arg);
if (!self) return;
void Buzzer::timerCb(void* arg) {
auto* self = static_cast<Buzzer*>(arg);
if (!self)
return;
if (self->_in_gap) {
self->popFront();
self->startNext();
@@ -109,7 +98,7 @@ void Buzzer::timerCb(void *arg) {
}
// Tone finished
if (!self->empty()) {
auto &s = self->front();
auto& s = self->front();
if (s.gap_ms) {
self->applyFreq(0);
self->schedule(s.gap_ms, true);
@@ -121,7 +110,8 @@ void Buzzer::timerCb(void *arg) {
}
void Buzzer::tone(uint32_t freq, uint32_t duration_ms, uint32_t gap_ms) {
if (_muted) return; // ignore while muted
if (_muted)
return; // ignore while muted
Step s{freq, duration_ms, gap_ms};
enqueue(s);
if (!_running)
@@ -149,7 +139,8 @@ void Buzzer::beepGameOver() {
}
void Buzzer::setMuted(bool m) {
if (m == _muted) return;
if (m == _muted)
return;
_muted = m;
if (_muted) {
clearQueue();
@@ -164,28 +155,6 @@ void Buzzer::setMuted(bool m) {
tone(1500, 40, 10);
tone(1900, 60, 0);
}
saveState();
}
void Buzzer::toggleMuted() { setMuted(!_muted); }
void Buzzer::loadState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READONLY, &h) == ESP_OK) {
uint8_t v = 0;
if (nvs_get_u8(h, "mute", &v) == ESP_OK) {
_muted = (v != 0);
}
nvs_close(h);
}
if (_muted) applyFreq(0);
}
void Buzzer::saveState() {
nvs_handle_t h;
if (nvs_open("cfg", NVS_READWRITE, &h) == ESP_OK) {
nvs_set_u8(h, "mute", _muted ? 1 : 0);
nvs_commit(h);
nvs_close(h);
}
}

View File

@@ -7,7 +7,6 @@
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/backend/esp/fs_helper.hpp"
#include "cardboy/backend/esp/i2c_global.hpp"
#include "cardboy/backend/esp/power_helper.hpp"
#include "cardboy/backend/esp/shutdowner.hpp"
#include "cardboy/backend/esp/spi_global.hpp"
@@ -58,14 +57,14 @@ public:
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
void beepGameOver() override { Buzzer::get().beepGameOver(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
};
class EspRuntime::BatteryService final : public cardboy::sdk::IBatteryMonitor {
public:
[[nodiscard]] bool hasData() const override { return true; }
[[nodiscard]] bool hasData() const override { return true; }
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
@@ -112,22 +111,21 @@ public:
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
};
class EspRuntime::PowerService final : public cardboy::sdk::IPowerManager {
public:
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
};
class EspRuntime::FilesystemService final : public cardboy::sdk::IFilesystem {
public:
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
[[nodiscard]] std::string basePath() const override {
const char* path = FsHelper::get().basePath();
return path ? std::string(path) : std::string{};
}
};
class EspRuntime::LoopHooksService final : public cardboy::sdk::ILoopHooks {
public:
void onLoopIteration() override { vTaskDelay(1); }
};
EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
initializeHardware();
@@ -136,16 +134,20 @@ EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
storageService = std::make_unique<StorageService>();
randomService = std::make_unique<RandomService>();
highResClockService = std::make_unique<HighResClockService>();
powerService = std::make_unique<PowerService>();
filesystemService = std::make_unique<FilesystemService>();
eventBus = std::make_unique<EventBus>();
loopHooksService = std::make_unique<LoopHooksService>();
services.buzzer = buzzerService.get();
services.battery = batteryService.get();
services.storage = storageService.get();
services.random = randomService.get();
services.highResClock = highResClockService.get();
services.powerManager = powerService.get();
services.filesystem = filesystemService.get();
services.eventBus = eventBus.get();
services.loopHooks = loopHooksService.get();
Buttons::get().setEventBus(eventBus.get());
}
EspRuntime::~EspRuntime() = default;
@@ -160,7 +162,6 @@ void EspRuntime::initializeHardware() {
ensureNvsInit();
PowerHelper::get();
Shutdowner::get();
Buttons::get();
@@ -170,7 +171,6 @@ void EspRuntime::initializeHardware() {
}
Shutdowner::get().install_isr();
PowerHelper::get().install_isr();
Buttons::get().install_isr();
I2cGlobal::get();
@@ -182,16 +182,6 @@ void EspRuntime::initializeHardware() {
FsHelper::get().mount();
}
int EspFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
int EspFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
void EspFramebuffer::drawPixel_impl(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= width_impl() || y >= height_impl())
return;
SMD::set_pixel(x, y, on);
}
void EspFramebuffer::clear_impl(bool on) {
for (int y = 0; y < height_impl(); ++y)
for (int x = 0; x < width_impl(); ++x)
@@ -234,7 +224,7 @@ std::uint32_t EspClock::millis_impl() {
void EspClock::sleep_ms_impl(std::uint32_t ms) {
if (ms == 0)
return;
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
vTaskDelay(pdMS_TO_TICKS(ms));
}
} // namespace cardboy::backend::esp

View File

@@ -0,0 +1,81 @@
#include "cardboy/backend/esp/event_bus.hpp"
#include "cardboy/sdk/event_bus.hpp"
#include "freertos/portmacro.h"
#include <algorithm>
namespace cardboy::backend::esp {
namespace {
[[nodiscard]] TickType_t toTicks(std::uint32_t timeout_ms) {
if (timeout_ms == cardboy::sdk::IEventBus::kWaitForever)
return portMAX_DELAY;
return pdMS_TO_TICKS(timeout_ms);
}
} // namespace
static void timerCallback(TimerHandle_t handle) {
auto* bus = static_cast<EventBus*>(pvTimerGetTimerID(handle));
if (bus)
bus->signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
}
EventBus::EventBus() :
group(xEventGroupCreate()), timer(xTimerCreate("EventBusTimer", pdMS_TO_TICKS(1), pdFALSE, this, timerCallback)) {}
EventBus::~EventBus() {
if (timer)
xTimerDelete(timer, portMAX_DELAY);
if (group)
vEventGroupDelete(group);
}
void EventBus::signal(std::uint32_t bits) {
if (!group || bits == 0)
return;
xEventGroupSetBits(group, bits);
}
void EventBus::signalFromISR(std::uint32_t bits) {
if (!group || bits == 0)
return;
BaseType_t higherPriorityTaskWoken = pdFALSE;
xEventGroupSetBitsFromISR(group, bits, &higherPriorityTaskWoken);
if (higherPriorityTaskWoken == pdTRUE)
portYIELD_FROM_ISR(higherPriorityTaskWoken);
}
std::uint32_t EventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (!group || mask == 0)
return 0;
const EventBits_t bits = xEventGroupWaitBits(group, mask, pdTRUE, pdFALSE, toTicks(timeout_ms));
return static_cast<std::uint32_t>(bits & mask);
}
void EventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
if (!timer)
return;
xTimerStop(timer, 0);
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
const TickType_t ticks = std::max<TickType_t>(pdMS_TO_TICKS(delay_ms), 1);
if (xTimerChangePeriod(timer, ticks, 0) == pdPASS)
xTimerStart(timer, 0);
}
void EventBus::cancelTimerSignal() {
if (!timer)
return;
xTimerStop(timer, 0);
}
} // namespace cardboy::backend::esp

View File

@@ -1,58 +0,0 @@
//
// Created by Stepan Usatiuk on 03.03.2025.
//
#include "cardboy/backend/esp/power_helper.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
PowerHelper& PowerHelper::get() {
static PowerHelper powerHelper;
return powerHelper;
}
bool PowerHelper::is_slow() const { return _slow; }
void PowerHelper::set_slow(bool slow) {
_slow = slow;
if (_slow) {
xEventGroupClearBits(_event_group, 1);
} else {
xEventGroupSetBits(_event_group, 1);
}
}
BaseType_t PowerHelper::reset_slow_isr(BaseType_t* xHigherPriorityTaskWoken) {
_slow = false;
return xEventGroupSetBitsFromISR(_event_group, 1, xHigherPriorityTaskWoken);
}
static void wakeup(void* arg) {
BaseType_t xHigherPriorityTaskWoken, xResult;
xHigherPriorityTaskWoken = pdFALSE;
xResult = static_cast<PowerHelper*>(arg)->reset_slow_isr(&xHigherPriorityTaskWoken);
if (xResult != pdFAIL) {
portYIELD_FROM_ISR(xHigherPriorityTaskWoken);
};
}
PowerHelper::PowerHelper() : _event_group(xEventGroupCreate()) { set_slow(false); }
void PowerHelper::delay(int slow_ms, int normal_ms) {
if (is_slow()) {
auto cur_ticks = xTaskGetTickCount();
TickType_t to_wait = slow_ms / portTICK_PERIOD_MS;
TickType_t to_wait_normal = normal_ms / portTICK_PERIOD_MS;
auto expected_ticks = cur_ticks + to_wait_normal;
xEventGroupWaitBits(_event_group, 1, pdFALSE, pdTRUE, to_wait);
auto realTicks = xTaskGetTickCount();
if (realTicks < expected_ticks) {
vTaskDelay(expected_ticks - realTicks);
}
} else {
vTaskDelay(normal_ms / portTICK_PERIOD_MS);
}
}
void PowerHelper::install_isr() {
// gpio_isr_handler_add(EXP_INT, wakeup, this);
}

248
Firmware/ghettoprof.sh Executable file
View File

@@ -0,0 +1,248 @@
#!/usr/bin/env bash
# parallel-pc-profile.sh — parallel symbol resolver + optional annotated disassembly
# Supports C++ demangling, LLVM disassembler, and optional no-inlines aggregation (symbol-table based).
#
# Usage:
# ./parallel-pc-profile.sh [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt
set -euo pipefail
usage() {
echo "Usage: $0 [-j jobs] [--annotate] [--no-inlines] firmware.elf pcs.txt"
exit 1
}
ANNOTATE=0
JOBS=""
NO_INLINES=0
# ---- args ----
while [[ $# -gt 0 ]]; do
case "$1" in
-j) JOBS="$2"; shift 2 ;;
--annotate) ANNOTATE=1; shift ;;
--no-inlines) NO_INLINES=1; shift ;;
-h|--help) usage ;;
*) break ;;
esac
done
[[ $# -lt 2 ]] && usage
ELF="$1"
PCS_IN="$2"
[[ ! -f "$ELF" ]] && { echo "ELF not found: $ELF" >&2; exit 2; }
[[ ! -f "$PCS_IN" ]] && { echo "PC log not found: $PCS_IN" >&2; exit 3; }
# ---- tools ----
ADDR2LINE=""
for t in llvm-addr2line eu-addr2line riscv32-esp-elf-addr2line xtensa-esp32-elf-addr2line addr2line; do
if command -v "$t" >/dev/null 2>&1; then ADDR2LINE="$t"; break; fi
done
[[ -z "$ADDR2LINE" ]] && { echo "No addr2line found"; exit 4; }
if command -v llvm-objdump >/dev/null 2>&1; then
OBJDUMP="llvm-objdump"
else
for t in riscv32-esp-elf-objdump xtensa-esp32-elf-objdump objdump; do
if command -v "$t" >/dev/null 2>&1; then OBJDUMP="$t"; break; fi
done
fi
[[ -z "${OBJDUMP:-}" ]] && { echo "No objdump found"; exit 5; }
if command -v llvm-nm >/dev/null 2>&1; then
NM="llvm-nm"
elif command -v nm >/dev/null 2>&1; then
NM="nm"
else
NM=""
fi
if command -v c++filt >/dev/null 2>&1; then
CPPFILT="c++filt"
elif command -v llvm-cxxfilt >/dev/null 2>&1; then
CPPFILT="llvm-cxxfilt"
else
CPPFILT=""
fi
# ---- cores ----
if [[ -z "$JOBS" ]]; then
if command -v nproc >/dev/null 2>&1; then JOBS=$(nproc)
elif [[ "$OSTYPE" == "darwin"* ]]; then JOBS=$(sysctl -n hw.ncpu 2>/dev/null || echo 4)
else JOBS=$(getconf _NPROCESSORS_ONLN 2>/dev/null || echo 4)
fi
fi
(( JOBS = JOBS > 1 ? JOBS - 1 : 1 ))
echo ">> Using $JOBS parallel jobs"
TMP=$(mktemp -d)
trap 'rm -rf "$TMP"' EXIT
# ---- extract PCs ----
grep -aoE '0x[0-9a-fA-F]+' "$PCS_IN" | tr 'A-F' 'a-f' | sort | uniq -c >"$TMP/pc_counts.txt" || true
awk '{print $2}' "$TMP/pc_counts.txt" >"$TMP/addrs.txt"
[[ ! -s "$TMP/addrs.txt" ]] && { echo "No addresses found"; exit 5; }
# ---- parallel addr2line (live PC -> function to stderr) ----
CHUNK=400
split -l "$CHUNK" "$TMP/addrs.txt" "$TMP/chunk."
find "$TMP" -name 'chunk.*' -type f -print0 \
| xargs -0 -I{} -n1 -P "$JOBS" bash -c '
set -euo pipefail
ADDR2LINE="$1"; ELF="$2"; CHUNK="$3"; CPP="$4"
OUT="${CHUNK}.sym"
"$ADDR2LINE" -a -f -e "$ELF" $(cat "$CHUNK") \
| tee "$OUT" \
| awk '"'"'NR%3==1{a=$0;next} NR%3==2{f=$0; printf "%s\t%s\n",a,f; next} NR%3==0{next}'"'"' \
| { if [[ -n "$CPP" ]]; then "$CPP"; else cat; fi; } 1>&2
' _ "$ADDR2LINE" "$ELF" {} "$CPPFILT"
# Collate triplets
cat "$TMP"/chunk.*.sym > "$TMP/symbols.raw"
# ---- parse 3-line addr/func/file:line ----
# Normalize leading zeros in addresses so joins match grep-extracted PCs
awk 'NR%3==1{a=$0; sub(/^0x0+/, "0x", a); next} NR%3==2{f=$0; next} NR%3==0{print a "\t" f "\t" $0}' \
"$TMP/symbols.raw" >"$TMP/map.tsv"
# ---- counts: addr -> samplecount ----
awk '{printf "%s\t%s\n",$2,$1}' "$TMP/pc_counts.txt" | sort -k1,1 >"$TMP/counts.tsv"
# ---- choose mapping: default (addr2line; may show inlined names) vs --no-inlines (symbol-table) ----
DEFAULT_ADDR_FUNC="$TMP/addr_func.tsv"
cut -f1,2 "$TMP/map.tsv" | sort -k1,1 >"$DEFAULT_ADDR_FUNC"
if [[ "$NO_INLINES" == "1" ]]; then
if [[ -z "$NM" ]]; then
echo "WARNING: nm/llvm-nm not found; falling back to inline-aware mapping." >&2
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
else
echo ">> Building symbol table for no-inlines mapping..."
# Create sorted function symbols: hexaddr\tname (demangled if possible afterwards)
# Try llvm-nm format first; fall back to generic nm.
if [[ "$NM" == "llvm-nm" ]]; then
# llvm-nm -n --defined-only emits: ADDRESS TYPE NAME
"$NM" -n --defined-only "$ELF" \
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw"
else
# generic nm -n emits: ADDRESS TYPE NAME (varies a bit across platforms)
"$NM" -n --defined-only "$ELF" 2>/dev/null \
| awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
# macOS nm might output different columns; handle common alt layout:
if [[ ! -s "$TMP/syms.raw" ]]; then
"$NM" -n "$ELF" 2>/dev/null | awk '/ [Tt] /{print $1 "\t" $3}' > "$TMP/syms.raw" || true
fi
fi
if [[ -n "$CPPFILT" && -s "$TMP/syms.raw" ]]; then
"$CPPFILT" < "$TMP/syms.raw" > "$TMP/syms.dem.raw" || cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
else
cp "$TMP/syms.raw" "$TMP/syms.dem.raw"
fi
# Normalize addresses and sort ascending
awk '{addr=$1; sub(/^0x0+/, "0x", addr); print addr "\t" $2}' "$TMP/syms.dem.raw" \
| awk 'NF' \
| sort -k1,1 > "$TMP/syms.tsv"
if [[ ! -s "$TMP/syms.tsv" ]]; then
echo "WARNING: no text symbols found; falling back to inline-aware mapping." >&2
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
else
# Map each PC to the *containing* function: last symbol with addr <= PC.
# Both syms.tsv and addrs.txt are sorted asc → single pass merge.
awk '
function hex2num(h, x, n,i,c) {
gsub(/^0x/,"",h); n=0
for(i=1;i<=length(h);i++){ c=substr(h,i,1)
x = index("0123456789abcdef", tolower(c)) - 1
if (x<0) x = index("0123456789ABCDEF", c) - 1
n = n*16 + x
}
return n
}
BEGIN {
# preload symbols
while ((getline < ARGV[1]) > 0) {
saddr[NSYM]=$1; sname[NSYM]=$2; NSYM++
}
# load PCs
while ((getline < ARGV[2]) > 0) {
pc[NPC]=$0; NPC++
}
# pointers
si=0
for (i=0; i<NPC; i++) {
p=pc[i]; pn=hex2num(p)
# advance symbol index while next symbol start <= pc
while (si+1<NSYM && hex2num(saddr[si+1]) <= pn) si++
# output mapping: p -> sname[si] (if any)
if (si<NSYM && hex2num(saddr[si]) <= pn)
printf "%s\t%s\n", p, sname[si]
else
printf "%s\t<unknown>\n", p
}
exit 0
}
' "$TMP/syms.tsv" "$TMP/addrs.txt" \
| sort -k1,1 > "$TMP/addr_func.noinline.tsv"
ADDR_FUNC_FILE="$TMP/addr_func.noinline.tsv"
fi
fi
else
ADDR_FUNC_FILE="$DEFAULT_ADDR_FUNC"
fi
# ---- aggregate to hot functions ----
join -t $'\t' -a1 -e "<unknown>" -o 1.2,2.2 "$TMP/counts.tsv" "$ADDR_FUNC_FILE" \
| awk -F'\t' '{s[$2]+=$1} END{for(k in s) printf "%8d %s\n",s[k],k}' \
| sort -nr > "$TMP/hot.txt"
# ---- demangle final hot list (if available) ----
if [[ -n "$CPPFILT" ]]; then
"$CPPFILT" < "$TMP/hot.txt" > hot_functions.txt
else
cp "$TMP/hot.txt" hot_functions.txt
fi
echo "=== Top 50 hot functions ==="
head -50 hot_functions.txt
echo "Full list in: hot_functions.txt"
# ---- annotated source+assembly (optional) ----
if (( ANNOTATE )); then
echo ">> Generating annotated source+assembly..."
awk '{printf "%s %s\n",$2,$1}' "$TMP/pc_counts.txt" >"$TMP/count.map"
if [[ "$OBJDUMP" == "llvm-objdump" ]]; then
# Portable across llvm-objdump versions
"$OBJDUMP" --source -l --demangle -d "$ELF" >"$TMP/disasm.txt"
else
"$OBJDUMP" -S -C -l -d "$ELF" >"$TMP/disasm.txt"
fi
# Overlay hit counts onto the disassembly
awk -v counts="$TMP/count.map" '
BEGIN {
while ((getline < counts) > 0) {
addr=$1; cnt=$2
gsub(/^0x/,"",addr)
map[addr]=cnt
}
}
/^[[:space:]]*[0-9a-f]+:/ {
split($1,a,":"); key=a[1]
if (key in map)
printf("%-12s %6d | %s\n", $1, map[key], substr($0, index($0,$2)))
else
print $0
next
}
{ print }
' "$TMP/disasm.txt" > annotated.S
echo "Annotated source + assembly written to: annotated.S"
echo "Tip: less -R annotated.S"
fi

View File

@@ -1,17 +1,20 @@
// Cardboy firmware entry point: boot platform services and run the modular app system.
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/apps/clock_app.hpp"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/esp_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_err.h"
#include "esp_pm.h"
#include "esp_sleep.h"
#include "esp_system.h"
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "sdkconfig.h"
#include <algorithm>
@@ -19,9 +22,9 @@
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <span>
#include <string>
#include <string_view>
#include <span>
#include <vector>
namespace {
@@ -53,11 +56,11 @@ constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
namespace {
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
constexpr uint32_t kStatsTaskStack = 4096;
constexpr char kStatsTaskName[] = "TaskStats";
constexpr TickType_t kStatsTaskDelayTicks = pdMS_TO_TICKS(5000);
constexpr TickType_t kStatsWarmupDelay = pdMS_TO_TICKS(2000);
constexpr UBaseType_t kStatsTaskPriority = tskIDLE_PRIORITY + 1;
constexpr uint32_t kStatsTaskStack = 4096;
constexpr char kStatsTaskName[] = "TaskStats";
struct TaskRuntimeSample {
TaskHandle_t handle;
@@ -65,11 +68,11 @@ struct TaskRuntimeSample {
};
struct TaskUsageRow {
std::string name;
uint64_t delta;
UBaseType_t priority;
uint32_t stackHighWaterBytes;
bool isIdle;
std::string name;
uint64_t delta;
UBaseType_t priority;
uint32_t stackHighWaterBytes;
bool isIdle;
};
[[nodiscard]] uint64_t deltaWithWrap(uint32_t current, uint32_t previous) {
@@ -79,7 +82,7 @@ struct TaskUsageRow {
}
void task_usage_monitor(void*) {
static constexpr char tag[] = "TaskUsage";
static constexpr char tag[] = "TaskUsage";
std::vector<TaskRuntimeSample> lastSamples;
uint32_t lastTotal = 0;
@@ -94,7 +97,7 @@ void task_usage_monitor(void*) {
std::vector<TaskStatus_t> statusBuffer(taskCount);
uint32_t totalRuntime = 0;
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
const UBaseType_t captured = uxTaskGetSystemState(statusBuffer.data(), statusBuffer.size(), &totalRuntime);
if (captured == 0)
continue;
statusBuffer.resize(captured);
@@ -118,8 +121,8 @@ void task_usage_monitor(void*) {
std::vector<TaskUsageRow> rows;
rows.reserve(statusBuffer.size());
uint64_t idleDelta = 0;
uint64_t activeDelta = 0;
uint64_t idleDelta = 0;
uint64_t activeDelta = 0;
uint64_t accountedDelta = 0;
for (const auto& status: statusBuffer) {
@@ -128,18 +131,18 @@ void task_usage_monitor(void*) {
});
const uint32_t previousRuntime = (it != lastSamples.end()) ? it->runtime : status.ulRunTimeCounter;
const uint64_t taskDelta = (it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
const uint64_t taskDelta =
(it != lastSamples.end()) ? deltaWithWrap(status.ulRunTimeCounter, previousRuntime) : 0ULL;
currentSamples.push_back({status.xHandle, status.ulRunTimeCounter});
TaskUsageRow row{
.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
.delta = taskDelta,
.priority = status.uxCurrentPriority,
.stackHighWaterBytes = static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)
};
TaskUsageRow row{.name = std::string(status.pcTaskName ? status.pcTaskName : ""),
.delta = taskDelta,
.priority = status.uxCurrentPriority,
.stackHighWaterBytes =
static_cast<uint32_t>(status.usStackHighWaterMark) * sizeof(StackType_t),
.isIdle = status.uxCurrentPriority == tskIDLE_PRIORITY ||
(status.pcTaskName && std::strncmp(status.pcTaskName, "IDLE", 4) == 0)};
rows.push_back(std::move(row));
@@ -156,9 +159,8 @@ void task_usage_monitor(void*) {
if (rows.empty())
continue;
std::sort(rows.begin(), rows.end(), [](const TaskUsageRow& a, const TaskUsageRow& b) {
return a.delta > b.delta;
});
std::sort(rows.begin(), rows.end(),
[](const TaskUsageRow& a, const TaskUsageRow& b) { return a.delta > b.delta; });
const double windowMs = static_cast<double>(totalDelta) / 1000.0;
@@ -181,14 +183,20 @@ void task_usage_monitor(void*) {
std::printf(" %-16s %6.2f%% (ISRs / scheduler)\n", "<isr>", residualPct);
}
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag,
(activeDelta * 100.0) / static_cast<double>(totalDelta), idlePct);
std::printf("[%s] Active %.2f%% | Idle %.2f%%\n", tag, (activeDelta * 100.0) / static_cast<double>(totalDelta),
idlePct);
const uint32_t heapFree = esp_get_free_heap_size();
const uint32_t heapMinimum = esp_get_minimum_free_heap_size();
std::printf("[%s] Heap free %lu B | Min free %lu B\n", tag, static_cast<unsigned long>(heapFree),
static_cast<unsigned long>(heapMinimum));
std::fflush(stdout);
}
}
void start_task_usage_monitor() {
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr, 0);
xTaskCreatePinnedToCore(task_usage_monitor, kStatsTaskName, kStatsTaskStack, nullptr, kStatsTaskPriority, nullptr,
0);
}
} // namespace
@@ -197,15 +205,6 @@ inline void start_task_usage_monitor() {}
#endif
extern "C" void app_main() {
#ifdef CONFIG_PM_ENABLE
// const esp_pm_config_t pm_config = {
// .max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
// .min_freq_mhz = 16,
// .light_sleep_enable = true};
// ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
#endif
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
static cardboy::backend::esp::EspRuntime runtime;
@@ -215,12 +214,30 @@ extern "C" void app_main() {
cardboy::sdk::AppSystem system(context);
context.system = &system;
const cardboy::sdk::PersistentSettings persistentSettings =
cardboy::sdk::loadPersistentSettings(context.getServices());
if (auto* buzzer = context.buzzer())
buzzer->setMuted(persistentSettings.mute);
#ifdef CONFIG_PM_ENABLE
if (persistentSettings.autoLightSleep) {
const esp_pm_config_t pm_config = {
.max_freq_mhz = CONFIG_ESP_DEFAULT_CPU_FREQ_MHZ,
.min_freq_mhz = 16,
.light_sleep_enable = true,
};
ESP_ERROR_CHECK(esp_pm_configure(&pm_config));
}
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
#endif
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.registerApp(apps::createGameboyAppFactory());
start_task_usage_monitor();
// start_task_usage_monitor();
system.run();
}

View File

@@ -5,6 +5,8 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
add_subdirectory(utils)
add_subdirectory(backend_interface)
set(CARDBOY_SDK_BACKEND_LIBRARY "" CACHE STRING "Backend implementation library for Cardboy SDK")

View File

@@ -14,5 +14,6 @@ target_compile_features(cardboy_apps PUBLIC cxx_std_20)
add_subdirectory(menu)
add_subdirectory(clock)
add_subdirectory(settings)
add_subdirectory(gameboy)
add_subdirectory(tetris)

View File

@@ -25,14 +25,14 @@ using Framebuffer = typename AppContext::Framebuffer;
using Clock = typename AppContext::Clock;
struct TimeSnapshot {
bool hasWallTime = false;
int hour24 = 0;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
bool hasWallTime = false;
int hour24 = 0;
int minute = 0;
int second = 0;
int year = 0;
int month = 0;
int day = 0;
int weekday = 0;
std::uint64_t uptimeSeconds = 0;
};
@@ -42,8 +42,8 @@ public:
void onStart() override {
cancelRefreshTimer();
lastSnapshot = {};
dirty = true;
lastSnapshot = {};
dirty = true;
const auto snap = captureTime();
renderIfNeeded(snap);
lastSnapshot = snap;
@@ -69,8 +69,8 @@ private:
Framebuffer& framebuffer;
Clock& clock;
bool use24Hour = true;
bool dirty = false;
bool use24Hour = true;
bool dirty = false;
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
@@ -199,7 +199,7 @@ private:
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
if (!snap.hasWallTime) {
char uptimeLine[32];
char uptimeLine[32];
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
@@ -225,7 +225,7 @@ private:
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kClockAppName; }
const char* name() const override { return kClockAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<ClockApp>(context);
}

View File

@@ -1,10 +1,17 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp
${CMAKE_CURRENT_SOURCE_DIR}/minigb_apu/minigb_apu.c
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
${CMAKE_CURRENT_SOURCE_DIR}
)
target_compile_definitions(cardboy_apps
PRIVATE
MINIGB_APU_AUDIO_FORMAT_S16SYS=1
)

View File

@@ -577,15 +577,6 @@ union cart_rtc
*/
struct gb_s
{
/**
* Return byte from ROM at given address.
*
* \param gb_s emulator context
* \param addr address
* \return byte at address in ROM
*/
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t addr);
/**
* Return byte from cart RAM at given address.
*
@@ -738,9 +729,14 @@ struct gb_s
/* Implementation defined data. Set to NULL if not required. */
void *priv;
const uint8_t* rom;
} direct;
};
__attribute__((always_inline)) inline uint8_t gb_rom_read(struct gb_s* s, const uint_fast32_t addr) {
return s->direct.rom[addr];
}
#ifndef PEANUT_GB_HEADER_ONLY
#define IO_JOYP 0x00
@@ -796,17 +792,17 @@ uint8_t __gb_read(struct gb_s *gb, uint16_t addr)
case 0x1:
case 0x2:
case 0x3:
return gb->gb_rom_read(gb, addr);
return gb_rom_read(gb, addr);
case 0x4:
case 0x5:
case 0x6:
case 0x7:
if(gb->mbc == 1 && gb->cart_mode_select)
return gb->gb_rom_read(gb,
return gb_rom_read(gb,
addr + ((gb->selected_rom_bank & 0x1F) - 1) * ROM_BANK_SIZE);
else
return gb->gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
return gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
case 0x8:
case 0x9:
@@ -3542,7 +3538,7 @@ int gb_get_save_size_s(struct gb_s *gb, size_t *ram_size)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3570,7 +3566,7 @@ uint_fast32_t gb_get_save_size(struct gb_s *gb)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3603,7 +3599,7 @@ uint8_t gb_colour_hash(struct gb_s *gb)
uint16_t i;
for(i = ROM_TITLE_START_ADDR; i <= ROM_TITLE_END_ADDR; i++)
x += gb->gb_rom_read(gb, i);
x += gb_rom_read(gb, i);
return x;
}
@@ -3626,7 +3622,7 @@ void gb_reset(struct gb_s *gb)
if(gb->gb_bootrom_read == NULL)
{
uint8_t hdr_chk;
hdr_chk = gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
hdr_chk = gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
gb->cpu_reg.a = 0x01;
gb->cpu_reg.f.f_bits.z = 1;
@@ -3692,11 +3688,10 @@ void gb_reset(struct gb_s *gb)
}
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv)
void *priv, const uint8_t* rom_data)
{
const uint16_t mbc_location = 0x0147;
const uint16_t bank_count_location = 0x0148;
@@ -3731,11 +3726,11 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
* some early homebrew ROMs supposedly may use this value. */
const uint8_t num_ram_banks[] = { 0, 1, 1, 4, 16, 8 };
gb->gb_rom_read = gb_rom_read;
gb->gb_cart_ram_read = gb_cart_ram_read;
gb->gb_cart_ram_write = gb_cart_ram_write;
gb->gb_error = gb_error;
gb->direct.priv = priv;
gb->direct.rom = rom_data;
/* Initialise serial transfer function to NULL. If the front-end does
* not provide serial support, Peanut-GB will emulate no cable connected
@@ -3751,24 +3746,24 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
uint16_t i;
for(i = 0x0134; i <= 0x014C; i++)
x = x - gb->gb_rom_read(gb, i) - 1;
x = x - gb_rom_read(gb, i) - 1;
if(x != gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
if(x != gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
return GB_INIT_INVALID_CHECKSUM;
}
/* Check if cartridge type is supported, and set MBC type. */
{
const uint8_t mbc_value = gb->gb_rom_read(gb, mbc_location);
const uint8_t mbc_value = gb_rom_read(gb, mbc_location);
if(mbc_value > sizeof(cart_mbc) - 1 ||
(gb->mbc = cart_mbc[mbc_value]) == -1)
return GB_INIT_CARTRIDGE_UNSUPPORTED;
}
gb->num_rom_banks_mask = num_rom_banks_mask[gb->gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb->gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb->gb_rom_read(gb, ram_size_location)];
gb->num_rom_banks_mask = num_rom_banks_mask[gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb_rom_read(gb, ram_size_location)];
/* If the ROM says that it support RAM, but has 0 RAM banks, then
* disable RAM reads from the cartridge. */
@@ -3804,7 +3799,7 @@ const char* gb_get_rom_name(struct gb_s* gb, char *title_str)
for(; title_loc <= title_end; title_loc++)
{
const char title_char = gb->gb_rom_read(gb, title_loc);
const char title_char = gb_rom_read(gb, title_loc);
if(title_char >= ' ' && title_char <= '_')
{
@@ -3886,11 +3881,10 @@ void gb_set_rtc(struct gb_s *gb, const struct tm * const time)
* \returns 0 on success or an enum that describes the error.
*/
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv);
void *priv, const uint8_t* rom_data);
/**
* Executes the emulator and runs for the duration of time equal to one frame.

View File

@@ -0,0 +1,542 @@
/**
* Game Boy APU emulator.
* Copyright (c) 2019 Mahyar Koshkouei <mk@deltabeard.com>
* Copyright (c) 2017 Alex Baines <alex@abaines.me.uk>
* minigb_apu is released under the terms of the MIT license.
*
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
*/
#include <stdbool.h>
#include <stdint.h>
#include <string.h>
#include "minigb_apu.h"
#define DMG_CLOCK_FREQ_U ((unsigned)DMG_CLOCK_FREQ)
#define AUDIO_NSAMPLES (AUDIO_SAMPLES_TOTAL)
#define MAX(a, b) ( a > b ? a : b )
#define MIN(a, b) ( a <= b ? a : b )
/* Factor in which values are multiplied to compensate for fixed-point
* arithmetic. Some hard-coded values in this project must be recreated. */
#ifndef FREQ_INC_MULT
# define FREQ_INC_MULT 105
#endif
/* Handles time keeping for sound generation.
* FREQ_INC_REF must be equal to, or larger than AUDIO_SAMPLE_RATE in order
* to avoid a division by zero error.
* Using a square of 2 simplifies calculations. */
#define FREQ_INC_REF (AUDIO_SAMPLE_RATE * FREQ_INC_MULT)
#define MAX_CHAN_VOLUME 15
static void set_note_freq(struct chan *c)
{
/* Lowest expected value of freq is 64. */
uint32_t freq = (DMG_CLOCK_FREQ_U / 4) / (2048 - c->freq);
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
}
static void chan_enable(struct minigb_apu_ctx *ctx,
const uint_fast8_t i, const bool enable)
{
uint8_t val;
ctx->chans[i].enabled = enable;
val = (ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] & 0x80) |
(ctx->chans[3].enabled << 3) | (ctx->chans[2].enabled << 2) |
(ctx->chans[1].enabled << 1) | (ctx->chans[0].enabled << 0);
ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] = val;
}
static void update_env(struct chan *c)
{
c->env.counter += c->env.inc;
while (c->env.counter > FREQ_INC_REF) {
if (c->env.step) {
c->volume += c->env.up ? 1 : -1;
if (c->volume == 0 || c->volume == MAX_CHAN_VOLUME) {
c->env.inc = 0;
}
c->volume = MAX(0, MIN(MAX_CHAN_VOLUME, c->volume));
}
c->env.counter -= FREQ_INC_REF;
}
}
static void update_len(struct minigb_apu_ctx *ctx, struct chan *c)
{
if (!c->len.enabled)
return;
c->len.counter += c->len.inc;
if (c->len.counter > FREQ_INC_REF) {
chan_enable(ctx, c - ctx->chans, 0);
c->len.counter = 0;
}
}
static bool update_freq(struct chan *c, uint32_t *pos)
{
uint32_t inc = c->freq_inc - *pos;
c->freq_counter += inc;
if (c->freq_counter > FREQ_INC_REF) {
*pos = c->freq_inc - (c->freq_counter - FREQ_INC_REF);
c->freq_counter = 0;
return true;
} else {
*pos = c->freq_inc;
return false;
}
}
static void update_sweep(struct chan *c)
{
c->sweep.counter += c->sweep.inc;
while (c->sweep.counter > FREQ_INC_REF) {
if (c->sweep.shift) {
uint16_t inc = (c->sweep.freq >> c->sweep.shift);
if (c->sweep.down)
inc *= -1;
c->freq = c->sweep.freq + inc;
if (c->freq > 2047) {
c->enabled = 0;
} else {
set_note_freq(c);
c->sweep.freq = c->freq;
}
} else if (c->sweep.rate) {
c->enabled = 0;
}
c->sweep.counter -= FREQ_INC_REF;
}
}
static void update_square(struct minigb_apu_ctx *ctx, audio_sample_t *samples,
const bool ch2)
{
struct chan *c = &ctx->chans[ch2];
if (!c->powered || !c->enabled)
return;
set_note_freq(c);
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
update_len(ctx, c);
if (!c->enabled)
return;
update_env(c);
if (!c->volume)
continue;
if (!ch2)
update_sweep(c);
uint32_t pos = 0;
uint32_t prev_pos = 0;
int32_t sample = 0;
while (update_freq(c, &pos)) {
c->square.duty_counter = (c->square.duty_counter + 1) & 7;
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
c->val = (c->square.duty & (1 << c->square.duty_counter)) ?
VOL_INIT_MAX / MAX_CHAN_VOLUME :
VOL_INIT_MIN / MAX_CHAN_VOLUME;
prev_pos = pos;
}
sample += c->val;
sample *= c->volume;
sample /= 4;
samples[i + 0] += sample * c->on_left * ctx->vol_l;
samples[i + 1] += sample * c->on_right * ctx->vol_r;
}
}
static uint8_t wave_sample(struct minigb_apu_ctx *ctx,
const unsigned int pos, const unsigned int volume)
{
uint8_t sample;
sample = ctx->audio_mem[(0xFF30 + pos / 2) - AUDIO_ADDR_COMPENSATION];
if (pos & 1) {
sample &= 0xF;
} else {
sample >>= 4;
}
return volume ? (sample >> (volume - 1)) : 0;
}
static void update_wave(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
{
struct chan *c = &ctx->chans[2];
if (!c->powered || !c->enabled || !c->volume)
return;
set_note_freq(c);
c->freq_inc *= 2;
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
update_len(ctx, c);
if (!c->enabled)
return;
uint32_t pos = 0;
uint32_t prev_pos = 0;
audio_sample_t sample = 0;
c->wave.sample = wave_sample(ctx, c->val, c->volume);
while (update_freq(c, &pos)) {
c->val = (c->val + 1) & 31;
sample += ((pos - prev_pos) / c->freq_inc) *
((audio_sample_t)c->wave.sample - 8) *
(AUDIO_SAMPLE_MAX/64);
c->wave.sample = wave_sample(ctx, c->val, c->volume);
prev_pos = pos;
}
sample += ((audio_sample_t)c->wave.sample - 8) *
(audio_sample_t)(AUDIO_SAMPLE_MAX/64);
{
/* First element is unused. */
audio_sample_t div[] = { AUDIO_SAMPLE_MAX, 1, 2, 4 };
sample = sample / (div[c->volume]);
}
sample /= 4;
samples[i + 0] += sample * c->on_left * ctx->vol_l;
samples[i + 1] += sample * c->on_right * ctx->vol_r;
}
}
static void update_noise(struct minigb_apu_ctx *ctx, audio_sample_t *samples)
{
struct chan *c = &ctx->chans[3];
if (c->freq >= 14)
c->enabled = 0;
if (!c->powered || !c->enabled)
return;
{
const uint32_t lfsr_div_lut[] = {
8, 16, 32, 48, 64, 80, 96, 112
};
uint32_t freq;
freq = DMG_CLOCK_FREQ_U / (lfsr_div_lut[c->noise.lfsr_div] << c->freq);
c->freq_inc = freq * (uint32_t)(FREQ_INC_REF / AUDIO_SAMPLE_RATE);
}
for (uint_fast16_t i = 0; i < AUDIO_NSAMPLES; i += 2) {
update_len(ctx, c);
if (!c->enabled)
return;
update_env(c);
if (!c->volume)
continue;
uint32_t pos = 0;
uint32_t prev_pos = 0;
int32_t sample = 0;
while (update_freq(c, &pos)) {
c->noise.lfsr_reg = (c->noise.lfsr_reg << 1) |
(c->val >= VOL_INIT_MAX/MAX_CHAN_VOLUME);
if (c->noise.lfsr_wide) {
c->val = !(((c->noise.lfsr_reg >> 14) & 1) ^
((c->noise.lfsr_reg >> 13) & 1)) ?
VOL_INIT_MAX / MAX_CHAN_VOLUME :
VOL_INIT_MIN / MAX_CHAN_VOLUME;
} else {
c->val = !(((c->noise.lfsr_reg >> 6) & 1) ^
((c->noise.lfsr_reg >> 5) & 1)) ?
VOL_INIT_MAX / MAX_CHAN_VOLUME :
VOL_INIT_MIN / MAX_CHAN_VOLUME;
}
sample += ((pos - prev_pos) / c->freq_inc) * c->val;
prev_pos = pos;
}
sample += c->val;
sample *= c->volume;
sample /= 4;
samples[i + 0] += sample * c->on_left * ctx->vol_l;
samples[i + 1] += sample * c->on_right * ctx->vol_r;
}
}
/**
* SDL2 style audio callback function.
*/
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
audio_sample_t *stream)
{
memset(stream, 0, AUDIO_SAMPLES_TOTAL * sizeof(audio_sample_t));
update_square(ctx, stream, 0);
update_square(ctx, stream, 1);
update_wave(ctx, stream);
update_noise(ctx, stream);
}
static void chan_trigger(struct minigb_apu_ctx *ctx, uint_fast8_t i)
{
struct chan *c = &ctx->chans[i];
chan_enable(ctx, i, 1);
c->volume = c->volume_init;
// volume envelope
{
/* LUT created in Julia with:
* `(FREQ_INC_MULT * 64)./vcat(8, 1:7)`
* Must be recreated when FREQ_INC_MULT modified.
*/
const uint32_t inc_lut[8] = {
#if FREQ_INC_MULT == 16
128, 1024, 512, 341,
256, 205, 171, 146
#elif FREQ_INC_MULT == 64
512, 4096, 2048, 1365,
1024, 819, 683, 585
#elif FREQ_INC_MULT == 105
/* Multiples of 105 provide integer values. */
840, 6720, 3360, 2240,
1680, 1344, 1120, 960
#else
#error "LUT not calculated for this value of FREQ_INC_MULT"
#endif
};
uint8_t val;
val = ctx->audio_mem[(0xFF12 + (i * 5)) - AUDIO_ADDR_COMPENSATION];
c->env.step = val & 0x7;
c->env.up = val & 0x8;
c->env.inc = inc_lut[c->env.step];
c->env.counter = 0;
}
// freq sweep
if (i == 0) {
uint8_t val = ctx->audio_mem[0xFF10 - AUDIO_ADDR_COMPENSATION];
c->sweep.freq = c->freq;
c->sweep.rate = (val >> 4) & 0x07;
c->sweep.down = (val & 0x08);
c->sweep.shift = (val & 0x07);
c->sweep.inc = c->sweep.rate ?
((128u * FREQ_INC_REF) / (c->sweep.rate * AUDIO_SAMPLE_RATE)) : 0;
c->sweep.counter = FREQ_INC_REF;
}
int len_max = 64;
if (i == 2) { // wave
len_max = 256;
c->val = 0;
} else if (i == 3) { // noise
c->noise.lfsr_reg = 0xFFFF;
c->val = VOL_INIT_MIN / MAX_CHAN_VOLUME;
}
c->len.inc = (256u * FREQ_INC_REF) / (AUDIO_SAMPLE_RATE * (len_max - c->len.load));
c->len.counter = 0;
}
/**
* Read audio register.
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
* This is not checked in this function.
* \return Byte at address.
*/
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr)
{
static const uint8_t ortab[] = {
0x80, 0x3f, 0x00, 0xff, 0xbf,
0xff, 0x3f, 0x00, 0xff, 0xbf,
0x7f, 0xff, 0x9f, 0xff, 0xbf,
0xff, 0xff, 0x00, 0x00, 0xbf,
0x00, 0x00, 0x70,
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00
};
return ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] |
ortab[addr - AUDIO_ADDR_COMPENSATION];
}
/**
* Write audio register.
* \param addr Address of audio register. Must be 0xFF10 <= addr <= 0xFF3F.
* This is not checked in this function.
* \param val Byte to write at address.
*/
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
const uint16_t addr, const uint8_t val)
{
/* Find sound channel corresponding to register address. */
uint_fast8_t i;
if(addr == 0xFF26)
{
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val & 0x80;
/* On APU power off, clear all registers apart from wave
* RAM. */
if((val & 0x80) == 0)
{
memset(ctx->audio_mem,
0x00, 0xFF26 - AUDIO_ADDR_COMPENSATION);
ctx->chans[0].enabled = false;
ctx->chans[1].enabled = false;
ctx->chans[2].enabled = false;
ctx->chans[3].enabled = false;
}
return;
}
/* Ignore register writes if APU powered off. */
if(ctx->audio_mem[0xFF26 - AUDIO_ADDR_COMPENSATION] == 0x00)
return;
ctx->audio_mem[addr - AUDIO_ADDR_COMPENSATION] = val;
i = (addr - AUDIO_ADDR_COMPENSATION) / 5;
switch (addr) {
case 0xFF12:
case 0xFF17:
case 0xFF21: {
ctx->chans[i].volume_init = val >> 4;
ctx->chans[i].powered = (val >> 3) != 0;
// "zombie mode" stuff, needed for Prehistorik Man and probably
// others
if (ctx->chans[i].powered && ctx->chans[i].enabled) {
if ((ctx->chans[i].env.step == 0 && ctx->chans[i].env.inc != 0)) {
if (val & 0x08) {
ctx->chans[i].volume++;
} else {
ctx->chans[i].volume += 2;
}
} else {
ctx->chans[i].volume = 16 - ctx->chans[i].volume;
}
ctx->chans[i].volume &= 0x0F;
ctx->chans[i].env.step = val & 0x07;
}
} break;
case 0xFF1C:
ctx->chans[i].volume = ctx->chans[i].volume_init = (val >> 5) & 0x03;
break;
case 0xFF11:
case 0xFF16:
case 0xFF20: {
const uint8_t duty_lookup[] = { 0x10, 0x30, 0x3C, 0xCF };
ctx->chans[i].len.load = val & 0x3f;
ctx->chans[i].square.duty = duty_lookup[val >> 6];
break;
}
case 0xFF1B:
ctx->chans[i].len.load = val;
break;
case 0xFF13:
case 0xFF18:
case 0xFF1D:
ctx->chans[i].freq &= 0xFF00;
ctx->chans[i].freq |= val;
break;
case 0xFF1A:
ctx->chans[i].powered = (val & 0x80) != 0;
chan_enable(ctx, i, val & 0x80);
break;
case 0xFF14:
case 0xFF19:
case 0xFF1E:
ctx->chans[i].freq &= 0x00FF;
ctx->chans[i].freq |= ((val & 0x07) << 8);
/* Intentional fall-through. */
case 0xFF23:
ctx->chans[i].len.enabled = val & 0x40;
if (val & 0x80)
chan_trigger(ctx, i);
break;
case 0xFF22:
ctx->chans[3].freq = val >> 4;
ctx->chans[3].noise.lfsr_wide = !(val & 0x08);
ctx->chans[3].noise.lfsr_div = val & 0x07;
break;
case 0xFF24:
{
ctx->vol_l = ((val >> 4) & 0x07);
ctx->vol_r = (val & 0x07);
break;
}
case 0xFF25:
for (uint_fast8_t j = 0; j < 4; j++) {
ctx->chans[j].on_left = (val >> (4 + j)) & 1;
ctx->chans[j].on_right = (val >> j) & 1;
}
break;
}
}
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx)
{
/* Initialise channels and samples. */
memset(ctx->chans, 0, sizeof(ctx->chans));
ctx->chans[0].val = ctx->chans[1].val = -1;
/* Initialise IO registers. */
{
const uint8_t regs_init[] = { 0x80, 0xBF, 0xF3, 0xFF, 0x3F,
0xFF, 0x3F, 0x00, 0xFF, 0x3F,
0x7F, 0xFF, 0x9F, 0xFF, 0x3F,
0xFF, 0xFF, 0x00, 0x00, 0x3F,
0x77, 0xF3, 0xF1 };
for(uint_fast8_t i = 0; i < sizeof(regs_init); ++i)
minigb_apu_audio_write(ctx, 0xFF10 + i, regs_init[i]);
}
/* Initialise Wave Pattern RAM. */
{
const uint8_t wave_init[] = { 0xac, 0xdd, 0xda, 0x48,
0x36, 0x02, 0xcf, 0x16,
0x2c, 0x04, 0xe5, 0x2c,
0xac, 0xdd, 0xda, 0x48 };
for(uint_fast8_t i = 0; i < sizeof(wave_init); ++i)
minigb_apu_audio_write(ctx, 0xFF30 + i, wave_init[i]);
}
}

View File

@@ -0,0 +1,149 @@
/**
* minigb_apu is released under the terms listed within the LICENSE file.
*
* minigb_apu emulates the audio processing unit (APU) of the Game Boy. This
* project is based on MiniGBS by Alex Baines: https://github.com/baines/MiniGBS
*/
#pragma once
#include <stdint.h>
#ifndef AUDIO_SAMPLE_RATE
# define AUDIO_SAMPLE_RATE 20000
#endif
/* The audio output format is in platform native endian. */
#if defined(MINIGB_APU_AUDIO_FORMAT_S16SYS)
typedef int16_t audio_sample_t;
# define AUDIO_SAMPLE_MAX INT16_MAX
# define AUDIO_SAMPLE_MIN INT16_MIN
# define VOL_INIT_MAX (AUDIO_SAMPLE_MAX/8)
# define VOL_INIT_MIN (AUDIO_SAMPLE_MIN/8)
#elif defined(MINIGB_APU_AUDIO_FORMAT_S32SYS)
typedef int32_t audio_sample_t;
# define AUDIO_SAMPLE_MAX INT32_MAX
# define AUDIO_SAMPLE_MIN INT32_MIN
# define VOL_INIT_MAX (INT32_MAX/8)
# define VOL_INIT_MIN (INT32_MIN/8)
#else
#error MiniGB APU: Invalid or unsupported audio format selected
#endif
#define DMG_CLOCK_FREQ 4194304.0
#define SCREEN_REFRESH_CYCLES 70224.0
#define VERTICAL_SYNC (DMG_CLOCK_FREQ/SCREEN_REFRESH_CYCLES)
/* Number of audio samples in each channel. */
#define AUDIO_SAMPLES ((unsigned)(AUDIO_SAMPLE_RATE / VERTICAL_SYNC))
/* Number of audio channels. The audio output is in interleaved stereo format.*/
#define AUDIO_CHANNELS 2
/* Number of audio samples output in each audio_callback call. */
#define AUDIO_SAMPLES_TOTAL (AUDIO_SAMPLES * 2)
#define AUDIO_MEM_SIZE (0xFF3F - 0xFF10 + 1)
#define AUDIO_ADDR_COMPENSATION 0xFF10
struct chan_len_ctr {
uint8_t load;
uint8_t enabled;
uint32_t counter;
uint32_t inc;
};
struct chan_vol_env {
uint8_t step;
uint8_t up;
uint32_t counter;
uint32_t inc;
};
struct chan_freq_sweep {
uint8_t rate;
uint8_t shift;
uint8_t down;
uint16_t freq;
uint32_t counter;
uint32_t inc;
};
struct chan {
uint8_t enabled;
uint8_t powered;
uint8_t on_left;
uint8_t on_right;
uint8_t volume;
uint8_t volume_init;
uint16_t freq;
uint32_t freq_counter;
uint32_t freq_inc;
int32_t val;
struct chan_len_ctr len;
struct chan_vol_env env;
struct chan_freq_sweep sweep;
union {
struct {
uint8_t duty;
uint8_t duty_counter;
} square;
struct {
uint16_t lfsr_reg;
uint8_t lfsr_wide;
uint8_t lfsr_div;
} noise;
struct {
uint8_t sample;
} wave;
};
};
struct minigb_apu_ctx {
struct chan chans[4];
int32_t vol_l, vol_r;
/**
* Memory holding audio registers between 0xFF10 and 0xFF3F inclusive.
*/
uint8_t audio_mem[AUDIO_MEM_SIZE];
};
/**
* Fill allocated buffer "stream" with AUDIO_SAMPLES_TOTAL number of 16-bit
* signed samples (native endian order) in stereo interleaved format.
* Each call corresponds to the time taken for each VSYNC in the Game Boy.
*
* \param ctx Library context. Must be initialised with audio_init().
* \param stream Allocated pointer to store audio samples. Must be at least
* AUDIO_SAMPLES_TOTAL in size.
*/
void minigb_apu_audio_callback(struct minigb_apu_ctx *ctx,
audio_sample_t *stream);
/**
* Read audio register at given address "addr".
* \param ctx Library context. Must be initialised with audio_init().
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
* inclusive.
*/
uint8_t minigb_apu_audio_read(struct minigb_apu_ctx *ctx, const uint16_t addr);
/**
* Write "val" to audio register at given address "addr".
* \param ctx Library context. Must be initialised with audio_init().
* \param addr Address of registers to read. Must be within 0xFF10 and 0xFF3F,
* inclusive.
* \param val Value to write to address.
*/
void minigb_apu_audio_write(struct minigb_apu_ctx *ctx,
const uint16_t addr, const uint8_t val);
/**
* Initialise audio driver.
* \param ctx Library context.
*/
void minigb_apu_audio_init(struct minigb_apu_ctx *ctx);

File diff suppressed because it is too large Load Diff

View File

@@ -47,7 +47,7 @@ public:
moveSelection(+1);
}
const bool launch = (current.a && !previous.a) || (current.select && !previous.select);
const bool launch = (current.a && !previous.a) || (current.start && !previous.start);
if (launch)
launchSelected();
@@ -153,7 +153,7 @@ private:
font16x8::drawText(framebuffer, topRightX, 20, indexLabel, 1, true, 0);
drawPagerDots();
drawCenteredText(framebuffer, framebuffer.height() - 48, "A/SELECT START", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 48, "A START APP", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
}
@@ -163,7 +163,7 @@ private:
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kMenuAppName; }
const char* name() const override { return kMenuAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<MenuApp>(context);
}

View File

@@ -0,0 +1,9 @@
target_sources(cardboy_apps
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/src/settings_app.cpp
)
target_include_directories(cardboy_apps
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)

View File

@@ -0,0 +1,13 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
inline constexpr char kSettingsAppName[] = "Settings";
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory();
} // namespace apps

View File

@@ -0,0 +1,198 @@
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include <array>
#include <cstddef>
#include <memory>
#include <string>
#include <string_view>
namespace apps {
namespace {
using cardboy::sdk::AppContext;
using Framebuffer = typename AppContext::Framebuffer;
enum class SettingOption {
Sound,
AutoLightSleep,
};
constexpr std::array<SettingOption, 2> kOptions = {
SettingOption::Sound,
SettingOption::AutoLightSleep,
};
class SettingsApp final : public cardboy::sdk::IApp {
public:
explicit SettingsApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
void onStart() override {
loadSettings();
dirty = true;
renderIfNeeded();
}
void handleEvent(const cardboy::sdk::AppEvent& event) override {
if (event.type != cardboy::sdk::AppEventType::Button)
return;
const auto& current = event.button.current;
const auto& previous = event.button.previous;
const bool previousAvailable = buzzerAvailable;
syncBuzzerState();
if (previousAvailable != buzzerAvailable)
dirty = true;
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;
}
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();
if (moved)
dirty = true;
renderIfNeeded();
}
private:
AppContext& context;
Framebuffer& framebuffer;
bool buzzerAvailable = false;
cardboy::sdk::PersistentSettings settings{};
std::size_t selectedIndex = 0;
bool dirty = false;
void loadSettings() {
settings = cardboy::sdk::loadPersistentSettings(context.getServices());
syncBuzzerState();
}
void syncBuzzerState() {
auto* buzzer = context.buzzer();
buzzerAvailable = (buzzer != nullptr);
if (!buzzer)
return;
if (buzzer->isMuted() != settings.mute)
buzzer->setMuted(settings.mute);
}
void moveSelection(int delta) {
const int count = static_cast<int>(kOptions.size());
if (count == 0)
return;
const int current = static_cast<int>(selectedIndex);
int next = (current + delta) % count;
if (next < 0)
next += count;
selectedIndex = static_cast<std::size_t>(next);
}
void handleToggle() {
switch (kOptions[selectedIndex]) {
case SettingOption::Sound:
toggleSound();
break;
case SettingOption::AutoLightSleep:
toggleAutoLightSleep();
break;
}
}
void toggleSound() {
if (!buzzerAvailable)
return;
settings.mute = !settings.mute;
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
syncBuzzerState();
if (!settings.mute) {
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
}
dirty = true;
}
void toggleAutoLightSleep() {
settings.autoLightSleep = !settings.autoLightSleep;
cardboy::sdk::savePersistentSettings(context.getServices(), settings);
dirty = true;
}
static void drawCenteredText(Framebuffer& fb, int y, std::string_view text, int scale, int letterSpacing = 1) {
const int width = font16x8::measureText(text, scale, letterSpacing);
const int x = (fb.width() - width) / 2;
font16x8::drawText(fb, x, y, text, scale, true, letterSpacing);
}
void drawOptionRow(int row, std::string_view label, std::string_view value, bool selected) {
std::string prefix = selected ? "> " : " ";
std::string line = prefix;
line.append(label);
line.append(": ");
line.append(value);
const int x = 24;
const int y = 56 + row * 24;
font16x8::drawText(framebuffer, x, y, line, 1, true, 1);
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
framebuffer.frameReady();
framebuffer.clear(false);
drawCenteredText(framebuffer, 24, "SETTINGS", 1, 1);
const std::string soundValue = buzzerAvailable ? (settings.mute ? "OFF" : "ON") : "N/A";
drawOptionRow(0, "SOUND", soundValue, selectedIndex == 0);
const std::string lightSleepValue = settings.autoLightSleep ? "ON" : "OFF";
drawOptionRow(1, "AUTO LIGHT SLEEP", lightSleepValue, selectedIndex == 1);
if (!buzzerAvailable)
drawCenteredText(framebuffer, 120, "SOUND CONTROL UNAVAILABLE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 54, "UP/DOWN MOVE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 36, "A/START/SELECT TOGGLE", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK | LIGHT SLEEP AFTER RESET", 1, 1);
framebuffer.sendFrame();
}
};
class SettingsAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kSettingsAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<SettingsApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createSettingsAppFactory() { return std::make_unique<SettingsAppFactory>(); }
} // namespace apps

View File

@@ -70,13 +70,13 @@ constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks)
}
constexpr std::array<Tetromino, 7> kPieces = {{
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
}};
class RandomBag {
@@ -107,22 +107,22 @@ private:
};
struct ActivePiece {
int type = 0;
int rotation = 0;
int x = 0;
int y = 0;
int type = 0;
int rotation = 0;
int x = 0;
int y = 0;
};
struct GameState {
std::array<int, kBoardWidth * kBoardHeight> board{};
ActivePiece current{};
int nextPiece = 0;
int level = 1;
int linesCleared = 0;
int score = 0;
int highScore = 0;
bool paused = false;
bool gameOver = false;
ActivePiece current{};
int nextPiece = 0;
int level = 1;
int linesCleared = 0;
int score = 0;
int highScore = 0;
bool paused = false;
bool gameOver = false;
};
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
@@ -161,21 +161,21 @@ public:
}
private:
AppContext& context;
AppContext& context;
typename AppContext::Framebuffer& framebuffer;
GameState state;
RandomBag bag;
InputState lastInput{};
bool dirty = false;
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
GameState state;
RandomBag bag;
InputState lastInput{};
bool dirty = false;
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
void reset() {
cancelTimers();
int oldHigh = state.highScore;
state = {};
state.highScore = oldHigh;
int oldHigh = state.highScore;
state = {};
state.highScore = oldHigh;
state.current.type = bag.next();
state.nextPiece = bag.next();
state.current.x = kBoardWidth / 2;
@@ -185,12 +185,10 @@ private:
state.paused = false;
dirty = true;
scheduleDropTimer();
if (auto* power = context.powerManager())
power->setSlowMode(false);
}
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
const auto& cur = evt.current;
const auto& prev = evt.previous;
lastInput = cur;
@@ -204,8 +202,6 @@ private:
reset();
} else {
state.paused = !state.paused;
if (auto* power = context.powerManager())
power->setSlowMode(state.paused);
}
dirty = true;
}
@@ -260,7 +256,7 @@ private:
void scheduleDropTimer() {
cancelDropTimer();
const std::uint32_t interval = dropIntervalMs();
dropTimer = context.scheduleRepeatingTimer(interval);
dropTimer = context.scheduleRepeatingTimer(interval);
}
void cancelDropTimer() {
@@ -276,8 +272,8 @@ private:
}
[[nodiscard]] std::uint32_t dropIntervalMs() const {
const int base = 700;
const int step = 50;
const int base = 700;
const int step = 50;
int interval = base - (state.level - 1) * step;
if (interval < 120)
interval = 120;
@@ -322,7 +318,7 @@ private:
void rotate(int direction) {
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
nextRot = ((nextRot % 4) + 4) % 4;
nextRot = ((nextRot % 4) + 4) % 4;
if (canPlace(state.current.x, state.current.y, nextRot)) {
state.current.rotation = nextRot;
dirty = true;
@@ -387,8 +383,6 @@ private:
cancelDropTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepGameOver();
if (auto* power = context.powerManager())
power->setSlowMode(true);
} else {
if (auto* buzzer = context.buzzer())
buzzer->beepLock();
@@ -484,11 +478,10 @@ private:
for (int y = 0; y < kBoardHeight; ++y) {
for (int x = 0; x < kBoardWidth; ++x) {
if (int value = cellAt(x, y); value != 0)
drawCell(originX, originY, x, y, value, true);
drawCell(originX, originY, x, y, value - 1, true);
}
}
drawGuides(originX, originY);
drawBoardFrame(originX, originY);
}
void drawActivePiece() {
@@ -502,32 +495,76 @@ private:
int gy = state.current.y + block.y;
if (gy < 0)
continue;
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
drawCell(originX, originY, gx, gy, state.current.type, false);
}
}
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
const int x0 = originX + cx * kCellSize;
const int y0 = originY + cy * kCellSize;
for (int dy = 0; dy < kCellSize; ++dy) {
for (int dx = 0; dx < kCellSize; ++dx) {
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
static bool patternPixel(int pieceIndex, int dx, int dy) {
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
const int mx = dx & 0x3;
const int my = dy & 0x3;
switch (idx) {
case 0: // I - vertical stripes
return mx < 2;
case 1: // J - horizontal stripes
return my < 2;
case 2: { // L - forward diagonal
const int sum = (mx + my) & 0x3;
return sum < 2;
}
case 3: // O - diamond centerpiece
return (mx == 1 && my == 1) || (mx == 2 && my == 1) || (mx == 1 && my == 2) || (mx == 2 && my == 2);
case 4: // S - checkerboard
return ((mx ^ my) & 0x1) == 0;
case 5: { // T - cross
return (mx == 0) || (my == 0);
}
case 6: { // Z - backward diagonal
int diff = mx - my;
if (diff < 0)
diff += 4;
diff &= 0x3;
return diff < 2;
}
}
return true;
}
void drawPatternBlock(int x0, int y0, int size, int pieceIndex, bool locked) {
const int idx = std::clamp(pieceIndex, 0, static_cast<int>(kPieces.size()) - 1);
for (int dy = 0; dy < size; ++dy) {
for (int dx = 0; dx < size; ++dx) {
const bool border = dx == 0 || dx == size - 1 || dy == 0 || dy == size - 1;
bool fill = patternPixel(idx, dx, dy);
if (!locked && !border)
fill = fill && (((dx + dy) & 0x1) == 0);
const bool on = border || fill;
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
}
}
(void) value; // value currently unused (monochrome display)
}
void drawGuides(int originX, int originY) {
for (int y = 0; y <= kBoardHeight; ++y) {
const int py = originY + y * kCellSize;
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
void drawCell(int originX, int originY, int cx, int cy, int pieceIndex, bool locked) {
const int x0 = originX + cx * kCellSize;
const int y0 = originY + cy * kCellSize;
drawPatternBlock(x0, y0, kCellSize, pieceIndex, locked);
}
void drawBoardFrame(int originX, int originY) {
const int widthPixels = kBoardWidth * kCellSize;
const int heightPixels = kBoardHeight * kCellSize;
const int x0 = originX;
const int y0 = originY;
const int x1 = originX + widthPixels - 1;
const int y1 = originY + heightPixels - 1;
for (int x = x0; x <= x1; ++x) {
framebuffer.drawPixel(x, y0, true);
framebuffer.drawPixel(x, y1, true);
}
for (int x = 0; x <= kBoardWidth; ++x) {
const int px = originX + x * kCellSize;
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
for (int y = y0; y <= y1; ++y) {
framebuffer.drawPixel(x0, y, true);
framebuffer.drawPixel(x1, y, true);
}
}
@@ -539,15 +576,14 @@ private:
for (int dy = 0; dy < boxSize; ++dy)
for (int dx = 0; dx < boxSize; ++dx)
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
framebuffer.drawPixel(originX + dx, originY + dy,
(dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
const auto& piece = kPieces[state.nextPiece];
for (const auto& block: piece.rotations[0]) {
const int px = originX + (block.x + 1) * blockSize;
const int py = originY + (block.y + 1) * blockSize;
for (int dy = 1; dy < blockSize - 1; ++dy)
for (int dx = 1; dx < blockSize - 1; ++dx)
framebuffer.drawPixel(px + dx, py + dy, true);
const int px = originX + (block.x + 1) * blockSize + 1;
const int py = originY + (block.y + 1) * blockSize + 1;
drawPatternBlock(px, py, blockSize - 2, state.nextPiece, true);
}
}
@@ -608,7 +644,7 @@ private:
class TetrisFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kTetrisAppName; }
const char* name() const override { return kTetrisAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
return std::make_unique<TetrisApp>(context);
}
@@ -616,8 +652,6 @@ public:
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
return std::make_unique<TetrisFactory>();
}
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() { return std::make_unique<TetrisFactory>(); }
} // namespace apps

View File

@@ -1,5 +1,7 @@
add_library(cardboy_backend_interface INTERFACE)
target_link_libraries(cardboy_backend_interface INTERFACE cardboy_utils)
set_target_properties(cardboy_backend_interface PROPERTIES
EXPORT_NAME backend_interface
)
@@ -14,6 +16,8 @@ target_sources(cardboy_backend_interface
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/backend/backend_interface.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/backend.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/display_spec.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/event_bus.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/loop_hooks.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/input_state.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/services.hpp

View File

@@ -0,0 +1,42 @@
#pragma once
#include <cstdint>
namespace cardboy::sdk {
enum class EventBusSignal : std::uint32_t {
None = 0,
Input = 1u << 0,
Timer = 1u << 1,
External = 1u << 2,
};
inline EventBusSignal operator|(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) | static_cast<std::uint32_t>(rhs));
}
inline EventBusSignal& operator|=(EventBusSignal& lhs, EventBusSignal rhs) {
lhs = lhs | rhs;
return lhs;
}
inline EventBusSignal operator&(EventBusSignal lhs, EventBusSignal rhs) {
return static_cast<EventBusSignal>(static_cast<std::uint32_t>(lhs) & static_cast<std::uint32_t>(rhs));
}
inline std::uint32_t to_event_bits(EventBusSignal signal) { return static_cast<std::uint32_t>(signal); }
class IEventBus {
public:
static constexpr std::uint32_t kWaitForever = 0xFFFFFFFFu;
virtual ~IEventBus() = default;
virtual void signal(std::uint32_t bits) = 0;
virtual void signalFromISR(std::uint32_t bits) = 0;
virtual std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) = 0;
virtual void scheduleTimerSignal(std::uint32_t delay_ms) = 0;
virtual void cancelTimerSignal() = 0;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,18 @@
#pragma once
namespace cardboy::sdk {
class FramebufferHooks {
public:
using PreSendHook = void (*)(void* framebuffer, void* userData);
static void setPreSendHook(PreSendHook hook, void* userData);
static void clearPreSendHook();
static void invokePreSend(void* framebuffer);
private:
static PreSendHook hook_;
static void* userData_;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,11 @@
#pragma once
namespace cardboy::sdk {
class ILoopHooks {
public:
virtual ~ILoopHooks() = default;
virtual void onLoopIteration() = 0;
};
} // namespace cardboy::sdk

View File

@@ -1,5 +1,6 @@
#pragma once
#include "cardboy/sdk/framebuffer_hooks.hpp"
#include "input_state.hpp"
#include <concepts>
@@ -23,6 +24,9 @@ concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
{ impl.sendFrame_impl(flag) };
};
template<typename Impl>
concept HasDrawBits8Impl = requires(Impl& impl, int x, int y, std::uint8_t bits) { impl.drawBits8_impl(x, y, bits); };
template<typename Impl>
concept HasFrameInFlightImpl = requires(const Impl& impl) {
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
@@ -37,11 +41,19 @@ concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
template<typename Impl>
class FramebufferFacade {
public:
[[nodiscard]] int width() const { return impl().width_impl(); }
[[nodiscard]] int height() const { return impl().height_impl(); }
[[nodiscard]] __attribute__((always_inline)) int width() const { return impl().width_impl(); }
[[nodiscard]] __attribute__((always_inline)) int height() const { return impl().height_impl(); }
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
__attribute__((always_inline)) void drawBits8(int x, int y, std::uint8_t bits) {
if constexpr (detail::HasDrawBits8Impl<Impl>) {
impl().drawBits8_impl(x, y, bits);
} else {
defaultDrawBits8(x, y, bits);
}
}
void clear(bool on) {
if constexpr (detail::HasClearImpl<Impl>) {
impl().clear_impl(on);
@@ -56,8 +68,10 @@ public:
}
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
if constexpr (detail::HasSendFrameImpl<Impl>)
if constexpr (detail::HasSendFrameImpl<Impl>) {
FramebufferHooks::invokePreSend(&impl());
impl().sendFrame_impl(clearDrawBuffer);
}
}
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
@@ -79,6 +93,14 @@ private:
for (int x = 0; x < width(); ++x)
drawPixel(x, y, on);
}
void defaultDrawBits8(int x, int y, std::uint8_t bits) {
for (int col = 0; col < 8; ++col) {
const std::uint8_t mask = static_cast<std::uint8_t>(1u << (7 - col));
const bool pixelOn = (bits & mask) != 0;
drawPixel(x + col, y, pixelOn);
}
}
};
template<typename Impl>

View File

@@ -1,5 +1,8 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/loop_hooks.hpp"
#include <cstdint>
#include <string>
#include <string_view>
@@ -57,14 +60,6 @@ public:
[[nodiscard]] virtual std::uint64_t micros() = 0;
};
class IPowerManager {
public:
virtual ~IPowerManager() = default;
virtual void setSlowMode(bool enable) = 0;
[[nodiscard]] virtual bool isSlowMode() const = 0;
};
class IFilesystem {
public:
virtual ~IFilesystem() = default;
@@ -80,8 +75,9 @@ struct Services {
IStorage* storage = nullptr;
IRandom* random = nullptr;
IHighResClock* highResClock = nullptr;
IPowerManager* powerManager = nullptr;
IFilesystem* filesystem = nullptr;
IEventBus* eventBus = nullptr;
ILoopHooks* loopHooks = nullptr;
};
} // namespace cardboy::sdk

View File

@@ -1,14 +1,17 @@
#pragma once
#include "cardboy/sdk/event_bus.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
#include <SFML/Graphics.hpp>
#include <SFML/Window/Keyboard.hpp>
#include <chrono>
#include <condition_variable>
#include <cstdint>
#include <filesystem>
#include <limits>
#include <mutex>
#include <random>
#include <string>
#include <string_view>
@@ -70,15 +73,6 @@ private:
const std::chrono::steady_clock::time_point start;
};
class DesktopPowerManager final : public cardboy::sdk::IPowerManager {
public:
void setSlowMode(bool enable) override { slowMode = enable; }
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
private:
bool slowMode = false;
};
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
public:
DesktopFilesystem();
@@ -92,6 +86,29 @@ private:
bool mounted = false;
};
class DesktopEventBus final : public cardboy::sdk::IEventBus {
public:
explicit DesktopEventBus(DesktopRuntime& owner);
~DesktopEventBus() override;
void signal(std::uint32_t bits) override;
void signalFromISR(std::uint32_t bits) override;
std::uint32_t wait(std::uint32_t mask, std::uint32_t timeout_ms) override;
void scheduleTimerSignal(std::uint32_t delay_ms) override;
void cancelTimerSignal() override;
private:
DesktopRuntime& runtime;
std::mutex mutex;
std::condition_variable cv;
std::uint32_t pendingBits = 0;
std::mutex timerMutex;
std::condition_variable timerCv;
std::thread timerThread;
bool timerCancel = false;
};
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
@@ -168,8 +185,8 @@ private:
DesktopStorage storageService;
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopPowerManager powerService;
DesktopFilesystem filesystemService;
DesktopEventBus eventBusService;
cardboy::sdk::Services services{};
};

View File

@@ -13,6 +13,92 @@
namespace cardboy::backend::desktop {
DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {}
DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); }
void DesktopEventBus::signal(std::uint32_t bits) {
if (bits == 0)
return;
{
std::lock_guard<std::mutex> lock(mutex);
pendingBits |= bits;
}
cv.notify_all();
}
void DesktopEventBus::signalFromISR(std::uint32_t bits) { signal(bits); }
std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (mask == 0)
return 0;
const auto start = std::chrono::steady_clock::now();
const bool infinite = timeout_ms == cardboy::sdk::IEventBus::kWaitForever;
while (true) {
{
std::lock_guard<std::mutex> lock(mutex);
const std::uint32_t ready = pendingBits & mask;
if (ready != 0) {
pendingBits &= ~mask;
return ready;
}
}
if (!infinite) {
const auto now = std::chrono::steady_clock::now();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
if (elapsedMs >= static_cast<std::int64_t>(timeout_ms))
return 0;
const auto remaining = timeout_ms - static_cast<std::uint32_t>(elapsedMs);
runtime.sleepFor(std::min<std::uint32_t>(remaining, 8));
} else {
runtime.sleepFor(8);
}
}
}
void DesktopEventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
cancelTimerSignal();
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
timerThread = std::thread([this, delay_ms]() {
std::unique_lock<std::mutex> lock(timerMutex);
const bool cancelled =
timerCv.wait_for(lock, std::chrono::milliseconds(delay_ms), [this] { return timerCancel; });
lock.unlock();
if (!cancelled)
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
});
}
void DesktopEventBus::cancelTimerSignal() {
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = true;
}
timerCv.notify_all();
if (timerThread.joinable())
timerThread.join();
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
}
bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) {
auto it = data.find(composeKey(ns, key));
if (it == data.end())
@@ -90,6 +176,7 @@ DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
bool handled = true;
switch (key) {
case sf::Keyboard::Key::Up:
state.up = pressed;
@@ -118,8 +205,11 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
state.start = pressed;
break;
default:
handled = false;
break;
}
if (handled)
runtime.eventBusService.signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
}
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
@@ -137,13 +227,13 @@ DesktopRuntime::DesktopRuntime() :
"Cardboy Desktop"),
texture(), sprite(texture),
pixels(static_cast<std::size_t>(cardboy::sdk::kDisplayWidth * cardboy::sdk::kDisplayHeight) * 4, 0),
framebuffer(*this), input(*this), clock(*this) {
framebuffer(*this), input(*this), clock(*this), eventBusService(*this) {
window.setFramerateLimit(60);
if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight}))
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
sprite.setTexture(texture, true);
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
clearPixels(false);
clearPixels(true);
presentIfNeeded();
services.buzzer = &buzzerService;
@@ -151,8 +241,9 @@ DesktopRuntime::DesktopRuntime() :
services.storage = &storageService;
services.random = &randomService;
services.highResClock = &highResService;
services.powerManager = &powerService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
services.loopHooks = nullptr;
}
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }
@@ -161,7 +252,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
return;
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
pixels[idx + 0] = value;
pixels[idx + 1] = value;
pixels[idx + 2] = value;
@@ -170,7 +261,7 @@ void DesktopRuntime::setPixel(int x, int y, bool on) {
}
void DesktopRuntime::clearPixels(bool on) {
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
const std::uint8_t value = on ? static_cast<std::uint8_t>(0) : static_cast<std::uint8_t>(255);
for (std::size_t i = 0; i < pixels.size(); i += 4) {
pixels[i + 0] = value;
pixels[i + 1] = value;

View File

@@ -2,6 +2,9 @@ cmake_minimum_required(VERSION 3.16)
add_library(cardboy_sdk STATIC
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/status_bar.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/framebuffer_hooks.cpp
${CMAKE_CURRENT_SOURCE_DIR}/src/persistent_settings.cpp
)
set_target_properties(cardboy_sdk PROPERTIES

View File

@@ -26,22 +26,76 @@ inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
return fonts_Terminess_Powerline[uc];
}
enum class Rotation {
None,
Clockwise90,
CounterClockwise90,
};
struct TextBounds {
int width = 0;
int height = 0;
};
template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto& rows = glyphBitmap(ch);
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
fb.drawBits8(x, y + row, rowBits);
}
return;
}
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
for (int col = 0; col < kGlyphWidth; ++col) {
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
if (rowBits & mask) {
for (int sx = 0; sx < scale; ++sx)
for (int sy = 0; sy < scale; ++sy)
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, on);
if ((rowBits & mask) == 0)
continue;
for (int sx = 0; sx < scale; ++sx) {
for (int sy = 0; sy < scale; ++sy) {
int dstX = x;
int dstY = y;
switch (rotation) {
case Rotation::None:
dstX += col * scale + sx;
dstY += row * scale + sy;
break;
case Rotation::Clockwise90:
dstX += row * scale + sx;
dstY += (kGlyphWidth - 1 - col) * scale + sy;
break;
case Rotation::CounterClockwise90:
dstX += (kGlyphHeight - 1 - row) * scale + sx;
dstY += col * scale + sy;
break;
}
fb.drawPixel(dstX, dstY, on);
}
}
}
}
}
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
Rotation rotation = Rotation::None) {
if (text.empty())
return {};
const int advance = (kGlyphWidth + letterSpacing) * scale;
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
const int height = kGlyphHeight * scale;
switch (rotation) {
case Rotation::None:
return {extent, height};
case Rotation::Clockwise90:
case Rotation::CounterClockwise90:
return {height, extent};
}
return {extent, height};
}
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
if (text.empty())
return 0;
@@ -51,11 +105,22 @@ inline int measureText(std::string_view text, int scale = 1, int letterSpacing =
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 cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on);
cursor += (kGlyphWidth + letterSpacing) * scale;
int letterSpacing = 1, Rotation rotation = Rotation::None) {
if (text.empty())
return;
const int advance = (kGlyphWidth + letterSpacing) * scale;
if (rotation == Rotation::None) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
cursor += advance;
}
} else {
int cursor = y;
for (char ch: text) {
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
cursor += advance;
}
}
}

View File

@@ -61,8 +61,9 @@ struct AppContext {
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
[[nodiscard]] IEventBus* eventBus() const { return services ? services->eventBus : nullptr; }
[[nodiscard]] ILoopHooks* loopHooks() const { return services ? services->loopHooks : nullptr; }
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;

View File

@@ -1,5 +1,6 @@
#pragma once
#include <cardboy/sdk/event_bus.hpp>
#include "app_framework.hpp"
#include <cstdint>
@@ -12,6 +13,7 @@ namespace cardboy::sdk {
class AppSystem {
public:
explicit AppSystem(AppContext context);
~AppSystem();
void registerApp(std::unique_ptr<IAppFactory> factory);
bool startApp(const std::string& name);
@@ -19,12 +21,12 @@ public:
void run();
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
@@ -50,16 +52,18 @@ private:
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
void notifyEventBus(EventBusSignal signal);
AppContext context;
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
bool suppressInputs = false;
};
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {

View File

@@ -0,0 +1,20 @@
#pragma once
#include <cstddef>
namespace cardboy::sdk {
class FramebufferHooks {
public:
using PreSendHook = void (*)(void* framebuffer, void* userData);
static void setPreSendHook(PreSendHook hook, void* userData);
static void clearPreSendHook();
static void invokePreSend(void* framebuffer);
private:
static PreSendHook hook_;
static void* userData_;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,15 @@
#pragma once
#include "cardboy/sdk/services.hpp"
namespace cardboy::sdk {
struct PersistentSettings {
bool mute = false;
bool autoLightSleep = false;
};
PersistentSettings loadPersistentSettings(Services* services);
void savePersistentSettings(Services* services, const PersistentSettings& settings);
} // namespace cardboy::sdk

View File

@@ -0,0 +1,92 @@
#pragma once
#include "cardboy/sdk/input_state.hpp"
#include "cardboy/sdk/services.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include <algorithm>
#include <cstddef>
#include <cstdint>
#include <string>
#include <string_view>
namespace cardboy::sdk {
class StatusBar {
public:
static StatusBar& instance();
void setServices(Services* services) { services_ = services; }
void setEnabled(bool value);
void toggle();
[[nodiscard]] bool isEnabled() const { return enabled_; }
void setCurrentAppName(std::string_view name);
[[nodiscard]] bool handleToggleInput(const InputState& current, const InputState& previous);
template<typename Framebuffer>
void renderIfEnabled(Framebuffer& fb) {
if (!enabled_)
return;
renderBar(fb);
}
private:
StatusBar() = default;
template<typename Framebuffer>
void renderBar(Framebuffer& fb) {
const int width = fb.width();
if (width <= 0)
return;
const int barHeight = font16x8::kGlyphHeight + 2;
const int fillHeight = std::min(barHeight, fb.height());
if (fillHeight <= 0)
return;
const std::string leftText = prepareLeftText(width);
const std::string rightText = prepareRightText();
for (int y = 0; y < fillHeight; ++y) {
for (int x = 0; x < width; ++x)
fb.drawPixel(x, y, true);
}
for (int x = 0; x < width; ++x)
fb.drawPixel(x, 0, false);
const int textY = 1;
const int bottomSeparatorY = textY + font16x8::kGlyphHeight + 1;
if (bottomSeparatorY < fillHeight) {
for (int x = 0; x < width; ++x)
fb.drawPixel(x, bottomSeparatorY, (x % 2) != 0);
}
const int leftX = 2;
if (!leftText.empty())
font16x8::drawText(fb, leftX, textY, leftText, 1, false, 1);
if (!rightText.empty()) {
int rightWidth = font16x8::measureText(rightText, 1, 1);
int rightX = width - rightWidth - 2;
const int minRightX = leftX + font16x8::measureText(leftText, 1, 1) + 6;
if (rightX < minRightX)
rightX = std::max(minRightX, width / 2);
if (rightX < width)
font16x8::drawText(fb, rightX, textY, rightText, 1, false, 1);
}
}
[[nodiscard]] std::string prepareLeftText(int displayWidth) const;
[[nodiscard]] std::string prepareRightText() const;
bool enabled_ = false;
Services* services_ = nullptr;
std::string appName_{};
};
} // namespace cardboy::sdk

View File

@@ -1,4 +1,6 @@
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/framebuffer_hooks.hpp"
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <limits>
@@ -11,10 +13,28 @@ namespace {
a.select != b.select || a.start != b.start;
}
constexpr std::uint32_t kIdlePollMs = 16;
[[nodiscard]] bool anyButtonPressed(const InputState& state) {
return state.up || state.down || state.left || state.right || state.a || state.b || state.select || state.start;
}
template<typename Framebuffer>
void statusBarPreSendHook(void* framebuffer, void* userData) {
auto* fb = static_cast<Framebuffer*>(framebuffer);
auto* status = static_cast<StatusBar*>(userData);
if (fb && status)
status->renderIfEnabled(*fb);
}
} // namespace
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) {
context.system = this;
auto& statusBar = StatusBar::instance();
statusBar.setServices(context.services);
using FBType = typename AppContext::Framebuffer;
FramebufferHooks::setPreSendHook(&statusBarPreSendHook<FBType>, &statusBar);
}
AppSystem::~AppSystem() { FramebufferHooks::clearPreSendHook(); }
void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
if (!factory)
@@ -53,6 +73,9 @@ bool AppSystem::startAppByIndex(std::size_t index) {
clearTimersForCurrentApp();
current = std::move(app);
lastInputState = context.input.readState();
suppressInputs = true;
StatusBar::instance().setServices(context.services);
StatusBar::instance().setCurrentAppName(activeFactory ? activeFactory->name() : "");
current->onStart();
return true;
}
@@ -67,12 +90,21 @@ void AppSystem::run() {
events.reserve(4);
while (true) {
if (auto* hooks = context.loopHooks())
hooks->onLoopIteration();
events.clear();
const std::uint32_t now = context.clock.millis();
processDueTimers(now, events);
const InputState inputNow = context.input.readState();
if (inputsDiffer(inputNow, lastInputState)) {
const InputState inputNow = context.input.readState();
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
if (suppressInputs) {
lastInputState = inputNow;
if (!anyButtonPressed(inputNow))
suppressInputs = false;
} else if (!consumedByStatusToggle && inputsDiffer(inputNow, lastInputState)) {
AppEvent evt{};
evt.type = AppEventType::Button;
evt.timestamp_ms = now;
@@ -80,6 +112,8 @@ void AppSystem::run() {
evt.button.previous = lastInputState;
events.push_back(evt);
lastInputState = inputNow;
} else if (consumedByStatusToggle) {
lastInputState = inputNow;
}
for (const auto& evt: events) {
@@ -94,13 +128,19 @@ void AppSystem::run() {
if (waitMs == 0)
continue;
if (waitMs == std::numeric_limits<std::uint32_t>::max())
waitMs = kIdlePollMs;
else
waitMs = std::min(waitMs, kIdlePollMs);
auto* eventBus = context.eventBus();
if (!eventBus)
return;
if (waitMs > 0)
context.clock.sleep_ms(waitMs);
const std::uint32_t mask = to_event_bits(EventBusSignal::Input) | to_event_bits(EventBusSignal::Timer);
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
eventBus->cancelTimerSignal();
eventBus->wait(mask, IEventBus::kWaitForever);
} else {
eventBus->scheduleTimerSignal(waitMs);
eventBus->wait(mask, IEventBus::kWaitForever);
}
}
}
@@ -134,24 +174,32 @@ AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
record.repeat = repeat;
record.active = true;
timers.push_back(record);
notifyEventBus(EventBusSignal::Timer);
return record.id;
}
void AppSystem::cancelTimer(AppTimerHandle handle) {
auto* timer = findTimer(handle);
if (timer)
timer->active = false;
if (!timer)
return;
timer->active = false;
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
notifyEventBus(EventBusSignal::Timer);
}
void AppSystem::cancelAllTimers() {
bool changed = false;
for (auto& timer: timers) {
if (timer.generation == currentGeneration)
if (timer.generation == currentGeneration && timer.active) {
timer.active = false;
changed = true;
}
}
timers.erase(std::remove_if(timers.begin(), timers.end(), [](const TimerRecord& rec) { return !rec.active; }),
timers.end());
if (changed)
notifyEventBus(EventBusSignal::Timer);
}
void AppSystem::dispatchEvent(const AppEvent& event) {
@@ -196,8 +244,11 @@ std::uint32_t AppSystem::nextTimerDueMs(std::uint32_t now) const {
}
void AppSystem::clearTimersForCurrentApp() {
const bool hadTimers = !timers.empty();
++currentGeneration;
timers.clear();
if (hadTimers)
notifyEventBus(EventBusSignal::Timer);
}
AppSystem::TimerRecord* AppSystem::findTimer(AppTimerHandle handle) {
@@ -227,5 +278,11 @@ bool AppSystem::handlePendingSwitchRequest() {
return switched;
}
} // namespace cardboy::sdk
void AppSystem::notifyEventBus(EventBusSignal signal) {
if (signal == EventBusSignal::None)
return;
if (auto* bus = context.eventBus())
bus->signal(to_event_bits(signal));
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,23 @@
#include "cardboy/sdk/framebuffer_hooks.hpp"
namespace cardboy::sdk {
FramebufferHooks::PreSendHook FramebufferHooks::hook_ = nullptr;
void* FramebufferHooks::userData_ = nullptr;
void FramebufferHooks::setPreSendHook(PreSendHook hook, void* userData) {
hook_ = hook;
userData_ = userData;
}
void FramebufferHooks::clearPreSendHook() {
hook_ = nullptr;
userData_ = nullptr;
}
void FramebufferHooks::invokePreSend(void* framebuffer) {
if (hook_)
hook_(framebuffer, userData_);
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,40 @@
#include "cardboy/sdk/persistent_settings.hpp"
#include <cstdint>
#include <string_view>
namespace cardboy::sdk {
namespace {
constexpr std::string_view kNamespace = "settings";
constexpr std::string_view kMuteKey = "mute";
constexpr std::string_view kAutoLightSleepKey = "autosleep";
[[nodiscard]] std::uint32_t boolToStorage(bool value) { return value ? 1U : 0U; }
[[nodiscard]] bool storageToBool(std::uint32_t value) { return value != 0U; }
} // namespace
PersistentSettings loadPersistentSettings(Services* services) {
PersistentSettings settings{};
if (!services || !services->storage)
return settings;
std::uint32_t raw = 0;
if (services->storage->readUint32(kNamespace, kMuteKey, raw))
settings.mute = storageToBool(raw);
if (services->storage->readUint32(kNamespace, kAutoLightSleepKey, raw))
settings.autoLightSleep = storageToBool(raw);
return settings;
}
void savePersistentSettings(Services* services, const PersistentSettings& settings) {
if (!services || !services->storage)
return;
services->storage->writeUint32(kNamespace, kMuteKey, boolToStorage(settings.mute));
services->storage->writeUint32(kNamespace, kAutoLightSleepKey, boolToStorage(settings.autoLightSleep));
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,75 @@
#include "cardboy/sdk/status_bar.hpp"
#include <algorithm>
#include <cctype>
#include <cmath>
#include <cstdio>
namespace cardboy::sdk {
StatusBar& StatusBar::instance() {
static StatusBar bar;
return bar;
}
void StatusBar::setEnabled(bool value) { enabled_ = value; }
void StatusBar::toggle() {
enabled_ = !enabled_;
if (services_ && services_->buzzer)
services_->buzzer->beepMove();
}
void StatusBar::setCurrentAppName(std::string_view name) {
appName_.assign(name.begin(), name.end());
std::transform(appName_.begin(), appName_.end(), appName_.begin(),
[](unsigned char ch) { return static_cast<char>(std::toupper(ch)); });
}
bool StatusBar::handleToggleInput(const InputState& current, const InputState& previous) {
const bool comboNow = current.start && current.select && current.up;
const bool comboPrev = previous.start && previous.select && previous.up;
if (comboNow && !comboPrev) {
toggle();
return true;
}
return false;
}
std::string StatusBar::prepareLeftText(int displayWidth) const {
std::string text = appName_.empty() ? std::string("CARDBOY") : appName_;
int maxWidth = std::max(0, displayWidth - 32);
while (!text.empty() && font16x8::measureText(text, 1, 1) > maxWidth)
text.pop_back();
return text;
}
std::string StatusBar::prepareRightText() const {
if (!services_)
return {};
std::string right;
if (services_->battery && services_->battery->hasData()) {
const float current = services_->battery->current();
const float chargeMah = services_->battery->charge();
const float fallbackV = services_->battery->voltage();
char buf[64];
if (std::isfinite(current) && std::isfinite(chargeMah)) {
std::snprintf(buf, sizeof(buf), "cur %.2fmA chr %.2fmAh", static_cast<double>(current),
static_cast<double>(chargeMah));
} else {
std::snprintf(buf, sizeof(buf), "vol %.2fV", static_cast<double>(fallbackV));
}
right.assign(buf);
}
if (services_->buzzer && services_->buzzer->isMuted()) {
if (!right.empty())
right.append(" ");
right.append("MUTE");
}
return right;
}
} // namespace cardboy::sdk

View File

@@ -1,9 +1,11 @@
#include "cardboy/apps/clock_app.hpp"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/settings_app.hpp"
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/desktop_backend.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/persistent_settings.hpp"
#include <cstdio>
#include <exception>
@@ -18,7 +20,13 @@ int main() {
context.services = &runtime.serviceRegistry();
cardboy::sdk::AppSystem system(context);
const cardboy::sdk::PersistentSettings persistentSettings =
cardboy::sdk::loadPersistentSettings(context.getServices());
if (auto* buzzer = context.buzzer())
buzzer->setMuted(persistentSettings.mute);
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createSettingsAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createGameboyAppFactory());
system.registerApp(apps::createTetrisAppFactory());

View File

@@ -0,0 +1,21 @@
add_library(cardboy_utils INTERFACE)
option(CARDBOY_MORE_CHECKS "More checks" OFF)
set_target_properties(cardboy_utils PROPERTIES
EXPORT_NAME utils
)
target_include_directories(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
if(CARDBOY_MORE_CHECKS)
target_compile_definitions(cardboy_utils INTERFACE CARDBOY_MORE_CHECKS=1)
endif()
target_sources(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/utils/utils.hpp
)

View File

@@ -0,0 +1,59 @@
//
// Created by Stepan Usatiuk on 11.10.2025.
//
#ifndef CARDBOY_SDK_UTILS_HPP
#define CARDBOY_SDK_UTILS_HPP
#ifndef CARDBOY_MORE_CHECKS
#define CARDBOY_MORE_CHECKS 0
#endif
#if CARDBOY_MORE_CHECKS
#include <cstdio>
#include <cstdlib>
// Fails the program with a message. Internal use.
#define CARDBOY__CHECK_FAIL_IMPL(expr_str, file, line, func, msg_opt) \
do { \
std::fprintf(stderr, \
"CARDBOY_CHECK failed: %s\n at %s:%d in %s%s%s\n", \
(expr_str), (file), (line), (func), \
((msg_opt) ? "\n message: " : ""), \
((msg_opt) ? (msg_opt) : "")); \
std::fflush(stderr); \
std::abort(); \
} while (0)
// Runtime check that is active only when CARDBOY_MORE_CHECKS != 0.
// Evaluates the expression exactly once.
#define CARDBOY_CHECK(expr) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, NULL); \
} \
} while (0)
// Same as CARDBOY_CHECK but allows providing a custom C-string message.
#define CARDBOY_CHECK_MSG(expr, msg) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, (msg));\
} \
} while (0)
// Execute arbitrary code only when checks are enabled.
#define CARDBOY_CHECK_CODE(code) \
do { \
code; \
} while (0)
#else
// Checks compiled out when CARDBOY_MORE_CHECKS == 0.
#define CARDBOY_CHECK(expr) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_MSG(expr, _) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_CODE(code) do { } while (0)
#endif
#endif // CARDBOY_SDK_UTILS_HPP

View File

@@ -644,7 +644,7 @@ CONFIG_COMPILER_RT_LIB_GCCLIB=y
CONFIG_COMPILER_RT_LIB_NAME="gcc"
CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set
CONFIG_COMPILER_STATIC_ANALYZER=y
# CONFIG_COMPILER_STATIC_ANALYZER is not set
# end of Compiler options
#