Compare commits

...

14 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
30 changed files with 1468 additions and 185 deletions

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

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

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

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

@@ -2,6 +2,7 @@
#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"
@@ -58,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

@@ -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"
@@ -112,12 +111,6 @@ 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; }
@@ -128,6 +121,11 @@ public:
}
};
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();
@@ -224,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);
}

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

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

View File

@@ -1,6 +1,33 @@
#include "cardboy/apps/gameboy_app.hpp"
uint8_t audio_read(uint16_t addr);
void audio_write(uint16_t addr, uint8_t value);
namespace {
using AudioReadThunk = uint8_t (*)(void*, uint16_t);
using AudioWriteThunk = void (*)(void*, uint16_t, uint8_t);
void* gAudioCtx = nullptr;
AudioReadThunk gAudioReadThunk = nullptr;
AudioWriteThunk gAudioWriteThunk = nullptr;
} // namespace
uint8_t audio_read(uint16_t addr) {
if (gAudioReadThunk && gAudioCtx)
return gAudioReadThunk(gAudioCtx, addr);
return 0xFF;
}
void audio_write(uint16_t addr, uint8_t value) {
if (gAudioWriteThunk && gAudioCtx)
gAudioWriteThunk(gAudioCtx, addr, value);
}
#define ENABLE_SOUND 1
#include "cardboy/apps/peanut_gb.h"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
@@ -12,17 +39,84 @@
#include <algorithm>
#include <array>
#include <atomic>
#include <cctype>
#include <cerrno>
#include <chrono>
#include <cmath>
#include <cstdint>
#include <cstdio>
#include <cstring>
#include <dirent.h>
#include <mutex>
#include <string>
#include <string_view>
#include <sys/stat.h>
#include <vector>
#include "esp_pm.h"
#include "driver/gpio.h"
#include "driver/gptimer.h"
#include "hal/sdm_types.h"
#include "esp_attr.h"
#include "esp_err.h"
#include "esp_heap_caps.h"
#include "esp_log.h"
#include "esp_check.h"
#include "esp_pm.h"
#include "esp_clk_tree.h"
#include "driver/gpio.h"
#include "driver/sdm.h"
#include "hal/gpio_hal.h"
#include "hal/sdm_hal.h"
#include "hal/sdm_ll.h"
#include "hal/hal_utils.h"
#include "soc/sdm_periph.h"
struct sdm_group_t {
int group_id; // Group ID, index from 0
portMUX_TYPE spinlock; // to protect per-group register level concurrent access
sdm_hal_context_t hal; // hal context
sdm_channel_t *channels[SOC_SDM_CHANNELS_PER_GROUP]; // array of sdm channels
sdm_clock_source_t clk_src; // Clock source
};
struct sdm_platform_t {
_lock_t mutex; // platform level mutex lock
sdm_group_t *groups[SOC_SDM_GROUPS]; // sdm group pool
int group_ref_counts[SOC_SDM_GROUPS]; // reference count used to protect group install/uninstall
};
typedef enum {
SDM_FSM_INIT,
SDM_FSM_ENABLE,
} sdm_fsm_t;
#define SDM_PM_LOCK_NAME_LEN_MAX 16
struct sdm_channel_t {
sdm_group_t *group; // which group the sdm channel belongs to
uint32_t chan_id; // allocated channel numerical ID
int gpio_num; // GPIO number
uint32_t sample_rate_hz; // Sample rate, in Hz
portMUX_TYPE spinlock; // to protect per-channels resources concurrently accessed by task and ISR handler
esp_pm_lock_handle_t pm_lock; // PM lock, for glitch filter, as that module can only be functional under APB
sdm_fsm_t fsm; // FSM state
#if CONFIG_PM_ENABLE
char pm_lock_name[SDM_PM_LOCK_NAME_LEN_MAX]; // pm lock name
#endif
};
#include "hal/sdm_hal.h"
#include "hal/sdm_ll.h"
#include "driver/sdm.h"
#include "esp_err.h"
#include "esp_log.h"
#include "esp_random.h"
extern "C" {
#include "minigb_apu/minigb_apu.h"
}
#define GAMEBOY_PERF_METRICS 0
@@ -38,6 +132,12 @@
static constexpr uint8_t kPattern2x2[4] = {0, 2, 3, 1}; // [ (y&1)<<1 | (x&1) ]
static constexpr bool kEnableGbPwmPrototype = true;
static constexpr gpio_num_t kGbPwmPrototypePin = GPIO_NUM_25;
static constexpr uint32_t kGbPwmSampleRateHz = AUDIO_SAMPLE_RATE;
static constexpr uint32_t kGbPwmCarrierHz = 8000000;
static constexpr int kGbSdmHardwareChannels = 4; // ESP32-H2 SDM offers up to four channels per TRM Section 4.4
// exact predicate per your shouldPixelBeOn
static constexpr uint8_t on_bit(uint8_t v, uint8_t threshold) {
if (v >= 3)
@@ -106,6 +206,22 @@ namespace {
constexpr int kMenuStartY = 48;
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
// Compile-time toggles for APU channel mix
#ifndef GB_BUZZER_ENABLE_CH1
#define GB_BUZZER_ENABLE_CH1 1
#endif
#ifndef GB_BUZZER_ENABLE_CH2
#define GB_BUZZER_ENABLE_CH2 1
#endif
#ifndef GB_BUZZER_ENABLE_CH3
#define GB_BUZZER_ENABLE_CH3 1
#endif
#ifndef GB_BUZZER_ENABLE_CH4
#define GB_BUZZER_ENABLE_CH4 1
#endif
class GameboyApp;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
@@ -174,10 +290,15 @@ public:
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
void onStart() override {
::gAudioCtx = this;
::gAudioReadThunk = &GameboyApp::audioReadThunk;
::gAudioWriteThunk = &GameboyApp::audioWriteThunk;
apu.attach(this);
apu.reset();
cancelTick();
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.resetAll();)
prevInput = {};
prevInput = context.input.readState();
statusMessage.clear();
resetFpsStats();
scaleMode = ScaleMode::FullHeightWide;
@@ -193,6 +314,13 @@ public:
frameDelayCarryUs = 0;
GB_PERF_ONLY(perf.maybePrintAggregate(true);)
unloadRom();
apu.reset();
apu.attach(nullptr);
if (::gAudioCtx == this) {
::gAudioCtx = nullptr;
::gAudioReadThunk = nullptr;
::gAudioWriteThunk = nullptr;
}
}
void handleEvent(const AppEvent& event) override {
@@ -250,6 +378,8 @@ public:
gb_run_frame(&gb);
GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;)
apu.generateFrameAudio();
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderGameFrame();
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
@@ -266,7 +396,224 @@ public:
GB_PERF_ONLY(perf.maybePrintAggregate();)
}
[[nodiscard]] uint8_t audioReadRegister(uint16_t addr) const { return apu.read(addr); }
void audioWriteRegister(uint16_t addr, uint8_t value) { apu.write(addr, value); }
private:
class SimpleApu {
public:
SimpleApu() {}
void attach(GameboyApp* ownerInstance) { owner = ownerInstance; }
void reset() {
stopAudio();
minigb_apu_audio_init(&ctx);
frameCursor = kFramesPerBuffer;
lastDensity = 0;
}
[[nodiscard]] uint8_t read(uint16_t addr) const {
return minigb_apu_audio_read(const_cast<minigb_apu_ctx*>(&ctx), addr);
}
void write(uint16_t addr, uint8_t value) { minigb_apu_audio_write(&ctx, addr, value); }
void generateFrameAudio() {
if (!kEnableGbPwmPrototype)
return;
minigb_apu_audio_callback(&ctx, playbackBuf.data());
// pendingReady = true;
}
void startAudio() {
if (!kEnableGbPwmPrototype)
return;
if (audioRunning)
return;
stopAudio();
{
// std::lock_guard<std::mutex> lock(bufferMutex);
pendingReady = false;
frameCursor = kFramesPerBuffer;
}
generateFrameAudio();
frameCursor = 0;
sdm_config_t config{};
config.gpio_num = static_cast<int>(kGbPwmPrototypePin);
config.clk_src = SDM_CLK_SRC_DEFAULT;
config.sample_rate_hz = kGbPwmCarrierHz;
esp_err_t err = sdm_new_channel(&config, &sdmChannel);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "sdm_new_channel failed (%s)", esp_err_to_name(err));
sdmChannel = nullptr;
return;
}
err = sdm_channel_enable(sdmChannel);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "sdm_channel_enable failed (%s)", esp_err_to_name(err));
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
auto cleanupTimer = [&]() {
if (!audioTimer)
return;
gptimer_stop(audioTimer);
gptimer_disable(audioTimer);
gptimer_del_timer(audioTimer);
audioTimer = nullptr;
};
gptimer_config_t timerConfig{};
timerConfig.clk_src = GPTIMER_CLK_SRC_DEFAULT;
timerConfig.direction = GPTIMER_COUNT_UP;
timerConfig.resolution_hz = kTimerResolutionHz;
err = gptimer_new_timer(&timerConfig, &audioTimer);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "gptimer_new_timer failed (%s)", esp_err_to_name(err));
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
gptimer_event_callbacks_t callbacks{};
callbacks.on_alarm = &SimpleApu::audioTimerThunk;
err = gptimer_register_event_callbacks(audioTimer, &callbacks, this);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "gptimer_register_event_callbacks failed (%s)", esp_err_to_name(err));
cleanupTimer();
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
const uint32_t ticksPerSample = std::max<uint32_t>(1, timerConfig.resolution_hz / kGbPwmSampleRateHz);
gptimer_alarm_config_t alarmConfig{};
alarmConfig.reload_count = 0;
alarmConfig.alarm_count = ticksPerSample;
alarmConfig.flags.auto_reload_on_alarm = true;
err = gptimer_set_alarm_action(audioTimer, &alarmConfig);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "gptimer_set_alarm_action failed (%s)", esp_err_to_name(err));
cleanupTimer();
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
err = gptimer_enable(audioTimer);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "gptimer_enable failed (%s)", esp_err_to_name(err));
cleanupTimer();
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
err = gptimer_start(audioTimer);
if (err != ESP_OK) {
ESP_LOGE(kAudioLogTag, "gptimer_start failed (%s)", esp_err_to_name(err));
cleanupTimer();
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
return;
}
audioRunning = true;
lastDensity = 0;
sdm_channel_set_pulse_density(sdmChannel, 0);
ESP_LOGI(kAudioLogTag, "sigma-delta audio active on GPIO%d (%u Hz sample, %u Hz carrier)",
static_cast<int>(kGbPwmPrototypePin), kGbPwmSampleRateHz, kGbPwmCarrierHz);
}
void stopAudio() {
if (audioTimer) {
gptimer_stop(audioTimer);
gptimer_disable(audioTimer);
gptimer_del_timer(audioTimer);
audioTimer = nullptr;
}
if (sdmChannel) {
sdm_channel_set_pulse_density(sdmChannel, 0);
sdm_channel_disable(sdmChannel);
sdm_del_channel(sdmChannel);
sdmChannel = nullptr;
}
audioRunning = false;
frameCursor = kFramesPerBuffer;
lastDensity = 0;
}
bool computeEffectiveTone(uint32_t& outFreqHz, uint8_t& outLoudness) const {
(void) outFreqHz;
(void) outLoudness;
return false;
}
private:
static constexpr const char* kAudioLogTag = "gb_sdm";
static constexpr uint16_t kBaseAddr = 0xFF10;
static constexpr uint16_t kEndAddr = 0xFF3F;
static constexpr std::size_t kFramesPerBuffer = AUDIO_SAMPLES;
static constexpr std::size_t kSamplesPerBuffer = AUDIO_SAMPLES_TOTAL;
static constexpr uint32_t kTimerResolutionHz = 1'000'000;
static constexpr bool inRange(uint16_t addr) { return addr >= kBaseAddr && addr <= kEndAddr; }
static IRAM_ATTR bool audioTimerThunk(gptimer_handle_t, const gptimer_alarm_event_data_t*, void* userData) {
auto* self = static_cast<SimpleApu*>(userData);
if (self)
self->handleAudioTick();
return true;
}
IRAM_ATTR void handleAudioTick() {
if (!audioRunning || !sdmChannel)
return;
if (frameCursor >= kFramesPerBuffer) {
frameCursor = 0;
}
const std::size_t offset = frameCursor * 2;
++frameCursor;
const audio_sample_t sample = playbackBuf[offset];
int32_t density = sample >> 7;
if (density > 127)
density = 127;
else if (density < -128)
density = -128;
sdm_group_t *group = sdmChannel->group;
int chan_id = sdmChannel->chan_id;
sdm_ll_set_pulse_density(group->hal.dev, chan_id, density);
}
GameboyApp* owner = nullptr;
minigb_apu_ctx ctx{};
std::array<audio_sample_t, kSamplesPerBuffer> playbackBuf;
bool pendingReady = false;
std::size_t frameCursor = kFramesPerBuffer;
sdm_channel_handle_t sdmChannel = nullptr;
gptimer_handle_t audioTimer = nullptr;
bool audioRunning = false;
int8_t lastDensity = 0;
};
enum class Mode { Browse, Running };
enum class ScaleMode { Original, FullHeight, FullHeightWide };
@@ -575,6 +922,11 @@ private:
uint32_t fpsCurrent = 0;
std::string activeRomName;
std::string activeRomSavePath;
SimpleApu apu{};
// Smoothing state for buzzer tone
uint32_t lastFreqHz = 0;
uint8_t lastLoud = 0;
uint32_t stableFrames = 0;
void cancelTick() {
if (tickTimer != kInvalidAppTimer) {
@@ -760,6 +1112,11 @@ private:
if (roms.empty())
return;
if (input.b && !prevInput.b) {
context.requestAppSwitchByName(apps::kMenuAppName);
return;
}
const auto wrapDecrement = [&]() {
if (selectedIndex == 0)
selectedIndex = roms.size() - 1;
@@ -822,7 +1179,7 @@ private:
}
font16x8::drawText(framebuffer, 16, framebuffer.height() - 52, "A/START PLAY", 1, true, 1);
font16x8::drawText(framebuffer, 16, framebuffer.height() - 32, "SELECT RESCAN", 1, true, 1);
font16x8::drawText(framebuffer, 16, framebuffer.height() - 32, "B BACK SELECT RESCAN", 1, true, 1);
}
if (!statusMessage.empty()) {
@@ -891,6 +1248,7 @@ private:
return false;
}
apu.reset();
std::memset(&gb, 0, sizeof(gb));
const auto initResult = gb_init(&gb, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
&GameboyApp::errorCallback, this, romDataView);
@@ -927,6 +1285,8 @@ private:
frameDirty = true;
activeRomName = rom.name.empty() ? "Game" : rom.name;
apu.startAudio();
std::string statusText = "Running " + activeRomName;
if (!fsReady)
statusText.append(" (no save)");
@@ -943,12 +1303,15 @@ private:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
apu.stopAudio();
return;
}
maybeSaveRam();
resetFpsStats();
apu.stopAudio();
gbReady = false;
romData.clear();
romDataView = nullptr;
@@ -957,6 +1320,7 @@ private:
activeRomName.clear();
activeRomSavePath.clear();
std::memset(&gb, 0, sizeof(gb));
apu.reset();
mode = Mode::Browse;
browserDirty = true;
}
@@ -1165,6 +1529,15 @@ private:
setStatus("Partial save loaded");
}
}
static uint8_t audioReadThunk(void* ctx, uint16_t addr) {
auto* self = static_cast<GameboyApp*>(ctx);
return self ? self->audioReadRegister(addr) : 0xFF;
}
static void audioWriteThunk(void* ctx, uint16_t addr, uint8_t value) {
if (auto* self = static_cast<GameboyApp*>(ctx))
self->audioWriteRegister(addr, value);
}
void setStatus(std::string message) {
statusMessage = std::move(message);

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

@@ -64,8 +64,8 @@ public:
moved = true;
}
const bool togglePressed = (current.a && !previous.a) || (current.select && !previous.select) ||
(current.start && !previous.start);
const bool togglePressed = (current.a && !previous.a) || (current.start && !previous.start) ||
(current.select && !previous.select);
if (togglePressed)
handleToggle();

