Compare commits

...

3 Commits

Author SHA1 Message Date
5b75ff28e0 independnet gameboy 2025-10-10 17:11:49 +02:00
e9e371739b kinda sdk 2025-10-10 16:03:23 +02:00
28411535bb tetris high score 2025-10-10 11:18:04 +02:00
56 changed files with 1857 additions and 7513 deletions

View File

@@ -2,4 +2,4 @@ idf_component_register()
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
target_link_libraries(${COMPONENT_LIB} INTERFACE cbsdk)
target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk)

View File

@@ -1,10 +1,9 @@
idf_component_register(SRCS
src/app_main.cpp
src/app_system.cpp
src/apps/menu_app.cpp
src/apps/clock_app.cpp
src/apps/tetris_app.cpp
src/apps/gameboy_app.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
@@ -16,8 +15,9 @@ idf_component_register(SRCS
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"
INCLUDE_DIRS "include" "../sdk/include"
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)
littlefs_create_partition_image(littlefs ../flash_data FLASH_IN_PROJECT)

View File

@@ -1,118 +1,28 @@
#pragma once
#include "app_platform.hpp"
#include "input_state.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/platform.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
class AppSystem;
using AppTimerHandle = std::uint32_t;
constexpr AppTimerHandle kInvalidAppTimer = 0;
enum class AppEventType {
Button,
Timer,
};
struct AppButtonEvent {
InputState current{};
InputState previous{};
};
struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppEvent {
AppEventType type;
std::uint32_t timestamp_ms = 0;
AppButtonEvent button{};
AppTimerEvent timer{};
};
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>
struct BasicAppContext {
using Framebuffer = FramebufferT;
using Input = InputT;
using Clock = ClockT;
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
BasicAppContext() = delete;
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
using AppContext = cardboy::sdk::AppContext;
FramebufferT& framebuffer;
InputT& input;
ClockT& clock;
AppSystem* system = nullptr;
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;
pendingAppName.clear();
pendingSwitchByName = false;
pendingSwitch = true;
}
void requestAppSwitchByName(std::string_view name) {
pendingAppName.assign(name.begin(), name.end());
pendingSwitchByName = true;
pendingSwitch = true;
}
bool hasPendingAppSwitch() const { return pendingSwitch; }
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat = false) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(delay_ms, repeat);
}
AppTimerHandle scheduleRepeatingTimer(uint32_t interval_ms) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(interval_ms, true);
}
void cancelTimer(AppTimerHandle handle) {
if (!system)
return;
cancelTimerInternal(handle);
}
void cancelAllTimers() {
if (!system)
return;
cancelAllTimersInternal();
}
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
AppTimerHandle scheduleTimerInternal(uint32_t delay_ms, bool repeat);
void cancelTimerInternal(AppTimerHandle handle);
void cancelAllTimersInternal();
};
using AppContext = BasicAppContext<PlatformFramebuffer, PlatformInput, PlatformClock>;
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {
public:
virtual ~IAppFactory() = default;
virtual const char* name() const = 0;
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
};
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;

View File

@@ -1,7 +1,8 @@
#pragma once
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/platform.hpp"
#include "config.hpp"
#include "input_state.hpp"
#include <buttons.hpp>
#include <disp_tools.hpp>
@@ -10,29 +11,33 @@
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
class PlatformFramebuffer {
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
public:
int width() const { return DISP_WIDTH; }
int height() const { return DISP_HEIGHT; }
int width() const override { return cardboy::sdk::kDisplayWidth; }
int height() const override { return cardboy::sdk::kDisplayHeight; }
void drawPixel(int x, int y, bool on) {
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) {
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 {
class PlatformInput final : public cardboy::sdk::IInput {
public:
InputState readState() {
InputState state{};
const uint8_t pressed = Buttons::get().get_pressed();
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)
@@ -53,12 +58,16 @@ public:
}
};
class PlatformClock {
class PlatformClock final : public cardboy::sdk::IClock {
public:
uint32_t millis() {
std::uint32_t millis() override {
TickType_t ticks = xTaskGetTickCount();
return static_cast<uint32_t>((static_cast<uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
}
void sleep_ms(uint32_t ms) { PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); }
void sleep_ms(std::uint32_t ms) override {
if (ms == 0)
return;
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));
}
};

View File

@@ -1,79 +1,5 @@
#pragma once
#include "app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
class AppSystem {
public:
explicit AppSystem(AppContext context);
void registerApp(std::unique_ptr<IAppFactory> factory);
bool startApp(const std::string& name);
bool startAppByIndex(std::size_t index);
void run();
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
template<typename FramebufferT, typename InputT, typename ClockT>
friend struct BasicAppContext;
struct TimerRecord {
AppTimerHandle id = kInvalidAppTimer;
std::uint32_t generation = 0;
std::uint32_t due_ms = 0;
std::uint32_t interval_ms = 0;
bool repeat = false;
bool active = false;
};
AppTimerHandle scheduleTimer(uint32_t delay_ms, bool repeat);
void cancelTimer(AppTimerHandle handle);
void cancelAllTimers();
void dispatchEvent(const AppEvent& event);
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(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) {
if (system)
system->cancelTimer(handle);
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
if (system)
system->cancelAllTimers();
}
using AppSystem = cardboy::sdk::AppSystem;

View File

@@ -1,12 +1,4 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<IAppFactory> createClockAppFactory();
}
#include "cardboy/apps/clock_app.hpp"

View File

@@ -1,15 +1,3 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kGameboyAppName[] = "Game Boy";
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
std::unique_ptr<IAppFactory> createGameboyAppFactory();
} // namespace apps
#include "cardboy/apps/gameboy_app.hpp"

View File

@@ -1,15 +1,3 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kMenuAppName[] = "Menu";
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
std::unique_ptr<IAppFactory> createMenuAppFactory();
} // namespace apps
#include "cardboy/apps/menu_app.hpp"

View File

@@ -1,11 +1,3 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<IAppFactory> createTetrisAppFactory();
}
#include "cardboy/apps/tetris_app.hpp"

View File

@@ -15,8 +15,10 @@
#define SPI_BUS SPI2_HOST
#define DISP_WIDTH 400
#define DISP_HEIGHT 240
#include "cardboy/sdk/display_spec.hpp"
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
#define BUZZER_PIN GPIO_NUM_25

View File

