From fa2715a60ac80401e46a1c702b07b3f2de418c63 Mon Sep 17 00:00:00 2001 From: Stepan Usatiuk Date: Sat, 11 Oct 2025 14:26:42 +0200 Subject: [PATCH] cleaner backend --- Firmware/sdk/CMakeLists.txt | 82 ++--- Firmware/sdk/apps/CMakeLists.txt | 18 + Firmware/sdk/apps/clock/CMakeLists.txt | 9 + .../clock}/include/cardboy/apps/clock_app.hpp | 0 .../sdk/apps/{ => clock/src}/clock_app.cpp | 0 Firmware/sdk/apps/gameboy/CMakeLists.txt | 10 + .../include/cardboy/apps/gameboy_app.hpp | 0 .../gameboy}/include/cardboy/apps/peanut_gb.h | 0 .../apps/{ => gameboy/src}/gameboy_app.cpp | 0 Firmware/sdk/apps/menu/CMakeLists.txt | 9 + .../menu}/include/cardboy/apps/menu_app.hpp | 0 Firmware/sdk/apps/{ => menu/src}/menu_app.cpp | 0 Firmware/sdk/apps/tetris/CMakeLists.txt | 9 + .../include/cardboy/apps/tetris_app.hpp | 0 .../sdk/apps/{ => tetris/src}/tetris_app.cpp | 0 Firmware/sdk/backend_interface/CMakeLists.txt | 15 + .../cardboy/backend/backend_interface.hpp | 14 + Firmware/sdk/backends/desktop/CMakeLists.txt | 40 ++ .../include/cardboy/backend/backend_impl.hpp | 1 - .../cardboy/backend/desktop_backend.hpp | 186 ++++++++++ .../backends/desktop/src/desktop_backend.cpp | 235 ++++++++++++ .../cardboy/backend/desktop_backend.hpp | 123 +++++++ Firmware/sdk/hosts/sfml_main.cpp | 347 +----------------- Firmware/sdk/include/cardboy/sdk/backend.hpp | 5 + Firmware/sdk/launchers/desktop/CMakeLists.txt | 16 + Firmware/sdk/launchers/desktop/src/main.cpp | 33 ++ 26 files changed, 747 insertions(+), 405 deletions(-) create mode 100644 Firmware/sdk/apps/CMakeLists.txt create mode 100644 Firmware/sdk/apps/clock/CMakeLists.txt rename Firmware/sdk/{ => apps/clock}/include/cardboy/apps/clock_app.hpp (100%) rename Firmware/sdk/apps/{ => clock/src}/clock_app.cpp (100%) create mode 100644 Firmware/sdk/apps/gameboy/CMakeLists.txt rename Firmware/sdk/{ => apps/gameboy}/include/cardboy/apps/gameboy_app.hpp (100%) rename Firmware/sdk/{ => apps/gameboy}/include/cardboy/apps/peanut_gb.h (100%) rename Firmware/sdk/apps/{ => gameboy/src}/gameboy_app.cpp (100%) create mode 100644 Firmware/sdk/apps/menu/CMakeLists.txt rename Firmware/sdk/{ => apps/menu}/include/cardboy/apps/menu_app.hpp (100%) rename Firmware/sdk/apps/{ => menu/src}/menu_app.cpp (100%) create mode 100644 Firmware/sdk/apps/tetris/CMakeLists.txt rename Firmware/sdk/{ => apps/tetris}/include/cardboy/apps/tetris_app.hpp (100%) rename Firmware/sdk/apps/{ => tetris/src}/tetris_app.cpp (100%) create mode 100644 Firmware/sdk/backend_interface/CMakeLists.txt create mode 100644 Firmware/sdk/backend_interface/include/cardboy/backend/backend_interface.hpp create mode 100644 Firmware/sdk/backends/desktop/CMakeLists.txt rename Firmware/sdk/{hosts => backends/desktop}/include/cardboy/backend/backend_impl.hpp (99%) create mode 100644 Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp create mode 100644 Firmware/sdk/backends/desktop/src/desktop_backend.cpp create mode 100644 Firmware/sdk/launchers/desktop/CMakeLists.txt create mode 100644 Firmware/sdk/launchers/desktop/src/main.cpp diff --git a/Firmware/sdk/CMakeLists.txt b/Firmware/sdk/CMakeLists.txt index 2d91769..554499e 100644 --- a/Firmware/sdk/CMakeLists.txt +++ b/Firmware/sdk/CMakeLists.txt @@ -5,7 +5,25 @@ set(CMAKE_CXX_STANDARD 20) set(CMAKE_CXX_STANDARD_REQUIRED YES) set(CMAKE_CXX_EXTENSIONS NO) -add_library(cardboy_backend INTERFACE) +add_subdirectory(backend_interface) + +set(CARDBOY_SDK_BACKEND_LIBRARY "" CACHE STRING "Backend implementation library for Cardboy SDK") +set(_cardboy_backend_default "${CARDBOY_SDK_BACKEND_LIBRARY}") + +option(CARDBOY_BUILD_SFML "Build desktop SFML backend and launcher" ON) + +if (CARDBOY_BUILD_SFML) + add_subdirectory(backends/desktop) + if (DEFINED CARDBOY_DESKTOP_BACKEND_TARGET AND NOT CARDBOY_DESKTOP_BACKEND_TARGET STREQUAL "") + set(_cardboy_backend_default "${CARDBOY_DESKTOP_BACKEND_TARGET}") + endif () +endif () + +if (_cardboy_backend_default STREQUAL "") + message(FATAL_ERROR "CARDBOY_SDK_BACKEND_LIBRARY is not set. Provide a backend implementation library or enable one of the available backends.") +endif () + +set(CARDBOY_SDK_BACKEND_LIBRARY "${_cardboy_backend_default}" CACHE STRING "Backend implementation library for Cardboy SDK" FORCE) add_library(cardboy_sdk STATIC src/app_system.cpp @@ -17,67 +35,15 @@ target_include_directories(cardboy_sdk ) target_compile_features(cardboy_sdk PUBLIC cxx_std_20) -target_link_libraries(cardboy_sdk PUBLIC cardboy_backend) -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 +target_link_libraries(cardboy_sdk PUBLIC - ${CMAKE_CURRENT_SOURCE_DIR}/include + cardboy_backend_interface + ${CARDBOY_SDK_BACKEND_LIBRARY} ) -target_link_libraries(cardboy_apps - PUBLIC - cardboy_sdk - cardboy_backend -) - -target_compile_features(cardboy_apps PUBLIC cxx_std_20) - -option(CARDBOY_BUILD_SFML "Build SFML harness" OFF) +add_subdirectory(apps) 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) - - target_include_directories(cardboy_backend - INTERFACE - ${CMAKE_CURRENT_SOURCE_DIR}/hosts/include - ) - - target_link_libraries(cardboy_backend - INTERFACE - SFML::Window - SFML::Graphics - SFML::System - ) - add_executable(cardboy_desktop - hosts/sfml_main.cpp - ) - - target_link_libraries(cardboy_desktop - PRIVATE - cardboy_apps - cardboy_sdk - ) - - target_compile_features(cardboy_desktop PRIVATE cxx_std_20) + add_subdirectory(launchers/desktop) endif () diff --git a/Firmware/sdk/apps/CMakeLists.txt b/Firmware/sdk/apps/CMakeLists.txt new file mode 100644 index 0000000..07c31a4 --- /dev/null +++ b/Firmware/sdk/apps/CMakeLists.txt @@ -0,0 +1,18 @@ +add_library(cardboy_apps STATIC) + +set_target_properties(cardboy_apps PROPERTIES + EXPORT_NAME apps +) + +target_link_libraries(cardboy_apps + PUBLIC + cardboy_sdk + ${CARDBOY_SDK_BACKEND_LIBRARY} +) + +target_compile_features(cardboy_apps PUBLIC cxx_std_20) + +add_subdirectory(menu) +add_subdirectory(clock) +add_subdirectory(gameboy) +add_subdirectory(tetris) diff --git a/Firmware/sdk/apps/clock/CMakeLists.txt b/Firmware/sdk/apps/clock/CMakeLists.txt new file mode 100644 index 0000000..95de453 --- /dev/null +++ b/Firmware/sdk/apps/clock/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(cardboy_apps + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/clock_app.cpp +) + +target_include_directories(cardboy_apps + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) diff --git a/Firmware/sdk/include/cardboy/apps/clock_app.hpp b/Firmware/sdk/apps/clock/include/cardboy/apps/clock_app.hpp similarity index 100% rename from Firmware/sdk/include/cardboy/apps/clock_app.hpp rename to Firmware/sdk/apps/clock/include/cardboy/apps/clock_app.hpp diff --git a/Firmware/sdk/apps/clock_app.cpp b/Firmware/sdk/apps/clock/src/clock_app.cpp similarity index 100% rename from Firmware/sdk/apps/clock_app.cpp rename to Firmware/sdk/apps/clock/src/clock_app.cpp diff --git a/Firmware/sdk/apps/gameboy/CMakeLists.txt b/Firmware/sdk/apps/gameboy/CMakeLists.txt new file mode 100644 index 0000000..76b4b6f --- /dev/null +++ b/Firmware/sdk/apps/gameboy/CMakeLists.txt @@ -0,0 +1,10 @@ +target_sources(cardboy_apps + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h +) + +target_include_directories(cardboy_apps + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) diff --git a/Firmware/sdk/include/cardboy/apps/gameboy_app.hpp b/Firmware/sdk/apps/gameboy/include/cardboy/apps/gameboy_app.hpp similarity index 100% rename from Firmware/sdk/include/cardboy/apps/gameboy_app.hpp rename to Firmware/sdk/apps/gameboy/include/cardboy/apps/gameboy_app.hpp diff --git a/Firmware/sdk/include/cardboy/apps/peanut_gb.h b/Firmware/sdk/apps/gameboy/include/cardboy/apps/peanut_gb.h similarity index 100% rename from Firmware/sdk/include/cardboy/apps/peanut_gb.h rename to Firmware/sdk/apps/gameboy/include/cardboy/apps/peanut_gb.h diff --git a/Firmware/sdk/apps/gameboy_app.cpp b/Firmware/sdk/apps/gameboy/src/gameboy_app.cpp similarity index 100% rename from Firmware/sdk/apps/gameboy_app.cpp rename to Firmware/sdk/apps/gameboy/src/gameboy_app.cpp diff --git a/Firmware/sdk/apps/menu/CMakeLists.txt b/Firmware/sdk/apps/menu/CMakeLists.txt new file mode 100644 index 0000000..8fba440 --- /dev/null +++ b/Firmware/sdk/apps/menu/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(cardboy_apps + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/menu_app.cpp +) + +target_include_directories(cardboy_apps + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) diff --git a/Firmware/sdk/include/cardboy/apps/menu_app.hpp b/Firmware/sdk/apps/menu/include/cardboy/apps/menu_app.hpp similarity index 100% rename from Firmware/sdk/include/cardboy/apps/menu_app.hpp rename to Firmware/sdk/apps/menu/include/cardboy/apps/menu_app.hpp diff --git a/Firmware/sdk/apps/menu_app.cpp b/Firmware/sdk/apps/menu/src/menu_app.cpp similarity index 100% rename from Firmware/sdk/apps/menu_app.cpp rename to Firmware/sdk/apps/menu/src/menu_app.cpp diff --git a/Firmware/sdk/apps/tetris/CMakeLists.txt b/Firmware/sdk/apps/tetris/CMakeLists.txt new file mode 100644 index 0000000..d2ba45b --- /dev/null +++ b/Firmware/sdk/apps/tetris/CMakeLists.txt @@ -0,0 +1,9 @@ +target_sources(cardboy_apps + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/src/tetris_app.cpp +) + +target_include_directories(cardboy_apps + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include +) diff --git a/Firmware/sdk/include/cardboy/apps/tetris_app.hpp b/Firmware/sdk/apps/tetris/include/cardboy/apps/tetris_app.hpp similarity index 100% rename from Firmware/sdk/include/cardboy/apps/tetris_app.hpp rename to Firmware/sdk/apps/tetris/include/cardboy/apps/tetris_app.hpp diff --git a/Firmware/sdk/apps/tetris_app.cpp b/Firmware/sdk/apps/tetris/src/tetris_app.cpp similarity index 100% rename from Firmware/sdk/apps/tetris_app.cpp rename to Firmware/sdk/apps/tetris/src/tetris_app.cpp diff --git a/Firmware/sdk/backend_interface/CMakeLists.txt b/Firmware/sdk/backend_interface/CMakeLists.txt new file mode 100644 index 0000000..e45b40a --- /dev/null +++ b/Firmware/sdk/backend_interface/CMakeLists.txt @@ -0,0 +1,15 @@ +add_library(cardboy_backend_interface INTERFACE) + +set_target_properties(cardboy_backend_interface PROPERTIES + EXPORT_NAME backend_interface +) + +target_include_directories(cardboy_backend_interface + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include +) + +target_sources(cardboy_backend_interface + INTERFACE + ${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/backend/backend_interface.hpp +) diff --git a/Firmware/sdk/backend_interface/include/cardboy/backend/backend_interface.hpp b/Firmware/sdk/backend_interface/include/cardboy/backend/backend_interface.hpp new file mode 100644 index 0000000..2075b42 --- /dev/null +++ b/Firmware/sdk/backend_interface/include/cardboy/backend/backend_interface.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +namespace cardboy::backend { + +template +concept BackendInterface = requires { + typename Backend::Framebuffer; + typename Backend::Input; + typename Backend::Clock; +}; + +} // namespace cardboy::backend diff --git a/Firmware/sdk/backends/desktop/CMakeLists.txt b/Firmware/sdk/backends/desktop/CMakeLists.txt new file mode 100644 index 0000000..042f0cd --- /dev/null +++ b/Firmware/sdk/backends/desktop/CMakeLists.txt @@ -0,0 +1,40 @@ +include(FetchContent) + +set(SFML_BUILD_AUDIO OFF CACHE BOOL "Disable SFML audio module" FORCE) +set(SFML_BUILD_NETWORK OFF CACHE BOOL "Disable SFML network module" FORCE) +set(SFML_BUILD_EXAMPLES OFF CACHE BOOL "Disable SFML examples" FORCE) +set(SFML_BUILD_TESTS OFF CACHE BOOL "Disable SFML tests" FORCE) +set(SFML_USE_SYSTEM_DEPS OFF CACHE BOOL "Use bundled SFML dependencies" FORCE) + +FetchContent_Declare( + SFML + GIT_REPOSITORY https://github.com/SFML/SFML.git + GIT_TAG 3.0.2 + GIT_SHALLOW ON +) +FetchContent_MakeAvailable(SFML) + +add_library(cardboy_backend_desktop STATIC + src/desktop_backend.cpp +) + +set_target_properties(cardboy_backend_desktop PROPERTIES + EXPORT_NAME backend_desktop +) + +target_include_directories(cardboy_backend_desktop + PUBLIC + ${CMAKE_CURRENT_SOURCE_DIR}/include + PRIVATE + ${CMAKE_CURRENT_SOURCE_DIR}/../../include +) + +target_link_libraries(cardboy_backend_desktop + PUBLIC + cardboy_backend_interface + SFML::Window + SFML::Graphics + SFML::System +) + +set(CARDBOY_DESKTOP_BACKEND_TARGET cardboy_backend_desktop PARENT_SCOPE) diff --git a/Firmware/sdk/hosts/include/cardboy/backend/backend_impl.hpp b/Firmware/sdk/backends/desktop/include/cardboy/backend/backend_impl.hpp similarity index 99% rename from Firmware/sdk/hosts/include/cardboy/backend/backend_impl.hpp rename to Firmware/sdk/backends/desktop/include/cardboy/backend/backend_impl.hpp index fdd4ca8..b84bf29 100644 --- a/Firmware/sdk/hosts/include/cardboy/backend/backend_impl.hpp +++ b/Firmware/sdk/backends/desktop/include/cardboy/backend/backend_impl.hpp @@ -5,4 +5,3 @@ namespace cardboy::backend { using ActiveBackend = DesktopBackend; } - diff --git a/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp new file mode 100644 index 0000000..816c745 --- /dev/null +++ b/Firmware/sdk/backends/desktop/include/cardboy/backend/desktop_backend.hpp @@ -0,0 +1,186 @@ +#pragma once + +#include "cardboy/sdk/platform.hpp" +#include "cardboy/sdk/services.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace cardboy::backend::desktop { + +constexpr int kPixelScale = 2; + +class DesktopRuntime; + +class DesktopBuzzer final : public cardboy::sdk::IBuzzer { +public: + void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {} + void beepRotate() override {} + void beepMove() override {} + void beepLock() override {} + void beepLines(int) override {} + void beepLevelUp(int) override {} + void beepGameOver() override {} +}; + +class DesktopBattery final : public cardboy::sdk::IBatteryMonitor { +public: + [[nodiscard]] bool hasData() const override { return false; } +}; + +class DesktopStorage final : public cardboy::sdk::IStorage { +public: + [[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override; + void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override; + +private: + std::unordered_map data; + + static std::string composeKey(std::string_view ns, std::string_view key); +}; + +class DesktopRandom final : public cardboy::sdk::IRandom { +public: + DesktopRandom(); + + [[nodiscard]] std::uint32_t nextUint32() override; + +private: + std::mt19937 rng; + std::uniform_int_distribution dist; +}; + +class DesktopHighResClock final : public cardboy::sdk::IHighResClock { +public: + DesktopHighResClock(); + + [[nodiscard]] std::uint64_t micros() override; + +private: + const std::chrono::steady_clock::time_point start; +}; + +class DesktopPowerManager final : public cardboy::sdk::IPowerManager { +public: + void setSlowMode(bool enable) override { slowMode = enable; } + [[nodiscard]] bool isSlowMode() const override { return slowMode; } + +private: + bool slowMode = false; +}; + +class DesktopFilesystem final : public cardboy::sdk::IFilesystem { +public: + DesktopFilesystem(); + + bool mount() override; + [[nodiscard]] bool isMounted() const override { return mounted; } + [[nodiscard]] std::string basePath() const override { return basePathPath.string(); } + +private: + std::filesystem::path basePathPath; + bool mounted = false; +}; + +class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { +public: + explicit DesktopFramebuffer(DesktopRuntime& runtime); + + [[nodiscard]] int width_impl() const; + [[nodiscard]] int height_impl() const; + void drawPixel_impl(int x, int y, bool on); + void clear_impl(bool on); + void frameReady_impl(); + void sendFrame_impl(bool clearAfterSend); + [[nodiscard]] bool frameInFlight_impl() const { return false; } + +private: + DesktopRuntime& runtime; +}; + +class DesktopInput final : public cardboy::sdk::InputFacade { +public: + explicit DesktopInput(DesktopRuntime& runtime); + + cardboy::sdk::InputState readState_impl(); + void handleKey(sf::Keyboard::Key key, bool pressed); + +private: + DesktopRuntime& runtime; + cardboy::sdk::InputState state{}; +}; + +class DesktopClock final : public cardboy::sdk::ClockFacade { +public: + explicit DesktopClock(DesktopRuntime& runtime); + + std::uint32_t millis_impl(); + void sleep_ms_impl(std::uint32_t ms); + +private: + DesktopRuntime& runtime; + const std::chrono::steady_clock::time_point start; +}; + +class DesktopRuntime { +public: + DesktopRuntime(); + + cardboy::sdk::Services& serviceRegistry(); + void processEvents(); + void presentIfNeeded(); + void sleepFor(std::uint32_t ms); + + [[nodiscard]] bool isRunning() const { return running; } + + DesktopFramebuffer framebuffer; + DesktopInput input; + DesktopClock clock; + +private: + friend class DesktopFramebuffer; + friend class DesktopInput; + friend class DesktopClock; + + void setPixel(int x, int y, bool on); + void clearPixels(bool on); + + sf::RenderWindow window; + sf::Texture texture; + sf::Sprite sprite; + std::vector pixels; + bool dirty = true; + bool running = true; + bool clearNextFrame = true; + + DesktopBuzzer buzzerService; + DesktopBattery batteryService; + DesktopStorage storageService; + DesktopRandom randomService; + DesktopHighResClock highResService; + DesktopPowerManager powerService; + DesktopFilesystem filesystemService; + cardboy::sdk::Services services{}; +}; + +struct Backend { + using Framebuffer = DesktopFramebuffer; + using Input = DesktopInput; + using Clock = DesktopClock; +}; + +} // namespace cardboy::backend::desktop + +namespace cardboy::backend { +using DesktopBackend = desktop::Backend; +} // namespace cardboy::backend diff --git a/Firmware/sdk/backends/desktop/src/desktop_backend.cpp b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp new file mode 100644 index 0000000..64a83f2 --- /dev/null +++ b/Firmware/sdk/backends/desktop/src/desktop_backend.cpp @@ -0,0 +1,235 @@ +#include "cardboy/backend/desktop_backend.hpp" + +#include "cardboy/sdk/display_spec.hpp" + +#include +#include + +#include +#include +#include +#include +#include + +namespace cardboy::backend::desktop { + +bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) { + auto it = data.find(composeKey(ns, key)); + if (it == data.end()) + return false; + out = it->second; + return true; +} + +void DesktopStorage::writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) { + data[composeKey(ns, key)] = value; +} + +std::string DesktopStorage::composeKey(std::string_view ns, std::string_view key) { + std::string result; + result.reserve(ns.size() + key.size() + 1); + result.append(ns.begin(), ns.end()); + result.push_back(':'); + result.append(key.begin(), key.end()); + return result; +} + +DesktopRandom::DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits::max()) {} + +std::uint32_t DesktopRandom::nextUint32() { return dist(rng); } + +DesktopHighResClock::DesktopHighResClock() : start(std::chrono::steady_clock::now()) {} + +std::uint64_t DesktopHighResClock::micros() { + const auto now = std::chrono::steady_clock::now(); + return static_cast(std::chrono::duration_cast(now - start).count()); +} + +DesktopFilesystem::DesktopFilesystem() { + if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) { + basePathPath = std::filesystem::path(env); + } else { + basePathPath = std::filesystem::current_path() / "roms"; + } +} + +bool DesktopFilesystem::mount() { + std::error_code ec; + if (std::filesystem::exists(basePathPath, ec)) { + mounted = std::filesystem::is_directory(basePathPath, ec); + } else { + mounted = std::filesystem::create_directories(basePathPath, ec); + } + return mounted; +} + +DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {} + +int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; } + +int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; } + +void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); } + +void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); } + +void DesktopFramebuffer::frameReady_impl() { + if (runtime.clearNextFrame) { + runtime.clearPixels(false); + runtime.clearNextFrame = false; + } +} + +void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) { + runtime.clearNextFrame = clearAfterSend; + runtime.dirty = true; +} + +DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {} + +cardboy::sdk::InputState DesktopInput::readState_impl() { return state; } + +void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) { + switch (key) { + case sf::Keyboard::Key::Up: + state.up = pressed; + break; + case sf::Keyboard::Key::Down: + state.down = pressed; + break; + case sf::Keyboard::Key::Left: + state.left = pressed; + break; + case sf::Keyboard::Key::Right: + state.right = pressed; + break; + case sf::Keyboard::Key::Z: + case sf::Keyboard::Key::A: + state.a = pressed; + break; + case sf::Keyboard::Key::X: + case sf::Keyboard::Key::S: + state.b = pressed; + break; + case sf::Keyboard::Key::Space: + state.select = pressed; + break; + case sf::Keyboard::Key::Enter: + state.start = pressed; + break; + default: + break; + } +} + +DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {} + +std::uint32_t DesktopClock::millis_impl() { + const auto now = std::chrono::steady_clock::now(); + return static_cast(std::chrono::duration_cast(now - start).count()); +} + +void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); } + +DesktopRuntime::DesktopRuntime() : + window(sf::VideoMode( + sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale, cardboy::sdk::kDisplayHeight * kPixelScale}), + "Cardboy Desktop"), + texture(), sprite(texture), + pixels(static_cast(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(kPixelScale), static_cast(kPixelScale)}); + clearPixels(false); + presentIfNeeded(); + + services.buzzer = &buzzerService; + services.battery = &batteryService; + services.storage = &storageService; + services.random = &randomService; + services.highResClock = &highResService; + services.powerManager = &powerService; + services.filesystem = &filesystemService; +} + +cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; } + +void DesktopRuntime::setPixel(int x, int y, bool on) { + if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight) + return; + const std::size_t idx = static_cast(y * cardboy::sdk::kDisplayWidth + x) * 4; + const std::uint8_t value = on ? static_cast(255) : static_cast(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(255) : static_cast(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()) { + running = false; + window.close(); + continue; + } + + if (const auto* keyPressed = event.getIf()) { + input.handleKey(keyPressed->code, true); + continue; + } + + if (const auto* keyReleased = event.getIf()) { + 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(target - now); + if (remaining.count() > 2) + std::this_thread::sleep_for(std::min(remaining, std::chrono::milliseconds(8))); + else + std::this_thread::yield(); + } while (true); +} + +} // namespace cardboy::backend::desktop diff --git a/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp b/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp index e296383..816c745 100644 --- a/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp +++ b/Firmware/sdk/hosts/include/cardboy/backend/desktop_backend.hpp @@ -1,15 +1,97 @@ #pragma once #include "cardboy/sdk/platform.hpp" +#include "cardboy/sdk/services.hpp" +#include #include #include #include +#include +#include +#include +#include +#include +#include +#include +#include namespace cardboy::backend::desktop { +constexpr int kPixelScale = 2; + class DesktopRuntime; +class DesktopBuzzer final : public cardboy::sdk::IBuzzer { +public: + void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {} + void beepRotate() override {} + void beepMove() override {} + void beepLock() override {} + void beepLines(int) override {} + void beepLevelUp(int) override {} + void beepGameOver() override {} +}; + +class DesktopBattery final : public cardboy::sdk::IBatteryMonitor { +public: + [[nodiscard]] bool hasData() const override { return false; } +}; + +class DesktopStorage final : public cardboy::sdk::IStorage { +public: + [[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override; + void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override; + +private: + std::unordered_map data; + + static std::string composeKey(std::string_view ns, std::string_view key); +}; + +class DesktopRandom final : public cardboy::sdk::IRandom { +public: + DesktopRandom(); + + [[nodiscard]] std::uint32_t nextUint32() override; + +private: + std::mt19937 rng; + std::uniform_int_distribution dist; +}; + +class DesktopHighResClock final : public cardboy::sdk::IHighResClock { +public: + DesktopHighResClock(); + + [[nodiscard]] std::uint64_t micros() override; + +private: + const std::chrono::steady_clock::time_point start; +}; + +class DesktopPowerManager final : public cardboy::sdk::IPowerManager { +public: + void setSlowMode(bool enable) override { slowMode = enable; } + [[nodiscard]] bool isSlowMode() const override { return slowMode; } + +private: + bool slowMode = false; +}; + +class DesktopFilesystem final : public cardboy::sdk::IFilesystem { +public: + DesktopFilesystem(); + + bool mount() override; + [[nodiscard]] bool isMounted() const override { return mounted; } + [[nodiscard]] std::string basePath() const override { return basePathPath.string(); } + +private: + std::filesystem::path basePathPath; + bool mounted = false; +}; + class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade { public: explicit DesktopFramebuffer(DesktopRuntime& runtime); @@ -50,6 +132,47 @@ private: const std::chrono::steady_clock::time_point start; }; +class DesktopRuntime { +public: + DesktopRuntime(); + + cardboy::sdk::Services& serviceRegistry(); + void processEvents(); + void presentIfNeeded(); + void sleepFor(std::uint32_t ms); + + [[nodiscard]] bool isRunning() const { return running; } + + DesktopFramebuffer framebuffer; + DesktopInput input; + DesktopClock clock; + +private: + friend class DesktopFramebuffer; + friend class DesktopInput; + friend class DesktopClock; + + void setPixel(int x, int y, bool on); + void clearPixels(bool on); + + sf::RenderWindow window; + sf::Texture texture; + sf::Sprite sprite; + std::vector pixels; + bool dirty = true; + bool running = true; + bool clearNextFrame = true; + + DesktopBuzzer buzzerService; + DesktopBattery batteryService; + DesktopStorage storageService; + DesktopRandom randomService; + DesktopHighResClock highResService; + DesktopPowerManager powerService; + DesktopFilesystem filesystemService; + cardboy::sdk::Services services{}; +}; + struct Backend { using Framebuffer = DesktopFramebuffer; using Input = DesktopInput; diff --git a/Firmware/sdk/hosts/sfml_main.cpp b/Firmware/sdk/hosts/sfml_main.cpp index 52f25e4..3c8e0c8 100644 --- a/Firmware/sdk/hosts/sfml_main.cpp +++ b/Firmware/sdk/hosts/sfml_main.cpp @@ -4,354 +4,9 @@ #include "cardboy/apps/tetris_app.hpp" #include "cardboy/backend/desktop_backend.hpp" #include "cardboy/sdk/app_system.hpp" -#include "cardboy/sdk/display_spec.hpp" -#include "cardboy/sdk/services.hpp" -#include -#include - -#include -#include #include -#include -#include #include -#include -#include -#include -#include -#include -#include -#include -#include -#include -#include - -namespace cardboy::backend::desktop { - -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 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::max()) {} - - [[nodiscard]] std::uint32_t nextUint32() override { return dist(rng); } - -private: - std::mt19937 rng; - std::uniform_int_distribution 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::chrono::duration_cast(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 { -private: - friend class DesktopFramebuffer; - friend class DesktopInput; - friend class DesktopClock; - - sf::RenderWindow window; - sf::Texture texture; - sf::Sprite sprite; - std::vector pixels; // RGBA buffer - bool dirty = true; - bool running = true; - bool clearNextFrame = true; - - DesktopBuzzer buzzerService; - DesktopBattery batteryService; - DesktopStorage storageService; - DesktopRandom randomService; - DesktopHighResClock highResService; - DesktopPowerManager powerService; - DesktopFilesystem filesystemService; - cardboy::sdk::Services services{}; - -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(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(kPixelScale), static_cast(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(y * cardboy::sdk::kDisplayWidth + x) * 4; - const std::uint8_t value = on ? static_cast(255) : static_cast(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(255) : static_cast(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()) { - running = false; - window.close(); - continue; - } - - if (const auto* keyPressed = event.getIf()) { - input.handleKey(keyPressed->code, true); - continue; - } - - if (const auto* keyReleased = event.getIf()) { - 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(target - now); - if (remaining.count() > 2) - std::this_thread::sleep_for(std::min(remaining, std::chrono::milliseconds(8))); - else - std::this_thread::yield(); - } while (true); -} - -DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {} - -int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; } - -int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; } - -void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); } - -void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); } - -void DesktopFramebuffer::frameReady_impl() { - if (runtime.clearNextFrame) { - runtime.clearPixels(false); - runtime.clearNextFrame = false; - } -} - -void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) { - runtime.clearNextFrame = clearAfterSend; - runtime.dirty = true; -} - -DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {} - -cardboy::sdk::InputState DesktopInput::readState_impl() { return state; } - -void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) { - switch (key) { - case sf::Keyboard::Key::Up: - state.up = pressed; - break; - case sf::Keyboard::Key::Down: - state.down = pressed; - break; - case sf::Keyboard::Key::Left: - state.left = pressed; - break; - case sf::Keyboard::Key::Right: - state.right = pressed; - break; - case sf::Keyboard::Key::Z: - case sf::Keyboard::Key::A: - state.a = pressed; - break; - case sf::Keyboard::Key::X: - case sf::Keyboard::Key::S: - state.b = pressed; - break; - case sf::Keyboard::Key::Space: - state.select = pressed; - break; - case sf::Keyboard::Key::Enter: - state.start = pressed; - break; - default: - break; - } -} - -DesktopClock::DesktopClock(DesktopRuntime& runtime) - : runtime(runtime), start(std::chrono::steady_clock::now()) {} - -std::uint32_t DesktopClock::millis_impl() { - const auto now = std::chrono::steady_clock::now(); - return static_cast( - std::chrono::duration_cast(now - start).count()); -} - -void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); } - -} // namespace cardboy::backend::desktop using cardboy::backend::desktop::DesktopRuntime; @@ -361,7 +16,7 @@ int main() { cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock); context.services = &runtime.serviceRegistry(); - cardboy::sdk::AppSystem system(context); + cardboy::sdk::AppSystem system(context); system.registerApp(apps::createMenuAppFactory()); system.registerApp(apps::createClockAppFactory()); diff --git a/Firmware/sdk/include/cardboy/sdk/backend.hpp b/Firmware/sdk/include/cardboy/sdk/backend.hpp index d0b61c9..b9ef05d 100644 --- a/Firmware/sdk/include/cardboy/sdk/backend.hpp +++ b/Firmware/sdk/include/cardboy/sdk/backend.hpp @@ -3,3 +3,8 @@ #include "platform.hpp" #include "cardboy/backend/backend_impl.hpp" +#include "cardboy/backend/backend_interface.hpp" + +namespace cardboy::backend { +static_assert(BackendInterface, "ActiveBackend must provide Framebuffer, Input, Clock types"); +} // namespace cardboy::backend diff --git a/Firmware/sdk/launchers/desktop/CMakeLists.txt b/Firmware/sdk/launchers/desktop/CMakeLists.txt new file mode 100644 index 0000000..7ec1d50 --- /dev/null +++ b/Firmware/sdk/launchers/desktop/CMakeLists.txt @@ -0,0 +1,16 @@ +add_executable(cardboy_desktop + src/main.cpp +) + +set_target_properties(cardboy_desktop PROPERTIES + EXPORT_NAME desktop_launcher +) + +target_link_libraries(cardboy_desktop + PRIVATE + cardboy_apps + cardboy_sdk + ${CARDBOY_SDK_BACKEND_LIBRARY} +) + +target_compile_features(cardboy_desktop PRIVATE cxx_std_20) diff --git a/Firmware/sdk/launchers/desktop/src/main.cpp b/Firmware/sdk/launchers/desktop/src/main.cpp new file mode 100644 index 0000000..3c8e0c8 --- /dev/null +++ b/Firmware/sdk/launchers/desktop/src/main.cpp @@ -0,0 +1,33 @@ +#include "cardboy/apps/clock_app.hpp" +#include "cardboy/apps/gameboy_app.hpp" +#include "cardboy/apps/menu_app.hpp" +#include "cardboy/apps/tetris_app.hpp" +#include "cardboy/backend/desktop_backend.hpp" +#include "cardboy/sdk/app_system.hpp" + +#include +#include + +using cardboy::backend::desktop::DesktopRuntime; + +int main() { + try { + DesktopRuntime runtime; + + cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock); + context.services = &runtime.serviceRegistry(); + cardboy::sdk::AppSystem system(context); + + system.registerApp(apps::createMenuAppFactory()); + system.registerApp(apps::createClockAppFactory()); + system.registerApp(apps::createGameboyAppFactory()); + system.registerApp(apps::createTetrisAppFactory()); + + system.run(); + } catch (const std::exception& ex) { + std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what()); + return 1; + } + + return 0; +}