mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
Compare commits
12 Commits
a6713859b2
...
e37f8e3dc8
| Author | SHA1 | Date | |
|---|---|---|---|
| e37f8e3dc8 | |||
| df7c4ff3b9 | |||
| 07186b4b73 | |||
| 6a1f7d48ce | |||
| 031ff1952b | |||
| df55b8f2e1 | |||
| ed1cee82d2 | |||
| 7474f65aaa | |||
| 088c6e47bd | |||
| d91b7540fc | |||
| d5506b9455 | |||
| db88e16aaa |
@@ -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
|
||||
|
||||
@@ -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,
|
||||
@@ -27,6 +29,7 @@ public:
|
||||
uint8_t get_pressed();
|
||||
void install_isr();
|
||||
void register_listener(TaskHandle_t task);
|
||||
void setEventBus(cardboy::sdk::IEventBus* bus);
|
||||
|
||||
TaskHandle_t _pooler_task;
|
||||
|
||||
@@ -35,6 +38,7 @@ private:
|
||||
|
||||
volatile uint8_t _current;
|
||||
volatile TaskHandle_t _listener = nullptr;
|
||||
cardboy::sdk::IEventBus* _eventBus = nullptr;
|
||||
};
|
||||
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
|
||||
@@ -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, ®, 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; }
|
||||
|
||||
@@ -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
|
||||
|
||||
81
Firmware/components/backend-esp/src/event_bus.cpp
Normal file
81
Firmware/components/backend-esp/src/event_bus.cpp
Normal 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
|
||||
@@ -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);
|
||||
}
|
||||
@@ -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"
|
||||
@@ -15,6 +42,7 @@
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cmath>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -106,6 +134,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 +218,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 +242,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 {
|
||||
@@ -266,7 +322,462 @@ 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:
|
||||
public:
|
||||
class SimpleApu {
|
||||
public:
|
||||
void attach(GameboyApp* ownerInstance) { owner = ownerInstance; }
|
||||
|
||||
void reset() {
|
||||
regs.fill(0);
|
||||
for (std::size_t i = 0; i < kInitialRegistersCount; ++i)
|
||||
regs[i] = kInitialRegisters[i];
|
||||
for (std::size_t i = 0; i < kInitialWaveCount; ++i)
|
||||
regs[kWaveOffset + i] = kInitialWave[i];
|
||||
regs[kPowerIndex] = 0x80;
|
||||
enabled = true;
|
||||
squareAlternate = 0;
|
||||
lastChannel = 0xFF;
|
||||
filteredFreqHz = 0.0;
|
||||
lastSquareRaw = {0, 0};
|
||||
squareStable = {0, 0};
|
||||
}
|
||||
|
||||
[[nodiscard]] uint8_t read(uint16_t addr) const {
|
||||
if (!inRange(addr))
|
||||
return 0xFF;
|
||||
const std::size_t idx = static_cast<std::size_t>(addr - kBaseAddr);
|
||||
return static_cast<uint8_t>(regs[idx] | kReadMask[idx]);
|
||||
}
|
||||
|
||||
void write(uint16_t addr, uint8_t value) {
|
||||
if (!inRange(addr))
|
||||
return;
|
||||
const std::size_t idx = static_cast<std::size_t>(addr - kBaseAddr);
|
||||
|
||||
if (addr == kPowerAddr) {
|
||||
enabled = (value & 0x80U) != 0;
|
||||
regs[idx] = static_cast<uint8_t>(value & 0x80U);
|
||||
if (!enabled) {
|
||||
std::array<uint8_t, kWaveRamSize> wave{};
|
||||
for (std::size_t i = 0; i < kWaveRamSize; ++i)
|
||||
wave[i] = regs[kWaveOffset + i];
|
||||
regs.fill(0);
|
||||
for (std::size_t i = 0; i < kWaveRamSize; ++i)
|
||||
regs[kWaveOffset + i] = wave[i];
|
||||
regs[kPowerIndex] = static_cast<uint8_t>(value & 0x80U);
|
||||
squareAlternate = 0;
|
||||
lastChannel = 0xFF;
|
||||
filteredFreqHz = 0.0;
|
||||
lastSquareRaw = {0, 0};
|
||||
squareStable = {0, 0};
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (!enabled) {
|
||||
if (addr >= kWaveBase && addr <= kWaveEnd)
|
||||
regs[idx] = value;
|
||||
return;
|
||||
}
|
||||
|
||||
regs[idx] = value;
|
||||
|
||||
if ((addr == kCh1TriggerAddr && (value & 0x80U)) || (addr == kCh2TriggerAddr && (value & 0x80U))) {
|
||||
const int channelIndex = (addr == kCh1TriggerAddr) ? 0 : 1;
|
||||
// Reflect channel enable in NR52 status bits (no immediate beep; handled in per-frame mixer)
|
||||
regs[kPowerIndex] = static_cast<uint8_t>((regs[kPowerIndex] & 0xF0U) | 0x80U | (1U << channelIndex));
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
static constexpr uint16_t kBaseAddr = 0xFF10;
|
||||
static constexpr std::size_t kRegisterCount = 0x30;
|
||||
static constexpr uint16_t kPowerAddr = 0xFF26;
|
||||
static constexpr std::size_t kPowerIndex = static_cast<std::size_t>(kPowerAddr - kBaseAddr);
|
||||
static constexpr uint16_t kCh1LenAddr = 0xFF11;
|
||||
static constexpr uint16_t kCh1EnvAddr = 0xFF12;
|
||||
static constexpr uint16_t kCh1FreqLoAddr = 0xFF13;
|
||||
static constexpr uint16_t kCh1TriggerAddr = 0xFF14;
|
||||
static constexpr uint16_t kCh2LenAddr = 0xFF16;
|
||||
static constexpr uint16_t kCh2EnvAddr = 0xFF17;
|
||||
static constexpr uint16_t kCh2FreqLoAddr = 0xFF18;
|
||||
static constexpr uint16_t kCh2TriggerAddr = 0xFF19;
|
||||
static constexpr uint16_t kCh3EnableAddr = 0xFF1A;
|
||||
static constexpr uint16_t kCh3LevelAddr = 0xFF1C;
|
||||
static constexpr uint16_t kCh3FreqLoAddr = 0xFF1D;
|
||||
static constexpr uint16_t kCh3TriggerAddr = 0xFF1E;
|
||||
static constexpr uint16_t kCh4EnvAddr = 0xFF21;
|
||||
static constexpr uint16_t kCh4PolyAddr = 0xFF22;
|
||||
static constexpr uint16_t kCh4TriggerAddr = 0xFF23;
|
||||
static constexpr uint16_t kVolumeAddr = 0xFF24;
|
||||
static constexpr uint16_t kRoutingAddr = 0xFF25;
|
||||
static constexpr uint16_t kWaveBase = 0xFF30;
|
||||
static constexpr uint16_t kWaveEnd = 0xFF3F;
|
||||
static constexpr std::size_t kWaveOffset = static_cast<std::size_t>(kWaveBase - kBaseAddr);
|
||||
static constexpr std::size_t kWaveRamSize = static_cast<std::size_t>(kWaveEnd - kWaveBase + 1);
|
||||
static constexpr uint8_t kReadMask[kRegisterCount] = {
|
||||
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, 0x00,
|
||||
0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00};
|
||||
static constexpr std::size_t kInitialRegistersCount = 0x17;
|
||||
static constexpr uint8_t kInitialRegisters[kInitialRegistersCount] = {
|
||||
0x80, 0xBF, 0xF3, 0xFF, 0x3F, 0xFF, 0x3F, 0x00, 0xFF, 0x3F, 0x7F, 0xFF,
|
||||
0x9F, 0xFF, 0x3F, 0xFF, 0xFF, 0x00, 0x00, 0x3F, 0x77, 0xF3, 0xF1};
|
||||
static constexpr std::size_t kInitialWaveCount = 16;
|
||||
static constexpr uint8_t kInitialWave[kInitialWaveCount] = {0xAC, 0xDD, 0xDA, 0x48, 0x36, 0x02, 0xCF, 0x16,
|
||||
0x2C, 0x04, 0xE5, 0x2C, 0xAC, 0xDD, 0xDA, 0x48};
|
||||
|
||||
enum class Channel : uint8_t { Square1 = 0, Square2 = 1, Wave = 2, Noise = 3 };
|
||||
|
||||
GameboyApp* owner = nullptr;
|
||||
std::array<uint8_t, kRegisterCount> regs{};
|
||||
bool enabled = true;
|
||||
mutable uint8_t squareAlternate = 0;
|
||||
mutable uint8_t lastChannel = 0xFF;
|
||||
mutable double filteredFreqHz = 0.0;
|
||||
mutable std::array<uint16_t, 2> lastSquareRaw{};
|
||||
mutable std::array<uint8_t, 2> squareStable{};
|
||||
|
||||
static constexpr bool inRange(uint16_t addr) {
|
||||
return addr >= kBaseAddr && addr <= (kBaseAddr + static_cast<uint16_t>(kRegisterCount) - 1);
|
||||
}
|
||||
|
||||
[[nodiscard]] uint8_t reg(uint16_t addr) const { return regs[static_cast<std::size_t>(addr - kBaseAddr)]; }
|
||||
|
||||
[[nodiscard]] double squareFrequency(int channelIndex, uint16_t* outRaw = nullptr) const {
|
||||
const uint16_t freqLoAddr = (channelIndex == 0) ? kCh1FreqLoAddr : kCh2FreqLoAddr;
|
||||
const uint16_t freqHiAddr = (channelIndex == 0) ? kCh1TriggerAddr : kCh2TriggerAddr;
|
||||
const uint16_t raw = static_cast<uint16_t>(((reg(freqHiAddr) & 0x07U) << 8) | reg(freqLoAddr));
|
||||
if (outRaw)
|
||||
*outRaw = raw;
|
||||
if (raw >= 2048U)
|
||||
return 0.0;
|
||||
const double denom = static_cast<double>(2048U - raw);
|
||||
if (denom <= 0.0)
|
||||
return 0.0;
|
||||
return 131072.0 / denom;
|
||||
}
|
||||
|
||||
[[nodiscard]] static double snapNoiseFrequency(double freq) {
|
||||
static constexpr double kNoisePreferredHz[] = {650.0, 820.0, 990.0, 1200.0, 1500.0,
|
||||
1850.0, 2200.0, 2600.0, 3100.0, 3600.0};
|
||||
if (!(freq > 0.0))
|
||||
return freq;
|
||||
double bestFreq = freq;
|
||||
double bestDiff = 1.0e9;
|
||||
for (double target: kNoisePreferredHz) {
|
||||
double diff = freq - target;
|
||||
if (diff < 0.0)
|
||||
diff = -diff;
|
||||
if (diff < bestDiff) {
|
||||
bestDiff = diff;
|
||||
bestFreq = target;
|
||||
}
|
||||
}
|
||||
if (bestDiff <= 500.0)
|
||||
return bestFreq;
|
||||
return freq;
|
||||
}
|
||||
|
||||
// Mixer: compute best single-tone approximation for the buzzer.
|
||||
// Returns true if a tone is suggested, with outFreqHz set.
|
||||
public:
|
||||
bool computeEffectiveTone(uint32_t& outFreqHz, uint8_t& outLoudness) const {
|
||||
const uint8_t nr50 = reg(kVolumeAddr);
|
||||
const uint8_t master = static_cast<uint8_t>(std::max(nr50 & 0x07U, (nr50 >> 4) & 0x07U));
|
||||
if (master == 0) {
|
||||
filteredFreqHz = 0.0;
|
||||
lastChannel = 0xFF;
|
||||
return false;
|
||||
}
|
||||
const uint8_t routing = reg(kRoutingAddr);
|
||||
|
||||
struct Candidate {
|
||||
double freq;
|
||||
uint8_t loud;
|
||||
int prio;
|
||||
Channel channel;
|
||||
};
|
||||
|
||||
Candidate candidates[4];
|
||||
std::size_t candidateCount = 0;
|
||||
constexpr std::size_t kMaxCandidates = sizeof(candidates) / sizeof(candidates[0]);
|
||||
|
||||
// Track how stable each square channel's raw frequency is so we can bias selection.
|
||||
auto updateSquareHistory = [&](int idx, uint16_t raw) {
|
||||
if (raw == 0 || raw >= 2048U) {
|
||||
squareStable[static_cast<std::size_t>(idx)] = 0;
|
||||
lastSquareRaw[static_cast<std::size_t>(idx)] = 0;
|
||||
return;
|
||||
}
|
||||
if (lastSquareRaw[static_cast<std::size_t>(idx)] == raw) {
|
||||
const uint8_t current = squareStable[static_cast<std::size_t>(idx)];
|
||||
if (current < 0xFD)
|
||||
squareStable[static_cast<std::size_t>(idx)] = static_cast<uint8_t>(current + 1);
|
||||
} else {
|
||||
lastSquareRaw[static_cast<std::size_t>(idx)] = raw;
|
||||
squareStable[static_cast<std::size_t>(idx)] = 0;
|
||||
}
|
||||
};
|
||||
|
||||
bool squareActive[2] = {false, false};
|
||||
|
||||
auto pushCandidate = [&](double freq, uint8_t loud, int prio, Channel channel) {
|
||||
if (candidateCount >= kMaxCandidates)
|
||||
return;
|
||||
if (!std::isfinite(freq) || freq <= 10.0 || loud == 0)
|
||||
return;
|
||||
candidates[candidateCount++] = Candidate{freq, loud, prio, channel};
|
||||
};
|
||||
|
||||
#if GB_BUZZER_ENABLE_CH1
|
||||
if ((reg(kPowerAddr) & 0x01U) != 0) {
|
||||
const uint8_t env = reg(kCh1EnvAddr);
|
||||
const uint8_t vol4 = (env >> 4) & 0x0FU;
|
||||
const bool routed = ((routing & 0x11U) != 0);
|
||||
if (vol4 && routed) {
|
||||
uint16_t raw = 0;
|
||||
const double freq = squareFrequency(0, &raw);
|
||||
const uint8_t loud = static_cast<uint8_t>(vol4 * master);
|
||||
if (freq > 0.0) {
|
||||
squareActive[0] = true;
|
||||
updateSquareHistory(0, raw);
|
||||
pushCandidate(freq, loud, 3, Channel::Square1);
|
||||
} else {
|
||||
squareStable[0] = 0;
|
||||
lastSquareRaw[0] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GB_BUZZER_ENABLE_CH2
|
||||
if ((reg(kPowerAddr) & 0x02U) != 0) {
|
||||
const uint8_t env = reg(kCh2EnvAddr);
|
||||
const uint8_t vol4 = (env >> 4) & 0x0FU;
|
||||
const bool routed = ((routing & 0x22U) != 0);
|
||||
if (vol4 && routed) {
|
||||
uint16_t raw = 0;
|
||||
const double freq = squareFrequency(1, &raw);
|
||||
const uint8_t loud = static_cast<uint8_t>(vol4 * master);
|
||||
if (freq > 0.0) {
|
||||
squareActive[1] = true;
|
||||
updateSquareHistory(1, raw);
|
||||
pushCandidate(freq, loud, 3, Channel::Square2);
|
||||
} else {
|
||||
squareStable[1] = 0;
|
||||
lastSquareRaw[1] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GB_BUZZER_ENABLE_CH3
|
||||
if ((reg(kPowerAddr) & 0x04U) != 0 && (reg(kCh3EnableAddr) & 0x80U) != 0) {
|
||||
const uint8_t levelSel = (reg(kCh3LevelAddr) >> 5) & 0x03U;
|
||||
const bool routed = ((routing & 0x44U) != 0);
|
||||
uint8_t loudBase = 0;
|
||||
if (levelSel == 1)
|
||||
loudBase = 16;
|
||||
else if (levelSel == 2)
|
||||
loudBase = 8;
|
||||
else if (levelSel == 3)
|
||||
loudBase = 4;
|
||||
if (levelSel != 0 && routed && loudBase != 0) {
|
||||
const uint16_t raw =
|
||||
static_cast<uint16_t>(((reg(kCh3TriggerAddr) & 0x07U) << 8) | reg(kCh3FreqLoAddr));
|
||||
if (raw < 2048U) {
|
||||
const double denom = static_cast<double>(2048U - raw);
|
||||
const double freq = 2097152.0 / denom;
|
||||
const uint8_t loud = static_cast<uint8_t>(loudBase * master);
|
||||
pushCandidate(freq, loud, 2, Channel::Wave);
|
||||
}
|
||||
}
|
||||
}
|
||||
#endif
|
||||
#if GB_BUZZER_ENABLE_CH4
|
||||
if ((reg(kPowerAddr) & 0x08U) != 0) {
|
||||
const bool routed = ((routing & 0x88U) != 0);
|
||||
const uint8_t env = reg(kCh4EnvAddr);
|
||||
const uint8_t vol4 = (env >> 4) & 0x0FU;
|
||||
if (vol4 && routed) {
|
||||
const uint8_t nr43 = reg(kCh4PolyAddr);
|
||||
const uint8_t shift = (nr43 >> 4) & 0x0FU;
|
||||
const uint8_t dividerId = nr43 & 0x07U;
|
||||
static const int divLut[8] = {8, 16, 32, 48, 64, 80, 96, 112};
|
||||
const int div = divLut[dividerId];
|
||||
double freq =
|
||||
4194304.0 / (static_cast<double>(div) * std::pow(2.0, static_cast<double>(shift + 1)));
|
||||
freq = snapNoiseFrequency(std::clamp(freq, 600.0, 3600.0));
|
||||
const uint8_t loud = static_cast<uint8_t>(vol4 * master);
|
||||
pushCandidate(freq, loud, 1, Channel::Noise);
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
if (candidateCount == 0) {
|
||||
lastChannel = 0xFF;
|
||||
filteredFreqHz = 0.0;
|
||||
return false;
|
||||
}
|
||||
|
||||
for (int idx = 0; idx < 2; ++idx) {
|
||||
if (!squareActive[idx]) {
|
||||
squareStable[static_cast<std::size_t>(idx)] = 0;
|
||||
lastSquareRaw[static_cast<std::size_t>(idx)] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
const Candidate* squareCandidates[2] = {nullptr, nullptr};
|
||||
const Candidate* waveCandidate = nullptr;
|
||||
bool waveBass = false;
|
||||
const Candidate* bestOther = nullptr;
|
||||
int bestOtherScore = -1;
|
||||
|
||||
for (std::size_t i = 0; i < candidateCount; ++i) {
|
||||
const Candidate* cand = &candidates[i];
|
||||
if (cand->channel == Channel::Square1)
|
||||
squareCandidates[0] = cand;
|
||||
else if (cand->channel == Channel::Square2)
|
||||
squareCandidates[1] = cand;
|
||||
else {
|
||||
if (cand->channel == Channel::Wave) {
|
||||
waveCandidate = cand;
|
||||
waveBass = (cand->freq > 0.0 && cand->freq < 220.0);
|
||||
}
|
||||
int score = static_cast<int>(cand->loud);
|
||||
if (waveBass) {
|
||||
if (cand->channel == Channel::Wave)
|
||||
score += 3;
|
||||
else if (cand->channel == Channel::Noise)
|
||||
score -= 1;
|
||||
}
|
||||
if (score < 0)
|
||||
score = 0;
|
||||
if (!bestOther || score > bestOtherScore ||
|
||||
(score == bestOtherScore && cand->prio > bestOther->prio) ||
|
||||
(score == bestOtherScore && cand->prio == bestOther->prio && cand->freq > bestOther->freq)) {
|
||||
bestOther = cand;
|
||||
bestOtherScore = score;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const Candidate* bestSquare = nullptr;
|
||||
if (squareCandidates[0] && squareCandidates[1]) {
|
||||
int loudDiff =
|
||||
static_cast<int>(squareCandidates[0]->loud) - static_cast<int>(squareCandidates[1]->loud);
|
||||
if (loudDiff < 0)
|
||||
loudDiff = -loudDiff;
|
||||
const int stable0 = static_cast<int>(squareStable[0]);
|
||||
const int stable1 = static_cast<int>(squareStable[1]);
|
||||
const int stableMargin = waveBass ? 0 : 2;
|
||||
if (stable0 > stable1 + stableMargin)
|
||||
bestSquare = squareCandidates[0];
|
||||
else if (stable1 > stable0 + stableMargin)
|
||||
bestSquare = squareCandidates[1];
|
||||
else if (loudDiff > 2) {
|
||||
bestSquare = (squareCandidates[0]->loud > squareCandidates[1]->loud) ? squareCandidates[0]
|
||||
: squareCandidates[1];
|
||||
} else {
|
||||
if (waveBass && stable0 != stable1)
|
||||
bestSquare = (stable0 >= stable1) ? squareCandidates[0] : squareCandidates[1];
|
||||
if (!bestSquare && stable0 <= 1 && stable1 <= 1) {
|
||||
if (lastChannel == static_cast<uint8_t>(Channel::Square1))
|
||||
bestSquare = squareCandidates[0];
|
||||
else if (lastChannel == static_cast<uint8_t>(Channel::Square2))
|
||||
bestSquare = squareCandidates[1];
|
||||
}
|
||||
if (!bestSquare) {
|
||||
const Candidate* preferred =
|
||||
(squareAlternate & 1U) ? squareCandidates[1] : squareCandidates[0];
|
||||
bestSquare = preferred;
|
||||
squareAlternate ^= 1U;
|
||||
}
|
||||
}
|
||||
} else if (squareCandidates[0] || squareCandidates[1]) {
|
||||
bestSquare = squareCandidates[0] ? squareCandidates[0] : squareCandidates[1];
|
||||
}
|
||||
|
||||
const Candidate* best = bestSquare;
|
||||
if (!best)
|
||||
best = bestOther;
|
||||
else if (bestOther) {
|
||||
int bestScore = static_cast<int>(best->loud);
|
||||
int otherScore = static_cast<int>(bestOther->loud);
|
||||
if (waveBass) {
|
||||
if (best->channel == Channel::Wave)
|
||||
bestScore += 3;
|
||||
else if (best->channel == Channel::Noise)
|
||||
bestScore -= 1;
|
||||
else if (best->channel == Channel::Square1 || best->channel == Channel::Square2)
|
||||
bestScore -= 2;
|
||||
if (bestOther->channel == Channel::Wave)
|
||||
otherScore += 3;
|
||||
else if (bestOther->channel == Channel::Noise)
|
||||
otherScore -= 1;
|
||||
else if (bestOther->channel == Channel::Square1 || bestOther->channel == Channel::Square2)
|
||||
otherScore -= 2;
|
||||
}
|
||||
if (bestScore < 0)
|
||||
bestScore = 0;
|
||||
if (otherScore < 0)
|
||||
otherScore = 0;
|
||||
if (otherScore > bestScore || (otherScore == bestScore && bestOther->prio > best->prio) ||
|
||||
(otherScore == bestScore && bestOther->prio == best->prio &&
|
||||
static_cast<uint8_t>(bestOther->channel) == lastChannel &&
|
||||
static_cast<uint8_t>(best->channel) != lastChannel))
|
||||
best = bestOther;
|
||||
}
|
||||
|
||||
if (waveBass && waveCandidate && best && best->channel != Channel::Wave) {
|
||||
int waveScore = static_cast<int>(waveCandidate->loud) + 3;
|
||||
int bestScore = static_cast<int>(best->loud);
|
||||
if (best->channel == Channel::Noise)
|
||||
bestScore -= 1;
|
||||
else if (best->channel == Channel::Square1 || best->channel == Channel::Square2)
|
||||
bestScore -= 2;
|
||||
if (waveScore < 0)
|
||||
waveScore = 0;
|
||||
if (bestScore < 0)
|
||||
bestScore = 0;
|
||||
if (waveScore >= bestScore)
|
||||
best = waveCandidate;
|
||||
}
|
||||
|
||||
if (!best)
|
||||
return false;
|
||||
|
||||
double selectedFreq = best->freq;
|
||||
if (!(selectedFreq > 0.0) || !std::isfinite(selectedFreq))
|
||||
return false;
|
||||
|
||||
const double prevFiltered = filteredFreqHz;
|
||||
if (!(prevFiltered > 0.0) || !std::isfinite(prevFiltered) ||
|
||||
static_cast<uint8_t>(best->channel) != lastChannel) {
|
||||
filteredFreqHz = selectedFreq;
|
||||
} else {
|
||||
double diff = selectedFreq - prevFiltered;
|
||||
if (diff < 0.0 && -diff > 1200.0)
|
||||
filteredFreqHz = selectedFreq;
|
||||
else if (diff > 1200.0)
|
||||
filteredFreqHz = selectedFreq;
|
||||
else {
|
||||
double alpha = (best->channel == Channel::Noise) ? 0.45 : 0.35;
|
||||
filteredFreqHz = prevFiltered + (diff * alpha);
|
||||
}
|
||||
}
|
||||
|
||||
const double clamped = std::clamp(filteredFreqHz, 40.0, 5500.0);
|
||||
outFreqHz = static_cast<uint32_t>(clamped + 0.5);
|
||||
outLoudness = best->loud;
|
||||
lastChannel = static_cast<uint8_t>(best->channel);
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
||||
enum class Mode { Browse, Running };
|
||||
enum class ScaleMode { Original, FullHeight, FullHeightWide };
|
||||
|
||||
@@ -575,6 +1086,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 +1276,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 +1343,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 +1412,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);
|
||||
@@ -957,6 +1479,7 @@ private:
|
||||
activeRomName.clear();
|
||||
activeRomSavePath.clear();
|
||||
std::memset(&gb, 0, sizeof(gb));
|
||||
apu.reset();
|
||||
mode = Mode::Browse;
|
||||
browserDirty = true;
|
||||
}
|
||||
@@ -1166,6 +1689,23 @@ private:
|
||||
}
|
||||
}
|
||||
|
||||
void playTone(uint32_t freqHz, uint32_t durationMs, uint32_t gapMs) {
|
||||
if (freqHz == 0 || durationMs == 0)
|
||||
return;
|
||||
if (auto* buzzer = context.buzzer())
|
||||
buzzer->tone(freqHz, durationMs, gapMs);
|
||||
}
|
||||
|
||||
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);
|
||||
browserDirty = true;
|
||||
@@ -1387,6 +1927,35 @@ private:
|
||||
drawLineOriginal(*self, pixels, static_cast<int>(line));
|
||||
break;
|
||||
}
|
||||
|
||||
// Simple per-scanline hook: at end of last line, decide tone for the frame.
|
||||
if (line + 1 == LCD_HEIGHT) {
|
||||
uint32_t freqHz = 0;
|
||||
uint8_t loud = 0;
|
||||
if (self->apu.computeEffectiveTone(freqHz, loud)) {
|
||||
// Basic smoothing: if freq didn't change much, keep it; otherwise snap quickly
|
||||
const uint32_t prev = self->lastFreqHz;
|
||||
if (prev != 0 && freqHz != 0) {
|
||||
const uint32_t diff = (prev > freqHz) ? (prev - freqHz) : (freqHz - prev);
|
||||
if (diff < 15) {
|
||||
freqHz = prev; // minor jitter suppression
|
||||
++self->stableFrames;
|
||||
} else {
|
||||
self->stableFrames = 0;
|
||||
}
|
||||
} else {
|
||||
self->stableFrames = 0;
|
||||
}
|
||||
self->lastFreqHz = freqHz;
|
||||
self->lastLoud = loud;
|
||||
const uint32_t durMs = 17;
|
||||
self->playTone(freqHz, durMs, 0);
|
||||
} else {
|
||||
self->lastFreqHz = 0;
|
||||
self->lastLoud = 0;
|
||||
// Don't enqueue anything; queue naturally drains and buzzer stops
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static const char* initErrorToString(enum gb_init_error_e err) {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
@@ -0,0 +1,11 @@
|
||||
#pragma once
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class ILoopHooks {
|
||||
public:
|
||||
virtual ~ILoopHooks() = default;
|
||||
virtual void onLoopIteration() = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
@@ -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
|
||||
|
||||
@@ -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{};
|
||||
};
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
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
|
||||
|
||||
@@ -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(" ");
|
||||
|
||||
Reference in New Issue
Block a user