Compare commits

...

9 Commits

Author SHA1 Message Date
df57e55171 better default scale mode 2025-10-12 00:52:58 +02:00
a3b837f329 a bit more speedup 2025-10-12 00:33:48 +02:00
fc9e85aea0 fast wide scale 2025-10-12 00:20:22 +02:00
b55feb68f8 some opt 2025-10-12 00:03:01 +02:00
f04b026d46 a little faster gameboy 2025-10-11 22:21:31 +02:00
e18278e130 disable analyzer 2025-10-11 20:59:29 +02:00
9a392d6aec check macro 2025-10-11 20:36:43 +02:00
961453e28a 8bit draw 2025-10-11 20:03:00 +02:00
a4c2719077 text 2025-10-11 16:54:41 +02:00
13 changed files with 514 additions and 284 deletions

View File

@@ -21,6 +21,7 @@ idf_component_register(
nvs_flash
)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/utils" cardboy_utils_esp)
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk/backend_interface" backend_interface_from_backend_esp)
add_library(cardboy_backend_esp INTERFACE)

View File

@@ -6,6 +6,7 @@
#define CB_DISPLAY_HPP
#include "cardboy/backend/esp/config.hpp"
#include "cardboy/utils/utils.hpp"
#include "driver/spi_master.h"
// (Async memcpy removed for debugging simplification)
@@ -13,7 +14,7 @@
#include <array>
#include <bitset>
#include <cassert>
#include <cstdint>
namespace SMD {
static constexpr size_t kLineBytes = DISP_WIDTH / 8;
@@ -34,7 +35,7 @@ void frame_ready();
bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
__attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
unsigned bitIdx = 1 << (7 - (x % 8)) % 8;
@@ -46,6 +47,14 @@ __attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
}
}
__attribute__((always_inline)) static void set_pixel_8(int x, int y, std::uint8_t value) {
CARDBOY_CHECK(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
CARDBOY_CHECK((x % 8) == 0);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);
dma_buf[lineIdx] = ~value;
}
extern "C" void s_spi_post_cb(spi_transaction_t* trans);
static inline spi_device_interface_config_t _devcfg = {

View File

@@ -1,5 +1,7 @@
#pragma once
#include <cardboy/sdk/display_spec.hpp>
#include "cardboy/backend/esp/display.hpp"
#include "cardboy/sdk/platform.hpp"
#include "cardboy/sdk/services.hpp"
@@ -14,9 +16,12 @@ class EspFramebuffer final : public cardboy::sdk::FramebufferFacade<EspFramebuff
public:
EspFramebuffer() = default;
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
[[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
[[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
void __attribute__((always_inline)) drawPixel_impl(int x, int y, bool on) { SMD::set_pixel(x, y, on); }
void __attribute__((always_inline)) drawBits8_impl(int x, int y, std::uint8_t bits) {
SMD::set_pixel_8(x, y, bits);
}
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);

View File

@@ -58,14 +58,14 @@ public:
void beepLevelUp(int level) override { Buzzer::get().beepLevelUp(level); }
void beepGameOver() override { Buzzer::get().beepGameOver(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
void setMuted(bool muted) override { Buzzer::get().setMuted(muted); }
void toggleMuted() override { Buzzer::get().toggleMuted(); }
[[nodiscard]] bool isMuted() const override { return Buzzer::get().isMuted(); }
};
class EspRuntime::BatteryService final : public cardboy::sdk::IBatteryMonitor {
public:
[[nodiscard]] bool hasData() const override { return true; }
[[nodiscard]] bool hasData() const override { return true; }
[[nodiscard]] float voltage() const override { return BatMon::get().get_voltage(); }
[[nodiscard]] float charge() const override { return BatMon::get().get_charge(); }
[[nodiscard]] float current() const override { return BatMon::get().get_current(); }
@@ -114,14 +114,14 @@ public:
class EspRuntime::PowerService final : public cardboy::sdk::IPowerManager {
public:
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
void setSlowMode(bool enable) override { PowerHelper::get().set_slow(enable); }
[[nodiscard]] bool isSlowMode() const override { return PowerHelper::get().is_slow(); }
};
class EspRuntime::FilesystemService final : public cardboy::sdk::IFilesystem {
public:
bool mount() override { return FsHelper::get().mount() == ESP_OK; }
[[nodiscard]] bool isMounted() const override { return FsHelper::get().isMounted(); }
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{};
@@ -182,16 +182,6 @@ void EspRuntime::initializeHardware() {
FsHelper::get().mount();
}
int EspFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
int EspFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
void EspFramebuffer::drawPixel_impl(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= width_impl() || y >= height_impl())
return;
SMD::set_pixel(x, y, on);
}
void EspFramebuffer::clear_impl(bool on) {
for (int y = 0; y < height_impl(); ++y)
for (int x = 0; x < width_impl(); ++x)

View File

@@ -5,6 +5,8 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO)
add_subdirectory(utils)
add_subdirectory(backend_interface)
set(CARDBOY_SDK_BACKEND_LIBRARY "" CACHE STRING "Backend implementation library for Cardboy SDK")

View File

@@ -577,15 +577,6 @@ union cart_rtc
*/
struct gb_s
{
/**
* Return byte from ROM at given address.
*
* \param gb_s emulator context
* \param addr address
* \return byte at address in ROM
*/
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t addr);
/**
* Return byte from cart RAM at given address.
*
@@ -738,9 +729,14 @@ struct gb_s
/* Implementation defined data. Set to NULL if not required. */
void *priv;
const uint8_t* rom;
} direct;
};
__attribute__((always_inline)) inline uint8_t gb_rom_read(struct gb_s* s, const uint_fast32_t addr) {
return s->direct.rom[addr];
}
#ifndef PEANUT_GB_HEADER_ONLY
#define IO_JOYP 0x00
@@ -796,17 +792,17 @@ uint8_t __gb_read(struct gb_s *gb, uint16_t addr)
case 0x1:
case 0x2:
case 0x3:
return gb->gb_rom_read(gb, addr);
return gb_rom_read(gb, addr);
case 0x4:
case 0x5:
case 0x6:
case 0x7:
if(gb->mbc == 1 && gb->cart_mode_select)
return gb->gb_rom_read(gb,
return gb_rom_read(gb,
addr + ((gb->selected_rom_bank & 0x1F) - 1) * ROM_BANK_SIZE);
else
return gb->gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
return gb_rom_read(gb, addr + (gb->selected_rom_bank - 1) * ROM_BANK_SIZE);
case 0x8:
case 0x9:
@@ -3542,7 +3538,7 @@ int gb_get_save_size_s(struct gb_s *gb, size_t *ram_size)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3570,7 +3566,7 @@ uint_fast32_t gb_get_save_size(struct gb_s *gb)
/* 0, 2KiB, 8KiB, 32KiB, 128KiB, 64KiB */
0x00, 0x800, 0x2000, 0x8000, 0x20000, 0x10000
};
uint8_t ram_size_code = gb->gb_rom_read(gb, ram_size_location);
uint8_t ram_size_code = gb_rom_read(gb, ram_size_location);
/* MBC2 always has 512 half-bytes of cart RAM.
* This assumes that only the lower nibble of each byte is used; the
@@ -3603,7 +3599,7 @@ uint8_t gb_colour_hash(struct gb_s *gb)
uint16_t i;
for(i = ROM_TITLE_START_ADDR; i <= ROM_TITLE_END_ADDR; i++)
x += gb->gb_rom_read(gb, i);
x += gb_rom_read(gb, i);
return x;
}
@@ -3626,7 +3622,7 @@ void gb_reset(struct gb_s *gb)
if(gb->gb_bootrom_read == NULL)
{
uint8_t hdr_chk;
hdr_chk = gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
hdr_chk = gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC) != 0;
gb->cpu_reg.a = 0x01;
gb->cpu_reg.f.f_bits.z = 1;
@@ -3692,11 +3688,10 @@ void gb_reset(struct gb_s *gb)
}
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv)
void *priv, const uint8_t* rom_data)
{
const uint16_t mbc_location = 0x0147;
const uint16_t bank_count_location = 0x0148;
@@ -3731,11 +3726,11 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
* some early homebrew ROMs supposedly may use this value. */
const uint8_t num_ram_banks[] = { 0, 1, 1, 4, 16, 8 };
gb->gb_rom_read = gb_rom_read;
gb->gb_cart_ram_read = gb_cart_ram_read;
gb->gb_cart_ram_write = gb_cart_ram_write;
gb->gb_error = gb_error;
gb->direct.priv = priv;
gb->direct.rom = rom_data;
/* Initialise serial transfer function to NULL. If the front-end does
* not provide serial support, Peanut-GB will emulate no cable connected
@@ -3751,24 +3746,24 @@ enum gb_init_error_e gb_init(struct gb_s *gb,
uint16_t i;
for(i = 0x0134; i <= 0x014C; i++)
x = x - gb->gb_rom_read(gb, i) - 1;
x = x - gb_rom_read(gb, i) - 1;
if(x != gb->gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
if(x != gb_rom_read(gb, ROM_HEADER_CHECKSUM_LOC))
return GB_INIT_INVALID_CHECKSUM;
}
/* Check if cartridge type is supported, and set MBC type. */
{
const uint8_t mbc_value = gb->gb_rom_read(gb, mbc_location);
const uint8_t mbc_value = gb_rom_read(gb, mbc_location);
if(mbc_value > sizeof(cart_mbc) - 1 ||
(gb->mbc = cart_mbc[mbc_value]) == -1)
return GB_INIT_CARTRIDGE_UNSUPPORTED;
}
gb->num_rom_banks_mask = num_rom_banks_mask[gb->gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb->gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb->gb_rom_read(gb, ram_size_location)];
gb->num_rom_banks_mask = num_rom_banks_mask[gb_rom_read(gb, bank_count_location)] - 1;
gb->cart_ram = cart_ram[gb_rom_read(gb, mbc_location)];
gb->num_ram_banks = num_ram_banks[gb_rom_read(gb, ram_size_location)];
/* If the ROM says that it support RAM, but has 0 RAM banks, then
* disable RAM reads from the cartridge. */
@@ -3804,7 +3799,7 @@ const char* gb_get_rom_name(struct gb_s* gb, char *title_str)
for(; title_loc <= title_end; title_loc++)
{
const char title_char = gb->gb_rom_read(gb, title_loc);
const char title_char = gb_rom_read(gb, title_loc);
if(title_char >= ' ' && title_char <= '_')
{
@@ -3886,11 +3881,10 @@ void gb_set_rtc(struct gb_s *gb, const struct tm * const time)
* \returns 0 on success or an enum that describes the error.
*/
enum gb_init_error_e gb_init(struct gb_s *gb,
uint8_t (*gb_rom_read)(struct gb_s*, const uint_fast32_t),
uint8_t (*gb_cart_ram_read)(struct gb_s*, const uint_fast32_t),
void (*gb_cart_ram_write)(struct gb_s*, const uint_fast32_t, const uint8_t),
void (*gb_error)(struct gb_s*, const enum gb_error_e, const uint16_t),
void *priv);
void *priv, const uint8_t* rom_data);
/**
* Executes the emulator and runs for the duration of time equal to one frame.

View File

@@ -4,7 +4,9 @@
#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"
#include <inttypes.h>
@@ -34,6 +36,70 @@
#define GB_PERF_ONLY(...)
#endif
static constexpr uint8_t kPattern2x2[4] = {0, 2, 3, 1}; // [ (y&1)<<1 | (x&1) ]
// exact predicate per your shouldPixelBeOn
static constexpr uint8_t on_bit(uint8_t v, uint8_t threshold) {
if (v >= 3)
return 1;
if (v == 0)
return 0;
return static_cast<uint8_t>(v > threshold);
}
// Build 4 output bits (MSB = first pixel) from 4 packed 2-bit pixels.
// packed4 layout: p0 | p1<<2 | p2<<4 | p3<<6
static constexpr uint8_t makeNibble(uint8_t packed4, int xParity, int yParity) {
uint8_t out = 0;
int xp = xParity;
for (int i = 0; i < 4; ++i) {
const uint8_t v = (packed4 >> (i * 2)) & 0x3;
const uint8_t th = kPattern2x2[(yParity << 1) | (xp & 1)];
out = static_cast<uint8_t>((out << 1) | on_bit(v, th));
xp ^= 1; // toggle x parity each pixel
}
return out;
}
using LUT256 = std::array<uint8_t, 256>;
using LUT2x256 = std::array<LUT256, 2>;
using LUTFull = std::array<LUT2x256, 2>;
static constexpr LUTFull buildNibbleLUT() {
LUTFull L{};
for (int yp = 0; yp < 2; ++yp)
for (int xp = 0; xp < 2; ++xp)
for (int p = 0; p < 256; ++p)
L[yp][xp][p] = makeNibble(static_cast<uint8_t>(p), xp, yp);
return L;
}
inline constexpr LUTFull kNibbleLUT = buildNibbleLUT();
static constexpr LUTFull buildFullHeightWideByteLUT() {
LUTFull L{};
for (int yp = 0; yp < 2; ++yp)
for (int xp = 0; xp < 2; ++xp)
for (int p = 0; p < 256; ++p) {
const uint8_t p0 = static_cast<uint8_t>(p & 0x03);
const uint8_t p1 = static_cast<uint8_t>((p >> 2) & 0x03);
const uint8_t p2 = static_cast<uint8_t>((p >> 4) & 0x03);
const uint8_t p3 = static_cast<uint8_t>((p >> 6) & 0x03);
const uint8_t p4a = static_cast<uint8_t>(p0 | (p0 << 2) | (p1 << 4) | (p1 << 6));
const uint8_t p4b = static_cast<uint8_t>(p2 | (p2 << 2) | (p3 << 4) | (p3 << 6));
const uint8_t n0 = makeNibble(p4a, xp, yp);
const uint8_t n1 = makeNibble(p4b, xp, yp);
L[yp][xp][p] = static_cast<uint8_t>((n0 << 4) | (n1 & 0x0F));
}
return L;
}
inline constexpr LUTFull kFullHeightWideByteLUT = buildFullHeightWideByteLUT();
namespace apps {
namespace {
@@ -102,49 +168,6 @@ void appendEmbeddedRoms(std::vector<RomEntry>& out) {
}
}
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(Framebuffer& 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(Framebuffer& 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 cardboy::sdk::IApp {
public:
explicit GameboyApp(AppContext& ctx) :
@@ -157,8 +180,7 @@ public:
prevInput = {};
statusMessage.clear();
resetFpsStats();
scaleMode = ScaleMode::Original;
geometryDirty = true;
scaleMode = ScaleMode::FullHeightWide;
ensureFilesystemReady();
refreshRomList();
mode = Mode::Browse;
@@ -222,9 +244,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);
@@ -248,7 +268,7 @@ public:
private:
enum class Mode { Browse, Running };
enum class ScaleMode { Original, FullHeight };
enum class ScaleMode { Original, FullHeight, FullHeightWide };
struct PerfTracker {
enum class CallbackKind { RomRead, CartRamRead, CartRamWrite, LcdDraw, Error };
@@ -489,20 +509,40 @@ 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 constexpr int kFullHeightWideWidth = LCD_WIDTH * 2;
static constexpr int kFullHeightWideOffsetX = (((cardboy::sdk::kDisplayWidth - kFullHeightWideWidth) / 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);
static_assert(kFullHeightWideWidth % 8 == 0);
static_assert(kFullHeightWideOffsetX % 8 == 0);
static_assert(kFullHeightWideOffsetX + kFullHeightWideWidth <= 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;
@@ -513,10 +553,8 @@ private:
int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original;
bool geometryDirty = true;
RenderGeometry geometry{};
Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::FullHeightWide;
std::vector<RomEntry> roms;
std::size_t selectedIndex = 0;
bool browserDirty = true;
@@ -695,88 +733,21 @@ private:
}
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;
switch (scaleMode) {
case ScaleMode::Original:
scaleMode = ScaleMode::FullHeight;
setStatus("Scale: Full height");
break;
case ScaleMode::FullHeight:
scaleMode = ScaleMode::FullHeightWide;
setStatus("Scale: Full height 2x");
break;
case ScaleMode::FullHeightWide:
scaleMode = ScaleMode::Original;
setStatus("Scale: Original");
break;
}
frameDirty = true;
}
void handleBrowserInput(const InputState& input) {
@@ -921,8 +892,8 @@ private:
}
std::memset(&gb, 0, sizeof(gb));
const auto initResult = gb_init(&gb, &GameboyApp::romRead, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
&GameboyApp::errorCallback, this);
const auto initResult = gb_init(&gb, &GameboyApp::cartRamRead, &GameboyApp::cartRamWrite,
&GameboyApp::errorCallback, this, romDataView);
if (initResult != GB_INIT_NO_ERROR) {
setStatus(initErrorToString(initResult));
romData.clear();
@@ -954,7 +925,6 @@ private:
gbReady = true;
mode = Mode::Running;
frameDirty = true;
geometryDirty = true;
activeRomName = rom.name.empty() ? "Game" : rom.name;
std::string statusText = "Running " + activeRomName;
@@ -973,7 +943,6 @@ private:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
geometryDirty = true;
return;
}
@@ -987,7 +956,6 @@ private:
cartRam.clear();
activeRomName.clear();
activeRomSavePath.clear();
geometryDirty = true;
std::memset(&gb, 0, sizeof(gb));
mode = Mode::Browse;
browserDirty = true;
@@ -1034,8 +1002,6 @@ private:
return;
frameDirty = false;
ensureRenderGeometry();
++fpsFrameCounter;
++totalFrameCounter;
const uint32_t nowMs = context.clock.millis();
@@ -1052,21 +1018,26 @@ private:
char fpsValueBuf[16];
std::snprintf(fpsValueBuf, sizeof(fpsValueBuf), "%u", static_cast<unsigned>(fpsCurrent));
const std::string fpsValue(fpsValueBuf);
const std::string fpsLabel = "FPS";
const std::string fpsText = fpsValue + " FPS";
const std::string scaleHint = (scaleMode == ScaleMode::FullHeight) ? "START+B NORMAL" : "START+B SCALE";
const std::string fpsLabel = "FPS";
const std::string fpsText = fpsValue + " FPS";
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);
std::string scaleHint;
switch (scaleMode) {
case ScaleMode::Original:
scaleHint = "START+B FULL";
break;
case ScaleMode::FullHeight:
scaleHint = "START+B WIDE";
break;
case ScaleMode::FullHeightWide:
scaleHint = "START+B NORMAL";
break;
}
if (scaleMode == ScaleMode::FullHeight || scaleMode == ScaleMode::FullHeightWide) {
const int textScale = 1;
const int screenHeight = framebuffer.height();
const int screenWidth = framebuffer.width();
const int horizontalPadding = 8;
const int fpsLineGap = 4;
@@ -1081,18 +1052,24 @@ private:
if (!activeRomName.empty()) {
const std::string rotatedRomName(activeRomName.rbegin(), activeRomName.rend());
const int textHeight = measureVerticalText(rotatedRomName, textScale);
const int maxOrigin = std::max(0, screenHeight - textHeight);
int leftX = 8;
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
drawTextRotated(framebuffer, leftX, leftY, rotatedRomName, true, textScale, true, 1);
const auto textBounds =
font16x8::measureTextBounds(rotatedRomName, textScale, 1, font16x8::Rotation::Clockwise90);
const int textHeight = textBounds.height;
const int maxOrigin = std::max(0, screenHeight - textHeight);
int leftX = 8;
int leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
font16x8::drawText(framebuffer, leftX, leftY, rotatedRomName, textScale, true, 1,
font16x8::Rotation::Clockwise90);
if (!statusMessage.empty()) {
const std::string rotatedStatusMessage(statusMessage.rbegin(), statusMessage.rend());
const int textHeight = measureVerticalText(rotatedStatusMessage, textScale);
const int maxOrigin = std::max(0, screenHeight - textHeight);
leftX = leftX + 20;
leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
drawTextRotated(framebuffer, leftX, leftY, rotatedStatusMessage, true, textScale, true, 1);
const auto statusBounds = font16x8::measureTextBounds(rotatedStatusMessage, textScale, 1,
font16x8::Rotation::Clockwise90);
const int textHeight = statusBounds.height;
const int maxOrigin = std::max(0, screenHeight - textHeight);
leftX = leftX + 20;
leftY = std::clamp((screenHeight - textHeight) / 2, 0, maxOrigin);
font16x8::drawText(framebuffer, leftX, leftY, rotatedStatusMessage, textScale, true, 1,
font16x8::Rotation::Clockwise90);
}
}
@@ -1110,7 +1087,8 @@ private:
std::string rotated(text.rbegin(), text.rend());
if (totalRightHeight > 0)
totalRightHeight += gap;
totalRightHeight += measureVerticalText(rotated, textScale);
const auto bounds = font16x8::measureTextBounds(rotated, textScale, 1, font16x8::Rotation::Clockwise90);
totalRightHeight += bounds.height;
}
const int maxRightOrigin = std::max(0, screenHeight - totalRightHeight);
@@ -1125,8 +1103,10 @@ private:
if (text.empty())
continue;
std::string rotated(text.rbegin(), text.rend());
rightY = screenHeight - measureVerticalText(rotated, textScale) - 8;
drawTextRotated(framebuffer, rightX, rightY, rotated, true, textScale, true, 1);
const auto bounds = font16x8::measureTextBounds(rotated, textScale, 1, font16x8::Rotation::Clockwise90);
rightY = screenHeight - bounds.height - 8;
font16x8::drawText(framebuffer, rightX, rightY, rotated, textScale, true, 1,
font16x8::Rotation::Clockwise90);
rightX -= 20;
}
@@ -1221,8 +1201,7 @@ private:
}
static GameboyApp* fromGb(struct gb_s* gb) {
if (!gb)
return nullptr;
CARDBOY_CHECK_CODE(if (!gb) return nullptr;);
return static_cast<GameboyApp*>(gb->direct.priv);
}
@@ -1236,16 +1215,122 @@ 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;
const int yParity = dstY & 1;
CARDBOY_CHECK((baseX % 8) == 0);
CARDBOY_CHECK((LCD_WIDTH % 8) == 0); // 160 is fine
int x = 0;
while (x < LCD_WIDTH) {
// Build two 4-pixel packs (2 bits per pixel → 8-bit pack)
uint8_t p4a = pixels[x + 0] | (pixels[x + 1] << 2) | (pixels[x + 2] << 4) | (pixels[x + 3] << 6);
uint8_t p4b = pixels[x + 4] | (pixels[x + 5] << 2) | (pixels[x + 6] << 4) | (pixels[x + 7] << 6);
const int xParity = (baseX + x) & 1; // start parity for this byte
const uint8_t n0 = kNibbleLUT[yParity][xParity][p4a];
const uint8_t n1 = kNibbleLUT[yParity][xParity][p4b]; // same parity after 4 pixels
const uint8_t pack = static_cast<uint8_t>((n0 << 4) | (n1 & 0x0F));
fb.drawBits8(baseX + x, dstY, pack);
x += 8;
}
}
static void drawLineFullHeight(GameboyApp& self, const uint8_t pixels[160], int srcLine) {
Framebuffer& fb = self.framebuffer;
const int yStart = kFullHeightRowBounds[static_cast<std::size_t>(srcLine)];
const 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) {
std::array<uint8_t, 8> vals{}; // collected 2-bit pixel values for this output byte
int collected = 0;
const int yParity = dstY & 1;
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);
// expand this source pixel across its horizontal span
const uint8_t v = static_cast<uint8_t>(pixels[x]);
for (int dstX = colStart; dstX < colEnd; ++dstX) {
vals[static_cast<std::size_t>(collected)] = v;
++collected;
if (collected == 8) {
const int byteStart = dstX - 7;
CARDBOY_CHECK((byteStart % 8) == 0);
const int xParity = byteStart & 1;
// build two 4-pixel packs from the 8 collected values (mask to 2 bits)
const uint8_t p4a = (vals[0]) | ((vals[1]) << 2) | ((vals[2]) << 4) | ((vals[3]) << 6);
const uint8_t p4b = (vals[4]) | ((vals[5]) << 2) | ((vals[6]) << 4) | ((vals[7]) << 6);
// two LUT hits → two nibbles → one byte
const uint8_t n0 = kNibbleLUT[yParity][xParity][p4a];
const uint8_t n1 = kNibbleLUT[yParity][xParity][p4b];
const uint8_t pack = static_cast<uint8_t>((n0 << 4) | (n1 & 0x0F));
fb.drawBits8(byteStart, dstY, pack);
collected = 0; // reset for next byte
}
}
}
CARDBOY_CHECK(collected == 0); // must end on byte boundary
}
}
static void drawLineFullHeightWide(GameboyApp& self, const uint8_t pixels[160], int srcLine) {
Framebuffer& fb = self.framebuffer;
const int yStart = kFullHeightRowBounds[static_cast<std::size_t>(srcLine)];
const int yEnd = kFullHeightRowBounds[static_cast<std::size_t>(srcLine) + 1];
CARDBOY_CHECK(yEnd > yStart);
CARDBOY_CHECK((kFullHeightWideOffsetX % 8) == 0);
const int xParityBase = kFullHeightWideOffsetX & 1;
for (int dstY = yStart; dstY < yEnd; ++dstY) {
const int yParity = dstY & 1;
int dstX = kFullHeightWideOffsetX;
for (int srcX = 0; srcX < LCD_WIDTH; srcX += 4) {
const uint8_t packIndex = static_cast<uint8_t>((pixels[srcX + 0]) | ((pixels[srcX + 1]) << 2) |
((pixels[srcX + 2]) << 4) | ((pixels[srcX + 3]) << 6));
const uint8_t pack = kFullHeightWideByteLUT[yParity][xParityBase][packIndex];
fb.drawBits8(dstX, dstY, pack);
dstX += 8;
}
CARDBOY_CHECK(dstX == kFullHeightWideOffsetX + kFullHeightWideWidth);
}
}
public:
static uint8_t romRead(struct gb_s* gb, const uint_fast32_t addr) {
auto* self = fromGb(gb);
if (!self)
return 0xFF;
// ScopedCallbackTimer timer(self, PerfTracker::CallbackKind::RomRead);
if (!self->romDataView || addr >= self->romDataViewSize)
return 0xFF;
CARDBOY_CHECK_CODE(if (!self) return 0xFF;
if (!self->romDataView || addr >= self->romDataViewSize) return 0xFF;);
// ScopedCallbackTimer timer(self,
// PerfTracker::CallbackKind::RomRead);
return self->romDataView[static_cast<std::size_t>(addr)];
}
private:
static uint8_t cartRamRead(struct gb_s* gb, const uint_fast32_t addr) {
auto* self = fromGb(gb);
if (!self)
@@ -1281,48 +1366,26 @@ 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);
}
switch (self->scaleMode) {
case ScaleMode::FullHeight:
drawLineFullHeight(*self, pixels, static_cast<int>(line));
break;
case ScaleMode::FullHeightWide:
drawLineFullHeightWide(*self, pixels, static_cast<int>(line));
break;
case ScaleMode::Original:
default:
drawLineOriginal(*self, pixels, static_cast<int>(line));
break;
}
}

View File

@@ -1,5 +1,7 @@
add_library(cardboy_backend_interface INTERFACE)
target_link_libraries(cardboy_backend_interface INTERFACE cardboy_utils)
set_target_properties(cardboy_backend_interface PROPERTIES
EXPORT_NAME backend_interface
)

View File

@@ -23,6 +23,9 @@ concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
{ impl.sendFrame_impl(flag) };
};
template<typename Impl>
concept HasDrawBits8Impl = requires(Impl& impl, int x, int y, std::uint8_t bits) { impl.drawBits8_impl(x, y, bits); };
template<typename Impl>
concept HasFrameInFlightImpl = requires(const Impl& impl) {
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
@@ -37,11 +40,19 @@ concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
template<typename Impl>
class FramebufferFacade {
public:
[[nodiscard]] int width() const { return impl().width_impl(); }
[[nodiscard]] int height() const { return impl().height_impl(); }
[[nodiscard]] __attribute__((always_inline)) int width() const { return impl().width_impl(); }
[[nodiscard]] __attribute__((always_inline)) int height() const { return impl().height_impl(); }
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
__attribute__((always_inline)) void drawBits8(int x, int y, std::uint8_t bits) {
if constexpr (detail::HasDrawBits8Impl<Impl>) {
impl().drawBits8_impl(x, y, bits);
} else {
defaultDrawBits8(x, y, bits);
}
}
void clear(bool on) {
if constexpr (detail::HasClearImpl<Impl>) {
impl().clear_impl(on);
@@ -79,6 +90,14 @@ private:
for (int x = 0; x < width(); ++x)
drawPixel(x, y, on);
}
void defaultDrawBits8(int x, int y, std::uint8_t bits) {
for (int col = 0; col < 8; ++col) {
const std::uint8_t mask = static_cast<std::uint8_t>(1u << (7 - col));
const bool pixelOn = (bits & mask) != 0;
drawPixel(x + col, y, pixelOn);
}
}
};
template<typename Impl>

View File

@@ -26,22 +26,76 @@ inline const std::array<uint8_t, kGlyphHeight>& glyphBitmap(char ch) {
return fonts_Terminess_Powerline[uc];
}
enum class Rotation {
None,
Clockwise90,
CounterClockwise90,
};
struct TextBounds {
int width = 0;
int height = 0;
};
template<typename Framebuffer>
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true) {
inline void drawGlyph(Framebuffer& fb, int x, int y, char ch, int scale = 1, bool on = true,
Rotation rotation = Rotation::None) {
const auto& rows = glyphBitmap(ch);
if (rotation == Rotation::None && scale == 1 && on && ((x % 8) == 0)) {
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
fb.drawBits8(x, y + row, rowBits);
}
return;
}
for (int row = 0; row < kGlyphHeight; ++row) {
const uint8_t rowBits = rows[row];
for (int col = 0; col < kGlyphWidth; ++col) {
const uint8_t mask = static_cast<uint8_t>(1u << (kGlyphWidth - 1 - col));
if (rowBits & mask) {
for (int sx = 0; sx < scale; ++sx)
for (int sy = 0; sy < scale; ++sy)
fb.drawPixel(x + col * scale + sx, y + row * scale + sy, on);
if ((rowBits & mask) == 0)
continue;
for (int sx = 0; sx < scale; ++sx) {
for (int sy = 0; sy < scale; ++sy) {
int dstX = x;
int dstY = y;
switch (rotation) {
case Rotation::None:
dstX += col * scale + sx;
dstY += row * scale + sy;
break;
case Rotation::Clockwise90:
dstX += row * scale + sx;
dstY += (kGlyphWidth - 1 - col) * scale + sy;
break;
case Rotation::CounterClockwise90:
dstX += (kGlyphHeight - 1 - row) * scale + sx;
dstY += col * scale + sy;
break;
}
fb.drawPixel(dstX, dstY, on);
}
}
}
}
}
inline TextBounds measureTextBounds(std::string_view text, int scale = 1, int letterSpacing = 1,
Rotation rotation = Rotation::None) {
if (text.empty())
return {};
const int advance = (kGlyphWidth + letterSpacing) * scale;
const int extent = static_cast<int>(text.size()) * advance - letterSpacing * scale;
const int height = kGlyphHeight * scale;
switch (rotation) {
case Rotation::None:
return {extent, height};
case Rotation::Clockwise90:
case Rotation::CounterClockwise90:
return {height, extent};
}
return {extent, height};
}
inline int measureText(std::string_view text, int scale = 1, int letterSpacing = 1) {
if (text.empty())
return 0;
@@ -51,11 +105,22 @@ inline int measureText(std::string_view text, int scale = 1, int letterSpacing =
template<typename Framebuffer>
inline void drawText(Framebuffer& fb, int x, int y, std::string_view text, int scale = 1, bool on = true,
int letterSpacing = 1) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on);
cursor += (kGlyphWidth + letterSpacing) * scale;
int letterSpacing = 1, Rotation rotation = Rotation::None) {
if (text.empty())
return;
const int advance = (kGlyphWidth + letterSpacing) * scale;
if (rotation == Rotation::None) {
int cursor = x;
for (char ch: text) {
drawGlyph(fb, cursor, y, ch, scale, on, rotation);
cursor += advance;
}
} else {
int cursor = y;
for (char ch: text) {
drawGlyph(fb, x, cursor, ch, scale, on, rotation);
cursor += advance;
}
}
}

View File

@@ -0,0 +1,21 @@
add_library(cardboy_utils INTERFACE)
option(CARDBOY_MORE_CHECKS "More checks" OFF)
set_target_properties(cardboy_utils PROPERTIES
EXPORT_NAME utils
)
target_include_directories(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include
)
if(CARDBOY_MORE_CHECKS)
target_compile_definitions(cardboy_utils INTERFACE CARDBOY_MORE_CHECKS=1)
endif()
target_sources(cardboy_utils
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include/cardboy/utils/utils.hpp
)

View File

@@ -0,0 +1,59 @@
//
// Created by Stepan Usatiuk on 11.10.2025.
//
#ifndef CARDBOY_SDK_UTILS_HPP
#define CARDBOY_SDK_UTILS_HPP
#ifndef CARDBOY_MORE_CHECKS
#define CARDBOY_MORE_CHECKS 0
#endif
#if CARDBOY_MORE_CHECKS
#include <cstdio>
#include <cstdlib>
// Fails the program with a message. Internal use.
#define CARDBOY__CHECK_FAIL_IMPL(expr_str, file, line, func, msg_opt) \
do { \
std::fprintf(stderr, \
"CARDBOY_CHECK failed: %s\n at %s:%d in %s%s%s\n", \
(expr_str), (file), (line), (func), \
((msg_opt) ? "\n message: " : ""), \
((msg_opt) ? (msg_opt) : "")); \
std::fflush(stderr); \
std::abort(); \
} while (0)
// Runtime check that is active only when CARDBOY_MORE_CHECKS != 0.
// Evaluates the expression exactly once.
#define CARDBOY_CHECK(expr) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, NULL); \
} \
} while (0)
// Same as CARDBOY_CHECK but allows providing a custom C-string message.
#define CARDBOY_CHECK_MSG(expr, msg) \
do { \
if (!(expr)) { \
CARDBOY__CHECK_FAIL_IMPL(#expr, __FILE__, __LINE__, __func__, (msg));\
} \
} while (0)
// Execute arbitrary code only when checks are enabled.
#define CARDBOY_CHECK_CODE(code) \
do { \
code; \
} while (0)
#else
// Checks compiled out when CARDBOY_MORE_CHECKS == 0.
#define CARDBOY_CHECK(expr) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_MSG(expr, _) do { (void)sizeof(expr); } while (0)
#define CARDBOY_CHECK_CODE(code) do { } while (0)
#endif
#endif // CARDBOY_SDK_UTILS_HPP

View File

@@ -644,7 +644,7 @@ CONFIG_COMPILER_RT_LIB_GCCLIB=y
CONFIG_COMPILER_RT_LIB_NAME="gcc"
CONFIG_COMPILER_ORPHAN_SECTIONS_WARNING=y
# CONFIG_COMPILER_ORPHAN_SECTIONS_PLACE is not set
CONFIG_COMPILER_STATIC_ANALYZER=y
# CONFIG_COMPILER_STATIC_ANALYZER is not set
# end of Compiler options
#