some cleanup

This commit is contained in:
2025-10-11 11:15:39 +02:00
parent 5b75ff28e0
commit 535b0078e5
25 changed files with 359 additions and 368 deletions

View File

@@ -2,5 +2,7 @@
# CMakeLists in this exact order for cmake to work correctly # CMakeLists in this exact order for cmake to work correctly
cmake_minimum_required(VERSION 3.16) cmake_minimum_required(VERSION 3.16)
add_compile_options(-Ofast)
include($ENV{IDF_PATH}/tools/cmake/project.cmake) include($ENV{IDF_PATH}/tools/cmake/project.cmake)
project(hello_world) project(hello_world)

View File

@@ -2,4 +2,20 @@ idf_component_register()
add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build) add_subdirectory("${CMAKE_CURRENT_LIST_DIR}/../../sdk" cb-sdk-build)
target_link_libraries(${COMPONENT_LIB} INTERFACE cardboy_sdk cardboy_sdk) target_link_libraries(${COMPONENT_LIB}
INTERFACE
cardboy_sdk
cardboy_apps
)
target_compile_definitions(${COMPONENT_LIB}
INTERFACE
CARDBOY_SDK_BACKEND_HEADER=\"cardboy/backend/esp_backend.hpp\"
CARDBOY_SDK_ACTIVE_BACKEND_TYPE=cardboy::backend::EspBackend
)
target_include_directories(${COMPONENT_LIB}
INTERFACE
${CMAKE_CURRENT_LIST_DIR}/../../main/include
)
target_compile_options(${COMPONENT_LIB} INTERFACE -fjump-tables -ftree-switch-conversion)

View File

