Files
cardboy/Firmware/sdk/hosts/sfml_main.cpp
2025-10-11 11:15:39 +02:00

379 lines
12 KiB
C++

#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 "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 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;
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;
}