View File

@@ -185,8 +185,6 @@ private:
state.paused = false;
dirty = true;
scheduleDropTimer();
if (auto* power = context.powerManager())
power->setSlowMode(false);
}
void handleButtons(const AppButtonEvent& evt) {
@@ -204,8 +202,6 @@ private:
reset();
} else {
state.paused = !state.paused;
if (auto* power = context.powerManager())
power->setSlowMode(state.paused);
}
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();

View File

@@ -16,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,11 @@
#pragma once
namespace cardboy::sdk {
class ILoopHooks {
public:
virtual ~ILoopHooks() = default;
virtual void onLoopIteration() = 0;
};
} // namespace cardboy::sdk

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

@@ -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>
@@ -51,6 +52,7 @@ private:
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
void notifyEventBus(EventBusSignal signal);
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
@@ -61,6 +63,7 @@ private:
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

@@ -13,7 +13,9 @@ 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) {
@@ -71,6 +73,7 @@ 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();
@@ -87,6 +90,9 @@ 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);
@@ -94,7 +100,11 @@ void AppSystem::run() {
const InputState inputNow = context.input.readState();
const bool consumedByStatusToggle = StatusBar::instance().handleToggleInput(inputNow, lastInputState);
if (!consumedByStatusToggle && inputsDiffer(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;
@@ -118,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);
}
}
}
@@ -158,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) {
@@ -220,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) {
@@ -251,4 +278,11 @@ bool AppSystem::handlePendingSwitchRequest() {
return switched;
}
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

@@ -63,12 +63,6 @@ std::string StatusBar::prepareRightText() const {
right.assign(buf);
}
if (services_->powerManager && services_->powerManager->isSlowMode()) {
if (!right.empty())
right.append(" ");
right.append("SLOW");
}
if (services_->buzzer && services_->buzzer->isMuted()) {
if (!right.empty())
right.append(" ");