mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
379 lines
12 KiB
C++
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;
|
|
}
|