@@ -1,21 +1,14 @@
idf_component_register(SRCS idf_component_register(SRCS
src/app_main.cpp src/app_main.cpp
../sdk/apps/menu_app.cpp
../sdk/apps/clock_app.cpp
../sdk/apps/tetris_app.cpp
../sdk/apps/gameboy_app.cpp
src/display.cpp src/display.cpp
src/bat_mon.cpp src/bat_mon.cpp
src/spi_global.cpp src/spi_global.cpp
src/i2c_global.cpp src/i2c_global.cpp
src/disp_tools.cpp
src/disp_tty.cpp
src/shutdowner.cpp src/shutdowner.cpp
src/buttons.cpp src/buttons.cpp
src/power_helper.cpp src/power_helper.cpp
src/buzzer.cpp src/buzzer.cpp
src/fs_helper.cpp src/fs_helper.cpp
../sdk/src/app_system.cpp
PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs PRIV_REQUIRES spi_flash esp_driver_i2c driver sdk-esp esp_timer nvs_flash littlefs
INCLUDE_DIRS "include" "../sdk/include" INCLUDE_DIRS "include" "../sdk/include"
EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb") EMBED_FILES "roms/builtin_demo1.gb" "roms/builtin_demo2.gb")

View File

@@ -11,9 +11,6 @@ using AppButtonEvent = cardboy::sdk::AppButtonEvent;
using AppTimerEvent = cardboy::sdk::AppTimerEvent; using AppTimerEvent = cardboy::sdk::AppTimerEvent;
using AppEvent = cardboy::sdk::AppEvent; using AppEvent = cardboy::sdk::AppEvent;
template<typename FramebufferT, typename InputT, typename ClockT>
using BasicAppContext = cardboy::sdk::BasicAppContext<FramebufferT, InputT, ClockT>;
using AppContext = cardboy::sdk::AppContext; using AppContext = cardboy::sdk::AppContext;
using IApp = cardboy::sdk::IApp; using IApp = cardboy::sdk::IApp;

View File

@@ -5,37 +5,37 @@
#include "config.hpp" #include "config.hpp"
#include <buttons.hpp> #include <buttons.hpp>
#include <disp_tools.hpp> #include <display.hpp>
#include <power_helper.hpp> #include <power_helper.hpp>
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/task.h" #include "freertos/task.h"
class PlatformFramebuffer final : public cardboy::sdk::IFramebuffer { class PlatformFramebuffer final : public cardboy::sdk::FramebufferFacade<PlatformFramebuffer> {
public: public:
int width() const override { return cardboy::sdk::kDisplayWidth; } [[nodiscard]] int width_impl() const { return cardboy::sdk::kDisplayWidth; }
int height() const override { return cardboy::sdk::kDisplayHeight; } [[nodiscard]] int height_impl() const { return cardboy::sdk::kDisplayHeight; }
void drawPixel(int x, int y, bool on) override { __attribute__((always_inline)) void drawPixel_impl(int x, int y, bool on) {
if (x < 0 || y < 0 || x >= width() || y >= height()) if (x < 0 || y < 0 || x >= width() || y >= height())
return; return;
DispTools::set_pixel(x, y, on); SMD::set_pixel(x, y, on);
} }
void clear(bool on) override { void clear_impl(bool on) {
for (int y = 0; y < height(); ++y) for (int y = 0; y < height(); ++y)
for (int x = 0; x < width(); ++x) for (int x = 0; x < width(); ++x)
DispTools::set_pixel(x, y, on); SMD::set_pixel(x, y, on);
} }
void beginFrame() override { DispTools::draw_to_display_async_wait(); } __attribute__((always_inline)) void frameReady_impl() { SMD::frame_ready(); }
void endFrame() override { DispTools::draw_to_display_async_start(); } __attribute__((always_inline)) void sendFrame_impl(bool clear) { SMD::send_frame(clear); }
bool isFrameInFlight() const override { return DispTools::draw_to_display_async_busy(); } __attribute__((always_inline)) [[nodiscard]] bool frameInFlight_impl() const { return SMD::frame_transfer_in_flight(); }
}; };
class PlatformInput final : public cardboy::sdk::IInput { class PlatformInput final : public cardboy::sdk::InputFacade<PlatformInput> {
public: public:
cardboy::sdk::InputState readState() override { cardboy::sdk::InputState readState_impl() {
cardboy::sdk::InputState state{}; cardboy::sdk::InputState state{};
const uint8_t pressed = Buttons::get().get_pressed(); const uint8_t pressed = Buttons::get().get_pressed();
if (pressed & BTN_UP) if (pressed & BTN_UP)
@@ -58,14 +58,14 @@ public:
} }
}; };
class PlatformClock final : public cardboy::sdk::IClock { class PlatformClock final : public cardboy::sdk::ClockFacade<PlatformClock> {
public: public:
std::uint32_t millis() override { std::uint32_t millis_impl() {
TickType_t ticks = xTaskGetTickCount(); TickType_t ticks = xTaskGetTickCount();
return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ); return static_cast<std::uint32_t>((static_cast<std::uint64_t>(ticks) * 1000ULL) / configTICK_RATE_HZ);
} }
void sleep_ms(std::uint32_t ms) override { void sleep_ms_impl(std::uint32_t ms) {
if (ms == 0) if (ms == 0)
return; return;
PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms)); PowerHelper::get().delay(static_cast<int>(ms), static_cast<int>(ms));

View File

@@ -0,0 +1,14 @@
#pragma once
#include "app_platform.hpp"
#include "cardboy/sdk/platform.hpp"
namespace cardboy::backend {
struct EspBackend {
using Framebuffer = PlatformFramebuffer;
using Input = PlatformInput;
using Clock = PlatformClock;
};
} // namespace cardboy::backend

View File

@@ -1,53 +0,0 @@
//
// Created by Stepan Usatiuk on 02.03.2025.
//
#ifndef CB_DISP_TOOLS_HPP
#define CB_DISP_TOOLS_HPP
#include <display.hpp>
namespace DispTools {
static void clear() {
for (int y = 0; y < DISP_HEIGHT; y++) {
for (int x = 0; x < DISP_WIDTH; x++) {
SMD::set_pixel(x, y, true);
}
}
}
static bool get_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
assert(false); // Not implemented
return true;
// return disp_frame[y][x];
}
static void reset_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
SMD::set_pixel(x, y, false);
}
static void set_pixel(int x, int y) {
if (x < 0 || x >= DISP_WIDTH || y < 0 || y >= DISP_HEIGHT)
assert(false);
SMD::set_pixel(x, y, true);
}
static void set_pixel(int x, int y, bool on) {
if (on) {
set_pixel(x, y);
} else {
reset_pixel(x, y);
}
}
// New simplified async pipeline wrappers
static void async_frame_start() { SMD::async_draw_wait(); } // call at frame start
static void async_frame_end() { SMD::async_draw_start(); } // call after rendering
// Legacy names (temporary) mapped to new API in case of straggling calls
static void draw_to_display_async_start() { SMD::async_draw_start(); }
static void draw_to_display_async_wait() { SMD::async_draw_wait(); }
static bool draw_to_display_async_busy() { return SMD::async_draw_busy(); }
};
#endif // DISP_TOOLS_HPP

View File

@@ -1,42 +0,0 @@
//
// Created by Stepan Usatiuk on 02.03.2025.
//
#ifndef DISP_TTY_HPP
#define DISP_TTY_HPP
#include <array>
#include <cstddef>
#include "config.hpp"
#include <format>
class FbTty {
public:
void putchar(char c);
void putstr(const char* str);
void reset();
template<typename... Args>
auto fmt(std::format_string<Args...> fmt, Args&&... args) {
auto str = std::format(fmt, std::forward<Args>(args)...);
putstr(str.c_str());
}
private:
void draw_char(int col, int row);
int _cur_col = 0;
int _cur_row = 0;
static constexpr size_t _max_col = DISP_WIDTH / 8;
static constexpr size_t _max_row = DISP_HEIGHT / 16;
std::array<std::array<char, _max_row>, _max_col> _buf = {};
void next_col();
void next_row();
};
#endif // DISP_TTY_HPP

View File

@@ -24,15 +24,15 @@ extern uint8_t* dma_buf;
void init(); void init();
// Double-buffered asynchronous frame pipeline: // Double-buffered asynchronous frame pipeline:
// Usage pattern each frame: // Usage pattern each frame:
// SMD::async_draw_wait(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced // SMD::frame_ready(); // (start of frame) waits for previous transfer & ensures draw buffer is ready/synced
// ... write pixels into dma_buf via set_pixel / surface ... // ... write pixels into dma_buf via set_pixel / surface ...
// SMD::async_draw_start(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer // SMD::send_frame(); // (end of frame) queues SPI DMA of current framebuffer; once SPI finishes the sent buffer
// // is asynchronously cleared so the alternate buffer is ready for the next frame // // is optionally cleared so the alternate buffer is ready for the next frame
void async_draw_start(); void send_frame(bool clear_after_send = true);
void async_draw_wait(); void frame_ready();
bool async_draw_busy(); // optional diagnostic: is a frame transfer still in flight? bool frame_transfer_in_flight(); // optional diagnostic: is a frame transfer still in flight?
static void set_pixel(int x, int y, bool value) { __attribute__((always_inline)) static void set_pixel(int x, int y, bool value) {
assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT); assert(x >= 0 && x < DISP_WIDTH && y >= 0 && y < DISP_HEIGHT);
unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8); unsigned lineIdx = 2 + kLineMultiSingle * y + (x / 8);

View File

@@ -6,6 +6,7 @@
#define POWER_HELPER_HPP #define POWER_HELPER_HPP
#include "freertos/FreeRTOS.h" #include "freertos/FreeRTOS.h"
#include "freertos/event_groups.h"
class PowerHelper { class PowerHelper {
public: public:

View File

@@ -14,7 +14,6 @@
#include <bat_mon.hpp> #include <bat_mon.hpp>
#include <buttons.hpp> #include <buttons.hpp>
#include <buzzer.hpp> #include <buzzer.hpp>
#include <disp_tools.hpp>
#include <display.hpp> #include <display.hpp>
#include <fs_helper.hpp> #include <fs_helper.hpp>
#include <i2c_global.hpp> #include <i2c_global.hpp>
@@ -299,8 +298,6 @@ extern "C" void app_main() {
BatMon::get(); BatMon::get();
SpiGlobal::get(); SpiGlobal::get();
SMD::init(); SMD::init();
DispTools::clear();
Buzzer::get().init(); Buzzer::get().init();
FsHelper::get().mount(); FsHelper::get().mount();

View File

@@ -1,10 +0,0 @@
//
// Created by Stepan Usatiuk on 02.03.2025.
//
#include "disp_tools.hpp"
#include <cmath>
#include <display.hpp>

View File

@@ -1,62 +0,0 @@
//
// Created by Stepan Usatiuk on 26.04.2024.
//
#include "disp_tty.hpp"
#include <disp_tools.hpp>
#include "cardboy/gfx/Fonts.hpp"
void FbTty::draw_char(int col, int row) {
for (int x = 0; x < 8; x++) {
for (int y = 0; y < 16; y++) {
bool color = fonts_Terminess_Powerline[_buf[col][row]][y] & (1 << (8 - x));
if (color)
DispTools::set_pixel(col * 8 + x, row * 16 + y);
else
DispTools::reset_pixel(col * 8 + x, row * 16 + y);
}
}
}
void FbTty::reset() {
_cur_col = 0;
_cur_row = 0;
}
void FbTty::putchar(char c) {
if (c == '\n') {
next_row();
return;
}
_buf[_cur_col][_cur_row] = c;
draw_char(_cur_col, _cur_row);
next_col();
}
void FbTty::putstr(const char* str) {
while (*str != 0) {
putchar(*str);
str++;
}
}
void FbTty::next_col() {
_cur_col++;
_cur_col = _cur_col % _max_col;
if (_cur_col == 0) {
next_row();
} else {
_buf[_cur_col][_cur_row] = ' ';
draw_char(_cur_col, _cur_row);
}
}
void FbTty::next_row() {
_cur_col = 0;
_cur_row++;
_cur_row = _cur_row % _max_row;
for (int i = 0; i < _max_col; i++) {
_buf[i][_cur_row] = ' ';
draw_char(i, _cur_row);
}
}

View File

@@ -4,7 +4,6 @@
#include <cassert> #include <cassert>
#include <cstring> #include <cstring>
#include <driver/gpio.h> #include <driver/gpio.h>
#include "disp_tools.hpp"
#include "driver/spi_master.h" #include "driver/spi_master.h"
#include "esp_async_memcpy.h" #include "esp_async_memcpy.h"
#include "esp_timer.h" #include "esp_timer.h"
@@ -27,6 +26,7 @@ static bool _vcom = false;
static TaskHandle_t s_clearTaskHandle = nullptr; static TaskHandle_t s_clearTaskHandle = nullptr;
static SemaphoreHandle_t s_clearReqSem = nullptr; static SemaphoreHandle_t s_clearReqSem = nullptr;
static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr}; static SemaphoreHandle_t s_bufferSem[2] = {nullptr, nullptr};
static bool s_clearPending[2] = {true, true};
static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG(); static async_memcpy_config_t config = ASYNC_MEMCPY_DEFAULT_CONFIG();
// update the maximum data stream supported by underlying DMA engine // update the maximum data stream supported by underlying DMA engine
@@ -58,8 +58,17 @@ static void clear_task(void*) {
ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0)); ESP_ERROR_CHECK(spi_device_get_trans_result(SMD::_spi, &r, 0));
int bufIdx = (int) r->user; int bufIdx = (int) r->user;
xSemaphoreGive(_txSem); xSemaphoreGive(_txSem);
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, 12480U, const bool shouldClear = s_clearPending[bufIdx];
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx]))); s_clearPending[bufIdx] = true;
if (shouldClear) {
constexpr unsigned alignedSize = SMD::kLineDataBytes - (SMD::kLineDataBytes % 4);
static_assert(SMD::kLineDataBytes - alignedSize < 8); // Last byte is zero anyway
ESP_ERROR_CHECK(esp_async_memcpy(driver, s_dma_buffers[bufIdx], dma_buf_template, alignedSize,
my_async_memcpy_cb, static_cast<void*>(s_bufferSem[bufIdx])));
} else {
if (!xSemaphoreGive(s_bufferSem[bufIdx]))
assert(false);
}
} }
} }
} }
@@ -101,9 +110,9 @@ void SMD::init() {
xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle); xTaskCreate(clear_task, "fbclr", 1536, nullptr, tskIDLE_PRIORITY + 1, &s_clearTaskHandle);
} }
bool SMD::async_draw_busy() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; } bool SMD::frame_transfer_in_flight() { return uxSemaphoreGetCount(s_bufferSem[s_drawBufIdx]) == 0; }
void SMD::async_draw_start() { void SMD::send_frame(bool clear_after_send) {
assert(driver != nullptr); assert(driver != nullptr);
if (!xSemaphoreTake(_txSem, portMAX_DELAY)) if (!xSemaphoreTake(_txSem, portMAX_DELAY))
assert(false); assert(false);
@@ -115,7 +124,8 @@ void SMD::async_draw_start() {
if (!xSemaphoreTake(sem, 0)) if (!xSemaphoreTake(sem, 0))
assert(false); assert(false);
const int nextDrawIdx = sendIdx ^ 1; const int nextDrawIdx = sendIdx ^ 1;
s_clearPending[sendIdx] = clear_after_send;
_vcom = !_vcom; _vcom = !_vcom;
_tx = {}; _tx = {};
@@ -129,7 +139,7 @@ void SMD::async_draw_start() {
dma_buf = s_dma_buffers[nextDrawIdx]; dma_buf = s_dma_buffers[nextDrawIdx];
} }
void SMD::async_draw_wait() { void SMD::frame_ready() {
SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx]; SemaphoreHandle_t sem = s_bufferSem[s_drawBufIdx];
// uint64_t waitedUs = 0; // uint64_t waitedUs = 0;
if (!uxSemaphoreGetCount(sem)) { if (!uxSemaphoreGetCount(sem)) {

View File

@@ -5,35 +5,39 @@ set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CXX_STANDARD_REQUIRED YES) set(CMAKE_CXX_STANDARD_REQUIRED YES)
set(CMAKE_CXX_EXTENSIONS NO) set(CMAKE_CXX_EXTENSIONS NO)
add_library(cardboy_sdk STATIC add_library(cardboy_sdk INTERFACE)
src/app_system.cpp
)
target_include_directories(cardboy_sdk target_include_directories(cardboy_sdk
PUBLIC INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
) )
target_compile_features(cardboy_sdk PUBLIC cxx_std_20) target_compile_features(cardboy_sdk INTERFACE cxx_std_20)
add_library(cardboy_apps STATIC target_sources(cardboy_sdk
apps/menu_app.cpp INTERFACE
apps/clock_app.cpp ${CMAKE_CURRENT_SOURCE_DIR}/src/app_system.cpp
apps/tetris_app.cpp
apps/gameboy_app.cpp
) )
add_library(cardboy_apps INTERFACE)
target_include_directories(cardboy_apps target_include_directories(cardboy_apps
PUBLIC INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/include ${CMAKE_CURRENT_SOURCE_DIR}/include
) )
target_link_libraries(cardboy_apps target_link_libraries(cardboy_apps
PUBLIC INTERFACE
cardboy_sdk cardboy_sdk
) )
target_compile_features(cardboy_apps PUBLIC cxx_std_20) target_sources(cardboy_apps
INTERFACE
${CMAKE_CURRENT_SOURCE_DIR}/apps/menu_app.cpp
${CMAKE_CURRENT_SOURCE_DIR}/apps/clock_app.cpp
${CMAKE_CURRENT_SOURCE_DIR}/apps/tetris_app.cpp
${CMAKE_CURRENT_SOURCE_DIR}/apps/gameboy_app.cpp
)
option(CARDBOY_BUILD_SFML "Build SFML harness" OFF) option(CARDBOY_BUILD_SFML "Build SFML harness" OFF)
@@ -66,5 +70,16 @@ if (CARDBOY_BUILD_SFML)
SFML::System SFML::System
) )
target_include_directories(cardboy_desktop
PRIVATE
${CMAKE_CURRENT_SOURCE_DIR}/hosts/include
)
target_compile_definitions(cardboy_desktop
PRIVATE
CARDBOY_SDK_BACKEND_HEADER=\"cardboy/backend/desktop_backend.hpp\"
CARDBOY_SDK_ACTIVE_BACKEND_TYPE=cardboy::backend::DesktopBackend
)
target_compile_features(cardboy_desktop PRIVATE cxx_std_20) target_compile_features(cardboy_desktop PRIVATE cxx_std_20)
endif () endif ()

View File

@@ -160,8 +160,7 @@ private:
return; return;
dirty = false; dirty = false;
framebuffer.beginFrame(); framebuffer.frameReady();
framebuffer.clear(false);
const int scaleLarge = 3; const int scaleLarge = 3;
const int scaleSeconds = 2; const int scaleSeconds = 2;
@@ -220,7 +219,7 @@ private:
drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1); drawCenteredText(framebuffer, framebuffer.height() - 36, "SELECT TOGGLE 12/24H", 1, 1);
drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1); drawCenteredText(framebuffer, framebuffer.height() - 18, "B BACK", 1, 1);
framebuffer.endFrame(); framebuffer.sendFrame();
} }
}; };

