Files
cardboy/Firmware/sdk/backends/desktop/src/desktop_backend.cpp
2025-10-13 00:05:56 +02:00

327 lines
10 KiB
C++

#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 {
DesktopEventBus::DesktopEventBus(DesktopRuntime& owner) : runtime(owner) {}
DesktopEventBus::~DesktopEventBus() { cancelTimerSignal(); }
void DesktopEventBus::signal(std::uint32_t bits) {
if (bits == 0)
return;
{
std::lock_guard<std::mutex> lock(mutex);
pendingBits |= bits;
}
cv.notify_all();
}
void DesktopEventBus::signalFromISR(std::uint32_t bits) { signal(bits); }
std::uint32_t DesktopEventBus::wait(std::uint32_t mask, std::uint32_t timeout_ms) {
if (mask == 0)
return 0;
const auto start = std::chrono::steady_clock::now();
const bool infinite = timeout_ms == cardboy::sdk::IEventBus::kWaitForever;
while (true) {
{
std::lock_guard<std::mutex> lock(mutex);
const std::uint32_t ready = pendingBits & mask;
if (ready != 0) {
pendingBits &= ~mask;
return ready;
}
}
if (!infinite) {
const auto now = std::chrono::steady_clock::now();
const auto elapsedMs = std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count();
if (elapsedMs >= static_cast<std::int64_t>(timeout_ms))
return 0;
const auto remaining = timeout_ms - static_cast<std::uint32_t>(elapsedMs);
runtime.sleepFor(std::min<std::uint32_t>(remaining, 8));
} else {
runtime.sleepFor(8);
}
}
}
void DesktopEventBus::scheduleTimerSignal(std::uint32_t delay_ms) {
cancelTimerSignal();
if (delay_ms == cardboy::sdk::IEventBus::kWaitForever)
return;
if (delay_ms == 0) {
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
return;
}
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
timerThread = std::thread([this, delay_ms]() {
std::unique_lock<std::mutex> lock(timerMutex);
const bool cancelled =
timerCv.wait_for(lock, std::chrono::milliseconds(delay_ms), [this] { return timerCancel; });
lock.unlock();
if (!cancelled)
signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Timer));
});
}
void DesktopEventBus::cancelTimerSignal() {
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = true;
}
timerCv.notify_all();
if (timerThread.joinable())
timerThread.join();
{
std::lock_guard<std::mutex> lock(timerMutex);
timerCancel = false;
}
}
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) {
bool handled = true;
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:
handled = false;
break;
}
if (handled)
runtime.eventBusService.signal(cardboy::sdk::to_event_bits(cardboy::sdk::EventBusSignal::Input));
}
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), eventBusService(*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(true);
presentIfNeeded();
services.buzzer = &buzzerService;
services.battery = &batteryService;
services.storage = &storageService;
services.random = &randomService;
services.highResClock = &highResService;
services.filesystem = &filesystemService;
services.eventBus = &eventBusService;
services.loopHooks = nullptr;
}
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>(0) : static_cast<std::uint8_t>(255);
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>(0) : static_cast<std::uint8_t>(255);
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