@@ -13,8 +13,6 @@
#include <array>
#include <bitset>
#include "Surface.hpp"
#include "Window.hpp"
namespace SMD {
static constexpr size_t kLineBytes = DISP_WIDTH / 8;

View File

@@ -1,12 +1,5 @@
#pragma once
struct InputState {
bool up = false;
bool left = false;
bool right = false;
bool down = false;
bool a = false;
bool b = false;
bool select = false;
bool start = false;
};
#include "cardboy/sdk/input_state.hpp"
using InputState = cardboy::sdk::InputState;

View File

@@ -3,11 +3,13 @@
#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>
@@ -16,6 +18,8 @@
#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>
@@ -26,6 +30,8 @@
#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"
@@ -36,8 +42,94 @@
#include <cstdio>
#include <cstring>
#include <string>
#include <string_view>
#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);
}
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{};
}
};
} // namespace
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
namespace {
@@ -217,8 +309,26 @@ extern "C" void app_main() {
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);
AppSystem system(context);
context.services = &services;
AppSystem system(context);
context.system = &system;
system.registerApp(apps::createMenuAppFactory());

File diff suppressed because it is too large Load Diff

View File

@@ -6,7 +6,7 @@
#include <disp_tools.hpp>
#include "Fonts.hpp"
#include "cardboy/gfx/Fonts.hpp"
void FbTty::draw_char(int col, int row) {
for (int x = 0; x < 8; x++) {

View File

@@ -1,11 +1,70 @@
cmake_minimum_required(VERSION 3.10)
project(sdk-top)
cmake_minimum_required(VERSION 3.16)
project(cardboy_sdk LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
add_subdirectory(library)
if (NOT CMAKE_CROSSCOMPILING)
add_subdirectory(sfml-port)
add_subdirectory(examples)
add_library(cardboy_sdk STATIC
src/app_system.cpp
)
target_include_directories(cardboy_sdk
PUBLIC
${CMAKE_CURRENT_SOURCE_DIR}/include
)
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)
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)
endif ()

View File

@@ -1,10 +1,10 @@
#include "apps/clock_app.hpp"
#include "cardboy/apps/clock_app.hpp"
#include "app_system.hpp"
#include "apps/menu_app.hpp"
#include "font16x8.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include <disp_tools.hpp>
#include "cardboy/gfx/font16x8.hpp"
#include <algorithm>
#include <cstdint>
@@ -17,6 +17,8 @@ namespace apps {
namespace {
using cardboy::sdk::AppContext;
constexpr const char* kClockAppName = "Clock";
using Framebuffer = typename AppContext::Framebuffer;
@@ -31,10 +33,10 @@ struct TimeSnapshot {
int month = 0;
int day = 0;
int weekday = 0;
uint64_t uptimeSeconds = 0;
std::uint64_t uptimeSeconds = 0;
};
class ClockApp final : public IApp {
class ClockApp final : public cardboy::sdk::IApp {
public:
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
@@ -50,12 +52,12 @@ public:
void onStop() override { cancelRefreshTimer(); }
void handleEvent(const AppEvent& event) override {
void handleEvent(const cardboy::sdk::AppEvent& event) override {
switch (event.type) {
case AppEventType::Button:
case cardboy::sdk::AppEventType::Button:
handleButtonEvent(event.button);
break;
case AppEventType::Timer:
case cardboy::sdk::AppEventType::Timer:
if (event.timer.handle == refreshTimer)
updateDisplay();
break;
@@ -69,18 +71,18 @@ private:
bool use24Hour = true;
bool dirty = false;
AppTimerHandle refreshTimer = kInvalidAppTimer;
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
TimeSnapshot lastSnapshot{};
void cancelRefreshTimer() {
if (refreshTimer != kInvalidAppTimer) {
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(refreshTimer);
refreshTimer = kInvalidAppTimer;
refreshTimer = cardboy::sdk::kInvalidAppTimer;
}
}
void handleButtonEvent(const AppButtonEvent& button) {
void handleButtonEvent(const cardboy::sdk::AppButtonEvent& button) {
const auto& current = button.current;
const auto& previous = button.previous;
@@ -113,8 +115,8 @@ private:
TimeSnapshot snap{};
snap.uptimeSeconds = clock.millis() / 1000ULL;
time_t raw = 0;
if (time(&raw) != static_cast<time_t>(-1) && raw > 0) {
std::time_t raw = 0;
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
std::tm tm{};
if (localtime_r(&raw, &tm) != nullptr) {
snap.hasWallTime = true;
@@ -158,7 +160,7 @@ private:
return;
dirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.beginFrame();
framebuffer.clear(false);
const int scaleLarge = 3;
@@ -198,11 +200,11 @@ private:
drawCenteredText(framebuffer, timeY + font16x8::kGlyphHeight * scaleLarge + 28, dateLine, scaleSmall, 1);
if (!snap.hasWallTime) {
char uptimeLine[32];
const uint64_t days = snap.uptimeSeconds / 86400ULL;
const uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
const uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
const uint64_t secs = snap.uptimeSeconds % 60ULL;
char uptimeLine[32];
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
const std::uint64_t secs = snap.uptimeSeconds % 60ULL;
if (days > 0) {
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
@@ -218,18 +220,20 @@ private:
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
DispTools::draw_to_display_async_start();
framebuffer.endFrame();
}
};
class ClockAppFactory final : public IAppFactory {
class ClockAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kClockAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<ClockApp>(context); }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<ClockApp>(context);
}
};
} // namespace
std::unique_ptr<IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory() { return std::make_unique<ClockAppFactory>(); }
} // namespace apps

View File

@@ -1,16 +1,11 @@
#pragma GCC optimize("Ofast")
#include "apps/gameboy_app.hpp"
#include "apps/peanut_gb.h"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/peanut_gb.h"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "font16x8.hpp"
#include <disp_tools.hpp>
#include <fs_helper.hpp>
#include "esp_timer.h"
#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>
@@ -26,6 +21,7 @@
#include <string_view>
#include <sys/stat.h>
#include <vector>
#include <chrono>
#define GAMEBOY_PERF_METRICS 0
@@ -45,16 +41,16 @@ namespace {
constexpr int kMenuStartY = 48;
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
using cardboy::sdk::kInvalidAppTimer;
using Framebuffer = typename AppContext::Framebuffer;
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
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[];
}
struct EmbeddedRomDescriptor {
std::string_view name;
std::string_view saveSlug;
@@ -62,6 +58,14 @@ struct EmbeddedRomDescriptor {
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",
@@ -74,6 +78,9 @@ static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
_binary_builtin_demo2_gb_start,
_binary_builtin_demo2_gb_end,
}}};
#else
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
#endif
struct RomEntry {
std::string name; // short display name
@@ -167,9 +174,10 @@ void drawTextRotated(Framebuffer& fb, int x, int y, std::string_view text, bool
}
}
class GameboyApp final : public IApp {
class GameboyApp final : public cardboy::sdk::IApp {
public:
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
explicit GameboyApp(AppContext& ctx) :
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
void onStart() override {
cancelTick();
@@ -197,9 +205,9 @@ public:
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
const uint64_t frameStartUs = esp_timer_get_time();
const uint64_t frameStartUs = nowMicros();
performStep();
const uint64_t frameEndUs = esp_timer_get_time();
const uint64_t frameEndUs = nowMicros();
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
scheduleAfterFrame(elapsedUs);
@@ -214,27 +222,27 @@ public:
void performStep() {
GB_PERF_ONLY(perf.resetForStep();)
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t inputStartUs = nowMicros();)
const InputState input = context.input.readState();
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
GB_PERF_ONLY(perf.inputUs = nowMicros() - inputStartUs;)
const Mode stepMode = mode;
switch (stepMode) {
case Mode::Browse: {
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handleBrowserInput(input);
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderBrowser();
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
case Mode::Running: {
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handleGameInput(input);
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
if (!gbReady) {
mode = Mode::Browse;
@@ -242,17 +250,21 @@ public:
break;
}
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
framebuffer.beginFrame();
framebuffer.clear(false);
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
ensureRenderGeometry();
GB_PERF_ONLY(perf.geometryUs = esp_timer_get_time() - geometryStartUs;)
GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;)
GB_PERF_ONLY(const uint64_t runStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();)
gb_run_frame(&gb);
GB_PERF_ONLY(perf.runUs = esp_timer_get_time() - runStartUs;)
GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderGameFrame();
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
framebuffer.endFrame();
break;
}
}
@@ -329,14 +341,14 @@ private:
}
void resetForStep() {
stepStartUs = esp_timer_get_time();
stepStartUs = clockMicros();
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
}
void finishStep() {
const uint64_t now = esp_timer_get_time();
const uint64_t now = clockMicros();
totalUs = now - stepStartUs;
lastStepEndUs = now;
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
@@ -418,7 +430,7 @@ private:
return;
if (!aggStartUs)
aggStartUs = stepStartUs;
const uint64_t now = lastStepEndUs ? lastStepEndUs : esp_timer_get_time();
const uint64_t now = lastStepEndUs ? lastStepEndUs : clockMicros();
const uint64_t span = now - aggStartUs;
if (!force && span < 1000000ULL)
return;
@@ -469,6 +481,12 @@ private:
aggCbLcdCalls = 0;
aggCbErrorCalls = 0;
}
static uint64_t clockMicros() {
const auto now = std::chrono::steady_clock::now();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
}
};
class ScopedCallbackTimer {
@@ -478,7 +496,7 @@ private:
if (instance) {
app = instance;
cbKind = kind;
startUs = esp_timer_get_time();
startUs = instance->nowMicros();
}
#else
(void) instance;
@@ -490,7 +508,7 @@ private:
#if GAMEBOY_PERF_METRICS
if (!app)
return;
const uint64_t end = esp_timer_get_time();
const uint64_t end = app->nowMicros();
app->perf.addCallback(cbKind, end - startUs);
#endif
}
@@ -520,6 +538,8 @@ private:
AppContext& context;
Framebuffer& framebuffer;
cardboy::sdk::IFilesystem* filesystem = nullptr;
cardboy::sdk::IHighResClock* highResClock = nullptr;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
@@ -584,9 +604,13 @@ private:
}
bool ensureFilesystemReady() {
esp_err_t err = FsHelper::get().mount();
if (err != ESP_OK) {
setStatus("LittleFS mount failed");
if (!filesystem) {
setStatus("Storage unavailable");
return false;
}
if (!filesystem->isMounted() && !filesystem->mount()) {
setStatus("Storage mount failed");
return false;
}
@@ -610,7 +634,9 @@ private:
}
[[nodiscard]] std::string romDirectory() const {
std::string result(FsHelper::get().basePath());
std::string result;
if (filesystem)
result = filesystem->basePath();
if (!result.empty() && result.back() != '/')
result.push_back('/');
result.append("roms");
@@ -631,7 +657,7 @@ private:
void refreshRomList() {
roms.clear();
bool fsMounted = FsHelper::get().isMounted();
bool fsMounted = filesystem ? filesystem->isMounted() : false;
std::string statusHint;
const auto updateStatusHintIfEmpty = [&](std::string value) {
if (statusHint.empty())
@@ -641,7 +667,7 @@ private:
if (!fsMounted) {
fsMounted = ensureFilesystemReady();
if (!fsMounted)
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
updateStatusHintIfEmpty("Built-in ROMs only (filesystem unavailable)");
}
if (fsMounted) {
@@ -676,9 +702,9 @@ private:
}
closedir(dir);
if (roms.empty())
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
updateStatusHintIfEmpty("Copy .gb/.gbc into ROMS/");
} else {
updateStatusHintIfEmpty("No /lfs/roms directory");
updateStatusHintIfEmpty("ROM directory missing");
}
}
@@ -823,7 +849,8 @@ private:
return;
browserDirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.beginFrame();
framebuffer.clear(false);
const std::string_view title = "GAME BOY";
const int titleWidth = font16x8::measureText(title, 2, 1);
@@ -832,7 +859,7 @@ private:
if (roms.empty()) {
font16x8::drawText(framebuffer, 24, kMenuStartY + 12, "NO ROMS FOUND", 1, true, 1);
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "/LFS/ROMS", 1, true, 1);
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "ADD FILES TO ROMS/", 1, true, 1);
} else {
const std::size_t visibleCount =
static_cast<std::size_t>(std::max(1, (framebuffer.height() - kMenuStartY - 64) / kMenuSpacing));
@@ -866,7 +893,7 @@ private:
font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1);
}
DispTools::draw_to_display_async_start();
framebuffer.endFrame();
}
bool loadRom(std::size_t index) {
@@ -948,7 +975,7 @@ private:
const uint_fast32_t saveSize = gb_get_save_size(&gb);
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
std::string savePath;
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
if (fsReady)
savePath = buildSavePath(rom, romDirectory());
activeRomSavePath = savePath;
@@ -1158,7 +1185,6 @@ private:
}
}
DispTools::draw_to_display_async_start();
}
void maybeSaveRam() {
@@ -1198,6 +1224,14 @@ private:
browserDirty = true;
}
[[nodiscard]] uint64_t nowMicros() const {
if (highResClock)
return highResClock->micros();
const auto now = std::chrono::steady_clock::now();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
}
void resetFpsStats() {
fpsLastSampleMs = 0;
fpsFrameCounter = 0;
@@ -1302,10 +1336,6 @@ private:
Framebuffer& fb = self->framebuffer;
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
DispTools::draw_to_display_async_wait();
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
@@ -1366,14 +1396,18 @@ private:
}
};
class GameboyAppFactory final : public IAppFactory {
class GameboyAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kGameboyAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<GameboyApp>(context); }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<GameboyApp>(context);
}
};
} // namespace
std::unique_ptr<IAppFactory> createGameboyAppFactory() { return std::make_unique<GameboyAppFactory>(); }
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() {
return std::make_unique<GameboyAppFactory>();
}
} // namespace apps

