mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
cleaner backend
This commit is contained in:
@@ -5,7 +5,25 @@ set(CMAKE_CXX_STANDARD 20)
|
|||||||
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
set(CMAKE_CXX_STANDARD_REQUIRED YES)
|
||||||
set(CMAKE_CXX_EXTENSIONS NO)
|
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
|
add_library(cardboy_sdk STATIC
|
||||||
src/app_system.cpp
|
src/app_system.cpp
|
||||||
@@ -17,67 +35,15 @@ target_include_directories(cardboy_sdk
|
|||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
target_compile_features(cardboy_sdk PUBLIC cxx_std_20)
|
||||||
target_link_libraries(cardboy_sdk PUBLIC cardboy_backend)
|
|
||||||
|
|
||||||
add_library(cardboy_apps STATIC
|
target_link_libraries(cardboy_sdk
|
||||||
apps/menu_app.cpp
|
|
||||||
apps/clock_app.cpp
|
|
||||||
apps/tetris_app.cpp
|
|
||||||
apps/gameboy_app.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_include_directories(cardboy_apps
|
|
||||||
PUBLIC
|
PUBLIC
|
||||||
${CMAKE_CURRENT_SOURCE_DIR}/include
|
cardboy_backend_interface
|
||||||
|
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||||
)
|
)
|
||||||
|
|
||||||
target_link_libraries(cardboy_apps
|
add_subdirectory(apps)
|
||||||
PUBLIC
|
|
||||||
cardboy_sdk
|
|
||||||
cardboy_backend
|
|
||||||
)
|
|
||||||
|
|
||||||
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
|
||||||
|
|
||||||
option(CARDBOY_BUILD_SFML "Build SFML harness" OFF)
|
|
||||||
|
|
||||||
if (CARDBOY_BUILD_SFML)
|
if (CARDBOY_BUILD_SFML)
|
||||||
include(FetchContent)
|
add_subdirectory(launchers/desktop)
|
||||||
|
|
||||||
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)
|
|
||||||
endif ()
|
endif ()
|
||||||
|
|||||||
18
Firmware/sdk/apps/CMakeLists.txt
Normal file
18
Firmware/sdk/apps/CMakeLists.txt
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
add_library(cardboy_apps STATIC)
|
||||||
|
|
||||||
|
set_target_properties(cardboy_apps PROPERTIES
|
||||||
|
EXPORT_NAME apps
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
cardboy_sdk
|
||||||
|
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(cardboy_apps PUBLIC cxx_std_20)
|
||||||
|
|
||||||
|
add_subdirectory(menu)
|
||||||
|
add_subdirectory(clock)
|
||||||
|
add_subdirectory(gameboy)
|
||||||
|
add_subdirectory(tetris)
|
||||||
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/clock/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
target_sources(cardboy_apps
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/clock_app.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
10
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
10
Firmware/sdk/apps/gameboy/CMakeLists.txt
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
target_sources(cardboy_apps
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/gameboy_app.cpp
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/apps/peanut_gb.h
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/menu/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
target_sources(cardboy_apps
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/menu_app.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
9
Firmware/sdk/apps/tetris/CMakeLists.txt
Normal file
@@ -0,0 +1,9 @@
|
|||||||
|
target_sources(cardboy_apps
|
||||||
|
PRIVATE
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/src/tetris_app.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
target_include_directories(cardboy_apps
|
||||||
|
PUBLIC
|
||||||
|
${CMAKE_CURRENT_SOURCE_DIR}/include
|
||||||
|
)
|
||||||
15
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
15
Firmware/sdk/backend_interface/CMakeLists.txt
Normal file
@@ -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
|
||||||
|
)
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <concepts>
|
||||||
|
|
||||||
|
namespace cardboy::backend {
|
||||||
|
|
||||||
|
template<typename Backend>
|
||||||
|
concept BackendInterface = requires {
|
||||||
|
typename Backend::Framebuffer;
|
||||||
|
typename Backend::Input;
|
||||||
|
typename Backend::Clock;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::backend
|
||||||
40
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
40
Firmware/sdk/backends/desktop/CMakeLists.txt
Normal file
@@ -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)
|
||||||
@@ -5,4 +5,3 @@
|
|||||||
namespace cardboy::backend {
|
namespace cardboy::backend {
|
||||||
using ActiveBackend = DesktopBackend;
|
using ActiveBackend = DesktopBackend;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -0,0 +1,186 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "cardboy/sdk/platform.hpp"
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
|
#include <SFML/Graphics.hpp>
|
||||||
|
#include <SFML/Window/Keyboard.hpp>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <limits>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace cardboy::backend::desktop {
|
||||||
|
|
||||||
|
constexpr int kPixelScale = 2;
|
||||||
|
|
||||||
|
class DesktopRuntime;
|
||||||
|
|
||||||
|
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||||
|
public:
|
||||||
|
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||||
|
void beepRotate() override {}
|
||||||
|
void beepMove() override {}
|
||||||
|
void beepLock() override {}
|
||||||
|
void beepLines(int) override {}
|
||||||
|
void beepLevelUp(int) override {}
|
||||||
|
void beepGameOver() override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool hasData() const override { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
|
||||||
|
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::uint32_t> data;
|
||||||
|
|
||||||
|
static std::string composeKey(std::string_view ns, std::string_view key);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||||
|
public:
|
||||||
|
DesktopRandom();
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t nextUint32() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mt19937 rng;
|
||||||
|
std::uniform_int_distribution<std::uint32_t> dist;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||||
|
public:
|
||||||
|
DesktopHighResClock();
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t micros() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::chrono::steady_clock::time_point start;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopPowerManager final : public cardboy::sdk::IPowerManager {
|
||||||
|
public:
|
||||||
|
void setSlowMode(bool enable) override { slowMode = enable; }
|
||||||
|
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool slowMode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
|
||||||
|
public:
|
||||||
|
DesktopFilesystem();
|
||||||
|
|
||||||
|
bool mount() override;
|
||||||
|
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||||
|
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path basePathPath;
|
||||||
|
bool mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
|
||||||
|
public:
|
||||||
|
explicit DesktopFramebuffer(DesktopRuntime& runtime);
|
||||||
|
|
||||||
|
[[nodiscard]] int width_impl() const;
|
||||||
|
[[nodiscard]] int height_impl() const;
|
||||||
|
void drawPixel_impl(int x, int y, bool on);
|
||||||
|
void clear_impl(bool on);
|
||||||
|
void frameReady_impl();
|
||||||
|
void sendFrame_impl(bool clearAfterSend);
|
||||||
|
[[nodiscard]] bool frameInFlight_impl() const { return false; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopRuntime& runtime;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
|
||||||
|
public:
|
||||||
|
explicit DesktopInput(DesktopRuntime& runtime);
|
||||||
|
|
||||||
|
cardboy::sdk::InputState readState_impl();
|
||||||
|
void handleKey(sf::Keyboard::Key key, bool pressed);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopRuntime& runtime;
|
||||||
|
cardboy::sdk::InputState state{};
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
|
||||||
|
public:
|
||||||
|
explicit DesktopClock(DesktopRuntime& runtime);
|
||||||
|
|
||||||
|
std::uint32_t millis_impl();
|
||||||
|
void sleep_ms_impl(std::uint32_t ms);
|
||||||
|
|
||||||
|
private:
|
||||||
|
DesktopRuntime& runtime;
|
||||||
|
const std::chrono::steady_clock::time_point start;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopRuntime {
|
||||||
|
public:
|
||||||
|
DesktopRuntime();
|
||||||
|
|
||||||
|
cardboy::sdk::Services& serviceRegistry();
|
||||||
|
void processEvents();
|
||||||
|
void presentIfNeeded();
|
||||||
|
void sleepFor(std::uint32_t ms);
|
||||||
|
|
||||||
|
[[nodiscard]] bool isRunning() const { return running; }
|
||||||
|
|
||||||
|
DesktopFramebuffer framebuffer;
|
||||||
|
DesktopInput input;
|
||||||
|
DesktopClock clock;
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class DesktopFramebuffer;
|
||||||
|
friend class DesktopInput;
|
||||||
|
friend class DesktopClock;
|
||||||
|
|
||||||
|
void setPixel(int x, int y, bool on);
|
||||||
|
void clearPixels(bool on);
|
||||||
|
|
||||||
|
sf::RenderWindow window;
|
||||||
|
sf::Texture texture;
|
||||||
|
sf::Sprite sprite;
|
||||||
|
std::vector<std::uint8_t> pixels;
|
||||||
|
bool dirty = true;
|
||||||
|
bool running = true;
|
||||||
|
bool clearNextFrame = true;
|
||||||
|
|
||||||
|
DesktopBuzzer buzzerService;
|
||||||
|
DesktopBattery batteryService;
|
||||||
|
DesktopStorage storageService;
|
||||||
|
DesktopRandom randomService;
|
||||||
|
DesktopHighResClock highResService;
|
||||||
|
DesktopPowerManager powerService;
|
||||||
|
DesktopFilesystem filesystemService;
|
||||||
|
cardboy::sdk::Services services{};
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Backend {
|
||||||
|
using Framebuffer = DesktopFramebuffer;
|
||||||
|
using Input = DesktopInput;
|
||||||
|
using Clock = DesktopClock;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace cardboy::backend::desktop
|
||||||
|
|
||||||
|
namespace cardboy::backend {
|
||||||
|
using DesktopBackend = desktop::Backend;
|
||||||
|
} // namespace cardboy::backend
|
||||||
235
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
235
Firmware/sdk/backends/desktop/src/desktop_backend.cpp
Normal file
@@ -0,0 +1,235 @@
|
|||||||
|
#include "cardboy/backend/desktop_backend.hpp"
|
||||||
|
|
||||||
|
#include "cardboy/sdk/display_spec.hpp"
|
||||||
|
|
||||||
|
#include <SFML/Graphics.hpp>
|
||||||
|
#include <SFML/Window.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <chrono>
|
||||||
|
#include <cstdlib>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <system_error>
|
||||||
|
|
||||||
|
namespace cardboy::backend::desktop {
|
||||||
|
|
||||||
|
bool DesktopStorage::readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) {
|
||||||
|
auto it = data.find(composeKey(ns, key));
|
||||||
|
if (it == data.end())
|
||||||
|
return false;
|
||||||
|
out = it->second;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopStorage::writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) {
|
||||||
|
data[composeKey(ns, key)] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string DesktopStorage::composeKey(std::string_view ns, std::string_view key) {
|
||||||
|
std::string result;
|
||||||
|
result.reserve(ns.size() + key.size() + 1);
|
||||||
|
result.append(ns.begin(), ns.end());
|
||||||
|
result.push_back(':');
|
||||||
|
result.append(key.begin(), key.end());
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopRandom::DesktopRandom() : rng(std::random_device{}()), dist(0u, std::numeric_limits<std::uint32_t>::max()) {}
|
||||||
|
|
||||||
|
std::uint32_t DesktopRandom::nextUint32() { return dist(rng); }
|
||||||
|
|
||||||
|
DesktopHighResClock::DesktopHighResClock() : start(std::chrono::steady_clock::now()) {}
|
||||||
|
|
||||||
|
std::uint64_t DesktopHighResClock::micros() {
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
return static_cast<std::uint64_t>(std::chrono::duration_cast<std::chrono::microseconds>(now - start).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopFilesystem::DesktopFilesystem() {
|
||||||
|
if (const char* env = std::getenv("CARDBOY_ROM_DIR"); env && *env) {
|
||||||
|
basePathPath = std::filesystem::path(env);
|
||||||
|
} else {
|
||||||
|
basePathPath = std::filesystem::current_path() / "roms";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool DesktopFilesystem::mount() {
|
||||||
|
std::error_code ec;
|
||||||
|
if (std::filesystem::exists(basePathPath, ec)) {
|
||||||
|
mounted = std::filesystem::is_directory(basePathPath, ec);
|
||||||
|
} else {
|
||||||
|
mounted = std::filesystem::create_directories(basePathPath, ec);
|
||||||
|
}
|
||||||
|
return mounted;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||||
|
|
||||||
|
int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
||||||
|
|
||||||
|
int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
||||||
|
|
||||||
|
void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
||||||
|
|
||||||
|
void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); }
|
||||||
|
|
||||||
|
void DesktopFramebuffer::frameReady_impl() {
|
||||||
|
if (runtime.clearNextFrame) {
|
||||||
|
runtime.clearPixels(false);
|
||||||
|
runtime.clearNextFrame = false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) {
|
||||||
|
runtime.clearNextFrame = clearAfterSend;
|
||||||
|
runtime.dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
||||||
|
|
||||||
|
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
|
||||||
|
|
||||||
|
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
||||||
|
switch (key) {
|
||||||
|
case sf::Keyboard::Key::Up:
|
||||||
|
state.up = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Down:
|
||||||
|
state.down = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Left:
|
||||||
|
state.left = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Right:
|
||||||
|
state.right = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Z:
|
||||||
|
case sf::Keyboard::Key::A:
|
||||||
|
state.a = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::X:
|
||||||
|
case sf::Keyboard::Key::S:
|
||||||
|
state.b = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Space:
|
||||||
|
state.select = pressed;
|
||||||
|
break;
|
||||||
|
case sf::Keyboard::Key::Enter:
|
||||||
|
state.start = pressed;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
DesktopClock::DesktopClock(DesktopRuntime& runtime) : runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
||||||
|
|
||||||
|
std::uint32_t DesktopClock::millis_impl() {
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
return static_cast<std::uint32_t>(std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); }
|
||||||
|
|
||||||
|
DesktopRuntime::DesktopRuntime() :
|
||||||
|
window(sf::VideoMode(
|
||||||
|
sf::Vector2u{cardboy::sdk::kDisplayWidth * kPixelScale, cardboy::sdk::kDisplayHeight * kPixelScale}),
|
||||||
|
"Cardboy Desktop"),
|
||||||
|
texture(), sprite(texture),
|
||||||
|
pixels(static_cast<std::size_t>(cardboy::sdk::kDisplayWidth * cardboy::sdk::kDisplayHeight) * 4, 0),
|
||||||
|
framebuffer(*this), input(*this), clock(*this) {
|
||||||
|
window.setFramerateLimit(60);
|
||||||
|
if (!texture.resize(sf::Vector2u{cardboy::sdk::kDisplayWidth, cardboy::sdk::kDisplayHeight}))
|
||||||
|
throw std::runtime_error("Failed to allocate texture for desktop framebuffer");
|
||||||
|
sprite.setTexture(texture, true);
|
||||||
|
sprite.setScale(sf::Vector2f{static_cast<float>(kPixelScale), static_cast<float>(kPixelScale)});
|
||||||
|
clearPixels(false);
|
||||||
|
presentIfNeeded();
|
||||||
|
|
||||||
|
services.buzzer = &buzzerService;
|
||||||
|
services.battery = &batteryService;
|
||||||
|
services.storage = &storageService;
|
||||||
|
services.random = &randomService;
|
||||||
|
services.highResClock = &highResService;
|
||||||
|
services.powerManager = &powerService;
|
||||||
|
services.filesystem = &filesystemService;
|
||||||
|
}
|
||||||
|
|
||||||
|
cardboy::sdk::Services& DesktopRuntime::serviceRegistry() { return services; }
|
||||||
|
|
||||||
|
void DesktopRuntime::setPixel(int x, int y, bool on) {
|
||||||
|
if (x < 0 || y < 0 || x >= cardboy::sdk::kDisplayWidth || y >= cardboy::sdk::kDisplayHeight)
|
||||||
|
return;
|
||||||
|
const std::size_t idx = static_cast<std::size_t>(y * cardboy::sdk::kDisplayWidth + x) * 4;
|
||||||
|
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||||
|
pixels[idx + 0] = value;
|
||||||
|
pixels[idx + 1] = value;
|
||||||
|
pixels[idx + 2] = value;
|
||||||
|
pixels[idx + 3] = 255;
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopRuntime::clearPixels(bool on) {
|
||||||
|
const std::uint8_t value = on ? static_cast<std::uint8_t>(255) : static_cast<std::uint8_t>(0);
|
||||||
|
for (std::size_t i = 0; i < pixels.size(); i += 4) {
|
||||||
|
pixels[i + 0] = value;
|
||||||
|
pixels[i + 1] = value;
|
||||||
|
pixels[i + 2] = value;
|
||||||
|
pixels[i + 3] = 255;
|
||||||
|
}
|
||||||
|
dirty = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopRuntime::processEvents() {
|
||||||
|
while (auto eventOpt = window.pollEvent()) {
|
||||||
|
const sf::Event& event = *eventOpt;
|
||||||
|
|
||||||
|
if (event.is<sf::Event::Closed>()) {
|
||||||
|
running = false;
|
||||||
|
window.close();
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* keyPressed = event.getIf<sf::Event::KeyPressed>()) {
|
||||||
|
input.handleKey(keyPressed->code, true);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (const auto* keyReleased = event.getIf<sf::Event::KeyReleased>()) {
|
||||||
|
input.handleKey(keyReleased->code, false);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopRuntime::presentIfNeeded() {
|
||||||
|
if (!dirty)
|
||||||
|
return;
|
||||||
|
texture.update(pixels.data());
|
||||||
|
window.clear(sf::Color::Black);
|
||||||
|
window.draw(sprite);
|
||||||
|
window.display();
|
||||||
|
dirty = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
void DesktopRuntime::sleepFor(std::uint32_t ms) {
|
||||||
|
const auto target = std::chrono::steady_clock::now() + std::chrono::milliseconds(ms);
|
||||||
|
do {
|
||||||
|
processEvents();
|
||||||
|
presentIfNeeded();
|
||||||
|
if (!running)
|
||||||
|
std::exit(0);
|
||||||
|
if (ms == 0)
|
||||||
|
return;
|
||||||
|
const auto now = std::chrono::steady_clock::now();
|
||||||
|
if (now >= target)
|
||||||
|
return;
|
||||||
|
const auto remaining = std::chrono::duration_cast<std::chrono::milliseconds>(target - now);
|
||||||
|
if (remaining.count() > 2)
|
||||||
|
std::this_thread::sleep_for(std::min<std::chrono::milliseconds>(remaining, std::chrono::milliseconds(8)));
|
||||||
|
else
|
||||||
|
std::this_thread::yield();
|
||||||
|
} while (true);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace cardboy::backend::desktop
|
||||||
@@ -1,15 +1,97 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "cardboy/sdk/platform.hpp"
|
#include "cardboy/sdk/platform.hpp"
|
||||||
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
|
#include <SFML/Graphics.hpp>
|
||||||
#include <SFML/Window/Keyboard.hpp>
|
#include <SFML/Window/Keyboard.hpp>
|
||||||
#include <chrono>
|
#include <chrono>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <limits>
|
||||||
|
#include <random>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <thread>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
namespace cardboy::backend::desktop {
|
namespace cardboy::backend::desktop {
|
||||||
|
|
||||||
|
constexpr int kPixelScale = 2;
|
||||||
|
|
||||||
class DesktopRuntime;
|
class DesktopRuntime;
|
||||||
|
|
||||||
|
class DesktopBuzzer final : public cardboy::sdk::IBuzzer {
|
||||||
|
public:
|
||||||
|
void tone(std::uint32_t, std::uint32_t, std::uint32_t) override {}
|
||||||
|
void beepRotate() override {}
|
||||||
|
void beepMove() override {}
|
||||||
|
void beepLock() override {}
|
||||||
|
void beepLines(int) override {}
|
||||||
|
void beepLevelUp(int) override {}
|
||||||
|
void beepGameOver() override {}
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopBattery final : public cardboy::sdk::IBatteryMonitor {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool hasData() const override { return false; }
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopStorage final : public cardboy::sdk::IStorage {
|
||||||
|
public:
|
||||||
|
[[nodiscard]] bool readUint32(std::string_view ns, std::string_view key, std::uint32_t& out) override;
|
||||||
|
void writeUint32(std::string_view ns, std::string_view key, std::uint32_t value) override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::unordered_map<std::string, std::uint32_t> data;
|
||||||
|
|
||||||
|
static std::string composeKey(std::string_view ns, std::string_view key);
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopRandom final : public cardboy::sdk::IRandom {
|
||||||
|
public:
|
||||||
|
DesktopRandom();
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint32_t nextUint32() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::mt19937 rng;
|
||||||
|
std::uniform_int_distribution<std::uint32_t> dist;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopHighResClock final : public cardboy::sdk::IHighResClock {
|
||||||
|
public:
|
||||||
|
DesktopHighResClock();
|
||||||
|
|
||||||
|
[[nodiscard]] std::uint64_t micros() override;
|
||||||
|
|
||||||
|
private:
|
||||||
|
const std::chrono::steady_clock::time_point start;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopPowerManager final : public cardboy::sdk::IPowerManager {
|
||||||
|
public:
|
||||||
|
void setSlowMode(bool enable) override { slowMode = enable; }
|
||||||
|
[[nodiscard]] bool isSlowMode() const override { return slowMode; }
|
||||||
|
|
||||||
|
private:
|
||||||
|
bool slowMode = false;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DesktopFilesystem final : public cardboy::sdk::IFilesystem {
|
||||||
|
public:
|
||||||
|
DesktopFilesystem();
|
||||||
|
|
||||||
|
bool mount() override;
|
||||||
|
[[nodiscard]] bool isMounted() const override { return mounted; }
|
||||||
|
[[nodiscard]] std::string basePath() const override { return basePathPath.string(); }
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path basePathPath;
|
||||||
|
bool mounted = false;
|
||||||
|
};
|
||||||
|
|
||||||
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
|
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
|
||||||
public:
|
public:
|
||||||
explicit DesktopFramebuffer(DesktopRuntime& runtime);
|
explicit DesktopFramebuffer(DesktopRuntime& runtime);
|
||||||
@@ -50,6 +132,47 @@ private:
|
|||||||
const std::chrono::steady_clock::time_point start;
|
const std::chrono::steady_clock::time_point start;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
class DesktopRuntime {
|
||||||
|
public:
|
||||||
|
DesktopRuntime();
|
||||||
|
|
||||||
|
cardboy::sdk::Services& serviceRegistry();
|
||||||
|
void processEvents();
|
||||||
|
void presentIfNeeded();
|
||||||
|
void sleepFor(std::uint32_t ms);
|
||||||
|
|
||||||
|
[[nodiscard]] bool isRunning() const { return running; }
|
||||||
|
|
||||||
|
DesktopFramebuffer framebuffer;
|
||||||
|
DesktopInput input;
|
||||||
|
DesktopClock clock;
|
||||||
|
|
||||||
|
private:
|
||||||
|
friend class DesktopFramebuffer;
|
||||||
|
friend class DesktopInput;
|
||||||
|
friend class DesktopClock;
|
||||||
|
|
||||||
|
void setPixel(int x, int y, bool on);
|
||||||
|
void clearPixels(bool on);
|
||||||
|
|
||||||
|
sf::RenderWindow window;
|
||||||
|
sf::Texture texture;
|
||||||
|
sf::Sprite sprite;
|
||||||
|
std::vector<std::uint8_t> pixels;
|
||||||
|
bool dirty = true;
|
||||||
|
bool running = true;
|
||||||
|
bool clearNextFrame = true;
|
||||||
|
|
||||||
|
DesktopBuzzer buzzerService;
|
||||||
|
DesktopBattery batteryService;
|
||||||
|
DesktopStorage storageService;
|
||||||
|
DesktopRandom randomService;
|
||||||
|
DesktopHighResClock highResService;
|
||||||
|
DesktopPowerManager powerService;
|
||||||
|
DesktopFilesystem filesystemService;
|
||||||
|
cardboy::sdk::Services services{};
|
||||||
|
};
|
||||||
|
|
||||||
struct Backend {
|
struct Backend {
|
||||||
using Framebuffer = DesktopFramebuffer;
|
using Framebuffer = DesktopFramebuffer;
|
||||||
using Input = DesktopInput;
|
using Input = DesktopInput;
|
||||||
|
|||||||
@@ -4,354 +4,9 @@
|
|||||||
#include "cardboy/apps/tetris_app.hpp"
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
#include "cardboy/backend/desktop_backend.hpp"
|
#include "cardboy/backend/desktop_backend.hpp"
|
||||||
#include "cardboy/sdk/app_system.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 <cstdio>
|
||||||
#include <cstdint>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <exception>
|
#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 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<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 {
|
|
||||||
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;
|
|
||||||
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<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);
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
|
|
||||||
|
|
||||||
int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
|
|
||||||
|
|
||||||
int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
|
|
||||||
|
|
||||||
void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); }
|
|
||||||
|
|
||||||
void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); }
|
|
||||||
|
|
||||||
void DesktopFramebuffer::frameReady_impl() {
|
|
||||||
if (runtime.clearNextFrame) {
|
|
||||||
runtime.clearPixels(false);
|
|
||||||
runtime.clearNextFrame = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) {
|
|
||||||
runtime.clearNextFrame = clearAfterSend;
|
|
||||||
runtime.dirty = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
|
|
||||||
|
|
||||||
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
|
|
||||||
|
|
||||||
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
|
|
||||||
switch (key) {
|
|
||||||
case sf::Keyboard::Key::Up:
|
|
||||||
state.up = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Down:
|
|
||||||
state.down = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Left:
|
|
||||||
state.left = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Right:
|
|
||||||
state.right = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Z:
|
|
||||||
case sf::Keyboard::Key::A:
|
|
||||||
state.a = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::X:
|
|
||||||
case sf::Keyboard::Key::S:
|
|
||||||
state.b = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Space:
|
|
||||||
state.select = pressed;
|
|
||||||
break;
|
|
||||||
case sf::Keyboard::Key::Enter:
|
|
||||||
state.start = pressed;
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
DesktopClock::DesktopClock(DesktopRuntime& runtime)
|
|
||||||
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
|
|
||||||
|
|
||||||
std::uint32_t DesktopClock::millis_impl() {
|
|
||||||
const auto now = std::chrono::steady_clock::now();
|
|
||||||
return static_cast<std::uint32_t>(
|
|
||||||
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
|
|
||||||
}
|
|
||||||
|
|
||||||
void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); }
|
|
||||||
|
|
||||||
} // namespace cardboy::backend::desktop
|
|
||||||
|
|
||||||
using cardboy::backend::desktop::DesktopRuntime;
|
using cardboy::backend::desktop::DesktopRuntime;
|
||||||
|
|
||||||
@@ -361,7 +16,7 @@ int main() {
|
|||||||
|
|
||||||
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||||
context.services = &runtime.serviceRegistry();
|
context.services = &runtime.serviceRegistry();
|
||||||
cardboy::sdk::AppSystem system(context);
|
cardboy::sdk::AppSystem system(context);
|
||||||
|
|
||||||
system.registerApp(apps::createMenuAppFactory());
|
system.registerApp(apps::createMenuAppFactory());
|
||||||
system.registerApp(apps::createClockAppFactory());
|
system.registerApp(apps::createClockAppFactory());
|
||||||
|
|||||||
@@ -3,3 +3,8 @@
|
|||||||
#include "platform.hpp"
|
#include "platform.hpp"
|
||||||
|
|
||||||
#include "cardboy/backend/backend_impl.hpp"
|
#include "cardboy/backend/backend_impl.hpp"
|
||||||
|
#include "cardboy/backend/backend_interface.hpp"
|
||||||
|
|
||||||
|
namespace cardboy::backend {
|
||||||
|
static_assert(BackendInterface<ActiveBackend>, "ActiveBackend must provide Framebuffer, Input, Clock types");
|
||||||
|
} // namespace cardboy::backend
|
||||||
|
|||||||
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
16
Firmware/sdk/launchers/desktop/CMakeLists.txt
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
add_executable(cardboy_desktop
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set_target_properties(cardboy_desktop PROPERTIES
|
||||||
|
EXPORT_NAME desktop_launcher
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(cardboy_desktop
|
||||||
|
PRIVATE
|
||||||
|
cardboy_apps
|
||||||
|
cardboy_sdk
|
||||||
|
${CARDBOY_SDK_BACKEND_LIBRARY}
|
||||||
|
)
|
||||||
|
|
||||||
|
target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
|
||||||
33
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
33
Firmware/sdk/launchers/desktop/src/main.cpp
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
|
#include "cardboy/backend/desktop_backend.hpp"
|
||||||
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
|
|
||||||
|
#include <cstdio>
|
||||||
|
#include <exception>
|
||||||
|
|
||||||
|
using cardboy::backend::desktop::DesktopRuntime;
|
||||||
|
|
||||||
|
int main() {
|
||||||
|
try {
|
||||||
|
DesktopRuntime runtime;
|
||||||
|
|
||||||
|
cardboy::sdk::AppContext context(runtime.framebuffer, runtime.input, runtime.clock);
|
||||||
|
context.services = &runtime.serviceRegistry();
|
||||||
|
cardboy::sdk::AppSystem system(context);
|
||||||
|
|
||||||
|
system.registerApp(apps::createMenuAppFactory());
|
||||||
|
system.registerApp(apps::createClockAppFactory());
|
||||||
|
system.registerApp(apps::createGameboyAppFactory());
|
||||||
|
system.registerApp(apps::createTetrisAppFactory());
|
||||||
|
|
||||||
|
system.run();
|
||||||
|
} catch (const std::exception& ex) {
|
||||||
|
std::fprintf(stderr, "Cardboy desktop runtime failed: %s\n", ex.what());
|
||||||
|
return 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user