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/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/services.hpp"
#include "cardboy/utils/utils.hpp"
@@ -116,7 +117,6 @@ public:
statusMessage.clear();
resetFpsStats();
scaleMode = ScaleMode::Original;
geometryDirty = true;
ensureFilesystemReady();
refreshRomList();
mode = Mode::Browse;
@@ -180,9 +180,7 @@ public:
}
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
ensureRenderGeometry();
GB_PERF_ONLY(perf.geometryUs = nowMicros() - geometryStartUs;)
GB_PERF_ONLY(perf.geometryUs = 0;)
GB_PERF_ONLY(const uint64_t runStartUs = nowMicros();)
gb_run_frame(&gb);
@@ -447,20 +445,34 @@ private:
#endif
};
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{};
};
static constexpr int kOriginalOffsetX = (cardboy::sdk::kDisplayWidth - LCD_WIDTH) / 2;
static constexpr int kOriginalOffsetY = (cardboy::sdk::kDisplayHeight - LCD_HEIGHT) / 2;
static constexpr int kFullHeightScaledWidth =
(((LCD_WIDTH * cardboy::sdk::kDisplayHeight + LCD_HEIGHT / 2) / LCD_HEIGHT) + 7) & ~7;
static constexpr int kFullHeightOffsetX = (((cardboy::sdk::kDisplayWidth - kFullHeightScaledWidth) / 2) / 8) * 8;
static_assert(kFullHeightScaledWidth % 8 == 0);
static_assert(kFullHeightOffsetX % 8 == 0);
static_assert(kOriginalOffsetX % 8 == 0);
static_assert(kFullHeightOffsetX + kFullHeightScaledWidth <= cardboy::sdk::kDisplayWidth);
inline static constexpr std::array<int, LCD_WIDTH + 1> kFullHeightColumnBounds = []() constexpr {
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;
Framebuffer& framebuffer;
@@ -473,8 +485,6 @@ private:
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;
@@ -657,86 +667,10 @@ private:
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) {
refreshRomList();
@@ -912,7 +846,6 @@ private:
gbReady = true;
mode = Mode::Running;
frameDirty = true;
geometryDirty = true;
activeRomName = rom.name.empty() ? "Game" : rom.name;
std::string statusText = "Running " + activeRomName;
@@ -931,7 +864,6 @@ private:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
geometryDirty = true;
return;
}
@@ -945,7 +877,6 @@ private:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
geometryDirty = true;
std::memset(&gb, 0, sizeof(gb));
mode = Mode::Browse;
browserDirty = true;
@@ -992,8 +923,6 @@ private:
return;
frameDirty = false;
ensureRenderGeometry();
++fpsFrameCounter;
++totalFrameCounter;
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";
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);
const int horizontalPadding = 8;
const int fpsLineGap = 4;
@@ -1202,6 +1124,54 @@ private:
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) {
auto* self = fromGb(gb);
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],
const uint_fast8_t line) {
auto* self = fromGb(gb);
if (!self || line >= LCD_HEIGHT)
return;
CARDBOY_CHECK(self && line < LCD_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;
self->frameDirty = true;
Framebuffer& fb = self->framebuffer;
fb.frameReady();
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 = 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);
}
}
if (self->scaleMode == ScaleMode::FullHeight)
drawLineFullHeight(*self, pixels, static_cast<int>(line));
else
drawLineOriginal(*self, pixels, static_cast<int>(line));
}
static const char* initErrorToString(enum gb_init_error_e err) {