diff --git a/Firmware/main/CMakeLists.txt b/Firmware/main/CMakeLists.txt index 5e231a5..aeffcfd 100644 --- a/Firmware/main/CMakeLists.txt +++ b/Firmware/main/CMakeLists.txt @@ -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 diff --git a/Firmware/main/include/app_framework.hpp b/Firmware/main/include/app_framework.hpp index 2971284..5177034 100644 --- a/Firmware/main/include/app_framework.hpp +++ b/Firmware/main/include/app_framework.hpp @@ -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; diff --git a/Firmware/main/include/apps/gameboy_app.hpp b/Firmware/main/include/apps/gameboy_app.hpp index 9af77fc..a4c3656 100644 --- a/Firmware/main/include/apps/gameboy_app.hpp +++ b/Firmware/main/include/apps/gameboy_app.hpp @@ -1,15 +1,3 @@ #pragma once -#include "app_framework.hpp" - -#include -#include - -namespace apps { - -inline constexpr char kGameboyAppName[] = "Game Boy"; -inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName; - -std::unique_ptr createGameboyAppFactory(); - -} // namespace apps +#include "cardboy/apps/gameboy_app.hpp" diff --git a/Firmware/main/src/app_main.cpp b/Firmware/main/src/app_main.cpp index 3da7ca4..278a7ed 100644 --- a/Firmware/main/src/app_main.cpp +++ b/Firmware/main/src/app_main.cpp @@ -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; diff --git a/Firmware/sdk/CMakeLists.txt b/Firmware/sdk/CMakeLists.txt index c324f2c..ce603d7 100644 --- a/Firmware/sdk/CMakeLists.txt +++ b/Firmware/sdk/CMakeLists.txt @@ -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 diff --git a/Firmware/main/src/apps/gameboy_app.cpp b/Firmware/sdk/apps/gameboy_app.cpp similarity index 92% rename from Firmware/main/src/apps/gameboy_app.cpp rename to Firmware/sdk/apps/gameboy_app.cpp index f0acdb0..2c0e942 100644 --- a/Firmware/main/src/apps/gameboy_app.cpp +++ b/Firmware/sdk/apps/gameboy_app.cpp @@ -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 -#include - - -#include "esp_timer.h" +#include "cardboy/sdk/services.hpp" #include @@ -27,6 +21,7 @@ #include #include #include +#include #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 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 kEmbeddedRomDescriptors = {{{ "Builtin Demo 1", "builtin_demo1", @@ -75,6 +78,9 @@ static const std::array kEmbeddedRomDescriptors = {{{ _binary_builtin_demo2_gb_start, _binary_builtin_demo2_gb_end, }}}; +#else +static const std::array 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( + std::chrono::duration_cast(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::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(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( + std::chrono::duration_cast(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 create(AppContext& context) override { return std::make_unique(context); } + std::unique_ptr create(cardboy::sdk::AppContext& context) override { + return std::make_unique(context); + } }; } // namespace -std::unique_ptr createGameboyAppFactory() { return std::make_unique(); } +std::unique_ptr createGameboyAppFactory() { + return std::make_unique(); +} } // namespace apps diff --git a/Firmware/sdk/hosts/sfml_main.cpp b/Firmware/sdk/hosts/sfml_main.cpp index 05cb992..69adb21 100644 --- a/Firmware/sdk/hosts/sfml_main.cpp +++ b/Firmware/sdk/hosts/sfml_main.cpp @@ -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 #include #include +#include #include #include #include +#include #include #include -#include #include #include #include @@ -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(); diff --git a/Firmware/sdk/include/cardboy/apps/gameboy_app.hpp b/Firmware/sdk/include/cardboy/apps/gameboy_app.hpp new file mode 100644 index 0000000..7f0897e --- /dev/null +++ b/Firmware/sdk/include/cardboy/apps/gameboy_app.hpp @@ -0,0 +1,16 @@ +#pragma once + +#include "cardboy/sdk/app_framework.hpp" + +#include +#include + +namespace apps { + +inline constexpr char kGameboyAppName[] = "Game Boy"; +inline constexpr std::string_view kGameboyAppNameView = kGameboyAppName; + +std::unique_ptr createGameboyAppFactory(); + +} // namespace apps + diff --git a/Firmware/main/include/apps/peanut_gb.h b/Firmware/sdk/include/cardboy/apps/peanut_gb.h similarity index 99% rename from Firmware/main/include/apps/peanut_gb.h rename to Firmware/sdk/include/cardboy/apps/peanut_gb.h index 8063c1c..b945c7c 100644 --- a/Firmware/main/include/apps/peanut_gb.h +++ b/Firmware/sdk/include/cardboy/apps/peanut_gb.h @@ -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. */ diff --git a/Firmware/sdk/include/cardboy/sdk/app_framework.hpp b/Firmware/sdk/include/cardboy/sdk/app_framework.hpp index 41315d2..2f96d18 100644 --- a/Firmware/sdk/include/cardboy/sdk/app_framework.hpp +++ b/Firmware/sdk/include/cardboy/sdk/app_framework.hpp @@ -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; diff --git a/Firmware/sdk/include/cardboy/sdk/services.hpp b/Firmware/sdk/include/cardboy/sdk/services.hpp index 92552f6..8ce8061 100644 --- a/Firmware/sdk/include/cardboy/sdk/services.hpp +++ b/Firmware/sdk/include/cardboy/sdk/services.hpp @@ -1,6 +1,7 @@ #pragma once #include +#include #include 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 -