independnet gameboy

This commit is contained in:
2025-10-10 17:11:49 +02:00
parent e9e371739b
commit 5b75ff28e0
11 changed files with 173 additions and 78 deletions

View File

@@ -3,7 +3,7 @@ idf_component_register(SRCS
../sdk/apps/menu_app.cpp
../sdk/apps/clock_app.cpp
../sdk/apps/tetris_app.cpp
src/apps/gameboy_app.cpp
../sdk/apps/gameboy_app.cpp
src/display.cpp
src/bat_mon.cpp
src/spi_global.cpp

View File

@@ -25,3 +25,4 @@ using IStorage = cardboy::sdk::IStorage;
using IRandom = cardboy::sdk::IRandom;
using IHighResClock = cardboy::sdk::IHighResClock;
using IPowerManager = cardboy::sdk::IPowerManager;
using IFilesystem = cardboy::sdk::IFilesystem;

View File

@@ -1,15 +1,3 @@
#pragma once
#include "app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kGameboyAppName[] = "Game Boy";
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
std::unique_ptr<IAppFactory> createGameboyAppFactory();
} // namespace apps
#include "cardboy/apps/gameboy_app.hpp"

View File

@@ -118,6 +118,16 @@ public:
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
};
class EspFilesystem final : public cardboy::sdk::IFilesystem {
public:
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
[[nodiscard]] std::string basePath() const override {
const char* path = FsHelper::get().basePath();
return path ? std::string(path) : std::string{};
}
};
} // namespace
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
@@ -305,6 +315,7 @@ extern "C" void app_main() {
static EspRandom randomService;
static EspHighResClock highResClockService;
static EspPowerManager powerService;
static EspFilesystem filesystemService;
static cardboy::sdk::Services services{};
services.buzzer = &buzzerService;
@@ -313,6 +324,7 @@ extern "C" void app_main() {
services.random = &randomService;
services.highResClock = &highResClockService;
services.powerManager = &powerService;
services.filesystem = &filesystemService;
AppContext context(framebuffer, input, clock);
context.services = &services;

View File

@@ -20,6 +20,7 @@ add_library(cardboy_apps STATIC
apps/menu_app.cpp
apps/clock_app.cpp
apps/tetris_app.cpp
apps/gameboy_app.cpp
)
target_include_directories(cardboy_apps

View File

@@ -1,17 +1,11 @@
#pragma GCC optimize("Ofast")
#include "apps/gameboy_app.hpp"
#include "apps/peanut_gb.h"
#include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/peanut_gb.h"
#include "app_framework.hpp"
#include "app_system.hpp"
#include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "input_state.hpp"
#include <disp_tools.hpp>
#include <fs_helper.hpp>
#include "esp_timer.h"
#include "cardboy/sdk/services.hpp"
#include <inttypes.h>
@@ -27,6 +21,7 @@
#include <string_view>
#include <sys/stat.h>
#include <vector>
#include <chrono>
#define GAMEBOY_PERF_METRICS 0
@@ -46,16 +41,16 @@ namespace {
constexpr int kMenuStartY = 48;
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType;
using cardboy::sdk::AppTimerHandle;
using cardboy::sdk::InputState;
using cardboy::sdk::kInvalidAppTimer;
using Framebuffer = typename AppContext::Framebuffer;
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
extern "C" {
extern const uint8_t _binary_builtin_demo1_gb_start[];
extern const uint8_t _binary_builtin_demo1_gb_end[];
extern const uint8_t _binary_builtin_demo2_gb_start[];
extern const uint8_t _binary_builtin_demo2_gb_end[];
}
struct EmbeddedRomDescriptor {
std::string_view name;
std::string_view saveSlug;
@@ -63,6 +58,14 @@ struct EmbeddedRomDescriptor {
const uint8_t* end;
};
#ifdef ESP_PLATFORM
extern "C" {
extern const uint8_t _binary_builtin_demo1_gb_start[];
extern const uint8_t _binary_builtin_demo1_gb_end[];
extern const uint8_t _binary_builtin_demo2_gb_start[];
extern const uint8_t _binary_builtin_demo2_gb_end[];
}
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
"Builtin Demo 1",
"builtin_demo1",
@@ -75,6 +78,9 @@ static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
_binary_builtin_demo2_gb_start,
_binary_builtin_demo2_gb_end,
}}};
#else
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
#endif
struct RomEntry {
std::string name; // short display name
@@ -168,9 +174,10 @@ void drawTextRotated(Framebuffer& fb, int x, int y, std::string_view text, bool
}
}
class GameboyApp final : public IApp {
class GameboyApp final : public cardboy::sdk::IApp {
public:
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
explicit GameboyApp(AppContext& ctx) :
context(ctx), framebuffer(ctx.framebuffer), filesystem(ctx.filesystem()), highResClock(ctx.highResClock()) {}
void onStart() override {
cancelTick();
@@ -198,9 +205,9 @@ public:
void handleEvent(const AppEvent& event) override {
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
tickTimer = kInvalidAppTimer;
const uint64_t frameStartUs = esp_timer_get_time();
const uint64_t frameStartUs = nowMicros();
performStep();
const uint64_t frameEndUs = esp_timer_get_time();
const uint64_t frameEndUs = nowMicros();
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
scheduleAfterFrame(elapsedUs);
@@ -215,27 +222,27 @@ public:
void performStep() {
GB_PERF_ONLY(perf.resetForStep();)
GB_PERF_ONLY(const uint64_t inputStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t inputStartUs = nowMicros();)
const InputState input = context.input.readState();
GB_PERF_ONLY(perf.inputUs = esp_timer_get_time() - inputStartUs;)
GB_PERF_ONLY(perf.inputUs = nowMicros() - inputStartUs;)
const Mode stepMode = mode;
switch (stepMode) {
case Mode::Browse: {
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handleBrowserInput(input);
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderBrowser();
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
break;
}
case Mode::Running: {
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
handleGameInput(input);
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
if (!gbReady) {
mode = Mode::Browse;
@@ -243,17 +250,21 @@ public:
break;
}
GB_PERF_ONLY(const uint64_t geometryStartUs = esp_timer_get_time();)
framebuffer.beginFrame();
framebuffer.clear(false);
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
ensureRenderGeometry();
GB_PERF_ONLY(perf.geometryUs = esp_timer_get_time() - geometryStartUs;)
GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;)
GB_PERF_ONLY(const uint64_t runStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();)
gb_run_frame(&gb);
GB_PERF_ONLY(perf.runUs = esp_timer_get_time() - runStartUs;)
GB_PERF_ONLY(perf.runUs = nowMicros() - runStartUs;)
GB_PERF_ONLY(const uint64_t renderStartUs = esp_timer_get_time();)
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderGameFrame();
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
framebuffer.endFrame();
break;
}
}
@@ -330,14 +341,14 @@ private:
}
void resetForStep() {
stepStartUs = esp_timer_get_time();
stepStartUs = clockMicros();
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
}
void finishStep() {
const uint64_t now = esp_timer_get_time();
const uint64_t now = clockMicros();
totalUs = now - stepStartUs;
lastStepEndUs = now;
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
@@ -419,7 +430,7 @@ private:
return;
if (!aggStartUs)
aggStartUs = stepStartUs;
const uint64_t now = lastStepEndUs ? lastStepEndUs : esp_timer_get_time();
const uint64_t now = lastStepEndUs ? lastStepEndUs : clockMicros();
const uint64_t span = now - aggStartUs;
if (!force && span < 1000000ULL)
return;
@@ -470,6 +481,12 @@ private:
aggCbLcdCalls = 0;
aggCbErrorCalls = 0;
}
static uint64_t clockMicros() {
const auto now = std::chrono::steady_clock::now();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
}
};
class ScopedCallbackTimer {
@@ -479,7 +496,7 @@ private:
if (instance) {
app = instance;
cbKind = kind;
startUs = esp_timer_get_time();
startUs = instance->nowMicros();
}
#else
(void) instance;
@@ -491,7 +508,7 @@ private:
#if GAMEBOY_PERF_METRICS
if (!app)
return;
const uint64_t end = esp_timer_get_time();
const uint64_t end = app->nowMicros();
app->perf.addCallback(cbKind, end - startUs);
#endif
}
@@ -521,6 +538,8 @@ private:
AppContext& context;
Framebuffer& framebuffer;
cardboy::sdk::IFilesystem* filesystem = nullptr;
cardboy::sdk::IHighResClock* highResClock = nullptr;
PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0;
@@ -585,9 +604,13 @@ private:
}
bool ensureFilesystemReady() {
esp_err_t err = FsHelper::get().mount();
if (err != ESP_OK) {
setStatus("LittleFS mount failed");
if (!filesystem) {
setStatus("Storage unavailable");
return false;
}
if (!filesystem->isMounted() && !filesystem->mount()) {
setStatus("Storage mount failed");
return false;
}
@@ -611,7 +634,9 @@ private:
}
[[nodiscard]] std::string romDirectory() const {
std::string result(FsHelper::get().basePath());
std::string result;
if (filesystem)
result = filesystem->basePath();
if (!result.empty() && result.back() != '/')
result.push_back('/');
result.append("roms");
@@ -632,7 +657,7 @@ private:
void refreshRomList() {
roms.clear();
bool fsMounted = FsHelper::get().isMounted();
bool fsMounted = filesystem ? filesystem->isMounted() : false;
std::string statusHint;
const auto updateStatusHintIfEmpty = [&](std::string value) {
if (statusHint.empty())
@@ -642,7 +667,7 @@ private:
if (!fsMounted) {
fsMounted = ensureFilesystemReady();
if (!fsMounted)
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
updateStatusHintIfEmpty("Built-in ROMs only (filesystem unavailable)");
}
if (fsMounted) {
@@ -677,9 +702,9 @@ private:
}
closedir(dir);
if (roms.empty())
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
updateStatusHintIfEmpty("Copy .gb/.gbc into ROMS/");
} else {
updateStatusHintIfEmpty("No /lfs/roms directory");
updateStatusHintIfEmpty("ROM directory missing");
}
}
@@ -824,7 +849,8 @@ private:
return;
browserDirty = false;
DispTools::draw_to_display_async_wait();
framebuffer.beginFrame();
framebuffer.clear(false);
const std::string_view title = "GAME BOY";
const int titleWidth = font16x8::measureText(title, 2, 1);
@@ -833,7 +859,7 @@ private:
if (roms.empty()) {
font16x8::drawText(framebuffer, 24, kMenuStartY + 12, "NO ROMS FOUND", 1, true, 1);
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "/LFS/ROMS", 1, true, 1);
font16x8::drawText(framebuffer, 24, kMenuStartY + kMenuSpacing + 12, "ADD FILES TO ROMS/", 1, true, 1);
} else {
const std::size_t visibleCount =
static_cast<std::size_t>(std::max(1, (framebuffer.height() - kMenuStartY - 64) / kMenuSpacing));
@@ -867,7 +893,7 @@ private:
font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1);
}
DispTools::draw_to_display_async_start();
framebuffer.endFrame();
}
bool loadRom(std::size_t index) {
@@ -949,7 +975,7 @@ private:
const uint_fast32_t saveSize = gb_get_save_size(&gb);
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
std::string savePath;
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
if (fsReady)
savePath = buildSavePath(rom, romDirectory());
activeRomSavePath = savePath;
@@ -1159,7 +1185,6 @@ private:
}
}
DispTools::draw_to_display_async_start();
}
void maybeSaveRam() {
@@ -1199,6 +1224,14 @@ private:
browserDirty = true;
}
[[nodiscard]] uint64_t nowMicros() const {
if (highResClock)
return highResClock->micros();
const auto now = std::chrono::steady_clock::now();
return static_cast<uint64_t>(
std::chrono::duration_cast<std::chrono::microseconds>(now.time_since_epoch()).count());
}
void resetFpsStats() {
fpsLastSampleMs = 0;
fpsFrameCounter = 0;
@@ -1303,10 +1336,6 @@ private:
Framebuffer& fb = self->framebuffer;
GB_PERF_ONLY(const uint64_t waitStartUs = esp_timer_get_time();)
DispTools::draw_to_display_async_wait();
GB_PERF_ONLY(self->perf.waitUs = esp_timer_get_time() - waitStartUs;)
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
@@ -1367,14 +1396,18 @@ private:
}
};
class GameboyAppFactory final : public IAppFactory {
class GameboyAppFactory final : public cardboy::sdk::IAppFactory {
public:
const char* name() const override { return kGameboyAppName; }
std::unique_ptr<IApp> create(AppContext& context) override { return std::make_unique<GameboyApp>(context); }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<GameboyApp>(context);
}
};
} // namespace
std::unique_ptr<IAppFactory> createGameboyAppFactory() { return std::make_unique<GameboyAppFactory>(); }
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() {
return std::make_unique<GameboyAppFactory>();
}
} // namespace apps

