mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
Compare commits
3 Commits
54d5f85538
...
5b75ff28e0
| Author | SHA1 | Date | |
|---|---|---|---|
| 5b75ff28e0 | |||
| e9e371739b | |||
| 28411535bb |
@@ -2,4 +2,4 @@ idf_component_register()
|
|||||||
|
|
||||||
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
|
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)
|
||||||
@@ -1,10 +1,9 @@
|
|||||||
idf_component_register(SRCS
|
idf_component_register(SRCS
|
||||||
src/app_main.cpp
|
src/app_main.cpp
|
||||||
src/app_system.cpp
|
../sdk/apps/menu_app.cpp
|
||||||
src/apps/menu_app.cpp
|
../sdk/apps/clock_app.cpp
|
||||||
src/apps/clock_app.cpp
|
../sdk/apps/tetris_app.cpp
|
||||||
src/apps/tetris_app.cpp
|
../sdk/apps/gameboy_app.cpp
|
||||||
src/apps/gameboy_app.cpp
|
|
||||||
src/display.cpp
|
src/display.cpp
|
||||||
src/bat_mon.cpp
|
src/bat_mon.cpp
|
||||||
src/spi_global.cpp
|
src/spi_global.cpp
|
||||||
@@ -16,8 +15,9 @@ idf_component_register(SRCS
|
|||||||
src/power_helper.cpp
|
src/power_helper.cpp
|
||||||
src/buzzer.cpp
|
src/buzzer.cpp
|
||||||
src/fs_helper.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
|
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")
|
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)
|
||||||
@@ -1,118 +1,28 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_platform.hpp"
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
#include "input_state.hpp"
|
#include "cardboy/sdk/platform.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
using AppTimerHandle = cardboy::sdk::AppTimerHandle;
|
||||||
#include <memory>
|
constexpr AppTimerHandle kInvalidAppTimer = cardboy::sdk::kInvalidAppTimer;
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
class AppSystem;
|
using AppEventType = cardboy::sdk::AppEventType;
|
||||||
|
using AppButtonEvent = cardboy::sdk::AppButtonEvent;
|
||||||
using AppTimerHandle = std::uint32_t;
|
using AppTimerEvent = cardboy::sdk::AppTimerEvent;
|
||||||
constexpr AppTimerHandle kInvalidAppTimer = 0;
|
using AppEvent = cardboy::sdk::AppEvent;
|
||||||
|
|
||||||
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>
|
template<typename FramebufferT, typename InputT, typename ClockT>
|
||||||
struct BasicAppContext {
|
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
|
||||||
using Framebuffer = FramebufferT;
|
|
||||||
using Input = InputT;
|
|
||||||
using Clock = ClockT;
|
|
||||||
|
|
||||||
BasicAppContext() = delete;
|
using AppContext = cardboy::sdk::AppContext;
|
||||||
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {}
|
|
||||||
|
|
||||||
FramebufferT& framebuffer;
|
using IApp = cardboy::sdk::IApp;
|
||||||
InputT& input;
|
using IAppFactory = cardboy::sdk::IAppFactory;
|
||||||
ClockT& clock;
|
using Services = cardboy::sdk::Services;
|
||||||
AppSystem* system = nullptr;
|
using IBuzzer = cardboy::sdk::IBuzzer;
|
||||||
|
using IBatteryMonitor = cardboy::sdk::IBatteryMonitor;
|
||||||
void requestAppSwitchByIndex(std::size_t index) {
|
using IStorage = cardboy::sdk::IStorage;
|
||||||
pendingAppIndex = index;
|
using IRandom = cardboy::sdk::IRandom;
|
||||||
pendingAppName.clear();
|
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||||
pendingSwitchByName = false;
|
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||||
pendingSwitch = true;
|
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -1,7 +1,8 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/display_spec.hpp"
|
||||||
|
#include "cardboy/sdk/platform.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
#include "input_state.hpp"
|
|
||||||
|
|
||||||
#include <buttons.hpp>
|
#include <buttons.hpp>
|
||||||
#include <disp_tools.hpp>
|
#include <disp_tools.hpp>
|
||||||
@@ -10,28 +11,32 @@
|
|||||||
#include "freertos/FreeRTOS.h"
|
#include "freertos/FreeRTOS.h"
|
||||||
#include "freertos/task.h"
|
#include "freertos/task.h"
|
||||||
|
|
||||||
class PlatformFramebuffer {
|
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||||
public:
|
public:
|
||||||
int width() const { return DISP_WIDTH; }
|
int width() const override { return cardboy::sdk::kDisplayWidth; }
|
||||||
int height() const { return DISP_HEIGHT; }
|
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())
|
if (x < 0 || y < 0 || x >= width() || y >= height())
|
||||||
return;
|
return;
|
||||||
DispTools::set_pixel(x, y, on);
|
DispTools::set_pixel(x, y, on);
|
||||||
}
|
}
|
||||||
|
|
||||||
void clear(bool on) {
|
void clear(bool on) override {
|
||||||
for (int y = 0; y < height(); ++y)
|
for (int y = 0; y < height(); ++y)
|
||||||
for (int x = 0; x < width(); ++x)
|
for (int x = 0; x < width(); ++x)
|
||||||
DispTools::set_pixel(x, y, on);
|
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:
|
public:
|
||||||
InputState readState() {
|
cardboy::sdk::InputState readState() override {
|
||||||
InputState state{};
|
cardboy::sdk::InputState state{};
|
||||||
const uint8_t pressed = Buttons::get().get_pressed();
|
const uint8_t pressed = Buttons::get().get_pressed();
|
||||||
if (pressed & BTN_UP)
|
if (pressed & BTN_UP)
|
||||||
state.up = true;
|
state.up = true;
|
||||||
@@ -53,12 +58,16 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
class PlatformClock {
|
class PlatformClock final : public cardboy::sdk::IClock {
|
||||||
public:
|
public:
|
||||||
uint32_t millis() {
|
std::uint32_t millis() override {
|
||||||
TickType_t ticks = xTaskGetTickCount();
|
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));
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,79 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
|
||||||
#include <cstdint>
|
using AppSystem = cardboy::sdk::AppSystem;
|
||||||
#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();
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,12 +1,4 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace apps {
|
|
||||||
|
|
||||||
std::unique_ptr<IAppFactory> createClockAppFactory();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/apps/gameboy_app.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
|
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/apps/menu_app.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
|
|
||||||
|
|||||||
@@ -1,11 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
|
|
||||||
namespace apps {
|
|
||||||
|
|
||||||
std::unique_ptr<IAppFactory> createTetrisAppFactory();
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,8 +15,10 @@
|
|||||||
|
|
||||||
#define SPI_BUS SPI2_HOST
|
#define SPI_BUS SPI2_HOST
|
||||||
|
|
||||||
#define DISP_WIDTH 400
|
#include "cardboy/sdk/display_spec.hpp"
|
||||||
#define DISP_HEIGHT 240
|
|
||||||
|
#define DISP_WIDTH cardboy::sdk::kDisplayWidth
|
||||||
|
#define DISP_HEIGHT cardboy::sdk::kDisplayHeight
|
||||||
|
|
||||||
#define BUZZER_PIN GPIO_NUM_25
|
#define BUZZER_PIN GPIO_NUM_25
|
||||||
|
|
||||||
|
|||||||
@@ -13,8 +13,6 @@
|
|||||||
#include <array>
|
#include <array>
|
||||||
#include <bitset>
|
#include <bitset>
|
||||||
|
|
||||||
#include "Surface.hpp"
|
|
||||||
#include "Window.hpp"
|
|
||||||
|
|
||||||
namespace SMD {
|
namespace SMD {
|
||||||
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
|
||||||
|
|||||||
@@ -1,12 +1,5 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
struct InputState {
|
#include "cardboy/sdk/input_state.hpp"
|
||||||
bool up = false;
|
|
||||||
bool left = false;
|
using InputState = cardboy::sdk::InputState;
|
||||||
bool right = false;
|
|
||||||
bool down = false;
|
|
||||||
bool a = false;
|
|
||||||
bool b = false;
|
|
||||||
bool select = false;
|
|
||||||
bool start = false;
|
|
||||||
};
|
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
#include "app_system.hpp"
|
#include "app_system.hpp"
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "app_framework.hpp"
|
||||||
|
#include "app_platform.hpp"
|
||||||
#include "apps/clock_app.hpp"
|
#include "apps/clock_app.hpp"
|
||||||
#include "apps/gameboy_app.hpp"
|
#include "apps/gameboy_app.hpp"
|
||||||
#include "apps/menu_app.hpp"
|
#include "apps/menu_app.hpp"
|
||||||
#include "apps/tetris_app.hpp"
|
#include "apps/tetris_app.hpp"
|
||||||
#include "config.hpp"
|
#include "config.hpp"
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
#include <bat_mon.hpp>
|
#include <bat_mon.hpp>
|
||||||
#include <buttons.hpp>
|
#include <buttons.hpp>
|
||||||
@@ -16,6 +18,8 @@
|
|||||||
#include <display.hpp>
|
#include <display.hpp>
|
||||||
#include <fs_helper.hpp>
|
#include <fs_helper.hpp>
|
||||||
#include <i2c_global.hpp>
|
#include <i2c_global.hpp>
|
||||||
|
#include <nvs.h>
|
||||||
|
#include <nvs_flash.h>
|
||||||
#include <power_helper.hpp>
|
#include <power_helper.hpp>
|
||||||
#include <shutdowner.hpp>
|
#include <shutdowner.hpp>
|
||||||
#include <spi_global.hpp>
|
#include <spi_global.hpp>
|
||||||
@@ -26,6 +30,8 @@
|
|||||||
|
|
||||||
#include "driver/gpio.h"
|
#include "driver/gpio.h"
|
||||||
#include "esp_err.h"
|
#include "esp_err.h"
|
||||||
|
#include "esp_random.h"
|
||||||
|
#include "esp_timer.h"
|
||||||
#include "esp_pm.h"
|
#include "esp_pm.h"
|
||||||
#include "esp_sleep.h"
|
#include "esp_sleep.h"
|
||||||
#include "sdkconfig.h"
|
#include "sdkconfig.h"
|
||||||
@@ -36,8 +42,94 @@
|
|||||||
#include <cstdio>
|
#include <cstdio>
|
||||||
#include <cstring>
|
#include <cstring>
|
||||||
#include <string>
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
#include <vector>
|
#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
|
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -217,7 +309,25 @@ extern "C" void app_main() {
|
|||||||
static PlatformInput input;
|
static PlatformInput input;
|
||||||
static PlatformClock clock;
|
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);
|
AppContext context(framebuffer, input, clock);
|
||||||
|
context.services = &services;
|
||||||
AppSystem system(context);
|
AppSystem system(context);
|
||||||
context.system = &system;
|
context.system = &system;
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
#include <disp_tools.hpp>
|
#include <disp_tools.hpp>
|
||||||
|
|
||||||
#include "Fonts.hpp"
|
#include "cardboy/gfx/Fonts.hpp"
|
||||||
|
|
||||||
void FbTty::draw_char(int col, int row) {
|
void FbTty::draw_char(int col, int row) {
|
||||||
for (int x = 0; x < 8; x++) {
|
for (int x = 0; x < 8; x++) {
|
||||||
|
|||||||
@@ -1,11 +1,70 @@
|
|||||||
cmake_minimum_required(VERSION 3.10)
|
cmake_minimum_required(VERSION 3.16)
|
||||||
project(sdk-top)
|
project(cardboy_sdk LANGUAGES CXX)
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS NO)
|
||||||
|
|
||||||
add_subdirectory(library)
|
add_library(cardboy_sdk STATIC
|
||||||
if (NOT CMAKE_CROSSCOMPILING)
|
src/app_system.cpp
|
||||||
add_subdirectory(sfml-port)
|
)
|
||||||
add_subdirectory(examples)
|
|
||||||
|
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 ()
|
endif ()
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
#include "apps/clock_app.hpp"
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
|
|
||||||
#include "app_system.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
#include "apps/menu_app.hpp"
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
#include "font16x8.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
|
||||||
#include <disp_tools.hpp>
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -17,6 +17,8 @@ namespace apps {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using cardboy::sdk::AppContext;
|
||||||
|
|
||||||
constexpr const char* kClockAppName = "Clock";
|
constexpr const char* kClockAppName = "Clock";
|
||||||
|
|
||||||
using Framebuffer = typename AppContext::Framebuffer;
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
@@ -31,10 +33,10 @@ struct TimeSnapshot {
|
|||||||
int month = 0;
|
int month = 0;
|
||||||
int day = 0;
|
int day = 0;
|
||||||
int weekday = 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:
|
public:
|
||||||
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
explicit ClockApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer), clock(ctx.clock) {}
|
||||||
|
|
||||||
@@ -50,12 +52,12 @@ public:
|
|||||||
|
|
||||||
void onStop() override { cancelRefreshTimer(); }
|
void onStop() override { cancelRefreshTimer(); }
|
||||||
|
|
||||||
void handleEvent(const AppEvent& event) override {
|
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||||
switch (event.type) {
|
switch (event.type) {
|
||||||
case AppEventType::Button:
|
case cardboy::sdk::AppEventType::Button:
|
||||||
handleButtonEvent(event.button);
|
handleButtonEvent(event.button);
|
||||||
break;
|
break;
|
||||||
case AppEventType::Timer:
|
case cardboy::sdk::AppEventType::Timer:
|
||||||
if (event.timer.handle == refreshTimer)
|
if (event.timer.handle == refreshTimer)
|
||||||
updateDisplay();
|
updateDisplay();
|
||||||
break;
|
break;
|
||||||
@@ -69,18 +71,18 @@ private:
|
|||||||
|
|
||||||
bool use24Hour = true;
|
bool use24Hour = true;
|
||||||
bool dirty = false;
|
bool dirty = false;
|
||||||
AppTimerHandle refreshTimer = kInvalidAppTimer;
|
cardboy::sdk::AppTimerHandle refreshTimer = cardboy::sdk::kInvalidAppTimer;
|
||||||
|
|
||||||
TimeSnapshot lastSnapshot{};
|
TimeSnapshot lastSnapshot{};
|
||||||
|
|
||||||
void cancelRefreshTimer() {
|
void cancelRefreshTimer() {
|
||||||
if (refreshTimer != kInvalidAppTimer) {
|
if (refreshTimer != cardboy::sdk::kInvalidAppTimer) {
|
||||||
context.cancelTimer(refreshTimer);
|
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& current = button.current;
|
||||||
const auto& previous = button.previous;
|
const auto& previous = button.previous;
|
||||||
|
|
||||||
@@ -113,8 +115,8 @@ private:
|
|||||||
TimeSnapshot snap{};
|
TimeSnapshot snap{};
|
||||||
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
snap.uptimeSeconds = clock.millis() / 1000ULL;
|
||||||
|
|
||||||
time_t raw = 0;
|
std::time_t raw = 0;
|
||||||
if (time(&raw) != static_cast<time_t>(-1) && raw > 0) {
|
if (std::time(&raw) != static_cast<std::time_t>(-1) && raw > 0) {
|
||||||
std::tm tm{};
|
std::tm tm{};
|
||||||
if (localtime_r(&raw, &tm) != nullptr) {
|
if (localtime_r(&raw, &tm) != nullptr) {
|
||||||
snap.hasWallTime = true;
|
snap.hasWallTime = true;
|
||||||
@@ -158,7 +160,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
|
|
||||||
DispTools::draw_to_display_async_wait();
|
framebuffer.beginFrame();
|
||||||
framebuffer.clear(false);
|
framebuffer.clear(false);
|
||||||
|
|
||||||
const int scaleLarge = 3;
|
const int scaleLarge = 3;
|
||||||
@@ -199,10 +201,10 @@ private:
|
|||||||
|
|
||||||
if (!snap.hasWallTime) {
|
if (!snap.hasWallTime) {
|
||||||
char uptimeLine[32];
|
char uptimeLine[32];
|
||||||
const uint64_t days = snap.uptimeSeconds / 86400ULL;
|
const std::uint64_t days = snap.uptimeSeconds / 86400ULL;
|
||||||
const uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
const std::uint64_t hrs = (snap.uptimeSeconds / 3600ULL) % 24ULL;
|
||||||
const uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
const std::uint64_t mins = (snap.uptimeSeconds / 60ULL) % 60ULL;
|
||||||
const uint64_t secs = snap.uptimeSeconds % 60ULL;
|
const std::uint64_t secs = snap.uptimeSeconds % 60ULL;
|
||||||
if (days > 0) {
|
if (days > 0) {
|
||||||
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
|
std::snprintf(uptimeLine, sizeof(uptimeLine), "%llud %02llu:%02llu:%02llu UP",
|
||||||
static_cast<unsigned long long>(days), static_cast<unsigned long long>(hrs),
|
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() - 36, "SELECT TOGGLE 12/24H", 1, 1);
|
||||||
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 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:
|
public:
|
||||||
const char* name() const override { return kClockAppName; }
|
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
|
} // 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
|
} // namespace apps
|
||||||
@@ -1,16 +1,11 @@
|
|||||||
#pragma GCC optimize("Ofast")
|
#pragma GCC optimize("Ofast")
|
||||||
#include "apps/gameboy_app.hpp"
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
#include "apps/peanut_gb.h"
|
#include "cardboy/apps/peanut_gb.h"
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
#include "app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
#include "font16x8.hpp"
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
#include <disp_tools.hpp>
|
|
||||||
#include <fs_helper.hpp>
|
|
||||||
|
|
||||||
|
|
||||||
#include "esp_timer.h"
|
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
|
||||||
@@ -26,6 +21,7 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#define GAMEBOY_PERF_METRICS 0
|
#define GAMEBOY_PERF_METRICS 0
|
||||||
|
|
||||||
@@ -45,16 +41,16 @@ namespace {
|
|||||||
constexpr int kMenuStartY = 48;
|
constexpr int kMenuStartY = 48;
|
||||||
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
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;
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
|
|
||||||
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
|
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 {
|
struct EmbeddedRomDescriptor {
|
||||||
std::string_view name;
|
std::string_view name;
|
||||||
std::string_view saveSlug;
|
std::string_view saveSlug;
|
||||||
@@ -62,6 +58,14 @@ struct EmbeddedRomDescriptor {
|
|||||||
const uint8_t* end;
|
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 = {{{
|
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
||||||
"Builtin Demo 1",
|
"Builtin Demo 1",
|
||||||
"builtin_demo1",
|
"builtin_demo1",
|
||||||
@@ -74,6 +78,9 @@ static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
|||||||
_binary_builtin_demo2_gb_start,
|
_binary_builtin_demo2_gb_start,
|
||||||
_binary_builtin_demo2_gb_end,
|
_binary_builtin_demo2_gb_end,
|
||||||
}}};
|
}}};
|
||||||
|
#else
|
||||||
|
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
|
||||||
|
#endif
|
||||||
|
|
||||||
struct RomEntry {
|
struct RomEntry {
|
||||||
std::string name; // short display name
|
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:
|
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 {
|
void onStart() override {
|
||||||
cancelTick();
|
cancelTick();
|
||||||
@@ -197,9 +205,9 @@ public:
|
|||||||
void handleEvent(const AppEvent& event) override {
|
void handleEvent(const AppEvent& event) override {
|
||||||
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
||||||
tickTimer = kInvalidAppTimer;
|
tickTimer = kInvalidAppTimer;
|
||||||
const uint64_t frameStartUs = esp_timer_get_time();
|
const uint64_t frameStartUs = nowMicros();
|
||||||
performStep();
|
performStep();
|
||||||
const uint64_t frameEndUs = esp_timer_get_time();
|
const uint64_t frameEndUs = nowMicros();
|
||||||
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
||||||
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
||||||
scheduleAfterFrame(elapsedUs);
|
scheduleAfterFrame(elapsedUs);
|
||||||
@@ -214,27 +222,27 @@ public:
|
|||||||
void performStep() {
|
void performStep() {
|
||||||
GB_PERF_ONLY(perf.resetForStep();)
|
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();
|
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;
|
const Mode stepMode = mode;
|
||||||
|
|
||||||
switch (stepMode) {
|
switch (stepMode) {
|
||||||
case Mode::Browse: {
|
case Mode::Browse: {
|
||||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||||
handleBrowserInput(input);
|
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();
|
renderBrowser();
|
||||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Mode::Running: {
|
case Mode::Running: {
|
||||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||||
handleGameInput(input);
|
handleGameInput(input);
|
||||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
|
||||||
|
|
||||||
if (!gbReady) {
|
if (!gbReady) {
|
||||||
mode = Mode::Browse;
|
mode = Mode::Browse;
|
||||||
@@ -242,17 +250,21 @@ public:
|
|||||||
break;
|
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();
|
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_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();
|
renderGameFrame();
|
||||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||||
|
framebuffer.endFrame();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -329,14 +341,14 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetForStep() {
|
void resetForStep() {
|
||||||
stepStartUs = esp_timer_get_time();
|
stepStartUs = clockMicros();
|
||||||
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
||||||
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
||||||
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishStep() {
|
void finishStep() {
|
||||||
const uint64_t now = esp_timer_get_time();
|
const uint64_t now = clockMicros();
|
||||||
totalUs = now - stepStartUs;
|
totalUs = now - stepStartUs;
|
||||||
lastStepEndUs = now;
|
lastStepEndUs = now;
|
||||||
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
||||||
@@ -418,7 +430,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
if (!aggStartUs)
|
if (!aggStartUs)
|
||||||
aggStartUs = stepStartUs;
|
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;
|
const uint64_t span = now - aggStartUs;
|
||||||
if (!force && span < 1000000ULL)
|
if (!force && span < 1000000ULL)
|
||||||
return;
|
return;
|
||||||
@@ -469,6 +481,12 @@ private:
|
|||||||
aggCbLcdCalls = 0;
|
aggCbLcdCalls = 0;
|
||||||
aggCbErrorCalls = 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 {
|
class ScopedCallbackTimer {
|
||||||
@@ -478,7 +496,7 @@ private:
|
|||||||
if (instance) {
|
if (instance) {
|
||||||
app = instance;
|
app = instance;
|
||||||
cbKind = kind;
|
cbKind = kind;
|
||||||
startUs = esp_timer_get_time();
|
startUs = instance->nowMicros();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
(void) instance;
|
(void) instance;
|
||||||
@@ -490,7 +508,7 @@ private:
|
|||||||
#if GAMEBOY_PERF_METRICS
|
#if GAMEBOY_PERF_METRICS
|
||||||
if (!app)
|
if (!app)
|
||||||
return;
|
return;
|
||||||
const uint64_t end = esp_timer_get_time();
|
const uint64_t end = app->nowMicros();
|
||||||
app->perf.addCallback(cbKind, end - startUs);
|
app->perf.addCallback(cbKind, end - startUs);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -520,6 +538,8 @@ private:
|
|||||||
|
|
||||||
AppContext& context;
|
AppContext& context;
|
||||||
Framebuffer& framebuffer;
|
Framebuffer& framebuffer;
|
||||||
|
cardboy::sdk::IFilesystem* filesystem = nullptr;
|
||||||
|
cardboy::sdk::IHighResClock* highResClock = nullptr;
|
||||||
PerfTracker perf{};
|
PerfTracker perf{};
|
||||||
AppTimerHandle tickTimer = kInvalidAppTimer;
|
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||||
int64_t frameDelayCarryUs = 0;
|
int64_t frameDelayCarryUs = 0;
|
||||||
@@ -584,9 +604,13 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ensureFilesystemReady() {
|
bool ensureFilesystemReady() {
|
||||||
esp_err_t err = FsHelper::get().mount();
|
if (!filesystem) {
|
||||||
if (err != ESP_OK) {
|
setStatus("Storage unavailable");
|
||||||
setStatus("LittleFS mount failed");
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filesystem->isMounted() && !filesystem->mount()) {
|
||||||
|
setStatus("Storage mount failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -610,7 +634,9 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::string romDirectory() const {
|
[[nodiscard]] std::string romDirectory() const {
|
||||||
std::string result(FsHelper::get().basePath());
|
std::string result;
|
||||||
|
if (filesystem)
|
||||||
|
result = filesystem->basePath();
|
||||||
if (!result.empty() && result.back() != '/')
|
if (!result.empty() && result.back() != '/')
|
||||||
result.push_back('/');
|
result.push_back('/');
|
||||||
result.append("roms");
|
result.append("roms");
|
||||||
@@ -631,7 +657,7 @@ private:
|
|||||||
void refreshRomList() {
|
void refreshRomList() {
|
||||||
roms.clear();
|
roms.clear();
|
||||||
|
|
||||||
bool fsMounted = FsHelper::get().isMounted();
|
bool fsMounted = filesystem ? filesystem->isMounted() : false;
|
||||||
std::string statusHint;
|
std::string statusHint;
|
||||||
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
||||||
if (statusHint.empty())
|
if (statusHint.empty())
|
||||||
@@ -641,7 +667,7 @@ private:
|
|||||||
if (!fsMounted) {
|
if (!fsMounted) {
|
||||||
fsMounted = ensureFilesystemReady();
|
fsMounted = ensureFilesystemReady();
|
||||||
if (!fsMounted)
|
if (!fsMounted)
|
||||||
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
|
updateStatusHintIfEmpty("Built-in ROMs only (filesystem unavailable)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fsMounted) {
|
if (fsMounted) {
|
||||||
@@ -676,9 +702,9 @@ private:
|
|||||||
}
|
}
|
||||||
closedir(dir);
|
closedir(dir);
|
||||||
if (roms.empty())
|
if (roms.empty())
|
||||||
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
|
updateStatusHintIfEmpty("Copy .gb/.gbc into ROMS/");
|
||||||
} else {
|
} else {
|
||||||
updateStatusHintIfEmpty("No /lfs/roms directory");
|
updateStatusHintIfEmpty("ROM directory missing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -823,7 +849,8 @@ private:
|
|||||||
return;
|
return;
|
||||||
browserDirty = false;
|
browserDirty = false;
|
||||||
|
|
||||||
DispTools::draw_to_display_async_wait();
|
framebuffer.beginFrame();
|
||||||
|
framebuffer.clear(false);
|
||||||
|
|
||||||
const std::string_view title = "GAME BOY";
|
const std::string_view title = "GAME BOY";
|
||||||
const int titleWidth = font16x8::measureText(title, 2, 1);
|
const int titleWidth = font16x8::measureText(title, 2, 1);
|
||||||
@@ -832,7 +859,7 @@ private:
|
|||||||
|
|
||||||
if (roms.empty()) {
|
if (roms.empty()) {
|
||||||
font16x8::drawText(framebuffer, 24, kMenuStartY + 12, "NO ROMS FOUND", 1, true, 1);
|
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 {
|
} else {
|
||||||
const std::size_t visibleCount =
|
const std::size_t visibleCount =
|
||||||
static_cast<std::size_t>(std::max(1, (framebuffer.height() - kMenuStartY - 64) / kMenuSpacing));
|
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);
|
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) {
|
bool loadRom(std::size_t index) {
|
||||||
@@ -948,7 +975,7 @@ private:
|
|||||||
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||||
std::string savePath;
|
std::string savePath;
|
||||||
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
|
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
|
||||||
if (fsReady)
|
if (fsReady)
|
||||||
savePath = buildSavePath(rom, romDirectory());
|
savePath = buildSavePath(rom, romDirectory());
|
||||||
activeRomSavePath = savePath;
|
activeRomSavePath = savePath;
|
||||||
@@ -1158,7 +1185,6 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispTools::draw_to_display_async_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void maybeSaveRam() {
|
void maybeSaveRam() {
|
||||||
@@ -1198,6 +1224,14 @@ private:
|
|||||||
browserDirty = true;
|
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() {
|
void resetFpsStats() {
|
||||||
fpsLastSampleMs = 0;
|
fpsLastSampleMs = 0;
|
||||||
fpsFrameCounter = 0;
|
fpsFrameCounter = 0;
|
||||||
@@ -1302,10 +1336,6 @@ private:
|
|||||||
|
|
||||||
Framebuffer& fb = self->framebuffer;
|
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) &&
|
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
|
||||||
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
|
(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:
|
public:
|
||||||
const char* name() const override { return kGameboyAppName; }
|
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
|
} // 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
|
} // namespace apps
|
||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "apps/menu_app.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
|
|
||||||
#include "app_system.hpp"
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
#include "font16x8.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
|
||||||
#include <disp_tools.hpp>
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
@@ -15,6 +15,8 @@ namespace apps {
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
|
using cardboy::sdk::AppContext;
|
||||||
|
|
||||||
using Framebuffer = typename AppContext::Framebuffer;
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
|
|
||||||
struct MenuEntry {
|
struct MenuEntry {
|
||||||
@@ -22,7 +24,7 @@ struct MenuEntry {
|
|||||||
std::size_t index = 0;
|
std::size_t index = 0;
|
||||||
};
|
};
|
||||||
|
|
||||||
class MenuApp final : public IApp {
|
class MenuApp final : public cardboy::sdk::IApp {
|
||||||
public:
|
public:
|
||||||
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
explicit MenuApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) { refreshEntries(); }
|
||||||
|
|
||||||
@@ -32,8 +34,8 @@ public:
|
|||||||
renderIfNeeded();
|
renderIfNeeded();
|
||||||
}
|
}
|
||||||
|
|
||||||
void handleEvent(const AppEvent& event) override {
|
void handleEvent(const cardboy::sdk::AppEvent& event) override {
|
||||||
if (event.type != AppEventType::Button)
|
if (event.type != cardboy::sdk::AppEventType::Button)
|
||||||
return;
|
return;
|
||||||
|
|
||||||
const auto& current = event.button.current;
|
const auto& current = event.button.current;
|
||||||
@@ -85,7 +87,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
const std::size_t total = context.system->appCount();
|
const std::size_t total = context.system->appCount();
|
||||||
for (std::size_t i = 0; i < total; ++i) {
|
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)
|
if (!factory)
|
||||||
continue;
|
continue;
|
||||||
const char* name = factory->name();
|
const char* name = factory->name();
|
||||||
@@ -133,7 +135,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
dirty = false;
|
dirty = false;
|
||||||
|
|
||||||
DispTools::draw_to_display_async_wait();
|
framebuffer.beginFrame();
|
||||||
framebuffer.clear(false);
|
framebuffer.clear(false);
|
||||||
|
|
||||||
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
drawCenteredText(framebuffer, 24, "APPS", 1, 1);
|
||||||
@@ -156,18 +158,20 @@ private:
|
|||||||
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
|
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:
|
public:
|
||||||
const char* name() const override { return kMenuAppName; }
|
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
|
} // 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
|
} // namespace apps
|
||||||
624
Firmware/sdk/apps/tetris_app.cpp
Normal file
624
Firmware/sdk/apps/tetris_app.cpp
Normal 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
|
||||||
397
Firmware/sdk/hosts/sfml_main.cpp
Normal file
397
Firmware/sdk/hosts/sfml_main.cpp
Normal 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;
|
||||||
|
}
|
||||||
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/clock_app.hpp
Normal 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
|
||||||
|
|
||||||
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal 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
|
||||||
|
|
||||||
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/menu_app.hpp
Normal 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
|
||||||
|
|
||||||
@@ -92,12 +92,12 @@
|
|||||||
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
|
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
|
||||||
* pixel data. */
|
* pixel data. */
|
||||||
#ifndef PEANUT_GB_12_COLOUR
|
#ifndef PEANUT_GB_12_COLOUR
|
||||||
# define PEANUT_GB_12_COLOUR 1
|
# define PEANUT_GB_12_COLOUR 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Adds more code to improve LCD rendering accuracy. */
|
/* Adds more code to improve LCD rendering accuracy. */
|
||||||
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
|
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
|
||||||
# define PEANUT_GB_HIGH_LCD_ACCURACY 1
|
# define PEANUT_GB_HIGH_LCD_ACCURACY 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Use intrinsic functions. This may produce smaller and faster code. */
|
/* Use intrinsic functions. This may produce smaller and faster code. */
|
||||||
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal file
12
Firmware/sdk/include/cardboy/apps/tetris_app.hpp
Normal 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
|
||||||
|
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "Fonts.hpp"
|
#include "cardboy/gfx/Fonts.hpp"
|
||||||
#include "app_framework.hpp"
|
|
||||||
|
|
||||||
#include <array>
|
#include <array>
|
||||||
#include <cctype>
|
#include <cctype>
|
||||||
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal file
133
Firmware/sdk/include/cardboy/sdk/app_framework.hpp
Normal 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
|
||||||
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal file
85
Firmware/sdk/include/cardboy/sdk/app_system.hpp
Normal 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
|
||||||
|
|
||||||
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
9
Firmware/sdk/include/cardboy/sdk/display_spec.hpp
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
|
|
||||||
|
inline constexpr int kDisplayWidth = 400;
|
||||||
|
inline constexpr int kDisplayHeight = 240;
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
|
|
||||||
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal file
17
Firmware/sdk/include/cardboy/sdk/input_state.hpp
Normal 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
|
||||||
|
|
||||||
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal file
40
Firmware/sdk/include/cardboy/sdk/platform.hpp
Normal 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
|
||||||
|
|
||||||
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal file
87
Firmware/sdk/include/cardboy/sdk/services.hpp
Normal 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
|
||||||
@@ -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 ()
|
|
||||||
@@ -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
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -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
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Stepan Usatiuk on 26.07.2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "Event.hpp"
|
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Stepan Usatiuk on 26.07.2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "TextWindow.hpp"
|
|
||||||
#include "Fonts.hpp"
|
|
||||||
#include "Surface.hpp"
|
|
||||||
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
//
|
|
||||||
// Created by Stepan Usatiuk on 26.07.2025.
|
|
||||||
//
|
|
||||||
|
|
||||||
#include "Window.hpp"
|
|
||||||
@@ -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)
|
|
||||||
@@ -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);
|
|
||||||
}
|
|
||||||
@@ -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)
|
|
||||||
@@ -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
|
|
||||||
@@ -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() {}
|
|
||||||
@@ -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();
|
|
||||||
}
|
|
||||||
@@ -1,19 +1,17 @@
|
|||||||
#include "app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
|
||||||
#include <buttons.hpp>
|
|
||||||
|
|
||||||
#include "freertos/FreeRTOS.h"
|
|
||||||
#include "freertos/task.h"
|
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
|
|
||||||
|
namespace cardboy::sdk {
|
||||||
namespace {
|
namespace {
|
||||||
[[nodiscard]] bool inputsDiffer(const InputState& a, const InputState& b) {
|
[[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 ||
|
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;
|
a.select != b.select || a.start != b.start;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
constexpr std::uint32_t kIdlePollMs = 16;
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
AppSystem::AppSystem(AppContext ctx) : context(std::move(ctx)) { context.system = this; }
|
||||||
@@ -26,10 +24,9 @@ void AppSystem::registerApp(std::unique_ptr<IAppFactory> factory) {
|
|||||||
|
|
||||||
bool AppSystem::startApp(const std::string& name) {
|
bool AppSystem::startApp(const std::string& name) {
|
||||||
for (std::size_t i = 0; i < factories.size(); ++i) {
|
for (std::size_t i = 0; i < factories.size(); ++i) {
|
||||||
if (factories[i]->name() == name) {
|
if (factories[i]->name() == name)
|
||||||
return startAppByIndex(i);
|
return startAppByIndex(i);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ void AppSystem::run() {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
Buttons::get().register_listener(xTaskGetCurrentTaskHandle());
|
|
||||||
std::vector<AppEvent> events;
|
std::vector<AppEvent> events;
|
||||||
events.reserve(4);
|
events.reserve(4);
|
||||||
|
|
||||||
@@ -88,24 +84,23 @@ void AppSystem::run() {
|
|||||||
|
|
||||||
for (const auto& evt: events) {
|
for (const auto& evt: events) {
|
||||||
dispatchEvent(evt);
|
dispatchEvent(evt);
|
||||||
if (handlePendingSwitchRequest()) {
|
if (handlePendingSwitchRequest())
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const std::uint32_t waitBase = context.clock.millis();
|
const std::uint32_t waitBase = context.clock.millis();
|
||||||
const std::uint32_t waitMs = nextTimerDueMs(waitBase);
|
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
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)
|
if (!current)
|
||||||
return kInvalidAppTimer;
|
return kInvalidAppTimer;
|
||||||
TimerRecord record;
|
TimerRecord record;
|
||||||
@@ -225,10 +220,12 @@ bool AppSystem::handlePendingSwitchRequest() {
|
|||||||
context.pendingSwitchByName = false;
|
context.pendingSwitchByName = false;
|
||||||
context.pendingAppName.clear();
|
context.pendingAppName.clear();
|
||||||
bool switched = false;
|
bool switched = false;
|
||||||
if (byName) {
|
if (byName)
|
||||||
switched = startApp(reqName);
|
switched = startApp(reqName);
|
||||||
} else {
|
else
|
||||||
switched = startAppByIndex(reqIndex);
|
switched = startAppByIndex(reqIndex);
|
||||||
}
|
|
||||||
return switched;
|
return switched;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::sdk
|
||||||
|
|
||||||
Reference in New Issue
Block a user