mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 23:27:49 +01:00
independnet gameboy
This commit is contained in:
@@ -3,7 +3,7 @@ idf_component_register(SRCS
|
|||||||
../sdk/apps/menu_app.cpp
|
../sdk/apps/menu_app.cpp
|
||||||
../sdk/apps/clock_app.cpp
|
../sdk/apps/clock_app.cpp
|
||||||
../sdk/apps/tetris_app.cpp
|
../sdk/apps/tetris_app.cpp
|
||||||
src/apps/gameboy_app.cpp
|
../sdk/apps/gameboy_app.cpp
|
||||||
src/display.cpp
|
src/display.cpp
|
||||||
src/bat_mon.cpp
|
src/bat_mon.cpp
|
||||||
src/spi_global.cpp
|
src/spi_global.cpp
|
||||||
|
|||||||
@@ -25,3 +25,4 @@ using IStorage = cardboy::sdk::IStorage;
|
|||||||
using IRandom = cardboy::sdk::IRandom;
|
using IRandom = cardboy::sdk::IRandom;
|
||||||
using IHighResClock = cardboy::sdk::IHighResClock;
|
using IHighResClock = cardboy::sdk::IHighResClock;
|
||||||
using IPowerManager = cardboy::sdk::IPowerManager;
|
using IPowerManager = cardboy::sdk::IPowerManager;
|
||||||
|
using IFilesystem = cardboy::sdk::IFilesystem;
|
||||||
|
|||||||
@@ -1,15 +1,3 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/apps/gameboy_app.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
|
|
||||||
|
|||||||
@@ -118,6 +118,16 @@ public:
|
|||||||
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
|
[[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
|
} // namespace
|
||||||
|
|
||||||
#if CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS && CONFIG_FREERTOS_USE_TRACE_FACILITY
|
#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 EspRandom randomService;
|
||||||
static EspHighResClock highResClockService;
|
static EspHighResClock highResClockService;
|
||||||
static EspPowerManager powerService;
|
static EspPowerManager powerService;
|
||||||
|
static EspFilesystem filesystemService;
|
||||||
|
|
||||||
static cardboy::sdk::Services services{};
|
static cardboy::sdk::Services services{};
|
||||||
services.buzzer = &buzzerService;
|
services.buzzer = &buzzerService;
|
||||||
@@ -313,6 +324,7 @@ extern "C" void app_main() {
|
|||||||
services.random = &randomService;
|
services.random = &randomService;
|
||||||
services.highResClock = &highResClockService;
|
services.highResClock = &highResClockService;
|
||||||
services.powerManager = &powerService;
|
services.powerManager = &powerService;
|
||||||
|
services.filesystem = &filesystemService;
|
||||||
|
|
||||||
AppContext context(framebuffer, input, clock);
|
AppContext context(framebuffer, input, clock);
|
||||||
context.services = &services;
|
context.services = &services;
|
||||||
|
|||||||
@@ -20,6 +20,7 @@ add_library(cardboy_apps STATIC
|
|||||||
apps/menu_app.cpp
|
apps/menu_app.cpp
|
||||||
apps/clock_app.cpp
|
apps/clock_app.cpp
|
||||||
apps/tetris_app.cpp
|
apps/tetris_app.cpp
|
||||||
|
apps/gameboy_app.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(cardboy_apps
|
target_include_directories(cardboy_apps
|
||||||
|
|||||||
@@ -1,17 +1,11 @@
|
|||||||
#pragma GCC optimize("Ofast")
|
#pragma GCC optimize("Ofast")
|
||||||
#include "apps/gameboy_app.hpp"
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
#include "apps/peanut_gb.h"
|
#include "cardboy/apps/peanut_gb.h"
|
||||||
|
|
||||||
#include "app_framework.hpp"
|
#include "cardboy/sdk/app_framework.hpp"
|
||||||
#include "app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
#include "cardboy/gfx/font16x8.hpp"
|
#include "cardboy/gfx/font16x8.hpp"
|
||||||
#include "input_state.hpp"
|
#include "cardboy/sdk/services.hpp"
|
||||||
|
|
||||||
#include <disp_tools.hpp>
|
|
||||||
#include <fs_helper.hpp>
|
|
||||||
|
|
||||||
|
|
||||||
#include "esp_timer.h"
|
|
||||||
|
|
||||||
#include <inttypes.h>
|
#include <inttypes.h>
|
||||||
|
|
||||||
@@ -27,6 +21,7 @@
|
|||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <sys/stat.h>
|
#include <sys/stat.h>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
#include <chrono>
|
||||||
|
|
||||||
#define GAMEBOY_PERF_METRICS 0
|
#define GAMEBOY_PERF_METRICS 0
|
||||||
|
|
||||||
@@ -46,16 +41,16 @@ namespace {
|
|||||||
constexpr int kMenuStartY = 48;
|
constexpr int kMenuStartY = 48;
|
||||||
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
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;
|
using Framebuffer = typename AppContext::Framebuffer;
|
||||||
|
|
||||||
constexpr std::array<std::string_view, 2> kRomExtensions = {".gb", ".gbc"};
|
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 {
|
struct EmbeddedRomDescriptor {
|
||||||
std::string_view name;
|
std::string_view name;
|
||||||
std::string_view saveSlug;
|
std::string_view saveSlug;
|
||||||
@@ -63,6 +58,14 @@ struct EmbeddedRomDescriptor {
|
|||||||
const uint8_t* end;
|
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 = {{{
|
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
||||||
"Builtin Demo 1",
|
"Builtin Demo 1",
|
||||||
"builtin_demo1",
|
"builtin_demo1",
|
||||||
@@ -75,6 +78,9 @@ static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
|||||||
_binary_builtin_demo2_gb_start,
|
_binary_builtin_demo2_gb_start,
|
||||||
_binary_builtin_demo2_gb_end,
|
_binary_builtin_demo2_gb_end,
|
||||||
}}};
|
}}};
|
||||||
|
#else
|
||||||
|
static const std::array<EmbeddedRomDescriptor, 0> kEmbeddedRomDescriptors{};
|
||||||
|
#endif
|
||||||
|
|
||||||
struct RomEntry {
|
struct RomEntry {
|
||||||
std::string name; // short display name
|
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:
|
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 {
|
void onStart() override {
|
||||||
cancelTick();
|
cancelTick();
|
||||||
@@ -198,9 +205,9 @@ public:
|
|||||||
void handleEvent(const AppEvent& event) override {
|
void handleEvent(const AppEvent& event) override {
|
||||||
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
if (event.type == AppEventType::Timer && event.timer.handle == tickTimer) {
|
||||||
tickTimer = kInvalidAppTimer;
|
tickTimer = kInvalidAppTimer;
|
||||||
const uint64_t frameStartUs = esp_timer_get_time();
|
const uint64_t frameStartUs = nowMicros();
|
||||||
performStep();
|
performStep();
|
||||||
const uint64_t frameEndUs = esp_timer_get_time();
|
const uint64_t frameEndUs = nowMicros();
|
||||||
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
const uint64_t elapsedUs = (frameEndUs >= frameStartUs) ? (frameEndUs - frameStartUs) : 0;
|
||||||
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
GB_PERF_ONLY(printf("Step took %" PRIu64 " us\n", elapsedUs));
|
||||||
scheduleAfterFrame(elapsedUs);
|
scheduleAfterFrame(elapsedUs);
|
||||||
@@ -215,27 +222,27 @@ public:
|
|||||||
void performStep() {
|
void performStep() {
|
||||||
GB_PERF_ONLY(perf.resetForStep();)
|
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();
|
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;
|
const Mode stepMode = mode;
|
||||||
|
|
||||||
switch (stepMode) {
|
switch (stepMode) {
|
||||||
case Mode::Browse: {
|
case Mode::Browse: {
|
||||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||||
handleBrowserInput(input);
|
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();
|
renderBrowser();
|
||||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case Mode::Running: {
|
case Mode::Running: {
|
||||||
GB_PERF_ONLY(const uint64_t handleStartUs = esp_timer_get_time();)
|
GB_PERF_ONLY(const uint64_t handleStartUs = nowMicros();)
|
||||||
handleGameInput(input);
|
handleGameInput(input);
|
||||||
GB_PERF_ONLY(perf.handleUs = esp_timer_get_time() - handleStartUs;)
|
GB_PERF_ONLY(perf.handleUs = nowMicros() - handleStartUs;)
|
||||||
|
|
||||||
if (!gbReady) {
|
if (!gbReady) {
|
||||||
mode = Mode::Browse;
|
mode = Mode::Browse;
|
||||||
@@ -243,17 +250,21 @@ public:
|
|||||||
break;
|
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();
|
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_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();
|
renderGameFrame();
|
||||||
GB_PERF_ONLY(perf.renderUs = esp_timer_get_time() - renderStartUs;)
|
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
|
||||||
|
framebuffer.endFrame();
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -330,14 +341,14 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
void resetForStep() {
|
void resetForStep() {
|
||||||
stepStartUs = esp_timer_get_time();
|
stepStartUs = clockMicros();
|
||||||
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
||||||
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
||||||
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
void finishStep() {
|
void finishStep() {
|
||||||
const uint64_t now = esp_timer_get_time();
|
const uint64_t now = clockMicros();
|
||||||
totalUs = now - stepStartUs;
|
totalUs = now - stepStartUs;
|
||||||
lastStepEndUs = now;
|
lastStepEndUs = now;
|
||||||
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
||||||
@@ -419,7 +430,7 @@ private:
|
|||||||
return;
|
return;
|
||||||
if (!aggStartUs)
|
if (!aggStartUs)
|
||||||
aggStartUs = stepStartUs;
|
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;
|
const uint64_t span = now - aggStartUs;
|
||||||
if (!force && span < 1000000ULL)
|
if (!force && span < 1000000ULL)
|
||||||
return;
|
return;
|
||||||
@@ -470,6 +481,12 @@ private:
|
|||||||
aggCbLcdCalls = 0;
|
aggCbLcdCalls = 0;
|
||||||
aggCbErrorCalls = 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 {
|
class ScopedCallbackTimer {
|
||||||
@@ -479,7 +496,7 @@ private:
|
|||||||
if (instance) {
|
if (instance) {
|
||||||
app = instance;
|
app = instance;
|
||||||
cbKind = kind;
|
cbKind = kind;
|
||||||
startUs = esp_timer_get_time();
|
startUs = instance->nowMicros();
|
||||||
}
|
}
|
||||||
#else
|
#else
|
||||||
(void) instance;
|
(void) instance;
|
||||||
@@ -491,7 +508,7 @@ private:
|
|||||||
#if GAMEBOY_PERF_METRICS
|
#if GAMEBOY_PERF_METRICS
|
||||||
if (!app)
|
if (!app)
|
||||||
return;
|
return;
|
||||||
const uint64_t end = esp_timer_get_time();
|
const uint64_t end = app->nowMicros();
|
||||||
app->perf.addCallback(cbKind, end - startUs);
|
app->perf.addCallback(cbKind, end - startUs);
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
@@ -521,6 +538,8 @@ private:
|
|||||||
|
|
||||||
AppContext& context;
|
AppContext& context;
|
||||||
Framebuffer& framebuffer;
|
Framebuffer& framebuffer;
|
||||||
|
cardboy::sdk::IFilesystem* filesystem = nullptr;
|
||||||
|
cardboy::sdk::IHighResClock* highResClock = nullptr;
|
||||||
PerfTracker perf{};
|
PerfTracker perf{};
|
||||||
AppTimerHandle tickTimer = kInvalidAppTimer;
|
AppTimerHandle tickTimer = kInvalidAppTimer;
|
||||||
int64_t frameDelayCarryUs = 0;
|
int64_t frameDelayCarryUs = 0;
|
||||||
@@ -585,9 +604,13 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
bool ensureFilesystemReady() {
|
bool ensureFilesystemReady() {
|
||||||
esp_err_t err = FsHelper::get().mount();
|
if (!filesystem) {
|
||||||
if (err != ESP_OK) {
|
setStatus("Storage unavailable");
|
||||||
setStatus("LittleFS mount failed");
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!filesystem->isMounted() && !filesystem->mount()) {
|
||||||
|
setStatus("Storage mount failed");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -611,7 +634,9 @@ private:
|
|||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] std::string romDirectory() const {
|
[[nodiscard]] std::string romDirectory() const {
|
||||||
std::string result(FsHelper::get().basePath());
|
std::string result;
|
||||||
|
if (filesystem)
|
||||||
|
result = filesystem->basePath();
|
||||||
if (!result.empty() && result.back() != '/')
|
if (!result.empty() && result.back() != '/')
|
||||||
result.push_back('/');
|
result.push_back('/');
|
||||||
result.append("roms");
|
result.append("roms");
|
||||||
@@ -632,7 +657,7 @@ private:
|
|||||||
void refreshRomList() {
|
void refreshRomList() {
|
||||||
roms.clear();
|
roms.clear();
|
||||||
|
|
||||||
bool fsMounted = FsHelper::get().isMounted();
|
bool fsMounted = filesystem ? filesystem->isMounted() : false;
|
||||||
std::string statusHint;
|
std::string statusHint;
|
||||||
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
||||||
if (statusHint.empty())
|
if (statusHint.empty())
|
||||||
@@ -642,7 +667,7 @@ private:
|
|||||||
if (!fsMounted) {
|
if (!fsMounted) {
|
||||||
fsMounted = ensureFilesystemReady();
|
fsMounted = ensureFilesystemReady();
|
||||||
if (!fsMounted)
|
if (!fsMounted)
|
||||||
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
|
updateStatusHintIfEmpty("Built-in ROMs only (filesystem unavailable)");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fsMounted) {
|
if (fsMounted) {
|
||||||
@@ -677,9 +702,9 @@ private:
|
|||||||
}
|
}
|
||||||
closedir(dir);
|
closedir(dir);
|
||||||
if (roms.empty())
|
if (roms.empty())
|
||||||
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
|
updateStatusHintIfEmpty("Copy .gb/.gbc into ROMS/");
|
||||||
} else {
|
} else {
|
||||||
updateStatusHintIfEmpty("No /lfs/roms directory");
|
updateStatusHintIfEmpty("ROM directory missing");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -824,7 +849,8 @@ private:
|
|||||||
return;
|
return;
|
||||||
browserDirty = false;
|
browserDirty = false;
|
||||||
|
|
||||||
DispTools::draw_to_display_async_wait();
|
framebuffer.beginFrame();
|
||||||
|
framebuffer.clear(false);
|
||||||
|
|
||||||
const std::string_view title = "GAME BOY";
|
const std::string_view title = "GAME BOY";
|
||||||
const int titleWidth = font16x8::measureText(title, 2, 1);
|
const int titleWidth = font16x8::measureText(title, 2, 1);
|
||||||
@@ -833,7 +859,7 @@ private:
|
|||||||
|
|
||||||
if (roms.empty()) {
|
if (roms.empty()) {
|
||||||
font16x8::drawText(framebuffer, 24, kMenuStartY + 12, "NO ROMS FOUND", 1, true, 1);
|
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 {
|
} else {
|
||||||
const std::size_t visibleCount =
|
const std::size_t visibleCount =
|
||||||
static_cast<std::size_t>(std::max(1, (framebuffer.height() - kMenuStartY - 64) / kMenuSpacing));
|
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);
|
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) {
|
bool loadRom(std::size_t index) {
|
||||||
@@ -949,7 +975,7 @@ private:
|
|||||||
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
||||||
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
||||||
std::string savePath;
|
std::string savePath;
|
||||||
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
|
const bool fsReady = (filesystem && filesystem->isMounted()) || ensureFilesystemReady();
|
||||||
if (fsReady)
|
if (fsReady)
|
||||||
savePath = buildSavePath(rom, romDirectory());
|
savePath = buildSavePath(rom, romDirectory());
|
||||||
activeRomSavePath = savePath;
|
activeRomSavePath = savePath;
|
||||||
@@ -1159,7 +1185,6 @@ private:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
DispTools::draw_to_display_async_start();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void maybeSaveRam() {
|
void maybeSaveRam() {
|
||||||
@@ -1199,6 +1224,14 @@ private:
|
|||||||
browserDirty = true;
|
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() {
|
void resetFpsStats() {
|
||||||
fpsLastSampleMs = 0;
|
fpsLastSampleMs = 0;
|
||||||
fpsFrameCounter = 0;
|
fpsFrameCounter = 0;
|
||||||
@@ -1303,10 +1336,6 @@ private:
|
|||||||
|
|
||||||
Framebuffer& fb = self->framebuffer;
|
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) &&
|
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
|
||||||
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
|
(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:
|
public:
|
||||||
const char* name() const override { return kGameboyAppName; }
|
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
|
} // 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
|
} // namespace apps
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
#include "cardboy/apps/clock_app.hpp"
|
#include "cardboy/apps/clock_app.hpp"
|
||||||
|
#include "cardboy/apps/gameboy_app.hpp"
|
||||||
#include "cardboy/apps/menu_app.hpp"
|
#include "cardboy/apps/menu_app.hpp"
|
||||||
#include "cardboy/apps/tetris_app.hpp"
|
#include "cardboy/apps/tetris_app.hpp"
|
||||||
#include "cardboy/sdk/app_system.hpp"
|
#include "cardboy/sdk/app_system.hpp"
|
||||||
@@ -14,12 +15,13 @@
|
|||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
#include <cstdlib>
|
#include <cstdlib>
|
||||||
#include <exception>
|
#include <exception>
|
||||||
|
#include <filesystem>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <optional>
|
#include <optional>
|
||||||
#include <random>
|
#include <random>
|
||||||
|
#include <stdexcept>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
#include <stdexcept>
|
|
||||||
#include <thread>
|
#include <thread>
|
||||||
#include <unordered_map>
|
#include <unordered_map>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
@@ -105,6 +107,34 @@ private:
|
|||||||
bool slowMode = false;
|
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 DesktopRuntime;
|
||||||
|
|
||||||
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
|
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
|
||||||
@@ -172,6 +202,7 @@ private:
|
|||||||
DesktopRandom randomService;
|
DesktopRandom randomService;
|
||||||
DesktopHighResClock highResService;
|
DesktopHighResClock highResService;
|
||||||
DesktopPowerManager powerService;
|
DesktopPowerManager powerService;
|
||||||
|
DesktopFilesystem filesystemService;
|
||||||
cardboy::sdk::Services services{};
|
cardboy::sdk::Services services{};
|
||||||
|
|
||||||
public:
|
public:
|
||||||
@@ -218,6 +249,7 @@ DesktopRuntime::DesktopRuntime()
|
|||||||
services.random = &randomService;
|
services.random = &randomService;
|
||||||
services.highResClock = &highResService;
|
services.highResClock = &highResService;
|
||||||
services.powerManager = &powerService;
|
services.powerManager = &powerService;
|
||||||
|
services.filesystem = &filesystemService;
|
||||||
}
|
}
|
||||||
|
|
||||||
void DesktopRuntime::setPixel(int x, int y, bool on) {
|
void DesktopRuntime::setPixel(int x, int y, bool on) {
|
||||||
@@ -352,6 +384,7 @@ int main() {
|
|||||||
|
|
||||||
system.registerApp(apps::createMenuAppFactory());
|
system.registerApp(apps::createMenuAppFactory());
|
||||||
system.registerApp(apps::createClockAppFactory());
|
system.registerApp(apps::createClockAppFactory());
|
||||||
|
system.registerApp(apps::createGameboyAppFactory());
|
||||||
system.registerApp(apps::createTetrisAppFactory());
|
system.registerApp(apps::createTetrisAppFactory());
|
||||||
|
|
||||||
system.run();
|
system.run();
|
||||||
|
|||||||
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal file
16
Firmware/sdk/include/cardboy/apps/gameboy_app.hpp
Normal 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
|
||||||
|
|
||||||
@@ -92,12 +92,12 @@
|
|||||||
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
|
/* Enable 16 bit colour palette. If disabled, only four colour shades are set in
|
||||||
* pixel data. */
|
* pixel data. */
|
||||||
#ifndef PEANUT_GB_12_COLOUR
|
#ifndef PEANUT_GB_12_COLOUR
|
||||||
# define PEANUT_GB_12_COLOUR 1
|
# define PEANUT_GB_12_COLOUR 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Adds more code to improve LCD rendering accuracy. */
|
/* Adds more code to improve LCD rendering accuracy. */
|
||||||
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
|
#ifndef PEANUT_GB_HIGH_LCD_ACCURACY
|
||||||
# define PEANUT_GB_HIGH_LCD_ACCURACY 1
|
# define PEANUT_GB_HIGH_LCD_ACCURACY 0
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
/* Use intrinsic functions. This may produce smaller and faster code. */
|
/* Use intrinsic functions. This may produce smaller and faster code. */
|
||||||
@@ -60,6 +60,7 @@ struct BasicAppContext {
|
|||||||
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
|
[[nodiscard]] IRandom* random() const { return services ? services->random : nullptr; }
|
||||||
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
|
[[nodiscard]] IHighResClock* highResClock() const { return services ? services->highResClock : nullptr; }
|
||||||
[[nodiscard]] IPowerManager* powerManager() const { return services ? services->powerManager : 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) {
|
void requestAppSwitchByIndex(std::size_t index) {
|
||||||
pendingAppIndex = index;
|
pendingAppIndex = index;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
|
#include <string>
|
||||||
#include <string_view>
|
#include <string_view>
|
||||||
|
|
||||||
namespace cardboy::sdk {
|
namespace cardboy::sdk {
|
||||||
@@ -64,6 +65,15 @@ public:
|
|||||||
[[nodiscard]] virtual bool isSlowMode() const = 0;
|
[[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 {
|
struct Services {
|
||||||
IBuzzer* buzzer = nullptr;
|
IBuzzer* buzzer = nullptr;
|
||||||
IBatteryMonitor* battery = nullptr;
|
IBatteryMonitor* battery = nullptr;
|
||||||
@@ -71,7 +81,7 @@ struct Services {
|
|||||||
IRandom* random = nullptr;
|
IRandom* random = nullptr;
|
||||||
IHighResClock* highResClock = nullptr;
|
IHighResClock* highResClock = nullptr;
|
||||||
IPowerManager* powerManager = nullptr;
|
IPowerManager* powerManager = nullptr;
|
||||||
|
IFilesystem* filesystem = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace cardboy::sdk
|
} // namespace cardboy::sdk
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user