mirror of
https://github.com/usatiuk/cardboy.git
synced 2025-10-28 15:17:48 +01:00
Compare commits
9 Commits
f721ebcb4c
...
df57e55171
| Author | SHA1 | Date | |
|---|---|---|---|
| df57e55171 | |||
| a3b837f329 | |||
| fc9e85aea0 | |||
| b55feb68f8 | |||
| f04b026d46 | |||
| e18278e130 | |||
| 9a392d6aec | |||
| 961453e28a | |||
| a4c2719077 |
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
@@ -514,9 +554,7 @@ private:
|
||||
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
|
||||
|
||||
Mode mode = Mode::Browse;
|
||||
ScaleMode scaleMode = ScaleMode::Original;
|
||||
bool geometryDirty = true;
|
||||
RenderGeometry geometry{};
|
||||
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)
|
||||
switch (scaleMode) {
|
||||
case ScaleMode::Original:
|
||||
scaleMode = ScaleMode::FullHeight;
|
||||
else
|
||||
setStatus("Scale: Full height");
|
||||
break;
|
||||
case ScaleMode::FullHeight:
|
||||
scaleMode = ScaleMode::FullHeightWide;
|
||||
setStatus("Scale: Full height 2x");
|
||||
break;
|
||||
case ScaleMode::FullHeightWide:
|
||||
scaleMode = ScaleMode::Original;
|
||||
geometryDirty = true;
|
||||
setStatus("Scale: Original");
|
||||
break;
|
||||
}
|
||||
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) {
|
||||
@@ -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();
|
||||
@@ -1054,19 +1020,24 @@ private:
|
||||
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";
|
||||
|
||||
if (scaleMode == ScaleMode::FullHeight) {
|
||||
const auto& geom = geometry;
|
||||
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 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;
|
||||
@@ -1081,18 +1052,24 @@ private:
|
||||
|
||||
if (!activeRomName.empty()) {
|
||||
const std::string rotatedRomName(activeRomName.rbegin(), activeRomName.rend());
|
||||
const int textHeight = measureVerticalText(rotatedRomName, textScale);
|
||||
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);
|
||||
drawTextRotated(framebuffer, leftX, leftY, rotatedRomName, true, textScale, true, 1);
|
||||
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 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);
|
||||
drawTextRotated(framebuffer, leftX, leftY, rotatedStatusMessage, true, textScale, true, 1);
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
)
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -26,21 +26,75 @@ 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())
|
||||
@@ -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 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);
|
||||
cursor += (kGlyphWidth + letterSpacing) * scale;
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
21
Firmware/sdk/utils/CMakeLists.txt
Normal file
21
Firmware/sdk/utils/CMakeLists.txt
Normal 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
|
||||
)
|
||||
59
Firmware/sdk/utils/include/cardboy/utils/utils.hpp
Normal file
59
Firmware/sdk/utils/include/cardboy/utils/utils.hpp
Normal 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
|
||||
@@ -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
|
||||
|
||||
#
|
||||
|
||||
Reference in New Issue
Block a user