|
|
|
|
@@ -1,3 +1,4 @@
|
|
|
|
|
#pragma GCC optimize("Ofast")
|
|
|
|
|
#include "apps/gameboy_app.hpp"
|
|
|
|
|
|
|
|
|
|
#include "app_framework.hpp"
|
|
|
|
|
@@ -8,10 +9,15 @@
|
|
|
|
|
|
|
|
|
|
#include <peanut_gb.h>
|
|
|
|
|
|
|
|
|
|
#include "esp_timer.h"
|
|
|
|
|
|
|
|
|
|
#include <inttypes.h>
|
|
|
|
|
|
|
|
|
|
#include <algorithm>
|
|
|
|
|
#include <array>
|
|
|
|
|
#include <cctype>
|
|
|
|
|
#include <cerrno>
|
|
|
|
|
#include <cstdint>
|
|
|
|
|
#include <cstdio>
|
|
|
|
|
#include <cstring>
|
|
|
|
|
#include <dirent.h>
|
|
|
|
|
@@ -28,49 +34,203 @@ constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
|
|
|
|
|
|
|
|
|
|
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;
|
|
|
|
|
const uint8_t* start;
|
|
|
|
|
const uint8_t* end;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
static const std::array<EmbeddedRomDescriptor, 2> kEmbeddedRomDescriptors = {{{
|
|
|
|
|
"Builtin Demo 1",
|
|
|
|
|
"builtin_demo1",
|
|
|
|
|
_binary_builtin_demo1_gb_start,
|
|
|
|
|
_binary_builtin_demo1_gb_end,
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
"Builtin Demo 2",
|
|
|
|
|
"builtin_demo2",
|
|
|
|
|
_binary_builtin_demo2_gb_start,
|
|
|
|
|
_binary_builtin_demo2_gb_end,
|
|
|
|
|
}}};
|
|
|
|
|
|
|
|
|
|
struct RomEntry {
|
|
|
|
|
std::string name; // short display name
|
|
|
|
|
std::string fullPath; // absolute path on LittleFS
|
|
|
|
|
std::string fullPath; // absolute path on LittleFS or virtual path for embedded
|
|
|
|
|
std::string saveSlug; // base filename (without extension) used for save files
|
|
|
|
|
const uint8_t* embeddedData = nullptr;
|
|
|
|
|
std::size_t embeddedSize = 0;
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] bool isEmbedded() const { return embeddedData != nullptr && embeddedSize != 0; }
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
[[nodiscard]] std::string sanitizeSaveSlug(std::string_view text) {
|
|
|
|
|
std::string slug;
|
|
|
|
|
slug.reserve(text.size());
|
|
|
|
|
bool lastWasSeparator = false;
|
|
|
|
|
for (unsigned char ch: text) {
|
|
|
|
|
if (std::isalnum(ch)) {
|
|
|
|
|
slug.push_back(static_cast<char>(std::tolower(ch)));
|
|
|
|
|
lastWasSeparator = false;
|
|
|
|
|
} else if (!slug.empty() && !lastWasSeparator) {
|
|
|
|
|
slug.push_back('_');
|
|
|
|
|
lastWasSeparator = true;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
while (!slug.empty() && slug.back() == '_')
|
|
|
|
|
slug.pop_back();
|
|
|
|
|
if (slug.empty())
|
|
|
|
|
slug = "rom";
|
|
|
|
|
return slug;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void appendEmbeddedRoms(std::vector<RomEntry>& out) {
|
|
|
|
|
for (const auto& desc: kEmbeddedRomDescriptors) {
|
|
|
|
|
if (!desc.start || !desc.end || desc.end <= desc.start)
|
|
|
|
|
continue;
|
|
|
|
|
const std::size_t size = static_cast<std::size_t>(desc.end - desc.start);
|
|
|
|
|
if (size == 0)
|
|
|
|
|
continue;
|
|
|
|
|
RomEntry rom;
|
|
|
|
|
rom.name = std::string(desc.name);
|
|
|
|
|
const std::string slugCandidate =
|
|
|
|
|
desc.saveSlug.empty() ? sanitizeSaveSlug(desc.name) : std::string(desc.saveSlug);
|
|
|
|
|
rom.saveSlug = slugCandidate.empty() ? sanitizeSaveSlug(desc.name) : slugCandidate;
|
|
|
|
|
rom.fullPath = std::string("/embedded/") + rom.saveSlug + ".gb";
|
|
|
|
|
rom.embeddedData = desc.start;
|
|
|
|
|
rom.embeddedSize = size;
|
|
|
|
|
out.push_back(std::move(rom));
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int measureVerticalText(std::string_view text, int scale = 1, int letterSpacing = 1) {
|
|
|
|
|
if (text.empty())
|
|
|
|
|
return 0;
|
|
|
|
|
const int advance = (font16x8::kGlyphWidth + letterSpacing) * scale;
|
|
|
|
|
return static_cast<int>(text.size()) * advance - letterSpacing * scale;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawGlyphRotated(IFramebuffer& fb, int x, int y, char ch, bool clockwise, int scale = 1, bool on = true) {
|
|
|
|
|
const auto& rows = font16x8::glyphBitmap(ch);
|
|
|
|
|
for (int row = 0; row < font16x8::kGlyphHeight; ++row) {
|
|
|
|
|
const uint8_t rowBits = rows[row];
|
|
|
|
|
for (int col = 0; col < font16x8::kGlyphWidth; ++col) {
|
|
|
|
|
const uint8_t mask = static_cast<uint8_t>(1u << (font16x8::kGlyphWidth - 1 - col));
|
|
|
|
|
if ((rowBits & mask) == 0)
|
|
|
|
|
continue;
|
|
|
|
|
for (int sx = 0; sx < scale; ++sx) {
|
|
|
|
|
for (int sy = 0; sy < scale; ++sy) {
|
|
|
|
|
int dstX;
|
|
|
|
|
int dstY;
|
|
|
|
|
if (clockwise) {
|
|
|
|
|
dstX = x + row * scale + sx;
|
|
|
|
|
dstY = y + (font16x8::kGlyphWidth - 1 - col) * scale + sy;
|
|
|
|
|
} else {
|
|
|
|
|
dstX = x + (font16x8::kGlyphHeight - 1 - row) * scale + sx;
|
|
|
|
|
dstY = y + col * scale + sy;
|
|
|
|
|
}
|
|
|
|
|
fb.drawPixel(dstX, dstY, on);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void drawTextRotated(IFramebuffer& fb, int x, int y, std::string_view text, bool clockwise, int scale = 1,
|
|
|
|
|
bool on = true, int letterSpacing = 1) {
|
|
|
|
|
int cursor = y;
|
|
|
|
|
const int advance = (font16x8::kGlyphWidth + letterSpacing) * scale;
|
|
|
|
|
for (char ch: text) {
|
|
|
|
|
drawGlyphRotated(fb, x, cursor, ch, clockwise, scale, on);
|
|
|
|
|
cursor += advance;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class GameboyApp final : public IApp {
|
|
|
|
|
public:
|
|
|
|
|
explicit GameboyApp(AppContext& ctx) : context(ctx), framebuffer(ctx.framebuffer) {}
|
|
|
|
|
|
|
|
|
|
void onStart() override {
|
|
|
|
|
perf.resetAll();
|
|
|
|
|
prevInput = {};
|
|
|
|
|
statusMessage.clear();
|
|
|
|
|
resetFpsStats();
|
|
|
|
|
scaleMode = ScaleMode::Original;
|
|
|
|
|
geometryDirty = true;
|
|
|
|
|
ensureFilesystemReady();
|
|
|
|
|
refreshRomList();
|
|
|
|
|
mode = Mode::Browse;
|
|
|
|
|
browserDirty = true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void onStop() override { unloadRom(); }
|
|
|
|
|
void onStop() override {
|
|
|
|
|
perf.maybePrintAggregate(true);
|
|
|
|
|
unloadRom();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void step() override {
|
|
|
|
|
const InputState input = context.input.readState();
|
|
|
|
|
perf.resetForStep();
|
|
|
|
|
|
|
|
|
|
switch (mode) {
|
|
|
|
|
case Mode::Browse:
|
|
|
|
|
const uint64_t inputStartUs = esp_timer_get_time();
|
|
|
|
|
const InputState input = context.input.readState();
|
|
|
|
|
perf.inputUs = esp_timer_get_time() - inputStartUs;
|
|
|
|
|
|
|
|
|
|
const Mode stepMode = mode;
|
|
|
|
|
|
|
|
|
|
switch (stepMode) {
|
|
|
|
|
case Mode::Browse: {
|
|
|
|
|
const uint64_t handleStartUs = esp_timer_get_time();
|
|
|
|
|
handleBrowserInput(input);
|
|
|
|
|
perf.handleUs = esp_timer_get_time() - handleStartUs;
|
|
|
|
|
|
|
|
|
|
const uint64_t renderStartUs = esp_timer_get_time();
|
|
|
|
|
renderBrowser();
|
|
|
|
|
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
|
|
|
|
break;
|
|
|
|
|
case Mode::Running:
|
|
|
|
|
}
|
|
|
|
|
case Mode::Running: {
|
|
|
|
|
const uint64_t handleStartUs = esp_timer_get_time();
|
|
|
|
|
handleGameInput(input);
|
|
|
|
|
perf.handleUs = esp_timer_get_time() - handleStartUs;
|
|
|
|
|
|
|
|
|
|
if (!gbReady) {
|
|
|
|
|
mode = Mode::Browse;
|
|
|
|
|
browserDirty = true;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const uint64_t geometryStartUs = esp_timer_get_time();
|
|
|
|
|
ensureRenderGeometry();
|
|
|
|
|
perf.geometryUs = esp_timer_get_time() - geometryStartUs;
|
|
|
|
|
|
|
|
|
|
const uint64_t waitStartUs = esp_timer_get_time();
|
|
|
|
|
DispTools::draw_to_display_async_wait();
|
|
|
|
|
perf.waitUs = esp_timer_get_time() - waitStartUs;
|
|
|
|
|
|
|
|
|
|
const uint64_t runStartUs = esp_timer_get_time();
|
|
|
|
|
gb_run_frame(&gb);
|
|
|
|
|
perf.runUs = esp_timer_get_time() - runStartUs;
|
|
|
|
|
|
|
|
|
|
const uint64_t renderStartUs = esp_timer_get_time();
|
|
|
|
|
renderGameFrame();
|
|
|
|
|
perf.renderUs = esp_timer_get_time() - renderStartUs;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
prevInput = input;
|
|
|
|
|
|
|
|
|
|
perf.finishStep();
|
|
|
|
|
perf.accumulate();
|
|
|
|
|
perf.printStep(stepMode, gbReady, frameDirty, fpsCurrent, activeRomName, roms.size(), selectedIndex,
|
|
|
|
|
browserDirty);
|
|
|
|
|
perf.maybePrintAggregate();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
AppSleepPlan sleepPlan(uint32_t /*now*/) const override {
|
|
|
|
|
@@ -84,11 +244,252 @@ public:
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
enum class Mode { Browse, Running };
|
|
|
|
|
enum class ScaleMode { Original, FullHeight };
|
|
|
|
|
|
|
|
|
|
struct PerfTracker {
|
|
|
|
|
enum class CallbackKind { RomRead, CartRamRead, CartRamWrite, LcdDraw, Error };
|
|
|
|
|
|
|
|
|
|
uint64_t stepIndex = 0;
|
|
|
|
|
uint64_t stepStartUs = 0;
|
|
|
|
|
uint64_t lastStepEndUs = 0;
|
|
|
|
|
|
|
|
|
|
uint64_t inputUs = 0;
|
|
|
|
|
uint64_t handleUs = 0;
|
|
|
|
|
uint64_t geometryUs = 0;
|
|
|
|
|
uint64_t waitUs = 0;
|
|
|
|
|
uint64_t runUs = 0;
|
|
|
|
|
uint64_t renderUs = 0;
|
|
|
|
|
uint64_t totalUs = 0;
|
|
|
|
|
uint64_t otherUs = 0;
|
|
|
|
|
|
|
|
|
|
uint64_t cbRomReadUs = 0;
|
|
|
|
|
uint64_t cbCartReadUs = 0;
|
|
|
|
|
uint64_t cbCartWriteUs = 0;
|
|
|
|
|
uint64_t cbLcdUs = 0;
|
|
|
|
|
uint64_t cbErrorUs = 0;
|
|
|
|
|
uint32_t cbRomReadCalls = 0;
|
|
|
|
|
uint32_t cbCartReadCalls = 0;
|
|
|
|
|
uint32_t cbCartWriteCalls = 0;
|
|
|
|
|
uint32_t cbLcdCalls = 0;
|
|
|
|
|
uint32_t cbErrorCalls = 0;
|
|
|
|
|
|
|
|
|
|
uint64_t aggStartUs = 0;
|
|
|
|
|
uint64_t aggSteps = 0;
|
|
|
|
|
uint64_t aggInputUs = 0;
|
|
|
|
|
uint64_t aggHandleUs = 0;
|
|
|
|
|
uint64_t aggGeometryUs = 0;
|
|
|
|
|
uint64_t aggWaitUs = 0;
|
|
|
|
|
uint64_t aggRunUs = 0;
|
|
|
|
|
uint64_t aggRenderUs = 0;
|
|
|
|
|
uint64_t aggTotalUs = 0;
|
|
|
|
|
uint64_t aggOtherUs = 0;
|
|
|
|
|
uint64_t aggCbRomReadUs = 0;
|
|
|
|
|
uint64_t aggCbCartReadUs = 0;
|
|
|
|
|
uint64_t aggCbCartWriteUs = 0;
|
|
|
|
|
uint64_t aggCbLcdUs = 0;
|
|
|
|
|
uint64_t aggCbErrorUs = 0;
|
|
|
|
|
uint32_t aggCbRomReadCalls = 0;
|
|
|
|
|
uint32_t aggCbCartReadCalls = 0;
|
|
|
|
|
uint32_t aggCbCartWriteCalls = 0;
|
|
|
|
|
uint32_t aggCbLcdCalls = 0;
|
|
|
|
|
uint32_t aggCbErrorCalls = 0;
|
|
|
|
|
|
|
|
|
|
void resetAll() {
|
|
|
|
|
stepIndex = 0;
|
|
|
|
|
stepStartUs = 0;
|
|
|
|
|
lastStepEndUs = 0;
|
|
|
|
|
inputUs = handleUs = geometryUs = waitUs = runUs = renderUs = totalUs = otherUs = 0;
|
|
|
|
|
cbRomReadUs = cbCartReadUs = cbCartWriteUs = cbLcdUs = cbErrorUs = 0;
|
|
|
|
|
cbRomReadCalls = cbCartReadCalls = cbCartWriteCalls = cbLcdCalls = cbErrorCalls = 0;
|
|
|
|
|
resetAggregate(0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void resetForStep() {
|
|
|
|
|
stepStartUs = esp_timer_get_time();
|
|
|
|
|
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();
|
|
|
|
|
totalUs = now - stepStartUs;
|
|
|
|
|
lastStepEndUs = now;
|
|
|
|
|
const uint64_t accounted = inputUs + handleUs + geometryUs + waitUs + runUs + renderUs;
|
|
|
|
|
otherUs = (totalUs >= accounted) ? (totalUs - accounted) : 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void addCallback(CallbackKind kind, uint64_t durationUs) {
|
|
|
|
|
switch (kind) {
|
|
|
|
|
case CallbackKind::RomRead:
|
|
|
|
|
cbRomReadUs += durationUs;
|
|
|
|
|
++cbRomReadCalls;
|
|
|
|
|
break;
|
|
|
|
|
case CallbackKind::CartRamRead:
|
|
|
|
|
cbCartReadUs += durationUs;
|
|
|
|
|
++cbCartReadCalls;
|
|
|
|
|
break;
|
|
|
|
|
case CallbackKind::CartRamWrite:
|
|
|
|
|
cbCartWriteUs += durationUs;
|
|
|
|
|
++cbCartWriteCalls;
|
|
|
|
|
break;
|
|
|
|
|
case CallbackKind::LcdDraw:
|
|
|
|
|
cbLcdUs += durationUs;
|
|
|
|
|
++cbLcdCalls;
|
|
|
|
|
break;
|
|
|
|
|
case CallbackKind::Error:
|
|
|
|
|
cbErrorUs += durationUs;
|
|
|
|
|
++cbErrorCalls;
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void accumulate() {
|
|
|
|
|
++stepIndex;
|
|
|
|
|
if (!aggStartUs)
|
|
|
|
|
aggStartUs = stepStartUs;
|
|
|
|
|
++aggSteps;
|
|
|
|
|
aggInputUs += inputUs;
|
|
|
|
|
aggHandleUs += handleUs;
|
|
|
|
|
aggGeometryUs += geometryUs;
|
|
|
|
|
aggWaitUs += waitUs;
|
|
|
|
|
aggRunUs += runUs;
|
|
|
|
|
aggRenderUs += renderUs;
|
|
|
|
|
aggTotalUs += totalUs;
|
|
|
|
|
aggOtherUs += otherUs;
|
|
|
|
|
aggCbRomReadUs += cbRomReadUs;
|
|
|
|
|
aggCbCartReadUs += cbCartReadUs;
|
|
|
|
|
aggCbCartWriteUs += cbCartWriteUs;
|
|
|
|
|
aggCbLcdUs += cbLcdUs;
|
|
|
|
|
aggCbErrorUs += cbErrorUs;
|
|
|
|
|
aggCbRomReadCalls += cbRomReadCalls;
|
|
|
|
|
aggCbCartReadCalls += cbCartReadCalls;
|
|
|
|
|
aggCbCartWriteCalls += cbCartWriteCalls;
|
|
|
|
|
aggCbLcdCalls += cbLcdCalls;
|
|
|
|
|
aggCbErrorCalls += cbErrorCalls;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void printStep(Mode stepMode, bool gbReady, bool frameDirty, uint32_t fps, const std::string& romName,
|
|
|
|
|
std::size_t romCount, std::size_t selectedIdx, bool browserDirty) const {
|
|
|
|
|
auto toMs = [](uint64_t us) { return static_cast<double>(us) / 1000.0; };
|
|
|
|
|
const char* modeStr = (stepMode == Mode::Running) ? "RUN" : "BROWSE";
|
|
|
|
|
const char* name = romName.empty() ? "-" : romName.c_str();
|
|
|
|
|
const std::size_t safeIdx = (romCount == 0) ? 0 : std::min(selectedIdx, romCount - 1);
|
|
|
|
|
std::printf(
|
|
|
|
|
"GB STEP #%llu [%s] total=%.3fms input=%.3fms handle=%.3fms geom=%.3fms wait=%.3fms run=%.3fms "
|
|
|
|
|
"render=%.3fms other=%.3fms | cb lcd=%.3fms/%lu rom=%.3fms/%lu cramR=%.3fms/%lu cramW=%.3fms/%lu "
|
|
|
|
|
"err=%.3fms/%lu | info gbReady=%d frameDirty=%d fps=%lu rom='%s' roms=%zu sel=%zu dirty=%d\n",
|
|
|
|
|
static_cast<unsigned long long>(stepIndex), modeStr, toMs(totalUs), toMs(inputUs), toMs(handleUs),
|
|
|
|
|
toMs(geometryUs), toMs(waitUs), toMs(runUs), toMs(renderUs), toMs(otherUs), toMs(cbLcdUs),
|
|
|
|
|
static_cast<unsigned long>(cbLcdCalls), toMs(cbRomReadUs),
|
|
|
|
|
static_cast<unsigned long>(cbRomReadCalls), toMs(cbCartReadUs),
|
|
|
|
|
static_cast<unsigned long>(cbCartReadCalls), toMs(cbCartWriteUs),
|
|
|
|
|
static_cast<unsigned long>(cbCartWriteCalls), toMs(cbErrorUs),
|
|
|
|
|
static_cast<unsigned long>(cbErrorCalls), gbReady ? 1 : 0, frameDirty ? 1 : 0,
|
|
|
|
|
static_cast<unsigned long>(fps), name, romCount, safeIdx, browserDirty ? 1 : 0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void maybePrintAggregate(bool force = false) {
|
|
|
|
|
if (!aggSteps)
|
|
|
|
|
return;
|
|
|
|
|
if (!aggStartUs)
|
|
|
|
|
aggStartUs = stepStartUs;
|
|
|
|
|
const uint64_t now = lastStepEndUs ? lastStepEndUs : esp_timer_get_time();
|
|
|
|
|
const uint64_t span = now - aggStartUs;
|
|
|
|
|
if (!force && span < 1000000ULL)
|
|
|
|
|
return;
|
|
|
|
|
const double spanSec = span ? static_cast<double>(span) / 1e6 : 0.0;
|
|
|
|
|
auto avgStepMs = [&](uint64_t total) {
|
|
|
|
|
return aggSteps ? static_cast<double>(total) / static_cast<double>(aggSteps) / 1000.0 : 0.0;
|
|
|
|
|
};
|
|
|
|
|
auto avgCallMs = [&](uint64_t total, uint32_t calls) {
|
|
|
|
|
return calls ? static_cast<double>(total) / static_cast<double>(calls) / 1000.0 : 0.0;
|
|
|
|
|
};
|
|
|
|
|
std::printf("GB PERF %llu steps span=%.3fs%s | avg total=%.3fms input=%.3fms handle=%.3fms geom=%.3fms "
|
|
|
|
|
"wait=%.3fms "
|
|
|
|
|
"run=%.3fms render=%.3fms other=%.3fms | cb avg lcd=%.3fms (%lu) rom=%.3fms (%lu) cramR=%.3fms "
|
|
|
|
|
"(%lu) "
|
|
|
|
|
"cramW=%.3fms (%lu) err=%.3fms (%lu)\n",
|
|
|
|
|
static_cast<unsigned long long>(aggSteps), spanSec, force ? " (flush)" : "",
|
|
|
|
|
avgStepMs(aggTotalUs), avgStepMs(aggInputUs), avgStepMs(aggHandleUs), avgStepMs(aggGeometryUs),
|
|
|
|
|
avgStepMs(aggWaitUs), avgStepMs(aggRunUs), avgStepMs(aggRenderUs), avgStepMs(aggOtherUs),
|
|
|
|
|
avgCallMs(aggCbLcdUs, aggCbLcdCalls), static_cast<unsigned long>(aggCbLcdCalls),
|
|
|
|
|
avgCallMs(aggCbRomReadUs, aggCbRomReadCalls), static_cast<unsigned long>(aggCbRomReadCalls),
|
|
|
|
|
avgCallMs(aggCbCartReadUs, aggCbCartReadCalls), static_cast<unsigned long>(aggCbCartReadCalls),
|
|
|
|
|
avgCallMs(aggCbCartWriteUs, aggCbCartWriteCalls),
|
|
|
|
|
static_cast<unsigned long>(aggCbCartWriteCalls), avgCallMs(aggCbErrorUs, aggCbErrorCalls),
|
|
|
|
|
static_cast<unsigned long>(aggCbErrorCalls));
|
|
|
|
|
resetAggregate(now);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
void resetAggregate(uint64_t newStart) {
|
|
|
|
|
aggStartUs = newStart;
|
|
|
|
|
aggSteps = 0;
|
|
|
|
|
aggInputUs = 0;
|
|
|
|
|
aggHandleUs = 0;
|
|
|
|
|
aggGeometryUs = 0;
|
|
|
|
|
aggWaitUs = 0;
|
|
|
|
|
aggRunUs = 0;
|
|
|
|
|
aggRenderUs = 0;
|
|
|
|
|
aggTotalUs = 0;
|
|
|
|
|
aggOtherUs = 0;
|
|
|
|
|
aggCbRomReadUs = 0;
|
|
|
|
|
aggCbCartReadUs = 0;
|
|
|
|
|
aggCbCartWriteUs = 0;
|
|
|
|
|
aggCbLcdUs = 0;
|
|
|
|
|
aggCbErrorUs = 0;
|
|
|
|
|
aggCbRomReadCalls = 0;
|
|
|
|
|
aggCbCartReadCalls = 0;
|
|
|
|
|
aggCbCartWriteCalls = 0;
|
|
|
|
|
aggCbLcdCalls = 0;
|
|
|
|
|
aggCbErrorCalls = 0;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
class ScopedCallbackTimer {
|
|
|
|
|
public:
|
|
|
|
|
ScopedCallbackTimer(GameboyApp* instance, PerfTracker::CallbackKind kind) : app(instance), kind(kind) {
|
|
|
|
|
if (app)
|
|
|
|
|
startUs = esp_timer_get_time();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
~ScopedCallbackTimer() {
|
|
|
|
|
if (!app)
|
|
|
|
|
return;
|
|
|
|
|
const uint64_t end = esp_timer_get_time();
|
|
|
|
|
app->perf.addCallback(kind, end - startUs);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private:
|
|
|
|
|
GameboyApp* app;
|
|
|
|
|
PerfTracker::CallbackKind kind;
|
|
|
|
|
uint64_t startUs = 0;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
struct RenderGeometry {
|
|
|
|
|
float scaleX = 1.0f;
|
|
|
|
|
float scaleY = 1.0f;
|
|
|
|
|
int scaledWidth = LCD_WIDTH;
|
|
|
|
|
int scaledHeight = LCD_HEIGHT;
|
|
|
|
|
int offsetX = 0;
|
|
|
|
|
int offsetY = 0;
|
|
|
|
|
int leftMargin = 0;
|
|
|
|
|
int rightMargin = 0;
|
|
|
|
|
std::array<int, LCD_HEIGHT> lineYStart{};
|
|
|
|
|
std::array<int, LCD_HEIGHT> lineYEnd{};
|
|
|
|
|
std::array<int, LCD_WIDTH> colXStart{};
|
|
|
|
|
std::array<int, LCD_WIDTH> colXEnd{};
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
AppContext& context;
|
|
|
|
|
IFramebuffer& framebuffer;
|
|
|
|
|
PerfTracker perf{};
|
|
|
|
|
|
|
|
|
|
Mode mode = Mode::Browse;
|
|
|
|
|
ScaleMode scaleMode = ScaleMode::Original;
|
|
|
|
|
bool geometryDirty = true;
|
|
|
|
|
RenderGeometry geometry{};
|
|
|
|
|
std::vector<RomEntry> roms;
|
|
|
|
|
std::size_t selectedIndex = 0;
|
|
|
|
|
bool browserDirty = true;
|
|
|
|
|
@@ -99,6 +500,8 @@ private:
|
|
|
|
|
struct gb_s gb{};
|
|
|
|
|
bool gbReady = false;
|
|
|
|
|
std::vector<uint8_t> romData;
|
|
|
|
|
const uint8_t* romDataView = nullptr;
|
|
|
|
|
std::size_t romDataViewSize = 0;
|
|
|
|
|
std::vector<uint8_t> cartRam;
|
|
|
|
|
bool frameDirty = false;
|
|
|
|
|
uint32_t fpsLastSampleMs = 0;
|
|
|
|
|
@@ -155,16 +558,22 @@ private:
|
|
|
|
|
void refreshRomList() {
|
|
|
|
|
roms.clear();
|
|
|
|
|
|
|
|
|
|
if (!FsHelper::get().isMounted() && !ensureFilesystemReady())
|
|
|
|
|
return;
|
|
|
|
|
bool fsMounted = FsHelper::get().isMounted();
|
|
|
|
|
std::string statusHint;
|
|
|
|
|
const auto updateStatusHintIfEmpty = [&](std::string value) {
|
|
|
|
|
if (statusHint.empty())
|
|
|
|
|
statusHint = std::move(value);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const std::string dirPath = romDirectory();
|
|
|
|
|
DIR* dir = opendir(dirPath.c_str());
|
|
|
|
|
if (!dir) {
|
|
|
|
|
setStatus("No /lfs/roms directory");
|
|
|
|
|
return;
|
|
|
|
|
if (!fsMounted) {
|
|
|
|
|
fsMounted = ensureFilesystemReady();
|
|
|
|
|
if (!fsMounted)
|
|
|
|
|
updateStatusHintIfEmpty("Built-in ROMs only (LittleFS unavailable)");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (fsMounted) {
|
|
|
|
|
const std::string dirPath = romDirectory();
|
|
|
|
|
if (DIR* dir = opendir(dirPath.c_str())) {
|
|
|
|
|
while (dirent* entry = readdir(dir)) {
|
|
|
|
|
const std::string_view name(entry->d_name);
|
|
|
|
|
if (name == "." || name == "..")
|
|
|
|
|
@@ -176,14 +585,31 @@ private:
|
|
|
|
|
if (!rom.fullPath.empty() && rom.fullPath.back() != '/')
|
|
|
|
|
rom.fullPath.push_back('/');
|
|
|
|
|
rom.fullPath.append(entry->d_name);
|
|
|
|
|
rom.name = std::string(name);
|
|
|
|
|
// Trim extension for display.
|
|
|
|
|
const auto dotPos = rom.name.find_last_of('.');
|
|
|
|
|
|
|
|
|
|
std::string displayName(entry->d_name);
|
|
|
|
|
const auto dotPos = displayName.find_last_of('.');
|
|
|
|
|
if (dotPos != std::string::npos)
|
|
|
|
|
rom.name.resize(dotPos);
|
|
|
|
|
displayName.resize(dotPos);
|
|
|
|
|
rom.name = displayName;
|
|
|
|
|
|
|
|
|
|
std::string slugCandidate(entry->d_name);
|
|
|
|
|
if (dotPos != std::string::npos)
|
|
|
|
|
slugCandidate.resize(dotPos);
|
|
|
|
|
if (slugCandidate.empty())
|
|
|
|
|
slugCandidate = sanitizeSaveSlug(displayName);
|
|
|
|
|
rom.saveSlug = slugCandidate.empty() ? sanitizeSaveSlug(displayName) : slugCandidate;
|
|
|
|
|
|
|
|
|
|
roms.push_back(std::move(rom));
|
|
|
|
|
}
|
|
|
|
|
closedir(dir);
|
|
|
|
|
if (roms.empty())
|
|
|
|
|
updateStatusHintIfEmpty("Copy .gb/.gbc to /lfs/roms");
|
|
|
|
|
} else {
|
|
|
|
|
updateStatusHintIfEmpty("No /lfs/roms directory");
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
appendEmbeddedRoms(roms);
|
|
|
|
|
|
|
|
|
|
std::sort(roms.begin(), roms.end(), [](const RomEntry& a, const RomEntry& b) { return a.name < b.name; });
|
|
|
|
|
|
|
|
|
|
@@ -192,11 +618,99 @@ private:
|
|
|
|
|
|
|
|
|
|
browserDirty = true;
|
|
|
|
|
|
|
|
|
|
if (roms.empty())
|
|
|
|
|
setStatus("Copy .gb/.gbc to /lfs/roms");
|
|
|
|
|
else
|
|
|
|
|
if (roms.empty()) {
|
|
|
|
|
setStatus("No ROMs available");
|
|
|
|
|
} else if (!statusHint.empty()) {
|
|
|
|
|
setStatus(statusHint);
|
|
|
|
|
} else {
|
|
|
|
|
statusMessage.clear();
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void toggleScaleMode() {
|
|
|
|
|
if (scaleMode == ScaleMode::Original)
|
|
|
|
|
scaleMode = ScaleMode::FullHeight;
|
|
|
|
|
else
|
|
|
|
|
scaleMode = ScaleMode::Original;
|
|
|
|
|
geometryDirty = true;
|
|
|
|
|
frameDirty = true;
|
|
|
|
|
setStatus(scaleMode == ScaleMode::FullHeight ? "Scale: Full height" : "Scale: Original");
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ensureRenderGeometry() {
|
|
|
|
|
if (!geometryDirty)
|
|
|
|
|
return;
|
|
|
|
|
geometryDirty = false;
|
|
|
|
|
|
|
|
|
|
auto& geom = geometry;
|
|
|
|
|
const int fbWidth = framebuffer.width();
|
|
|
|
|
const int fbHeight = framebuffer.height();
|
|
|
|
|
const auto resetGeom = [&]() {
|
|
|
|
|
geom.scaleX = 1.0f;
|
|
|
|
|
geom.scaleY = 1.0f;
|
|
|
|
|
geom.scaledWidth = 0;
|
|
|
|
|
geom.scaledHeight = 0;
|
|
|
|
|
geom.offsetX = 0;
|
|
|
|
|
geom.offsetY = 0;
|
|
|
|
|
geom.leftMargin = 0;
|
|
|
|
|
geom.rightMargin = 0;
|
|
|
|
|
std::fill(geom.lineYStart.begin(), geom.lineYStart.end(), 0);
|
|
|
|
|
std::fill(geom.lineYEnd.begin(), geom.lineYEnd.end(), 0);
|
|
|
|
|
std::fill(geom.colXStart.begin(), geom.colXStart.end(), 0);
|
|
|
|
|
std::fill(geom.colXEnd.begin(), geom.colXEnd.end(), 0);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if (fbWidth <= 0 || fbHeight <= 0) {
|
|
|
|
|
resetGeom();
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int scaledWidth;
|
|
|
|
|
int scaledHeight;
|
|
|
|
|
if (scaleMode == ScaleMode::FullHeight) {
|
|
|
|
|
int targetHeight = fbHeight;
|
|
|
|
|
int targetWidth = static_cast<int>((static_cast<int64_t>(LCD_WIDTH) * targetHeight + LCD_HEIGHT / 2) /
|
|
|
|
|
std::max(1, LCD_HEIGHT));
|
|
|
|
|
if (targetWidth > fbWidth) {
|
|
|
|
|
targetWidth = fbWidth;
|
|
|
|
|
targetHeight = static_cast<int>((static_cast<int64_t>(LCD_HEIGHT) * targetWidth + LCD_WIDTH / 2) /
|
|
|
|
|
std::max(1, LCD_WIDTH));
|
|
|
|
|
}
|
|
|
|
|
scaledWidth = std::clamp(targetWidth, 1, fbWidth);
|
|
|
|
|
scaledHeight = std::clamp(targetHeight, 1, fbHeight);
|
|
|
|
|
} else {
|
|
|
|
|
scaledWidth = std::clamp(fbWidth, 1, LCD_WIDTH);
|
|
|
|
|
scaledHeight = std::clamp(fbHeight, 1, LCD_HEIGHT);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
geom.scaledWidth = scaledWidth;
|
|
|
|
|
geom.scaledHeight = scaledHeight;
|
|
|
|
|
geom.offsetX = std::max(0, (fbWidth - scaledWidth) / 2);
|
|
|
|
|
geom.offsetY = std::max(0, (fbHeight - scaledHeight) / 2);
|
|
|
|
|
geom.leftMargin = geom.offsetX;
|
|
|
|
|
geom.rightMargin = std::max(0, fbWidth - (geom.offsetX + scaledWidth));
|
|
|
|
|
geom.scaleX = static_cast<float>(scaledWidth) / static_cast<float>(LCD_WIDTH);
|
|
|
|
|
geom.scaleY = static_cast<float>(scaledHeight) / static_cast<float>(LCD_HEIGHT);
|
|
|
|
|
|
|
|
|
|
for (int srcLine = 0; srcLine < LCD_HEIGHT; ++srcLine) {
|
|
|
|
|
int start = geom.offsetY + static_cast<int>((static_cast<int64_t>(scaledHeight) * srcLine) / LCD_HEIGHT);
|
|
|
|
|
int end =
|
|
|
|
|
geom.offsetY + static_cast<int>((static_cast<int64_t>(scaledHeight) * (srcLine + 1)) / LCD_HEIGHT);
|
|
|
|
|
start = std::clamp(start, 0, fbHeight);
|
|
|
|
|
end = std::clamp(end, start, fbHeight);
|
|
|
|
|
geom.lineYStart[srcLine] = start;
|
|
|
|
|
geom.lineYEnd[srcLine] = end;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (int srcCol = 0; srcCol < LCD_WIDTH; ++srcCol) {
|
|
|
|
|
int start = geom.offsetX + static_cast<int>((static_cast<int64_t>(scaledWidth) * srcCol) / LCD_WIDTH);
|
|
|
|
|
int end = geom.offsetX + static_cast<int>((static_cast<int64_t>(scaledWidth) * (srcCol + 1)) / LCD_WIDTH);
|
|
|
|
|
start = std::clamp(start, 0, fbWidth);
|
|
|
|
|
end = std::clamp(end, start, fbWidth);
|
|
|
|
|
geom.colXStart[srcCol] = start;
|
|
|
|
|
geom.colXEnd[srcCol] = end;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void handleBrowserInput(const InputState& input) {
|
|
|
|
|
if (input.select && !prevInput.select) {
|
|
|
|
|
@@ -261,7 +775,9 @@ private:
|
|
|
|
|
const std::size_t idx = first + i;
|
|
|
|
|
const bool selected = (idx == selectedIndex);
|
|
|
|
|
const int y = kMenuStartY + static_cast<int>(i) * kMenuSpacing;
|
|
|
|
|
const std::string labelText = roms[idx].name.empty() ? "(unnamed)" : roms[idx].name;
|
|
|
|
|
std::string labelText = roms[idx].name.empty() ? "(unnamed)" : roms[idx].name;
|
|
|
|
|
if (roms[idx].isEmbedded())
|
|
|
|
|
labelText.append(" *");
|
|
|
|
|
if (selected)
|
|
|
|
|
font16x8::drawText(framebuffer, 16, y, ">", 1, true, 1);
|
|
|
|
|
font16x8::drawText(framebuffer, selected ? 32 : 40, y, labelText, 1, true, 1);
|
|
|
|
|
@@ -287,7 +803,17 @@ private:
|
|
|
|
|
unloadRom();
|
|
|
|
|
|
|
|
|
|
const RomEntry& rom = roms[index];
|
|
|
|
|
|
|
|
|
|
romDataView = nullptr;
|
|
|
|
|
romDataViewSize = 0;
|
|
|
|
|
if (rom.isEmbedded()) {
|
|
|
|
|
if (rom.embeddedSize == 0) {
|
|
|
|
|
setStatus("ROM empty");
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
romData.clear();
|
|
|
|
|
romDataView = rom.embeddedData;
|
|
|
|
|
romDataViewSize = rom.embeddedSize;
|
|
|
|
|
} else {
|
|
|
|
|
FILE* file = std::fopen(rom.fullPath.c_str(), "rb");
|
|
|
|
|
if (!file) {
|
|
|
|
|
setStatus("Open ROM failed");
|
|
|
|
|
@@ -315,6 +841,17 @@ private:
|
|
|
|
|
romData.clear();
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
romDataView = romData.data();
|
|
|
|
|
romDataViewSize = romData.size();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (!romDataView || romDataViewSize == 0) {
|
|
|
|
|
setStatus("ROM load failed");
|
|
|
|
|
romData.clear();
|
|
|
|
|
romDataView = nullptr;
|
|
|
|
|
romDataViewSize = 0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
std::memset(&gb, 0, sizeof(gb));
|
|
|
|
|
const auto initResult = gb_init(&gb, &GameboyApp::romRead, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
|
|
|
|
|
@@ -322,6 +859,8 @@ private:
|
|
|
|
|
if (initResult != GB_INIT_NO_ERROR) {
|
|
|
|
|
setStatus(initErrorToString(initResult));
|
|
|
|
|
romData.clear();
|
|
|
|
|
romDataView = nullptr;
|
|
|
|
|
romDataViewSize = 0;
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -334,7 +873,11 @@ private:
|
|
|
|
|
|
|
|
|
|
const uint_fast32_t saveSize = gb_get_save_size(&gb);
|
|
|
|
|
cartRam.assign(static_cast<std::size_t>(saveSize), 0);
|
|
|
|
|
activeRomSavePath = buildSavePath(rom.fullPath);
|
|
|
|
|
std::string savePath;
|
|
|
|
|
const bool fsReady = FsHelper::get().isMounted() || ensureFilesystemReady();
|
|
|
|
|
if (fsReady)
|
|
|
|
|
savePath = buildSavePath(rom, romDirectory());
|
|
|
|
|
activeRomSavePath = savePath;
|
|
|
|
|
loadSaveFile();
|
|
|
|
|
|
|
|
|
|
resetFpsStats();
|
|
|
|
|
@@ -343,9 +886,13 @@ private:
|
|
|
|
|
gbReady = true;
|
|
|
|
|
mode = Mode::Running;
|
|
|
|
|
frameDirty = true;
|
|
|
|
|
geometryDirty = true;
|
|
|
|
|
activeRomName = rom.name.empty() ? "Game" : rom.name;
|
|
|
|
|
|
|
|
|
|
setStatus("Running " + activeRomName);
|
|
|
|
|
std::string statusText = "Running " + activeRomName;
|
|
|
|
|
if (!fsReady)
|
|
|
|
|
statusText.append(" (no save)");
|
|
|
|
|
setStatus(std::move(statusText));
|
|
|
|
|
return true;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -353,9 +900,12 @@ private:
|
|
|
|
|
if (!gbReady) {
|
|
|
|
|
resetFpsStats();
|
|
|
|
|
romData.clear();
|
|
|
|
|
romDataView = nullptr;
|
|
|
|
|
romDataViewSize = 0;
|
|
|
|
|
cartRam.clear();
|
|
|
|
|
activeRomName.clear();
|
|
|
|
|
activeRomSavePath.clear();
|
|
|
|
|
geometryDirty = true;
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@@ -364,9 +914,12 @@ private:
|
|
|
|
|
|
|
|
|
|
gbReady = false;
|
|
|
|
|
romData.clear();
|
|
|
|
|
romDataView = nullptr;
|
|
|
|
|
romDataViewSize = 0;
|
|
|
|
|
cartRam.clear();
|
|
|
|
|
activeRomName.clear();
|
|
|
|
|
activeRomSavePath.clear();
|
|
|
|
|
geometryDirty = true;
|
|
|
|
|
std::memset(&gb, 0, sizeof(gb));
|
|
|
|
|
mode = Mode::Browse;
|
|
|
|
|
browserDirty = true;
|
|
|
|
|
@@ -376,6 +929,10 @@ private:
|
|
|
|
|
if (!gbReady)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const bool scaleToggleCombo = input.start && input.b && !(prevInput.start && prevInput.b);
|
|
|
|
|
if (scaleToggleCombo)
|
|
|
|
|
toggleScaleMode();
|
|
|
|
|
|
|
|
|
|
uint8_t joypad = 0xFF;
|
|
|
|
|
if (input.a)
|
|
|
|
|
joypad &= ~JOYPAD_A;
|
|
|
|
|
@@ -409,6 +966,8 @@ private:
|
|
|
|
|
return;
|
|
|
|
|
frameDirty = false;
|
|
|
|
|
|
|
|
|
|
ensureRenderGeometry();
|
|
|
|
|
|
|
|
|
|
++fpsFrameCounter;
|
|
|
|
|
const uint32_t nowMs = context.clock.millis();
|
|
|
|
|
if (fpsLastSampleMs == 0)
|
|
|
|
|
@@ -424,13 +983,80 @@ private:
|
|
|
|
|
char fpsBuf[16];
|
|
|
|
|
std::snprintf(fpsBuf, sizeof(fpsBuf), "%u FPS", static_cast<unsigned>(fpsCurrent));
|
|
|
|
|
const std::string fpsText(fpsBuf);
|
|
|
|
|
const int fpsWidth = font16x8::measureText(fpsText, 1, 1);
|
|
|
|
|
const int fpsX = std::max(16, framebuffer.width() - fpsWidth - 16);
|
|
|
|
|
const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE";
|
|
|
|
|
|
|
|
|
|
if (scaleMode == ScaleMode::FullHeight) {
|
|
|
|
|
const auto& geom = geometry;
|
|
|
|
|
const int textScale = 1;
|
|
|
|
|
const int rotatedWidth = font16x8::kGlyphHeight * textScale;
|
|
|
|
|
const int screenHeight = framebuffer.height();
|
|
|
|
|
const int screenWidth = framebuffer.width();
|
|
|
|
|
const int leftMargin = std::max(0, geom.leftMargin);
|
|
|
|
|
const int rightMargin = std::max(0, geom.rightMargin);
|
|
|
|
|
const int maxLeftX = std::max(0, screenWidth - rotatedWidth);
|
|
|
|
|
const int maxRightXBase = std::max(0, screenWidth - rotatedWidth);
|
|
|
|
|
|
|
|
|
|
if (!activeRomName.empty()) {
|
|
|
|
|
const int textHeight = measureVerticalText(activeRomName, textScale);
|
|
|
|
|
const int maxOrigin = std::max(0, screenHeight - textHeight);
|
|
|
|
|
int leftX = std::clamp((leftMargin - rotatedWidth) / 2, 0, maxLeftX);
|
|
|
|
|
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
|
|
|
|
|
drawTextRotated(framebuffer, leftX, leftY, activeRomName, false, textScale, true, 1);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const int gap = 8;
|
|
|
|
|
int totalRight = 0;
|
|
|
|
|
const auto accumulateHeight = [&](std::string_view text) {
|
|
|
|
|
if (text.empty())
|
|
|
|
|
return;
|
|
|
|
|
if (totalRight > 0)
|
|
|
|
|
totalRight += gap;
|
|
|
|
|
totalRight += measureVerticalText(text, textScale);
|
|
|
|
|
};
|
|
|
|
|
accumulateHeight(fpsText);
|
|
|
|
|
accumulateHeight("START+SELECT BACK");
|
|
|
|
|
accumulateHeight(scaleHint);
|
|
|
|
|
if (!statusMessage.empty())
|
|
|
|
|
accumulateHeight(statusMessage);
|
|
|
|
|
|
|
|
|
|
const int maxRightOrigin = std::max(0, screenHeight - totalRight);
|
|
|
|
|
int rightY = std::clamp((screenHeight - totalRight) / 2, 0, maxRightOrigin);
|
|
|
|
|
int rightX = screenWidth - rightMargin + std::max(0, (rightMargin - rotatedWidth) / 2);
|
|
|
|
|
rightX = std::clamp(rightX, 0, maxRightXBase);
|
|
|
|
|
|
|
|
|
|
const auto drawRight = [&](std::string_view text) {
|
|
|
|
|
if (text.empty())
|
|
|
|
|
return;
|
|
|
|
|
drawTextRotated(framebuffer, rightX, rightY, text, true, textScale, true, 1);
|
|
|
|
|
rightY += measureVerticalText(text, textScale);
|
|
|
|
|
rightY += gap;
|
|
|
|
|
};
|
|
|
|
|
drawRight(fpsText);
|
|
|
|
|
drawRight("START+SELECT BACK");
|
|
|
|
|
drawRight(scaleHint);
|
|
|
|
|
if (!statusMessage.empty())
|
|
|
|
|
drawRight(statusMessage);
|
|
|
|
|
} else {
|
|
|
|
|
if (!activeRomName.empty())
|
|
|
|
|
font16x8::drawText(framebuffer, 16, 16, activeRomName, 1, true, 1);
|
|
|
|
|
|
|
|
|
|
const int fpsWidth = font16x8::measureText(fpsText, 1, 1);
|
|
|
|
|
const int fpsX = std::max(16, framebuffer.width() - fpsWidth - 16);
|
|
|
|
|
font16x8::drawText(framebuffer, fpsX, 16, fpsText, 1, true, 1);
|
|
|
|
|
font16x8::drawText(framebuffer, 16, framebuffer.height() - 24, "START+SELECT BACK", 1, true, 1);
|
|
|
|
|
|
|
|
|
|
int instructionY = framebuffer.height() - 24;
|
|
|
|
|
font16x8::drawText(framebuffer, 16, instructionY, "START+SELECT BACK", 1, true, 1);
|
|
|
|
|
|
|
|
|
|
instructionY -= font16x8::kGlyphHeight + 6;
|
|
|
|
|
font16x8::drawText(framebuffer, 16, instructionY, scaleHint, 1, true, 1);
|
|
|
|
|
|
|
|
|
|
if (!statusMessage.empty()) {
|
|
|
|
|
const int statusWidth = font16x8::measureText(statusMessage, 1, 1);
|
|
|
|
|
const int statusX = std::max(12, (framebuffer.width() - statusWidth) / 2);
|
|
|
|
|
const int statusY = std::max(16, instructionY - font16x8::kGlyphHeight - 4);
|
|
|
|
|
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
DispTools::draw_to_display_async_start();
|
|
|
|
|
}
|
|
|
|
|
@@ -478,11 +1104,17 @@ private:
|
|
|
|
|
fpsCurrent = 0;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static std::string buildSavePath(const std::string& romPath) {
|
|
|
|
|
std::string result = romPath;
|
|
|
|
|
const auto dot = result.find_last_of('.');
|
|
|
|
|
if (dot != std::string::npos)
|
|
|
|
|
result.resize(dot);
|
|
|
|
|
static std::string buildSavePath(const RomEntry& rom, std::string_view romDir) {
|
|
|
|
|
std::string slug = rom.saveSlug;
|
|
|
|
|
if (slug.empty())
|
|
|
|
|
slug = sanitizeSaveSlug(rom.name);
|
|
|
|
|
if (slug.empty())
|
|
|
|
|
slug = "rom";
|
|
|
|
|
|
|
|
|
|
std::string result(romDir);
|
|
|
|
|
if (!result.empty() && result.back() != '/')
|
|
|
|
|
result.push_back('/');
|
|
|
|
|
result.append(slug);
|
|
|
|
|
result.append(".sav");
|
|
|
|
|
return result;
|
|
|
|
|
}
|
|
|
|
|
@@ -495,21 +1127,30 @@ private:
|
|
|
|
|
|
|
|
|
|
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
|
|
|
|
|
auto* self = fromGb(gb);
|
|
|
|
|
if (!self || addr >= self->romData.size())
|
|
|
|
|
if (!self)
|
|
|
|
|
return 0xFF;
|
|
|
|
|
return self->romData[static_cast<std::size_t>(addr)];
|
|
|
|
|
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead);
|
|
|
|
|
if (!self->romDataView || addr >= self->romDataViewSize)
|
|
|
|
|
return 0xFF;
|
|
|
|
|
return self->romDataView[static_cast<std::size_t>(addr)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static uint8_t cartRamRead(struct gb_s* gb, const uint_fast32_t addr) {
|
|
|
|
|
auto* self = fromGb(gb);
|
|
|
|
|
if (!self || addr >= self->cartRam.size())
|
|
|
|
|
if (!self)
|
|
|
|
|
return 0xFF;
|
|
|
|
|
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::CartRamRead);
|
|
|
|
|
if (addr >= self->cartRam.size())
|
|
|
|
|
return 0xFF;
|
|
|
|
|
return self->cartRam[static_cast<std::size_t>(addr)];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static void cartRamWrite(struct gb_s* gb, const uint_fast32_t addr, const uint8_t value) {
|
|
|
|
|
auto* self = fromGb(gb);
|
|
|
|
|
if (!self || addr >= self->cartRam.size())
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::CartRamWrite);
|
|
|
|
|
if (addr >= self->cartRam.size())
|
|
|
|
|
return;
|
|
|
|
|
self->cartRam[static_cast<std::size_t>(addr)] = value;
|
|
|
|
|
}
|
|
|
|
|
@@ -518,6 +1159,7 @@ private:
|
|
|
|
|
auto* self = fromGb(gb);
|
|
|
|
|
if (!self)
|
|
|
|
|
return;
|
|
|
|
|
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::Error);
|
|
|
|
|
|
|
|
|
|
char buf[64];
|
|
|
|
|
std::snprintf(buf, sizeof(buf), "EMU %s %04X", errorToString(err), val);
|
|
|
|
|
@@ -530,22 +1172,44 @@ private:
|
|
|
|
|
if (!self || line >= LCD_HEIGHT)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const int offsetX = (self->framebuffer.width() - LCD_WIDTH) / 2;
|
|
|
|
|
const int offsetY = (self->framebuffer.height() - LCD_HEIGHT) / 2;
|
|
|
|
|
const int dstY = offsetY + static_cast<int>(line);
|
|
|
|
|
if (dstY < 0 || dstY >= self->framebuffer.height())
|
|
|
|
|
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::LcdDraw);
|
|
|
|
|
|
|
|
|
|
self->ensureRenderGeometry();
|
|
|
|
|
const auto& geom = self->geometry;
|
|
|
|
|
if (geom.scaledWidth <= 0 || geom.scaledHeight <= 0)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
const int yStart = geom.lineYStart[line];
|
|
|
|
|
const int yEnd = geom.lineYEnd[line];
|
|
|
|
|
if (yStart >= yEnd)
|
|
|
|
|
return;
|
|
|
|
|
|
|
|
|
|
for (int x = 0; x < LCD_WIDTH; ++x) {
|
|
|
|
|
// Collapse 2-bit colour into monochrome.
|
|
|
|
|
const uint8_t shade = pixels[x] & 0x03u;
|
|
|
|
|
const bool on = (shade >= 2);
|
|
|
|
|
const int dstX = offsetX + x;
|
|
|
|
|
if (dstX < 0 || dstX >= self->framebuffer.width())
|
|
|
|
|
continue;
|
|
|
|
|
self->framebuffer.drawPixel(dstX, dstY, on);
|
|
|
|
|
}
|
|
|
|
|
self->frameDirty = true;
|
|
|
|
|
|
|
|
|
|
IFramebuffer& fb = self->framebuffer;
|
|
|
|
|
|
|
|
|
|
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
|
|
|
|
|
const int dstY = yStart;
|
|
|
|
|
const int dstXBase = geom.offsetX;
|
|
|
|
|
for (int x = 0; x < LCD_WIDTH; ++x) {
|
|
|
|
|
const bool on = (pixels[x] & 0x03u) >= 2;
|
|
|
|
|
fb.drawPixel(dstXBase + x, dstY, on);
|
|
|
|
|
}
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const auto& colStart = geom.colXStart;
|
|
|
|
|
const auto& colEnd = geom.colXEnd;
|
|
|
|
|
for (int x = 0; x < LCD_WIDTH; ++x) {
|
|
|
|
|
const int drawStart = colStart[x];
|
|
|
|
|
const int drawEnd = colEnd[x];
|
|
|
|
|
if (drawStart >= drawEnd)
|
|
|
|
|
continue;
|
|
|
|
|
const bool on = (pixels[x] & 0x03u) >= 2;
|
|
|
|
|
for (int dstY = yStart; dstY < yEnd; ++dstY)
|
|
|
|
|
for (int dstX = drawStart; dstX < drawEnd; ++dstX)
|
|
|
|
|
fb.drawPixel(dstX, dstY, on);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
static const char* initErrorToString(enum gb_init_error_e err) {
|
|
|
|
|
|