View File

@@ -1,4 +1,5 @@
#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/sdk/app_system.hpp"
@@ -14,12 +15,13 @@
#include <cstdint>
#include <cstdlib>
#include <exception>
#include <filesystem>
#include <limits>
#include <optional>
#include <random>
#include <stdexcept>
#include <string>
#include <string_view>
#include <stdexcept>
#include <thread>
#include <unordered_map>
#include <vector>
@@ -105,6 +107,34 @@ 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;
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
@@ -172,6 +202,7 @@ private:
DesktopRandom randomService;
DesktopHighResClock highResService;
DesktopPowerManager powerService;
DesktopFilesystem filesystemService;
cardboy::sdk::Services services{};
public:
@@ -218,6 +249,7 @@ DesktopRuntime::DesktopRuntime()
services.random = &randomService;
services.highResClock = &highResService;
services.powerManager = &powerService;
services.filesystem = &filesystemService;
}
void DesktopRuntime::setPixel(int x, int y, bool on) {
@@ -352,6 +384,7 @@ int main() {
system.registerApp(apps::createMenuAppFactory());
system.registerApp(apps::createClockAppFactory());
system.registerApp(apps::createGameboyAppFactory());
system.registerApp(apps::createTetrisAppFactory());
system.run();

View File

@@ -0,0 +1,16 @@
#pragma once
#include "cardboy/sdk/app_framework.hpp"
#include <memory>
#include <string_view>
namespace apps {
inline constexpr char kGameboyAppName[] = "Game Boy";
inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName;
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory();
} // namespace apps

View File

@@ -92,12 +92,12 @@
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
* pixel data. */
#ifndef PEANUT_GB_12_COLOUR
# define PEANUT_GB_12_COLOUR 1
# define PEANUT_GB_12_COLOUR 0
#endif
/* Adds more code to improve LCD rendering accuracy. */
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
# define PEANUT_GB_HIGH_LCD_ACCURACY 1
# define PEANUT_GB_HIGH_LCD_ACCURACY 0
#endif
/* Use intrinsic functions. This may produce smaller and faster code. */

View File

@@ -60,6 +60,7 @@ struct BasicAppContext {
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : nullptr; }
[[nodiscard]] IFilesystem* filesystem() const { return services ? services->filesystem : nullptr; }
void requestAppSwitchByIndex(std::size_t index) {
pendingAppIndex = index;

View File

@@ -1,6 +1,7 @@
#pragma once
#include <cstdint>
#include <string>
#include <string_view>
namespace cardboy::sdk {
@@ -64,6 +65,15 @@ public:
[[nodiscard]] virtual bool isSlowMode() const = 0;
};
class IFilesystem {
public:
virtual ~IFilesystem() = default;
virtual bool mount() = 0;
[[nodiscard]] virtual bool isMounted() const = 0;
[[nodiscard]] virtual std::string basePath() const = 0;
};
struct Services {
IBuzzer* buzzer = nullptr;
IBatteryMonitor* battery = nullptr;
@@ -71,7 +81,7 @@ struct Services {
IRandom* random = nullptr;
IHighResClock* highResClock = nullptr;
IPowerManager* powerManager = nullptr;
IFilesystem* filesystem = nullptr;
};
} // namespace cardboy::sdk