View File

@@ -2,9 +2,9 @@
#include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/peanut_gb.h" #include "cardboy/apps/peanut_gb.h"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/app_framework.hpp" #include "cardboy/sdk/app_framework.hpp"
#include "cardboy/sdk/app_system.hpp" #include "cardboy/sdk/app_system.hpp"
#include "cardboy/gfx/font16x8.hpp"
#include "cardboy/sdk/services.hpp" #include "cardboy/sdk/services.hpp"
#include <inttypes.h> #include <inttypes.h>
@@ -13,6 +13,7 @@
#include <array> #include <array>
#include <cctype> #include <cctype>
#include <cerrno> #include <cerrno>
#include <chrono>
#include <cstdint> #include <cstdint>
#include <cstdio> #include <cstdio>
#include <cstring> #include <cstring>
@@ -21,8 +22,7 @@
#include <string_view> #include <string_view>
#include <sys/stat.h> #include <sys/stat.h>
#include <vector> #include <vector>
#include <chrono> #define ESP_PLATFORM 1
#define GAMEBOY_PERF_METRICS 0 #define GAMEBOY_PERF_METRICS 0
#ifndef GAMEBOY_PERF_METRICS #ifndef GAMEBOY_PERF_METRICS
@@ -41,6 +41,7 @@ namespace {
constexpr int kMenuStartY = 48; constexpr int kMenuStartY = 48;
constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6; constexpr int kMenuSpacing = font16x8::kGlyphHeight + 6;
using cardboy::sdk::AppContext; using cardboy::sdk::AppContext;
using cardboy::sdk::AppEvent; using cardboy::sdk::AppEvent;
using cardboy::sdk::AppEventType; using cardboy::sdk::AppEventType;
@@ -250,8 +251,6 @@ public:
break; break;
} }
framebuffer.beginFrame();
framebuffer.clear(false);
GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();) GB_PERF_ONLY(const uint64_t geometryStartUs = nowMicros();)
ensureRenderGeometry(); ensureRenderGeometry();
@@ -264,7 +263,6 @@ public:
GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();) GB_PERF_ONLY(const uint64_t renderStartUs = nowMicros();)
renderGameFrame(); renderGameFrame();
GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;) GB_PERF_ONLY(perf.renderUs = nowMicros() - renderStartUs;)
framebuffer.endFrame();
break; break;
} }
} }
@@ -536,14 +534,14 @@ private:
std::array<int, LCD_WIDTH> colXEnd{}; std::array<int, LCD_WIDTH> colXEnd{};
}; };
AppContext& context; AppContext& context;
Framebuffer& framebuffer; Framebuffer& framebuffer;
cardboy::sdk::IFilesystem* filesystem = nullptr; cardboy::sdk::IFilesystem* filesystem = nullptr;
cardboy::sdk::IHighResClock* highResClock = nullptr; cardboy::sdk::IHighResClock* highResClock = nullptr;
PerfTracker perf{}; PerfTracker perf{};
AppTimerHandle tickTimer = kInvalidAppTimer; AppTimerHandle tickTimer = kInvalidAppTimer;
int64_t frameDelayCarryUs = 0; int64_t frameDelayCarryUs = 0;
static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms static constexpr uint32_t kTargetFrameUs = 1000000 / 60; // ~16.6 ms
Mode mode = Mode::Browse; Mode mode = Mode::Browse;
ScaleMode scaleMode = ScaleMode::Original; ScaleMode scaleMode = ScaleMode::Original;
@@ -849,8 +847,7 @@ private:
return; return;
browserDirty = false; browserDirty = false;
framebuffer.beginFrame(); framebuffer.frameReady();
framebuffer.clear(false);
const std::string_view title = "GAME BOY"; const std::string_view title = "GAME BOY";
const int titleWidth = font16x8::measureText(title, 2, 1); const int titleWidth = font16x8::measureText(title, 2, 1);
@@ -893,7 +890,7 @@ private:
font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1); font16x8::drawText(framebuffer, x, framebuffer.height() - 16, statusMessage, 1, true, 1);
} }
framebuffer.endFrame(); framebuffer.sendFrame();
} }
bool loadRom(std::size_t index) { bool loadRom(std::size_t index) {
@@ -1184,7 +1181,7 @@ private:
font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1); font16x8::drawText(framebuffer, statusX, statusY, statusMessage, 1, true, 1);
} }
} }
framebuffer.sendFrame();
} }
void maybeSaveRam() { void maybeSaveRam() {
@@ -1259,10 +1256,7 @@ private:
return static_cast<GameboyApp*>(gb->direct.priv); return static_cast<GameboyApp*>(gb->direct.priv);
} }
static bool shouldPixelBeOn(int value, int dstX, int dstY, bool useDither) { __attribute__((always_inline)) static bool shouldPixelBeOn(int value, int dstX, int dstY) {
value &= 0x03;
if (!useDither)
return value >= 2;
if (value >= 3) if (value >= 3)
return true; return true;
if (value <= 0) if (value <= 0)
@@ -1335,16 +1329,13 @@ private:
self->frameDirty = true; self->frameDirty = true;
Framebuffer& fb = self->framebuffer; Framebuffer& fb = self->framebuffer;
fb.frameReady();
const bool useDither = (self->scaleMode == ScaleMode::FullHeight) &&
(geom.scaledWidth != LCD_WIDTH || geom.scaledHeight != LCD_HEIGHT);
if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) { if (geom.scaledWidth == LCD_WIDTH && geom.scaledHeight == LCD_HEIGHT) {
const int dstY = yStart; const int dstY = yStart;
const int dstXBase = geom.offsetX; const int dstXBase = geom.offsetX;
for (int x = 0; x < LCD_WIDTH; ++x) { for (int x = 0; x < LCD_WIDTH; ++x) {
const int val = (pixels[x] & 0x03u); const bool on = shouldPixelBeOn(pixels[x], dstXBase + x, dstY);
const bool on = shouldPixelBeOn(val, dstXBase + x, dstY, useDither);
fb.drawPixel(dstXBase + x, dstY, on); fb.drawPixel(dstXBase + x, dstY, on);
} }
return; return;
@@ -1359,7 +1350,7 @@ private:
continue; continue;
for (int dstY = yStart; dstY < yEnd; ++dstY) for (int dstY = yStart; dstY < yEnd; ++dstY)
for (int dstX = drawStart; dstX < drawEnd; ++dstX) { for (int dstX = drawStart; dstX < drawEnd; ++dstX) {
const bool on = shouldPixelBeOn(pixels[x], dstX, dstY, useDither); const bool on = shouldPixelBeOn(pixels[x], dstX, dstY);
fb.drawPixel(dstX, dstY, on); fb.drawPixel(dstX, dstY, on);
} }
} }
@@ -1398,7 +1389,7 @@ private:
class GameboyAppFactory final : public cardboy::sdk::IAppFactory { class GameboyAppFactory final : public cardboy::sdk::IAppFactory {
public: public:
const char* name() const override { return kGameboyAppName; } const char* name() const override { return kGameboyAppName; }
std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override { std::unique_ptr<cardboy::sdk::IApp> create(cardboy::sdk::AppContext& context) override {
return std::make_unique<GameboyApp>(context); return std::make_unique<GameboyApp>(context);
} }
@@ -1406,8 +1397,6 @@ public:
} // namespace } // namespace
std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() { std::unique_ptr<cardboy::sdk::IAppFactory> createGameboyAppFactory() { return std::make_unique<GameboyAppFactory>(); }
return std::make_unique<GameboyAppFactory>();
}
} // namespace apps } // namespace apps

View File

@@ -135,8 +135,7 @@ private:
return; return;
dirty = false; dirty = false;
framebuffer.beginFrame(); framebuffer.frameReady();
framebuffer.clear(false);
drawCenteredText(framebuffer, 24, "APPS", 1, 1); drawCenteredText(framebuffer, 24, "APPS", 1, 1);
@@ -158,7 +157,7 @@ private:
drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1); drawCenteredText(framebuffer, framebuffer.height() - 28, "L/R CHOOSE", 1, 1);
} }
framebuffer.endFrame(); framebuffer.sendFrame();
} }
}; };