View File

@@ -1,9 +1,9 @@
#include "apps/menu_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "app_system.hpp"
#include "font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include <disp_tools.hpp>
#include "cardboy/gfx/font16x8.hpp"
#include <algorithm>
#include <cstdlib>
@@ -15,6 +15,8 @@ namespace apps {
namespace {
using cardboy::sdk::AppContext;
using Framebuffer = typename AppContext::Framebuffer;
struct MenuEntry {
@@ -22,7 +24,7 @@ struct MenuEntry {
std::size_t index = 0;
};
class MenuApp final : public IApp {
class MenuApp final : public cardboy::sdk::IApp {
public:
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
@@ -32,11 +34,11 @@ public:
renderIfNeeded();
}
void handleEvent(const AppEvent& event) override {
if (event.type != AppEventType::Button)
void handleEvent(const cardboy::sdk::AppEvent& event) override {
if (event.type != cardboy::sdk::AppEventType::Button)
return;
const auto& current = event.button.current;
const auto& current = event.button.current;
const auto& previous = event.button.previous;
if (current.left && !previous.left) {
@@ -85,7 +87,7 @@ private:
return;
const std::size_t total = context.system->appCount();
for (std::size_t i = 0; i < total; ++i) {
const IAppFactory* factory = context.system->factoryAt(i);
const cardboy::sdk::IAppFactory* factory = context.system->factoryAt(i);
if (!factory)
continue;
const char* name = factory->name();
@@ -133,7 +135,7 @@ private:
return;
dirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.beginFrame();
framebuffer.clear(false);
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
@@ -156,18 +158,20 @@ private:
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
}
DispTools::draw_to_display_async_start();
framebuffer.endFrame();
}
};
class MenuAppFactory final : public IAppFactory {
class MenuAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kMenuAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<MenuApp>(context); }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<MenuApp>(context);
}
};
} // namespace
std::unique_ptr<IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory() { return std::make_unique<MenuAppFactory>(); }
} // namespace apps

View File

@@ -0,0 +1,624 @@
#include "cardboy/apps/tetris_app.hpp"
#include "cardboy/apps/menu_app.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/input_state.hpp"
#include <algorithm>
#include <array>
#include <cstdint>
#include <cstdio>
#include <random>
#include <string>
#include <string_view>
#include <vector>
namespace apps {
namespace {
using cardboy::sdk::AppButtonEvent;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
constexpr char kTetrisAppName[] = "Tetris";
constexpr int kBoardWidth = 10;
constexpr int kBoardHeight = 20;
constexpr int kCellSize = 10;
constexpr std::array<int, 5> kLineScores = {0, 40, 100, 300, 1200};
struct BlockOffset {
int x = 0;
int y = 0;
};
struct Tetromino {
std::array<std::array<BlockOffset, 4>, 4> rotations{};
};
constexpr std::array<BlockOffset, 4> makeOffsets(std::initializer_list<BlockOffset> blocks) {
std::array<BlockOffset, 4> out{};
std::size_t idx = 0;
for (const auto& b: blocks) {
out[idx++] = b;
}
return out;
}
constexpr std::array<BlockOffset, 4> rotate(const std::array<BlockOffset, 4>& src) {
std::array<BlockOffset, 4> out{};
for (std::size_t i = 0; i < src.size(); ++i) {
out[i].x = -src[i].y;
out[i].y = src[i].x;
}
return out;
}
constexpr Tetromino makeTetromino(std::initializer_list<BlockOffset> baseBlocks) {
Tetromino tet{};
tet.rotations[0] = makeOffsets(baseBlocks);
for (int r = 1; r < 4; ++r)
tet.rotations[r] = rotate(tet.rotations[r - 1]);
return tet;
}
constexpr std::array<Tetromino, 7> kPieces = {{
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {2, 0}}), // I
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {1, 1}}), // J
makeTetromino({{-1, 1}, {-1, 0}, {0, 0}, {1, 0}}), // L
makeTetromino({{0, 0}, {1, 0}, {0, 1}, {1, 1}}), // O
makeTetromino({{-1, 0}, {0, 0}, {0, 1}, {1, 1}}), // S
makeTetromino({{-1, 0}, {0, 0}, {1, 0}, {0, 1}}), // T
makeTetromino({{-1, 1}, {0, 1}, {0, 0}, {1, 0}}), // Z
}};
class RandomBag {
public:
RandomBag() { refill(); }
void seed(std::uint32_t value) { rng.seed(value); }
int next() {
if (bag.empty())
refill();
int val = bag.back();
bag.pop_back();
return val;
}
private:
std::vector<int> bag;
std::mt19937 rng{std::random_device{}()};
void refill() {
bag.clear();
bag.reserve(7);
for (int i = 0; i < 7; ++i)
bag.push_back(i);
std::shuffle(bag.begin(), bag.end(), rng);
}
};
struct ActivePiece {
int type = 0;
int rotation = 0;
int x = 0;
int y = 0;
};
struct GameState {
std::array<int, kBoardWidth * kBoardHeight> board{};
ActivePiece current{};
int nextPiece = 0;
int level = 1;
int linesCleared = 0;
int score = 0;
int highScore = 0;
bool paused = false;
bool gameOver = false;
};
[[nodiscard]] std::uint32_t randomSeed(AppContext& ctx) {
if (auto* rnd = ctx.random())
return rnd->nextUint32();
static std::random_device rd;
return rd();
}
class TetrisGame {
public:
explicit TetrisGame(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {
bag.seed(randomSeed(context));
loadHighScore();
reset();
}
void onStart() {
scheduleDropTimer();
dirty = true;
renderIfNeeded();
}
void onStop() { cancelTimers(); }
void handleEvent(const AppEvent& event) {
switch (event.type) {
case AppEventType::Button:
handleButtons(event.button);
break;
case AppEventType::Timer:
handleTimer(event.timer.handle);
break;
}
renderIfNeeded();
}
private:
AppContext& context;
typename AppContext::Framebuffer& framebuffer;
GameState state;
RandomBag bag;
InputState lastInput{};
bool dirty = false;
AppTimerHandle dropTimer = cardboy::sdk::kInvalidAppTimer;
AppTimerHandle softTimer = cardboy::sdk::kInvalidAppTimer;
void reset() {
cancelTimers();
int oldHigh = state.highScore;
state = {};
state.highScore = oldHigh;
state.current.type = bag.next();
state.nextPiece = bag.next();
state.current.x = kBoardWidth / 2;
state.current.y = 0;
state.level = 1;
state.gameOver = false;
state.paused = false;
dirty = true;
scheduleDropTimer();
if (auto* power = context.powerManager())
power->setSlowMode(false);
}
void handleButtons(const AppButtonEvent& evt) {
const auto& cur = evt.current;
const auto& prev = evt.previous;
lastInput = cur;
if (cur.b && !prev.b) {
context.requestAppSwitchByName(kMenuAppName);
return;
}
if (cur.start && !prev.start) {
if (state.gameOver) {
reset();
} else {
state.paused = !state.paused;
if (auto* power = context.powerManager())
power->setSlowMode(state.paused);
}
dirty = true;
}
if (state.paused || state.gameOver)
return;
if (cur.left && !prev.left)
tryMove(-1, 0);
if (cur.right && !prev.right)
tryMove(1, 0);
if (cur.a && !prev.a)
rotate(1);
if (cur.select && !prev.select)
hardDrop();
if (cur.down && !prev.down) {
softDropStep();
scheduleSoftDropTimer();
} else if (!cur.down && prev.down) {
cancelSoftDropTimer();
}
}
void handleTimer(AppTimerHandle handle) {
if (handle == dropTimer) {
if (!state.paused && !state.gameOver)
gravityStep();
} else if (handle == softTimer) {
if (lastInput.down && !state.paused && !state.gameOver)
softDropStep();
else
cancelSoftDropTimer();
}
}
void cancelTimers() {
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(dropTimer);
dropTimer = cardboy::sdk::kInvalidAppTimer;
}
cancelSoftDropTimer();
}
void cancelSoftDropTimer() {
if (softTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(softTimer);
softTimer = cardboy::sdk::kInvalidAppTimer;
}
}
void scheduleDropTimer() {
cancelDropTimer();
const std::uint32_t interval = dropIntervalMs();
dropTimer = context.scheduleRepeatingTimer(interval);
}
void cancelDropTimer() {
if (dropTimer != cardboy::sdk::kInvalidAppTimer) {
context.cancelTimer(dropTimer);
dropTimer = cardboy::sdk::kInvalidAppTimer;
}
}
void scheduleSoftDropTimer() {
cancelSoftDropTimer();
softTimer = context.scheduleRepeatingTimer(60);
}
[[nodiscard]] std::uint32_t dropIntervalMs() const {
const int base = 700;
const int step = 50;
int interval = base - (state.level - 1) * step;
if (interval < 120)
interval = 120;
return static_cast<std::uint32_t>(interval);
}
[[nodiscard]] const Tetromino& currentPiece() const { return kPieces[state.current.type]; }
bool canPlace(int nx, int ny, int rot) const {
const auto& piece = kPieces[state.current.type];
rot = ((rot % 4) + 4) % 4;
for (const auto& block: piece.rotations[rot]) {
int gx = nx + block.x;
int gy = ny + block.y;
if (gx < 0 || gx >= kBoardWidth)
return false;
if (gy >= kBoardHeight)
return false;
if (gy >= 0 && cellAt(gx, gy) != 0)
return false;
}
return true;
}
[[nodiscard]] int cellAt(int x, int y) const { return state.board[y * kBoardWidth + x]; }
void setCell(int x, int y, int value) { state.board[y * kBoardWidth + x] = value; }
void tryMove(int dx, int dy) {
int nx = state.current.x + dx;
int ny = state.current.y + dy;
if (canPlace(nx, ny, state.current.rotation)) {
state.current.x = nx;
state.current.y = ny;
dirty = true;
if (dx != 0) {
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
}
}
}
void rotate(int direction) {
int nextRot = state.current.rotation + (direction >= 0 ? 1 : -1);
nextRot = ((nextRot % 4) + 4) % 4;
if (canPlace(state.current.x, state.current.y, nextRot)) {
state.current.rotation = nextRot;
dirty = true;
if (auto* buzzer = context.buzzer())
buzzer->beepRotate();
}
}
void gravityStep() {
if (!canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
lockPiece();
} else {
state.current.y++;
dirty = true;
}
}
void softDropStep() {
if (canPlace(state.current.x, state.current.y + 1, state.current.rotation)) {
state.current.y++;
state.score += 1;
updateHighScore();
dirty = true;
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
} else {
lockPiece();
}
}
void hardDrop() {
int distance = 0;
while (canPlace(state.current.x, state.current.y + distance + 1, state.current.rotation))
++distance;
if (distance > 0) {
state.current.y += distance;
state.score += distance * 2;
updateHighScore();
dirty = true;
if (auto* buzzer = context.buzzer())
buzzer->beepMove();
}
lockPiece();
}
void lockPiece() {
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
int gx = state.current.x + block.x;
int gy = state.current.y + block.y;
if (gy >= 0 && gy < kBoardHeight && gx >= 0 && gx < kBoardWidth)
setCell(gx, gy, state.current.type + 1);
if (gy < 0)
state.gameOver = true;
}
handleLineClear();
spawnNext();
dirty = true;
if (state.gameOver) {
cancelSoftDropTimer();
cancelDropTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepGameOver();
if (auto* power = context.powerManager())
power->setSlowMode(true);
} else {
if (auto* buzzer = context.buzzer())
buzzer->beepLock();
}
}
void handleLineClear() {
int cleared = 0;
for (int y = kBoardHeight - 1; y >= 0; --y) {
bool full = true;
for (int x = 0; x < kBoardWidth; ++x) {
if (cellAt(x, y) == 0) {
full = false;
break;
}
}
if (full) {
++cleared;
for (int pull = y; pull > 0; --pull)
for (int x = 0; x < kBoardWidth; ++x)
setCell(x, pull, cellAt(x, pull - 1));
for (int x = 0; x < kBoardWidth; ++x)
setCell(x, 0, 0);
++y; // re-check same row after collapse
}
}
if (cleared > 0) {
state.linesCleared += cleared;
if (cleared < static_cast<int>(kLineScores.size()))
state.score += kLineScores[cleared] * state.level;
else
state.score += kLineScores.back() * state.level;
int newLevel = 1 + state.linesCleared / 10;
if (newLevel != state.level) {
state.level = newLevel;
scheduleDropTimer();
if (auto* buzzer = context.buzzer())
buzzer->beepLevelUp(state.level);
}
updateHighScore();
if (auto* buzzer = context.buzzer())
buzzer->beepLines(cleared);
}
}
void spawnNext() {
state.current.type = state.nextPiece;
state.current.rotation = 0;
state.current.x = kBoardWidth / 2;
state.current.y = 0;
state.nextPiece = bag.next();
if (!canPlace(state.current.x, state.current.y, state.current.rotation))
state.gameOver = true;
}
void updateHighScore() {
if (state.score > state.highScore) {
state.highScore = state.score;
if (auto* storage = context.storage())
storage->writeUint32("tetris", "best", static_cast<std::uint32_t>(state.highScore));
}
}
void loadHighScore() {
if (auto* storage = context.storage()) {
std::uint32_t stored = 0;
if (storage->readUint32("tetris", "best", stored))
state.highScore = static_cast<int>(stored);
}
}
void renderIfNeeded() {
if (!dirty)
return;
dirty = false;
framebuffer.beginFrame();
framebuffer.clear(false);
drawBoard();
drawActivePiece();
drawNextPreview();
drawHUD();
framebuffer.endFrame();
}
void drawBoard() {
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
for (int y = 0; y < kBoardHeight; ++y) {
for (int x = 0; x < kBoardWidth; ++x) {
if (int value = cellAt(x, y); value != 0)
drawCell(originX, originY, x, y, value, true);
}
}
drawGuides(originX, originY);
}
void drawActivePiece() {
if (state.gameOver)
return;
const int originX = (cardboy::sdk::kDisplayWidth - kBoardWidth * kCellSize) / 2;
const int originY = (cardboy::sdk::kDisplayHeight - kBoardHeight * kCellSize) / 2;
for (const auto& block: currentPiece().rotations[state.current.rotation]) {
int gx = state.current.x + block.x;
int gy = state.current.y + block.y;
if (gy < 0)
continue;
drawCell(originX, originY, gx, gy, state.current.type + 1, false);
}
}
void drawCell(int originX, int originY, int cx, int cy, int value, bool solid) {
const int x0 = originX + cx * kCellSize;
const int y0 = originY + cy * kCellSize;
for (int dy = 0; dy < kCellSize; ++dy) {
for (int dx = 0; dx < kCellSize; ++dx) {
bool on = solid ? true : (dx == 0 || dx == kCellSize - 1 || dy == 0 || dy == kCellSize - 1);
framebuffer.drawPixel(x0 + dx, y0 + dy, on);
}
}
(void) value; // value currently unused (monochrome display)
}
void drawGuides(int originX, int originY) {
for (int y = 0; y <= kBoardHeight; ++y) {
const int py = originY + y * kCellSize;
for (int x = 0; x < kBoardWidth * kCellSize; ++x)
framebuffer.drawPixel(originX + x, py, (y % 5) == 0);
}
for (int x = 0; x <= kBoardWidth; ++x) {
const int px = originX + x * kCellSize;
for (int y = 0; y < kBoardHeight * kCellSize; ++y)
framebuffer.drawPixel(px, originY + y, (x % 5) == 0);
}
}
void drawNextPreview() {
const int blockSize = kCellSize;
const int boxSize = blockSize * 4;
const int originX = (cardboy::sdk::kDisplayWidth + kBoardWidth * kCellSize) / 2 + 24;
const int originY = (cardboy::sdk::kDisplayHeight - boxSize) / 2;
for (int dy = 0; dy < boxSize; ++dy)
for (int dx = 0; dx < boxSize; ++dx)
framebuffer.drawPixel(originX + dx, originY + dy, (dy == 0 || dy == boxSize - 1 || dx == 0 || dx == boxSize - 1));
const auto& piece = kPieces[state.nextPiece];
for (const auto& block: piece.rotations[0]) {
const int px = originX + (block.x + 1) * blockSize;
const int py = originY + (block.y + 1) * blockSize;
for (int dy = 1; dy < blockSize - 1; ++dy)
for (int dx = 1; dx < blockSize - 1; ++dx)
framebuffer.drawPixel(px + dx, py + dy, true);
}
}
void drawLabel(int x, int y, std::string_view text, int scale = 1) {
font16x8::drawText(framebuffer, x, y, text, scale, true, 1);
}
void drawHUD() {
const int margin = 16;
drawLabel(margin, margin, "SCORE", 1);
drawLabel(margin, margin + 16, std::to_string(state.score), 1);
drawLabel(margin, margin + 40, "BEST", 1);
drawLabel(margin, margin + 56, std::to_string(state.highScore), 1);
drawLabel(margin, margin + 80, "LEVEL", 1);
drawLabel(margin, margin + 96, std::to_string(state.level), 1);
if (auto* battery = context.battery(); battery && battery->hasData()) {
char line[32];
std::snprintf(line, sizeof(line), "BAT %.2fV", battery->voltage());
drawLabel(margin, margin + 120, line, 1);
}
drawLabel(margin, cardboy::sdk::kDisplayHeight - 48, "A ROTATE", 1);
drawLabel(margin, cardboy::sdk::kDisplayHeight - 32, "DOWN DROP", 1);
drawLabel(margin, cardboy::sdk::kDisplayHeight - 16, "B MENU", 1);
if (state.paused)
drawCenteredBanner("PAUSED");
else if (state.gameOver)
drawCenteredBanner("GAME OVER");
}
void drawCenteredBanner(std::string_view text) {
const int w = font16x8::measureText(text, 2, 1);
const int h = font16x8::kGlyphHeight * 2;
const int x = (cardboy::sdk::kDisplayWidth - w) / 2;
const int y = (cardboy::sdk::kDisplayHeight - h) / 2;
for (int yy = -4; yy < h + 4; ++yy)
for (int xx = -6; xx < w + 6; ++xx)
framebuffer.drawPixel(x + xx, y + yy, yy == -4 || yy == h + 3 || xx == -6 || xx == w + 5);
font16x8::drawText(framebuffer, x, y, text, 2, true, 1);
}
};
class TetrisApp final : public cardboy::sdk::IApp {
public:
explicit TetrisApp(AppContext& ctx) : game(ctx) {}
void onStart() override { game.onStart(); }
void onStop() override { game.onStop(); }
void handleEvent(const AppEvent& event) override { game.handleEvent(event); }
private:
TetrisGame game;
};
class TetrisFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kTetrisAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(AppContext& context) override {
return std::make_unique<TetrisApp>(context);
}
};
} // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory() {
return std::make_unique<TetrisFactory>();
}
} // namespace apps

