mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-29 07:37:48 +01:00
327 lines
10 KiB
C++
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
|