mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
6 Commits
5b75ff28e0
...
f721ebcb4c
| Author | SHA1 | Date | |
|---|---|---|---|
| f721ebcb4c | |||
| e9a05259c5 | |||
| 23400d817b | |||
| fa2715a60a | |||
| 899bfeef41 | |||
| 535b0078e5 |
@@ -2,5 +2,7 @@
|
||||
# CMakeLists in this exact order for cmake to work correctly
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_compile_options(-Ofast)
|
||||
|
||||
include($ENV{IDF_PATH}/tools/cmake/project.cmake)
|
||||
project(hello_world)
|
||||
|
||||
35
Firmware/components/backend-esp/CMakeLists.txt
Normal file
35
Firmware/components/backend-esp/CMakeLists.txt
Normal file
@@ -0,0 +1,35 @@
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/bat_mon.cpp"
|
||||
"src/buttons.cpp"
|
||||
"src/buzzer.cpp"
|
||||
"src/esp_backend.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
|
||||
"include"
|
||||
PRIV_REQUIRES
|
||||
driver
|
||||
esp_timer
|
||||
esp_driver_i2c
|
||||
esp_driver_spi
|
||||
littlefs
|
||||
nvs_flash
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/backend_interface" backend_interface_from_backend_esp)
|
||||
|
||||
add_library(cardboy_backend_esp INTERFACE)
|
||||
target_link_libraries(cardboy_backend_esp
|
||||
INTERFACE
|
||||
${COMPONENT_LIB}
|
||||
)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB}
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
)
|
||||
@@ -0,0 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
using ActiveBackend = EspBackend;
|
||||
}
|
||||
|
||||
@@ -5,10 +5,12 @@
|
||||
#ifndef CB_BAT_MON_HPP
|
||||
#define CB_BAT_MON_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <cstdint>
|
||||
|
||||
class BatMon {
|
||||
public:
|
||||
@@ -7,6 +7,7 @@
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include <cstdint>
|
||||
|
||||
typedef enum {
|
||||
BTN_START = 1 << 1,
|
||||
@@ -5,13 +5,14 @@
|
||||
#ifndef CB_DISPLAY_HPP
|
||||
#define CB_DISPLAY_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
// (Async memcpy removed for debugging simplification)
|
||||
|
||||
#include <array>
|
||||
#include <bitset>
|
||||
#include <cassert>
|
||||
|
||||
|
||||
namespace SMD {
|
||||
@@ -24,15 +25,15 @@ extern uint8_t* dma_buf;
|
||||
void init();
|
||||
// Double-buffered asynchronous frame pipeline:
|
||||
// Usage pattern each frame:
|
||||
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// SMD::frame_ready(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
|
||||
// ... write pixels into dma_buf via set_pixel / surface ...
|
||||
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||
// // is asynchronously cleared so the alternate buffer is ready for the next frame
|
||||
void async_draw_start();
|
||||
void async_draw_wait();
|
||||
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight?
|
||||
// SMD::send_frame(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
|
||||
// // is optionally cleared so the alternate buffer is ready for the next frame
|
||||
void send_frame(bool clear_after_send = true);
|
||||
void frame_ready();
|
||||
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
|
||||
|
||||
static void set_pixel(int x, int y, bool value) {
|
||||
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
|
||||
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
|
||||
|
||||
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef CB_I2C_GLOBAL_HPP
|
||||
#define CB_I2C_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/i2c_master.h"
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
#define POWER_HELPER_HPP
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/event_groups.h"
|
||||
|
||||
class PowerHelper {
|
||||
public:
|
||||
@@ -5,7 +5,7 @@
|
||||
#ifndef CB_SPI_GLOBAL_HPP
|
||||
#define CB_SPI_GLOBAL_HPP
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include "driver/spi_master.h"
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
class EspRuntime;
|
||||
|
||||
class EspFramebuffer final : public cardboy::sdk::FramebufferFacade<EspFramebuffer> {
|
||||
public:
|
||||
EspFramebuffer() = default;
|
||||
|
||||
[[nodiscard]] int width_impl() const;
|
||||
[[nodiscard]] int height_impl() const;
|
||||
void drawPixel_impl(int x, int y, bool on);
|
||||
void clear_impl(bool on);
|
||||
void frameReady_impl();
|
||||
void sendFrame_impl(bool clearAfterSend);
|
||||
[[nodiscard]] bool frameInFlight_impl() const;
|
||||
};
|
||||
|
||||
class EspInput final : public cardboy::sdk::InputFacade<EspInput> {
|
||||
public:
|
||||
cardboy::sdk::InputState readState_impl();
|
||||
};
|
||||
|
||||
class EspClock final : public cardboy::sdk::ClockFacade<EspClock> {
|
||||
public:
|
||||
std::uint32_t millis_impl();
|
||||
void sleep_ms_impl(std::uint32_t ms);
|
||||
};
|
||||
|
||||
class EspRuntime {
|
||||
public:
|
||||
EspRuntime();
|
||||
~EspRuntime();
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry();
|
||||
|
||||
EspFramebuffer framebuffer;
|
||||
EspInput input;
|
||||
EspClock clock;
|
||||
|
||||
private:
|
||||
void initializeHardware();
|
||||
|
||||
class BuzzerService;
|
||||
class BatteryService;
|
||||
class StorageService;
|
||||
class RandomService;
|
||||
class HighResClockService;
|
||||
class PowerService;
|
||||
class FilesystemService;
|
||||
|
||||
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;
|
||||
|
||||
cardboy::sdk::Services services{};
|
||||
};
|
||||
|
||||
struct Backend {
|
||||
using Framebuffer = EspFramebuffer;
|
||||
using Input = EspInput;
|
||||
using Clock = EspClock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
|
||||
namespace cardboy::backend {
|
||||
using EspBackend = esp::Backend;
|
||||
} // namespace cardboy::backend
|
||||
@@ -2,15 +2,15 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "bat_mon.hpp"
|
||||
#include "cardboy/backend/esp/bat_mon.hpp"
|
||||
|
||||
#include <power_helper.hpp>
|
||||
#include "cardboy/backend/esp/power_helper.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
|
||||
@@ -2,18 +2,18 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "buttons.hpp"
|
||||
#include "cardboy/backend/esp/buttons.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_err.h>
|
||||
#include <power_helper.hpp>
|
||||
#include "cardboy/backend/esp/power_helper.hpp"
|
||||
#include <rom/ets_sys.h>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "config.hpp"
|
||||
#include "i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
|
||||
static i2c_master_dev_handle_t dev_handle;
|
||||
static inline i2c_device_config_t dev_cfg = {
|
||||
@@ -1,6 +1,6 @@
|
||||
// Buzzer implementation
|
||||
#include "buzzer.hpp"
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/buzzer.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
#include <driver/ledc.h>
|
||||
#include <esp_err.h>
|
||||
@@ -1,10 +1,9 @@
|
||||
// Double-buffered display implementation with async memcpy ---------------------------------
|
||||
|
||||
#include "display.hpp"
|
||||
#include "cardboy/backend/esp/display.hpp"
|
||||
#include <cassert>
|
||||
#include <cstring>
|
||||
#include <driver/gpio.h>
|
||||
#include "disp_tools.hpp"
|
||||
#include "driver/spi_master.h"
|
||||
#include "esp_async_memcpy.h"
|
||||
#include "esp_timer.h"
|
||||
@@ -27,6 +26,7 @@ static bool _vcom = false;
|
||||
static TaskHandle_t s_clearTaskHandle = nullptr;
|
||||
static SemaphoreHandle_t s_clearReqSem = nullptr;
|
||||
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
|
||||
static bool s_clearPending[2] = {true, true};
|
||||
|
||||
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
|
||||
// update the maximum data stream supported by underlying DMA engine
|
||||
@@ -58,8 +58,17 @@ static void clear_task(void*) {
|
||||
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
|
||||
int bufIdx = (int) r->user;
|
||||
xSemaphoreGive(_txSem);
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U,
|
||||
const bool shouldClear = s_clearPending[bufIdx];
|
||||
s_clearPending[bufIdx] = true;
|
||||
if (shouldClear) {
|
||||
constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4);
|
||||
static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway
|
||||
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize,
|
||||
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
|
||||
} else {
|
||||
if (!xSemaphoreGive(s_bufferSem[bufIdx]))
|
||||
assert(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,9 +110,9 @@ void SMD::init() {
|
||||
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
|
||||
}
|
||||
|
||||
bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
bool SMD::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
|
||||
|
||||
void SMD::async_draw_start() {
|
||||
void SMD::send_frame(bool clear_after_send) {
|
||||
assert(driver != nullptr);
|
||||
if (!xSemaphoreTake(_txSem, portMAX_DELAY))
|
||||
assert(false);
|
||||
@@ -116,6 +125,7 @@ void SMD::async_draw_start() {
|
||||
assert(false);
|
||||
|
||||
const int nextDrawIdx = sendIdx ^ 1;
|
||||
s_clearPending[sendIdx] = clear_after_send;
|
||||
|
||||
_vcom = !_vcom;
|
||||
_tx = {};
|
||||
@@ -129,7 +139,7 @@ void SMD::async_draw_start() {
|
||||
dma_buf = s_dma_buffers[nextDrawIdx];
|
||||
}
|
||||
|
||||
void SMD::async_draw_wait() {
|
||||
void SMD::frame_ready() {
|
||||
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
|
||||
// uint64_t waitedUs = 0;
|
||||
if (!uxSemaphoreGetCount(sem)) {
|
||||
240
Firmware/components/backend-esp/src/esp_backend.cpp
Normal file
240
Firmware/components/backend-esp/src/esp_backend.cpp
Normal file
@@ -0,0 +1,240 @@
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
|
||||
#include "cardboy/backend/esp/bat_mon.hpp"
|
||||
#include "cardboy/backend/esp/buttons.hpp"
|
||||
#include "cardboy/backend/esp/buzzer.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
#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"
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
#include "nvs.h"
|
||||
#include "nvs_flash.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
|
||||
namespace cardboy::backend::esp {
|
||||
|
||||
namespace {
|
||||
void ensureNvsInit() {
|
||||
static bool nvsReady = false;
|
||||
if (nvsReady)
|
||||
return;
|
||||
|
||||
esp_err_t err = nvs_flash_init();
|
||||
if (err == ESP_ERR_NVS_NO_FREE_PAGES || err == ESP_ERR_NVS_NEW_VERSION_FOUND) {
|
||||
ESP_ERROR_CHECK(nvs_flash_erase());
|
||||
err = nvs_flash_init();
|
||||
}
|
||||
ESP_ERROR_CHECK(err);
|
||||
nvsReady = true;
|
||||
}
|
||||
} // namespace
|
||||
|
||||
class EspRuntime::BuzzerService final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
||||
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
||||
}
|
||||
|
||||
void beepRotate() override { Buzzer::get().beepRotate(); }
|
||||
void beepMove() override { Buzzer::get().beepMove(); }
|
||||
void beepLock() override { Buzzer::get().beepLock(); }
|
||||
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
||||
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
||||
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
||||
|
||||
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
||||
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
||||
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
||||
};
|
||||
|
||||
class EspRuntime::BatteryService final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return true; }
|
||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||
};
|
||||
|
||||
class EspRuntime::StorageService final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
ensureNvsInit();
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
std::uint32_t value = 0;
|
||||
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
ensureNvsInit();
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
||||
return;
|
||||
nvs_set_u32(handle, keyStr.c_str(), value);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
};
|
||||
|
||||
class EspRuntime::RandomService final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspRuntime::HighResClockService final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspRuntime::PowerService final : public cardboy::sdk::IPowerManager {
|
||||
public:
|
||||
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
|
||||
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
|
||||
};
|
||||
|
||||
class EspRuntime::FilesystemService final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
||||
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
||||
[[nodiscard]] std::string basePath() const override {
|
||||
const char* path = FsHelper::get().basePath();
|
||||
return path ? std::string(path) : std::string{};
|
||||
}
|
||||
};
|
||||
|
||||
EspRuntime::EspRuntime() : framebuffer(), input(), clock() {
|
||||
initializeHardware();
|
||||
|
||||
buzzerService = std::make_unique<BuzzerService>();
|
||||
batteryService = std::make_unique<BatteryService>();
|
||||
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>();
|
||||
|
||||
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();
|
||||
}
|
||||
|
||||
EspRuntime::~EspRuntime() = default;
|
||||
|
||||
cardboy::sdk::Services& EspRuntime::serviceRegistry() { return services; }
|
||||
|
||||
void EspRuntime::initializeHardware() {
|
||||
static bool initialized = false;
|
||||
if (initialized)
|
||||
return;
|
||||
initialized = true;
|
||||
|
||||
ensureNvsInit();
|
||||
|
||||
PowerHelper::get();
|
||||
Shutdowner::get();
|
||||
Buttons::get();
|
||||
|
||||
esp_err_t isrErr = gpio_install_isr_service(0);
|
||||
if (isrErr != ESP_OK && isrErr != ESP_ERR_INVALID_STATE) {
|
||||
ESP_ERROR_CHECK(isrErr);
|
||||
}
|
||||
|
||||
Shutdowner::get().install_isr();
|
||||
PowerHelper::get().install_isr();
|
||||
Buttons::get().install_isr();
|
||||
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::init();
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
}
|
||||
|
||||
int EspFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int EspFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void EspFramebuffer::drawPixel_impl(int x, int y, bool on) {
|
||||
if (x < 0 || y < 0 || x >= width_impl() || y >= height_impl())
|
||||
return;
|
||||
SMD::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void EspFramebuffer::clear_impl(bool on) {
|
||||
for (int y = 0; y < height_impl(); ++y)
|
||||
for (int x = 0; x < width_impl(); ++x)
|
||||
SMD::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void EspFramebuffer::frameReady_impl() { SMD::frame_ready(); }
|
||||
|
||||
void EspFramebuffer::sendFrame_impl(bool clearAfterSend) { SMD::send_frame(clearAfterSend); }
|
||||
|
||||
bool EspFramebuffer::frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
|
||||
|
||||
cardboy::sdk::InputState EspInput::readState_impl() {
|
||||
cardboy::sdk::InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & BTN_UP)
|
||||
state.up = true;
|
||||
if (pressed & BTN_LEFT)
|
||||
state.left = true;
|
||||
if (pressed & BTN_RIGHT)
|
||||
state.right = true;
|
||||
if (pressed & BTN_DOWN)
|
||||
state.down = true;
|
||||
if (pressed & BTN_A)
|
||||
state.a = true;
|
||||
if (pressed & BTN_B)
|
||||
state.b = true;
|
||||
if (pressed & BTN_SELECT)
|
||||
state.select = true;
|
||||
if (pressed & BTN_START)
|
||||
state.start = true;
|
||||
return state;
|
||||
}
|
||||
|
||||
std::uint32_t EspClock::millis_impl() {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void EspClock::sleep_ms_impl(std::uint32_t ms) {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::esp
|
||||
@@ -1,4 +1,4 @@
|
||||
#include "fs_helper.hpp"
|
||||
#include "cardboy/backend/esp/fs_helper.hpp"
|
||||
|
||||
#include <esp_idf_version.h>
|
||||
#include <esp_littlefs.h>
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "i2c_global.hpp"
|
||||
#include "cardboy/backend/esp/i2c_global.hpp"
|
||||
|
||||
|
||||
I2cGlobal& I2cGlobal::get() {
|
||||
@@ -2,11 +2,7 @@
|
||||
// Created by Stepan Usatiuk on 03.03.2025.
|
||||
//
|
||||
|
||||
#include "power_helper.hpp"
|
||||
|
||||
#include <config.hpp>
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
#include "cardboy/backend/esp/power_helper.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
@@ -2,12 +2,12 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "shutdowner.hpp"
|
||||
#include "cardboy/backend/esp/shutdowner.hpp"
|
||||
|
||||
#include <driver/gpio.h>
|
||||
#include <esp_sleep.h>
|
||||
|
||||
#include "config.hpp"
|
||||
#include "cardboy/backend/esp/config.hpp"
|
||||
|
||||
Shutdowner& Shutdowner::get() {
|
||||
static Shutdowner instance;
|
||||
@@ -2,7 +2,7 @@
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "spi_global.hpp"
|
||||
#include "cardboy/backend/esp/spi_global.hpp"
|
||||
|
||||
SpiGlobal& SpiGlobal::get() {
|
||||
static SpiGlobal SpiGlobal;
|
||||
@@ -1,5 +1,31 @@
|
||||
idf_component_register()
|
||||
idf_component_register(
|
||||
INCLUDE_DIRS ""
|
||||
REQUIRES backend-esp
|
||||
)
|
||||
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
|
||||
set(CARDBOY_BUILD_SFML OFF CACHE BOOL "Disable desktop backend build" FORCE)
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY cardboy_backend_esp CACHE STRING "Cardboy backend implementation" FORCE)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk)
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/core" cb-sdk-build-core)
|
||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/apps" cb-sdk-build-apps)
|
||||
|
||||
target_compile_options(cardboy_backend_esp
|
||||
INTERFACE
|
||||
-fjump-tables
|
||||
-ftree-switch-conversion
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_backend_esp
|
||||
INTERFACE
|
||||
idf::driver
|
||||
idf::esp_timer
|
||||
idf::esp_driver_spi
|
||||
idf::freertos
|
||||
)
|
||||
|
||||
target_link_libraries(${COMPONENT_LIB}
|
||||
INTERFACE
|
||||
cardboy_backend_esp
|
||||
cardboy_sdk
|
||||
cardboy_apps
|
||||
)
|
||||
|
||||
@@ -1,23 +1,16 @@
|
||||
idf_component_register(SRCS
|
||||
src/app_main.cpp
|
||||
../sdk/apps/menu_app.cpp
|
||||
../sdk/apps/clock_app.cpp
|
||||
../sdk/apps/tetris_app.cpp
|
||||
../sdk/apps/gameboy_app.cpp
|
||||
src/display.cpp
|
||||
src/bat_mon.cpp
|
||||
src/spi_global.cpp
|
||||
src/i2c_global.cpp
|
||||
src/disp_tools.cpp
|
||||
src/disp_tty.cpp
|
||||
src/shutdowner.cpp
|
||||
src/buttons.cpp
|
||||
src/power_helper.cpp
|
||||
src/buzzer.cpp
|
||||
src/fs_helper.cpp
|
||||
../sdk/src/app_system.cpp
|
||||
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
|
||||
INCLUDE_DIRS "include" "../sdk/include"
|
||||
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
|
||||
idf_component_register(
|
||||
SRCS
|
||||
"src/app_main.cpp"
|
||||
PRIV_REQUIRES
|
||||
sdk-esp
|
||||
littlefs
|
||||
REQUIRES
|
||||
backend-esp
|
||||
EMBED_FILES
|
||||
"roms/builtin_demo1.gb"
|
||||
"roms/builtin_demo2.gb"
|
||||
INCLUDE_DIRS
|
||||
""
|
||||
)
|
||||
|
||||
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
|
||||
|
||||
@@ -1,28 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
|
||||
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||
|
||||
using AppEventType = cardboy::sdk::AppEventType;
|
||||
using AppButtonEvent = cardboy::sdk::AppButtonEvent;
|
||||
using AppTimerEvent = cardboy::sdk::AppTimerEvent;
|
||||
using AppEvent = cardboy::sdk::AppEvent;
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
|
||||
|
||||
using AppContext = cardboy::sdk::AppContext;
|
||||
|
||||
using IApp = cardboy::sdk::IApp;
|
||||
using IAppFactory = cardboy::sdk::IAppFactory;
|
||||
using Services = cardboy::sdk::Services;
|
||||
using IBuzzer = cardboy::sdk::IBuzzer;
|
||||
using IBatteryMonitor = cardboy::sdk::IBatteryMonitor;
|
||||
using IStorage = cardboy::sdk::IStorage;
|
||||
using IRandom = cardboy::sdk::IRandom;
|
||||
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||
@@ -1,73 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "config.hpp"
|
||||
|
||||
#include <buttons.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
#include <power_helper.hpp>
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
int width() const override { return cardboy::sdk::kDisplayWidth; }
|
||||
int height() const override { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void drawPixel(int x, int y, bool on) override {
|
||||
if (x < 0 || y < 0 || x >= width() || y >= height())
|
||||
return;
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void clear(bool on) override {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
DispTools::set_pixel(x, y, on);
|
||||
}
|
||||
|
||||
void beginFrame() override { DispTools::draw_to_display_async_wait(); }
|
||||
void endFrame() override { DispTools::draw_to_display_async_start(); }
|
||||
bool isFrameInFlight() const override { return DispTools::draw_to_display_async_busy(); }
|
||||
};
|
||||
|
||||
class PlatformInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
cardboy::sdk::InputState readState() override {
|
||||
cardboy::sdk::InputState state{};
|
||||
const uint8_t pressed = Buttons::get().get_pressed();
|
||||
if (pressed & BTN_UP)
|
||||
state.up = true;
|
||||
if (pressed & BTN_LEFT)
|
||||
state.left = true;
|
||||
if (pressed & BTN_RIGHT)
|
||||
state.right = true;
|
||||
if (pressed & BTN_DOWN)
|
||||
state.down = true;
|
||||
if (pressed & BTN_A)
|
||||
state.a = true;
|
||||
if (pressed & BTN_B)
|
||||
state.b = true;
|
||||
if (pressed & BTN_SELECT)
|
||||
state.select = true;
|
||||
if (pressed & BTN_START)
|
||||
state.start = true;
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
class PlatformClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
std::uint32_t millis() override {
|
||||
TickType_t ticks = xTaskGetTickCount();
|
||||
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
|
||||
}
|
||||
|
||||
void sleep_ms(std::uint32_t ms) override {
|
||||
if (ms == 0)
|
||||
return;
|
||||
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
|
||||
}
|
||||
};
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
using AppSystem = cardboy::sdk::AppSystem;
|
||||
@@ -1,4 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
@@ -1,3 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
@@ -1,53 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef CB_DISP_TOOLS_HPP
|
||||
#define CB_DISP_TOOLS_HPP
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
namespace DispTools {
|
||||
static void clear() {
|
||||
for (int y = 0; y < DISP_HEIGHT; y++) {
|
||||
for (int x = 0; x < DISP_WIDTH; x++) {
|
||||
SMD::set_pixel(x, y, true);
|
||||
}
|
||||
}
|
||||
}
|
||||
static bool get_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
assert(false); // Not implemented
|
||||
return true;
|
||||
// return disp_frame[y][x];
|
||||
}
|
||||
static void reset_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
SMD::set_pixel(x, y, false);
|
||||
}
|
||||
static void set_pixel(int x, int y) {
|
||||
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
|
||||
assert(false);
|
||||
|
||||
SMD::set_pixel(x, y, true);
|
||||
}
|
||||
static void set_pixel(int x, int y, bool on) {
|
||||
if (on) {
|
||||
set_pixel(x, y);
|
||||
} else {
|
||||
reset_pixel(x, y);
|
||||
}
|
||||
}
|
||||
// New simplified async pipeline wrappers
|
||||
static void async_frame_start() { SMD::async_draw_wait(); } // call at frame start
|
||||
static void async_frame_end() { SMD::async_draw_start(); } // call after rendering
|
||||
// Legacy names (temporary) mapped to new API in case of straggling calls
|
||||
static void draw_to_display_async_start() { SMD::async_draw_start(); }
|
||||
static void draw_to_display_async_wait() { SMD::async_draw_wait(); }
|
||||
static bool draw_to_display_async_busy() { return SMD::async_draw_busy(); }
|
||||
};
|
||||
|
||||
|
||||
#endif // DISP_TOOLS_HPP
|
||||
@@ -1,42 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#ifndef DISP_TTY_HPP
|
||||
#define DISP_TTY_HPP
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
|
||||
#include "config.hpp"
|
||||
|
||||
#include <format>
|
||||
|
||||
class FbTty {
|
||||
public:
|
||||
void putchar(char c);
|
||||
void putstr(const char* str);
|
||||
void reset();
|
||||
|
||||
template<typename... Args>
|
||||
auto fmt(std::format_string<Args...> fmt, Args&&... args) {
|
||||
auto str = std::format(fmt, std::forward<Args>(args)...);
|
||||
putstr(str.c_str());
|
||||
}
|
||||
private:
|
||||
void draw_char(int col, int row);
|
||||
|
||||
int _cur_col = 0;
|
||||
int _cur_row = 0;
|
||||
|
||||
static constexpr size_t _max_col = DISP_WIDTH / 8;
|
||||
static constexpr size_t _max_row = DISP_HEIGHT / 16;
|
||||
|
||||
std::array<std::array<char, _max_row>, _max_col> _buf = {};
|
||||
|
||||
void next_col();
|
||||
void next_row();
|
||||
};
|
||||
|
||||
|
||||
#endif // DISP_TTY_HPP
|
||||
@@ -1,5 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/input_state.hpp"
|
||||
|
||||
using InputState = cardboy::sdk::InputState;
|
||||
@@ -1,37 +1,15 @@
|
||||
// Cardboy firmware entry point: boot platform services and run the modular app system.
|
||||
|
||||
#include "app_system.hpp"
|
||||
|
||||
#include "app_framework.hpp"
|
||||
#include "app_platform.hpp"
|
||||
#include "apps/clock_app.hpp"
|
||||
#include "apps/gameboy_app.hpp"
|
||||
#include "apps/menu_app.hpp"
|
||||
#include "apps/tetris_app.hpp"
|
||||
#include "config.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <bat_mon.hpp>
|
||||
#include <buttons.hpp>
|
||||
#include <buzzer.hpp>
|
||||
#include <disp_tools.hpp>
|
||||
#include <display.hpp>
|
||||
#include <fs_helper.hpp>
|
||||
#include <i2c_global.hpp>
|
||||
#include <nvs.h>
|
||||
#include <nvs_flash.h>
|
||||
#include <power_helper.hpp>
|
||||
#include <shutdowner.hpp>
|
||||
#include <spi_global.hpp>
|
||||
|
||||
#include "cardboy/backend/esp_backend.hpp"
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include "freertos/FreeRTOS.h"
|
||||
#include "freertos/task.h"
|
||||
|
||||
#include "driver/gpio.h"
|
||||
#include "esp_err.h"
|
||||
#include "esp_random.h"
|
||||
#include "esp_timer.h"
|
||||
#include "esp_pm.h"
|
||||
#include "esp_sleep.h"
|
||||
#include "sdkconfig.h"
|
||||
@@ -43,89 +21,31 @@
|
||||
#include <cstring>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
class EspBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) override {
|
||||
Buzzer::get().tone(freq, duration_ms, gap_ms);
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_builtin_demo1_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo1_gb_end[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_end[];
|
||||
}
|
||||
|
||||
void beepRotate() override { Buzzer::get().beepRotate(); }
|
||||
void beepMove() override { Buzzer::get().beepMove(); }
|
||||
void beepLock() override { Buzzer::get().beepLock(); }
|
||||
void beepLines(int lines) override { Buzzer::get().beepLines(lines); }
|
||||
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
|
||||
void beepGameOver() override { Buzzer::get().beepGameOver(); }
|
||||
|
||||
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
|
||||
void toggleMuted() override { Buzzer::get().toggleMuted(); }
|
||||
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
|
||||
};
|
||||
|
||||
class EspBatteryMonitor final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return true; }
|
||||
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
|
||||
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
|
||||
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
|
||||
};
|
||||
|
||||
class EspStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READONLY, &handle) != ESP_OK)
|
||||
return false;
|
||||
std::uint32_t value = 0;
|
||||
esp_err_t err = nvs_get_u32(handle, keyStr.c_str(), &value);
|
||||
nvs_close(handle);
|
||||
if (err != ESP_OK)
|
||||
return false;
|
||||
out = value;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
nvs_handle_t handle;
|
||||
std::string nsStr(ns);
|
||||
std::string keyStr(key);
|
||||
if (nvs_open(nsStr.c_str(), NVS_READWRITE, &handle) != ESP_OK)
|
||||
return;
|
||||
nvs_set_u32(handle, keyStr.c_str(), value);
|
||||
nvs_commit(handle);
|
||||
nvs_close(handle);
|
||||
}
|
||||
};
|
||||
|
||||
class EspRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return esp_random(); }
|
||||
};
|
||||
|
||||
class EspHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
[[nodiscard]] std::uint64_t micros() override { return static_cast<std::uint64_t>(esp_timer_get_time()); }
|
||||
};
|
||||
|
||||
class EspPowerManager 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 EspFilesystem final : public cardboy::sdk::IFilesystem {
|
||||
public:
|
||||
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
|
||||
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
|
||||
[[nodiscard]] std::string basePath() const override {
|
||||
const char* path = FsHelper::get().basePath();
|
||||
return path ? std::string(path) : std::string{};
|
||||
}
|
||||
constexpr apps::EmbeddedRomDescriptor kEmbeddedRoms[] = {
|
||||
{
|
||||
.name = "Builtin Demo 1",
|
||||
.saveSlug = "builtin_demo1",
|
||||
.start = _binary_builtin_demo1_gb_start,
|
||||
.end = _binary_builtin_demo1_gb_end,
|
||||
},
|
||||
{
|
||||
.name = "Builtin Demo 2",
|
||||
.saveSlug = "builtin_demo2",
|
||||
.start = _binary_builtin_demo2_gb_start,
|
||||
.end = _binary_builtin_demo2_gb_end,
|
||||
},
|
||||
};
|
||||
|
||||
} // namespace
|
||||
@@ -286,49 +206,13 @@ extern "C" void app_main() {
|
||||
ESP_ERROR_CHECK(esp_sleep_enable_gpio_wakeup());
|
||||
#endif
|
||||
|
||||
PowerHelper::get();
|
||||
Shutdowner::get();
|
||||
Buttons::get();
|
||||
apps::setGameboyEmbeddedRoms(std::span<const apps::EmbeddedRomDescriptor>(kEmbeddedRoms));
|
||||
|
||||
ESP_ERROR_CHECK(gpio_install_isr_service(0));
|
||||
Shutdowner::get().install_isr();
|
||||
PowerHelper::get().install_isr();
|
||||
Buttons::get().install_isr();
|
||||
static cardboy::backend::esp::EspRuntime runtime;
|
||||
|
||||
I2cGlobal::get();
|
||||
BatMon::get();
|
||||
SpiGlobal::get();
|
||||
SMD::init();
|
||||
|
||||
DispTools::clear();
|
||||
Buzzer::get().init();
|
||||
|
||||
FsHelper::get().mount();
|
||||
|
||||
static PlatformFramebuffer framebuffer;
|
||||
static PlatformInput input;
|
||||
static PlatformClock clock;
|
||||
|
||||
static EspBuzzer buzzerService;
|
||||
static EspBatteryMonitor batteryService;
|
||||
static EspStorage storageService;
|
||||
static EspRandom randomService;
|
||||
static EspHighResClock highResClockService;
|
||||
static EspPowerManager powerService;
|
||||
static EspFilesystem filesystemService;
|
||||
|
||||
static cardboy::sdk::Services services{};
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResClockService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
|
||||
AppContext context(framebuffer, input, clock);
|
||||
context.services = &services;
|
||||
AppSystem system(context);
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
context.system = &system;
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 02.03.2025.
|
||||
//
|
||||
|
||||
#include "disp_tools.hpp"
|
||||
|
||||
#include <cmath>
|
||||
|
||||
#include <display.hpp>
|
||||
|
||||
@@ -1,62 +0,0 @@
|
||||
//
|
||||
// Created by Stepan Usatiuk on 26.04.2024.
|
||||
//
|
||||
|
||||
#include "disp_tty.hpp"
|
||||
|
||||
#include <disp_tools.hpp>
|
||||
|
||||
#include "cardboy/gfx/Fonts.hpp"
|
||||
|
||||
void FbTty::draw_char(int col, int row) {
|
||||
for (int x = 0; x < 8; x++) {
|
||||
for (int y = 0; y < 16; y++) {
|
||||
bool color = fonts_Terminess_Powerline[_buf[col][row]][y] & (1 << (8 - x));
|
||||
if (color)
|
||||
DispTools::set_pixel(col * 8 + x, row * 16 + y);
|
||||
else
|
||||
DispTools::reset_pixel(col * 8 + x, row * 16 + y);
|
||||
}
|
||||
}
|
||||
}
|
||||
void FbTty::reset() {
|
||||
_cur_col = 0;
|
||||
_cur_row = 0;
|
||||
}
|
||||
void FbTty::putchar(char c) {
|
||||
if (c == '\n') {
|
||||
next_row();
|
||||
return;
|
||||
}
|
||||
|
||||
_buf[_cur_col][_cur_row] = c;
|
||||
|
||||
draw_char(_cur_col, _cur_row);
|
||||
|
||||
next_col();
|
||||
}
|
||||
void FbTty::putstr(const char* str) {
|
||||
while (*str != 0) {
|
||||
putchar(*str);
|
||||
str++;
|
||||
}
|
||||
}
|
||||
void FbTty::next_col() {
|
||||
_cur_col++;
|
||||
_cur_col = _cur_col % _max_col;
|
||||
if (_cur_col == 0) {
|
||||
next_row();
|
||||
} else {
|
||||
_buf[_cur_col][_cur_row] = ' ';
|
||||
draw_char(_cur_col, _cur_row);
|
||||
}
|
||||
}
|
||||
void FbTty::next_row() {
|
||||
_cur_col = 0;
|
||||
_cur_row++;
|
||||
_cur_row = _cur_row % _max_row;
|
||||
for (int i = 0; i < _max_col; i++) {
|
||||
_buf[i][_cur_row] = ' ';
|
||||
draw_char(i, _cur_row);
|
||||
}
|
||||
}
|
||||
@@ -5,66 +5,31 @@ set(CMAKE_CXX_STANDARD 20)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
||||
set(CMAKE_CXX_EXTENSIONS NO)
|
||||
|
||||
add_library(cardboy_sdk STATIC
|
||||
src/app_system.cpp
|
||||
)
|
||||
add_subdirectory(backend_interface)
|
||||
|
||||
target_include_directories(cardboy_sdk
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY "" CACHE STRING "Backend implementation library for Cardboy SDK")
|
||||
set(_cardboy_backend_default "${CARDBOY_SDK_BACKEND_LIBRARY}")
|
||||
|
||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||
|
||||
add_library(cardboy_apps STATIC
|
||||
apps/menu_app.cpp
|
||||
apps/clock_app.cpp
|
||||
apps/tetris_app.cpp
|
||||
apps/gameboy_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_apps
|
||||
PUBLIC
|
||||
cardboy_sdk
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||
|
||||
option(CARDBOY_BUILD_SFML "Build SFML harness" OFF)
|
||||
option(CARDBOY_BUILD_SFML "Build desktop SFML backend and launcher" ON)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
include(FetchContent)
|
||||
|
||||
set(SFML_BUILD_AUDIO OFF CACHE BOOL "Disable SFML audio module" FORCE)
|
||||
set(SFML_BUILD_NETWORK OFF CACHE BOOL "Disable SFML network module" FORCE)
|
||||
set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "Disable SFML examples" FORCE)
|
||||
set(SFML_BUILD_TESTS OFF CACHE BOOL "Disable SFML tests" FORCE)
|
||||
set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use bundled SFML dependencies" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
SFML
|
||||
GIT_REPOSITORY https://github.com/SFML/SFML.git
|
||||
GIT_TAG 3.0.2
|
||||
GIT_SHALLOW ON
|
||||
)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
|
||||
add_executable(cardboy_desktop
|
||||
hosts/sfml_main.cpp
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_desktop
|
||||
PRIVATE
|
||||
cardboy_apps
|
||||
SFML::Graphics
|
||||
SFML::Window
|
||||
SFML::System
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||
add_subdirectory(backends/desktop)
|
||||
if (DEFINED CARDBOY_DESKTOP_BACKEND_TARGET AND NOT CARDBOY_DESKTOP_BACKEND_TARGET STREQUAL "")
|
||||
set(_cardboy_backend_default "${CARDBOY_DESKTOP_BACKEND_TARGET}")
|
||||
endif ()
|
||||
endif ()
|
||||
|
||||
if (_cardboy_backend_default STREQUAL "")
|
||||
message(FATAL_ERROR "CARDBOY_SDK_BACKEND_LIBRARY is not set. Provide a backend implementation library or enable one of the available backends.")
|
||||
endif ()
|
||||
|
||||
set(CARDBOY_SDK_BACKEND_LIBRARY "${_cardboy_backend_default}" CACHE STRING "Backend implementation library for Cardboy SDK" FORCE)
|
||||
|
||||
|
||||
add_subdirectory(core)
|
||||
|
||||
add_subdirectory(apps)
|
||||
|
||||
if (CARDBOY_BUILD_SFML)
|
||||
add_subdirectory(launchers/desktop)
|
||||
endif ()
|
||||
|
||||
18
Firmware/sdk/apps/CMakeLists.txt
Normal file
18
Firmware/sdk/apps/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
||||
add_library(cardboy_apps STATIC)
|
||||
|
||||
set_target_properties(cardboy_apps PROPERTIES
|
||||
EXPORT_NAME apps
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_apps
|
||||
PUBLIC
|
||||
cardboy_sdk
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||
|
||||
add_subdirectory(menu)
|
||||
add_subdirectory(clock)
|
||||
add_subdirectory(gameboy)
|
||||
add_subdirectory(tetris)
|
||||
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/clock_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
@@ -160,8 +160,7 @@ private:
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
framebuffer.frameReady();
|
||||
|
||||
const int scaleLarge = 3;
|
||||
const int scaleSeconds = 2;
|
||||
@@ -220,7 +219,7 @@ private:
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
|
||||
|
||||
framebuffer.endFrame();
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
};
|
||||
|
||||
10
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
10
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
@@ -4,13 +4,23 @@
|
||||
|
||||
#include <memory>
|
||||
#include <string_view>
|
||||
#include <span>
|
||||
#include <cstdint>
|
||||
|
||||
namespace apps {
|
||||
|
||||
inline constexpr char kGameboyAppName[] = "Game Boy";
|
||||
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
|
||||
|
||||
struct EmbeddedRomDescriptor {
|
||||
std::string_view name;
|
||||
std::string_view saveSlug;
|
||||
const std::uint8_t* start = nullptr;
|
||||
const std::uint8_t* end = nullptr;
|
||||
};
|
||||
|
||||
void setGameboyEmbeddedRoms(std::span<const EmbeddedRomDescriptor> descriptors);
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
|
||||
|
||||
} // namespace apps
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
#pragma GCC optimize("Ofast")
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/peanut_gb.h"
|
||||
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/app_framework.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/gfx/font16x8.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <inttypes.h>
|
||||
@@ -13,6 +12,7 @@
|
||||
#include <array>
|
||||
#include <cctype>
|
||||
#include <cerrno>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
#include <cstring>
|
||||
@@ -21,7 +21,6 @@
|
||||
#include <string_view>
|
||||
#include <sys/stat.h>
|
||||
#include <vector>
|
||||
#include <chrono>
|
||||
|
||||
#define GAMEBOY_PERF_METRICS 0
|
||||
|
||||
@@ -41,6 +40,7 @@ namespace {
|
||||
constexpr int kMenuStartY = 48;
|
||||
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
||||
|
||||
|
||||
using cardboy::sdk::AppContext;
|
||||
using cardboy::sdk::AppEvent;
|
||||
using cardboy::sdk::AppEventType;
|
||||
@@ -51,36 +51,7 @@ using Framebuffer = typename AppContext::Framebuffer;
|
||||
|
||||
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
|
||||
|
||||
struct EmbeddedRomDescriptor {
|
||||
std::string_view name;
|
||||
std::string_view saveSlug;
|
||||
const uint8_t* start;
|
||||
const uint8_t* end;
|
||||
};
|
||||
|
||||
#ifdef ESP_PLATFORM
|
||||
extern "C" {
|
||||
extern const uint8_t _binary_builtin_demo1_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo1_gb_end[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_start[];
|
||||
extern const uint8_t _binary_builtin_demo2_gb_end[];
|
||||
}
|
||||
|
||||
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
||||
"Builtin Demo 1",
|
||||
"builtin_demo1",
|
||||
_binary_builtin_demo1_gb_start,
|
||||
_binary_builtin_demo1_gb_end,
|
||||
},
|
||||
{
|
||||
"Builtin Demo 2",
|
||||
"builtin_demo2",
|
||||
_binary_builtin_demo2_gb_start,
|
||||
_binary_builtin_demo2_gb_end,
|
||||
}}};
|
||||
#else
|
||||
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
|
||||
#endif
|
||||
static std::span<const EmbeddedRomDescriptor> gEmbeddedRomDescriptors{};
|
||||
|
||||
struct RomEntry {
|
||||
std::string name; // short display name
|
||||
@@ -113,7 +84,7 @@ struct RomEntry {
|
||||
}
|
||||
|
||||
void appendEmbeddedRoms(std::vector<RomEntry>& out) {
|
||||
for (const auto& desc: kEmbeddedRomDescriptors) {
|
||||
for (const auto& desc: gEmbeddedRomDescriptors) {
|
||||
if (!desc.start || !desc.end || desc.end <= desc.start)
|
||||
continue;
|
||||
const std::size_t size = static_cast<std::size_t>(desc.end - desc.start);
|
||||
@@ -250,8 +221,6 @@ public:
|
||||
break;
|
||||
}
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
|
||||
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
|
||||
ensureRenderGeometry();
|
||||
@@ -264,7 +233,6 @@ public:
|
||||
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
|
||||
renderGameFrame();
|
||||
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||
framebuffer.endFrame();
|
||||
break;
|
||||
}
|
||||
}
|
||||
@@ -849,8 +817,7 @@ private:
|
||||
return;
|
||||
browserDirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
framebuffer.frameReady();
|
||||
|
||||
const std::string_view title = "GAME BOY";
|
||||
const int titleWidth = font16x8::measureText(title, 2, 1);
|
||||
@@ -893,7 +860,7 @@ private:
|
||||
font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1);
|
||||
}
|
||||
|
||||
framebuffer.endFrame();
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
|
||||
bool loadRom(std::size_t index) {
|
||||
@@ -1184,7 +1151,7 @@ private:
|
||||
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
|
||||
}
|
||||
}
|
||||
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
|
||||
void maybeSaveRam() {
|
||||
@@ -1259,10 +1226,7 @@ private:
|
||||
return static_cast<GameboyApp*>(gb->direct.priv);
|
||||
}
|
||||
|
||||
static bool shouldPixelBeOn(int value, int dstX, int dstY, bool useDither) {
|
||||
value &= 0x03;
|
||||
if (!useDither)
|
||||
return value >= 2;
|
||||
__attribute__((always_inline)) static bool shouldPixelBeOn(int value, int dstX, int dstY) {
|
||||
if (value >= 3)
|
||||
return true;
|
||||
if (value <= 0)
|
||||
@@ -1335,16 +1299,13 @@ private:
|
||||
self->frameDirty = true;
|
||||
|
||||
Framebuffer& fb = self->framebuffer;
|
||||
|
||||
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
|
||||
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
|
||||
fb.frameReady();
|
||||
|
||||
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
|
||||
const int dstY = yStart;
|
||||
const int dstXBase = geom.offsetX;
|
||||
for (int x = 0; x < LCD_WIDTH; ++x) {
|
||||
const int val = (pixels[x] & 0x03u);
|
||||
const bool on = shouldPixelBeOn(val, dstXBase + x, dstY, useDither);
|
||||
const bool on = shouldPixelBeOn(pixels[x], dstXBase + x, dstY);
|
||||
fb.drawPixel(dstXBase + x, dstY, on);
|
||||
}
|
||||
return;
|
||||
@@ -1359,7 +1320,7 @@ private:
|
||||
continue;
|
||||
for (int dstY = yStart; dstY < yEnd; ++dstY)
|
||||
for (int dstX = drawStart; dstX < drawEnd; ++dstX) {
|
||||
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY, useDither);
|
||||
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY);
|
||||
fb.drawPixel(dstX, dstY, on);
|
||||
}
|
||||
}
|
||||
@@ -1406,8 +1367,10 @@ public:
|
||||
|
||||
} // namespace
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() {
|
||||
return std::make_unique<GameboyAppFactory>();
|
||||
void setGameboyEmbeddedRoms(std::span<const EmbeddedRomDescriptor> descriptors) {
|
||||
gEmbeddedRomDescriptors = descriptors;
|
||||
}
|
||||
|
||||
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() { return std::make_unique<GameboyAppFactory>(); }
|
||||
|
||||
} // namespace apps
|
||||
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/menu_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
@@ -135,8 +135,7 @@ private:
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
framebuffer.frameReady();
|
||||
|
||||
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
||||
|
||||
@@ -158,7 +157,7 @@ private:
|
||||
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
||||
}
|
||||
|
||||
framebuffer.endFrame();
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
};
|
||||
|
||||
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
||||
target_sources(cardboy_apps
|
||||
PRIVATE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/tetris_app.cpp
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_apps
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
@@ -467,15 +467,14 @@ private:
|
||||
return;
|
||||
dirty = false;
|
||||
|
||||
framebuffer.beginFrame();
|
||||
framebuffer.clear(false);
|
||||
framebuffer.frameReady();
|
||||
|
||||
drawBoard();
|
||||
drawActivePiece();
|
||||
drawNextPreview();
|
||||
drawHUD();
|
||||
|
||||
framebuffer.endFrame();
|
||||
framebuffer.sendFrame();
|
||||
}
|
||||
|
||||
void drawBoard() {
|
||||
20
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
20
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
@@ -0,0 +1,20 @@
|
||||
add_library(cardboy_backend_interface INTERFACE)
|
||||
|
||||
set_target_properties(cardboy_backend_interface PROPERTIES
|
||||
EXPORT_NAME backend_interface
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_backend_interface
|
||||
INTERFACE
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_sources(cardboy_backend_interface
|
||||
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/input_state.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/platform.hpp
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/sdk/services.hpp
|
||||
)
|
||||
@@ -0,0 +1,14 @@
|
||||
#pragma once
|
||||
|
||||
#include <concepts>
|
||||
|
||||
namespace cardboy::backend {
|
||||
|
||||
template<typename Backend>
|
||||
concept BackendInterface = requires {
|
||||
typename Backend::Framebuffer;
|
||||
typename Backend::Input;
|
||||
typename Backend::Clock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend
|
||||
@@ -0,0 +1,10 @@
|
||||
#pragma once
|
||||
|
||||
#include <cardboy/sdk/platform.hpp>
|
||||
|
||||
#include <cardboy/backend/backend_impl.hpp>
|
||||
#include <cardboy/backend/backend_interface.hpp>
|
||||
|
||||
namespace cardboy::backend {
|
||||
static_assert(BackendInterface<ActiveBackend>, "ActiveBackend must provide Framebuffer, Input, Clock types");
|
||||
} // namespace cardboy::backend
|
||||
@@ -6,4 +6,3 @@ inline constexpr int kDisplayWidth = 400;
|
||||
inline constexpr int kDisplayHeight = 240;
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
@@ -14,4 +14,3 @@ struct InputState {
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
115
Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp
Normal file
115
Firmware/sdk/backend_interface/include/cardboy/sdk/platform.hpp
Normal file
@@ -0,0 +1,115 @@
|
||||
#pragma once
|
||||
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <concepts>
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
namespace detail {
|
||||
template<typename Impl>
|
||||
concept HasClearImpl = requires(Impl& impl, bool value) {
|
||||
{ impl.clear_impl(value) };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasFrameReadyImpl = requires(Impl& impl) {
|
||||
{ impl.frameReady_impl() };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
|
||||
{ impl.sendFrame_impl(flag) };
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasFrameInFlightImpl = requires(const Impl& impl) {
|
||||
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
|
||||
{ impl.sleep_ms_impl(value) };
|
||||
};
|
||||
} // namespace detail
|
||||
|
||||
template<typename Impl>
|
||||
class FramebufferFacade {
|
||||
public:
|
||||
[[nodiscard]] int width() const { return impl().width_impl(); }
|
||||
[[nodiscard]] int height() const { return impl().height_impl(); }
|
||||
|
||||
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
|
||||
|
||||
void clear(bool on) {
|
||||
if constexpr (detail::HasClearImpl<Impl>) {
|
||||
impl().clear_impl(on);
|
||||
} else {
|
||||
defaultClear(on);
|
||||
}
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void frameReady() {
|
||||
if constexpr (detail::HasFrameReadyImpl<Impl>)
|
||||
impl().frameReady_impl();
|
||||
}
|
||||
|
||||
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
|
||||
if constexpr (detail::HasSendFrameImpl<Impl>)
|
||||
impl().sendFrame_impl(clearDrawBuffer);
|
||||
}
|
||||
|
||||
[[nodiscard]] __attribute__((always_inline)) bool isFrameInFlight() const {
|
||||
if constexpr (detail::HasFrameInFlightImpl<Impl>)
|
||||
return impl().frameInFlight_impl();
|
||||
return false;
|
||||
}
|
||||
|
||||
protected:
|
||||
FramebufferFacade() = default;
|
||||
~FramebufferFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] __attribute__((always_inline)) Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
[[nodiscard]] __attribute__((always_inline)) const Impl& impl() const { return static_cast<const Impl&>(*this); }
|
||||
|
||||
void defaultClear(bool on) {
|
||||
for (int y = 0; y < height(); ++y)
|
||||
for (int x = 0; x < width(); ++x)
|
||||
drawPixel(x, y, on);
|
||||
}
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
class InputFacade {
|
||||
public:
|
||||
InputState readState() { return impl().readState_impl(); }
|
||||
|
||||
protected:
|
||||
InputFacade() = default;
|
||||
~InputFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
};
|
||||
|
||||
template<typename Impl>
|
||||
class ClockFacade {
|
||||
public:
|
||||
std::uint32_t millis() { return impl().millis_impl(); }
|
||||
|
||||
void sleep_ms(std::uint32_t ms) {
|
||||
if constexpr (detail::HasSleepMsImpl<Impl>)
|
||||
impl().sleep_ms_impl(ms);
|
||||
}
|
||||
|
||||
protected:
|
||||
ClockFacade() = default;
|
||||
~ClockFacade() = default;
|
||||
|
||||
private:
|
||||
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
38
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
38
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
@@ -0,0 +1,38 @@
|
||||
include(FetchContent)
|
||||
|
||||
set(SFML_BUILD_AUDIO OFF CACHE BOOL "Disable SFML audio module" FORCE)
|
||||
set(SFML_BUILD_NETWORK OFF CACHE BOOL "Disable SFML network module" FORCE)
|
||||
set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "Disable SFML examples" FORCE)
|
||||
set(SFML_BUILD_TESTS OFF CACHE BOOL "Disable SFML tests" FORCE)
|
||||
set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use bundled SFML dependencies" FORCE)
|
||||
|
||||
FetchContent_Declare(
|
||||
SFML
|
||||
GIT_REPOSITORY https://github.com/SFML/SFML.git
|
||||
GIT_TAG 3.0.2
|
||||
GIT_SHALLOW ON
|
||||
)
|
||||
FetchContent_MakeAvailable(SFML)
|
||||
|
||||
add_library(cardboy_backend_desktop STATIC
|
||||
src/desktop_backend.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_backend_desktop PROPERTIES
|
||||
EXPORT_NAME backend_desktop
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_backend_desktop
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_backend_desktop
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
SFML::Window
|
||||
SFML::Graphics
|
||||
SFML::System
|
||||
)
|
||||
|
||||
set(CARDBOY_DESKTOP_BACKEND_TARGET cardboy_backend_desktop PARENT_SCOPE)
|
||||
@@ -0,0 +1,7 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
|
||||
namespace cardboy::backend {
|
||||
using ActiveBackend = DesktopBackend;
|
||||
}
|
||||
@@ -0,0 +1,186 @@
|
||||
#pragma once
|
||||
|
||||
#include "cardboy/sdk/platform.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window/Keyboard.hpp>
|
||||
#include <chrono>
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <random>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace cardboy::backend::desktop {
|
||||
|
||||
constexpr int kPixelScale = 2;
|
||||
|
||||
class DesktopRuntime;
|
||||
|
||||
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||
void beepRotate() override {}
|
||||
void beepMove() override {}
|
||||
void beepLock() override {}
|
||||
void beepLines(int) override {}
|
||||
void beepLevelUp(int) override {}
|
||||
void beepGameOver() override {}
|
||||
};
|
||||
|
||||
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return false; }
|
||||
};
|
||||
|
||||
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::uint32_t> data;
|
||||
|
||||
static std::string composeKey(std::string_view ns, std::string_view key);
|
||||
};
|
||||
|
||||
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
DesktopRandom();
|
||||
|
||||
[[nodiscard]] std::uint32_t nextUint32() override;
|
||||
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<std::uint32_t> dist;
|
||||
};
|
||||
|
||||
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
DesktopHighResClock();
|
||||
|
||||
[[nodiscard]] std::uint64_t micros() override;
|
||||
|
||||
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();
|
||||
|
||||
bool mount() override;
|
||||
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||
|
||||
private:
|
||||
std::filesystem::path basePathPath;
|
||||
bool mounted = false;
|
||||
};
|
||||
|
||||
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
|
||||
public:
|
||||
explicit DesktopFramebuffer(DesktopRuntime& runtime);
|
||||
|
||||
[[nodiscard]] int width_impl() const;
|
||||
[[nodiscard]] int height_impl() const;
|
||||
void drawPixel_impl(int x, int y, bool on);
|
||||
void clear_impl(bool on);
|
||||
void frameReady_impl();
|
||||
void sendFrame_impl(bool clearAfterSend);
|
||||
[[nodiscard]] bool frameInFlight_impl() const { return false; }
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
};
|
||||
|
||||
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
|
||||
public:
|
||||
explicit DesktopInput(DesktopRuntime& runtime);
|
||||
|
||||
cardboy::sdk::InputState readState_impl();
|
||||
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
cardboy::sdk::InputState state{};
|
||||
};
|
||||
|
||||
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
|
||||
public:
|
||||
explicit DesktopClock(DesktopRuntime& runtime);
|
||||
|
||||
std::uint32_t millis_impl();
|
||||
void sleep_ms_impl(std::uint32_t ms);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopRuntime {
|
||||
public:
|
||||
DesktopRuntime();
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry();
|
||||
void processEvents();
|
||||
void presentIfNeeded();
|
||||
void sleepFor(std::uint32_t ms);
|
||||
|
||||
[[nodiscard]] bool isRunning() const { return running; }
|
||||
|
||||
DesktopFramebuffer framebuffer;
|
||||
DesktopInput input;
|
||||
DesktopClock clock;
|
||||
|
||||
private:
|
||||
friend class DesktopFramebuffer;
|
||||
friend class DesktopInput;
|
||||
friend class DesktopClock;
|
||||
|
||||
void setPixel(int x, int y, bool on);
|
||||
void clearPixels(bool on);
|
||||
|
||||
sf::RenderWindow window;
|
||||
sf::Texture texture;
|
||||
sf::Sprite sprite;
|
||||
std::vector<std::uint8_t> pixels;
|
||||
bool dirty = true;
|
||||
bool running = true;
|
||||
bool clearNextFrame = true;
|
||||
|
||||
DesktopBuzzer buzzerService;
|
||||
DesktopBattery batteryService;
|
||||
DesktopStorage storageService;
|
||||
DesktopRandom randomService;
|
||||
DesktopHighResClock highResService;
|
||||
DesktopPowerManager powerService;
|
||||
DesktopFilesystem filesystemService;
|
||||
cardboy::sdk::Services services{};
|
||||
};
|
||||
|
||||
struct Backend {
|
||||
using Framebuffer = DesktopFramebuffer;
|
||||
using Input = DesktopInput;
|
||||
using Clock = DesktopClock;
|
||||
};
|
||||
|
||||
} // namespace cardboy::backend::desktop
|
||||
|
||||
namespace cardboy::backend {
|
||||
using DesktopBackend = desktop::Backend;
|
||||
} // namespace cardboy::backend
|
||||
235
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
235
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
@@ -0,0 +1,235 @@
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdlib>
|
||||
#include <stdexcept>
|
||||
#include <system_error>
|
||||
|
||||
namespace cardboy::backend::desktop {
|
||||
|
||||
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())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void DesktopStorage::writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) {
|
||||
data[composeKey(ns, key)] = value;
|
||||
}
|
||||
|
||||
std::string DesktopStorage::composeKey(std::string_view ns, std::string_view key) {
|
||||
std::string result;
|
||||
result.reserve(ns.size() + key.size() + 1);
|
||||
result.append(ns.begin(), ns.end());
|
||||
result.push_back(':');
|
||||
result.append(key.begin(), key.end());
|
||||
return result;
|
||||
}
|
||||
|
||||
DesktopRandom::DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||
|
||||
std::uint32_t DesktopRandom::nextUint32() { return dist(rng); }
|
||||
|
||||
DesktopHighResClock::DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint64_t DesktopHighResClock::micros() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||
}
|
||||
|
||||
DesktopFilesystem::DesktopFilesystem() {
|
||||
if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) {
|
||||
basePathPath = std::filesystem::path(env);
|
||||
} else {
|
||||
basePathPath = std::filesystem::current_path() / "roms";
|
||||
}
|
||||
}
|
||||
|
||||
bool DesktopFilesystem::mount() {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(basePathPath, ec)) {
|
||||
mounted = std::filesystem::is_directory(basePathPath, ec);
|
||||
} else {
|
||||
mounted = std::filesystem::create_directories(basePathPath, ec);
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||
|
||||
void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); }
|
||||
|
||||
void DesktopFramebuffer::frameReady_impl() {
|
||||
if (runtime.clearNextFrame) {
|
||||
runtime.clearPixels(false);
|
||||
runtime.clearNextFrame = false;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) {
|
||||
runtime.clearNextFrame = clearAfterSend;
|
||||
runtime.dirty = true;
|
||||
}
|
||||
|
||||
DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
|
||||
|
||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||
switch (key) {
|
||||
case sf::Keyboard::Key::Up:
|
||||
state.up = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Down:
|
||||
state.down = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Left:
|
||||
state.left = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Right:
|
||||
state.right = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Z:
|
||||
case sf::Keyboard::Key::A:
|
||||
state.a = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::X:
|
||||
case sf::Keyboard::Key::S:
|
||||
state.b = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Space:
|
||||
state.select = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Enter:
|
||||
state.start = pressed;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint32_t DesktopClock::millis_impl() {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
||||
}
|
||||
|
||||
void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||
|
||||
DesktopRuntime::DesktopRuntime() :
|
||||
window(sf::VideoMode(
|
||||
sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale, cardboy::sdk::kDisplayHeight * kPixelScale}),
|
||||
"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) {
|
||||
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);
|
||||
presentIfNeeded();
|
||||
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
}
|
||||
|
||||
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }
|
||||
|
||||
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);
|
||||
pixels[idx + 0] = value;
|
||||
pixels[idx + 1] = value;
|
||||
pixels[idx + 2] = value;
|
||||
pixels[idx + 3] = 255;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::clearPixels(bool on) {
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||
pixels[i + 0] = value;
|
||||
pixels[i + 1] = value;
|
||||
pixels[i + 2] = value;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::processEvents() {
|
||||
while (auto eventOpt = window.pollEvent()) {
|
||||
const sf::Event& event = *eventOpt;
|
||||
|
||||
if (event.is<sf::Event::Closed>()) {
|
||||
running = false;
|
||||
window.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyPressed = event.getIf<sf::Event::KeyPressed>()) {
|
||||
input.handleKey(keyPressed->code, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyReleased = event.getIf<sf::Event::KeyReleased>()) {
|
||||
input.handleKey(keyReleased->code, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopRuntime::presentIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
texture.update(pixels.data());
|
||||
window.clear(sf::Color::Black);
|
||||
window.draw(sprite);
|
||||
window.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void DesktopRuntime::sleepFor(std::uint32_t ms) {
|
||||
const auto target = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
|
||||
do {
|
||||
processEvents();
|
||||
presentIfNeeded();
|
||||
if (!running)
|
||||
std::exit(0);
|
||||
if (ms == 0)
|
||||
return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= target)
|
||||
return;
|
||||
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(target - now);
|
||||
if (remaining.count() > 2)
|
||||
std::this_thread::sleep_for(std::min<std::chrono::milliseconds>(remaining, std::chrono::milliseconds(8)));
|
||||
else
|
||||
std::this_thread::yield();
|
||||
} while (true);
|
||||
}
|
||||
|
||||
} // namespace cardboy::backend::desktop
|
||||
22
Firmware/sdk/core/CMakeLists.txt
Normal file
22
Firmware/sdk/core/CMakeLists.txt
Normal file
@@ -0,0 +1,22 @@
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
|
||||
add_library(cardboy_sdk STATIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_sdk PROPERTIES
|
||||
EXPORT_NAME sdk
|
||||
)
|
||||
|
||||
target_include_directories(cardboy_sdk
|
||||
PUBLIC
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||
|
||||
target_link_libraries(cardboy_sdk
|
||||
PUBLIC
|
||||
cardboy_backend_interface
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
@@ -1,7 +1,8 @@
|
||||
#pragma once
|
||||
|
||||
#include "platform.hpp"
|
||||
#include "services.hpp"
|
||||
#include <cardboy/sdk/backend.hpp>
|
||||
#include <cardboy/sdk/platform.hpp>
|
||||
#include <cardboy/sdk/services.hpp>
|
||||
|
||||
#include <cstdint>
|
||||
#include <memory>
|
||||
@@ -37,18 +38,19 @@ struct AppEvent {
|
||||
AppTimerEvent timer{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
struct BasicAppContext {
|
||||
using Framebuffer = FramebufferT;
|
||||
using Input = InputT;
|
||||
using Clock = ClockT;
|
||||
using ActiveBackend = cardboy::backend::ActiveBackend;
|
||||
|
||||
BasicAppContext() = delete;
|
||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
struct AppContext {
|
||||
using Framebuffer = typename ActiveBackend::Framebuffer;
|
||||
using Input = typename ActiveBackend::Input;
|
||||
using Clock = typename ActiveBackend::Clock;
|
||||
|
||||
FramebufferT& framebuffer;
|
||||
InputT& input;
|
||||
ClockT& clock;
|
||||
AppContext() = delete;
|
||||
AppContext(Framebuffer& fb, Input& in, Clock& clk) : framebuffer(fb), input(in), clock(clk) {}
|
||||
|
||||
Framebuffer& framebuffer;
|
||||
Input& input;
|
||||
Clock& clock;
|
||||
AppSystem* system = nullptr;
|
||||
Services* services = nullptr;
|
||||
|
||||
@@ -113,8 +115,6 @@ private:
|
||||
void cancelAllTimersInternal();
|
||||
};
|
||||
|
||||
using AppContext = BasicAppContext<IFramebuffer, IInput, IClock>;
|
||||
|
||||
class IApp {
|
||||
public:
|
||||
virtual ~IApp() = default;
|
||||
@@ -28,8 +28,7 @@ public:
|
||||
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
|
||||
|
||||
private:
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
friend struct BasicAppContext;
|
||||
friend struct AppContext;
|
||||
|
||||
struct TimerRecord {
|
||||
AppTimerHandle id = kInvalidAppTimer;
|
||||
@@ -63,23 +62,18 @@ private:
|
||||
InputState lastInputState{};
|
||||
};
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(std::uint32_t delay_ms,
|
||||
bool repeat) {
|
||||
inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
|
||||
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
|
||||
inline void AppContext::cancelTimerInternal(AppTimerHandle handle) {
|
||||
if (system)
|
||||
system->cancelTimer(handle);
|
||||
}
|
||||
|
||||
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
|
||||
inline void AppContext::cancelAllTimersInternal() {
|
||||
if (system)
|
||||
system->cancelAllTimers();
|
||||
}
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
@@ -1,397 +0,0 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
#include "cardboy/sdk/display_spec.hpp"
|
||||
#include "cardboy/sdk/services.hpp"
|
||||
|
||||
#include <SFML/Graphics.hpp>
|
||||
#include <SFML/Window.hpp>
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
#include <cstdio>
|
||||
#include <cstdint>
|
||||
#include <cstdlib>
|
||||
#include <exception>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <optional>
|
||||
#include <random>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
namespace {
|
||||
|
||||
constexpr int kPixelScale = 2;
|
||||
|
||||
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||
public:
|
||||
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||
void beepRotate() override {}
|
||||
void beepMove() override {}
|
||||
void beepLock() override {}
|
||||
void beepLines(int) override {}
|
||||
void beepLevelUp(int) override {}
|
||||
void beepGameOver() override {}
|
||||
};
|
||||
|
||||
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||
public:
|
||||
[[nodiscard]] bool hasData() const override { return false; }
|
||||
};
|
||||
|
||||
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||
public:
|
||||
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override {
|
||||
auto it = data.find(composeKey(ns, key));
|
||||
if (it == data.end())
|
||||
return false;
|
||||
out = it->second;
|
||||
return true;
|
||||
}
|
||||
|
||||
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override {
|
||||
data[composeKey(ns, key)] = value;
|
||||
}
|
||||
|
||||
private:
|
||||
std::unordered_map<std::string, std::uint32_t> data;
|
||||
|
||||
static std::string composeKey(std::string_view ns, std::string_view key) {
|
||||
std::string result;
|
||||
result.reserve(ns.size() + key.size() + 1);
|
||||
result.append(ns.begin(), ns.end());
|
||||
result.push_back(':');
|
||||
result.append(key.begin(), key.end());
|
||||
return result;
|
||||
}
|
||||
};
|
||||
|
||||
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||
public:
|
||||
DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||
|
||||
[[nodiscard]] std::uint32_t nextUint32() override { return dist(rng); }
|
||||
|
||||
private:
|
||||
std::mt19937 rng;
|
||||
std::uniform_int_distribution<std::uint32_t> dist;
|
||||
};
|
||||
|
||||
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||
public:
|
||||
DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
[[nodiscard]] std::uint64_t micros() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint64_t>(
|
||||
std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||
}
|
||||
|
||||
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() {
|
||||
if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) {
|
||||
basePathPath = std::filesystem::path(env);
|
||||
} else {
|
||||
basePathPath = std::filesystem::current_path() / "roms";
|
||||
}
|
||||
}
|
||||
|
||||
bool mount() override {
|
||||
std::error_code ec;
|
||||
if (std::filesystem::exists(basePathPath, ec)) {
|
||||
mounted = std::filesystem::is_directory(basePathPath, ec);
|
||||
} else {
|
||||
mounted = std::filesystem::create_directories(basePathPath, ec);
|
||||
}
|
||||
return mounted;
|
||||
}
|
||||
|
||||
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||
|
||||
private:
|
||||
std::filesystem::path basePathPath;
|
||||
bool mounted = false;
|
||||
};
|
||||
|
||||
class DesktopRuntime;
|
||||
|
||||
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||
public:
|
||||
explicit DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
int width() const override;
|
||||
int height() const override;
|
||||
void drawPixel(int x, int y, bool on) override;
|
||||
void clear(bool on) override;
|
||||
void beginFrame() override {}
|
||||
void endFrame() override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
};
|
||||
|
||||
class DesktopInput final : public cardboy::sdk::IInput {
|
||||
public:
|
||||
explicit DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||
|
||||
cardboy::sdk::InputState readState() override { return state; }
|
||||
|
||||
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
cardboy::sdk::InputState state{};
|
||||
};
|
||||
|
||||
class DesktopClock final : public cardboy::sdk::IClock {
|
||||
public:
|
||||
explicit DesktopClock(DesktopRuntime& runtime)
|
||||
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||
|
||||
std::uint32_t millis() override {
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
return static_cast<std::uint32_t>(
|
||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
||||
}
|
||||
|
||||
void sleep_ms(std::uint32_t ms) override;
|
||||
|
||||
private:
|
||||
DesktopRuntime& runtime;
|
||||
const std::chrono::steady_clock::time_point start;
|
||||
};
|
||||
|
||||
class DesktopRuntime {
|
||||
private:
|
||||
friend class DesktopFramebuffer;
|
||||
friend class DesktopInput;
|
||||
friend class DesktopClock;
|
||||
|
||||
sf::RenderWindow window;
|
||||
sf::Texture texture;
|
||||
sf::Sprite sprite;
|
||||
std::vector<std::uint8_t> pixels; // RGBA buffer
|
||||
bool dirty = true;
|
||||
bool running = true;
|
||||
|
||||
DesktopBuzzer buzzerService;
|
||||
DesktopBattery batteryService;
|
||||
DesktopStorage storageService;
|
||||
DesktopRandom randomService;
|
||||
DesktopHighResClock highResService;
|
||||
DesktopPowerManager powerService;
|
||||
DesktopFilesystem filesystemService;
|
||||
cardboy::sdk::Services services{};
|
||||
|
||||
public:
|
||||
DesktopRuntime();
|
||||
|
||||
DesktopFramebuffer framebuffer;
|
||||
DesktopInput input;
|
||||
DesktopClock clock;
|
||||
|
||||
cardboy::sdk::Services& serviceRegistry() { return services; }
|
||||
|
||||
void processEvents();
|
||||
void presentIfNeeded();
|
||||
void sleepFor(std::uint32_t ms);
|
||||
|
||||
[[nodiscard]] bool isRunning() const { return running; }
|
||||
|
||||
private:
|
||||
void setPixel(int x, int y, bool on);
|
||||
void clearPixels(bool on);
|
||||
};
|
||||
|
||||
DesktopRuntime::DesktopRuntime()
|
||||
: window(sf::VideoMode(sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale,
|
||||
cardboy::sdk::kDisplayHeight * kPixelScale}),
|
||||
"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) {
|
||||
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);
|
||||
presentIfNeeded();
|
||||
|
||||
services.buzzer = &buzzerService;
|
||||
services.battery = &batteryService;
|
||||
services.storage = &storageService;
|
||||
services.random = &randomService;
|
||||
services.highResClock = &highResService;
|
||||
services.powerManager = &powerService;
|
||||
services.filesystem = &filesystemService;
|
||||
}
|
||||
|
||||
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);
|
||||
pixels[idx + 0] = value;
|
||||
pixels[idx + 1] = value;
|
||||
pixels[idx + 2] = value;
|
||||
pixels[idx + 3] = 255;
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::clearPixels(bool on) {
|
||||
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||
pixels[i + 0] = value;
|
||||
pixels[i + 1] = value;
|
||||
pixels[i + 2] = value;
|
||||
pixels[i + 3] = 255;
|
||||
}
|
||||
dirty = true;
|
||||
}
|
||||
|
||||
void DesktopRuntime::processEvents() {
|
||||
while (auto eventOpt = window.pollEvent()) {
|
||||
const sf::Event& event = *eventOpt;
|
||||
|
||||
if (event.is<sf::Event::Closed>()) {
|
||||
running = false;
|
||||
window.close();
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyPressed = event.getIf<sf::Event::KeyPressed>()) {
|
||||
input.handleKey(keyPressed->code, true);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (const auto* keyReleased = event.getIf<sf::Event::KeyReleased>()) {
|
||||
input.handleKey(keyReleased->code, false);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopRuntime::presentIfNeeded() {
|
||||
if (!dirty)
|
||||
return;
|
||||
texture.update(pixels.data());
|
||||
window.clear(sf::Color::Black);
|
||||
window.draw(sprite);
|
||||
window.display();
|
||||
dirty = false;
|
||||
}
|
||||
|
||||
void DesktopRuntime::sleepFor(std::uint32_t ms) {
|
||||
const auto target = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
|
||||
do {
|
||||
processEvents();
|
||||
presentIfNeeded();
|
||||
if (!running)
|
||||
std::exit(0);
|
||||
if (ms == 0)
|
||||
return;
|
||||
const auto now = std::chrono::steady_clock::now();
|
||||
if (now >= target)
|
||||
return;
|
||||
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(target - now);
|
||||
if (remaining.count() > 2)
|
||||
std::this_thread::sleep_for(std::min<std::chrono::milliseconds>(remaining, std::chrono::milliseconds(8)));
|
||||
else
|
||||
std::this_thread::yield();
|
||||
} while (true);
|
||||
}
|
||||
|
||||
int DesktopFramebuffer::width() const { return cardboy::sdk::kDisplayWidth; }
|
||||
|
||||
int DesktopFramebuffer::height() const { return cardboy::sdk::kDisplayHeight; }
|
||||
|
||||
void DesktopFramebuffer::drawPixel(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||
|
||||
void DesktopFramebuffer::clear(bool on) { runtime.clearPixels(on); }
|
||||
|
||||
void DesktopFramebuffer::endFrame() { runtime.dirty = true; }
|
||||
|
||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||
switch (key) {
|
||||
case sf::Keyboard::Key::Up:
|
||||
state.up = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Down:
|
||||
state.down = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Left:
|
||||
state.left = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Right:
|
||||
state.right = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Z:
|
||||
case sf::Keyboard::Key::A:
|
||||
state.a = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::X:
|
||||
case sf::Keyboard::Key::S:
|
||||
state.b = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Space:
|
||||
state.select = pressed;
|
||||
break;
|
||||
case sf::Keyboard::Key::Enter:
|
||||
state.start = pressed;
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
void DesktopClock::sleep_ms(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||
|
||||
} // namespace
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DesktopRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
|
||||
system.run();
|
||||
} catch (const std::exception& ex) {
|
||||
std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
#pragma once
|
||||
|
||||
#include "input_state.hpp"
|
||||
|
||||
#include <cstdint>
|
||||
|
||||
namespace cardboy::sdk {
|
||||
|
||||
class IFramebuffer {
|
||||
public:
|
||||
virtual ~IFramebuffer() = default;
|
||||
|
||||
virtual int width() const = 0;
|
||||
virtual int height() const = 0;
|
||||
virtual void drawPixel(int x, int y, bool on) = 0;
|
||||
virtual void clear(bool on) = 0;
|
||||
|
||||
// Optional hooks for async display pipelines; default to no-ops.
|
||||
virtual void beginFrame() {}
|
||||
virtual void endFrame() {}
|
||||
virtual bool isFrameInFlight() const { return false; }
|
||||
};
|
||||
|
||||
class IInput {
|
||||
public:
|
||||
virtual ~IInput() = default;
|
||||
|
||||
virtual InputState readState() = 0;
|
||||
};
|
||||
|
||||
class IClock {
|
||||
public:
|
||||
virtual ~IClock() = default;
|
||||
|
||||
virtual std::uint32_t millis() = 0;
|
||||
virtual void sleep_ms(std::uint32_t ms) = 0;
|
||||
};
|
||||
|
||||
} // namespace cardboy::sdk
|
||||
|
||||
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
||||
add_executable(cardboy_desktop
|
||||
src/main.cpp
|
||||
)
|
||||
|
||||
set_target_properties(cardboy_desktop PROPERTIES
|
||||
EXPORT_NAME desktop_launcher
|
||||
)
|
||||
|
||||
target_link_libraries(cardboy_desktop
|
||||
PRIVATE
|
||||
cardboy_apps
|
||||
cardboy_sdk
|
||||
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||
)
|
||||
|
||||
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||
33
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
33
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
@@ -0,0 +1,33 @@
|
||||
#include "cardboy/apps/clock_app.hpp"
|
||||
#include "cardboy/apps/gameboy_app.hpp"
|
||||
#include "cardboy/apps/menu_app.hpp"
|
||||
#include "cardboy/apps/tetris_app.hpp"
|
||||
#include "cardboy/backend/desktop_backend.hpp"
|
||||
#include "cardboy/sdk/app_system.hpp"
|
||||
|
||||
#include <cstdio>
|
||||
#include <exception>
|
||||
|
||||
using cardboy::backend::desktop::DesktopRuntime;
|
||||
|
||||
int main() {
|
||||
try {
|
||||
DesktopRuntime runtime;
|
||||
|
||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||
context.services = &runtime.serviceRegistry();
|
||||
cardboy::sdk::AppSystem system(context);
|
||||
|
||||
system.registerApp(apps::createMenuAppFactory());
|
||||
system.registerApp(apps::createClockAppFactory());
|
||||
system.registerApp(apps::createGameboyAppFactory());
|
||||
system.registerApp(apps::createTetrisAppFactory());
|
||||
|
||||
system.run();
|
||||
} catch (const std::exception& ex) {
|
||||
std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what());
|
||||
return 1;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
Reference in New Issue
Block a user