View File

@@ -0,0 +1,397 @@
#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;
}

View File

@@ -0,0 +1,12 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<cardboy::sdk::IAppFactory> createClockAppFactory();
} // namespace apps

View File

@@ -0,0 +1,16 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kGameboyAppName[] = "Game Boy";
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
} // namespace apps

View File

@@ -0,0 +1,16 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kMenuAppName[] = "Menu";
inline constexpr std::string_view kMenuAppNameView = kMenuAppName;
std::unique_ptr<cardboy::sdk::IAppFactory> createMenuAppFactory();
} // namespace apps

View File

@@ -92,12 +92,12 @@
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
* pixel data. */
#ifndef PEANUT_GB_12_COLOUR
# define PEANUT_GB_12_COLOUR 1
# define PEANUT_GB_12_COLOUR 0
#endif
/* Adds more code to improve LCD rendering accuracy. */
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
# define PEANUT_GB_HIGH_LCD_ACCURACY 1
# define PEANUT_GB_HIGH_LCD_ACCURACY 0
#endif
/* Use intrinsic functions. This may produce smaller and faster code. */

View File

@@ -0,0 +1,12 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
namespace apps {
std::unique_ptr<cardboy::sdk::IAppFactory> createTetrisAppFactory();
} // namespace apps

View File