View File

@@ -467,15 +467,14 @@ private:
return; return;
dirty = false; dirty = false;
framebuffer.beginFrame(); framebuffer.frameReady();
framebuffer.clear(false);
drawBoard(); drawBoard();
drawActivePiece(); drawActivePiece();
drawNextPreview(); drawNextPreview();
drawHUD(); drawHUD();
framebuffer.endFrame(); framebuffer.sendFrame();
} }
void drawBoard() { void drawBoard() {

View File

@@ -0,0 +1,63 @@
#pragma once
#include "cardboy/sdk/platform.hpp"
#include <SFML/Window/Keyboard.hpp>
#include <chrono>
#include <cstdint>
namespace cardboy::backend::desktop {
class DesktopRuntime;
class DesktopFramebuffer final : public cardboy::sdk::FramebufferFacade<DesktopFramebuffer> {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime);
[[nodiscard]] int width_impl() const;
[[nodiscard]] int height_impl() const;
void drawPixel_impl(int x, int y, bool on);
void clear_impl(bool on);
void frameReady_impl();
void sendFrame_impl(bool clearAfterSend);
[[nodiscard]] bool frameInFlight_impl() const { return false; }
private:
DesktopRuntime& runtime;
};
class DesktopInput final : public cardboy::sdk::InputFacade<DesktopInput> {
public:
explicit DesktopInput(DesktopRuntime& runtime);
cardboy::sdk::InputState readState_impl();
void handleKey(sf::Keyboard::Key key, bool pressed);
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
class DesktopClock final : public cardboy::sdk::ClockFacade<DesktopClock> {
public:
explicit DesktopClock(DesktopRuntime& runtime);
std::uint32_t millis_impl();
void sleep_ms_impl(std::uint32_t ms);
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
struct Backend {
using Framebuffer = DesktopFramebuffer;
using Input = DesktopInput;
using Clock = DesktopClock;
};
} // namespace cardboy::backend::desktop
namespace cardboy::backend {
using DesktopBackend = desktop::Backend;
} // namespace cardboy::backend

View File

@@ -2,6 +2,7 @@
#include "cardboy/apps/gameboy_app.hpp" #include "cardboy/apps/gameboy_app.hpp"
#include "cardboy/apps/menu_app.hpp" #include "cardboy/apps/menu_app.hpp"
#include "cardboy/apps/tetris_app.hpp" #include "cardboy/apps/tetris_app.hpp"
#include "cardboy/backend/desktop_backend.hpp"
#include "cardboy/sdk/app_system.hpp" #include "cardboy/sdk/app_system.hpp"
#include "cardboy/sdk/display_spec.hpp" #include "cardboy/sdk/display_spec.hpp"
#include "cardboy/sdk/services.hpp" #include "cardboy/sdk/services.hpp"
@@ -26,7 +27,7 @@
#include <unordered_map> #include <unordered_map>
#include <vector> #include <vector>
namespace { namespace cardboy::backend::desktop {
constexpr int kPixelScale = 2; constexpr int kPixelScale = 2;
@@ -135,54 +136,6 @@ private:
bool mounted = false; bool mounted = false;
}; };
class DesktopRuntime;
class DesktopFramebuffer final : public cardboy::sdk::IFramebuffer {
public:
explicit DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
int width() const override;
int height() const override;
void drawPixel(int x, int y, bool on) override;
void clear(bool on) override;
void beginFrame() override {}
void endFrame() override;
private:
DesktopRuntime& runtime;
};
class DesktopInput final : public cardboy::sdk::IInput {
public:
explicit DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
cardboy::sdk::InputState readState() override { return state; }
void handleKey(sf::Keyboard::Key key, bool pressed);
private:
DesktopRuntime& runtime;
cardboy::sdk::InputState state{};
};
class DesktopClock final : public cardboy::sdk::IClock {
public:
explicit DesktopClock(DesktopRuntime& runtime)
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
std::uint32_t millis() override {
const auto now = std::chrono::steady_clock::now();
return static_cast<std::uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
}
void sleep_ms(std::uint32_t ms) override;
private:
DesktopRuntime& runtime;
const std::chrono::steady_clock::time_point start;
};
class DesktopRuntime { class DesktopRuntime {
private: private:
friend class DesktopFramebuffer; friend class DesktopFramebuffer;
@@ -193,8 +146,9 @@ private:
sf::Texture texture; sf::Texture texture;
sf::Sprite sprite; sf::Sprite sprite;
std::vector<std::uint8_t> pixels; // RGBA buffer std::vector<std::uint8_t> pixels; // RGBA buffer
bool dirty = true; bool dirty = true;
bool running = true; bool running = true;
bool clearNextFrame = true;
DesktopBuzzer buzzerService; DesktopBuzzer buzzerService;
DesktopBattery batteryService; DesktopBattery batteryService;
@@ -326,16 +280,32 @@ void DesktopRuntime::sleepFor(std::uint32_t ms) {
std::this_thread::yield(); std::this_thread::yield();
} while (true); } while (true);
} }
DesktopFramebuffer::DesktopFramebuffer(DesktopRuntime& runtime) : runtime(runtime) {}
int DesktopFramebuffer::width() const { return cardboy::sdk::kDisplayWidth; } int DesktopFramebuffer::width_impl() const { return cardboy::sdk::kDisplayWidth; }
int DesktopFramebuffer::height() const { return cardboy::sdk::kDisplayHeight; } int DesktopFramebuffer::height_impl() const { return cardboy::sdk::kDisplayHeight; }
void DesktopFramebuffer::drawPixel(int x, int y, bool on) { runtime.setPixel(x, y, on); } void DesktopFramebuffer::drawPixel_impl(int x, int y, bool on) { runtime.setPixel(x, y, on); }
void DesktopFramebuffer::clear(bool on) { runtime.clearPixels(on); } void DesktopFramebuffer::clear_impl(bool on) { runtime.clearPixels(on); }
void DesktopFramebuffer::endFrame() { runtime.dirty = true; } void DesktopFramebuffer::frameReady_impl() {
if (runtime.clearNextFrame) {
runtime.clearPixels(false);
runtime.clearNextFrame = false;
}
}
void DesktopFramebuffer::sendFrame_impl(bool clearAfterSend) {
runtime.clearNextFrame = clearAfterSend;
runtime.dirty = true;
}
DesktopInput::DesktopInput(DesktopRuntime& runtime) : runtime(runtime) {}
cardboy::sdk::InputState DesktopInput::readState_impl() { return state; }
void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) { void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
switch (key) { switch (key) {
@@ -370,9 +340,20 @@ void DesktopInput::handleKey(sf::Keyboard::Key key, bool pressed) {
} }
} }
void DesktopClock::sleep_ms(std::uint32_t ms) { runtime.sleepFor(ms); } DesktopClock::DesktopClock(DesktopRuntime& runtime)
: runtime(runtime), start(std::chrono::steady_clock::now()) {}
} // namespace std::uint32_t DesktopClock::millis_impl() {
const auto now = std::chrono::steady_clock::now();
return static_cast<std::uint32_t>(
std::chrono::duration_cast<std::chrono::milliseconds>(now - start).count());
}
void DesktopClock::sleep_ms_impl(std::uint32_t ms) { runtime.sleepFor(ms); }
} // namespace cardboy::backend::desktop
using cardboy::backend::desktop::DesktopRuntime;
int main() { int main() {
try { try {

View File

@@ -1,5 +1,6 @@
#pragma once #pragma once
#include "backend.hpp"
#include "platform.hpp" #include "platform.hpp"
#include "services.hpp" #include "services.hpp"
@@ -37,18 +38,23 @@ struct AppEvent {
AppTimerEvent timer{}; AppTimerEvent timer{};
}; };
template<typename FramebufferT, typename InputT, typename ClockT> #ifdef CARDBOY_SDK_ACTIVE_BACKEND_TYPE
struct BasicAppContext { using ActiveBackend = CARDBOY_SDK_ACTIVE_BACKEND_TYPE;
using Framebuffer = FramebufferT; #else
using Input = InputT; #error "CARDBOY_SDK_ACTIVE_BACKEND_TYPE must name the active backend type"
using Clock = ClockT; #endif
BasicAppContext() = delete; struct AppContext {
BasicAppContext(FramebufferT& fb, InputT& in, ClockT& clk) : framebuffer(fb), input(in), clock(clk) {} using Framebuffer = typename ActiveBackend::Framebuffer;
using Input = typename ActiveBackend::Input;
using Clock = typename ActiveBackend::Clock;
FramebufferT& framebuffer; AppContext() = delete;
InputT& input; AppContext(Framebuffer& fb, Input& in, Clock& clk) : framebuffer(fb), input(in), clock(clk) {}
ClockT& clock;
Framebuffer& framebuffer;
Input& input;
Clock& clock;
AppSystem* system = nullptr; AppSystem* system = nullptr;
Services* services = nullptr; Services* services = nullptr;
@@ -113,8 +119,6 @@ private:
void cancelAllTimersInternal(); void cancelAllTimersInternal();
}; };
using AppContext = BasicAppContext<IFramebuffer, IInput, IClock>;
class IApp { class IApp {
public: public:
virtual ~IApp() = default; virtual ~IApp() = default;

View File

@@ -28,8 +28,7 @@ public:
[[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; } [[nodiscard]] const IAppFactory* currentFactory() const { return activeFactory; }
private: private:
template<typename FramebufferT, typename InputT, typename ClockT> friend struct AppContext;
friend struct BasicAppContext;
struct TimerRecord { struct TimerRecord {
AppTimerHandle id = kInvalidAppTimer; AppTimerHandle id = kInvalidAppTimer;
@@ -63,23 +62,18 @@ private:
InputState lastInputState{}; InputState lastInputState{};
}; };
template<typename FramebufferT, typename InputT, typename ClockT> inline AppTimerHandle AppContext::scheduleTimerInternal(std::uint32_t delay_ms, bool repeat) {
AppTimerHandle BasicAppContext<FramebufferT, InputT, ClockT>::scheduleTimerInternal(std::uint32_t delay_ms,
bool repeat) {
return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer; return system ? system->scheduleTimer(delay_ms, repeat) : kInvalidAppTimer;
} }
template<typename FramebufferT, typename InputT, typename ClockT> inline void AppContext::cancelTimerInternal(AppTimerHandle handle) {
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelTimerInternal(AppTimerHandle handle) {
if (system) if (system)
system->cancelTimer(handle); system->cancelTimer(handle);
} }
template<typename FramebufferT, typename InputT, typename ClockT> inline void AppContext::cancelAllTimersInternal() {
void BasicAppContext<FramebufferT, InputT, ClockT>::cancelAllTimersInternal() {
if (system) if (system)
system->cancelAllTimers(); system->cancelAllTimers();
} }
} // namespace cardboy::sdk } // namespace cardboy::sdk

View File

@@ -0,0 +1,10 @@
#pragma once
#include "platform.hpp"
#ifndef CARDBOY_SDK_BACKEND_HEADER
#error "CARDBOY_SDK_BACKEND_HEADER must expand to the active backend header path"
#endif
#include CARDBOY_SDK_BACKEND_HEADER

View File

@@ -2,39 +2,114 @@
#include "input_state.hpp" #include "input_state.hpp"
#include <concepts>
#include <cstdint> #include <cstdint>
namespace cardboy::sdk { namespace cardboy::sdk {
class IFramebuffer { namespace detail {
public: template<typename Impl>
virtual ~IFramebuffer() = default; concept HasClearImpl = requires(Impl& impl, bool value) {
{ impl.clear_impl(value) };
virtual int width() const = 0;
virtual int height() const = 0;
virtual void drawPixel(int x, int y, bool on) = 0;
virtual void clear(bool on) = 0;
// Optional hooks for async display pipelines; default to no-ops.
virtual void beginFrame() {}
virtual void endFrame() {}
virtual bool isFrameInFlight() const { return false; }
}; };
class IInput { template<typename Impl>
public: concept HasFrameReadyImpl = requires(Impl& impl) {
virtual ~IInput() = default; { impl.frameReady_impl() };
virtual InputState readState() = 0;
}; };
class IClock { template<typename Impl>
public: concept HasSendFrameImpl = requires(Impl& impl, bool flag) {
virtual ~IClock() = default; { impl.sendFrame_impl(flag) };
};
virtual std::uint32_t millis() = 0; template<typename Impl>
virtual void sleep_ms(std::uint32_t ms) = 0; concept HasFrameInFlightImpl = requires(const Impl& impl) {
{ impl.frameInFlight_impl() } -> std::convertible_to<bool>;
};
template<typename Impl>
concept HasSleepMsImpl = requires(Impl& impl, std::uint32_t value) {
{ impl.sleep_ms_impl(value) };
};
} // namespace detail
template<typename Impl>
class FramebufferFacade {
public:
[[nodiscard]] int width() const { return impl().width_impl(); }
[[nodiscard]] int height() const { return impl().height_impl(); }
__attribute__((always_inline)) void drawPixel(int x, int y, bool on) { impl().drawPixel_impl(x, y, on); }
void clear(bool on) {
if constexpr (detail::HasClearImpl<Impl>) {
impl().clear_impl(on);
} else {
defaultClear(on);
}
}
__attribute__((always_inline)) void frameReady() {
if constexpr (detail::HasFrameReadyImpl<Impl>)
impl().frameReady_impl();
}
__attribute__((always_inline)) void sendFrame(bool clearDrawBuffer = true) {
if constexpr (detail::HasSendFrameImpl<Impl>)
impl().sendFrame_impl(clearDrawBuffer);
}
__attribute__((always_inline)) [[nodiscard]] bool isFrameInFlight() const {
if constexpr (detail::HasFrameInFlightImpl<Impl>)
return impl().frameInFlight_impl();
return false;
}
protected:
FramebufferFacade() = default;
~FramebufferFacade() = default;
private:
__attribute__((always_inline)) [[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
__attribute__((always_inline)) [[nodiscard]] const Impl& impl() const { return static_cast<const Impl&>(*this); }
void defaultClear(bool on) {
for (int y = 0; y < height(); ++y)
for (int x = 0; x < width(); ++x)
drawPixel(x, y, on);
}
};
template<typename Impl>
class InputFacade {
public:
InputState readState() { return impl().readState_impl(); }
protected:
InputFacade() = default;
~InputFacade() = default;
private:
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
};
template<typename Impl>
class ClockFacade {
public:
std::uint32_t millis() { return impl().millis_impl(); }
void sleep_ms(std::uint32_t ms) {
if constexpr (detail::HasSleepMsImpl<Impl>)
impl().sleep_ms_impl(ms);
}
protected:
ClockFacade() = default;
~ClockFacade() = default;
private:
[[nodiscard]] Impl& impl() { return static_cast<Impl&>(*this); }
}; };
} // namespace cardboy::sdk } // namespace cardboy::sdk