a little faster gameboy

This commit is contained in:
2025-10-11 22:21:31 +02:00
parent e18278e130
commit f04b026d46

View File

@@ -4,6 +4,7 @@
#include "cardboy/gfx/font16x8.hpp" #include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp" #include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/services.hpp" #include "cardboy/sdk/services.hpp"
#include "cardboy/utils/utils.hpp" #include "cardboy/utils/utils.hpp"
@@ -115,8 +116,7 @@ public:
prevInput = {}; prevInput = {};
statusMessage.clear(); statusMessage.clear();
resetFpsStats(); resetFpsStats();
scaleMode = ScaleMode::Original; scaleMode = ScaleMode::Original;
geometryDirty = true;
ensureFilesystemReady(); ensureFilesystemReady();
refreshRomList(); refreshRomList();
mode = Mode::Browse; mode = Mode::Browse;
@@ -180,9 +180,7 @@ public:
} }
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();) GB_PERF_ONLY(perf.geometryUs = 0;)
ensureRenderGeometry();
GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;)
GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();) GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();)
gb_run_frame(&gb); gb_run_frame(&gb);
@@ -447,20 +445,34 @@ private:
#endif #endif
}; };
struct RenderGeometry { static constexpr int kOriginalOffsetX = (cardboy::sdk::kDisplayWidth - LCD_WIDTH) / 2;
float scaleX = 1.0f; static constexpr int kOriginalOffsetY = (cardboy::sdk::kDisplayHeight - LCD_HEIGHT) / 2;
float scaleY = 1.0f; static constexpr int kFullHeightScaledWidth =
int scaledWidth = LCD_WIDTH; (((LCD_WIDTH * cardboy::sdk::kDisplayHeight + LCD_HEIGHT / 2) / LCD_HEIGHT) + 7) & ~7;
int scaledHeight = LCD_HEIGHT; static constexpr int kFullHeightOffsetX = (((cardboy::sdk::kDisplayWidth - kFullHeightScaledWidth) / 2) / 8) * 8;
int offsetX = 0;
int offsetY = 0; static_assert(kFullHeightScaledWidth % 8 == 0);
int leftMargin = 0; static_assert(kFullHeightOffsetX % 8 == 0);
int rightMargin = 0; static_assert(kOriginalOffsetX % 8 == 0);
std::array<int, LCD_HEIGHT> lineYStart{}; static_assert(kFullHeightOffsetX + kFullHeightScaledWidth <= cardboy::sdk::kDisplayWidth);
std::array<int, LCD_HEIGHT> lineYEnd{};
std::array<int, LCD_WIDTH> colXStart{}; inline static constexpr std::array<int, LCD_WIDTH + 1> kFullHeightColumnBounds = []() constexpr {
std::array<int, LCD_WIDTH> colXEnd{}; std::array<int, LCD_WIDTH + 1> bounds{};
}; for (int x = 0; x <= LCD_WIDTH; ++x)
bounds[static_cast<std::size_t>(x)] = kFullHeightOffsetX + (kFullHeightScaledWidth * x) / LCD_WIDTH;
return bounds;
}();
inline static constexpr std::array<int, LCD_HEIGHT + 1> kFullHeightRowBounds = []() constexpr {
std::array<int, LCD_HEIGHT + 1> bounds{};
for (int y = 0; y <= LCD_HEIGHT; ++y)
bounds[static_cast<std::size_t>(y)] = (cardboy::sdk::kDisplayHeight * y) / LCD_HEIGHT;
return bounds;
}();
static_assert(kFullHeightColumnBounds[0] % 8 == 0);
static_assert(kFullHeightColumnBounds[LCD_WIDTH] - kFullHeightColumnBounds[0] == kFullHeightScaledWidth);
static_assert(kFullHeightRowBounds[LCD_HEIGHT] == cardboy::sdk::kDisplayHeight);
AppContext& context; AppContext& context;
Framebuffer& framebuffer; Framebuffer& framebuffer;
@@ -471,10 +483,8 @@ private:
int64_t frameDelayCarryUs = 0; int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse; Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original; ScaleMode scaleMode = ScaleMode::Original;
bool geometryDirty = true;
RenderGeometry geometry{};
std::vector<RomEntry> roms; std::vector<RomEntry> roms;
std::size_t selectedIndex = 0; std::size_t selectedIndex = 0;
bool browserDirty = true; bool browserDirty = true;
@@ -657,86 +667,10 @@ private:
scaleMode = ScaleMode::FullHeight; scaleMode = ScaleMode::FullHeight;
else else
scaleMode = ScaleMode::Original; scaleMode = ScaleMode::Original;
geometryDirty = true; frameDirty = true;
frameDirty = true;
setStatus(scaleMode == ScaleMode::FullHeight ? "Scale: Full height" : "Scale: Original"); 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) { void handleBrowserInput(const InputState& input) {
if (input.select && !prevInput.select) { if (input.select && !prevInput.select) {
refreshRomList(); refreshRomList();
@@ -912,7 +846,6 @@ private:
gbReady = true; gbReady = true;
mode = Mode::Running; mode = Mode::Running;
frameDirty = true; frameDirty = true;
geometryDirty = true;
activeRomName = rom.name.empty() ? "Game" : rom.name; activeRomName = rom.name.empty() ? "Game" : rom.name;
std::string statusText = "Running " + activeRomName; std::string statusText = "Running " + activeRomName;
@@ -931,7 +864,6 @@ private:
cartRam.clear(); cartRam.clear();
activeRomName.clear(); activeRomName.clear();
activeRomSavePath.clear(); activeRomSavePath.clear();
geometryDirty = true;
return; return;
} }
@@ -945,7 +877,6 @@ private:
cartRam.clear(); cartRam.clear();
activeRomName.clear(); activeRomName.clear();
activeRomSavePath.clear(); activeRomSavePath.clear();
geometryDirty = true;
std::memset(&gb, 0, sizeof(gb)); std::memset(&gb, 0, sizeof(gb));
mode = Mode::Browse; mode = Mode::Browse;
browserDirty = true; browserDirty = true;
@@ -992,8 +923,6 @@ private:
return; return;
frameDirty = false; frameDirty = false;
ensureRenderGeometry();
++fpsFrameCounter; ++fpsFrameCounter;
++totalFrameCounter; ++totalFrameCounter;
const uint32_t nowMs = context.clock.millis(); const uint32_t nowMs = context.clock.millis();
@@ -1015,16 +944,9 @@ private:
const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE"; const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE";
if (scaleMode == ScaleMode::FullHeight) { if (scaleMode == ScaleMode::FullHeight) {
const auto& geom = geometry; const int textScale = 1;
const int textScale = 1; const int screenHeight = framebuffer.height();
const int rotatedWidth = font16x8::kGlyphHeight * textScale; const int screenWidth = framebuffer.width();
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);
const int horizontalPadding = 8; const int horizontalPadding = 8;
const int fpsLineGap = 4; const int fpsLineGap = 4;
@@ -1202,6 +1124,54 @@ private:
return value > threshold; return value > threshold;
} }
static void drawLineOriginal(GameboyApp& self, const uint8_t pixels[160], int srcLine) {
Framebuffer& fb = self.framebuffer;
const int dstY = kOriginalOffsetY + srcLine;
const int baseX = kOriginalOffsetX;
CARDBOY_CHECK((baseX % 8) == 0);
int x = 0;
while (x < LCD_WIDTH) {
uint8_t pack = 0;
for (int i = 0; i < 8; ++i, ++x) {
const bool on = shouldPixelBeOn(pixels[x], baseX + x, dstY);
pack = static_cast<uint8_t>((pack << 1) | (on ? 1 : 0));
}
fb.drawBits8(baseX + x - 8, dstY, pack);
}
}
static void drawLineFullHeight(GameboyApp& self, const uint8_t pixels[160], int srcLine) {
Framebuffer& fb = self.framebuffer;
int yStart = kFullHeightRowBounds[static_cast<std::size_t>(srcLine)];
int yEnd = kFullHeightRowBounds[static_cast<std::size_t>(srcLine) + 1];
CARDBOY_CHECK(yEnd > yStart);
CARDBOY_CHECK((kFullHeightColumnBounds[0] % 8) == 0);
for (int dstY = yStart; dstY < yEnd; ++dstY) {
uint8_t pack = 0;
int bitsCollected = 0;
for (int x = 0; x < LCD_WIDTH; ++x) {
const int colStart = kFullHeightColumnBounds[static_cast<std::size_t>(x)];
const int colEnd = kFullHeightColumnBounds[static_cast<std::size_t>(x) + 1];
CARDBOY_CHECK(colEnd > colStart);
for (int dstX = colStart; dstX < colEnd; ++dstX) {
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY);
pack = static_cast<uint8_t>((pack << 1) | (on ? 1 : 0));
++bitsCollected;
if (bitsCollected == 8) {
const int byteStart = dstX - 7;
CARDBOY_CHECK((byteStart % 8) == 0);
fb.drawBits8(byteStart, dstY, pack);
pack = 0;
bitsCollected = 0;
}
}
}
CARDBOY_CHECK(bitsCollected == 0);
}
}
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) { static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
auto* self = fromGb(gb); auto* self = fromGb(gb);
CARDBOY_CHECK_CODE(if (!self) return 0xFF; CARDBOY_CHECK_CODE(if (!self) return 0xFF;
@@ -1246,49 +1216,19 @@ private:
__attribute__((optimize("Ofast"))) static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160], __attribute__((optimize("Ofast"))) static void lcdDrawLine(struct gb_s* gb, const uint8_t pixels[160],
const uint_fast8_t line) { const uint_fast8_t line) {
auto* self = fromGb(gb); auto* self = fromGb(gb);
if (!self || line >= LCD_HEIGHT) CARDBOY_CHECK(self && line < LCD_HEIGHT);
return;
ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::LcdDraw); 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;
self->frameDirty = true; self->frameDirty = true;
Framebuffer& fb = self->framebuffer; Framebuffer& fb = self->framebuffer;
fb.frameReady(); fb.frameReady();
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) { if (self->scaleMode == ScaleMode::FullHeight)
const int dstY = yStart; drawLineFullHeight(*self, pixels, static_cast<int>(line));
const int dstXBase = geom.offsetX; else
for (int x = 0; x < LCD_WIDTH; ++x) { drawLineOriginal(*self, pixels, static_cast<int>(line));
const bool on = shouldPixelBeOn(pixels[x], dstXBase + x, dstY);
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;
for (int dstY = yStart; dstY < yEnd; ++dstY)
for (int dstX = drawStart; dstX < drawEnd; ++dstX) {
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY);
fb.drawPixel(dstX, dstY, on);
}
}
} }
static const char* initErrorToString(enum gb_init_error_e err) { static const char* initErrorToString(enum gb_init_error_e err) {