@@ -1,7 +1,6 @@
#pragma once
#include "Fonts.hpp"
#include "app_framework.hpp"
#include "cardboy/gfx/Fonts.hpp"
#include <array>
#include <cctype>

View File

@@ -0,0 +1,133 @@
#pragma once
#include "platform.hpp"
#include "services.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <string_view>
#include <vector>
namespace cardboy::sdk {
class AppSystem;
using AppTimerHandle = std::uint32_t;
constexpr AppTimerHandle kInvalidAppTimer = 0;
enum class AppEventType {
Button,
Timer,
};
struct AppButtonEvent {
InputState current{};
InputState previous{};
};
struct AppTimerEvent {
AppTimerHandle handle = kInvalidAppTimer;
};
struct AppEvent {
AppEventType type;
std::uint32_t timestamp_ms = 0;
AppButtonEvent button{};
AppTimerEvent timer{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
struct BasicAppContext {
using Framebuffer = FramebufferT;
using Input = InputT;
using Clock = ClockT;
BasicAppContext() = delete;
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
FramebufferT& framebuffer;
InputT& input;
ClockT& clock;
AppSystem* system = nullptr;
Services* services = nullptr;
[[nodiscard]] Services* getServices() const { return services; }
[[nodiscard]] IBuzzer* buzzer() const { return services ? services->buzzer : nullptr; }
[[nodiscard]] IBatteryMonitor* battery() const { return services ? services->battery : nullptr; }
[[nodiscard]] IStorage* storage() const { return services ? services->storage : nullptr; }
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;
pendingAppName.clear();
pendingSwitchByName = false;
pendingSwitch = true;
}
void requestAppSwitchByName(std::string_view name) {
pendingAppName.assign(name.begin(), name.end());
pendingSwitchByName = true;
pendingSwitch = true;
}
[[nodiscard]] bool hasPendingAppSwitch() const { return pendingSwitch; }
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat = false) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(delay_ms, repeat);
}
AppTimerHandle scheduleRepeatingTimer(std::uint32_t interval_ms) {
if (!system)
return kInvalidAppTimer;
return scheduleTimerInternal(interval_ms, true);
}
void cancelTimer(AppTimerHandle handle) {
if (!system)
return;
cancelTimerInternal(handle);
}
void cancelAllTimers() {
if (!system)
return;
cancelAllTimersInternal();
}
private:
friend class AppSystem;
bool pendingSwitch = false;
bool pendingSwitchByName = false;
std::size_t pendingAppIndex = 0;
std::string pendingAppName;
AppTimerHandle scheduleTimerInternal(std::uint32_t delay_ms, bool repeat);
void cancelTimerInternal(AppTimerHandle handle);
void cancelAllTimersInternal();
};
using AppContext = BasicAppContext<IFramebuffer, IInput, IClock>;
class IApp {
public:
virtual ~IApp() = default;
virtual void onStart() {}
virtual void onStop() {}
virtual void handleEvent(const AppEvent& event) = 0;
};
class IAppFactory {
public:
virtual ~IAppFactory() = default;
virtual const char* name() const = 0;
virtual std::unique_ptr<IApp> create(AppContext& context) = 0;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,85 @@
#pragma once
#include "app_framework.hpp"
#include <cstdint>
#include <memory>
#include <string>
#include <vector>
namespace cardboy::sdk {
class AppSystem {
public:
explicit AppSystem(AppContext context);
void registerApp(std::unique_ptr<IAppFactory> factory);
bool startApp(const std::string& name);
bool startAppByIndex(std::size_t index);
void run();
[[nodiscard]] std::size_t appCount() const { return factories.size(); }
[[nodiscard]] const IAppFactory* factoryAt(std::size_t index) const;
[[nodiscard]] std::size_t indexOfFactory(const IAppFactory* factory) const;
[[nodiscard]] std::size_t currentFactoryIndex() const { return activeIndex; }
[[nodiscard]] const IApp* currentApp() const { return current.get(); }
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private:
template<typename FramebufferT, typename InputT, typename ClockT>
friend struct BasicAppContext;
struct TimerRecord {
AppTimerHandle id = kInvalidAppTimer;
std::uint32_t generation = 0;
std::uint32_t due_ms = 0;
std::uint32_t interval_ms = 0;
bool repeat = false;
bool active = false;
};
AppTimerHandle scheduleTimer(std::uint32_t delay_ms, bool repeat);
void cancelTimer(AppTimerHandle handle);
void cancelAllTimers();
void dispatchEvent(const AppEvent& event);
void processDueTimers(std::uint32_t now, std::vector<AppEvent>& outEvents);
std::uint32_t nextTimerDueMs(std::uint32_t now) const;
void clearTimersForCurrentApp();
TimerRecord* findTimer(AppTimerHandle handle);
bool handlePendingSwitchRequest();
AppContext context;
std::vector<std::unique_ptr<IAppFactory>> factories;
std::unique_ptr<IApp> current;
IAppFactory* activeFactory = nullptr;
std::size_t activeIndex = static_cast<std::size_t>(-1);
std::vector<TimerRecord> timers;
AppTimerHandle nextTimerId = 1;
std::uint32_t currentGeneration = 0;
InputState lastInputState{};
};
template<typename FramebufferT, typename InputT, typename ClockT>
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::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) {
if (system)
system->cancelTimer(handle);
}
template<typename FramebufferT, typename InputT, typename ClockT>
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
if (system)
system->cancelAllTimers();
}
} // namespace cardboy::sdk

View File

@@ -0,0 +1,9 @@
#pragma once
namespace cardboy::sdk {
inline constexpr int kDisplayWidth = 400;
inline constexpr int kDisplayHeight = 240;
} // namespace cardboy::sdk

View File

@@ -0,0 +1,17 @@
#pragma once
namespace cardboy::sdk {
struct InputState {
bool up = false;
bool left = false;
bool right = false;
bool down = false;
bool a = false;
bool b = false;
bool select = false;
bool start = false;
};
} // namespace cardboy::sdk

View File

@@ -0,0 +1,40 @@
#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

View File

@@ -0,0 +1,87 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
namespace cardboy::sdk {
class IBuzzer {
public:
virtual ~IBuzzer() = default;
virtual void init() {}
virtual void tone(std::uint32_t freq, std::uint32_t duration_ms, std::uint32_t gap_ms = 0) = 0;
virtual void beepRotate() {}
virtual void beepMove() {}
virtual void beepLock() {}
virtual void beepLines(int /*lines*/) {}
virtual void beepLevelUp(int /*level*/) {}
virtual void beepGameOver() {}
virtual void setMuted(bool /*muted*/) {}
virtual void toggleMuted() {}
[[nodiscard]] virtual bool isMuted() const { return false; }
};
class IBatteryMonitor {
public:
virtual ~IBatteryMonitor() = default;
[[nodiscard]] virtual bool hasData() const { return false; }
[[nodiscard]] virtual float voltage() const { return 0.0f; }
[[nodiscard]] virtual float charge() const { return 0.0f; }
[[nodiscard]] virtual float current() const { return 0.0f; }
};
class IStorage {
public:
virtual ~IStorage() = default;
[[nodiscard]] virtual bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) = 0;
virtual void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) = 0;
};
class IRandom {
public:
virtual ~IRandom() = default;
[[nodiscard]] virtual std::uint32_t nextUint32() = 0;
};
class IHighResClock {
public:
virtual ~IHighResClock() = default;
[[nodiscard]] virtual std::uint64_t micros() = 0;
};
class IPowerManager {
public:
virtual ~IPowerManager() = default;
virtual void setSlowMode(bool enable) = 0;
[[nodiscard]] virtual bool isSlowMode() const = 0;
};
class IFilesystem {
public:
virtual ~IFilesystem() = default;
virtual bool mount() = 0;
[[nodiscard]] virtual bool isMounted() const = 0;
[[nodiscard]] virtual std::string basePath() const = 0;
};
struct Services {
IBuzzer* buzzer = nullptr;
IBatteryMonitor* battery = nullptr;
IStorage* storage = nullptr;
IRandom* random = nullptr;
IHighResClock* highResClock = nullptr;
IPowerManager* powerManager = nullptr;
IFilesystem* filesystem = nullptr;
};
} // namespace cardboy::sdk

View File

@@ -1,22 +0,0 @@
cmake_minimum_required(VERSION 3.10)
add_library(cbsdk
src/Window.cpp
include_public/Window.hpp
include_public/Pixel.hpp
src/Event.cpp
include_public/Event.hpp
include_public/StandardEvents.hpp
include_public/Surface.hpp
include_public/Fonts.hpp
src/TextWindow.cpp
include_public/TextWindow.hpp
include_public/utils.hpp
include_public/SubSurface.hpp)
target_include_directories(cbsdk PUBLIC include_public)
target_include_directories(cbsdk PRIVATE include)
if (NOT CMAKE_CROSSCOMPILING)
add_subdirectory(test)
endif ()

View File

@@ -1,139 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef EVENT_HPP
#define EVENT_HPP
#include <algorithm>
#include <concepts>
#include <condition_variable>
#include <list>
#include <mutex>
#include <optional>
#include <type_traits>
#include <variant>
#include <functional>
enum class EventHandlingResult { DONE, IGNORE, CONTINUE };
class Event {};
struct LoopQuitEvent : public Event {};
template<typename T>
concept IsEvent = std::is_base_of_v<Event, T>;
template<typename H, typename E>
concept HasHandleFor = requires(H h, E e) {
{ h.handle(e) } -> std::same_as<EventHandlingResult>;
};
template<typename H, typename... Ts>
concept HandlesAll = (HasHandleFor<H, Ts> && ...);
template<typename Derived, typename... T>
requires(IsEvent<T> && ...)
class EventHandler {
public:
EventHandler() { static_assert(HandlesAll<Derived, T...>); }
};
class EventLoop;
class EventQueueBase {
public:
virtual void process_events() = 0;
virtual ~EventQueueBase() = default;
};
template<typename HandlerType, typename... Ts>
class EventQueue : public EventQueueBase {
public:
EventQueue(EventLoop* loop, HandlerType* handler) : _loop(loop), _handler(handler) {};
std::optional<std::variant<Ts...>> poll();
void process_events() override {
while (auto event = poll()) {
std::visit([this](auto&& e) { _handler->handle(e); }, *event);
}
}
template<typename T>
requires std::disjunction_v<std::is_same<T, Ts>...>
void push(T&& event);
private:
EventLoop* _loop;
HandlerType* _handler;
std::list<std::variant<Ts...>> _events;
};
class EventLoop : EventHandler<EventLoop, LoopQuitEvent>, public EventQueue<EventLoop, LoopQuitEvent> {
public:
EventLoop() : EventQueue<EventLoop, LoopQuitEvent>(this, this) {}
template<typename... Ts>
void notify_pending(EventQueue<Ts...>* queue) {
std::lock_guard<std::mutex> lock(_mutex);
// TODO:
if (std::find(_events.begin(), _events.end(), queue) != _events.end()) {
return; // Already registered
}
_events.push_back(queue);
_condition.notify_all();
}
void run(std::function<void()> after_callback) {
while (_running) {
std::list<EventQueueBase*> new_events;
{
std::unique_lock<std::mutex> lock(_mutex);
_condition.wait(lock, [this] { return !_events.empty() || !_running; });
std::swap(new_events, _events);
}
for (auto queue: new_events) {
queue->process_events();
}
after_callback();
}
}
EventHandlingResult handle(LoopQuitEvent event) {
_running = false;
_condition.notify_all();
return EventHandlingResult::DONE;
}
private:
std::list<EventQueueBase*> _events;
std::mutex _mutex;
std::condition_variable _condition;
bool _running = true;
};
template<typename HandlerType, typename... Ts>
std::optional<std::variant<Ts...>> EventQueue<HandlerType, Ts...>::poll() {
if (_events.empty()) {
return std::nullopt;
}
auto event = std::move(_events.front());
_events.pop_front();
return event;
}
template<typename HandlerType, typename... Ts>
template<typename T>
requires std::disjunction_v<std::is_same<T, Ts>...>
void EventQueue<HandlerType, Ts...>::push(T&& event) {
_events.emplace_back(std::forward<T>(event));
_loop->notify_pending(static_cast<HandlerType*>(this));
}
#endif // EVENT_HPP

File diff suppressed because it is too large Load Diff

View File

@@ -1,128 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef GRIDWINDOW_HPP
#define GRIDWINDOW_HPP
#include <string>
#include "Fonts.hpp"
#include "SubSurface.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename SurfaceType, unsigned nWidth, unsigned nHeight>
class GridWindow : public Window<SurfaceType> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit GridWindow(SurfaceType* owner) : Window<SurfaceType>(owner) {
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
_grid[i][j].emplace(owner);
}
}
}
EventHandlingResult handle_v(KeyboardEvent keyboardEvent) override {
if (keyboardEvent.key_code == Key::Escape) {
if (!_has_focus) {
return EventHandlingResult::CONTINUE;
} else {
auto res = _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
if (res == EventHandlingResult::DONE) {
return EventHandlingResult::DONE;
} else {
_has_focus = false;
}
}
} else if (keyboardEvent.key_code == Key::Enter) {
if (!_has_focus) {
_has_focus = true;
} else {
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
}
} else {
if (_has_focus) {
return _grid[_current_focus_x][_current_focus_y]->get_window()->handle(keyboardEvent);
}
if (keyboardEvent.key_code == Key::Left) {
if (_current_focus_x > 0) {
_current_focus_x--;
}
} else if (keyboardEvent.key_code == Key::Right) {
if (_current_focus_x < nWidth - 1) {
_current_focus_x++;
}
} else if (keyboardEvent.key_code == Key::Up) {
if (_current_focus_y > 0) {
_current_focus_y--;
}
} else if (keyboardEvent.key_code == Key::Down) {
if (_current_focus_y < nHeight - 1) {
_current_focus_y++;
}
}
}
refresh();
return EventHandlingResult::DONE;
}
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
_cell_width = this->_owner->get_width() / nWidth;
_cell_height = this->_owner->get_height() / nHeight;
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
if constexpr (is_specialization_of<SubSurface, SurfaceType>::value) {
_grid[i][j]->set_pos(this->_owner->get_x_offset() + i * _cell_width + 1,
this->_owner->get_y_offset() + j * _cell_height + 1, _cell_width - 2,
_cell_height - 2);
} else {
_grid[i][j]->set_pos(i * _cell_width + 1, j * _cell_height + 1, _cell_width - 2, _cell_height - 2);
}
}
}
refresh();
return EventHandlingResult::DONE;
}
template<typename WindowType, typename... Args>
void set_window(unsigned x, unsigned y, Args&&... args) {
_grid[x][y]->template set_window<WindowType>(std::forward<Args>(args)...);
}
SubSurface<SurfaceType>& get_subsurface(unsigned x, unsigned y) {
// assert(x >= nWidth && y >= nHeight);
return *_grid[x][y];
}
void refresh() {
for (int i = 0; i < nWidth; ++i) {
for (int j = 0; j < nHeight; ++j) {
if (i == _current_focus_x && j == _current_focus_y) {
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
PixelType(true));
} else {
this->_owner->draw_rect(i * _cell_width, j * _cell_height, _cell_width, _cell_height,
PixelType(false));
}
}
}
}
private:
using SubType = std::conditional_t<is_specialization_of<SubSurface, SurfaceType>::value, SurfaceType,
SubSurface<SurfaceType>>;
std::array<std::array<std::optional<SubType>, nWidth>, nHeight> _grid;
unsigned _cell_width = 0;
unsigned _cell_height = 0;
unsigned _current_focus_x = 0;
unsigned _current_focus_y = 0;
bool _has_focus = false;
};
#endif // TEXTWINDOW_HPP

View File

@@ -1,21 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef PIXEL_HPP
#define PIXEL_HPP
class Pixel {
};
struct BwPixel : public Pixel {
bool on = false;
BwPixel() = default;
BwPixel(bool on) : on(on) {}
bool operator==(const BwPixel& other) const { return on == other.on; }
bool operator!=(const BwPixel& other) const { return !(*this == other); }
};
#endif //PIXEL_HPP

View File

@@ -1,163 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef STANDARDEVENTS_HPP
#define STANDARDEVENTS_HPP
#include "Event.hpp"
// TODO: rewrite this
////////////////////////////////////////////////////////////
//
// SFML - Simple and Fast Multimedia Library
// Copyright (C) 2007-2025 Laurent Gomila (laurent@sfml-dev.org)
//
// This software is provided 'as-is', without any express or implied warranty.
// In no event will the authors be held liable for any damages arising from the use of this software.
//
// Permission is granted to anyone to use this software for any purpose,
// including commercial applications, and to alter it and redistribute it freely,
// subject to the following restrictions:
//
// 1. The origin of this software must not be misrepresented;
// you must not claim that you wrote the original software.
// If you use this software in a product, an acknowledgment
// in the product documentation would be appreciated but is not required.
//
// 2. Altered source versions must be plainly marked as such,
// and must not be misrepresented as being the original software.
//
// 3. This notice may not be removed or altered from any source distribution.
//
////////////////////////////////////////////////////////////
enum class Key {
Unknown = -1, //!< Unhandled key
A = 0, //!< The A key
B, //!< The B key
C, //!< The C key
D, //!< The D key
E, //!< The E key
F, //!< The F key
G, //!< The G key
H, //!< The H key
I, //!< The I key
J, //!< The J key
K, //!< The K key
L, //!< The L key
M, //!< The M key
N, //!< The N key
O, //!< The O key
P, //!< The P key
Q, //!< The Q key
R, //!< The R key
S, //!< The S key
T, //!< The T key
U, //!< The U key
V, //!< The V key
W, //!< The W key
X, //!< The X key
Y, //!< The Y key
Z, //!< The Z key
Num0, //!< The 0 key
Num1, //!< The 1 key
Num2, //!< The 2 key
Num3, //!< The 3 key
Num4, //!< The 4 key
Num5, //!< The 5 key
Num6, //!< The 6 key
Num7, //!< The 7 key
Num8, //!< The 8 key
Num9, //!< The 9 key
Escape, //!< The Escape key
LControl, //!< The left Control key
LShift, //!< The left Shift key
LAlt, //!< The left Alt key
LSystem, //!< The left OS specific key: window (Windows and Linux), apple (macOS), ...
RControl, //!< The right Control key
RShift, //!< The right Shift key
RAlt, //!< The right Alt key
RSystem, //!< The right OS specific key: window (Windows and Linux), apple (macOS), ...
Menu, //!< The Menu key
LBracket, //!< The [ key
RBracket, //!< The ] key
Semicolon, //!< The ; key
Comma, //!< The , key
Period, //!< The . key
Apostrophe, //!< The ' key
Slash, //!< The / key
Backslash, //!< The \ key
Grave, //!< The ` key
Equal, //!< The = key
Hyphen, //!< The - key (hyphen)
Space, //!< The Space key
Enter, //!< The Enter/Return keys
Backspace, //!< The Backspace key
Tab, //!< The Tabulation key
PageUp, //!< The Page up key
PageDown, //!< The Page down key
End, //!< The End key
Home, //!< The Home key
Insert, //!< The Insert key
Delete, //!< The Delete key
Add, //!< The + key
Subtract, //!< The - key (minus, usually from numpad)
Multiply, //!< The * key
Divide, //!< The / key
Left, //!< Left arrow
Right, //!< Right arrow
Up, //!< Up arrow
Down, //!< Down arrow
Numpad0, //!< The numpad 0 key
Numpad1, //!< The numpad 1 key
Numpad2, //!< The numpad 2 key
Numpad3, //!< The numpad 3 key
Numpad4, //!< The numpad 4 key
Numpad5, //!< The numpad 5 key
Numpad6, //!< The numpad 6 key
Numpad7, //!< The numpad 7 key
Numpad8, //!< The numpad 8 key
Numpad9, //!< The numpad 9 key
F1, //!< The F1 key
F2, //!< The F2 key
F3, //!< The F3 key
F4, //!< The F4 key
F5, //!< The F5 key
F6, //!< The F6 key
F7, //!< The F7 key
F8, //!< The F8 key
F9, //!< The F9 key
F10, //!< The F10 key
F11, //!< The F11 key
F12, //!< The F12 key
F13, //!< The F13 key
F14, //!< The F14 key
F15, //!< The F15 key
Pause, //!< The Pause key
};
struct KeyboardEvent : public Event {
KeyboardEvent(Key key_code) : key_code(key_code) {}
Key key_code;
};
struct SurfaceEvent : public Event {
enum class EventType { CLOSED, OPENED };
EventType type;
};
struct SurfaceResizeEvent : public Event {
SurfaceResizeEvent(unsigned int width, unsigned int height) : width(width), height(height) {}
unsigned width;
unsigned height;
};
template<typename Derived>
using StandardEventHandler = EventHandler<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
template<typename Derived>
using StandardEventQueue = EventQueue<Derived, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>;
#endif // STANDARDEVENTS_HPP

View File

@@ -1,58 +0,0 @@
//
// Created by Stepan Usatiuk on 27.07.2025.
//
#ifndef SUBSURFACE_HPP
#define SUBSURFACE_HPP
#include <memory>
#include <type_traits>
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "Surface.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename SurfaceParent>
class SubSurface : public Surface<SubSurface<SurfaceParent>, typename SurfaceParent::PixelType> {
public:
using PixelType = typename SurfaceParent::PixelType;
SubSurface(SurfaceParent* parent) : _parent(parent) {}
SubSurface(SubSurface<SurfaceParent>* parent) : _parent(parent->_parent) {}
void draw_pixel_impl(unsigned x, unsigned y, const PixelType& pixel) {
if (x >= _x_size || y >= _y_size) {
assert(false);
}
_parent->draw_pixel(x + _x_offset, y + _y_offset, pixel);
}
unsigned get_x_offset() const { return _x_offset; }
unsigned get_y_offset() const { return _y_offset; }
unsigned get_width_impl() const { return _x_size; }
unsigned get_height_impl() const { return _y_size; }
void set_pos(unsigned x_offset, unsigned y_offset, unsigned x_size, unsigned y_size) {
_x_offset = x_offset;
_y_offset = y_offset;
_x_size = x_size;
_y_size = y_size;
this->handle(SurfaceResizeEvent(x_size, y_size));
}
private:
unsigned _x_offset = 0;
unsigned _y_offset = 0;
unsigned _x_size = 0;
unsigned _y_size = 0;
SurfaceParent* _parent;
};
#endif // SUBSURFACE_HPP

View File

@@ -1,81 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef SURFACE_HPP
#define SURFACE_HPP
#include <cassert>
#include <memory>
#include <type_traits>
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename Derived, typename PixelType>
requires std::is_base_of_v<Pixel, PixelType>
class Surface : public StandardEventHandler<Derived> {
public:
Surface() { static_assert(std::is_same_v<PixelType, typename Derived::PixelType>); }
void draw_pixel(unsigned x, unsigned y, const BwPixel& pixel) {
static_cast<Derived*>(this)->draw_pixel_impl(x, y, pixel);
}
void draw_rect(unsigned x, unsigned y, unsigned width, unsigned height, const BwPixel& pixel) {
for (unsigned i = 0; i < width; ++i) {
draw_pixel(x + i, y, pixel);
draw_pixel(x + i, y + height - 1, pixel);
}
for (unsigned i = 0; i < height; ++i) {
draw_pixel(x, y + i, pixel);
draw_pixel(x + width - 1, y + i, pixel);
}
}
void clear() {
for (unsigned x = 0; x < get_width(); x++) {
for (unsigned y = 0; y < get_height(); y++) {
draw_pixel(x, y, PixelType());
}
}
}
int get_width() const { return static_cast<const Derived*>(this)->get_width_impl(); }
int get_height() const { return static_cast<const Derived*>(this)->get_height_impl(); }
template<typename T>
EventHandlingResult handle(const T& event) {
if (_window.get())
return _window->handle(event);
return EventHandlingResult::CONTINUE;
}
template<typename WindowType, typename... Args>
void set_window(Args&&... args) {
_window = std::make_unique<WindowType>(static_cast<Derived*>(this), std::forward<Args>(args)...);
}
Surface(const Surface& other) = delete;
Surface(Surface&& other) noexcept = delete;
Surface& operator=(const Surface& other) = delete;
Surface& operator=(Surface&& other) noexcept = delete;
bool has_window() const { return _window != nullptr; }
Window<Derived>* get_window() {
assert(has_window());
return _window.get();
}
protected:
std::unique_ptr<Window<Derived>> _window = nullptr;
};
#endif // SURFACE_HPP

View File

@@ -1,72 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef TEXTWINDOW_HPP
#define TEXTWINDOW_HPP
#include <string>
#include "Fonts.hpp"
#include "Window.hpp"
#include "utils.hpp"
template<typename StringType>
struct TextUpdateEvent : public Event {
TextUpdateEvent(StringType text) : new_text(std::move(text)) {}
StringType new_text;
};
template<typename SurfaceType, typename StringType>
class TextWindow : public Window<SurfaceType>,
public EventHandler<TextUpdateEvent<StringType>>,
public EventQueue<TextWindow<SurfaceType, StringType>, TextUpdateEvent<StringType>> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit TextWindow(SurfaceType* owner, EventLoop* loop, StringType text = "") :
Window<SurfaceType>(owner), EventQueue<TextWindow, TextUpdateEvent<StringType>>(loop, this),
_text(std::move(text)) {}
EventHandlingResult handle_v(SurfaceResizeEvent resize) override {
refresh();
return EventHandlingResult::DONE;
}
EventHandlingResult handle(TextUpdateEvent<StringType> event) {
_text = std::move(event.new_text);
refresh();
return EventHandlingResult::DONE;
}
void refresh() {
this->_owner->clear();
size_t _max_col = this->_owner->get_width() / 8;
size_t _max_row = this->_owner->get_height() / 16;
int col = 0, row = 0;
for (char c: _text) {
if (c == '\n' || col >= _max_col) {
row++;
col = 0;
if (c == '\n')
continue;
}
if (row >= _max_row) {
break;
}
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 16; y++) {
bool color = fonts_Terminess_Powerline[c][y] & (1 << (8 - x));
this->_owner->draw_pixel(col * 8 + x, row * 16 + y, PixelType(color));
}
}
col++;
}
}
private:
StringType _text;
};
#endif // TEXTWINDOW_HPP

View File

@@ -1,40 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef WINDOW_HPP
#define WINDOW_HPP
#include <type_traits>
#include "Event.hpp"
#include "Pixel.hpp"
#include "StandardEvents.hpp"
#include "utils.hpp"
template<typename Derived, typename PixelType>
requires std::is_base_of_v<Pixel, PixelType>
class Surface;
template<typename SurfaceType>
class Window : StandardEventHandler<Window<SurfaceType>> {
public:
using PixelType = typename SurfaceType::PixelType;
explicit Window(SurfaceType* owner) : _owner(owner) {
// static_assert(is_specialization_of<Surface, SurfaceType>::value);
}
virtual ~Window() = default;
EventHandlingResult handle(auto Event) { return handle_v(Event); }
virtual EventHandlingResult handle_v(KeyboardEvent) { return EventHandlingResult::CONTINUE; }
virtual EventHandlingResult handle_v(SurfaceEvent) { return EventHandlingResult::CONTINUE; }
virtual EventHandlingResult handle_v(SurfaceResizeEvent) { return EventHandlingResult::CONTINUE; }
protected:
SurfaceType* _owner = nullptr;
};
#endif // SURFACE_HPP

View File

@@ -1,14 +0,0 @@
//
// Created by Stepan Usatiuk on 27.07.2025.
//
#ifndef UTILS_HPP
#define UTILS_HPP
template <template <typename...> class T, typename U>
struct is_specialization_of: std::false_type {};
template <template <typename...> class T, typename... Us>
struct is_specialization_of<T, T<Us...>>: std::true_type {};
#endif //UTILS_HPP

View File

@@ -1,5 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "Event.hpp"

View File

@@ -1,8 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "TextWindow.hpp"
#include "Fonts.hpp"
#include "Surface.hpp"

View File

@@ -1,5 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "Window.hpp"

View File

@@ -1,23 +0,0 @@
include(FetchContent)
FetchContent_Declare(
googletest
URL https://github.com/google/googletest/archive/03597a01ee50ed33e9dfd640b249b4be3799d395.zip
)
# For Windows: Prevent overriding the parent project's compiler/linker settings
set(gtest_force_shared_crt ON CACHE BOOL "" FORCE)
FetchContent_MakeAvailable(googletest)
enable_testing()
include(GoogleTest)
add_executable(
EventTests
src/EventTests.cpp
)
target_link_libraries(
EventTests PRIVATE
GTest::gtest_main cbsdk
)
gtest_discover_tests(EventTests DISCOVERY_TIMEOUT 600)

View File

@@ -1,44 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include <gtest/gtest.h>
#include "Event.hpp"
struct EventOne : public Event {
std::string name;
};
struct EventTwo : public Event {
int value;
};
template<typename Derived>
using TestEventHandler = EventHandler<Derived, EventOne, EventTwo>;
class EventHandlerTest : public TestEventHandler<EventHandlerTest> {
public:
template<typename T>
void handle(const T& event) {
seen_unknown = true;
}
void handle(const EventOne& event) {
seen_event_one = true;
}
bool seen_event_one = false;
bool seen_unknown = false;
};
TEST(Event, Basic) {
EventHandlerTest handler;
EventOne event_one;
EventTwo event_two;
handler.handle(event_one);
ASSERT_TRUE(handler.seen_event_one);
handler.handle(event_two);
ASSERT_TRUE(handler.seen_unknown);
}

View File

@@ -1,43 +0,0 @@
cmake_minimum_required(VERSION 3.10)
include(FetchContent)
FetchContent_Declare(SFML
GIT_REPOSITORY https://github.com/SFML/SFML.git
GIT_TAG 3.0.1
GIT_SHALLOW ON
EXCLUDE_FROM_ALL
SYSTEM)
FetchContent_MakeAvailable(SFML)
if (CMAKE_BUILD_TYPE STREQUAL "Debug")
# if (NOT DEFINED SANITIZE)
# set(SANITIZE YES)
# endif ()
endif ()
if (SANITIZE STREQUAL "YES")
message(STATUS "Enabling sanitizers!")
add_compile_options(-Werror -O0 -Wall -Wextra -pedantic -Wno-unused-parameter -Wno-unused-variable
-Wno-error=unused-function
-Wshadow -Wformat=2 -Wfloat-equal -D_GLIBCXX_DEBUG -Wconversion)
add_compile_options(-fsanitize=address -fno-sanitize-recover)
add_link_options(-fsanitize=address -fno-sanitize-recover)
endif ()
if (CMAKE_BUILD_TYPE STREQUAL "Release")
set(CMAKE_INTERPROCEDURAL_OPTIMIZATION TRUE)
endif ()
if (NOT CMAKE_BUILD_TYPE STREQUAL "Debug")
add_compile_options(-O3)
add_link_options(-O3)
endif ()
add_executable(main src/main.cpp
src/SfmlWindow.cpp
include_public/SfmlWindow.hpp)
target_include_directories(main PRIVATE include)
target_include_directories(main PUBLIC include_public)
target_link_libraries(main PRIVATE SFML::Graphics)
target_link_libraries(main PUBLIC cbsdk)

View File

@@ -1,41 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#ifndef SFMLWINDOW_HPP
#define SFMLWINDOW_HPP
#include "Surface.hpp"
#include "Window.hpp"
#include <SFML/Graphics.hpp>
class SfmlSurface : public Surface<SfmlSurface, BwPixel>, public StandardEventQueue<SfmlSurface> {
public:
using PixelType = BwPixel;
SfmlSurface(EventLoop* loop);
~SfmlSurface(); // override;
void draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel);
unsigned get_width_impl() const;
unsigned get_height_impl() const;
template<typename T>
EventHandlingResult handle(const T& event) {
return _window->handle(event);
}
EventHandlingResult handle(SurfaceResizeEvent event);
sf::RenderWindow _sf_window;
sf::Image _image;
sf::Texture _texture;
sf::Sprite _sprite;
};
#endif // SFMLWINDOW_HPP

View File

@@ -1,37 +0,0 @@
//
// Created by Stepan Usatiuk on 26.07.2025.
//
#include "SfmlWindow.hpp"
void SfmlSurface::draw_pixel_impl(unsigned x, unsigned y, const BwPixel& pixel) {
_image.setPixel({x, y}, pixel.on ? sf::Color::Black : sf::Color::White);
}
unsigned SfmlSurface::get_width_impl() const { return _image.getSize().x; }
unsigned SfmlSurface::get_height_impl() const { return _image.getSize().y; }
EventHandlingResult SfmlSurface::handle(SurfaceResizeEvent event) {
_sf_window.clear();
_image.resize({event.width, event.height});
_texture.resize({event.width, event.height});
_texture.update(_image);
_sprite = sf::Sprite(_texture);
sf::FloatRect view({0, 0}, {static_cast<float>(event.width), static_cast<float>(event.height)});
_sf_window.setView(sf::View(view));
return _window->handle(event);
}
SfmlSurface::SfmlSurface(EventLoop* loop) :
Surface<SfmlSurface, BwPixel>(),
EventQueue<SfmlSurface, KeyboardEvent, SurfaceEvent, SurfaceResizeEvent>(loop, this),
_sf_window(sf::VideoMode({640, 480}), "Test"), _image({640, 480}, sf::Color::White), _texture(_image),
_sprite(_texture) {
_sf_window.setFramerateLimit(60);
_sf_window.clear();
_sf_window.draw(_sprite);
_sf_window.display();
}
SfmlSurface::~SfmlSurface() {}

View File

@@ -1,75 +0,0 @@
#include <SFML/Graphics.hpp>
#include <barrier>
#include <latch>
#include <optional>
#include <thread>
#include "GridWindow.hpp"
#include "SfmlWindow.hpp"
#include "TextWindow.hpp"
int main() {
EventLoop loop;
std::latch barrier{1};
SfmlSurface* surface_ptr;
int i = 0;
std::thread loop_thread{[&] {
SfmlSurface surface(&loop);
surface_ptr = &surface;
barrier.count_down();
surface.set_window<GridWindow<SfmlSurface, 2, 2>>();
GridWindow<SfmlSurface, 2, 2>* window =
static_cast<GridWindow<SfmlSurface, 2, 2>*>(surface.get_window());
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 0, &loop, "hello");
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(0, 1, &loop, "hello1");
window->set_window<GridWindow<SubSurface<SfmlSurface>, 2, 2>>(1, 0);
GridWindow<SubSurface<SfmlSurface>, 2, 2>* window2 =
static_cast<GridWindow<SubSurface<SfmlSurface>, 2, 2>*>(
window->get_subsurface(1, 0).get_window());
window->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(1, 1, &loop, "hello3");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
0, 0, &loop, "hello2");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
0, 1, &loop, "hello4");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
1, 0, &loop, "hello5");
window2->set_window<TextWindow<SubSurface<SfmlSurface>, std::string>>(
1, 1, &loop, "hello6");
loop.run([&] {
surface._sf_window.clear();
surface._texture.update(surface._image);
surface._sf_window.draw(surface._sprite);
surface._sf_window.display();
static_cast<TextWindow<SubSurface<SfmlSurface>, std::string>*>(
window->get_subsurface(0, 0).get_window())
->push(TextUpdateEvent<std::string>{std::string("Hello, SFML!") + std::to_string(i++)});
});
}};
barrier.wait();
while (surface_ptr->_sf_window.isOpen()) {
while (const std::optional event = surface_ptr->_sf_window.pollEvent()) {
if (event->is<sf::Event::Closed>()) {
surface_ptr->_sf_window.close();
loop.push(LoopQuitEvent{});
}
if (event->is<sf::Event::Resized>()) {
auto newSize = event->getIf<sf::Event::Resized>()->size;
surface_ptr->push(SurfaceResizeEvent{newSize.x, newSize.y});
}
if (event->is<sf::Event::KeyPressed>()) {
auto key = event->getIf<sf::Event::KeyPressed>();
surface_ptr->push(KeyboardEvent{static_cast<Key>(key->code)});
}
}
}
loop_thread.join();
}

View File

@@ -1,19 +1,17 @@
#include "app_system.hpp"
#include <buttons.hpp>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "cardboy/sdk/app_system.hpp"
#include <algorithm>
#include <limits>
#include <utility>
namespace cardboy::sdk {
namespace {
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
return a.up != b.up || a.down != b.down || a.left != b.left || a.right != b.right || a.a != b.a || a.b != b.b ||
a.select != b.select || a.start != b.start;
}
constexpr std::uint32_t kIdlePollMs = 16;
} // namespace
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
@@ -26,9 +24,8 @@ void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
bool AppSystem::startApp(const std::string& name) {
for (std::size_t i = 0; i < factories.size(); ++i) {
if (factories[i]->name() == name) {
if (factories[i]->name() == name)
return startAppByIndex(i);
}
}
return false;
}
@@ -66,7 +63,6 @@ void AppSystem::run() {
return;
}
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
std::vector<AppEvent> events;
events.reserve(4);
@@ -88,24 +84,23 @@ void AppSystem::run() {
for (const auto& evt: events) {
dispatchEvent(evt);
if (handlePendingSwitchRequest()) {
if (handlePendingSwitchRequest())
break;
}
}
const std::uint32_t waitBase = context.clock.millis();
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
TickType_t waitTicks;
if (waitMs == std::numeric_limits<std::uint32_t>::max()) {
waitTicks = portMAX_DELAY;
} else {
waitTicks = pdMS_TO_TICKS(waitMs);
if (waitTicks == 0)
waitTicks = 1;
}
std::uint32_t waitMs = nextTimerDueMs(waitBase);
ulTaskNotifyTake(pdTRUE, waitTicks);
if (waitMs == 0)
continue;
if (waitMs == std::numeric_limits<std::uint32_t>::max())
waitMs = kIdlePollMs;
else
waitMs = std::min(waitMs, kIdlePollMs);
if (waitMs > 0)
context.clock.sleep_ms(waitMs);
}
}
@@ -125,7 +120,7 @@ std::size_t AppSystem::indexOfFactory(const IAppFactory* factory) const {
return static_cast<std::size_t>(-1);
}
AppTimerHandle AppSystem::scheduleTimer(uint32_t delay_ms, bool repeat) {
AppTimerHandle AppSystem::scheduleTimer(std::uint32_t delay_ms, bool repeat) {
if (!current)
return kInvalidAppTimer;
TimerRecord record;
@@ -225,10 +220,12 @@ bool AppSystem::handlePendingSwitchRequest() {
context.pendingSwitchByName = false;
context.pendingAppName.clear();
bool switched = false;
if (byName) {
if (byName)
switched = startApp(reqName);
} else {
else
switched = startAppByIndex(reqIndex);
}
return switched;
}
} // namespace cardboy::sdk